1
0
mirror of https://github.com/DCC-EX/CommandStation-EX.git synced 2025-07-30 19:03:44 +02:00

Compare commits

..

48 Commits

Author SHA1 Message Date
Gregor Baues
e721303ed1 Cleanup diags 2021-06-09 08:58:27 +02:00
Gregor Baues
e742a8b120 Merge branch 'RefactorCallbacks' into MQTT 2021-06-09 08:53:07 +02:00
Gregor Baues
1b687808cb Refactored 2021-06-09 08:49:10 +02:00
Gregor Baues
21e19128f9 Added ifndef to the <s> command not showing T/S/O 2021-06-07 09:42:46 +02:00
Gregor Baues
c30f1c7cbe freememory() available more generally
fixed some mem diags
2021-06-04 13:46:21 +02:00
Gregor Baues
083c73ebc3 Wait for ENC based chips
Memory measurements (temp)
2021-06-04 11:09:22 +02:00
Gregor Baues
9fcc69d273 Diag output over MQTT added 2021-06-03 21:42:14 +02:00
Gregor Baues
e51279b202 loopPing added
check for returning diag messages
on the main channel
2021-06-02 20:41:48 +02:00
Gregor Baues
3c89d713fd added StringLogger 2021-06-02 20:21:30 +02:00
Gregor Baues
dc9368665d fixed includes 2021-05-25 10:50:02 +02:00
Gregor Baues
6b6ae4e904 disabled WIFI andEthernet enabled MQTT 2021-05-20 16:31:13 +02:00
Gregor Baues
a52e864a6f deleted unnecessary files 2021-05-20 16:27:01 +02:00
Gregor Baues
d72636b4ec Minor fix removing misleading message 2021-05-20 15:40:04 +02:00
Gregor Baues
a33b463c74 updated config example
check for MQTT / Ethernet setup
2021-05-19 17:21:27 +02:00
Gregor Baues
41ed2aeeeb Merge branch 'master' into MQTT 2021-05-19 15:49:14 +02:00
Gregor Baues
a58bc63764 Merge branch 'master' of https://github.com/DCC-EX/CommandStation-EX 2021-05-19 15:38:05 +02:00
Gregor Baues
b59def8e7b V1.0-alpha 2021-05-19 15:37:39 +02:00
Gregor Baues
be5ca00b7e Fixed constructor 2021-05-19 15:33:08 +02:00
Gregor Baues
b5520f13ba Broker handling / connection updated 2021-05-19 15:31:30 +02:00
Gregor Baues
428628f6f0 Minor updates/cleanup 2021-05-19 11:37:20 +02:00
Gregor Baues
c3abb0018d Working inital version 2021-05-19 09:49:18 +02:00
Gregor Baues
981453d399 State and define.h changed 2021-05-14 10:53:52 +02:00
Gregor Baues
8f2f052e2a MQ and Ethenet support are independent 2021-05-14 10:53:17 +02:00
Gregor Baues
598fb116a1 still with Gremlin 2021-05-12 22:09:07 +02:00
Gregor Baues
ce154abe94 still with Gremlin 2021-05-12 22:08:46 +02:00
Gregor Baues
6fd866d273 ok with gremlin after 3/4 recieves 2021-05-12 21:30:15 +02:00
Gregor Baues
35d81cd848 working with multiple clients and
the ringstream for processing
2021-05-12 09:23:48 +02:00
Gregor Baues
508b1fcfce subscriber topics ok 2021-05-07 09:08:09 +02:00
Gregor Baues
0b0744cc94 channel setup ok
channel subscrition not ok yet
(maye be bc its done during the
callback)
2021-05-06 13:06:16 +02:00
Gregor Baues
595b6bad93 start adding MQTT client channel implementation 2021-05-05 11:23:59 +02:00
Gregor Baues
c042240019 Added ObjectPool 2021-05-04 10:35:07 +02:00
Gregor Baues
866833a19e update #2 2021-05-03 09:18:53 +02:00
Gregor Baues
851228fba6 update #1 2021-05-03 09:05:05 +02:00
Gregor Baues
6bd9e28be4 MQTT firt send/recv ok 2021-05-01 12:38:27 +02:00
Gregor Baues
da85e4e245 update 2021-04-27 17:01:21 +02:00
Gregor Baues
a6a36b50e3 Broker definition reconfig 2021-04-27 17:01:03 +02:00
Gregor Baues
4efa260003 MQ Init 2021-04-27 15:03:15 +02:00
Gregor Baues
7442e3452e update 2021-04-27 14:20:49 +02:00
Gregor Baues
caaad92887 update 2021-04-27 11:44:22 +02:00
Gregor Baues
c740e25cc1 update 2021-04-27 11:44:09 +02:00
Gregor Baues
8acfdc6190 update 2021-04-27 10:58:39 +02:00
Gregor Baues
f48965a676 updated 2021-04-27 10:58:12 +02:00
Gregor Baues
afc01f1967 More inital setup for MQTT 2021-04-27 09:56:16 +02:00
Gregor Baues
8fc1470e4e Conditional compile for MQTT 2021-04-25 21:59:39 +02:00
Gregor Baues
33f2474c20 Merge branch 'master' of https://github.com/DCC-EX/CommandStation-EX 2021-04-23 12:18:55 +02:00
Gregor Baues
35c5e875d3 addes v1 of the patch sh script 2021-04-16 21:24:16 +02:00
Gregor Baues
6166484783 update driver for my shield 2021-04-16 15:27:49 +02:00
Gregor Baues
4ab21294ce enable eth & wifi at the same time 2021-04-14 10:23:32 +02:00
153 changed files with 5383 additions and 22351 deletions

2
.github/FUNDING.yml vendored
View File

@@ -1,2 +0,0 @@
github: DCC-EX
patreon: dccex

View File

@@ -1,14 +0,0 @@
name: Label sponsors
on:
pull_request:
types: [opened]
issues:
types: [opened]
jobs:
build:
name: is-sponsor-label
runs-on: ubuntu-latest
steps:
- uses: JasonEtco/is-sponsor-label-action@v1.2.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -24,11 +24,10 @@ jobs:
sha=$(git rev-parse --short "$GITHUB_SHA")
echo "#define GITHUB_SHA \"$sha\"" > GITHUB_SHA.h
- uses: EndBug/add-and-commit@v8 # You can change this to use a specific version
- uses: EndBug/add-and-commit@v4 # You can change this to use a specific version
with:
add: 'GITHUB_SHA.h'
message: 'Committing a SHA'
commit: --amend
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Leave this line unchanged

8
.gitignore vendored
View File

@@ -7,9 +7,5 @@ Release/*
.pio/
.vscode/
config.h
mySetup.cpp
myHal.cpp
myFilter.cpp
my*.h
!my*.example.h
compile_commands.json
.vscode/extensions.json
mySetup.h

7
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"platformio.platformio-ide"
]
}

12
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,12 @@
{
"files.associations": {
"array": "cpp",
"deque": "cpp",
"string": "cpp",
"unordered_map": "cpp",
"vector": "cpp",
"string_view": "cpp",
"initializer_list": "cpp",
"cstdint": "cpp"
}
}

View File

@@ -1,10 +1,6 @@
/*
* © 2022 Harald Barth
* © 2020-2021 Chris Harlow
* © 2020 Gregor Baues
* © 2022 Colin Murdoch
* All rights reserved.
*
* © 2020,Gregor Baues, Chris Harlow. All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
@@ -20,218 +16,24 @@
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#include <Arduino.h>
#include "CommandDistributor.h"
#include "SerialManager.h"
#include "WiThrottle.h"
#include "DIAG.h"
#include "defines.h"
#include "DCCWaveform.h"
#include "DCC.h"
#include "TrackManager.h"
#include "StringFormatter.h"
// variables to hold clock time
int16_t lastclocktime;
int8_t lastclockrate;
DCCEXParser *CommandDistributor::parser = 0;
#if WIFI_ON || ETHERNET_ON || defined(SERIAL1_COMMANDS) || defined(SERIAL2_COMMANDS) || defined(SERIAL3_COMMANDS)
// use a buffer to allow broadcast
StringBuffer * CommandDistributor::broadcastBufferWriter=new StringBuffer();
template<typename... Targs> void CommandDistributor::broadcastReply(clientType type, Targs... msg){
broadcastBufferWriter->flush();
StringFormatter::send(broadcastBufferWriter, msg...);
broadcastToClients(type);
}
#else
// on a single USB connection config, write direct to Serial and ignore flush/shove
template<typename... Targs> void CommandDistributor::broadcastReply(clientType type, Targs... msg){
(void)type; //shut up compiler warning
StringFormatter::send(&USB_SERIAL, msg...);
}
#endif
#ifdef CD_HANDLE_RING
// wifi or ethernet ring streams with multiple client types
RingStream * CommandDistributor::ring=0;
CommandDistributor::clientType CommandDistributor::clients[8]={
NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE,NONE_TYPE};
// Parse is called by Withrottle or Ethernet interface to determine which
// protocol the client is using and call the appropriate part of dcc++Ex
void CommandDistributor::parse(byte clientId,byte * buffer, RingStream * stream) {
if (Diag::WIFI && Diag::CMD)
DIAG(F("Parse C=%d T=%d B=%s"),clientId, clients[clientId], buffer);
ring=stream;
// First check if the client is not known
// yet and in that case determinine type
// NOTE: First character of transmission determines if this
// client is using the DCC++ protocol where all commands start
// with '<'
if (clients[clientId] == NONE_TYPE) {
if (buffer[0] == '<')
clients[clientId]=COMMAND_TYPE;
else
clients[clientId]=WITHROTTLE_TYPE;
}
// mark buffer that is sent to parser
ring->mark(clientId);
// When type is known, send the string
// to the right parser
if (clients[clientId] == COMMAND_TYPE) {
DCCEXParser::parse(stream, buffer, ring);
} else if (clients[clientId] == WITHROTTLE_TYPE) {
WiThrottle::getThrottle(clientId)->parse(ring, buffer);
}
if (ring->peekTargetMark()!=RingStream::NO_CLIENT) {
// The commit call will either write the length bytes
// OR rollback to the mark because the reply is empty
// or the command generated more output than fits in
// the buffer
if (!ring->commit()) {
DIAG(F("OUTBOUND FULL processing cmd:%s"),buffer);
}
} else {
DIAG(F("CD parse: was alredy committed")); //XXX Could have been committed by broadcastClient?!
}
}
void CommandDistributor::forget(byte clientId) {
if (clients[clientId]==WITHROTTLE_TYPE) WiThrottle::forget(clientId);
clients[clientId]=NONE_TYPE;
}
#endif
// This will not be called on a uno
void CommandDistributor::broadcastToClients(clientType type) {
byte rememberClient;
(void)rememberClient; // shut up compiler warning
// Broadcast to Serials
if (type==COMMAND_TYPE) SerialManager::broadcast(broadcastBufferWriter->getString());
#ifdef CD_HANDLE_RING
// If we are broadcasting from a wifi/eth process we need to complete its output
// before merging broadcasts in the ring, then reinstate it in case
// the process continues to output to its client.
if (ring) {
if ((rememberClient = ring->peekTargetMark()) != RingStream::NO_CLIENT) {
//DIAG(F("CD precommit client %d"), rememberClient);
ring->commit();
}
// loop through ring clients
for (byte clientId=0; clientId<sizeof(clients); clientId++) {
if (clients[clientId]==type) {
//DIAG(F("CD mark client %d"), clientId);
ring->mark(clientId);
ring->print(broadcastBufferWriter->getString());
//DIAG(F("CD commit client %d"), clientId);
ring->commit();
}
}
// at this point ring is committed (NO_CLIENT) either from
// 4 or 13 lines above.
if (rememberClient != RingStream::NO_CLIENT) {
//DIAG(F("CD postmark client %d"), rememberClient);
ring->mark(rememberClient);
}
}
#endif
}
// Public broadcast functions below
void CommandDistributor::broadcastSensor(int16_t id, bool on ) {
broadcastReply(COMMAND_TYPE, F("<%c %d>\n"), on?'Q':'q', id);
}
void CommandDistributor::broadcastTurnout(int16_t id, bool isClosed ) {
// For DCC++ classic compatibility, state reported to JMRI is 1 for thrown and 0 for closed;
// The string below contains serial and Withrottle protocols which should
// be safe for both types.
broadcastReply(COMMAND_TYPE, F("<H %d %d>\n"),id, !isClosed);
#ifdef CD_HANDLE_RING
broadcastReply(WITHROTTLE_TYPE, F("PTA%c%d\n"), isClosed?'2':'4', id);
#endif
}
void CommandDistributor::broadcastClockTime(int16_t time, int8_t rate) {
// The JMRI clock command is of the form : PFT65871<;>4
// The CS broadcast is of the form "<jC mmmm nn" where mmmm is time minutes and dd speed
// The string below contains serial and Withrottle protocols which should
// be safe for both types.
broadcastReply(COMMAND_TYPE, F("<jC %d %d>\n"),time, rate);
#ifdef CD_HANDLE_RING
broadcastReply(WITHROTTLE_TYPE, F("PFT%d<;>%d\n"), time*60, rate);
#endif
}
void CommandDistributor::setClockTime(int16_t clocktime, int8_t clockrate, byte opt) {
// opt - case 1 save the latest time if changed
// case 2 broadcast the time when requested
// case 3 display latest time
switch (opt)
void CommandDistributor::parse(byte clientId, byte *buffer, RingStream *streamer)
{
if (buffer[0] == '<')
{
case 1:
if (clocktime != lastclocktime){
// CAH. DIAG removed because LCD does it anyway.
LCD(6,F("Clk Time:%d Sp %d"), clocktime, clockrate);
// look for an event for this time
RMFT2::clockEvent(clocktime,1);
// Now tell everyone else what the time is.
CommandDistributor::broadcastClockTime(clocktime, clockrate);
lastclocktime = clocktime;
lastclockrate = clockrate;
}
return;
case 2:
CommandDistributor::broadcastClockTime(lastclocktime, lastclockrate);
return;
if (!parser)
{
parser = new DCCEXParser();
}
parser->parse(streamer, buffer, streamer);
}
else
{
WiThrottle::getThrottle(clientId)->parse(streamer, buffer);
}
}
int16_t CommandDistributor::retClockTime() {
return lastclocktime;
}
void CommandDistributor::broadcastLoco(byte slot) {
DCC::LOCO * sp=&DCC::speedTable[slot];
broadcastReply(COMMAND_TYPE, F("<l %d %d %d %l>\n"), sp->loco,slot,sp->speedCode,sp->functions);
#ifdef CD_HANDLE_RING
WiThrottle::markForBroadcast(sp->loco);
#endif
}
void CommandDistributor::broadcastPower() {
bool main=TrackManager::getMainPower()==POWERMODE::ON;
bool prog=TrackManager::getProgPower()==POWERMODE::ON;
bool join=TrackManager::isJoined();
const FSH * reason=F("");
char state='1';
if (main && prog && join) reason=F(" JOIN");
else if (main && prog);
else if (main) reason=F(" MAIN");
else if (prog) reason=F(" PROG");
else state='0';
broadcastReply(COMMAND_TYPE, F("<p%c%S>\n"),state,reason);
#ifdef CD_HANDLE_RING
broadcastReply(WITHROTTLE_TYPE, F("PPA%c\n"), main?'1':'0');
#endif
LCD(2,F("Power %S%S"),state=='1'?F("On"):F("Off"),reason);
}
void CommandDistributor::broadcastRaw(clientType type, char * msg) {
broadcastReply(type, F("%s"),msg);
}
void CommandDistributor::broadcastTrackState(const FSH* format,byte trackLetter,int16_t dcAddr) {
broadcastReply(COMMAND_TYPE, format,trackLetter,dcAddr);
}

View File

@@ -1,11 +1,6 @@
/*
* © 2022 Harald Barth
* © 2020-2021 Chris Harlow
* © 2020 Gregor Baues
* © 2022 Colin Murdoch
*
* All rights reserved.
*
* © 2020,Gregor Baues, Chris Harlow. All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
@@ -25,39 +20,13 @@
#define CommandDistributor_h
#include "DCCEXParser.h"
#include "RingStream.h"
#include "StringBuffer.h"
#include "defines.h"
#include "EXRAIL2.h"
#if WIFI_ON | ETHERNET_ON
// Command Distributor must handle a RingStream of clients
#define CD_HANDLE_RING
#endif
class CommandDistributor {
public:
enum clientType: byte {NONE_TYPE,COMMAND_TYPE,WITHROTTLE_TYPE};
private:
static void broadcastToClients(clientType type);
static StringBuffer * broadcastBufferWriter;
#ifdef CD_HANDLE_RING
static RingStream * ring;
static clientType clients[8];
#endif
public :
static void parse(byte clientId,byte* buffer, RingStream * ring);
static void broadcastLoco(byte slot);
static void broadcastSensor(int16_t id, bool value);
static void broadcastTurnout(int16_t id, bool isClosed);
static void broadcastClockTime(int16_t time, int8_t rate);
static void setClockTime(int16_t time, int8_t rate, byte opt);
static int16_t retClockTime();
static void broadcastPower();
static void broadcastRaw(clientType type,char * msg);
static void broadcastTrackState(const FSH* format,byte trackLetter,int16_t dcAddr);
template<typename... Targs> static void broadcastReply(clientType type, Targs... msg);
static void forget(byte clientId);
static void parse(byte clientId,byte* buffer, RingStream * streamer);
private:
static DCCEXParser * parser;
};
#endif

View File

@@ -16,23 +16,17 @@
// If config.h is not found, config.example.h will be used with all defaults.
////////////////////////////////////////////////////////////////////////////////////
#if __has_include ( "config.h")
#include "config.h"
#ifndef MOTOR_SHIELD_TYPE
#error Your config.h must include a MOTOR_SHIELD_TYPE definition. If you see this warning in spite not having a config.h, you have a buggy preprocessor and must copy config.example.h to config.h
#endif
#if __has_include("config.h")
#include "config.h"
#else
#warning config.h not found. Using defaults from config.example.h
#include "config.example.h"
#warning config.h not found. Using defaults from config.example.h
#include "config.example.h"
#endif
/*
* © 2021 Neil McKechnie
* © 2020-2021 Chris Harlow, Harald Barth, David Cutting,
* Fred Decker, Gregor Baues, Anthony W - Dayton
* All rights reserved.
*
* This file is part of CommandStation-EX
* © 2020,2021 Chris Harlow, Harald Barth, David Cutting,
* Fred Decker, Gregor Baues, Anthony W - Dayton All rights reserved.
*
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -49,21 +43,11 @@
*/
#include "DCCEX.h"
#include "Display_Implementation.h"
#ifdef CPU_TYPE_ERROR
#error CANNOT COMPILE - DCC++ EX ONLY WORKS WITH THE ARCHITECTURES LISTED IN defines.h
#endif
#ifdef WIFI_WARNING
#warning You have defined that you want WiFi but your hardware has not enough memory to do that, so WiFi DISABLED
#endif
#ifdef ETHERNET_WARNING
#warning You have defined that you want Ethernet but your hardware has not enough memory to do that, so Ethernet DISABLED
#endif
#ifdef EXRAIL_WARNING
#warning You have myAutomation.h but your hardware has not enough memory to do that, so EX-RAIL DISABLED
#endif
// Create a serial command parser for the USB connection,
// This supports JMRI or manual diagnostics and commands
// to be issued from the USB serial console.
DCCEXParser serialParser;
void setup()
{
@@ -71,63 +55,56 @@ void setup()
// Responsibility 1: Start the usb connection for diagnostics
// This is normally Serial but uses SerialUSB on a SAMD processor
SerialManager::init();
Serial.begin(115200);
DIAG(F("License GPLv3 fsf.org (c) dcc-ex.com"));
CONDITIONAL_LCD_START
{
// This block is still executed for DIAGS if LCD not in use
LCD(0, F("DCC++ EX v%S"), F(VERSION));
LCD(1, F("Starting"));
}
// Initialise HAL layer before reading EEprom or setting up MotorDrivers
IODevice::begin();
// Start the WiFi interface on a MEGA, Uno cannot currently handle WiFi
DISPLAY_START (
// This block is still executed for DIAGS if display not in use
LCD(0,F("DCC-EX v%S"),F(VERSION));
LCD(1,F("Lic GPLv3"));
);
// Responsibility 2: Start all the communications before the DCC engine
// Start the WiFi interface on a MEGA, Uno cannot currently handle WiFi
// Start Ethernet if it exists
#ifndef ARDUINO_ARCH_ESP32
#if WIFI_ON
WifiInterface::setup(WIFI_SERIAL_LINK_SPEED, F(WIFI_SSID), F(WIFI_PASSWORD), F(WIFI_HOSTNAME), IP_PORT, WIFI_CHANNEL);
#endif // WIFI_ON
#else
// ESP32 needs wifi on always
WifiESP::setup(WIFI_SSID, WIFI_PASSWORD, WIFI_HOSTNAME, IP_PORT, WIFI_CHANNEL);
#endif // ARDUINO_ARCH_ESP32
#if ETHERNET_ON
EthernetInterface::setup();
#endif // ETHERNET_ON
// As the setup of a motor shield may require a read of the current sense input from the ADC,
// let's make sure to initialise the ADCee class!
ADCee::begin();
#if MQTT_ON
MQTTInterface::setup();
#endif
// Responsibility 3: Start the DCC engine.
// Note: this provides DCC with two motor drivers, main and prog, which handle the motor shield(s)
// Standard supported devices have pre-configured macros but custome hardware installations require
// detailed pin mappings and may also require modified subclasses of the MotorDriver to implement specialist logic.
// STANDARD_MOTOR_SHIELD, POLOLU_MOTOR_SHIELD, FIREBOX_MK1, FIREBOX_MK1S are pre defined in MotorShields.h
TrackManager::Setup(MOTOR_SHIELD_TYPE);
// Start RMFT aka EX-RAIL (ignored if no automnation)
DCC::begin(MOTOR_SHIELD_TYPE);
#if defined(RMFT_ACTIVE)
RMFT::begin();
#endif
#if __has_include("mySetup.h")
#define SETUP(cmd) serialParser.parse(F(cmd))
#include "mySetup.h"
#undef SETUP
#endif
// Invoke any DCC++EX commands in the form "SETUP("xxxx");"" found in optional file mySetup.h.
// This can be used to create turnouts, outputs, sensors etc. through the normal text commands.
#if __has_include ( "mySetup.h")
#define SETUP(cmd) DCCEXParser::parse(F(cmd))
#include "mySetup.h"
#undef SETUP
#endif
#if defined(LCN_SERIAL)
#if defined(LCN_SERIAL)
LCN_SERIAL.begin(115200);
LCN::init(LCN_SERIAL);
#endif
LCD(3, F("Ready"));
CommandDistributor::broadcastPower();
#endif
LCD(1, F("Ready"));
}
void loop()
@@ -139,42 +116,38 @@ void loop()
DCC::loop();
// Responsibility 2: handle any incoming commands on USB connection
SerialManager::loop();
serialParser.loop(Serial);
// Responsibility 3: Optionally handle any incoming WiFi traffic
#ifndef ARDUINO_ARCH_ESP32
// Responsibility 3: Optionally handle any incoming WiFi traffic
#if WIFI_ON
WifiInterface::loop();
#endif //WIFI_ON
#else //ARDUINO_ARCH_ESP32
#ifndef WIFI_TASK_ON_CORE0
WifiESP::loop();
#endif
#endif //ARDUINO_ARCH_ESP32
#if ETHERNET_ON
EthernetInterface::loop();
#endif
RMFT::loop(); // ignored if no automation
#if MQTT_ON
MQTTInterface::loop();
#endif
#if defined(LCN_SERIAL)
#if defined(RMFT_ACTIVE)
RMFT::loop();
#endif
#if defined(LCN_SERIAL)
LCN::loop();
#endif
#endif
// Display refresh
DisplayInterface::loop();
// Handle/update IO devices.
IODevice::loop();
Sensor::checkAll(); // Update and print changes
LCDDisplay::loop(); // ignored if LCD not in use
// Report any decrease in memory (will automatically trigger on first call)
static int ramLowWatermark = __INT_MAX__; // replaced on first loop
int freeNow = DCCTimer::getMinimumFreeMemory();
if (freeNow < ramLowWatermark) {
int freeNow = minimumFreeMemory();
if (freeNow < ramLowWatermark)
{
ramLowWatermark = freeNow;
LCD(3,F("Free RAM=%5db"), ramLowWatermark);
LCD(2, F("Free RAM=%5db"), ramLowWatermark);
}
}

722
DCC.cpp

File diff suppressed because it is too large Load Diff

138
DCC.h
View File

@@ -1,10 +1,5 @@
/*
* © 2021 Mike S
* © 2021 Fred Decker
* © 2021 Herb Morton
* © 2020-2021 Harald Barth
* © 2020-2021 Chris Harlow
* All rights reserved.
* © 2020, Chris Harlow. All rights reserved.
*
* This file is part of Asbelos DCC API
*
@@ -28,48 +23,76 @@
#include "MotorDrivers.h"
#include "FSH.h"
#include "defines.h"
#ifndef HIGHEST_SHORT_ADDR
#define HIGHEST_SHORT_ADDR 127
#else
#if HIGHEST_SHORT_ADDR > 127
#error short addr greater than 127 does not make sense
#endif
#endif
#include "DCCACK.h"
const uint16_t LONG_ADDR_MARKER = 0x4000;
typedef void (*ACK_CALLBACK)(int16_t result);
enum ackOp : byte
{ // Program opcodes for the ack Manager
BASELINE, // ensure enough resets sent before starting and obtain baseline current
W0,
W1, // issue write bit (0..1) packet
WB, // issue write byte packet
VB, // Issue validate Byte packet
V0, // Issue validate bit=0 packet
V1, // issue validate bit=1 packlet
WACK, // wait for ack (or absence of ack)
ITC1, // If True Callback(1) (if prevous WACK got an ACK)
ITC0, // If True callback(0);
ITCB, // If True callback(byte)
ITCB7, // If True callback(byte &0x7F)
NAKFAIL, // if false callback(-1)
FAIL, // callback(-1)
STARTMERGE, // Clear bit and byte settings ready for merge pass
MERGE, // Merge previous wack response with byte value and decrement bit number (use for readimng CV bytes)
SETBIT, // sets bit number to next prog byte
SETCV, // sets cv number to next prog byte
SETBYTE, // sets current byte to next prog byte
SETBYTEH, // sets current byte to word high byte
SETBYTEL, // sets current byte to word low byte
STASHLOCOID, // keeps current byte value for later
COMBINELOCOID, // combines current value with stashed value and returns it
ITSKIP, // skip to SKIPTARGET if ack true
SKIPTARGET = 0xFF // jump to target
};
enum CALLBACK_STATE : byte {
AFTER_WRITE, // Start callback sequence after something was written to the decoder
WAITING_100, // Waiting for 100mS of stable power
WAITING_30, // waiting to 30ms of power off gap.
READY, // Ready to complete callback
};
// Allocations with memory implications..!
// Base system takes approx 900 bytes + 8 per loco. Turnouts, Sensors etc are dynamically created
#if defined(HAS_ENOUGH_MEMORY)
const byte MAX_LOCOS = 50;
#ifdef ARDUINO_AVR_UNO
const byte MAX_LOCOS = 20;
#else
const byte MAX_LOCOS = 30;
const byte MAX_LOCOS = 50;
#endif
class DCC
{
public:
static void begin(const FSH * motorShieldName);
static void begin(const FSH * motorShieldName, MotorDriver *mainDriver, MotorDriver *progDriver);
static void setJoinRelayPin(byte joinRelayPin);
static void loop();
// Public DCC API functions
static void setThrottle(uint16_t cab, uint8_t tSpeed, bool tDirection);
static int8_t getThrottleSpeed(int cab);
static uint8_t getThrottleSpeedByte(int cab);
static uint8_t getThrottleSpeed(int cab);
static bool getThrottleDirection(int cab);
static void writeCVByteMain(int cab, int cv, byte bValue);
static void writeCVBitMain(int cab, int cv, byte bNum, bool bValue);
static void setFunction(int cab, byte fByte, byte eByte);
static bool setFn(int cab, int16_t functionNumber, bool on);
static void changeFn(int cab, int16_t functionNumber);
static void setFn(int cab, int16_t functionNumber, bool on);
static int changeFn(int cab, int16_t functionNumber, bool pressed);
static int getFn(int cab, int16_t functionNumber);
static uint32_t getFunctionMap(int cab);
static void updateGroupflags(byte &flags, int16_t functionNumber);
static void setAccessory(int address, byte port, bool gate, byte onoff = 2);
static void setAccessory(int aAdd, byte aNum, bool activate);
static bool writeTextPacket(byte *b, int nBytes);
static void setProgTrackSyncMain(bool on); // when true, prog track becomes driveable
static void setProgTrackBoost(bool on); // when true, special prog track current limit does not apply
// ACKable progtrack calls bitresults callback 0,0 or -1, cv returns value or -1
static void readCV(int16_t cv, ACK_CALLBACK callback);
static void readCVBit(int16_t cv, byte bitNum, ACK_CALLBACK callback); // -1 for error
@@ -85,11 +108,13 @@ public:
static void forgetLoco(int cab); // removes any speed reminders for this loco
static void forgetAllLocos(); // removes all speed reminders
static void displayCabList(Print *stream);
static FSH *getMotorShieldName();
static inline void setGlobalSpeedsteps(byte s) {
globalSpeedsteps = s;
};
private:
struct LOCO
{
int loco;
@@ -97,25 +122,39 @@ public:
byte groupFlags;
unsigned long functions;
};
static LOCO speedTable[MAX_LOCOS];
static int lookupSpeedTable(int locoId, bool autoCreate=true);
static byte cv1(byte opcode, int cv);
static byte cv2(int cv);
private:
static byte joinRelay;
static byte loopStatus;
static void setThrottle2(uint16_t cab, uint8_t speedCode);
static void updateLocoReminder(int loco, byte speedCode);
static void setFunctionInternal(int cab, byte fByte, byte eByte);
static bool issueReminder(int reg);
static int lastLocoReminder;
static int highestUsedReg;
static int nextLoco;
static FSH *shieldName;
static byte globalSpeedsteps;
static LOCO speedTable[MAX_LOCOS];
static byte cv1(byte opcode, int cv);
static byte cv2(int cv);
static int lookupSpeedTable(int locoId);
static void issueReminders();
static void callback(int value);
// ACK MANAGER
static ackOp const *ackManagerProg;
static byte ackManagerByte;
static byte ackManagerBitNum;
static int ackManagerCv;
static int ackManagerWord;
static byte ackManagerStash;
static bool ackReceived;
static bool ackManagerRejoin;
static ACK_CALLBACK ackManagerCallback;
static CALLBACK_STATE callbackState;
static void ackManagerSetup(int cv, byte bitNumOrbyteValue, ackOp const program[], ACK_CALLBACK callback);
static void ackManagerSetup(int wordval, ackOp const program[], ACK_CALLBACK callback);
static void ackManagerLoop();
static bool checkResets( uint8_t numResets);
static const int PROG_REPEATS = 8; // repeats of programming commands (some decoders need at least 8 to be reliable)
// NMRA codes #
static const byte SET_SPEED = 0x3f;
@@ -130,4 +169,31 @@ private:
static const byte BIT_OFF = 0x00;
};
#ifdef ARDUINO_AVR_MEGA // is using Mega 1280, define as Mega 2560 (pinouts and functionality are identical)
#define ARDUINO_AVR_MEGA2560
#endif
#if defined(ARDUINO_AVR_UNO)
#define ARDUINO_TYPE "UNO"
#elif defined(ARDUINO_AVR_NANO)
#define ARDUINO_TYPE "NANO"
#elif defined(ARDUINO_AVR_MEGA2560)
#define ARDUINO_TYPE "MEGA"
#elif defined(ARDUINO_ARCH_MEGAAVR)
#define ARDUINO_TYPE "MEGAAVR"
#elif defined(ARDUINO_TEENSY32)
#define ARDUINO_TYPE "TEENSY32"
#elif defined(ARDUINO_TEENSY35)
#define ARDUINO_TYPE "TEENSY35"
#elif defined(ARDUINO_TEENSY36)
#define ARDUINO_TYPE "TEENSY36"
#elif defined(ARDUINO_TEENSY40)
#define ARDUINO_TYPE "TEENSY40"
#elif defined(ARDUINO_TEENSY41)
#define ARDUINO_TYPE "TEENSY41"
#else
#error CANNOT COMPILE - DCC++ EX ONLY WORKS WITH AN ARDUINO UNO, NANO 328, OR ARDUINO MEGA 1280/2560
#endif
#endif

View File

@@ -1,469 +0,0 @@
/*
* © 2021 M Steve Todd
* © 2021 Mike S
* © 2021 Fred Decker
* © 2020-2021 Harald Barth
* © 2020-2022 Chris Harlow
* All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#include "DCCACK.h"
#include "DIAG.h"
#include "DCC.h"
#include "DCCWaveform.h"
#include "TrackManager.h"
unsigned int DCCACK::minAckPulseDuration = 2000; // micros
unsigned int DCCACK::maxAckPulseDuration = 20000; // micros
MotorDriver * DCCACK::progDriver=NULL;
ackOp const * DCCACK::ackManagerProg;
ackOp const * DCCACK::ackManagerProgStart;
byte DCCACK::ackManagerByte;
byte DCCACK::ackManagerByteVerify;
byte DCCACK::ackManagerStash;
int DCCACK::ackManagerWord;
byte DCCACK::ackManagerRetry;
byte DCCACK::ackRetry = 2;
int16_t DCCACK::ackRetrySum;
int16_t DCCACK::ackRetryPSum;
int DCCACK::ackManagerCv;
byte DCCACK::ackManagerBitNum;
bool DCCACK::ackReceived;
bool DCCACK::ackManagerRejoin;
volatile uint8_t DCCACK::numAckGaps=0;
volatile uint8_t DCCACK::numAckSamples=0;
uint8_t DCCACK::trailingEdgeCounter=0;
unsigned int DCCACK::ackPulseDuration; // micros
unsigned long DCCACK::ackPulseStart; // micros
volatile bool DCCACK::ackDetected;
unsigned long DCCACK::ackCheckStart; // millis
volatile bool DCCACK::ackPending;
bool DCCACK::autoPowerOff;
int DCCACK::ackThreshold;
int DCCACK::ackLimitmA = 50;
int DCCACK::ackMaxCurrent;
unsigned int DCCACK::ackCheckDuration; // millis
CALLBACK_STATE DCCACK::callbackState=READY;
ACK_CALLBACK DCCACK::ackManagerCallback;
void DCCACK::Setup(int cv, byte byteValueOrBitnum, ackOp const program[], ACK_CALLBACK callback) {
ackManagerRejoin=TrackManager::isJoined();
if (ackManagerRejoin) {
// Change from JOIN must zero resets packet.
TrackManager::setJoin(false);
DCCWaveform::progTrack.clearResets();
}
progDriver=TrackManager::getProgDriver();
if (progDriver==NULL) {
TrackManager::setJoin(ackManagerRejoin);
callback(-3); // we dont have a prog track!
return;
}
if (!progDriver->canMeasureCurrent()) {
TrackManager::setJoin(ackManagerRejoin);
callback(-2); // our prog track cant measure current
return;
}
autoPowerOff=false;
if (progDriver->getPower() == POWERMODE::OFF) {
autoPowerOff=true; // power off afterwards
if (Diag::ACK) DIAG(F("Auto Prog power on"));
progDriver->setPower(POWERMODE::ON);
/* TODO !!! in MotorDriver surely!
if (MotorDriver::commonFaultPin)
DCCWaveform::mainTrack.setPowerMode(POWERMODE::ON);
DCCWaveform::progTrack.clearResets();
**/
}
ackManagerCv = cv;
ackManagerProg = program;
ackManagerProgStart = program;
ackManagerRetry = ackRetry;
ackManagerByte = byteValueOrBitnum;
ackManagerByteVerify = byteValueOrBitnum;
ackManagerBitNum=byteValueOrBitnum;
ackManagerCallback = callback;
}
void DCCACK::Setup(int wordval, ackOp const program[], ACK_CALLBACK callback) {
ackManagerWord=wordval;
Setup(0, 0, program, callback);
}
const byte RESET_MIN=8; // tuning of reset counter before sending message
// checkRessets return true if the caller should yield back to loop and try later.
bool DCCACK::checkResets(uint8_t numResets) {
return DCCWaveform::progTrack.getResets() < numResets;
}
// Operations applicable to PROG track ONLY.
// (yes I know I could have subclassed the main track but...)
void DCCACK::setAckBaseline() {
int baseline=progDriver->getCurrentRaw();
ackThreshold= baseline + progDriver->mA2raw(ackLimitmA);
if (Diag::ACK) DIAG(F("ACK baseline=%d/%dmA Threshold=%d/%dmA Duration between %uus and %uus"),
baseline,progDriver->raw2mA(baseline),
ackThreshold,progDriver->raw2mA(ackThreshold),
minAckPulseDuration, maxAckPulseDuration);
}
void DCCACK::setAckPending() {
ackMaxCurrent=0;
ackPulseStart=0;
ackPulseDuration=0;
ackDetected=false;
ackCheckStart=millis();
numAckSamples=0;
numAckGaps=0;
ackPending=true; // interrupt routines will now take note
}
byte DCCACK::getAck() {
if (ackPending) return (2); // still waiting
if (Diag::ACK) DIAG(F("%S after %dmS max=%d/%dmA pulse=%uuS samples=%d gaps=%d"),ackDetected?F("ACK"):F("NO-ACK"), ackCheckDuration,
ackMaxCurrent,progDriver->raw2mA(ackMaxCurrent), ackPulseDuration, numAckSamples, numAckGaps);
if (ackDetected) return (1); // Yes we had an ack
return(0); // pending set off but not detected means no ACK.
}
void DCCACK::loop() {
while (ackManagerProg) {
byte opcode=GETFLASH(ackManagerProg);
// breaks from this switch will step to next prog entry
// returns from this switch will stay on same entry
// (typically waiting for a reset counter or ACK waiting, or when all finished.)
switch (opcode) {
case BASELINE:
if (progDriver->getPower()==POWERMODE::OVERLOAD) return;
if (checkResets(autoPowerOff || ackManagerRejoin ? 20 : 3)) return;
setAckBaseline();
callbackState=AFTER_READ;
break;
case W0: // write 0 bit
case W1: // write 1 bit
{
if (checkResets(RESET_MIN)) return;
if (Diag::ACK) DIAG(F("W%d cv=%d bit=%d"),opcode==W1, ackManagerCv,ackManagerBitNum);
byte instruction = WRITE_BIT | (opcode==W1 ? BIT_ON : BIT_OFF) | ackManagerBitNum;
byte message[] = {DCC::cv1(BIT_MANIPULATE, ackManagerCv), DCC::cv2(ackManagerCv), instruction };
DCCWaveform::progTrack.schedulePacket(message, sizeof(message), PROG_REPEATS);
setAckPending();
callbackState=AFTER_WRITE;
}
break;
case WB: // write byte
{
if (checkResets( RESET_MIN)) return;
if (Diag::ACK) DIAG(F("WB cv=%d value=%d"),ackManagerCv,ackManagerByte);
byte message[] = {DCC::cv1(WRITE_BYTE, ackManagerCv), DCC::cv2(ackManagerCv), ackManagerByte};
DCCWaveform::progTrack.schedulePacket(message, sizeof(message), PROG_REPEATS);
setAckPending();
callbackState=AFTER_WRITE;
}
break;
case VB: // Issue validate Byte packet
{
if (checkResets( RESET_MIN)) return;
if (Diag::ACK) DIAG(F("VB cv=%d value=%d"),ackManagerCv,ackManagerByte);
byte message[] = { DCC::cv1(VERIFY_BYTE, ackManagerCv), DCC::cv2(ackManagerCv), ackManagerByte};
DCCWaveform::progTrack.schedulePacket(message, sizeof(message), PROG_REPEATS);
setAckPending();
}
break;
case V0:
case V1: // Issue validate bit=0 or bit=1 packet
{
if (checkResets(RESET_MIN)) return;
if (Diag::ACK) DIAG(F("V%d cv=%d bit=%d"),opcode==V1, ackManagerCv,ackManagerBitNum);
byte instruction = VERIFY_BIT | (opcode==V0?BIT_OFF:BIT_ON) | ackManagerBitNum;
byte message[] = {DCC::cv1(BIT_MANIPULATE, ackManagerCv), DCC::cv2(ackManagerCv), instruction };
DCCWaveform::progTrack.schedulePacket(message, sizeof(message), PROG_REPEATS);
setAckPending();
}
break;
case WACK: // wait for ack (or absence of ack)
{
byte ackState=2; // keep polling
ackState=getAck();
if (ackState==2) return; // keep polling
ackReceived=ackState==1;
break; // we have a genuine ACK result
}
case ITC0:
case ITC1: // If True Callback(0 or 1) (if prevous WACK got an ACK)
if (ackReceived) {
callback(opcode==ITC0?0:1);
return;
}
break;
case ITCB: // If True callback(byte)
if (ackReceived) {
callback(ackManagerByte);
return;
}
break;
case ITCBV: // If True callback(byte) - Verify
if (ackReceived) {
if (ackManagerByte == ackManagerByteVerify) {
ackRetrySum ++;
LCD(1, F("v %d %d Sum=%d"), ackManagerCv, ackManagerByte, ackRetrySum);
}
callback(ackManagerByte);
return;
}
break;
case ITCB7: // If True callback(byte & 0x7F)
if (ackReceived) {
callback(ackManagerByte & 0x7F);
return;
}
break;
case NAKFAIL: // If nack callback(-1)
if (!ackReceived) {
callback(-1);
return;
}
break;
case CALLFAIL: // callback(-1)
callback(-1);
return;
case BIV: // ackManagerByte initial value
ackManagerByte = ackManagerByteVerify;
break;
case STARTMERGE:
ackManagerBitNum=7;
ackManagerByte=0;
break;
case MERGE: // Merge previous Validate zero wack response with byte value and update bit number (use for reading CV bytes)
ackManagerByte <<= 1;
// ackReceived means bit is zero.
if (!ackReceived) ackManagerByte |= 1;
ackManagerBitNum--;
break;
case SETBIT:
ackManagerProg++;
ackManagerBitNum=GETFLASH(ackManagerProg);
break;
case SETCV:
ackManagerProg++;
ackManagerCv=GETFLASH(ackManagerProg);
break;
case SETBYTE:
ackManagerProg++;
ackManagerByte=GETFLASH(ackManagerProg);
break;
case SETBYTEH:
ackManagerByte=highByte(ackManagerWord);
break;
case SETBYTEL:
ackManagerByte=lowByte(ackManagerWord);
break;
case STASHLOCOID:
ackManagerStash=ackManagerByte; // stash value from CV17
break;
case COMBINELOCOID:
// ackManagerStash is cv17, ackManagerByte is CV 18
callback( LONG_ADDR_MARKER | ( ackManagerByte + ((ackManagerStash - 192) << 8)));
return;
case ITSKIP:
if (!ackReceived) break;
// SKIP opcodes until SKIPTARGET found
while (opcode!=SKIPTARGET) {
ackManagerProg++;
opcode=GETFLASH(ackManagerProg);
}
break;
case SKIPTARGET:
break;
default:
DIAG(F("!! ackOp %d FAULT!!"),opcode);
callback( -1);
return;
} // end of switch
ackManagerProg++;
}
}
void DCCACK::callback(int value) {
// check for automatic retry
if (value == -1 && ackManagerRetry > 0) {
ackRetrySum ++;
LCD(0, F("Retry %d %d Sum=%d"), ackManagerCv, ackManagerRetry, ackRetrySum);
ackManagerRetry --;
ackManagerProg = ackManagerProgStart;
return;
}
static unsigned long callbackStart;
// We are about to leave programming mode
// Rule 1: If we have written to a decoder we must maintain power for 100mS
// Rule 2: If we are re-joining the main track we must power off for 30mS
switch (callbackState) {
case AFTER_READ:
if (ackManagerRejoin && autoPowerOff) {
progDriver->setPower(POWERMODE::OFF);
callbackStart=millis();
callbackState=WAITING_30;
if (Diag::ACK) DIAG(F("OFF 30mS"));
} else {
callbackState=READY;
}
break;
case AFTER_WRITE: // first attempt to callback after a write operation
if (!ackManagerRejoin && !autoPowerOff) {
callbackState=READY;
break;
} // lines 906-910 added. avoid wait after write. use 1 PROG
callbackStart=millis();
callbackState=WAITING_100;
if (Diag::ACK) DIAG(F("Stable 100mS"));
break;
case WAITING_100: // waiting for 100mS
if (millis()-callbackStart < 100) break;
// stable after power maintained for 100mS
// If we are going to power off anyway, it doesnt matter
// but if we will keep the power on, we must off it for 30mS
if (autoPowerOff) callbackState=READY;
else { // Need to cycle power off and on
progDriver->setPower(POWERMODE::OFF);
callbackStart=millis();
callbackState=WAITING_30;
if (Diag::ACK) DIAG(F("OFF 30mS"));
}
break;
case WAITING_30: // waiting for 30mS with power off
if (millis()-callbackStart < 30) break;
//power has been off for 30mS
progDriver->setPower(POWERMODE::ON);
callbackState=READY;
break;
case READY: // ready after read, or write after power delay and off period.
// power off if we powered it on
if (autoPowerOff) {
if (Diag::ACK) DIAG(F("Auto Prog power off"));
progDriver->setPower(POWERMODE::OFF);
/* TODO
if (MotorDriver::commonFaultPin)
DCCWaveform::mainTrack.setPowerMode(POWERMODE::OFF);
**/
}
// Restore <1 JOIN> to state before BASELINE
if (ackManagerRejoin) {
TrackManager::setJoin(true);
if (Diag::ACK) DIAG(F("Auto JOIN"));
}
ackManagerProg=NULL; // no more steps to execute
if (Diag::ACK) DIAG(F("Callback(%d)"),value);
(ackManagerCallback)( value);
}
}
void DCCACK::checkAck(byte sentResetsSincePacket) {
if (!ackPending) return;
// This function operates in interrupt() time so must be fast and can't DIAG
if (sentResetsSincePacket > 6) { //ACK timeout
ackCheckDuration=millis()-ackCheckStart;
ackPending = false;
return;
}
int current=progDriver->getCurrentRaw(true); // true means "from interrupt"
numAckSamples++;
if (current > ackMaxCurrent) ackMaxCurrent=current;
// An ACK is a pulse lasting between minAckPulseDuration and maxAckPulseDuration uSecs (refer @haba)
if (current>ackThreshold) {
if (trailingEdgeCounter > 0) {
numAckGaps++;
trailingEdgeCounter = 0;
}
if (ackPulseStart==0) ackPulseStart=micros(); // leading edge of pulse detected
return;
}
// not in pulse
if (ackPulseStart==0) return; // keep waiting for leading edge
// if we reach to this point, we have
// detected trailing edge of pulse
if (trailingEdgeCounter == 0) {
ackPulseDuration=micros()-ackPulseStart;
}
// but we do not trust it yet and return (which will force another
// measurement) and first the third time around with low current
// the ack detection will be finalized.
if (trailingEdgeCounter < 2) {
trailingEdgeCounter++;
return;
}
trailingEdgeCounter = 0;
if (ackPulseDuration>=minAckPulseDuration && ackPulseDuration<=maxAckPulseDuration) {
ackCheckDuration=millis()-ackCheckStart;
ackDetected=true;
ackPending=false;
DCCWaveform::progTrack.clearRepeats(); // shortcut remaining repeat packets
return; // we have a genuine ACK result
}
ackPulseStart=0; // We have detected a too-short or too-long pulse so ignore and wait for next leading edge
}

156
DCCACK.h
View File

@@ -1,156 +0,0 @@
/*
* © 2021 M Steve Todd
* © 2021 Mike S
* © 2021 Fred Decker
* © 2020-2021 Harald Barth
* © 2020-2022 Chris Harlow
* All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef DCCACK_h
#define DCCACK_h
#include "MotorDriver.h"
typedef void (*ACK_CALLBACK)(int16_t result);
enum ackOp : byte
{ // Program opcodes for the ack Manager
BASELINE, // ensure enough resets sent before starting and obtain baseline current
W0,
W1, // issue write bit (0..1) packet
WB, // issue write byte packet
VB, // Issue validate Byte packet
V0, // Issue validate bit=0 packet
V1, // issue validate bit=1 packlet
WACK, // wait for ack (or absence of ack)
ITC1, // If True Callback(1) (if prevous WACK got an ACK)
ITC0, // If True callback(0);
ITCB, // If True callback(byte)
ITCBV, // If True callback(byte) - end of Verify Byte
ITCB7, // If True callback(byte &0x7F)
NAKFAIL, // if false callback(-1)
CALLFAIL, // callback(-1)
BIV, // Set ackManagerByte to initial value for Verify retry
STARTMERGE, // Clear bit and byte settings ready for merge pass
MERGE, // Merge previous wack response with byte value and decrement bit number (use for readimng CV bytes)
SETBIT, // sets bit number to next prog byte
SETCV, // sets cv number to next prog byte
SETBYTE, // sets current byte to next prog byte
SETBYTEH, // sets current byte to word high byte
SETBYTEL, // sets current byte to word low byte
STASHLOCOID, // keeps current byte value for later
COMBINELOCOID, // combines current value with stashed value and returns it
ITSKIP, // skip to SKIPTARGET if ack true
SKIPTARGET = 0xFF // jump to target
};
enum CALLBACK_STATE : byte {
AFTER_READ, // Start callback sequence after something was read from the decoder
AFTER_WRITE, // Start callback sequence after something was written to the decoder
WAITING_100, // Waiting for 100mS of stable power
WAITING_30, // waiting to 30ms of power off gap.
READY, // Ready to complete callback
};
class DCCACK {
public:
static byte getAck(); //prog track only 0=NACK, 1=ACK 2=keep waiting
static void checkAck(byte sentResetsSincePacket); // Interrupt time ack checker
static inline void setAckLimit(int mA) {
ackLimitmA = mA;
}
static inline void setMinAckPulseDuration(unsigned int i) {
minAckPulseDuration = i;
}
static inline void setMaxAckPulseDuration(unsigned int i) {
maxAckPulseDuration = i;
}
static void Setup(int cv, byte byteValueOrBitnum, ackOp const program[], ACK_CALLBACK callback);
static void Setup(int wordval, ackOp const program[], ACK_CALLBACK callback);
static void loop();
static bool isActive() { return ackManagerProg!=NULL;}
static inline int16_t setAckRetry(byte retry) {
ackRetry = retry;
ackRetryPSum = ackRetrySum;
ackRetrySum = 0; // reset running total
return ackRetryPSum;
};
private:
static const byte SET_SPEED = 0x3f;
static const byte WRITE_BYTE = 0x7C;
static const byte VERIFY_BYTE = 0x74;
static const byte BIT_MANIPULATE = 0x78;
static const byte WRITE_BIT = 0xF0;
static const byte VERIFY_BIT = 0xE0;
static const byte BIT_ON = 0x08;
static const byte BIT_OFF = 0x00;
static void setAckBaseline();
static void setAckPending();
static void callback(int value);
static const int PROG_REPEATS = 8; // repeats of programming commands (some decoders need at least 8 to be reliable)
// ACK management (Prog track only)
static void checkAck();
static bool checkResets(uint8_t numResets);
static volatile bool ackPending;
static volatile bool ackDetected;
static int ackThreshold;
static int ackLimitmA;
static int ackMaxCurrent;
static unsigned long ackCheckStart; // millis
static unsigned int ackCheckDuration; // millis
static unsigned int ackPulseDuration; // micros
static unsigned long ackPulseStart; // micros
static unsigned int minAckPulseDuration ; // micros
static unsigned int maxAckPulseDuration ; // micros
static MotorDriver* progDriver;
static volatile uint8_t numAckGaps;
static volatile uint8_t numAckSamples;
static uint8_t trailingEdgeCounter;
static ackOp const * ackManagerProg;
static ackOp const * ackManagerProgStart;
static byte ackManagerByte;
static byte ackManagerByteVerify;
static byte ackManagerStash;
static int ackManagerWord;
static byte ackManagerRetry;
static byte ackRetry;
static int16_t ackRetrySum;
static int16_t ackRetryPSum;
static int ackManagerCv;
static byte ackManagerBitNum;
static bool ackReceived;
static bool ackManagerRejoin;
static bool autoPowerOff;
static CALLBACK_STATE callbackState;
static ACK_CALLBACK ackManagerCallback;
};
#endif

49
DCCEX.h
View File

@@ -1,25 +1,3 @@
/*
* © 2021 Fred Decker
* © 2020-2021 Harald Barth
* © 2020-2021 Chris Harlow
* All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
// This include is intended to visually simplify the .ino for the end users.
// If there were any #ifdefs required they are much better handled in here.
@@ -30,25 +8,24 @@
#include "DCC.h"
#include "DIAG.h"
#include "DCCEXParser.h"
#include "SerialManager.h"
#include "version.h"
#ifndef ARDUINO_ARCH_ESP32
#include "WifiInterface.h"
#else
#include "WifiESP32.h"
#endif
#if ETHERNET_ON == true
#include "EthernetInterface.h"
#endif
#include "Display_Implementation.h"
#if MQTT_ON == true
#include "MQTTInterface.h"
#endif
#include "LCD_Implementation.h"
#include "LCN.h"
#include "IODevice.h"
#include "Turnouts.h"
#include "Sensors.h"
#include "Outputs.h"
#include "CommandDistributor.h"
#include "TrackManager.h"
#include "DCCTimer.h"
#include "EXRAIL.h"
#include "freeMemory.h"
#if __has_include ( "myAutomation.h")
#include "RMFT.h"
#define RMFT_ACTIVE
#endif
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,5 @@
/*
* © 2021 Mike S
* © 2021 Fred Decker
* © 2020-2021 Chris Harlow
* All rights reserved.
* © 2020, Chris Harlow. All rights reserved.
*
* This file is part of Asbelos DCC API
*
@@ -26,14 +23,15 @@
#include "RingStream.h"
typedef void (*FILTER_CALLBACK)(Print * stream, byte & opcode, byte & paramCount, int16_t p[]);
typedef void (*AT_COMMAND_CALLBACK)(HardwareSerial * stream,const byte * command);
typedef void (*AT_COMMAND_CALLBACK)(const byte * command);
struct DCCEXParser
{
static void parse(Print * stream, byte * command, RingStream * ringStream);
static void parse(const FSH * cmd);
static void parseOne(Print * stream, byte * command, RingStream * ringStream);
DCCEXParser();
void loop(Stream & stream);
void parse(Print * stream, byte * command, RingStream * ringStream);
void parse(const FSH * cmd);
void flush();
static void setFilter(FILTER_CALLBACK filter);
static void setRMFTFilter(FILTER_CALLBACK filter);
static void setAtCommandCallback(AT_COMMAND_CALLBACK filter);
@@ -42,13 +40,17 @@ struct DCCEXParser
private:
static const int16_t MAX_BUFFER=50; // longest command sent in
static int16_t splitValues( int16_t result[MAX_COMMAND_PARAMS], const byte * command, bool usehex);
byte bufferLength=0;
bool inCommandPayload=false;
byte buffer[MAX_BUFFER+2];
int16_t splitValues( int16_t result[MAX_COMMAND_PARAMS], const byte * command);
int16_t splitHexValues( int16_t result[MAX_COMMAND_PARAMS], const byte * command);
static bool parseT(Print * stream, int16_t params, int16_t p[]);
static bool parseZ(Print * stream, int16_t params, int16_t p[]);
static bool parseS(Print * stream, int16_t params, int16_t p[]);
static bool parsef(Print * stream, int16_t params, int16_t p[]);
static bool parseD(Print * stream, int16_t params, int16_t p[]);
bool parseT(Print * stream, int16_t params, int16_t p[]);
bool parseZ(Print * stream, int16_t params, int16_t p[]);
bool parseS(Print * stream, int16_t params, int16_t p[]);
bool parsef(Print * stream, int16_t params, int16_t p[]);
bool parseD(Print * stream, int16_t params, int16_t p[]);
static Print * getAsyncReplyStream();
static void commitAsyncReplyStream();
@@ -59,9 +61,8 @@ struct DCCEXParser
static RingStream * stashRingStream;
static int16_t stashP[MAX_COMMAND_PARAMS];
static bool stashCallback(Print * stream, int16_t p[MAX_COMMAND_PARAMS], RingStream * ringStream);
bool stashCallback(Print * stream, int16_t p[MAX_COMMAND_PARAMS], RingStream * ringStream);
static void callback_W(int16_t result);
static void callback_W4(int16_t result);
static void callback_B(int16_t result);
static void callback_R(int16_t result);
static void callback_Rloco(int16_t result);
@@ -71,8 +72,7 @@ struct DCCEXParser
static FILTER_CALLBACK filterCallback;
static FILTER_CALLBACK filterRMFTCallback;
static AT_COMMAND_CALLBACK atCommandCallback;
static bool funcmap(int16_t cab, byte value, byte fstart, byte fstop);
static void sendFlashList(Print * stream,const int16_t flashList[]);
static void funcmap(int16_t cab, byte value, byte fstart, byte fstop);
};

View File

@@ -1,235 +0,0 @@
/*
* © 2021-2022, Harald Barth.
*
* This file is part of DCC-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#if defined(ARDUINO_ARCH_ESP32)
#include "defines.h"
#include "DIAG.h"
#include "DCCRMT.h"
#include "DCCTimer.h"
#include "DCCWaveform.h" // for MAX_PACKET_SIZE
#include "soc/gpio_sig_map.h"
// Number of bits resulting out of X bytes of DCC payload data
// Each byte has one bit extra and at the end we have one EOF marker
#define DATA_LEN(X) ((X)*9+1)
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4,2,0)
#error wrong IDF version
#endif
void setDCCBit1(rmt_item32_t* item) {
item->level0 = 1;
item->duration0 = DCC_1_HALFPERIOD;
item->level1 = 0;
item->duration1 = DCC_1_HALFPERIOD;
}
void setDCCBit0(rmt_item32_t* item) {
item->level0 = 1;
item->duration0 = DCC_0_HALFPERIOD;
item->level1 = 0;
item->duration1 = DCC_0_HALFPERIOD;
}
// special long zero to trigger scope
void setDCCBit0Long(rmt_item32_t* item) {
item->level0 = 1;
item->duration0 = DCC_0_HALFPERIOD + DCC_0_HALFPERIOD/10;
item->level1 = 0;
item->duration1 = DCC_0_HALFPERIOD + DCC_0_HALFPERIOD/10;
}
void setEOT(rmt_item32_t* item) {
item->val = 0;
}
// This is an array that contains the this pointers
// to all uses channel objects. This is used to determine
// which of the channels was triggering the ISR as there
// is only ONE common ISR routine for all channels.
RMTChannel *channelHandle[8] = { 0 };
void IRAM_ATTR interrupt(rmt_channel_t channel, void *t) {
RMTChannel *tt = channelHandle[channel];
if (tt) tt->RMTinterrupt();
if (channel == 0)
DCCTimer::updateMinimumFreeMemoryISR(0);
}
RMTChannel::RMTChannel(pinpair pins, bool isMain) {
byte ch;
byte plen;
if (isMain) {
ch = 0;
plen = PREAMBLE_BITS_MAIN;
} else {
ch = 2;
plen = PREAMBLE_BITS_PROG;
}
// preamble
preambleLen = plen+2; // plen 1 bits, one 0 bit and one EOF marker
preamble = (rmt_item32_t*)malloc(preambleLen*sizeof(rmt_item32_t));
for (byte n=0; n<plen; n++)
setDCCBit1(preamble + n); // preamble bits
#ifdef SCOPE
setDCCBit0Long(preamble + plen); // start of packet 0 bit long version
#else
setDCCBit0(preamble + plen); // start of packet 0 bit normal version
#endif
setEOT(preamble + plen + 1); // EOT marker
// idle
idleLen = 28;
idle = (rmt_item32_t*)malloc(idleLen*sizeof(rmt_item32_t));
if (isMain) {
for (byte n=0; n<8; n++) // 0 to 7
setDCCBit1(idle + n);
for (byte n=8; n<18; n++) // 8, 9 to 16, 17
setDCCBit0(idle + n);
for (byte n=18; n<26; n++) // 18 to 25
setDCCBit1(idle + n);
} else {
for (byte n=0; n<26; n++) // all zero
setDCCBit0(idle + n);
}
setDCCBit1(idle + 26); // end bit
setEOT(idle + 27); // EOT marker
// data: max packet size today is 5 + checksum
maxDataLen = DATA_LEN(MAX_PACKET_SIZE+1); // plus checksum
data = (rmt_item32_t*)malloc(maxDataLen*sizeof(rmt_item32_t));
rmt_config_t config;
// Configure the RMT channel for TX
bzero(&config, sizeof(rmt_config_t));
config.rmt_mode = RMT_MODE_TX;
config.channel = channel = (rmt_channel_t)ch;
config.clk_div = RMT_CLOCK_DIVIDER;
config.gpio_num = (gpio_num_t)pins.pin;
config.mem_block_num = 2; // With longest DCC packet 11 inc checksum (future expansion)
// number of bits needed is 22preamble + start +
// 11*9 + extrazero + EOT = 124
// 2 mem block of 64 RMT items should be enough
ESP_ERROR_CHECK(rmt_config(&config));
addPin(pins.invpin, true);
/*
// test: config another gpio pin
gpio_num_t gpioNum = (gpio_num_t)(pin-1);
PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[gpioNum], PIN_FUNC_GPIO);
gpio_set_direction(gpioNum, GPIO_MODE_OUTPUT);
gpio_matrix_out(gpioNum, RMT_SIG_OUT0_IDX, 0, 0);
*/
// NOTE: ESP_INTR_FLAG_IRAM is *NOT* included in this bitmask
ESP_ERROR_CHECK(rmt_driver_install(config.channel, 0, ESP_INTR_FLAG_LOWMED|ESP_INTR_FLAG_SHARED));
// DIAG(F("Register interrupt on core %d"), xPortGetCoreID());
ESP_ERROR_CHECK(rmt_set_tx_loop_mode(channel, true));
channelHandle[channel] = this; // used by interrupt
rmt_register_tx_end_callback(interrupt, 0);
rmt_set_tx_intr_en(channel, true);
DIAG(F("Channel %d DCC signal for %s start"), config.channel, isMain ? "MAIN" : "PROG");
// send one bit to kickstart the signal, remaining data will come from the
// packet queue. We intentionally do not wait for the RMT TX complete here.
//rmt_write_items(channel, preamble, preambleLen, false);
RMTprefill();
dataReady = false;
}
void RMTChannel::RMTprefill() {
rmt_fill_tx_items(channel, preamble, preambleLen, 0);
rmt_fill_tx_items(channel, idle, idleLen, preambleLen-1);
}
const byte transmitMask[] = {0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01};
int RMTChannel::RMTfillData(const byte buffer[], byte byteCount, byte repeatCount=0) {
//int RMTChannel::RMTfillData(dccPacket packet) {
// dataReady: Signals to then interrupt routine. It is set when
// we have data in the channel buffer which can be copied out
// to the HW. dataRepeat on the other hand signals back to
// the caller of this function if the data has been sent enough
// times (0 to 3 means 1 to 4 times in total).
if (dataRepeat > 0) // we have still old work to do
return dataRepeat;
if (dataReady == true) // the packet is not copied out yet
return 1000;
if (DATA_LEN(byteCount) > maxDataLen) { // this would overun our allocated memory for data
DIAG(F("Can not convert DCC bytes # %d to DCC bits %d, buffer too small"), byteCount, maxDataLen);
return -1; // something very broken, can not convert packet
}
// convert bytes to RMT stream of "bits"
byte bitcounter = 0;
for(byte n=0; n<byteCount; n++) {
for(byte bit=0; bit<8; bit++) {
if (buffer[n] & transmitMask[bit])
setDCCBit1(data + bitcounter++);
else
setDCCBit0(data + bitcounter++);
}
setDCCBit0(data + bitcounter++); // zero at end of each byte
}
setDCCBit1(data + bitcounter-1); // overwrite previous zero bit with one bit
setEOT(data + bitcounter++); // EOT marker
dataLen = bitcounter;
dataReady = true;
dataRepeat = repeatCount+1; // repeatCount of 0 means send once
return 0;
}
void IRAM_ATTR RMTChannel::RMTinterrupt() {
//no rmt_tx_start(channel,true) as we run in loop mode
//preamble is always loaded at beginning of buffer
packetCounter++;
if (!dataReady && dataRepeat == 0) { // we did run empty
rmt_fill_tx_items(channel, idle, idleLen, preambleLen-1);
return; // nothing to do about that
}
// take care of incoming data
if (dataReady) { // if we have new data, fill while preamble is running
rmt_fill_tx_items(channel, data, dataLen, preambleLen-1);
dataReady = false;
}
if (dataRepeat > 0) // if a repeat count was specified, work on that
dataRepeat--;
}
bool RMTChannel::addPin(byte pin, bool inverted) {
if (pin == UNUSED_PIN)
return true;
gpio_num_t gpioNum = (gpio_num_t)(pin);
esp_err_t err;
PIN_FUNC_SELECT(GPIO_PIN_MUX_REG[gpioNum], PIN_FUNC_GPIO);
err = gpio_set_direction(gpioNum, GPIO_MODE_OUTPUT);
if (err != ESP_OK) return false;
gpio_matrix_out(gpioNum, RMT_SIG_OUT0_IDX+channel, inverted, 0);
if (err != ESP_OK) return false;
return true;
}
bool RMTChannel::addPin(pinpair pins) {
return addPin(pins.pin) && addPin(pins.invpin, true);
}
#endif //ESP32

View File

@@ -1,66 +0,0 @@
/*
* © 2021-2022, Harald Barth.
*
* This file is part of DCC-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#if defined(ARDUINO_ARCH_ESP32)
#pragma once
#include <Arduino.h>
#include "driver/rmt.h"
#include "soc/rmt_reg.h"
#include "soc/rmt_struct.h"
#include "MotorDriver.h" // for class pinpair
// make calculations easy and set up for microseconds
#define RMT_CLOCK_DIVIDER 80
#define DCC_1_HALFPERIOD 58 //4640 // 1 / 80000000 * 4640 = 58us
#define DCC_0_HALFPERIOD 100 //8000
class RMTChannel {
public:
RMTChannel(pinpair pins, bool isMain);
bool addPin(byte pin, bool inverted=0);
bool addPin(pinpair pins);
void IRAM_ATTR RMTinterrupt();
void RMTprefill();
//int RMTfillData(dccPacket packet);
int RMTfillData(const byte buffer[], byte byteCount, byte repeatCount);
inline bool busy() {
if (dataRepeat > 0) // we have still old work to do
return true;
return dataReady;
};
inline uint32_t packetCount() { return packetCounter; };
private:
rmt_channel_t channel;
// 3 types of data to send, preamble and then idle or data
// if this is prog track, idle will contain reset instead
rmt_item32_t *idle;
byte idleLen;
rmt_item32_t *preamble;
byte preambleLen;
rmt_item32_t *data;
byte dataLen;
byte maxDataLen;
uint32_t packetCounter = 0;
// flags
volatile bool dataReady = false; // do we have real data available or send idle
volatile byte dataRepeat = 0;
};
#endif //ESP32

213
DCCTimer.cpp Normal file
View File

@@ -0,0 +1,213 @@
/*
* © 2021, Chris Harlow & David Cutting. All rights reserved.
*
* This file is part of Asbelos DCC API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/* This timer class is used to manage the single timer required to handle the DCC waveform.
* All timer access comes through this class so that it can be compiled for
* various hardware CPU types.
*
* DCCEX works on a single timer interrupt at a regular 58uS interval.
* The DCCWaveform class generates the signals to the motor shield
* based on this timer.
*
* If the motor drivers are BOTH configured to use the correct 2 pins for the architecture,
* (see isPWMPin() function. )
* then this allows us to use a hardware driven pin switching arrangement which is
* achieved by setting the duty cycle of the NEXT clock interrupt to 0% or 100% depending on
* the required pin state. (see setPWM())
* This is more accurate than the software interrupt but at the expense of
* limiting the choice of available pins.
* Fortunately, a standard motor shield on a Mega uses pins that qualify for PWM...
* Other shields may be jumpered to PWM pins or run directly using the software interrupt.
*
* Because the PWM-based waveform is effectively set half a cycle after the software version,
* it is not acceptable to drive the two tracks on different methiods or it would cause
* problems for <1 JOIN> etc.
*
*/
#include "DCCTimer.h"
const int DCC_SIGNAL_TIME=58; // this is the 58uS DCC 1-bit waveform half-cycle
const long CLOCK_CYCLES=(F_CPU / 1000000 * DCC_SIGNAL_TIME) >>1;
INTERRUPT_CALLBACK interruptHandler=0;
#ifdef ARDUINO_ARCH_MEGAAVR
// Arduino unoWifi Rev2 and nanoEvery architectire
void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
interruptHandler=callback;
noInterrupts();
ADC0.CTRLC = (ADC0.CTRLC & 0b00110000) | 0b01000011; // speed up analogRead sample time
TCB0.CTRLB = TCB_CNTMODE_INT_gc & ~TCB_CCMPEN_bm; // timer compare mode with output disabled
TCB0.CTRLA = TCB_CLKSEL_CLKDIV2_gc; // 8 MHz ~ 0.125 us
TCB0.CCMP = CLOCK_CYCLES -1; // 1 tick less for timer reset
TCB0.INTFLAGS = TCB_CAPT_bm; // clear interrupt request flag
TCB0.INTCTRL = TCB_CAPT_bm; // Enable the interrupt
TCB0.CNT = 0;
TCB0.CTRLA |= TCB_ENABLE_bm; // start
interrupts();
}
// ISR called by timer interrupt every 58uS
ISR(TCB0_INT_vect){
TCB0.INTFLAGS = TCB_CAPT_bm;
interruptHandler();
}
bool DCCTimer::isPWMPin(byte pin) {
(void) pin;
return false; // TODO what are the relevant pins?
}
void DCCTimer::setPWM(byte pin, bool high) {
(void) pin;
(void) high;
// TODO what are the relevant pins?
}
void DCCTimer::getSimulatedMacAddress(byte mac[6]) {
memcpy(mac,(void *) &SIGROW.SERNUM0,6); // serial number
mac[0] &= 0xFE;
mac[0] |= 0x02;
}
#elif defined(TEENSYDUINO)
IntervalTimer myDCCTimer;
void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
interruptHandler=callback;
myDCCTimer.begin(interruptHandler, DCC_SIGNAL_TIME);
}
bool DCCTimer::isPWMPin(byte pin) {
//Teensy: digitalPinHasPWM, todo
(void) pin;
return false; // TODO what are the relevant pins?
}
void DCCTimer::setPWM(byte pin, bool high) {
// TODO what are the relevant pins?
(void) pin;
(void) high;
}
void DCCTimer::getSimulatedMacAddress(byte mac[6]) {
#if defined(__IMXRT1062__) //Teensy 4.0 and Teensy 4.1
uint32_t m1 = HW_OCOTP_MAC1;
uint32_t m2 = HW_OCOTP_MAC0;
mac[0] = m1 >> 8;
mac[1] = m1 >> 0;
mac[2] = m2 >> 24;
mac[3] = m2 >> 16;
mac[4] = m2 >> 8;
mac[5] = m2 >> 0;
#else
read_mac(mac);
#endif
}
#if !defined(__IMXRT1062__)
void DCCTimer::read_mac(byte mac[6]) {
read(0xe,mac,0);
read(0xf,mac,3);
}
// http://forum.pjrc.com/threads/91-teensy-3-MAC-address
void DCCTimer::read(uint8_t word, uint8_t *mac, uint8_t offset) {
FTFL_FCCOB0 = 0x41; // Selects the READONCE command
FTFL_FCCOB1 = word; // read the given word of read once area
// launch command and wait until complete
FTFL_FSTAT = FTFL_FSTAT_CCIF;
while(!(FTFL_FSTAT & FTFL_FSTAT_CCIF));
*(mac+offset) = FTFL_FCCOB5; // collect only the top three bytes,
*(mac+offset+1) = FTFL_FCCOB6; // in the right orientation (big endian).
*(mac+offset+2) = FTFL_FCCOB7; // Skip FTFL_FCCOB4 as it's always 0.
}
#endif
#else
// Arduino nano, uno, mega etc
#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
#define TIMER1_A_PIN 11
#define TIMER1_B_PIN 12
#define TIMER1_C_PIN 13
#else
#define TIMER1_A_PIN 9
#define TIMER1_B_PIN 10
#endif
void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
interruptHandler=callback;
noInterrupts();
ADCSRA = (ADCSRA & 0b11111000) | 0b00000100; // speed up analogRead sample time
TCCR1A = 0;
ICR1 = CLOCK_CYCLES;
TCNT1 = 0;
TCCR1B = _BV(WGM13) | _BV(CS10); // Mode 8, clock select 1
TIMSK1 = _BV(TOIE1); // Enable Software interrupt
interrupts();
}
// ISR called by timer interrupt every 58uS
ISR(TIMER1_OVF_vect){ interruptHandler(); }
// Alternative pin manipulation via PWM control.
bool DCCTimer::isPWMPin(byte pin) {
return pin==TIMER1_A_PIN
|| pin==TIMER1_B_PIN
#ifdef TIMER1_C_PIN
|| pin==TIMER1_C_PIN
#endif
;
}
void DCCTimer::setPWM(byte pin, bool high) {
if (pin==TIMER1_A_PIN) {
TCCR1A |= _BV(COM1A1);
OCR1A= high?1024:0;
}
else if (pin==TIMER1_B_PIN) {
TCCR1A |= _BV(COM1B1);
OCR1B= high?1024:0;
}
#ifdef TIMER1_C_PIN
else if (pin==TIMER1_C_PIN) {
TCCR1A |= _BV(COM1C1);
OCR1C= high?1024:0;
}
#endif
}
#include <avr/boot.h>
void DCCTimer::getSimulatedMacAddress(byte mac[6]) {
for (byte i=0; i<6; i++) {
mac[i]=boot_signature_byte_get(0x0E + i);
}
mac[0] &= 0xFE;
mac[0] |= 0x02;
}
#endif

View File

@@ -1,54 +1,3 @@
/*
* © 2022-2023 Paul M. Antoine
* © 2021 Mike S
* © 2021-2023 Harald Barth
* © 2021 Fred Decker
* All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/* There are several different implementations of this class which the compiler will select
according to the hardware.
*/
/* This timer class is used to manage the single timer required to handle the DCC waveform.
* All timer access comes through this class so that it can be compiled for
* various hardware CPU types.
*
* DCCEX works on a single timer interrupt at a regular 58uS interval.
* The DCCWaveform class generates the signals to the motor shield
* based on this timer.
*
* If the motor drivers are BOTH configured to use the correct 2 pins for the architecture,
* (see isPWMPin() function. )
* then this allows us to use a hardware driven pin switching arrangement which is
* achieved by setting the duty cycle of the NEXT clock interrupt to 0% or 100% depending on
* the required pin state. (see setPWM())
* This is more accurate than the software interrupt but at the expense of
* limiting the choice of available pins.
* Fortunately, a standard motor shield on a Mega uses pins that qualify for PWM...
* Other shields may be jumpered to PWM pins or run directly using the software interrupt.
*
* Because the PWM-based waveform is effectively set half a cycle after the software version,
* it is not acceptable to drive the two tracks on different methiods or it would cause
* problems for <1 JOIN> etc.
*
*/
#ifndef DCCTimer_h
#define DCCTimer_h
#include "Arduino.h"
@@ -61,75 +10,11 @@ class DCCTimer {
static void getSimulatedMacAddress(byte mac[6]);
static bool isPWMPin(byte pin);
static void setPWM(byte pin, bool high);
static void clearPWM();
static void DCCEXanalogWriteFrequency(uint8_t pin, uint32_t frequency);
static void DCCEXanalogWrite(uint8_t pin, int value);
// Update low ram level. Allow for extra bytes to be specified
// by estimation or inspection, that may be used by other
// called subroutines. Must be called with interrupts disabled.
//
// Although __brkval may go up and down as heap memory is allocated
// and freed, this function records only the worst case encountered.
// So even if all of the heap is freed, the reported minimum free
// memory will not increase.
//
static void inline updateMinimumFreeMemoryISR(unsigned char extraBytes=0)
__attribute__((always_inline)) {
int spare = freeMemory()-extraBytes;
if (spare < 0) spare = 0;
if (spare < minimum_free_memory) minimum_free_memory = spare;
};
static int getMinimumFreeMemory();
static void reset();
private:
static int freeMemory();
static volatile int minimum_free_memory;
static const int DCC_SIGNAL_TIME=58; // this is the 58uS DCC 1-bit waveform half-cycle
#if defined(ARDUINO_ARCH_STM32) // TODO: PMA temporary hack - assumes 100Mhz F_CPU as STM32 can change frequency
static const long CLOCK_CYCLES=(100000000L / 1000000 * DCC_SIGNAL_TIME) >>1;
#else
static const long CLOCK_CYCLES=(F_CPU / 1000000 * DCC_SIGNAL_TIME) >>1;
#if (defined(TEENSYDUINO) && !defined(__IMXRT1062__))
static void read_mac(byte mac[6]);
static void read(uint8_t word, uint8_t *mac, uint8_t offset);
#endif
private:
};
// Class ADCee implements caching of the ADC value for platforms which
// have a too slow ADC read to wait for. On these platforms the ADC is
// scanned continiously in the background from an ISR. On such
// architectures that use the analog read during DCC waveform with
// specially configured ADC, for example AVR, init must be called
// PRIOR to the start of the waveform. It returns the current value so
// that an offset can be initialized.
class ADCee {
public:
// init does add the pin to the list of scanned pins (if this
// platform's implementation scans pins) and returns the first
// read value. It is called before the regular scan is started.
static int init(uint8_t pin);
// read does read the pin value from the scanned cache or directly
// if this is a platform that does not scan. fromISR is a hint if
// it was called from ISR because for some implementations that
// makes a difference.
static int read(uint8_t pin, bool fromISR=false);
// returns possible max value that the ADC can return
static int16_t ADCmax();
// begin is called for any setup that must be done before
// scan can be called.
static void begin();
private:
// On platforms that scan, it is called from waveform ISR
// only on a regular basis.
static void scan();
// bit array of used pins (max 16)
static uint16_t usedpins;
// cached analog values (malloc:ed to actual number of ADC channels)
static int *analogvals;
// ids to scan (new way)
static byte *idarr;
// friend so that we can call scan() and begin()
friend class DCCWaveform;
};
#endif

View File

@@ -1,234 +0,0 @@
/*
* © 2021 Mike S
* © 2021-2022 Harald Barth
* © 2021 Fred Decker
* © 2021 Chris Harlow
* © 2021 David Cutting
* All rights reserved.
*
* This file is part of Asbelos DCC API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
// ATTENTION: this file only compiles on a UNO or MEGA
// Please refer to DCCTimer.h for general comments about how this class works
// This is to avoid repetition and duplication.
#ifdef ARDUINO_ARCH_AVR
#include <avr/boot.h>
#include <avr/wdt.h>
#include "DCCTimer.h"
INTERRUPT_CALLBACK interruptHandler=0;
// Arduino nano, uno, mega etc
#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__)
#define TIMER1_A_PIN 11
#define TIMER1_B_PIN 12
#define TIMER1_C_PIN 13
#else
#define TIMER1_A_PIN 9
#define TIMER1_B_PIN 10
#endif
void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
interruptHandler=callback;
noInterrupts();
TCCR1A = 0;
ICR1 = CLOCK_CYCLES;
TCNT1 = 0;
TCCR1B = _BV(WGM13) | _BV(CS10); // Mode 8, clock select 1
TIMSK1 = _BV(TOIE1); // Enable Software interrupt
interrupts();
}
// ISR called by timer interrupt every 58uS
ISR(TIMER1_OVF_vect){ interruptHandler(); }
// Alternative pin manipulation via PWM control.
bool DCCTimer::isPWMPin(byte pin) {
return pin==TIMER1_A_PIN
|| pin==TIMER1_B_PIN
#ifdef TIMER1_C_PIN
|| pin==TIMER1_C_PIN
#endif
;
}
void DCCTimer::setPWM(byte pin, bool high) {
if (pin==TIMER1_A_PIN) {
TCCR1A |= _BV(COM1A1);
OCR1A= high?1024:0;
}
else if (pin==TIMER1_B_PIN) {
TCCR1A |= _BV(COM1B1);
OCR1B= high?1024:0;
}
#ifdef TIMER1_C_PIN
else if (pin==TIMER1_C_PIN) {
TCCR1A |= _BV(COM1C1);
OCR1C= high?1024:0;
}
#endif
}
void DCCTimer::clearPWM() {
TCCR1A= 0;
}
void DCCTimer::getSimulatedMacAddress(byte mac[6]) {
for (byte i=0; i<6; i++) {
// take the fist 3 and last 3 of the serial.
// the first 5 of 8 are at 0x0E to 0x013
// the last 3 of 8 are at 0x15 to 0x017
mac[i]=boot_signature_byte_get(0x0E + i + (i>2? 4 : 0));
}
mac[0] &= 0xFE;
mac[0] |= 0x02;
}
volatile int DCCTimer::minimum_free_memory=__INT_MAX__;
// Return low memory value...
int DCCTimer::getMinimumFreeMemory() {
noInterrupts(); // Disable interrupts to get volatile value
int retval = minimum_free_memory;
interrupts();
return retval;
}
extern char *__brkval;
extern char *__malloc_heap_start;
int DCCTimer::freeMemory() {
char top;
return __brkval ? &top - __brkval : &top - __malloc_heap_start;
}
void DCCTimer::reset() {
wdt_enable( WDTO_15MS); // set Arduino watchdog timer for 15ms
delay(50); // wait for the prescaller time to expire
}
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
#define NUM_ADC_INPUTS 16
#else
#define NUM_ADC_INPUTS 8
#endif
uint16_t ADCee::usedpins = 0;
int * ADCee::analogvals = NULL;
byte *ADCee::idarr = NULL;
static bool ADCusesHighPort = false;
/*
* Register a new pin to be scanned
* Returns current reading of pin and
* stores that as well
*/
int ADCee::init(uint8_t pin) {
uint8_t id = pin - A0;
byte n;
if (id >= NUM_ADC_INPUTS)
return -1023;
if (id > 7)
ADCusesHighPort = true;
pinMode(pin, INPUT);
int value = analogRead(pin);
if (analogvals == NULL) {
analogvals = (int *)calloc(NUM_ADC_INPUTS, sizeof(int));
for (n=0 ; n < NUM_ADC_INPUTS; n++) // set unreasonable value at startup as marker
analogvals[n] = -32768; // 16 bit int min value
idarr = (byte *)calloc(NUM_ADC_INPUTS+1, sizeof(byte)); // +1 for terminator value
for (n=0 ; n <= NUM_ADC_INPUTS; n++)
idarr[n] = 255; // set 255 as end of array marker
}
analogvals[id] = value; // store before enable by idarr[n]
for (n=0 ; n <= NUM_ADC_INPUTS; n++) {
if (idarr[n] == 255) {
idarr[n] = id;
break;
}
}
return value;
}
int16_t ADCee::ADCmax() {
return 1023;
}
/*
* Read function ADCee::read(pin) to get value instead of analogRead(pin)
*/
int ADCee::read(uint8_t pin, bool fromISR) {
(void)fromISR; // AVR does ignore this arg
uint8_t id = pin - A0;
int a;
// we do not need to check (analogvals == NULL)
// because usedpins would still be 0 in that case
noInterrupts();
a = analogvals[id];
interrupts();
return a;
}
/*
* Scan function that is called from interrupt
*/
#pragma GCC push_options
#pragma GCC optimize ("-O3")
void ADCee::scan() {
static byte num = 0; // index into id array
static bool waiting = false;
if (waiting) {
// look if we have a result
byte low, high;
if (bit_is_set(ADCSRA, ADSC))
return; // no result, continue to wait
// found value
low = ADCL; //must read low before high
high = ADCH;
bitSet(ADCSRA, ADIF);
analogvals[idarr[num]] = (high << 8) | low;
waiting = false;
}
if (!waiting) {
// cycle around in-use analogue pins
num++;
if (idarr[num] == 255)
num = 0;
// start new ADC aquire on id
#if defined(ADCSRB) && defined(MUX5)
if (ADCusesHighPort) { // if we ever have started to use high pins)
if (idarr[num] > 7) // if we use a high ADC pin
bitSet(ADCSRB, MUX5); // set MUX5 bit
else
bitClear(ADCSRB, MUX5);
}
#endif
ADMUX = (1 << REFS0) | (idarr[num] & 0x07); // select AVCC as reference and set MUX
bitSet(ADCSRA, ADSC); // start conversion
waiting = true;
}
}
#pragma GCC pop_options
void ADCee::begin() {
noInterrupts();
// ADCSRA = (ADCSRA & 0b11111000) | 0b00000100; // speed up analogRead sample time
// Set up ADC for free running mode
ADMUX=(1<<REFS0); //select AVCC as reference. We set MUX later
ADCSRA = (1<<ADEN)|(1 << ADPS2); // ADPS2 means divisor 32 and 16Mhz/32=500kHz.
//bitSet(ADCSRA, ADSC); //do not start the ADC yet. Done when we have set the MUX
interrupts();
}
#endif

View File

@@ -1,217 +0,0 @@
/*
* © 2020-2022 Harald Barth
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
// ATTENTION: this file only compiles on an ESP8266 and ESP32
// On ESP32 we do not even use the functions but they are here for completeness sake
// Please refer to DCCTimer.h for general comments about how this class works
// This is to avoid repetition and duplication.
#ifdef ARDUINO_ARCH_ESP8266
#include "DCCTimer.h"
INTERRUPT_CALLBACK interruptHandler=0;
void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
interruptHandler=callback;
timer1_disable();
// There seem to be differnt ways to attach interrupt handler
// ETS_FRC_TIMER1_INTR_ATTACH(NULL, NULL);
// ETS_FRC_TIMER1_NMI_INTR_ATTACH(interruptHandler);
// Let us choose the one from the API
timer1_attachInterrupt(interruptHandler);
// not exactly sure of order:
timer1_enable(TIM_DIV1, TIM_EDGE, TIM_LOOP);
timer1_write(CLOCK_CYCLES);
}
// We do not support to use PWM to make the Waveform on ESP
bool IRAM_ATTR DCCTimer::isPWMPin(byte pin) {
return false;
}
void IRAM_ATTR DCCTimer::setPWM(byte pin, bool high) {
}
void IRAM_ATTR DCCTimer::clearPWM() {
}
// Fake this as it should not be used
void DCCTimer::getSimulatedMacAddress(byte mac[6]) {
mac[0] = 0xFE;
mac[1] = 0xBE;
mac[2] = 0xEF;
mac[3] = 0xC0;
mac[4] = 0xFF;
mac[5] = 0xEE;
}
volatile int DCCTimer::minimum_free_memory=__INT_MAX__;
// Return low memory value...
int DCCTimer::getMinimumFreeMemory() {
noInterrupts(); // Disable interrupts to get volatile value
int retval = minimum_free_memory;
interrupts();
return retval;
}
int DCCTimer::freeMemory() {
return ESP.getFreeHeap();
}
#endif
////////////////////////////////////////////////////////////////////////
#ifdef ARDUINO_ARCH_ESP32
#include <driver/adc.h>
#include <soc/sens_reg.h>
#include <soc/sens_struct.h>
#undef ADC_INPUT_MAX_VALUE
#define ADC_INPUT_MAX_VALUE 4095 // 12 bit ADC
#define pinToADC1Channel(X) (adc1_channel_t)(((X) > 35) ? (X)-36 : (X)-28)
int IRAM_ATTR local_adc1_get_raw(int channel) {
uint16_t adc_value;
SENS.sar_meas_start1.sar1_en_pad = (1 << channel); // only one channel is selected
while (SENS.sar_slave_addr1.meas_status != 0);
SENS.sar_meas_start1.meas1_start_sar = 0;
SENS.sar_meas_start1.meas1_start_sar = 1;
while (SENS.sar_meas_start1.meas1_done_sar == 0);
adc_value = SENS.sar_meas_start1.meas1_data_sar;
return adc_value;
}
#include "DCCTimer.h"
INTERRUPT_CALLBACK interruptHandler=0;
// https://www.visualmicro.com/page/Timer-Interrupts-Explained.aspx
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;
void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
// This should not be called on ESP32 so disable it
return;
interruptHandler = callback;
hw_timer_t *timer = NULL;
timer = timerBegin(0, 2, true); // prescaler can be 2 to 65536 so choose 2
timerAttachInterrupt(timer, interruptHandler, true);
timerAlarmWrite(timer, CLOCK_CYCLES / 6, true); // divide by prescaler*3 (Clockbase is 80Mhz and not F_CPU 240Mhz)
timerAlarmEnable(timer);
}
// We do not support to use PWM to make the Waveform on ESP
bool IRAM_ATTR DCCTimer::isPWMPin(byte pin) {
return false;
}
void IRAM_ATTR DCCTimer::setPWM(byte pin, bool high) {
}
void IRAM_ATTR DCCTimer::clearPWM() {
}
// Fake this as it should not be used
void DCCTimer::getSimulatedMacAddress(byte mac[6]) {
mac[0] = 0xFE;
mac[1] = 0xBE;
mac[2] = 0xEF;
mac[3] = 0xC0;
mac[4] = 0xFF;
mac[5] = 0xEE;
}
volatile int DCCTimer::minimum_free_memory=__INT_MAX__;
// Return low memory value...
int DCCTimer::getMinimumFreeMemory() {
noInterrupts(); // Disable interrupts to get volatile value
int retval = minimum_free_memory;
interrupts();
return retval;
}
int DCCTimer::freeMemory() {
return ESP.getFreeHeap();
}
void DCCTimer::reset() {
ESP.restart();
}
#include "esp32-hal.h"
#include "soc/soc_caps.h"
#ifdef SOC_LEDC_SUPPORT_HS_MODE
#define LEDC_CHANNELS (SOC_LEDC_CHANNEL_NUM<<1)
#else
#define LEDC_CHANNELS (SOC_LEDC_CHANNEL_NUM)
#endif
static int8_t pin_to_channel[SOC_GPIO_PIN_COUNT] = { 0 };
static int cnt_channel = LEDC_CHANNELS;
void DCCTimer::DCCEXanalogWriteFrequency(uint8_t pin, uint32_t frequency) {
if (pin < SOC_GPIO_PIN_COUNT) {
if (pin_to_channel[pin] != 0) {
ledcSetup(pin_to_channel[pin], frequency, 8);
}
}
}
void DCCTimer::DCCEXanalogWrite(uint8_t pin, int value) {
if (pin < SOC_GPIO_PIN_COUNT) {
if (pin_to_channel[pin] == 0) {
if (!cnt_channel) {
log_e("No more PWM channels available! All %u already used", LEDC_CHANNELS);
return;
}
pin_to_channel[pin] = --cnt_channel;
ledcAttachPin(pin, cnt_channel);
ledcSetup(cnt_channel, 1000, 8);
} else {
ledcAttachPin(pin, pin_to_channel[pin]);
}
ledcWrite(pin_to_channel[pin], value);
}
}
int ADCee::init(uint8_t pin) {
pinMode(pin, ANALOG);
adc1_config_width(ADC_WIDTH_BIT_12);
adc1_config_channel_atten(pinToADC1Channel(pin),ADC_ATTEN_DB_11);
return adc1_get_raw(pinToADC1Channel(pin));
}
int16_t ADCee::ADCmax() {
return 4095;
}
/*
* Read function ADCee::read(pin) to get value instead of analogRead(pin)
*/
int ADCee::read(uint8_t pin, bool fromISR) {
return local_adc1_get_raw(pinToADC1Channel(pin));
}
/*
* Scan function that is called from interrupt
*/
void ADCee::scan() {
}
void ADCee::begin() {
}
#endif //ESP32

View File

@@ -1,155 +0,0 @@
/*
* © 2022 Paul M. Antoine
* © 2021 Mike S
* © 2021 Harald Barth
* © 2021 Fred Decker
* © 2021 Chris Harlow
* © 2021 David Cutting
* All rights reserved.
*
* This file is part of Asbelos DCC API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/* This timer class is used to manage the single timer required to handle the DCC waveform.
* All timer access comes through this class so that it can be compiled for
* various hardware CPU types.
*
* DCCEX works on a single timer interrupt at a regular 58uS interval.
* The DCCWaveform class generates the signals to the motor shield
* based on this timer.
*
* If the motor drivers are BOTH configured to use the correct 2 pins for the architecture,
* (see isPWMPin() function. )
* then this allows us to use a hardware driven pin switching arrangement which is
* achieved by setting the duty cycle of the NEXT clock interrupt to 0% or 100% depending on
* the required pin state. (see setPWM())
* This is more accurate than the software interrupt but at the expense of
* limiting the choice of available pins.
* Fortunately, a standard motor shield on a Mega uses pins that qualify for PWM...
* Other shields may be jumpered to PWM pins or run directly using the software interrupt.
*
* Because the PWM-based waveform is effectively set half a cycle after the software version,
* it is not acceptable to drive the two tracks on different methiods or it would cause
* problems for <1 JOIN> etc.
*
*/
// ATTENTION: this file only compiles on a UnoWifiRev3 or NanoEvery
// Please refer to DCCTimer.h for general comments about how this class works
// This is to avoid repetition and duplication.
#ifdef ARDUINO_ARCH_MEGAAVR
#include "DCCTimer.h"
INTERRUPT_CALLBACK interruptHandler=0;
extern char *__brkval;
extern char *__malloc_heap_start;
void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
interruptHandler=callback;
noInterrupts();
ADC0.CTRLC = (ADC0.CTRLC & 0b00110000) | 0b01000011; // speed up analogRead sample time
TCB0.CTRLB = TCB_CNTMODE_INT_gc & ~TCB_CCMPEN_bm; // timer compare mode with output disabled
TCB0.CTRLA = TCB_CLKSEL_CLKDIV2_gc; // 8 MHz ~ 0.125 us
TCB0.CCMP = CLOCK_CYCLES -1; // 1 tick less for timer reset
TCB0.INTFLAGS = TCB_CAPT_bm; // clear interrupt request flag
TCB0.INTCTRL = TCB_CAPT_bm; // Enable the interrupt
TCB0.CNT = 0;
TCB0.CTRLA |= TCB_ENABLE_bm; // start
interrupts();
}
// ISR called by timer interrupt every 58uS
ISR(TCB0_INT_vect){
TCB0.INTFLAGS = TCB_CAPT_bm; // Clear interrupt request flag
interruptHandler();
}
bool DCCTimer::isPWMPin(byte pin) {
(void) pin;
return false; // TODO what are the relevant pins?
}
void DCCTimer::setPWM(byte pin, bool high) {
(void) pin;
(void) high;
// TODO what are the relevant pins?
}
void DCCTimer::clearPWM() {
// Do nothing unless we implent HA
}
void DCCTimer::getSimulatedMacAddress(byte mac[6]) {
memcpy(mac,(void *) &SIGROW.SERNUM0,6); // serial number
mac[0] &= 0xFE;
mac[0] |= 0x02;
}
volatile int DCCTimer::minimum_free_memory=__INT_MAX__;
// Return low memory value...
int DCCTimer::getMinimumFreeMemory() {
noInterrupts(); // Disable interrupts to get volatile value
int retval = minimum_free_memory;
interrupts();
return retval;
}
extern char *__brkval;
extern char *__malloc_heap_start;
int DCCTimer::freeMemory() {
char top;
return __brkval ? &top - __brkval : &top - __malloc_heap_start;
}
void DCCTimer::reset() {
CPU_CCP=0xD8;
WDT.CTRLA=0x4;
while(true){}
}
int16_t ADCee::ADCmax() {
return 4095;
}
int ADCee::init(uint8_t pin) {
return analogRead(pin);
}
/*
* Read function ADCee::read(pin) to get value instead of analogRead(pin)
*/
int ADCee::read(uint8_t pin, bool fromISR) {
int current;
if (!fromISR) noInterrupts();
current = analogRead(pin);
if (!fromISR) interrupts();
return current;
}
/*
* Scan function that is called from interrupt
*/
void ADCee::scan() {
}
void ADCee::begin() {
noInterrupts();
interrupts();
}
#endif

View File

@@ -1,277 +0,0 @@
/*
* © 2022 Paul M. Antoine
* © 2021 Mike S
* © 2021-2022 Harald Barth
* © 2021 Fred Decker
* © 2021 Chris Harlow
* © 2021 David Cutting
* All rights reserved.
*
* This file is part of Asbelos DCC API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
// ATTENTION: this file only compiles on a SAMD21 based board
// Please refer to DCCTimer.h for general comments about how this class works
// This is to avoid repetition and duplication.
#ifdef ARDUINO_ARCH_SAMD
#include "DCCTimer.h"
#include <wiring_private.h>
INTERRUPT_CALLBACK interruptHandler=0;
void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
interruptHandler=callback;
noInterrupts();
// Timer setup - setup clock sources first
REG_GCLK_GENDIV = GCLK_GENDIV_DIV(1) | // Divide 48MHz by 1
GCLK_GENDIV_ID(4); // Apply to GCLK4
while (GCLK->STATUS.bit.SYNCBUSY); // Wait for synchronization
REG_GCLK_GENCTRL = GCLK_GENCTRL_GENEN | // Enable GCLK
GCLK_GENCTRL_SRC_DFLL48M | // Set the 48MHz clock source
GCLK_GENCTRL_ID(4); // Select GCLK4
while (GCLK->STATUS.bit.SYNCBUSY); // Wait for synchronization
REG_GCLK_CLKCTRL = GCLK_CLKCTRL_CLKEN | // Enable generic clock
4 << GCLK_CLKCTRL_GEN_Pos | // Apply to GCLK4
GCLK_CLKCTRL_ID_TCC0_TCC1; // Feed GCLK to TCC0/1
while (GCLK->STATUS.bit.SYNCBUSY);
// Assume we're using TCC0... as we're bit-bashing the DCC waveform output pins anyway
// for "normal accuracy" DCC waveform generation. For high accuracy we're going to need
// to a good deal more. The TCC waveform output pins are mux'd on the SAMD, and output
// pins for each TCC are only available on certain pins
TCC0->WAVE.reg = TCC_WAVE_WAVEGEN_NPWM; // Select NPWM as waveform
while (TCC0->SYNCBUSY.bit.WAVE); // Wait for sync
// Set the frequency
TCC0->CTRLA.reg |= TCC_CTRLA_PRESCALER(TCC_CTRLA_PRESCALER_DIV1_Val);
TCC0->PER.reg = CLOCK_CYCLES * 2;
while (TCC0->SYNCBUSY.bit.PER);
// Start the timer
TCC0->CTRLA.bit.ENABLE = 1;
while (TCC0->SYNCBUSY.bit.ENABLE);
// Set the interrupt condition, priority and enable it in the NVIC
TCC0->INTENSET.reg = TCC_INTENSET_OVF; // Only interrupt on overflow
int USBprio = NVIC_GetPriority((IRQn_Type) USB_IRQn); // Fetch the USB priority
NVIC_SetPriority((IRQn_Type)TCC0_IRQn, USBprio); // Match the USB priority
// NVIC_SetPriority((IRQn_Type)TCC0_IRQn, 0); // Make this highest priority
NVIC_EnableIRQ((IRQn_Type)TCC0_IRQn); // Enable the interrupt
interrupts();
}
// Timer IRQ handlers replace the dummy handlers (in cortex_handlers)
// copied from rf24 branch
void TCC0_Handler() {
if(TCC0->INTFLAG.bit.OVF) {
TCC0->INTFLAG.bit.OVF = 1; // writing a 1 clears the flag
interruptHandler();
}
}
void TCC1_Handler() {
if(TCC1->INTFLAG.bit.OVF) {
TCC1->INTFLAG.bit.OVF = 1; // writing a 1 clears the flag
interruptHandler();
}
}
void TCC2_Handler() {
if(TCC2->INTFLAG.bit.OVF) {
TCC2->INTFLAG.bit.OVF = 1; // writing a 1 clears the flag
interruptHandler();
}
}
bool DCCTimer::isPWMPin(byte pin) {
//TODO: SAMD whilst this call to digitalPinHasPWM will reveal which pins can do PWM,
// there's no support yet for High Accuracy, so for now return false
// return digitalPinHasPWM(pin);
return false;
}
void DCCTimer::setPWM(byte pin, bool high) {
// TODO: High Accuracy mode is not supported as yet, and may never need to be
(void) pin;
(void) high;
}
void DCCTimer::clearPWM() {
return;
}
void DCCTimer::getSimulatedMacAddress(byte mac[6]) {
volatile uint32_t *serno1 = (volatile uint32_t *)0x0080A00C;
volatile uint32_t *serno2 = (volatile uint32_t *)0x0080A040;
// volatile uint32_t *serno3 = (volatile uint32_t *)0x0080A044;
// volatile uint32_t *serno4 = (volatile uint32_t *)0x0080A048;
volatile uint32_t m1 = *serno1;
volatile uint32_t m2 = *serno2;
mac[0] = m1 >> 8;
mac[1] = m1 >> 0;
mac[2] = m2 >> 24;
mac[3] = m2 >> 16;
mac[4] = m2 >> 8;
mac[5] = m2 >> 0;
}
volatile int DCCTimer::minimum_free_memory=__INT_MAX__;
// Return low memory value...
int DCCTimer::getMinimumFreeMemory() {
noInterrupts(); // Disable interrupts to get volatile value
int retval = freeMemory();
interrupts();
return retval;
}
extern "C" char* sbrk(int incr);
int DCCTimer::freeMemory() {
char top;
return (int)(&top - reinterpret_cast<char *>(sbrk(0)));
}
void DCCTimer::reset() {
__disable_irq();
NVIC_SystemReset();
while(true) {};
}
#define NUM_ADC_INPUTS NUM_ANALOG_INPUTS
uint16_t ADCee::usedpins = 0;
int * ADCee::analogvals = NULL;
int ADCee::init(uint8_t pin) {
uint8_t id = pin - A0;
int value = 0;
if (id > NUM_ADC_INPUTS)
return -1023;
// Permanently configure SAMD IO MUX for that pin
pinPeripheral(pin, PIO_ANALOG);
ADC->INPUTCTRL.bit.MUXPOS = g_APinDescription[pin].ulADCChannelNumber; // Selection for the positive ADC input
// Start conversion
ADC->SWTRIG.bit.START = 1;
// Wait for the conversion to be ready
while (ADC->INTFLAG.bit.RESRDY == 0); // Waiting for conversion to complete
// Read the value
value = ADC->RESULT.reg;
if (analogvals == NULL)
analogvals = (int *)calloc(NUM_ADC_INPUTS+1, sizeof(int));
analogvals[id] = value;
usedpins |= (1<<id);
return value;
}
int16_t ADCee::ADCmax() {
return 4095;
}
/*
* Read function ADCee::read(pin) to get value instead of analogRead(pin)
*/
int ADCee::read(uint8_t pin, bool fromISR) {
uint8_t id = pin - A0;
if ((usedpins & (1<<id) ) == 0)
return -1023;
// we do not need to check (analogvals == NULL)
// because usedpins would still be 0 in that case
return analogvals[id];
}
/*
* Scan function that is called from interrupt
*/
#pragma GCC push_options
#pragma GCC optimize ("-O3")
void ADCee::scan() {
static uint8_t id = 0; // id and mask are the same thing but it is faster to
static uint16_t mask = 1; // increment and shift instead to calculate mask from id
static bool waiting = false;
if (waiting) {
// look if we have a result
if (ADC->INTFLAG.bit.RESRDY == 0)
return; // no result, continue to wait
// found value
analogvals[id] = ADC->RESULT.reg;
// advance at least one track
// for scope debug TrackManager::track[1]->setBrake(0);
waiting = false;
id++;
mask = mask << 1;
if (id == NUM_ADC_INPUTS+1) {
id = 0;
mask = 1;
}
}
if (!waiting) {
if (usedpins == 0) // otherwise we would loop forever
return;
// look for a valid track to sample or until we are around
while (true) {
if (mask & usedpins) {
// start new ADC aquire on id
ADC->INPUTCTRL.bit.MUXPOS = g_APinDescription[id + A0].ulADCChannelNumber; // Selection for the positive ADC input
// Start conversion
ADC->SWTRIG.bit.START = 1;
// for scope debug TrackManager::track[1]->setBrake(1);
waiting = true;
return;
}
id++;
mask = mask << 1;
if (id == NUM_ADC_INPUTS+1) {
id = 0;
mask = 1;
}
}
}
}
#pragma GCC pop_options
void ADCee::begin() {
noInterrupts();
// Set up ADC to do faster reads... default for Arduino Zero platform configs is 436uS,
// and we need sub-58uS. This code sets it to a read speed of around 5-6uS, and enables
// 12-bit mode
// Reconfigure ADC
ADC->CTRLA.bit.ENABLE = 0; // disable ADC
while( ADC->STATUS.bit.SYNCBUSY == 1 ); // wait for synchronization
ADC->CTRLB.reg &= 0b1111100011001111; // mask PRESCALER and RESSEL bits
ADC->CTRLB.reg |= ADC_CTRLB_PRESCALER_DIV64 | // divide Clock by 16
ADC_CTRLB_RESSEL_12BIT; // Result 12 bits, 10 bits possible
ADC->AVGCTRL.reg = ADC_AVGCTRL_SAMPLENUM_1 | // take 1 sample at a time
ADC_AVGCTRL_ADJRES(0x00ul); // adjusting result by 0
ADC->SAMPCTRL.reg = 0x00ul; // sampling Time Length = 0
ADC->CTRLA.bit.ENABLE = 1; // enable ADC
while( ADC->STATUS.bit.SYNCBUSY == 1 ); // wait for synchronization
interrupts();
}
#endif

View File

@@ -1,396 +0,0 @@
/*
* © 2023 Neil McKechnie
* © 2022-23 Paul M. Antoine
* © 2021 Mike S
* © 2021, 2023 Harald Barth
* © 2021 Fred Decker
* © 2021 Chris Harlow
* © 2021 David Cutting
* All rights reserved.
*
* This file is part of Asbelos DCC API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
// ATTENTION: this file only compiles on a STM32 based boards
// Please refer to DCCTimer.h for general comments about how this class works
// This is to avoid repetition and duplication.
#ifdef ARDUINO_ARCH_STM32
#include "DCCTimer.h"
#if defined(ARDUINO_NUCLEO_F411RE)
// Nucleo-64 boards don't have Serial1 defined by default
HardwareSerial Serial1(PB7, PA15); // Rx=PB7, Tx=PA15 -- CN7 pins 17 and 21 - F411RE
// Serial2 is defined to use USART2 by default, but is in fact used as the diag console
// via the debugger on the Nucleo-64. It is therefore unavailable for other DCC-EX uses like WiFi, DFPlayer, etc.
// Let's define Serial6 as an additional serial port (the only other option for the Nucleo-64s)
HardwareSerial Serial6(PA12, PA11); // Rx=PA12, Tx=PA11 -- CN10 pins 12 and 14 - F411RE
#elif defined(ARDUINO_NUCLEO_F446RE)
// Nucleo-64 boards don't have Serial1 defined by default
HardwareSerial Serial1(PA10, PB6); // Rx=PA10 (D2), Tx=PB6 (D10) -- CN10 pins 17 and 9 - F446RE
// Serial2 is defined to use USART2 by default, but is in fact used as the diag console
// via the debugger on the Nucleo-64. It is therefore unavailable for other DCC-EX uses like WiFi, DFPlayer, etc.
// NB: USART3 and USART6 are available but as yet undefined
#elif defined(ARDUINO_NUCLEO_F412ZG) || defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F446ZE)
// Nucleo-144 boards don't have Serial1 defined by default
HardwareSerial Serial1(PG9, PG14); // Rx=PG9, Tx=PG14 -- D0, D1 - F412ZG/F446ZE
// Serial2 is defined to use USART2 by default, but is in fact used as the diag console
// via the debugger on the Nucleo-144. It is therefore unavailable for other DCC-EX uses like WiFi, DFPlayer, etc.
// NB:
// On all of the above, USART3, and USART6 are available but as yet undefined
// On F446ZE and F429ZI, UART4, UART5 are also available but as yet undefined
// On F429ZI, UART7 and UART8 are also available but as yet undefined
#else
#error STM32 board selected is not yet explicitly supported - so Serial1 peripheral is not defined
#endif
///////////////////////////////////////////////////////////////////////////////////////////////
// Experimental code for High Accuracy (HA) DCC Signal mode
// Warning - use of TIM2 and TIM3 can affect the use of analogWrite() function on certain pins,
// which is used by the DC motor types.
///////////////////////////////////////////////////////////////////////////////////////////////
// INTERRUPT_CALLBACK interruptHandler=0;
// // Let's use STM32's timer #2 which supports hardware pulse generation on pin D13.
// // Also, timer #3 will do hardware pulses on pin D12. This gives
// // accurate timing, independent of the latency of interrupt handling.
// // We only need to interrupt on one of these (TIM2), the other will just generate
// // pulses.
// HardwareTimer timer(TIM2);
// HardwareTimer timerAux(TIM3);
// static bool tim2ModeHA = false;
// static bool tim3ModeHA = false;
// // Timer IRQ handler
// void Timer_Handler() {
// interruptHandler();
// }
// void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
// interruptHandler=callback;
// noInterrupts();
// // adc_set_sample_rate(ADC_SAMPLETIME_480CYCLES);
// timer.pause();
// timerAux.pause();
// timer.setPrescaleFactor(1);
// timer.setOverflow(DCC_SIGNAL_TIME, MICROSEC_FORMAT);
// timer.attachInterrupt(Timer_Handler);
// timer.refresh();
// timerAux.setPrescaleFactor(1);
// timerAux.setOverflow(DCC_SIGNAL_TIME, MICROSEC_FORMAT);
// timerAux.refresh();
// timer.resume();
// timerAux.resume();
// interrupts();
// }
// bool DCCTimer::isPWMPin(byte pin) {
// // Timer 2 Channel 1 controls pin D13, and Timer3 Channel 1 controls D12.
// // Enable the appropriate timer channel.
// switch (pin) {
// case 12:
// return true;
// case 13:
// return true;
// default:
// return false;
// }
// }
// void DCCTimer::setPWM(byte pin, bool high) {
// // Set the timer so that, at the next counter overflow, the requested
// // pin state is activated automatically before the interrupt code runs.
// // TIM2 is timer, TIM3 is timerAux.
// switch (pin) {
// case 12:
// if (!tim3ModeHA) {
// timerAux.setMode(1, TIMER_OUTPUT_COMPARE_INACTIVE, D12);
// tim3ModeHA = true;
// }
// if (high)
// TIM3->CCMR1 = (TIM3->CCMR1 & ~TIM_CCMR1_OC1M_Msk) | TIM_CCMR1_OC1M_0;
// else
// TIM3->CCMR1 = (TIM3->CCMR1 & ~TIM_CCMR1_OC1M_Msk) | TIM_CCMR1_OC1M_1;
// break;
// case 13:
// if (!tim2ModeHA) {
// timer.setMode(1, TIMER_OUTPUT_COMPARE_INACTIVE, D13);
// tim2ModeHA = true;
// }
// if (high)
// TIM2->CCMR1 = (TIM2->CCMR1 & ~TIM_CCMR1_OC1M_Msk) | TIM_CCMR1_OC1M_0;
// else
// TIM2->CCMR1 = (TIM2->CCMR1 & ~TIM_CCMR1_OC1M_Msk) | TIM_CCMR1_OC1M_1;
// break;
// }
// }
// void DCCTimer::clearPWM() {
// timer.setMode(1, TIMER_OUTPUT_COMPARE_INACTIVE, NC);
// tim2ModeHA = false;
// timerAux.setMode(1, TIMER_OUTPUT_COMPARE_INACTIVE, NC);
// tim3ModeHA = false;
// }
///////////////////////////////////////////////////////////////////////////////////////////////
INTERRUPT_CALLBACK interruptHandler=0;
// Let's use STM32's timer #11 until disabused of this notion
// Timer #11 is used for "servo" library, but as DCC-EX is not using
// this libary, we should be free and clear.
HardwareTimer timer(TIM11);
// Timer IRQ handler
void Timer11_Handler() {
interruptHandler();
}
void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
interruptHandler=callback;
noInterrupts();
// adc_set_sample_rate(ADC_SAMPLETIME_480CYCLES);
timer.pause();
timer.setPrescaleFactor(1);
// timer.setOverflow(CLOCK_CYCLES * 2);
timer.setOverflow(DCC_SIGNAL_TIME, MICROSEC_FORMAT);
timer.attachInterrupt(Timer11_Handler);
timer.refresh();
timer.resume();
interrupts();
}
bool DCCTimer::isPWMPin(byte pin) {
//TODO: SAMD whilst this call to digitalPinHasPWM will reveal which pins can do PWM,
// there's no support yet for High Accuracy, so for now return false
// return digitalPinHasPWM(pin);
return false;
}
void DCCTimer::setPWM(byte pin, bool high) {
// TODO: High Accuracy mode is not supported as yet, and may never need to be
(void) pin;
(void) high;
}
void DCCTimer::clearPWM() {
return;
}
void DCCTimer::getSimulatedMacAddress(byte mac[6]) {
volatile uint32_t *serno1 = (volatile uint32_t *)0x1FFF7A10;
volatile uint32_t *serno2 = (volatile uint32_t *)0x1FFF7A14;
// volatile uint32_t *serno3 = (volatile uint32_t *)0x1FFF7A18;
volatile uint32_t m1 = *serno1;
volatile uint32_t m2 = *serno2;
mac[0] = m1 >> 8;
mac[1] = m1 >> 0;
mac[2] = m2 >> 24;
mac[3] = m2 >> 16;
mac[4] = m2 >> 8;
mac[5] = m2 >> 0;
}
volatile int DCCTimer::minimum_free_memory=__INT_MAX__;
// Return low memory value...
int DCCTimer::getMinimumFreeMemory() {
noInterrupts(); // Disable interrupts to get volatile value
int retval = freeMemory();
interrupts();
return retval;
}
extern "C" char* sbrk(int incr);
int DCCTimer::freeMemory() {
char top;
return (int)(&top - reinterpret_cast<char *>(sbrk(0)));
}
void DCCTimer::reset() {
__disable_irq();
NVIC_SystemReset();
while(true) {};
}
#define NUM_ADC_INPUTS NUM_ANALOG_INPUTS
// TODO: may need to use uint32_t on STMF4xx variants with > 16 analog inputs!
#if defined(ARDUINO_NUCLEO_F446RE) || defined(ARDUINO_NUCLEO_F429ZI) || defined(ARDUINO_NUCLEO_F446ZE)
#warning STM32 board selected not fully supported - only use ADC1 inputs 0-15 for current sensing!
#endif
uint16_t ADCee::usedpins = 0;
int * ADCee::analogvals = NULL;
uint32_t * analogchans = NULL;
bool adc1configured = false;
int16_t ADCee::ADCmax() {
return 4095;
}
int ADCee::init(uint8_t pin) {
int value = 0;
PinName stmpin = analogInputToPinName(pin);
if (stmpin == NC) // do not continue if this is not an analog pin at all
return -1024; // some silly value as error
uint32_t stmgpio = STM_PORT(stmpin); // converts to the GPIO port (16-bits per port group on STM32)
uint32_t adcchan = STM_PIN_CHANNEL(pinmap_function(stmpin, PinMap_ADC)); // find ADC channel (only valid for ADC1!)
GPIO_TypeDef * gpioBase;
// Port config - find which port we're on and power it up
switch(stmgpio) {
case 0x00:
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; //Power up PORTA
gpioBase = GPIOA;
break;
case 0x01:
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN; //Power up PORTB
gpioBase = GPIOB;
break;
case 0x02:
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOCEN; //Power up PORTC
gpioBase = GPIOC;
break;
default:
return -1023; // some silly value as error
}
// Set pin mux mode to analog input, the 32 bit port mode register has 2 bits per pin
gpioBase->MODER |= (0b011 << (STM_PIN(stmpin) << 1)); // Set pin mux to analog mode (binary 11)
// Set the sampling rate for that analog input
// This is F411x specific! Different on for example F334
// STM32F11xC/E Reference manual
// 11.12.4 ADC sample time register 1 (ADC_SMPR1) (channels 10 to 18)
// 11.12.5 ADC sample time register 2 (ADC_SMPR2) (channels 0 to 9)
if (adcchan > 18)
return -1022; // silly value as error
if (adcchan < 10)
ADC1->SMPR2 |= (0b111 << (adcchan * 3)); // Channel sampling rate 480 cycles
else
ADC1->SMPR1 |= (0b111 << ((adcchan - 10) * 3)); // Channel sampling rate 480 cycles
// Read the inital ADC value for this analog input
ADC1->SQR3 = adcchan; // 1st conversion in regular sequence
ADC1->CR2 |= (1 << 30); // Start 1st conversion SWSTART
while(!(ADC1->SR & (1 << 1))); // Wait until conversion is complete
value = ADC1->DR; // Read value from register
uint8_t id = pin - PNUM_ANALOG_BASE;
if (id > 15) { // today we have not enough bits in the mask to support more
return -1021;
}
if (analogvals == NULL) { // allocate analogvals and analogchans if this is the first invocation of init.
analogvals = (int *)calloc(NUM_ADC_INPUTS+1, sizeof(int));
analogchans = (uint32_t *)calloc(NUM_ADC_INPUTS+1, sizeof(uint32_t));
}
analogvals[id] = value; // Store sampled value
analogchans[id] = adcchan; // Keep track of which ADC channel is used for reading this pin
usedpins |= (1 << id); // This pin is now ready
return value;
}
/*
* Read function ADCee::read(pin) to get value instead of analogRead(pin)
*/
int ADCee::read(uint8_t pin, bool fromISR) {
uint8_t id = pin - PNUM_ANALOG_BASE;
// Was this pin initialised yet?
if ((usedpins & (1<<id) ) == 0)
return -1023;
// We do not need to check (analogvals == NULL)
// because usedpins would still be 0 in that case
return analogvals[id];
}
/*
* Scan function that is called from interrupt
*/
#pragma GCC push_options
#pragma GCC optimize ("-O3")
void ADCee::scan() {
static uint8_t id = 0; // id and mask are the same thing but it is faster to
static uint16_t mask = 1; // increment and shift instead to calculate mask from id
static bool waiting = false;
if (waiting) {
// look if we have a result
if (!(ADC1->SR & (1 << 1)))
return; // no result, continue to wait
// found value
analogvals[id] = ADC1->DR;
// advance at least one track
// for scope debug TrackManager::track[1]->setBrake(0);
waiting = false;
id++;
mask = mask << 1;
if (id == NUM_ADC_INPUTS+1) {
id = 0;
mask = 1;
}
}
if (!waiting) {
if (usedpins == 0) // otherwise we would loop forever
return;
// look for a valid track to sample or until we are around
while (true) {
if (mask & usedpins) {
// start new ADC aquire on id
ADC1->SQR3 = analogchans[id]; //1st conversion in regular sequence
ADC1->CR2 |= (1 << 30); //Start 1st conversion SWSTART
// for scope debug TrackManager::track[1]->setBrake(1);
waiting = true;
return;
}
id++;
mask = mask << 1;
if (id == NUM_ADC_INPUTS+1) {
id = 0;
mask = 1;
}
}
}
}
#pragma GCC pop_options
void ADCee::begin() {
noInterrupts();
//ADC1 config sequence
// TODO: currently defaults to ADC1, may need more to handle other members of STM32F4xx family
RCC->APB2ENR |= (1 << 8); //Enable ADC1 clock (Bit8)
// Set ADC prescaler - DIV8 ~ 40ms, DIV6 ~ 30ms, DIV4 ~ 20ms, DIV2 ~ 11ms
ADC->CCR = (0 << 16); // Set prescaler 0=DIV2, 1=DIV4, 2=DIV6, 3=DIV8
ADC1->CR1 &= ~(1 << 8); //SCAN mode disabled (Bit8)
ADC1->CR1 &= ~(3 << 24); //12bit resolution (Bit24,25 0b00)
ADC1->SQR1 = (1 << 20); //Set number of conversions projected (L[3:0] 0b0001) -> 1 conversion
ADC1->CR2 &= ~(1 << 1); //Single conversion
ADC1->CR2 &= ~(1 << 11); //Right alignment of data bits bit12....bit0
ADC1->SQR1 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register
ADC1->SQR2 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register
ADC1->SQR3 &= ~(0x3FFFFFFF); //Clear whole 1st 30bits in register
ADC1->CR2 |= (1 << 0); // Switch on ADC1
interrupts();
}
#endif

View File

@@ -1,171 +0,0 @@
/*
* © 2022 Paul M Antoine
* © 2021 Mike S
* © 2021 Harald Barth
* © 2021 Fred Decker
* © 2021 Chris Harlow
* © 2021 David Cutting
* All rights reserved.
*
* This file is part of Asbelos DCC API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
// ATTENTION: this file only compiles on a TEENSY
// Please refer to DCCTimer.h for general comments about how this class works
// This is to avoid repetition and duplication.
#ifdef TEENSYDUINO
#include "DCCTimer.h"
INTERRUPT_CALLBACK interruptHandler=0;
IntervalTimer myDCCTimer;
void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
interruptHandler=callback;
myDCCTimer.begin(interruptHandler, DCC_SIGNAL_TIME);
}
bool DCCTimer::isPWMPin(byte pin) {
//Teensy: digitalPinHasPWM, todo
(void) pin;
return false; // TODO what are the relevant pins?
}
void DCCTimer::setPWM(byte pin, bool high) {
// TODO what are the relevant pins?
(void) pin;
(void) high;
}
void DCCTimer::clearPWM() {
// Do nothing unless we implent HA
}
#if defined(__IMXRT1062__) //Teensy 4.0 and Teensy 4.1
void DCCTimer::getSimulatedMacAddress(byte mac[6]) {
uint32_t m1 = HW_OCOTP_MAC1;
uint32_t m2 = HW_OCOTP_MAC0;
mac[0] = m1 >> 8;
mac[1] = m1 >> 0;
mac[2] = m2 >> 24;
mac[3] = m2 >> 16;
mac[4] = m2 >> 8;
mac[5] = m2 >> 0;
}
#else
// http://forum.pjrc.com/threads/91-teensy-3-MAC-address
void teensyRead(uint8_t word, uint8_t *mac, uint8_t offset) {
FTFL_FCCOB0 = 0x41; // Selects the READONCE command
FTFL_FCCOB1 = word; // read the given word of read once area
// launch command and wait until complete
FTFL_FSTAT = FTFL_FSTAT_CCIF;
while(!(FTFL_FSTAT & FTFL_FSTAT_CCIF));
*(mac+offset) = FTFL_FCCOB5; // collect only the top three bytes,
*(mac+offset+1) = FTFL_FCCOB6; // in the right orientation (big endian).
*(mac+offset+2) = FTFL_FCCOB7; // Skip FTFL_FCCOB4 as it's always 0.
}
void DCCTimer::getSimulatedMacAddress(byte mac[6]) {
teensyRead(0xe,mac,0);
teensyRead(0xf,mac,3);
}
#endif
volatile int DCCTimer::minimum_free_memory=__INT_MAX__;
// Return low memory value...
int DCCTimer::getMinimumFreeMemory() {
noInterrupts(); // Disable interrupts to get volatile value
int retval = freeMemory();
interrupts();
return retval;
}
extern "C" char* sbrk(int incr);
#if !defined(__IMXRT1062__)
int DCCTimer::freeMemory() {
char top;
return &top - reinterpret_cast<char*>(sbrk(0));
}
#else
#if defined(ARDUINO_TEENSY40)
static const unsigned DTCM_START = 0x20000000UL;
static const unsigned OCRAM_START = 0x20200000UL;
static const unsigned OCRAM_SIZE = 512;
static const unsigned FLASH_SIZE = 1984;
#elif defined(ARDUINO_TEENSY41)
static const unsigned DTCM_START = 0x20000000UL;
static const unsigned OCRAM_START = 0x20200000UL;
static const unsigned OCRAM_SIZE = 512;
static const unsigned FLASH_SIZE = 7936;
#if TEENSYDUINO>151
extern "C" uint8_t external_psram_size;
#endif
#endif
int DCCTimer::freeMemory() {
extern unsigned long _ebss;
extern unsigned long _sdata;
extern unsigned long _estack;
const unsigned DTCM_START = 0x20000000UL;
unsigned dtcm = (unsigned)&_estack - DTCM_START;
unsigned stackinuse = (unsigned) &_estack - (unsigned) __builtin_frame_address(0);
unsigned varsinuse = (unsigned)&_ebss - (unsigned)&_sdata;
unsigned freemem = dtcm - (stackinuse + varsinuse);
return freemem;
}
#endif
void DCCTimer::reset() {
// found at https://forum.pjrc.com/threads/59935-Reboot-Teensy-programmatically
SCB_AIRCR = 0x05FA0004;
}
int16_t ADCee::ADCmax() {
return 4095;
}
int ADCee::init(uint8_t pin) {
return analogRead(pin);
}
/*
* Read function ADCee::read(pin) to get value instead of analogRead(pin)
*/
int ADCee::read(uint8_t pin, bool fromISR) {
int current;
if (!fromISR) noInterrupts();
current = analogRead(pin);
if (!fromISR) interrupts();
return current;
}
/*
* Scan function that is called from interrupt
*/
void ADCee::scan() {
}
void ADCee::begin() {
noInterrupts();
interrupts();
}
#endif

View File

@@ -1,12 +1,8 @@
/*
* © 2021 Neil McKechnie
* © 2021 Mike S
* © 2021 Fred Decker
* © 2020-2022 Harald Barth
* © 2020-2021 Chris Harlow
* All rights reserved.
* © 2020, Chris Harlow. All rights reserved.
* © 2020, Harald Barth.
*
* This file is part of CommandStation-EX
* This file is part of Asbelos DCC API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,80 +17,69 @@
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef ARDUINO_ARCH_ESP32
// This code is replaced entirely on an ESP32
#pragma GCC optimize ("-O3")
#include <Arduino.h>
#include "DCCWaveform.h"
#include "TrackManager.h"
#include "DCCTimer.h"
#include "DCCACK.h"
#include "DIAG.h"
#include "freeMemory.h"
DCCWaveform DCCWaveform::mainTrack(PREAMBLE_BITS_MAIN, true);
DCCWaveform DCCWaveform::progTrack(PREAMBLE_BITS_PROG, false);
bool DCCWaveform::progTrackSyncMain=false;
bool DCCWaveform::progTrackBoosted=false;
int DCCWaveform::progTripValue=0;
volatile uint8_t DCCWaveform::numAckGaps=0;
volatile uint8_t DCCWaveform::numAckSamples=0;
uint8_t DCCWaveform::trailingEdgeCounter=0;
// This bitmask has 9 entries as each byte is trasmitted as a zero + 8 bits.
const byte bitMask[] = {0x00, 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01};
const byte idlePacket[] = {0xFF, 0x00, 0xFF};
const byte resetPacket[] = {0x00, 0x00, 0x00};
// For each state of the wave nextState=stateTransform[currentState]
const WAVE_STATE stateTransform[]={
/* WAVE_START -> */ WAVE_PENDING,
/* WAVE_MID_1 -> */ WAVE_START,
/* WAVE_HIGH_0 -> */ WAVE_MID_0,
/* WAVE_MID_0 -> */ WAVE_LOW_0,
/* WAVE_LOW_0 -> */ WAVE_START,
/* WAVE_PENDING (should not happen) -> */ WAVE_PENDING};
// For each state of the wave, signal pin is HIGH or LOW
const bool signalTransform[]={
/* WAVE_START -> */ HIGH,
/* WAVE_MID_1 -> */ LOW,
/* WAVE_HIGH_0 -> */ HIGH,
/* WAVE_MID_0 -> */ LOW,
/* WAVE_LOW_0 -> */ LOW,
/* WAVE_PENDING (should not happen) -> */ LOW};
void DCCWaveform::begin() {
void DCCWaveform::begin(MotorDriver * mainDriver, MotorDriver * progDriver) {
mainTrack.motorDriver=mainDriver;
progTrack.motorDriver=progDriver;
progTripValue = progDriver->mA2raw(TRIP_CURRENT_PROG); // need only calculate once hence static
mainTrack.setPowerMode(POWERMODE::OFF);
progTrack.setPowerMode(POWERMODE::OFF);
// Fault pin config for odd motor boards (example pololu)
MotorDriver::commonFaultPin = ((mainDriver->getFaultPin() == progDriver->getFaultPin())
&& (mainDriver->getFaultPin() != UNUSED_PIN));
// Only use PWM if both pins are PWM capable. Otherwise JOIN does not work
MotorDriver::usePWM= mainDriver->isPWMCapable() && progDriver->isPWMCapable();
if (MotorDriver::usePWM)
DIAG(F("Signal pin config: high accuracy waveform"));
else
DIAG(F("Signal pin config: normal accuracy waveform"));
DCCTimer::begin(DCCWaveform::interruptHandler);
}
void DCCWaveform::loop() {
// empty placemarker in case ESP32 needs something here
void DCCWaveform::loop(bool ackManagerActive) {
mainTrack.checkPowerOverload(false);
progTrack.checkPowerOverload(ackManagerActive);
}
#pragma GCC push_options
#pragma GCC optimize ("-O3")
void DCCWaveform::interruptHandler() {
// call the timer edge sensitive actions for progtrack and maintrack
// member functions would be cleaner but have more overhead
byte sigMain=signalTransform[mainTrack.state];
byte sigProg=TrackManager::progTrackSyncMain? sigMain : signalTransform[progTrack.state];
byte sigProg=progTrackSyncMain? sigMain : signalTransform[progTrack.state];
// Set the signal state for both tracks
TrackManager::setDCCSignal(sigMain);
TrackManager::setPROGSignal(sigProg);
// Refresh the values in the ADCee object buffering the values of the ADC HW
ADCee::scan();
mainTrack.motorDriver->setSignal(sigMain);
progTrack.motorDriver->setSignal(sigProg);
// Move on in the state engine
mainTrack.state=stateTransform[mainTrack.state];
progTrack.state=stateTransform[progTrack.state];
// WAVE_PENDING means we dont yet know what the next bit is
if (mainTrack.state==WAVE_PENDING) mainTrack.interrupt2();
if (progTrack.state==WAVE_PENDING) progTrack.interrupt2();
else DCCACK::checkAck(progTrack.getResets());
else if (progTrack.ackPending) progTrack.checkAck();
}
#pragma GCC pop_options
// An instance of this class handles the DCC transmissions for one track. (main or prog)
// Interrupts are marshalled via the statics.
@@ -102,6 +87,9 @@ void DCCWaveform::interruptHandler() {
// When the current buffer is exhausted, either the pending buffer (if there is one waiting) or an idle buffer.
// This bitmask has 9 entries as each byte is trasmitted as a zero + 8 bits.
const byte bitMask[] = {0x00, 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01};
DCCWaveform::DCCWaveform( byte preambleBits, bool isMain) {
isMainTrack = isMain;
@@ -113,12 +101,102 @@ DCCWaveform::DCCWaveform( byte preambleBits, bool isMain) {
requiredPreambles = preambleBits+1;
bytes_sent = 0;
bits_sent = 0;
sampleDelay = 0;
lastSampleTaken = millis();
ackPending=false;
}
POWERMODE DCCWaveform::getPowerMode() {
return powerMode;
}
void DCCWaveform::setPowerMode(POWERMODE mode) {
powerMode = mode;
bool ison = (mode == POWERMODE::ON);
motorDriver->setPower( ison);
}
void DCCWaveform::checkPowerOverload(bool ackManagerActive) {
if (millis() - lastSampleTaken < sampleDelay) return;
lastSampleTaken = millis();
int tripValue= motorDriver->getRawCurrentTripValue();
if (!isMainTrack && !ackManagerActive && !progTrackSyncMain && !progTrackBoosted)
tripValue=progTripValue;
switch (powerMode) {
case POWERMODE::OFF:
sampleDelay = POWER_SAMPLE_OFF_WAIT;
break;
case POWERMODE::ON:
// Check current
lastCurrent=motorDriver->getCurrentRaw();
if (lastCurrent < 0) {
// We have a fault pin condition to take care of
lastCurrent = -lastCurrent;
setPowerMode(POWERMODE::OVERLOAD); // Turn off, decide later how fast to turn on again
if (MotorDriver::commonFaultPin) {
if (lastCurrent <= tripValue) {
setPowerMode(POWERMODE::ON); // maybe other track
}
// Write this after the fact as we want to turn on as fast as possible
// because we don't know which output actually triggered the fault pin
DIAG(F("*** COMMON FAULT PIN ACTIVE - TOGGLED POWER on %S ***"), isMainTrack ? F("MAIN") : F("PROG"));
} else {
DIAG(F("*** %S FAULT PIN ACTIVE - OVERLOAD ***"), isMainTrack ? F("MAIN") : F("PROG"));
if (lastCurrent < tripValue) {
lastCurrent = tripValue; // exaggerate
}
}
}
if (lastCurrent < tripValue) {
sampleDelay = POWER_SAMPLE_ON_WAIT;
if(power_good_counter<100)
power_good_counter++;
else
if (power_sample_overload_wait>POWER_SAMPLE_OVERLOAD_WAIT) power_sample_overload_wait=POWER_SAMPLE_OVERLOAD_WAIT;
} else {
setPowerMode(POWERMODE::OVERLOAD);
unsigned int mA=motorDriver->raw2mA(lastCurrent);
unsigned int maxmA=motorDriver->raw2mA(tripValue);
power_good_counter=0;
sampleDelay = power_sample_overload_wait;
DIAG(F("*** %S TRACK POWER OVERLOAD current=%d max=%d offtime=%d ***"), isMainTrack ? F("MAIN") : F("PROG"), mA, maxmA, sampleDelay);
if (power_sample_overload_wait >= 10000)
power_sample_overload_wait = 10000;
else
power_sample_overload_wait *= 2;
}
break;
case POWERMODE::OVERLOAD:
// Try setting it back on after the OVERLOAD_WAIT
setPowerMode(POWERMODE::ON);
sampleDelay = POWER_SAMPLE_ON_WAIT;
// Debug code....
DIAG(F("*** %S TRACK POWER RESET delay=%d ***"), isMainTrack ? F("MAIN") : F("PROG"), sampleDelay);
break;
default:
sampleDelay = 999; // cant get here..meaningless statement to avoid compiler warning.
}
}
// For each state of the wave nextState=stateTransform[currentState]
const WAVE_STATE DCCWaveform::stateTransform[]={
/* WAVE_START -> */ WAVE_PENDING,
/* WAVE_MID_1 -> */ WAVE_START,
/* WAVE_HIGH_0 -> */ WAVE_MID_0,
/* WAVE_MID_0 -> */ WAVE_LOW_0,
/* WAVE_LOW_0 -> */ WAVE_START,
/* WAVE_PENDING (should not happen) -> */ WAVE_PENDING};
#pragma GCC push_options
#pragma GCC optimize ("-O3")
// For each state of the wave, signal pin is HIGH or LOW
const bool DCCWaveform::signalTransform[]={
/* WAVE_START -> */ HIGH,
/* WAVE_MID_1 -> */ LOW,
/* WAVE_HIGH_0 -> */ HIGH,
/* WAVE_MID_0 -> */ LOW,
/* WAVE_LOW_0 -> */ LOW,
/* WAVE_PENDING (should not happen) -> */ LOW};
void DCCWaveform::interrupt2() {
// calculate the next bit to be sent:
// set state WAVE_MID_1 for a 1=bit
@@ -129,7 +207,7 @@ void DCCWaveform::interrupt2() {
remainingPreambles--;
// Update free memory diagnostic as we don't have anything else to do this time.
// Allow for checkAck and its called functions using 22 bytes more.
DCCTimer::updateMinimumFreeMemoryISR(22);
updateMinimumFreeMemory(22);
return;
}
@@ -162,19 +240,20 @@ void DCCWaveform::interrupt2() {
transmitLength = pendingLength;
transmitRepeats = pendingRepeats;
packetPending = false;
clearResets();
sentResetsSincePacket=0;
}
else {
// Fortunately reset and idle packets are the same length
memcpy( transmitPacket, isMainTrack ? idlePacket : resetPacket, sizeof(idlePacket));
transmitLength = sizeof(idlePacket);
transmitRepeats = 0;
if (getResets() < 250) sentResetsSincePacket++; // only place to increment (private!)
if (sentResetsSincePacket<250) sentResetsSincePacket++;
}
}
}
}
#pragma GCC pop_options
// Wait until there is no packet pending, then make this pending
void DCCWaveform::schedulePacket(const byte buffer[], byte byteCount, byte repeats) {
@@ -191,90 +270,88 @@ void DCCWaveform::schedulePacket(const byte buffer[], byte byteCount, byte repea
pendingLength = byteCount + 1;
pendingRepeats = repeats;
packetPending = true;
clearResets();
sentResetsSincePacket=0;
}
bool DCCWaveform::getPacketPending() {
return packetPending;
// Operations applicable to PROG track ONLY.
// (yes I know I could have subclassed the main track but...)
void DCCWaveform::setAckBaseline() {
if (isMainTrack) return;
int baseline=motorDriver->getCurrentRaw();
ackThreshold= baseline + motorDriver->mA2raw(ackLimitmA);
if (Diag::ACK) DIAG(F("ACK baseline=%d/%dmA Threshold=%d/%dmA Duration between %dus and %dus"),
baseline,motorDriver->raw2mA(baseline),
ackThreshold,motorDriver->raw2mA(ackThreshold),
minAckPulseDuration, maxAckPulseDuration);
}
#endif
#ifdef ARDUINO_ARCH_ESP32
#include "DCCWaveform.h"
#include "DCCACK.h"
DCCWaveform DCCWaveform::mainTrack(PREAMBLE_BITS_MAIN, true);
DCCWaveform DCCWaveform::progTrack(PREAMBLE_BITS_PROG, false);
RMTChannel *DCCWaveform::rmtMainChannel = NULL;
RMTChannel *DCCWaveform::rmtProgChannel = NULL;
DCCWaveform::DCCWaveform(byte preambleBits, bool isMain) {
isMainTrack = isMain;
requiredPreambles = preambleBits;
void DCCWaveform::setAckPending() {
if (isMainTrack) return;
ackMaxCurrent=0;
ackPulseStart=0;
ackPulseDuration=0;
ackDetected=false;
ackCheckStart=millis();
numAckSamples=0;
numAckGaps=0;
ackPending=true; // interrupt routines will now take note
}
void DCCWaveform::begin() {
for(const auto& md: TrackManager::getMainDrivers()) {
pinpair p = md->getSignalPin();
if(rmtMainChannel) {
//DIAG(F("added pins %d %d to MAIN channel"), p.pin, p.invpin);
rmtMainChannel->addPin(p); // add pin to existing main channel
} else {
//DIAG(F("new MAIN channel with pins %d %d"), p.pin, p.invpin);
rmtMainChannel = new RMTChannel(p, true); /* create new main channel */
byte DCCWaveform::getAck() {
if (ackPending) return (2); // still waiting
if (Diag::ACK) DIAG(F("%S after %dmS max=%d/%dmA pulse=%duS samples=%d gaps=%d"),ackDetected?F("ACK"):F("NO-ACK"), ackCheckDuration,
ackMaxCurrent,motorDriver->raw2mA(ackMaxCurrent), ackPulseDuration, numAckSamples, numAckGaps);
if (ackDetected) return (1); // Yes we had an ack
return(0); // pending set off but not detected means no ACK.
}
void DCCWaveform::checkAck() {
// This function operates in interrupt() time so must be fast and can't DIAG
if (sentResetsSincePacket > 6) { //ACK timeout
ackCheckDuration=millis()-ackCheckStart;
ackPending = false;
return;
}
}
MotorDriver *md = TrackManager::getProgDriver();
if (md) {
pinpair p = md->getSignalPin();
if (rmtProgChannel) {
//DIAG(F("added pins %d %d to PROG channel"), p.pin, p.invpin);
rmtProgChannel->addPin(p); // add pin to existing prog channel
} else {
//DIAG(F("new PROGchannel with pins %d %d"), p.pin, p.invpin);
rmtProgChannel = new RMTChannel(p, false);
int current=motorDriver->getCurrentRaw();
numAckSamples++;
if (current > ackMaxCurrent) ackMaxCurrent=current;
// An ACK is a pulse lasting between minAckPulseDuration and maxAckPulseDuration uSecs (refer @haba)
if (current>ackThreshold) {
if (trailingEdgeCounter > 0) {
numAckGaps++;
trailingEdgeCounter = 0;
}
if (ackPulseStart==0) ackPulseStart=micros(); // leading edge of pulse detected
return;
}
// not in pulse
if (ackPulseStart==0) return; // keep waiting for leading edge
// if we reach to this point, we have
// detected trailing edge of pulse
if (trailingEdgeCounter == 0) {
ackPulseDuration=micros()-ackPulseStart;
}
}
}
void DCCWaveform::schedulePacket(const byte buffer[], byte byteCount, byte repeats) {
if (byteCount > MAX_PACKET_SIZE) return; // allow for chksum
byte checksum = 0;
for (byte b = 0; b < byteCount; b++) {
checksum ^= buffer[b];
pendingPacket[b] = buffer[b];
}
// buffer is MAX_PACKET_SIZE but pendingPacket is one bigger
pendingPacket[byteCount] = checksum;
pendingLength = byteCount + 1;
pendingRepeats = repeats;
// The resets will be zero not only now but as well repeats packets into the future
clearResets(repeats+1);
{
int ret;
do {
if(isMainTrack) {
if (rmtMainChannel != NULL)
ret = rmtMainChannel->RMTfillData(pendingPacket, pendingLength, pendingRepeats);
} else {
if (rmtProgChannel != NULL)
ret = rmtProgChannel->RMTfillData(pendingPacket, pendingLength, pendingRepeats);
}
} while(ret > 0);
}
}
// but we do not trust it yet and return (which will force another
// measurement) and first the third time around with low current
// the ack detection will be finalized.
if (trailingEdgeCounter < 2) {
trailingEdgeCounter++;
return;
}
trailingEdgeCounter = 0;
bool DCCWaveform::getPacketPending() {
if(isMainTrack) {
if (rmtMainChannel == NULL)
return true;
return rmtMainChannel->busy();
} else {
if (rmtProgChannel == NULL)
return true;
return rmtProgChannel->busy();
}
if (ackPulseDuration>=minAckPulseDuration && ackPulseDuration<=maxAckPulseDuration) {
ackCheckDuration=millis()-ackCheckStart;
ackDetected=true;
ackPending=false;
transmitRepeats=0; // shortcut remaining repeat packets
return; // we have a genuine ACK result
}
ackPulseStart=0; // We have detected a too-short or too-long pulse so ignore and wait for next leading edge
}
void IRAM_ATTR DCCWaveform::loop() {
DCCACK::checkAck(progTrack.getResets());
}
#endif

View File

@@ -1,12 +1,8 @@
/*
* © 2021 M Steve Todd
* © 2021 Mike S
* © 2021 Fred Decker
* © 2020-2021 Harald Barth
* © 2020-2021 Chris Harlow
* All rights reserved.
* © 2020, Chris Harlow. All rights reserved.
* © 2020, Harald Barth.
*
* This file is part of CommandStation-EX
* This file is part of Asbelos DCC API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,70 +21,107 @@
#define DCCWaveform_h
#include "MotorDriver.h"
#ifdef ARDUINO_ARCH_ESP32
#include "DCCRMT.h"
#include "TrackManager.h"
#endif
// Wait times for power management. Unit: milliseconds
const int POWER_SAMPLE_ON_WAIT = 100;
const int POWER_SAMPLE_OFF_WAIT = 1000;
const int POWER_SAMPLE_OVERLOAD_WAIT = 20;
// Number of preamble bits.
const int PREAMBLE_BITS_MAIN = 16;
const int PREAMBLE_BITS_PROG = 22;
const byte MAX_PACKET_SIZE = 5; // NMRA standard extended packets, payload size WITHOUT checksum.
// The WAVE_STATE enum is deliberately numbered because a change of order would be catastrophic
// to the transform array.
enum WAVE_STATE : byte {WAVE_START=0,WAVE_MID_1=1,WAVE_HIGH_0=2,WAVE_MID_0=3,WAVE_LOW_0=4,WAVE_PENDING=5};
// NOTE: static functions are used for the overall controller, then
// one instance is created for each track.
enum class POWERMODE : byte { OFF, ON, OVERLOAD };
const byte idlePacket[] = {0xFF, 0x00, 0xFF};
const byte resetPacket[] = {0x00, 0x00, 0x00};
class DCCWaveform {
public:
DCCWaveform( byte preambleBits, bool isMain);
static void begin();
static void loop();
static void begin(MotorDriver * mainDriver, MotorDriver * progDriver);
static void loop(bool ackManagerActive);
static DCCWaveform mainTrack;
static DCCWaveform progTrack;
inline void clearRepeats() { transmitRepeats=0; }
#ifndef ARDUINO_ARCH_ESP32
inline void clearResets() { sentResetsSincePacket=0; }
inline byte getResets() { return sentResetsSincePacket; }
#else
// extrafudge is added when we know that the resets will first come extrafudge packets in the future
inline void clearResets(byte extrafudge=0) {
if ((isMainTrack ? rmtMainChannel : rmtProgChannel) == NULL) return;
resetPacketBase = isMainTrack ? rmtMainChannel->packetCount() : rmtProgChannel->packetCount();
resetPacketBase += extrafudge;
};
inline byte getResets() {
if ((isMainTrack ? rmtMainChannel : rmtProgChannel) == NULL) return 0;
uint32_t packetcount = isMainTrack ?
rmtMainChannel->packetCount() : rmtProgChannel->packetCount();
uint32_t count = packetcount - resetPacketBase; // Beware of unsigned interger arithmetic.
if (count > UINT32_MAX/2) // we are in the extrafudge area
return 0;
if (count > 255) // cap to 255
return 255;
return count; // all special cases handled above
};
#endif
void beginTrack();
void setPowerMode(POWERMODE);
POWERMODE getPowerMode();
void checkPowerOverload(bool ackManagerActive);
inline int get1024Current() {
if (powerMode == POWERMODE::ON)
return (int)(lastCurrent*(long int)1024/motorDriver->getRawCurrentTripValue());
return 0;
}
inline int getCurrentmA() {
if (powerMode == POWERMODE::ON)
return motorDriver->raw2mA(lastCurrent);
return 0;
}
inline int getMaxmA() {
if (maxmA == 0) { //only calculate this for first request, it doesn't change
maxmA = motorDriver->raw2mA(motorDriver->getRawCurrentTripValue()); //TODO: replace with actual max value or calc
}
return maxmA;
}
inline int getTripmA() {
if (tripmA == 0) { //only calculate this for first request, it doesn't change
tripmA = motorDriver->raw2mA(motorDriver->getRawCurrentTripValue());
}
return tripmA;
}
void schedulePacket(const byte buffer[], byte byteCount, byte repeats);
bool getPacketPending();
private:
#ifndef ARDUINO_ARCH_ESP32
volatile bool packetPending;
volatile byte sentResetsSincePacket;
#else
volatile uint32_t resetPacketBase;
#endif
volatile bool autoPowerOff=false;
void setAckBaseline(); //prog track only
void setAckPending(); //prog track only
byte getAck(); //prog track only 0=NACK, 1=ACK 2=keep waiting
static bool progTrackSyncMain; // true when prog track is a siding switched to main
static bool progTrackBoosted; // true when prog track is not current limited
inline void doAutoPowerOff() {
if (autoPowerOff) {
setPowerMode(POWERMODE::OFF);
autoPowerOff=false;
}
};
inline bool canMeasureCurrent() {
return motorDriver->canMeasureCurrent();
};
inline void setAckLimit(int mA) {
ackLimitmA = mA;
}
inline void setMinAckPulseDuration(unsigned int i) {
minAckPulseDuration = i;
}
inline void setMaxAckPulseDuration(unsigned int i) {
maxAckPulseDuration = i;
}
private:
// For each state of the wave nextState=stateTransform[currentState]
static const WAVE_STATE stateTransform[6];
// For each state of the wave, signal pin is HIGH or LOW
static const bool signalTransform[6];
static void interruptHandler();
void interrupt2();
void checkAck();
bool isMainTrack;
MotorDriver* motorDriver;
// Transmission controller
byte transmitPacket[MAX_PACKET_SIZE+1]; // +1 for checksum
byte transmitLength;
@@ -101,9 +134,38 @@ class DCCWaveform {
byte pendingPacket[MAX_PACKET_SIZE+1]; // +1 for checksum
byte pendingLength;
byte pendingRepeats;
#ifdef ARDUINO_ARCH_ESP32
static RMTChannel *rmtMainChannel;
static RMTChannel *rmtProgChannel;
#endif
int lastCurrent;
static int progTripValue;
int maxmA;
int tripmA;
// current sampling
POWERMODE powerMode;
unsigned long lastSampleTaken;
unsigned int sampleDelay;
// Trip current for programming track, 250mA. Change only if you really
// need to be non-NMRA-compliant because of decoders that are not either.
static const int TRIP_CURRENT_PROG=250;
unsigned long power_sample_overload_wait = POWER_SAMPLE_OVERLOAD_WAIT;
unsigned int power_good_counter = 0;
// ACK management (Prog track only)
volatile bool ackPending;
volatile bool ackDetected;
int ackThreshold;
int ackLimitmA = 60;
int ackMaxCurrent;
unsigned long ackCheckStart; // millis
unsigned int ackCheckDuration; // millis
unsigned int ackPulseDuration; // micros
unsigned long ackPulseStart; // micros
unsigned int minAckPulseDuration = 4000; // micros
unsigned int maxAckPulseDuration = 8500; // micros
volatile static uint8_t numAckGaps;
volatile static uint8_t numAckSamples;
static uint8_t trailingEdgeCounter;
};
#endif

14
DIAG.h
View File

@@ -1,9 +1,7 @@
/*
* © 2021 Fred Decker
* © 2020 Chris Harlow
* All rights reserved.
* © 2020, Chris Harlow. All rights reserved.
*
* This file is part of CommandStation-EX
* This file is part of Asbelos DCC API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,11 +16,15 @@
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef DIAG_h
#define DIAG_h
#include "StringFormatter.h"
#define DIAG StringFormatter::diag
#include "DiagLogger.h"
// #define DIAG StringFormatter::diag // Std logging to serial only
#define DIAG DiagLogger::get().diag // allows to add other log writers
#define LCD StringFormatter::lcd
#define SCREEN StringFormatter::lcd2
#endif

63
DiagLogger.cpp Normal file
View File

@@ -0,0 +1,63 @@
/*
* © 2021, Gregor Baues, All rights reserved.
*
* This file is part of DCC-EX/CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*
*/
#include "DiagLogger.h"
// DIAG.h the #define DIAG points to here ...
// EthernetSetup , Wifisetup, etc can register a function to be called allowing the channel
// to publish the diag info to
// serial is default end enabled all the time
DiagLogger DiagLogger::singleton; // static instantiation;
void DiagLogger::addDiagWriter(DiagWriter l) {
if ( registered == MAXWRITERS ) {
Serial.println("Error: Max amount of writers exceeded.");
return;
}
writers[registered] = l;
registered++;
}
void DiagLogger::diag(const FSH *input,...)
{
va_list args;
va_start(args, input);
int len = 0;
len += sprintf(&b1[len], "<* ");
len += vsprintf_P(&b1[len], (const char *)input, args);
len += sprintf(&b1[len], " *>\n");
if ( len >= 256 ) { Serial.print("ERROR : Diag Buffer overflow"); return; }
// allways print to Serial
Serial.print(b1);
// callback the other registered diag writers
for (size_t i = 0; i < (size_t) registered; i++)
{
writers[i](b1, len);
}
va_end(args);
}

59
DiagLogger.h Normal file
View File

@@ -0,0 +1,59 @@
/*
* © 2021, Gregor Baues, All rights reserved.
*
* This file is part of DCC-EX/CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*
*/
#ifndef DiagLogger_h
#define DiagLogger_h
#include "StringFormatter.h"
#define MAXWRITERS 10
typedef void (*DiagWriter)(const char *msg, const int length);
class DiagLogger
{
private:
// Methods
DiagLogger() = default;
DiagLogger(const DiagLogger &); // non construction-copyable
DiagLogger &operator=(const DiagLogger &); // non copyable
// Members
static DiagLogger singleton; // unique instance of the MQTTInterface object
DiagWriter writers[MAXWRITERS];
int registered = 0; // number of registered writers ( Serial is not counted as always used )
char b1[256];
public:
// Methods
static DiagLogger &get() noexcept
{ // return a reference to the unique instance
return singleton;
}
void diag(const FSH *input...);
void addDiagWriter(DiagWriter l);
~DiagLogger() = default;
// Members
};
#endif

View File

@@ -1,219 +0,0 @@
/*
* © 2021, Chris Harlow, Neil McKechnie. All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
// CAUTION: the device dependent parts of this class are created in the .ini
// using LCD_Implementation.h
/* The strategy for drawing the screen is as follows.
* 1) There are up to eight rows of text to be displayed.
* 2) Blank rows of text are ignored.
* 3) If there are more non-blank rows than screen lines,
* then all of the rows are displayed, with the rest of the
* screen being blank.
* 4) If there are fewer non-blank rows than screen lines,
* then a scrolling strategy is adopted so that, on each screen
* refresh, a different subset of the rows is presented.
* 5) On each entry into loop2(), a single operation is sent to the
* screen; this may be a position command or a character for
* display. This spreads the onerous work of updating the screen
* and ensures that other loop() functions in the application are
* not held up significantly. The exception to this is when
* the loop2() function is called with force=true, where
* a screen update is executed to completion. This is normally
* only done during start-up.
* The scroll mode is selected by defining SCROLLMODE as 0, 1 or 2
* in the config.h.
* #define SCROLLMODE 0 is scroll continuous (fill screen if poss),
* #define SCROLLMODE 1 is by page (alternate between pages),
* #define SCROLLMODE 2 is by row (move up 1 row at a time).
*/
#include "Display.h"
// Constructor - allocates device driver.
Display::Display(DisplayDevice *deviceDriver) {
_deviceDriver = deviceDriver;
// Get device dimensions in characters (e.g. 16x2).
numScreenColumns = _deviceDriver->getNumCols();
numScreenRows = _deviceDriver->getNumRows();
for (uint8_t row = 0; row < MAX_CHARACTER_ROWS; row++)
rowBuffer[row][0] = '\0';
addDisplay(0); // Add this display as display number 0
};
void Display::begin() {
_deviceDriver->begin();
_deviceDriver->clearNative();
}
void Display::_clear() {
_deviceDriver->clearNative();
for (uint8_t row = 0; row < MAX_CHARACTER_ROWS; row++)
rowBuffer[row][0] = '\0';
}
void Display::_setRow(uint8_t line) {
hotRow = line;
hotCol = 0;
rowBuffer[hotRow][0] = '\0'; // Clear existing text
}
size_t Display::_write(uint8_t b) {
if (hotRow >= MAX_CHARACTER_ROWS || hotCol >= MAX_CHARACTER_COLS) return -1;
rowBuffer[hotRow][hotCol] = b;
hotCol++;
rowBuffer[hotRow][hotCol] = '\0';
return 1;
}
// Refresh screen completely (will block until complete). Used
// during start-up.
void Display::_refresh() {
loop2(true);
}
// On normal loop entries, loop will only make one output request on each
// entry, to avoid blocking while waiting for the I2C.
void Display::_displayLoop() {
// If output device is busy, don't do anything on this loop
// This avoids blocking while waiting for the device to complete.
if (!_deviceDriver->isBusy()) loop2(false);
}
Display *Display::loop2(bool force) {
unsigned long currentMillis = millis();
if (!force) {
// See if we're in the time between updates
if ((currentMillis - lastScrollTime) < DISPLAY_SCROLL_TIME)
return NULL;
} else {
// force full screen update from the beginning.
rowFirst = 0;
rowCurrent = 0;
bufferPointer = 0;
noMoreRowsToDisplay = false;
slot = 0;
}
do {
if (bufferPointer == 0) {
// Search for non-blank row
while (!noMoreRowsToDisplay) {
if (!isCurrentRowBlank()) break;
moveToNextRow();
if (rowCurrent == rowFirst) noMoreRowsToDisplay = true;
}
if (noMoreRowsToDisplay) {
// No non-blank lines left, so draw blank line
buffer[0] = '\0';
} else {
// Non-blank line found, so copy it (including terminator)
for (uint8_t i = 0; i <= MAX_CHARACTER_COLS; i++)
buffer[i] = rowBuffer[rowCurrent][i];
}
_deviceDriver->setRowNative(slot); // Set position for display
charIndex = 0;
bufferPointer = &buffer[0];
} else {
// Write next character, or a space to erase current position.
char ch = *bufferPointer;
if (ch) {
_deviceDriver->writeNative(ch);
bufferPointer++;
} else {
_deviceDriver->writeNative(' ');
}
if (++charIndex >= MAX_CHARACTER_COLS) {
// Screen slot completed, move to next nonblank row
bufferPointer = 0;
for (;;) {
moveToNextRow();
if (rowCurrent == rowFirst) {
noMoreRowsToDisplay = true;
break;
}
if (!isCurrentRowBlank()) break;
}
// Move to next screen slot, if available
slot++;
if (slot >= numScreenRows) {
// Last slot on screen written, so get ready for next screen update.
#if SCROLLMODE==0
// Scrollmode 0 scrolls continuously. If the rows fit on the screen,
// then restart at row 0, but otherwise continue with the row
// after the last one displayed.
if (countNonBlankRows() <= numScreenRows)
rowCurrent = 0;
rowFirst = rowCurrent;
#elif SCROLLMODE==1
// Scrollmode 1 scrolls by page, so if the last page has just completed then
// next time restart with row 0.
if (noMoreRowsToDisplay)
rowFirst = rowCurrent = 0;
#else
// Scrollmode 2 scrolls by row. If the rows don't fit on the screen,
// then start one row further on next time. If they do fit, then
// show them in order and start next page at row 0.
if (countNonBlankRows() <= numScreenRows) {
rowFirst = rowCurrent = 0;
} else {
// Find first non-blank row after the previous first row
rowCurrent = rowFirst;
do {
moveToNextRow();
} while (isCurrentRowBlank());
rowFirst = rowCurrent;
}
#endif
noMoreRowsToDisplay = false;
slot = 0;
lastScrollTime = currentMillis;
return NULL;
}
}
}
} while (force);
return NULL;
}
bool Display::isCurrentRowBlank() {
return (rowBuffer[rowCurrent][0] == '\0');
}
void Display::moveToNextRow() {
// Skip blank rows
if (++rowCurrent >= MAX_CHARACTER_ROWS)
rowCurrent = 0;
}
uint8_t Display::countNonBlankRows() {
uint8_t count = 0;
for (uint8_t rowNumber=0; rowNumber<MAX_CHARACTER_ROWS; rowNumber++) {
if (rowBuffer[rowNumber][0] != '\0')
count++;
}
return count;
}

View File

@@ -1,77 +0,0 @@
/*
* © 2021, Chris Harlow, Neil McKechnie. All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef Display_h
#define Display_h
#include <Arduino.h>
#include "defines.h"
#include "DisplayInterface.h"
// Allow maximum message length to be overridden from config.h
#if !defined(MAX_MSG_SIZE)
#define MAX_MSG_SIZE 20
#endif
// Set default scroll mode (overridable in config.h)
#if !defined(SCROLLMODE)
#define SCROLLMODE 1
#endif
// This class is created in Display_Implementation.h
class Display : public DisplayInterface {
public:
Display(DisplayDevice *deviceDriver);
static const int MAX_CHARACTER_ROWS = 8;
static const int MAX_CHARACTER_COLS = MAX_MSG_SIZE;
static const long DISPLAY_SCROLL_TIME = 3000; // 3 seconds
private:
DisplayDevice *_deviceDriver;
unsigned long lastScrollTime = 0;
uint8_t hotRow = 0;
uint8_t hotCol = 0;
uint8_t slot = 0;
uint8_t rowFirst = 0;
uint8_t rowCurrent = 0;
uint8_t charIndex = 0;
char buffer[MAX_CHARACTER_COLS + 1];
char* bufferPointer = 0;
bool noMoreRowsToDisplay = false;
uint16_t numScreenRows;
uint16_t numScreenColumns = MAX_CHARACTER_COLS;
char rowBuffer[MAX_CHARACTER_ROWS][MAX_CHARACTER_COLS+1];
public:
void begin() override;
void _clear() override;
void _setRow(uint8_t line) override;
size_t _write(uint8_t b) override;
void _refresh() override;
void _displayLoop() override;
Display *loop2(bool force);
bool findNonBlankRow();
bool isCurrentRowBlank();
void moveToNextRow();
uint8_t countNonBlankRows();
};
#endif

View File

@@ -1,99 +0,0 @@
/*
* © 2021 Neil McKechnie
* © 2021 Chris Harlow
* All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef DisplayInterface_h
#define DisplayInterface_h
#include <Arduino.h>
// Definition of base class for displays. The base class does nothing.
class DisplayInterface : public Print {
protected:
static DisplayInterface *_displayHandler;
static uint8_t _selectedDisplayNo; // Nothing selected.
DisplayInterface *_nextHandler = NULL;
uint8_t _displayNo = 0;
public:
// Add display object to list of displays
void addDisplay(uint8_t displayNo) {
_nextHandler = _displayHandler;
_displayHandler = this;
_displayNo = displayNo;
}
static DisplayInterface *getDisplayHandler() {
return _displayHandler;
}
uint8_t getDisplayNo() {
return _displayNo;
}
// The next functions are to provide compatibility with calls to the LCD function
// which does not specify a display number. These always apply to display '0'.
static void refresh() { refresh(0); };
static void setRow(uint8_t line) { setRow(0, line); };
static void clear() { clear(0); };
// Additional functions to support multiple displays. These perform a
// multicast to all displays that match the selected displayNo.
// Display number zero is the default one.
static void setRow(uint8_t displayNo, uint8_t line) {
_selectedDisplayNo = displayNo;
for (DisplayInterface *p = _displayHandler; p!=0; p=p->_nextHandler) {
if (displayNo == p->_displayNo) p->_setRow(line);
}
}
size_t write (uint8_t c) override {
for (DisplayInterface *p = _displayHandler; p!=0; p=p->_nextHandler)
if (_selectedDisplayNo == p->_displayNo) p->_write(c);
return _displayHandler ? 1 : 0;
}
static void clear(uint8_t displayNo) {
for (DisplayInterface *p = _displayHandler; p!=0; p=p->_nextHandler)
if (displayNo == p->_displayNo) p->_clear();
}
static void refresh(uint8_t displayNo) {
for (DisplayInterface *p = _displayHandler; p!=0; p=p->_nextHandler)
if (displayNo == p->_displayNo) p->_refresh();
}
static void loop() {
for (DisplayInterface *p = _displayHandler; p!=0; p=p->_nextHandler)
p->_displayLoop();
};
// The following are overridden within the specific device class
virtual void begin() {};
virtual size_t _write(uint8_t c) { (void)c; return 0; };
virtual void _setRow(uint8_t line) { (void)line; }
virtual void _clear() {}
virtual void _refresh() {}
virtual void _displayLoop() {}
};
class DisplayDevice {
public:
virtual bool begin() { return true; }
virtual void clearNative() = 0;
virtual void setRowNative(uint8_t line) = 0;
virtual size_t writeNative(uint8_t c) = 0;
virtual bool isBusy() = 0;
virtual uint16_t getNumRows() = 0;
virtual uint16_t getNumCols() = 0;
};
#endif

View File

@@ -1,12 +1,9 @@
/*
* © 2021 Neil McKechnie
* © 2021 Fred Decker
* © 2020-2022 Harald Barth
* © 2020-2021 Chris Harlow
* © 2013-2016 Gregg E. Berman
* All rights reserved.
* © 2020, Chris Harlow. All rights reserved.
* © 2020, Harald Barth.
*
* This file is part of CommandStation-EX
* This file is part of Asbelos DCC API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,92 +18,89 @@
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#include "defines.h"
#ifndef DISABLE_EEPROM
#include "EEStore.h"
#include "DIAG.h"
#include "Outputs.h"
#include "Sensors.h"
#include "Turnouts.h"
#include "Sensors.h"
#include "Outputs.h"
#include "DIAG.h"
#if defined(ARDUINO_ARCH_SAMC)
#if defined(ARDUINO_ARCH_SAMD)
ExternalEEPROM EEPROM;
#endif
void EEStore::init() {
#if defined(ARDUINO_ARCH_SAMC)
EEPROM.begin(0x50); // Address for Microchip 24-series EEPROM with all three
// A pins grounded (0b1010000 = 0x50)
void EEStore::init(){
#if defined(ARDUINO_ARCH_SAMD)
EEPROM.begin(0x50); // Address for Microchip 24-series EEPROM with all three A pins grounded (0b1010000 = 0x50)
#endif
eeStore = (EEStore *)calloc(1, sizeof(EEStore));
eeStore=(EEStore *)calloc(1,sizeof(EEStore));
EEPROM.get(0,eeStore->data); // get eeStore data
EEPROM.get(0, eeStore->data); // get eeStore data
if(strncmp(eeStore->data.id,EESTORE_ID,sizeof(EESTORE_ID))!=0){ // check to see that eeStore contains valid DCC++ ID
sprintf(eeStore->data.id,EESTORE_ID); // if not, create blank eeStore structure (no turnouts, no sensors) and save it back to EEPROM
eeStore->data.nTurnouts=0;
eeStore->data.nSensors=0;
eeStore->data.nOutputs=0;
EEPROM.put(0,eeStore->data);
}
// check to see that eeStore contains valid DCC++ ID
if (strncmp(eeStore->data.id, EESTORE_ID, sizeof(EESTORE_ID)) != 0) {
// if not, create blank eeStore structure (no
// turnouts, no sensors) and save it back to EEPROM
strncpy(eeStore->data.id, EESTORE_ID, sizeof(EESTORE_ID)+0);
eeStore->data.nTurnouts = 0;
eeStore->data.nSensors = 0;
eeStore->data.nOutputs = 0;
EEPROM.put(0, eeStore->data);
}
reset(); // set memory pointer to first free EEPROM space
Turnout::load(); // load turnout definitions
Sensor::load(); // load sensor definitions
Output::load(); // load output definitions
reset(); // set memory pointer to first free EEPROM space
Turnout::load(); // load turnout definitions
Sensor::load(); // load sensor definitions
Output::load(); // load output definitions
}
///////////////////////////////////////////////////////////////////////////////
void EEStore::clear() {
sprintf(eeStore->data.id,
EESTORE_ID); // create blank eeStore structure (no turnouts, no
// sensors) and save it back to EEPROM
eeStore->data.nTurnouts = 0;
eeStore->data.nSensors = 0;
eeStore->data.nOutputs = 0;
EEPROM.put(0, eeStore->data);
void EEStore::clear(){
sprintf(eeStore->data.id,EESTORE_ID); // create blank eeStore structure (no turnouts, no sensors) and save it back to EEPROM
eeStore->data.nTurnouts=0;
eeStore->data.nSensors=0;
eeStore->data.nOutputs=0;
EEPROM.put(0,eeStore->data);
}
///////////////////////////////////////////////////////////////////////////////
void EEStore::store() {
reset();
Turnout::store();
Sensor::store();
Output::store();
EEPROM.put(0, eeStore->data);
DIAG(F("EEPROM used: %d/%d bytes"), EEStore::pointer(), EEPROM.length());
void EEStore::store(){
reset();
Turnout::store();
Sensor::store();
Output::store();
EEPROM.put(0,eeStore->data);
}
///////////////////////////////////////////////////////////////////////////////
void EEStore::advance(int n) { eeAddress += n; }
void EEStore::advance(int n){
eeAddress+=n;
}
///////////////////////////////////////////////////////////////////////////////
void EEStore::reset() { eeAddress = sizeof(EEStore); }
void EEStore::reset(){
eeAddress=sizeof(EEStore);
}
///////////////////////////////////////////////////////////////////////////////
int EEStore::pointer() { return (eeAddress); }
int EEStore::pointer(){
return(eeAddress);
}
///////////////////////////////////////////////////////////////////////////////
void EEStore::dump(int num) {
byte b = 0;
DIAG(F("Addr 0x char"));
for (int n = 0; n < num; n++) {
EEPROM.get(n, b);
DIAG(F("%d %x %c"), n, b, isprint(b) ? b : ' ');
}
byte b;
DIAG(F("Addr 0x char"));
for (int n=0 ; n<num; n++) {
EEPROM.get(n, b);
DIAG(F("%d %x %c"),n,b,isprint(b) ? b : ' ');
}
}
///////////////////////////////////////////////////////////////////////////////
EEStore *EEStore::eeStore = NULL;
int EEStore::eeAddress = 0;
#endif
EEStore *EEStore::eeStore=NULL;
int EEStore::eeAddress=0;

View File

@@ -1,45 +1,22 @@
/*
* © 2021 Neil McKechnie
* © 2021 Fred Decker
* © 2020-2021 Harald Barth
* © 2020 Chris Harlow
* All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef DISABLE_EEPROM
#ifndef EEStore_h
#define EEStore_h
#include <Arduino.h>
#if defined(ARDUINO_ARCH_SAMC)
#if defined(ARDUINO_ARCH_SAMD)
#include <SparkFun_External_EEPROM.h>
extern ExternalEEPROM EEPROM;
#else
#include <EEPROM.h>
#endif
#define EESTORE_ID "DCC++1"
#define EESTORE_ID "DCC++"
struct EEStoreData{
char id[sizeof(EESTORE_ID)];
uint16_t nTurnouts;
uint16_t nSensors;
uint16_t nOutputs;
int nTurnouts;
int nSensors;
int nOutputs;
};
struct EEStore{
@@ -56,4 +33,3 @@ struct EEStore{
};
#endif
#endif // DISABLE_EEPROM

Binary file not shown.

View File

@@ -1,23 +0,0 @@
#ifndef EXRAIL_H
#define EXRAIL_H
#if defined(EXRAIL_ACTIVE)
#include "EXRAIL2.h"
class RMFT {
public:
static void inline begin() {RMFT2::begin();}
static void inline loop() {RMFT2::loop();}
};
#include "EXRAILMacros.h"
#else
// Dummy RMFT
class RMFT {
public:
static void inline begin() {}
static void inline loop() {}
};
#endif
#endif

File diff suppressed because it is too large Load Diff

209
EXRAIL2.h
View File

@@ -1,209 +0,0 @@
/*
* © 2021 Neil McKechnie
* © 2020-2022 Chris Harlow
* © 2022 Colin Murdoch
* © 2023 Harald Barth
* All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef EXRAIL2_H
#define EXRAIL2_H
#include "FSH.h"
#include "IODevice.h"
#include "Turnouts.h"
// The following are the operation codes (or instructions) for a kind of virtual machine.
// Each instruction is normally 3 bytes long with an operation code followed by a parameter.
// In cases where more than one parameter is required, the first parameter is followed by one
// or more OPCODE_PAD instructions with the subsequent parameters. This wastes a byte but makes
// searching easier as a parameter can never be confused with an opcode.
//
enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,
OPCODE_FWD,OPCODE_REV,OPCODE_SPEED,OPCODE_INVERT_DIRECTION,
OPCODE_RESERVE,OPCODE_FREE,
OPCODE_AT,OPCODE_AFTER,OPCODE_AUTOSTART,
OPCODE_ATGTE,OPCODE_ATLT,
OPCODE_ATTIMEOUT1,OPCODE_ATTIMEOUT2,
OPCODE_LATCH,OPCODE_UNLATCH,OPCODE_SET,OPCODE_RESET,
OPCODE_ENDIF,OPCODE_ELSE,
OPCODE_DELAY,OPCODE_DELAYMINS,OPCODE_DELAYMS,OPCODE_RANDWAIT,
OPCODE_FON,OPCODE_FOFF,OPCODE_XFON,OPCODE_XFOFF,
OPCODE_RED,OPCODE_GREEN,OPCODE_AMBER,OPCODE_DRIVE,
OPCODE_SERVO,OPCODE_SIGNAL,OPCODE_TURNOUT,OPCODE_WAITFOR,
OPCODE_PAD,OPCODE_FOLLOW,OPCODE_CALL,OPCODE_RETURN,
OPCODE_JOIN,OPCODE_UNJOIN,OPCODE_READ_LOCO1,OPCODE_READ_LOCO2,OPCODE_POM,
OPCODE_START,OPCODE_SETLOCO,OPCODE_SENDLOCO,OPCODE_FORGET,
OPCODE_PAUSE, OPCODE_RESUME,OPCODE_POWEROFF,OPCODE_POWERON,
OPCODE_ONCLOSE, OPCODE_ONTHROW, OPCODE_SERVOTURNOUT, OPCODE_PINTURNOUT,
OPCODE_PRINT,OPCODE_DCCACTIVATE,
OPCODE_ONACTIVATE,OPCODE_ONDEACTIVATE,
OPCODE_ROSTER,OPCODE_KILLALL,
OPCODE_ROUTE,OPCODE_AUTOMATION,OPCODE_SEQUENCE,
OPCODE_ENDTASK,OPCODE_ENDEXRAIL,
OPCODE_SET_TRACK,
OPCODE_ONRED,OPCODE_ONAMBER,OPCODE_ONGREEN,
OPCODE_ONCHANGE,
OPCODE_ONCLOCKTIME,
OPCODE_ONTIME,
// OPcodes below this point are skip-nesting IF operations
// placed here so that they may be skipped as a group
// see skipIfBlock()
IF_TYPE_OPCODES, // do not move this...
OPCODE_IFRED,OPCODE_IFAMBER,OPCODE_IFGREEN,
OPCODE_IFGTE,OPCODE_IFLT,
OPCODE_IFTIMEOUT,
OPCODE_IF,OPCODE_IFNOT,
OPCODE_IFRANDOM,OPCODE_IFRESERVE,
OPCODE_IFCLOSED,OPCODE_IFTHROWN,
OPCODE_IFRE,
OPCODE_IFLOCO
};
// Ensure thrunge_lcd is put last as there may be more than one display,
// sequentially numbered from thrunge_lcd.
enum thrunger: byte {
thrunge_print, thrunge_broadcast, thrunge_withrottle,
thrunge_serial,thrunge_parse,
thrunge_serial1, thrunge_serial2, thrunge_serial3,
thrunge_serial4, thrunge_serial5, thrunge_serial6,
thrunge_lcn,
thrunge_lcd, // Must be last!!
};
// Flag bits for status of hardware and TPL
static const byte SECTION_FLAG = 0x80;
static const byte LATCH_FLAG = 0x40;
static const byte TASK_FLAG = 0x20;
static const byte SPARE_FLAG = 0x10;
static const byte SIGNAL_MASK = 0x0C;
static const byte SIGNAL_RED = 0x08;
static const byte SIGNAL_AMBER = 0x0C;
static const byte SIGNAL_GREEN = 0x04;
static const byte MAX_STACK_DEPTH=4;
static const short MAX_FLAGS=256;
#define FLAGOVERFLOW(x) x>=MAX_FLAGS
class LookList {
public:
LookList(int16_t size);
void add(int16_t lookup, int16_t result);
int16_t find(int16_t value);
private:
int16_t m_size;
int16_t m_loaded;
int16_t * m_lookupArray;
int16_t * m_resultArray;
};
class RMFT2 {
public:
static void begin();
static void loop();
RMFT2(int progCounter);
RMFT2(int route, uint16_t cab);
~RMFT2();
static void readLocoCallback(int16_t cv);
static void createNewTask(int route, uint16_t cab);
static void turnoutEvent(int16_t id, bool closed);
static void activateEvent(int16_t addr, bool active);
static void changeEvent(int16_t id, bool change);
static void clockEvent(int16_t clocktime, bool change);
static const int16_t SERVO_SIGNAL_FLAG=0x4000;
static const int16_t ACTIVE_HIGH_SIGNAL_FLAG=0x2000;
static const int16_t DCC_SIGNAL_FLAG=0x1000;
static const int16_t SIGNAL_ID_MASK=0x0FFF;
// Throttle Info Access functions built by exrail macros
static const byte rosterNameCount;
static const int16_t HIGHFLASH routeIdList[];
static const int16_t HIGHFLASH automationIdList[];
static const int16_t HIGHFLASH rosterIdList[];
static const FSH * getRouteDescription(int16_t id);
static char getRouteType(int16_t id);
static const FSH * getTurnoutDescription(int16_t id);
static const FSH * getRosterName(int16_t id);
static const FSH * getRosterFunctions(int16_t id);
private:
static void ComandFilter(Print * stream, byte & opcode, byte & paramCount, int16_t p[]);
static bool parseSlash(Print * stream, byte & paramCount, int16_t p[]) ;
static void streamFlags(Print* stream);
static bool setFlag(VPIN id,byte onMask, byte OffMask=0);
static bool getFlag(VPIN id,byte mask);
static int16_t progtrackLocoId;
static void doSignal(int16_t id,char rag);
static bool isSignal(int16_t id,char rag);
static int16_t getSignalSlot(int16_t id);
static void setTurnoutHiddenState(Turnout * t);
static LookList* LookListLoader(OPCODE op1,
OPCODE op2=OPCODE_ENDEXRAIL,OPCODE op3=OPCODE_ENDEXRAIL);
static void handleEvent(const FSH* reason,LookList* handlers, int16_t id);
static uint16_t getOperand(int progCounter,byte n);
static RMFT2 * loopTask;
static RMFT2 * pausingTask;
void delayMe(long millisecs);
void driveLoco(byte speedo);
bool readSensor(uint16_t sensorId);
bool skipIfBlock();
bool readLoco();
void loop2();
void kill(const FSH * reason=NULL,int operand=0);
void printMessage(uint16_t id); // Built by RMFTMacros.h
void printMessage2(const FSH * msg);
void thrungeString(uint32_t strfar, thrunger mode, byte id=0);
uint16_t getOperand(byte n);
static bool diag;
static const HIGHFLASH byte RouteCode[];
static const HIGHFLASH int16_t SignalDefinitions[];
static byte flags[MAX_FLAGS];
static LookList * sequenceLookup;
static LookList * onThrowLookup;
static LookList * onCloseLookup;
static LookList * onActivateLookup;
static LookList * onDeactivateLookup;
static LookList * onRedLookup;
static LookList * onAmberLookup;
static LookList * onGreenLookup;
static LookList * onChangeLookup;
static LookList * onClockLookup;
// Local variables - exist for each instance/task
RMFT2 *next; // loop chain
int progCounter; // Byte offset of next route opcode in ROUTES table
unsigned long delayStart; // Used by opcodes that must be recalled before completing
unsigned long delayTime;
union {
unsigned long waitAfter; // Used by OPCODE_AFTER
unsigned long timeoutStart; // Used by OPCODE_ATTIMEOUT
};
bool timeoutFlag;
byte taskId;
uint16_t loco;
bool forward;
bool invert;
byte speedo;
int onEventStartPosition;
byte stackDepth;
int callStack[MAX_STACK_DEPTH];
};
#endif

View File

@@ -1,271 +0,0 @@
/*
* © 2020-2022 Chris Harlow. All rights reserved.
* © 2022 Colin Murdoch
* © 2023 Harald Barth
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
// This file cleans and resets the RMFT2 Macros.
// It is used between passes to reduce complexity in RMFT2Macros.h
// DO NOT add an include guard to this file.
// Undefine all RMFT macros
#undef ACTIVATE
#undef ACTIVATEL
#undef AFTER
#undef ALIAS
#undef AMBER
#undef ANOUT
#undef AT
#undef ATGTE
#undef ATLT
#undef ATTIMEOUT
#undef AUTOMATION
#undef AUTOSTART
#undef BROADCAST
#undef CALL
#undef CLOSE
#undef DCC_SIGNAL
#undef DEACTIVATE
#undef DEACTIVATEL
#undef DELAY
#undef DELAYMINS
#undef DELAYRANDOM
#undef DONE
#undef DRIVE
#undef ELSE
#undef ENDEXRAIL
#undef ENDIF
#undef ENDTASK
#undef ESTOP
#undef EXRAIL
#undef FADE
#undef FOFF
#undef FOLLOW
#undef FON
#undef FORGET
#undef FREE
#undef FWD
#undef GREEN
#undef HAL
#undef IF
#undef IFAMBER
#undef IFCLOSED
#undef IFGREEN
#undef IFGTE
#undef IFLOCO
#undef IFLT
#undef IFNOT
#undef IFRANDOM
#undef IFRED
#undef IFRESERVE
#undef IFTHROWN
#undef IFTIMEOUT
#undef IFRE
#undef INVERT_DIRECTION
#undef JOIN
#undef KILLALL
#undef LATCH
#undef LCD
#undef SCREEN
#undef LCN
#undef MOVETT
#undef ONACTIVATE
#undef ONACTIVATEL
#undef ONAMBER
#undef ONDEACTIVATE
#undef ONDEACTIVATEL
#undef ONCLOSE
#undef ONTIME
#undef ONCLOCKTIME
#undef ONCLOCKMINS
#undef ONGREEN
#undef ONRED
#undef ONTHROW
#undef ONCHANGE
#undef PARSE
#undef PAUSE
#undef PIN_TURNOUT
#undef PRINT
#undef POM
#undef POWEROFF
#undef POWERON
#undef READ_LOCO
#undef RED
#undef RESERVE
#undef RESET
#undef RESUME
#undef RETURN
#undef REV
#undef ROSTER
#undef ROUTE
#undef SENDLOCO
#undef SEQUENCE
#undef SERIAL
#undef SERIAL1
#undef SERIAL2
#undef SERIAL3
#undef SERIAL4
#undef SERIAL5
#undef SERIAL6
#undef SERVO
#undef SERVO2
#undef SERVO_TURNOUT
#undef SERVO_SIGNAL
#undef SET
#undef SET_TRACK
#undef SETLOCO
#undef SIGNAL
#undef SIGNALH
#undef SPEED
#undef START
#undef STOP
#undef THROW
#undef TURNOUT
#undef TURNOUTL
#undef UNJOIN
#undef UNLATCH
#undef VIRTUAL_SIGNAL
#undef VIRTUAL_TURNOUT
#undef WAITFOR
#undef WITHROTTLE
#undef XFOFF
#undef XFON
#ifndef RMFT2_UNDEF_ONLY
#define ACTIVATE(addr,subaddr)
#define ACTIVATEL(addr)
#define AFTER(sensor_id)
#define ALIAS(name,value...)
#define AMBER(signal_id)
#define ANOUT(vpin,value,param1,param2)
#define AT(sensor_id)
#define ATGTE(sensor_id,value)
#define ATLT(sensor_id,value)
#define ATTIMEOUT(sensor_id,timeout_ms)
#define AUTOMATION(id,description)
#define AUTOSTART
#define BROADCAST(msg)
#define CALL(route)
#define CLOSE(id)
#define DCC_SIGNAL(id,add,subaddr)
#define DEACTIVATE(addr,subaddr)
#define DEACTIVATEL(addr)
#define DELAY(mindelay)
#define DELAYMINS(mindelay)
#define DELAYRANDOM(mindelay,maxdelay)
#define DONE
#define DRIVE(analogpin)
#define ELSE
#define ENDEXRAIL
#define ENDIF
#define ENDTASK
#define ESTOP
#define EXRAIL
#define FADE(pin,value,ms)
#define FOFF(func)
#define FOLLOW(route)
#define FON(func)
#define FORGET
#define FREE(blockid)
#define FWD(speed)
#define GREEN(signal_id)
#define HAL(haltype,params...)
#define IF(sensor_id)
#define IFAMBER(signal_id)
#define IFCLOSED(turnout_id)
#define IFGREEN(signal_id)
#define IFGTE(sensor_id,value)
#define IFLOCO(loco_id)
#define IFLT(sensor_id,value)
#define IFNOT(sensor_id)
#define IFRANDOM(percent)
#define IFRED(signal_id)
#define IFTHROWN(turnout_id)
#define IFRESERVE(block)
#define IFTIMEOUT
#define IFRE(sensor_id,value)
#define INVERT_DIRECTION
#define JOIN
#define KILLALL
#define LATCH(sensor_id)
#define LCD(row,msg)
#define SCREEN(display,row,msg)
#define LCN(msg)
#define MOVETT(id,steps,activity)
#define ONACTIVATE(addr,subaddr)
#define ONACTIVATEL(linear)
#define ONAMBER(signal_id)
#define ONTIME(value)
#define ONCLOCKTIME(hours,mins)
#define ONCLOCKMINS(mins)
#define ONDEACTIVATE(addr,subaddr)
#define ONDEACTIVATEL(linear)
#define ONCLOSE(turnout_id)
#define ONGREEN(signal_id)
#define ONRED(signal_id)
#define ONTHROW(turnout_id)
#define ONCHANGE(sensor_id)
#define PAUSE
#define PIN_TURNOUT(id,pin,description...)
#define PRINT(msg)
#define PARSE(msg)
#define POM(cv,value)
#define POWEROFF
#define POWERON
#define READ_LOCO
#define RED(signal_id)
#define RESERVE(blockid)
#define RESET(pin)
#define RESUME
#define RETURN
#define REV(speed)
#define ROUTE(id,description)
#define ROSTER(cab,name,funcmap...)
#define SENDLOCO(cab,route)
#define SEQUENCE(id)
#define SERIAL(msg)
#define SERIAL1(msg)
#define SERIAL2(msg)
#define SERIAL3(msg)
#define SERIAL4(msg)
#define SERIAL5(msg)
#define SERIAL6(msg)
#define SERVO(id,position,profile)
#define SERVO2(id,position,duration)
#define SERVO_SIGNAL(vpin,redpos,amberpos,greenpos)
#define SERVO_TURNOUT(id,pin,activeAngle,inactiveAngle,profile,description...)
#define SET(pin)
#define SET_TRACK(track,mode)
#define SETLOCO(loco)
#define SIGNAL(redpin,amberpin,greenpin)
#define SIGNALH(redpin,amberpin,greenpin)
#define SPEED(speed)
#define START(route)
#define STOP
#define THROW(id)
#define TURNOUT(id,addr,subaddr,description...)
#define TURNOUTL(id,addr,description...)
#define UNJOIN
#define UNLATCH(sensor_id)
#define VIRTUAL_SIGNAL(id)
#define VIRTUAL_TURNOUT(id,description...)
#define WAITFOR(pin)
#define WITHROTTLE(msg)
#define XFOFF(cab,func)
#define XFON(cab,func)
#endif

View File

@@ -1,391 +0,0 @@
/*
* © 2021 Neil McKechnie
* © 2020-2022 Chris Harlow
* © 2022 Colin Murdoch
* © 2023 Harald Barth
* All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef EXRAILMacros_H
#define EXRAILMacros_H
// remove normal code LCD & SERIAL macros (will be restored later)
#undef LCD
#undef SERIAL
// This file will include and build the EXRAIL script and associated helper tricks.
// It does this by including myAutomation.h several times, each with a set of macros to
// extract the relevant parts.
// The entire automation script is contained within a byte array RMFT2::RouteCode[]
// made up of opcode and parameter pairs.
// ech opcode is a 1 byte operation plus 2 byte operand.
// The array is normally built using the macros below as this makes it easier
// to manage the cases where:
// - padding must be applied to ensure the correct alignment of the next instruction
// - large parameters must be split up
// - multiple parameters aligned correctly
// - a single macro requires multiple operations
// Descriptive texts for routes and animations are created in a sepaerate function which
// can be called to emit a list of routes/automatuions in a form suitable for Withrottle.
// PRINT(msg), LCD(row,msg) and SCREEN(display,row,msg) are implemented in a separate pass to create
// a getMessageText(id) function.
// CAUTION: The macros below are multiple passed over myAutomation.h
// helper macro for turnout descriptions, creates NULL for missing description
#define O_DESC(id, desc) case id: return ("" desc)[0]?F("" desc):NULL;
// helper macro for turnout description as HIDDEN
#define HIDDEN "\x01"
// helper macro to strip leading zeros off time inputs
// (10#mins)%100)
#define STRIP_ZERO(value) 10##value%100
// Pass 1 Implements aliases
#include "EXRAIL2MacroReset.h"
#undef ALIAS
#define ALIAS(name,value...) const int name= 1##value##0 ==10 ? -__COUNTER__ : value##0/10;
#include "myAutomation.h"
// Pass 1h Implements HAL macro by creating exrailHalSetup function
#include "EXRAIL2MacroReset.h"
#undef HAL
#define HAL(haltype,params...) haltype::create(params);
void exrailHalSetup() {
#include "myAutomation.h"
}
// Pass 2 create throttle route list
#include "EXRAIL2MacroReset.h"
#undef ROUTE
#define ROUTE(id, description) id,
const int16_t HIGHFLASH RMFT2::routeIdList[]= {
#include "myAutomation.h"
0};
// Pass 2a create throttle automation list
#include "EXRAIL2MacroReset.h"
#undef AUTOMATION
#define AUTOMATION(id, description) id,
const int16_t HIGHFLASH RMFT2::automationIdList[]= {
#include "myAutomation.h"
0};
// Pass 3 Create route descriptions:
#undef ROUTE
#define ROUTE(id, description) case id: return F(description);
#undef AUTOMATION
#define AUTOMATION(id, description) case id: return F(description);
const FSH * RMFT2::getRouteDescription(int16_t id) {
switch(id) {
#include "myAutomation.h"
default: break;
}
return F("");
}
// Pass 4... Create Text sending functions
#include "EXRAIL2MacroReset.h"
const int StringMacroTracker1=__COUNTER__;
#define THRUNGE(msg,mode) \
case (__COUNTER__ - StringMacroTracker1) : {\
static const char HIGHFLASH thrunge[]=msg;\
strfar=(uint32_t)GETFARPTR(thrunge);\
tmode=mode;\
break;\
}
#undef BROADCAST
#define BROADCAST(msg) THRUNGE(msg,thrunge_broadcast)
#undef PARSE
#define PARSE(msg) THRUNGE(msg,thrunge_parse)
#undef PRINT
#define PRINT(msg) THRUNGE(msg,thrunge_print)
#undef LCN
#define LCN(msg) THRUNGE(msg,thrunge_lcn)
#undef SERIAL
#define SERIAL(msg) THRUNGE(msg,thrunge_serial)
#undef SERIAL1
#define SERIAL1(msg) THRUNGE(msg,thrunge_serial1)
#undef SERIAL2
#define SERIAL2(msg) THRUNGE(msg,thrunge_serial2)
#undef SERIAL3
#define SERIAL3(msg) THRUNGE(msg,thrunge_serial3)
#undef SERIAL4
#define SERIAL4(msg) THRUNGE(msg,thrunge_serial4)
#undef SERIAL5
#define SERIAL5(msg) THRUNGE(msg,thrunge_serial5)
#undef SERIAL6
#define SERIAL6(msg) THRUNGE(msg,thrunge_serial6)
#undef LCD
#define LCD(id,msg) \
case (__COUNTER__ - StringMacroTracker1) : {\
static const char HIGHFLASH thrunge[]=msg;\
strfar=(uint32_t)GETFARPTR(thrunge);\
tmode=thrunge_lcd; \
lcdid=id;\
break;\
}
#undef SCREEN
#define SCREEN(display,id,msg) \
case (__COUNTER__ - StringMacroTracker1) : {\
static const char HIGHFLASH thrunge[]=msg;\
strfar=(uint32_t)GETFARPTR(thrunge);\
tmode=(thrunger)(thrunge_lcd+display); \
lcdid=id;\
break;\
}
#undef WITHROTTLE
#define WITHROTTLE(msg) THRUNGE(msg,thrunge_withrottle)
void RMFT2::printMessage(uint16_t id) {
thrunger tmode;
uint32_t strfar=0;
byte lcdid=0;
switch(id) {
#include "myAutomation.h"
default: break ;
}
if (strfar) thrungeString(strfar,tmode,lcdid);
}
// Pass 5: Turnout descriptions (optional)
#include "EXRAIL2MacroReset.h"
#undef TURNOUT
#define TURNOUT(id,addr,subaddr,description...) O_DESC(id,description)
#undef PIN_TURNOUT
#define PIN_TURNOUT(id,pin,description...) O_DESC(id,description)
#undef SERVO_TURNOUT
#define SERVO_TURNOUT(id,pin,activeAngle,inactiveAngle,profile,description...) O_DESC(id,description)
#undef VIRTUAL_TURNOUT
#define VIRTUAL_TURNOUT(id,description...) O_DESC(id,description)
const FSH * RMFT2::getTurnoutDescription(int16_t turnoutid) {
switch (turnoutid) {
#include "myAutomation.h"
default:break;
}
return NULL;
}
// Pass 6: Roster IDs (count)
#include "EXRAIL2MacroReset.h"
#undef ROSTER
#define ROSTER(cabid,name,funcmap...) +1
const byte RMFT2::rosterNameCount=0
#include "myAutomation.h"
;
// Pass 6: Roster IDs
#include "EXRAIL2MacroReset.h"
#undef ROSTER
#define ROSTER(cabid,name,funcmap...) cabid,
const int16_t HIGHFLASH RMFT2::rosterIdList[]={
#include "myAutomation.h"
0};
// Pass 7: Roster names getter
#include "EXRAIL2MacroReset.h"
#undef ROSTER
#define ROSTER(cabid,name,funcmap...) case cabid: return F(name);
const FSH * RMFT2::getRosterName(int16_t id) {
switch(id) {
#include "myAutomation.h"
default: break;
}
return F("");
}
// Pass to get roster functions
#undef ROSTER
#define ROSTER(cabid,name,funcmap...) case cabid: return F("" funcmap);
const FSH * RMFT2::getRosterFunctions(int16_t id) {
switch(id) {
#include "myAutomation.h"
default: break;
}
return F("");
}
// Pass 8 Signal definitions
#include "EXRAIL2MacroReset.h"
#undef SIGNAL
#define SIGNAL(redpin,amberpin,greenpin) redpin,redpin,amberpin,greenpin,
#undef SIGNALH
#define SIGNALH(redpin,amberpin,greenpin) redpin | RMFT2::ACTIVE_HIGH_SIGNAL_FLAG,redpin,amberpin,greenpin,
#undef SERVO_SIGNAL
#define SERVO_SIGNAL(vpin,redval,amberval,greenval) vpin | RMFT2::SERVO_SIGNAL_FLAG,redval,amberval,greenval,
#undef DCC_SIGNAL
#define DCC_SIGNAL(id,addr,subaddr) id | RMFT2::DCC_SIGNAL_FLAG,addr,subaddr,0,
#undef VIRTUAL_SIGNAL
#define VIRTUAL_SIGNAL(id) id,0,0,0,
const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = {
#include "myAutomation.h"
0,0,0,0 };
// Last Pass : create main routes table
// Only undef the macros, not dummy them.
#define RMFT2_UNDEF_ONLY
#include "EXRAIL2MacroReset.h"
// Define internal helper macros.
// Everything we generate here has to be compile-time evaluated to
// a constant.
#define V(val) (byte)(((int16_t)(val))&0x00FF),(byte)(((int16_t)(val)>>8)&0x00FF)
// Define macros for route code creation
#define ACTIVATE(addr,subaddr) OPCODE_DCCACTIVATE,V(addr<<3 | subaddr<<1 | 1),
#define ACTIVATEL(addr) OPCODE_DCCACTIVATE,V((addr+3)<<1 | 1),
#define AFTER(sensor_id) OPCODE_AT,V(sensor_id),OPCODE_AFTER,V(sensor_id),
#define ALIAS(name,value...)
#define AMBER(signal_id) OPCODE_AMBER,V(signal_id),
#define ANOUT(vpin,value,param1,param2) OPCODE_SERVO,V(vpin),OPCODE_PAD,V(value),OPCODE_PAD,V(param1),OPCODE_PAD,V(param2),
#define AT(sensor_id) OPCODE_AT,V(sensor_id),
#define ATGTE(sensor_id,value) OPCODE_ATGTE,V(sensor_id),OPCODE_PAD,V(value),
#define ATLT(sensor_id,value) OPCODE_ATLT,V(sensor_id),OPCODE_PAD,V(value),
#define ATTIMEOUT(sensor_id,timeout) OPCODE_ATTIMEOUT1,0,0,OPCODE_ATTIMEOUT2,V(sensor_id),OPCODE_PAD,V(timeout/100L),
#define AUTOMATION(id, description) OPCODE_AUTOMATION, V(id),
#define AUTOSTART OPCODE_AUTOSTART,0,0,
#define BROADCAST(msg) PRINT(msg)
#define CALL(route) OPCODE_CALL,V(route),
#define CLOSE(id) OPCODE_CLOSE,V(id),
#define DEACTIVATE(addr,subaddr) OPCODE_DCCACTIVATE,V(addr<<3 | subaddr<<1),
#define DEACTIVATEL(addr) OPCODE_DCCACTIVATE,V((addr+3)<<1),
#define DELAY(ms) ms<30000?OPCODE_DELAYMS:OPCODE_DELAY,V(ms/(ms<30000?1L:100L)),
#define DELAYMINS(mindelay) OPCODE_DELAYMINS,V(mindelay),
#define DELAYRANDOM(mindelay,maxdelay) DELAY(mindelay) OPCODE_RANDWAIT,V((maxdelay-mindelay)/100L),
#define DCC_SIGNAL(id,add,subaddr)
#define DONE OPCODE_ENDTASK,0,0,
#define DRIVE(analogpin) OPCODE_DRIVE,V(analogpin),
#define ELSE OPCODE_ELSE,0,0,
#define ENDEXRAIL
#define ENDIF OPCODE_ENDIF,0,0,
#define ENDTASK OPCODE_ENDTASK,0,0,
#define ESTOP OPCODE_SPEED,V(1),
#define EXRAIL
#define FADE(pin,value,ms) OPCODE_SERVO,V(pin),OPCODE_PAD,V(value),OPCODE_PAD,V(PCA9685::ProfileType::UseDuration|PCA9685::NoPowerOff),OPCODE_PAD,V(ms/100L),
#define FOFF(func) OPCODE_FOFF,V(func),
#define FOLLOW(route) OPCODE_FOLLOW,V(route),
#define FON(func) OPCODE_FON,V(func),
#define FORGET OPCODE_FORGET,0,0,
#define FREE(blockid) OPCODE_FREE,V(blockid),
#define FWD(speed) OPCODE_FWD,V(speed),
#define GREEN(signal_id) OPCODE_GREEN,V(signal_id),
#define HAL(haltype,params...)
#define IF(sensor_id) OPCODE_IF,V(sensor_id),
#define IFAMBER(signal_id) OPCODE_IFAMBER,V(signal_id),
#define IFCLOSED(turnout_id) OPCODE_IFCLOSED,V(turnout_id),
#define IFGREEN(signal_id) OPCODE_IFGREEN,V(signal_id),
#define IFGTE(sensor_id,value) OPCODE_IFGTE,V(sensor_id),OPCODE_PAD,V(value),
#define IFLOCO(loco_id) OPCODE_IFLOCO,V(loco_id),
#define IFLT(sensor_id,value) OPCODE_IFLT,V(sensor_id),OPCODE_PAD,V(value),
#define IFNOT(sensor_id) OPCODE_IFNOT,V(sensor_id),
#define IFRANDOM(percent) OPCODE_IFRANDOM,V(percent),
#define IFRED(signal_id) OPCODE_IFRED,V(signal_id),
#define IFRESERVE(block) OPCODE_IFRESERVE,V(block),
#define IFTHROWN(turnout_id) OPCODE_IFTHROWN,V(turnout_id),
#define IFTIMEOUT OPCODE_IFTIMEOUT,0,0,
#define IFRE(sensor_id,value) OPCODE_IFRE,V(sensor_id),OPCODE_PAD,V(value),
#define INVERT_DIRECTION OPCODE_INVERT_DIRECTION,0,0,
#define JOIN OPCODE_JOIN,0,0,
#define KILLALL OPCODE_KILLALL,0,0,
#define LATCH(sensor_id) OPCODE_LATCH,V(sensor_id),
#define LCD(id,msg) PRINT(msg)
#define SCREEN(display,id,msg) PRINT(msg)
#define LCN(msg) PRINT(msg)
#define MOVETT(id,steps,activity) OPCODE_SERVO,V(id),OPCODE_PAD,V(steps),OPCODE_PAD,V(EXTurntable::activity),OPCODE_PAD,V(0),
#define ONACTIVATE(addr,subaddr) OPCODE_ONACTIVATE,V(addr<<2|subaddr),
#define ONACTIVATEL(linear) OPCODE_ONACTIVATE,V(linear+3),
#define ONAMBER(signal_id) OPCODE_ONAMBER,V(signal_id),
#define ONCLOSE(turnout_id) OPCODE_ONCLOSE,V(turnout_id),
#define ONTIME(value) OPCODE_ONTIME,V(value),
#define ONCLOCKTIME(hours,mins) OPCODE_ONTIME,V((STRIP_ZERO(hours)*60)+STRIP_ZERO(mins)),
#define ONCLOCKMINS(mins) ONCLOCKTIME(25,mins)
#define ONDEACTIVATE(addr,subaddr) OPCODE_ONDEACTIVATE,V(addr<<2|subaddr),
#define ONDEACTIVATEL(linear) OPCODE_ONDEACTIVATE,V(linear+3),
#define ONGREEN(signal_id) OPCODE_ONGREEN,V(signal_id),
#define ONRED(signal_id) OPCODE_ONRED,V(signal_id),
#define ONTHROW(turnout_id) OPCODE_ONTHROW,V(turnout_id),
#define ONCHANGE(sensor_id) OPCODE_ONCHANGE,V(sensor_id),
#define PAUSE OPCODE_PAUSE,0,0,
#define PIN_TURNOUT(id,pin,description...) OPCODE_PINTURNOUT,V(id),OPCODE_PAD,V(pin),
#define POM(cv,value) OPCODE_POM,V(cv),OPCODE_PAD,V(value),
#define POWEROFF OPCODE_POWEROFF,0,0,
#define POWERON OPCODE_POWERON,0,0,
#define PRINT(msg) OPCODE_PRINT,V(__COUNTER__ - StringMacroTracker2),
#define PARSE(msg) PRINT(msg)
#define READ_LOCO OPCODE_READ_LOCO1,0,0,OPCODE_READ_LOCO2,0,0,
#define RED(signal_id) OPCODE_RED,V(signal_id),
#define RESERVE(blockid) OPCODE_RESERVE,V(blockid),
#define RESET(pin) OPCODE_RESET,V(pin),
#define RESUME OPCODE_RESUME,0,0,
#define RETURN OPCODE_RETURN,0,0,
#define REV(speed) OPCODE_REV,V(speed),
#define ROSTER(cabid,name,funcmap...)
#define ROUTE(id, description) OPCODE_ROUTE, V(id),
#define SENDLOCO(cab,route) OPCODE_SENDLOCO,V(cab),OPCODE_PAD,V(route),
#define SEQUENCE(id) OPCODE_SEQUENCE, V(id),
#define SERIAL(msg) PRINT(msg)
#define SERIAL1(msg) PRINT(msg)
#define SERIAL2(msg) PRINT(msg)
#define SERIAL3(msg) PRINT(msg)
#define SERIAL4(msg) PRINT(msg)
#define SERIAL5(msg) PRINT(msg)
#define SERIAL6(msg) PRINT(msg)
#define SERVO(id,position,profile) OPCODE_SERVO,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(PCA9685::profile),OPCODE_PAD,V(0),
#define SERVO2(id,position,ms) OPCODE_SERVO,V(id),OPCODE_PAD,V(position),OPCODE_PAD,V(PCA9685::Instant),OPCODE_PAD,V(ms/100L),
#define SERVO_SIGNAL(vpin,redpos,amberpos,greenpos)
#define SERVO_TURNOUT(id,pin,activeAngle,inactiveAngle,profile,description...) OPCODE_SERVOTURNOUT,V(id),OPCODE_PAD,V(pin),OPCODE_PAD,V(activeAngle),OPCODE_PAD,V(inactiveAngle),OPCODE_PAD,V(PCA9685::ProfileType::profile),
#define SET(pin) OPCODE_SET,V(pin),
#define SET_TRACK(track,mode) OPCODE_SET_TRACK,V(TRACK_MODE_##mode <<8 | TRACK_NUMBER_##track),
#define SETLOCO(loco) OPCODE_SETLOCO,V(loco),
#define SIGNAL(redpin,amberpin,greenpin)
#define SIGNALH(redpin,amberpin,greenpin)
#define SPEED(speed) OPCODE_SPEED,V(speed),
#define START(route) OPCODE_START,V(route),
#define STOP OPCODE_SPEED,V(0),
#define THROW(id) OPCODE_THROW,V(id),
#define TURNOUT(id,addr,subaddr,description...) OPCODE_TURNOUT,V(id),OPCODE_PAD,V(addr),OPCODE_PAD,V(subaddr),
#define TURNOUTL(id,addr,description...) TURNOUT(id,(addr-1)/4+1,(addr-1)%4, description)
#define UNJOIN OPCODE_UNJOIN,0,0,
#define UNLATCH(sensor_id) OPCODE_UNLATCH,V(sensor_id),
#define VIRTUAL_SIGNAL(id)
#define VIRTUAL_TURNOUT(id,description...) OPCODE_PINTURNOUT,V(id),OPCODE_PAD,V(0),
#define WITHROTTLE(msg) PRINT(msg)
#define WAITFOR(pin) OPCODE_WAITFOR,V(pin),
#define XFOFF(cab,func) OPCODE_XFOFF,V(cab),OPCODE_PAD,V(func),
#define XFON(cab,func) OPCODE_XFON,V(cab),OPCODE_PAD,V(func),
// Build RouteCode
const int StringMacroTracker2=__COUNTER__;
const HIGHFLASH byte RMFT2::RouteCode[] = {
#include "myAutomation.h"
OPCODE_ENDTASK,0,0,OPCODE_ENDEXRAIL,0,0 };
// Restore normal code LCD & SERIAL macro
#undef LCD
#define LCD StringFormatter::lcd
#undef SCREEN
#define SCREEN StringFormatter::lcd2
#undef SERIAL
#define SERIAL 0x0
#endif

View File

@@ -1,10 +1,5 @@
/*
* © 2022 Bruno Sanches
* © 2021 Fred Decker
* © 2020-2022 Harald Barth
* © 2020-2021 Chris Harlow
* © 2020 Gregor Baues
* All rights reserved.
* © 2020,Gregor Baues, Chris Harlow. All rights reserved.
*
* This file is part of DCC-EX/CommandStation-EX
*
@@ -22,12 +17,17 @@
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*
*/
#if __has_include ( "config.h")
#include "config.h"
#else
#warning config.h not found. Using defaults from config.example.h
#include "config.example.h"
#endif
#include "defines.h"
#if ETHERNET_ON == true
#include "EthernetInterface.h"
#include "DIAG.h"
#include "CommandDistributor.h"
#include "WiThrottle.h"
#include "DCCTimer.h"
EthernetInterface * EthernetInterface::singleton=NULL;
@@ -37,13 +37,8 @@ EthernetInterface * EthernetInterface::singleton=NULL;
*/
void EthernetInterface::setup()
{
if (singleton!=NULL) {
DIAG(F("Prog Error!"));
return;
}
if ((singleton=new EthernetInterface()))
return;
DIAG(F("Ethernet not initialized"));
singleton=new EthernetInterface();
if (!singleton->connected) singleton=NULL;
};
@@ -68,33 +63,27 @@ EthernetInterface::EthernetInterface()
return;
}
#endif
if (Ethernet.hardwareStatus() == EthernetNoHardware) {
DIAG(F("Ethernet shield not found or W5100"));
DIAG(F("begin OK."));
if (Ethernet.hardwareStatus() == EthernetNoHardware) {
DIAG(F("Ethernet shield not found"));
return;
}
unsigned long startmilli = millis();
while ((millis() - startmilli) < 5500) { // Loop to give time to check for cable connection
if (Ethernet.linkStatus() == LinkON)
break;
DIAG(F("Ethernet waiting for link (1sec) "));
delay(1000);
if (Ethernet.linkStatus() == LinkOFF) {
DIAG(F("Ethernet cable not connected"));
return;
}
// now we either do have link of we have a W5100
// where we do not know if we have link. That's
// the reason to now run checkLink.
// CheckLinks sets up outboundRing if it does
// not exist yet as well.
checkLink();
}
connected=true;
IPAddress ip = Ethernet.localIP(); // reassign the obtained ip address
/**
* @brief Cleanup any resources
*
* @return none
*/
EthernetInterface::~EthernetInterface() {
delete server;
delete outboundRing;
server = new EthernetServer(IP_PORT); // Ethernet Server listening on default port IP_PORT
server->begin();
LCD(4,F("IP: %d.%d.%d.%d"), ip[0], ip[1], ip[2], ip[3]);
LCD(5,F("Port:%d"), IP_PORT);
outboundRing=new RingStream(OUTBOUND_RING_SIZE);
}
/**
@@ -103,73 +92,33 @@ EthernetInterface::~EthernetInterface() {
*/
void EthernetInterface::loop()
{
if (!singleton || (!singleton->checkLink()))
return;
if (!singleton) return;
switch (Ethernet.maintain()) {
switch (Ethernet.maintain())
{
case 1:
//renewed fail
DIAG(F("Ethernet Error: renewed fail"));
singleton=NULL;
return;
case 3:
//rebind fail
DIAG(F("Ethernet Error: rebind fail"));
singleton=NULL;
return;
default:
//nothing happened
break;
}
singleton->loop2();
}
/**
* @brief Checks ethernet link cable status and detects when it connects / disconnects
*
* @return true when cable is connected, false otherwise
*/
bool EthernetInterface::checkLink() {
if (Ethernet.linkStatus() != LinkOFF) { // check for not linkOFF instead of linkON as the W5100 does return LinkUnknown
//if we are not connected yet, setup a new server
if(!connected) {
DIAG(F("Ethernet cable connected"));
connected=true;
#ifdef IP_ADDRESS
Ethernet.setLocalIP(IP_ADDRESS); // for static IP, set it again
#endif
IPAddress ip = Ethernet.localIP(); // look what IP was obtained (dynamic or static)
server = new EthernetServer(IP_PORT); // Ethernet Server listening on default port IP_PORT
server->begin();
LCD(4,F("IP: %d.%d.%d.%d"), ip[0], ip[1], ip[2], ip[3]);
LCD(5,F("Port:%d"), IP_PORT);
// only create a outboundRing it none exists, this may happen if the cable
// gets disconnected and connected again
if(!outboundRing)
outboundRing=new RingStream(OUTBOUND_RING_SIZE);
}
return true;
} else { // connected
DIAG(F("Ethernet cable disconnected"));
connected=false;
//clean up any client
for (byte socket = 0; socket < MAX_SOCK_NUM; socket++) {
if(clients[socket].connected())
clients[socket].stop();
}
// tear down server
delete server;
server = nullptr;
LCD(4,F("IP: None"));
}
return false;
}
void EthernetInterface::loop2() {
if (!outboundRing) { // no idea to call loop2() if we can't handle outgoing data in it
if (Diag::ETHERNET) DIAG(F("No outboundRing"));
return;
}
void EthernetInterface::loop2()
{
// get client from the server
EthernetClient client = server->accept();
@@ -205,7 +154,9 @@ void EthernetInterface::loop2() {
buffer[count] = '\0'; // terminate the string properly
if (Diag::ETHERNET) DIAG(F(",count=%d:%e"), socket,buffer);
// execute with data going directly back
outboundRing->mark(socket);
CommandDistributor::parse(socket,buffer,outboundRing);
outboundRing->commit();
return; // limit the amount of processing that takes place within 1 loop() cycle.
}
}
@@ -215,18 +166,13 @@ void EthernetInterface::loop2() {
for (int socket = 0; socket<MAX_SOCK_NUM; socket++) {
if (clients[socket] && !clients[socket].connected()) {
clients[socket].stop();
CommandDistributor::forget(socket);
if (Diag::ETHERNET) DIAG(F("Ethernet: disconnect %d "), socket);
}
}
WiThrottle::loop(outboundRing);
// handle at most 1 outbound transmission
int socketOut=outboundRing->read();
if (socketOut >= MAX_SOCK_NUM) {
DIAG(F("Ethernet outboundRing socket=%d error"), socketOut);
} else if (socketOut >= 0) {
if (socketOut>=0) {
int count=outboundRing->count();
if (Diag::ETHERNET) DIAG(F("Ethernet reply socket=%d, count=:%d"), socketOut,count);
for(;count>0;count--) clients[socketOut].write(outboundRing->read());

View File

@@ -1,10 +1,5 @@
/*
* © 2021 Neil McKechnie
* © 2021 Mike S
* © 2021 Fred Decker
* © 2020-2021 Chris Harlow
* © 2020 Gregor Baues
* All rights reserved.
* © 2020,Gregor Baues, Chris Harlow. All rights reserved.
*
* This file is part of DCC-EX/CommandStation-EX
*
@@ -27,11 +22,15 @@
#ifndef EthernetInterface_h
#define EthernetInterface_h
#include "defines.h"
#if __has_include ( "config.h")
#include "config.h"
#else
#warning config.h not found. Using defaults from config.example.h
#include "config.example.h"
#endif
#include "DCCEXParser.h"
#include <Arduino.h>
//#include <avr/pgmspace.h>
#include <avr/pgmspace.h>
#if defined (ARDUINO_TEENSY41)
#include <NativeEthernet.h> //TEENSY Ethernet Treiber
#include <NativeEthernetUdp.h>
@@ -52,20 +51,31 @@ class EthernetInterface {
public:
static void setup();
static void loop();
static void setup();
static void loop();
bool isConnected() { return connected; };
static EthernetInterface *get() { return singleton; };
EthernetClient *getClient(int socket) { return &clients[socket]; };
EthernetServer *getServer() { return server; };
~EthernetInterface() = default;
private:
EthernetInterface();
EthernetInterface(const EthernetInterface&); // non construction-copyable
EthernetInterface& operator=( const EthernetInterface& ); // non copyable
static EthernetInterface * singleton;
bool connected;
EthernetInterface();
~EthernetInterface();
void loop2();
bool checkLink();
EthernetServer * server = NULL;
EthernetServer * server;
EthernetClient clients[MAX_SOCK_NUM]; // accept up to MAX_SOCK_NUM client connections at the same time; This depends on the chipset used on the Shield
uint8_t buffer[MAX_ETH_BUFFER+1]; // buffer used by TCP for the recv
RingStream * outboundRing = NULL;
RingStream * outboundRing;
};
#endif

75
FSH.h
View File

@@ -1,25 +1,3 @@
/*
* © 2022 Paul M. Antoine
* © 2021 Neil McKechnie
* © 2021 Harald Barth
* © 2021 Fred Decker
* All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef FSH_h
#define FSH_h
@@ -33,61 +11,20 @@
* __FlashStringHelper Use FSH instead.
* PROGMEM use FLASH instead
* pgm_read_byte_near use GETFLASH instead.
* pgm_read_word_near use GETFLASHW instead.
*
* Also:
* HIGHFLASH - PROGMEM forced to end of link so needs far pointers.
* GETHIGHFLASH,GETHIGHFLASHW to access them
*
*/
#include <Arduino.h>
#ifdef ARDUINO_ARCH_AVR
// AVR devices have flash memory mapped differently
// progmem can be accessed by _near functions or _far
typedef __FlashStringHelper FSH;
#define FLASH PROGMEM
#define GETFLASH(addr) pgm_read_byte_near(addr)
#define STRCPY_P strcpy_P
#define STRCMP_P strcmp_P
#define STRNCPY_P strncpy_P
#define STRNCMP_P strncmp_P
#define STRLEN_P strlen_P
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
// AVR_MEGA memory deliberately placed at end of link may need _far functions
#define HIGHFLASH __attribute__((section(".fini2")))
#define GETFARPTR(data) pgm_get_far_address(data)
#define GETHIGHFLASH(data,offset) pgm_read_byte_far(GETFARPTR(data)+offset)
#define GETHIGHFLASHW(data,offset) pgm_read_word_far(GETFARPTR(data)+offset)
#else
// AVR_UNO/NANO runtime does not support _far functions so just use _near equivalent
// as there is no progmem above 32kb anyway.
#define HIGHFLASH PROGMEM
#define GETFARPTR(data) ((uint32_t)(data))
#define GETHIGHFLASH(data,offset) pgm_read_byte_near(GETFARPTR(data)+(offset))
#define GETHIGHFLASHW(data,offset) pgm_read_word_near(GETFARPTR(data)+(offset))
#endif
#else
// Non-AVR Flat-memory devices have no need of this support so can be remapped to normal memory access
#if defined(ARDUINO_ARCH_MEGAAVR)
#ifdef F
#undef F
#endif
#ifdef FLASH
#undef FLASH
#endif
#define F(str) (str)
typedef char FSH;
#define GETFLASH(addr) (*(const unsigned char *)(addr))
#define FLASH
#define HIGHFLASH
#define GETFARPTR(data) ((uint32_t)(data))
#define GETFLASH(addr) (*(const byte *)(addr))
#define GETHIGHFLASH(data,offset) (*(const byte *)(GETFARPTR(data)+offset))
#define GETHIGHFLASHW(data,offset) (*(const uint16_t *)(GETFARPTR(data)+offset))
#define STRCPY_P strcpy
#define STRCMP_P strcmp
#define STRNCPY_P strncpy
#define STRNCMP_P strncmp
#define STRLEN_P strlen
#else
typedef __FlashStringHelper FSH;
#define GETFLASH(addr) pgm_read_byte_near(addr)
#define FLASH PROGMEM
#endif
#endif

View File

@@ -1 +1 @@
#define GITHUB_SHA "devel-202305072222Z"
#define GITHUB_SHA "90487d2"

View File

@@ -1,7 +1,5 @@
/*
* © 2023, Neil McKechnie
* © 2022 Paul M Antoine
* All rights reserved.
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of CommandStation-EX
*
@@ -20,140 +18,14 @@
*/
#include <stdarg.h>
#include <Wire.h>
#include "I2CManager.h"
#include "DIAG.h"
// Include target-specific portions of I2CManager class
#if defined(I2C_USE_WIRE)
#include "I2CManager_Wire.h"
#elif defined(ARDUINO_ARCH_AVR)
#include "I2CManager_NonBlocking.h"
#include "I2CManager_AVR.h" // Uno/Nano/Mega2560
#elif defined(ARDUINO_ARCH_MEGAAVR)
#include "I2CManager_NonBlocking.h"
#include "I2CManager_Mega4809.h" // NanoEvery/UnoWifi
#elif defined(ARDUINO_ARCH_SAMD)
#include "I2CManager_NonBlocking.h"
#include "I2CManager_SAMD.h" // SAMD21 for now... SAMD51 as well later
#elif defined(ARDUINO_ARCH_STM32)
#include "I2CManager_NonBlocking.h"
#include "I2CManager_STM32.h" // STM32F411RE for now... more later
#else
#define I2C_USE_WIRE
#include "I2CManager_Wire.h" // Other platforms
#endif
// Helper function for listing device types
static const FSH * guessI2CDeviceType(uint8_t address) {
if (address >= 0x20 && address <= 0x26)
return F("GPIO Expander");
else if (address == 0x27)
return F("GPIO Expander or LCD Display");
else if (address == 0x29)
return F("Time-of-flight sensor");
else if (address >= 0x3c && address <= 0x3d)
return F("OLED Display");
else if (address >= 0x48 && address <= 0x4f)
return F("Analogue Inputs or PWM");
else if (address >= 0x40 && address <= 0x4f)
return F("PWM");
else if (address >= 0x50 && address <= 0x5f)
return F("EEPROM");
else if (address == 0x68)
return F("Real-time clock");
else if (address >= 0x70 && address <= 0x77)
return F("I2C Mux");
else
return F("?");
}
// If not already initialised, initialise I2C
// If not already initialised, initialise I2C (wire).
void I2CManagerClass::begin(void) {
if (!_beginCompleted) {
Wire.begin();
_beginCompleted = true;
// Check for short-circuit or floating lines (no pull-up) on I2C before enabling I2C
const FSH *message = F("WARNING: Check I2C %S line for short/pullup");
pinMode(SDA, INPUT);
if (!digitalRead(SDA))
DIAG(message, F("SDA"));
pinMode(SCL, INPUT);
if (!digitalRead(SCL))
DIAG(message, F("SCL"));
// Now initialise I2C
_initialise();
#if defined(I2C_USE_WIRE)
DIAG(F("I2CManager: Using Wire library"));
#endif
// Probe and list devices. Use standard mode
// (clock speed 100kHz) for best device compatibility.
_setClock(100000);
unsigned long originalTimeout = _timeout;
setTimeout(1000); // use 1ms timeout for probes
#if defined(I2C_EXTENDED_ADDRESS)
// First count the multiplexers and switch off all subbuses
_muxCount = 0;
for (uint8_t muxNo=I2CMux_0; muxNo <= I2CMux_7; muxNo++) {
if (I2CManager.muxSelectSubBus({(I2CMux)muxNo, SubBus_None})==I2C_STATUS_OK)
_muxCount++;
}
#endif
// Enumerate devices that are visible
bool found = false;
for (uint8_t addr=0x08; addr<0x78; addr++) {
if (exists(addr)) {
found = true;
DIAG(F("I2C Device found at 0x%x, %S?"), addr, guessI2CDeviceType(addr));
}
}
#if defined(I2C_EXTENDED_ADDRESS)
// Enumerate all I2C devices that are connected via multiplexer,
// i.e. that respond when only one multiplexer has one subBus enabled
// and the device doesn't respond when the mux subBus is disabled.
// If any probes time out, then assume that the subbus is dead and
// don't do any more on that subbus.
for (uint8_t muxNo=I2CMux_0; muxNo <= I2CMux_7; muxNo++) {
uint8_t muxAddr = I2C_MUX_BASE_ADDRESS + muxNo;
if (exists(muxAddr)) {
// Select Mux Subbus
for (uint8_t subBus=0; subBus<=SubBus_No; subBus++) {
muxSelectSubBus({(I2CMux)muxNo, (I2CSubBus)subBus});
for (uint8_t addr=0x08; addr<0x78; addr++) {
uint8_t status = checkAddress(addr);
if (status == I2C_STATUS_OK) {
// De-select subbus
muxSelectSubBus({(I2CMux)muxNo, SubBus_None});
if (!exists(addr)) {
// Device responds when subbus selected but not when
// subbus disabled - ergo it must be on subbus!
found = true;
DIAG(F("I2C Device found at {I2CMux_%d,SubBus_%d,0x%x}, %S?"),
muxNo, subBus, addr, guessI2CDeviceType(addr));
}
// Re-select subbus
muxSelectSubBus({(I2CMux)muxNo, (I2CSubBus)subBus});
} else if (status == I2C_STATUS_TIMEOUT) {
// Bus stuck, skip to next one.
break;
}
}
}
// Deselect all subBuses for this mux. Otherwise its devices will continue to
// respond when other muxes are being probed.
I2CManager.muxSelectSubBus({(I2CMux)muxNo, SubBus_None}); // Deselect Mux
}
}
#endif
if (!found) DIAG(F("No I2C Devices found"));
_setClock(_clockSpeed);
setTimeout(originalTimeout); // set timeout back to original
}
}
@@ -162,35 +34,49 @@ void I2CManagerClass::begin(void) {
void I2CManagerClass::setClock(uint32_t speed) {
if (speed < _clockSpeed && !_clockSpeedFixed) {
_clockSpeed = speed;
DIAG(F("I2C clock speed set to %l Hz"), _clockSpeed);
Wire.setClock(_clockSpeed);
}
_setClock(_clockSpeed);
}
// Force clock speed to that specified.
// Force clock speed to that specified. It can then only
// be overridden by calling Wire.setClock directly.
void I2CManagerClass::forceClock(uint32_t speed) {
_clockSpeed = speed;
_clockSpeedFixed = true;
_setClock(_clockSpeed);
DIAG(F("I2C clock speed forced to %l Hz"), _clockSpeed);
if (!_clockSpeedFixed) {
_clockSpeed = speed;
_clockSpeedFixed = true;
Wire.setClock(_clockSpeed);
}
}
// Check if specified I2C address is responding (blocking operation)
// Returns I2C_STATUS_OK (0) if OK, or error code.
// Suppress retries. If it doesn't respond first time it's out of the running.
uint8_t I2CManagerClass::checkAddress(I2CAddress address) {
I2CRB rb;
rb.setWriteParams(address, NULL, 0);
rb.suppressRetries(true);
queueRequest(&rb);
return rb.wait();
// Check if specified I2C address is responding.
// Returns 0 if OK, or error code.
uint8_t I2CManagerClass::checkAddress(uint8_t address) {
begin();
Wire.beginTransmission(address);
return Wire.endTransmission();
}
bool I2CManagerClass::exists(uint8_t address) {
return checkAddress(address)==0;
}
/***************************************************************************
* Write a transmission to I2C using a list of data (blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::write(I2CAddress address, uint8_t nBytes, ...) {
// Write a complete transmission to I2C using a supplied buffer of data
uint8_t I2CManagerClass::write(uint8_t address, const uint8_t buffer[], uint8_t size) {
Wire.beginTransmission(address);
Wire.write(buffer, size);
return Wire.endTransmission();
}
// Write a complete transmission to I2C using a supplied buffer of data in Flash
uint8_t I2CManagerClass::write_P(uint8_t address, const uint8_t buffer[], uint8_t size) {
uint8_t ramBuffer[size];
memcpy_P(ramBuffer, buffer, size);
return write(address, ramBuffer, size);
}
// Write a complete transmission to I2C using a list of data
uint8_t I2CManagerClass::write(uint8_t address, int nBytes, ...) {
uint8_t buffer[nBytes];
va_list args;
va_start(args, nBytes);
@@ -200,39 +86,31 @@ uint8_t I2CManagerClass::write(I2CAddress address, uint8_t nBytes, ...) {
return write(address, buffer, nBytes);
}
/***************************************************************************
* Initiate a write to an I2C device (blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::write(I2CAddress i2cAddress, const uint8_t writeBuffer[], uint8_t writeLen) {
I2CRB req;
uint8_t status = write(i2cAddress, writeBuffer, writeLen, &req);
return finishRB(&req, status);
// Write a command and read response, returns number of bytes received.
// Different modules use different ways of accessing registers:
// PCF8574 I/O expander justs needs the address (no data);
// PCA9685 needs a two byte command to select the register(s) to be read;
// MCP23016 needs a one-byte command to select the register.
// Some devices use 8-bit registers exclusively and some have 16-bit registers.
// Therefore the following function is general purpose, to apply to any
// type of I2C device.
//
uint8_t I2CManagerClass::read(uint8_t address, uint8_t readBuffer[], uint8_t readSize,
uint8_t writeBuffer[], uint8_t writeSize) {
if (writeSize > 0) {
Wire.beginTransmission(address);
Wire.write(writeBuffer, writeSize);
Wire.endTransmission(false); // Don't free bus yet
}
Wire.requestFrom(address, readSize);
uint8_t nBytes = 0;
while (Wire.available() && nBytes < readSize)
readBuffer[nBytes++] = Wire.read();
return nBytes;
}
/***************************************************************************
* Initiate a write from PROGMEM (flash) to an I2C device (blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::write_P(I2CAddress i2cAddress, const uint8_t * data, uint8_t dataLen) {
I2CRB req;
uint8_t status = write_P(i2cAddress, data, dataLen, &req);
return finishRB(&req, status);
}
/***************************************************************************
* Initiate a write (optional) followed by a read from the I2C device (blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::read(I2CAddress i2cAddress, uint8_t *readBuffer, uint8_t readLen,
const uint8_t *writeBuffer, uint8_t writeLen)
{
I2CRB req;
uint8_t status = read(i2cAddress, readBuffer, readLen, writeBuffer, writeLen, &req);
return finishRB(&req, status);
}
/***************************************************************************
* Overload of read() to allow command to be specified as a series of bytes (blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::read(I2CAddress address, uint8_t readBuffer[], uint8_t readSize,
// Overload of read() to allow command to be specified as a series of bytes.
uint8_t I2CManagerClass::read(uint8_t address, uint8_t readBuffer[], uint8_t readSize,
uint8_t writeSize, ...) {
va_list args;
// Copy the series of bytes into an array.
@@ -244,123 +122,8 @@ uint8_t I2CManagerClass::read(I2CAddress address, uint8_t readBuffer[], uint8_t
return read(address, readBuffer, readSize, writeBuffer, writeSize);
}
/***************************************************************************
* Finish off request block by posting status, etc. (blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::finishRB(I2CRB *rb, uint8_t status) {
if ((status == I2C_STATUS_OK) && rb)
status = rb->wait();
return status;
uint8_t I2CManagerClass::read(uint8_t address, uint8_t readBuffer[], uint8_t readSize) {
return read(address, readBuffer, readSize, NULL, 0);
}
/***************************************************************************
* Get a message corresponding to the error status
***************************************************************************/
const FSH *I2CManagerClass::getErrorMessage(uint8_t status) {
switch (status) {
case I2C_STATUS_OK: return F("OK");
case I2C_STATUS_TRUNCATED: return F("Transmission truncated");
case I2C_STATUS_NEGATIVE_ACKNOWLEDGE: return F("No response from device (address NAK)");
case I2C_STATUS_TRANSMIT_ERROR: return F("Transmit error (data NAK)");
case I2C_STATUS_OTHER_TWI_ERROR: return F("Other Wire/TWI error");
case I2C_STATUS_TIMEOUT: return F("I2C bus timeout");
case I2C_STATUS_ARBITRATION_LOST: return F("Arbitration lost");
case I2C_STATUS_BUS_ERROR: return F("I2C bus error");
case I2C_STATUS_UNEXPECTED_ERROR: return F("Unexpected error");
case I2C_STATUS_PENDING: return F("Request pending");
default: return F("Error code not recognised");
}
}
/***************************************************************************
* Declare singleton class instance.
***************************************************************************/
I2CManagerClass I2CManager = I2CManagerClass();
// Buffer for conversion of I2CAddress to char*.
/* static */ char I2CAddress::addressBuffer[30];
/////////////////////////////////////////////////////////////////////////////
// Helper functions associated with I2C Request Block
/////////////////////////////////////////////////////////////////////////////
/***************************************************************************
* Block waiting for request to complete, and return completion status.
* Timeout monitoring is performed in the I2CManager.loop() function.
***************************************************************************/
uint8_t I2CRB::wait() {
while (status==I2C_STATUS_PENDING) {
I2CManager.loop();
};
return status;
}
/***************************************************************************
* Check whether request is still in progress.
* Timeout monitoring is performed in the I2CManager.loop() function.
***************************************************************************/
bool I2CRB::isBusy() {
if (status==I2C_STATUS_PENDING) {
I2CManager.loop();
return true;
} else
return false;
}
/***************************************************************************
* Helper functions to fill the I2CRequest structure with parameters.
***************************************************************************/
void I2CRB::setReadParams(I2CAddress i2cAddress, uint8_t *readBuffer, uint8_t readLen) {
this->i2cAddress = i2cAddress;
this->writeLen = 0;
this->readBuffer = readBuffer;
this->readLen = readLen;
this->operation = OPERATION_READ;
this->status = I2C_STATUS_OK;
}
void I2CRB::setRequestParams(I2CAddress i2cAddress, uint8_t *readBuffer, uint8_t readLen,
const uint8_t *writeBuffer, uint8_t writeLen) {
this->i2cAddress = i2cAddress;
this->writeBuffer = writeBuffer;
this->writeLen = writeLen;
this->readBuffer = readBuffer;
this->readLen = readLen;
this->operation = OPERATION_REQUEST;
this->status = I2C_STATUS_OK;
}
void I2CRB::setWriteParams(I2CAddress i2cAddress, const uint8_t *writeBuffer, uint8_t writeLen) {
this->i2cAddress = i2cAddress;
this->writeBuffer = writeBuffer;
this->writeLen = writeLen;
this->readLen = 0;
this->operation = OPERATION_SEND;
this->status = I2C_STATUS_OK;
}
void I2CRB::suppressRetries(bool suppress) {
if (suppress)
this->operation |= OPERATION_NORETRY;
else
this->operation &= ~OPERATION_NORETRY;
}
// Helper function for converting a uint8_t to four characters (e.g. 0x23).
void I2CAddress::toHex(const uint8_t value, char *buffer) {
char *ptr = buffer;
// Just display hex value, two digits.
*ptr++ = '0';
*ptr++ = 'x';
uint8_t bits = (value >> 4) & 0xf;
*ptr++ = bits > 9 ? bits-10+'a' : bits+'0';
bits = value & 0xf;
*ptr++ = bits > 9 ? bits-10+'a' : bits+'0';
}
#if !defined(I2C_EXTENDED_ADDRESS)
/* static */ bool I2CAddress::_addressWarningDone = false;
#endif
I2CManagerClass I2CManager = I2CManagerClass();

View File

@@ -1,6 +1,5 @@
/*
* © 2023, Neil McKechnie. All rights reserved.
* © 2022 Paul M Antoine
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of CommandStation-EX
*
@@ -18,20 +17,15 @@
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef I2CMANAGER_H
#define I2CMANAGER_H
#ifndef I2CManager_h
#define I2CManager_h
#include <inttypes.h>
#include "FSH.h"
#include "defines.h"
#include "DIAG.h"
/*
* Manager for I2C communications. For portability, it allows use
* of the Wire class, but also has a native implementation for AVR
* which supports non-blocking queued I/O requests.
* Helper class to manage access to the I2C 'Wire' subsystem.
*
* Helps to avoid calling Wire.begin() multiple times (which is not
* Helps to avoid calling Wire.begin() multiple times (which is not)
* entirely benign as it reinitialises).
*
* Also helps to avoid the Wire clock from being set, by another device
@@ -39,531 +33,44 @@
*
* Thirdly, it provides a convenient way to check whether there is a
* device on a particular I2C address.
*
* Non-blocking requests are issued by creating an I2C Request Block
* (I2CRB) which is then added to the I2C manager's queue. The
* application refers to this block to check for completion of the
* operation, and for reading completion status.
*
* Examples:
* I2CRB rb;
* uint8_t status = I2CManager.write(address, buffer, sizeof(buffer), &rb);
* ...
* if (!rb.isBusy()) {
* status = rb.status;
* // Repeat write
* I2CManager.queueRequest(&rb);
* ...
* status = rb.wait(); // Wait for completion and read status
* }
* ...
* I2CRB rb2;
* outbuffer[0] = 12; // Register number in I2C device to be read
* rb2.setRequestParams(address, inBuffer, 1, outBuffer, 1);
* status = I2CManager.queueRequest(&rb2);
* if (status == I2C_STATUS_OK) {
* status = rb2.wait();
* if (status == I2C_STATUS_OK) {
* registerValue = inBuffer[0];
* }
* }
* ...
*
* Synchronous (blocking) calls are also possible, e.g.
* status = I2CManager.write(address, buffer, sizeof(buffer));
*
* When using non-blocking requests, neither the I2CRB nor the input or output
* buffers should be modified until the I2CRB is complete (not busy).
*
* Timeout monitoring is possible, but requires that the following call is made
* reasonably frequently in the program's loop() function:
* I2CManager.loop();
* So that the application doesn't need to do this explicitly, this call is performed
* from the I2CRB::isBusy() or I2CRB::wait() functions.
*
*/
/*
* I2C Multiplexer (e.g. TCA9547, TCA9548)
*
* A multiplexer offers a way of extending the address range of I2C devices. For example, GPIO extenders use address range 0x20-0x27
* to are limited to 8 on a bus. By adding a multiplexer, the limit becomes 8 for each of the multiplexer's 8 sub-buses, i.e. 64.
* And a single I2C bus can have up to 8 multiplexers, giving up to 64 sub-buses and, in theory, up to 512 I/O extenders; that's
* as many as 8192 input/output pins!
* Secondly, the capacitance of the bus is an electrical limiting factor of the length of the bus, speed and number of devices.
* The multiplexer isolates each sub-bus from the others, and so reduces the capacitance of the bus. For example, with one
* multiplexer and 64 GPIO extenders, only 9 devices are connected to the bus at any time (multiplexer plus 8 extenders).
* Thirdly, the multiplexer offers the ability to use mixed-speed devices more effectively, by allowing high-speed devices to be
* put on a different bus to low-speed devices, enabling the software to switch the I2C speed on-the-fly between I2C transactions.
*
*
* Non-interrupting I2C:
*
* Non-blocking I2C may be operated without interrupts (undefine I2C_USE_INTERRUPTS). Instead, the I2C state
* machine handler, currently invoked from the interrupt service routine, is invoked from the loop() function.
* The speed at which I2C operations can be performed then becomes highly dependent on the frequency that
* the loop() function is called, and may be adequate under some circumstances.
* The advantage of NOT using interrupts is that the impact of I2C upon the DCC waveform (when accurate timing mode isn't in use)
* becomes almost zero.
*
*/
// Maximum number of retries on an I2C operation.
// A value of zero will disable retries.
// Maximum value is 254 (unsigned byte counter)
// Note that timeout failures are not retried, but any timeout
// configured applies to each try separately.
#define MAX_I2C_RETRIES 2
// Add following line to config.h to enable Wire library instead of native I2C drivers
//#define I2C_USE_WIRE
// Add following line to config.h to disable the use of interrupts by the native I2C drivers.
//#define I2C_NO_INTERRUPTS
// Default to use interrupts within the native I2C drivers.
#ifndef I2C_NO_INTERRUPTS
#define I2C_USE_INTERRUPTS
#endif
// I2C Extended Address support I2C Multiplexers and allows various properties to be
// associated with an I2C address such as the MUX and SubBus. In the future, this
// may be extended to include multiple buses, and other features.
// Uncomment to enable extended address.
//
//#define I2C_EXTENDED_ADDRESS
/////////////////////////////////////////////////////////////////////////////////////
// Extended I2C Address type to facilitate extended I2C addresses including
// I2C multiplexer support.
/////////////////////////////////////////////////////////////////////////////////////
// Currently only one bus supported, and one instance of I2CManager to handle it.
enum I2CBus : uint8_t {
I2CBus_0 = 0,
};
// Currently I2CAddress supports one I2C bus, with up to eight
// multipexers (MUX) attached. Each MUX can have up to eight sub-buses.
enum I2CMux : uint8_t {
I2CMux_0 = 0,
I2CMux_1 = 1,
I2CMux_2 = 2,
I2CMux_3 = 3,
I2CMux_4 = 4,
I2CMux_5 = 5,
I2CMux_6 = 6,
I2CMux_7 = 7,
I2CMux_None = 255, // Address doesn't need mux switching
};
enum I2CSubBus : uint8_t {
SubBus_0 = 0, // Enable individual sub-buses...
SubBus_1 = 1,
#if !defined(I2CMUX_PCA9542)
SubBus_2 = 2,
SubBus_3 = 3,
#if !defined(I2CMUX_PCA9544)
SubBus_4 = 4,
SubBus_5 = 5,
SubBus_6 = 6,
SubBus_7 = 7,
#endif
#endif
SubBus_No, // Number of subbuses (highest + 1)
SubBus_None = 254, // Disable all sub-buses on selected mux
SubBus_All = 255, // Enable all sub-buses (not supported by some multiplexers)
};
// Type to hold I2C address
#if defined(I2C_EXTENDED_ADDRESS)
// First MUX address (they range between 0x70-0x77).
#define I2C_MUX_BASE_ADDRESS 0x70
// Currently I2C address supports one I2C bus, with up to eight
// multiplexers (MUX) attached. Each MUX can have up to eight sub-buses.
// This structure could be extended in the future (if there is a need)
// to support 10-bit I2C addresses, different I2C clock speed for each
// sub-bus, multiple I2C buses, and other features not yet thought of.
struct I2CAddress {
private:
// Fields
I2CBus _busNumber;
I2CMux _muxNumber;
I2CSubBus _subBus;
uint8_t _deviceAddress;
static char addressBuffer[];
public:
// Constructors
// For I2CAddress "{I2CBus_0, Mux_0, SubBus_0, 0x23}" syntax.
I2CAddress(const I2CBus busNumber, const I2CMux muxNumber, const I2CSubBus subBus, const uint8_t deviceAddress) {
_busNumber = busNumber;
_muxNumber = muxNumber;
_subBus = subBus;
_deviceAddress = deviceAddress;
}
// Basic constructor
I2CAddress() : I2CAddress(I2CMux_None, SubBus_None, 0) {}
// For I2CAddress "{Mux_0, SubBus_0, 0x23}" syntax.
I2CAddress(const I2CMux muxNumber, const I2CSubBus subBus, const uint8_t deviceAddress) :
I2CAddress(I2CBus_0, muxNumber, subBus, deviceAddress) {}
// For I2CAddress in form "{SubBus_0, 0x23}" - assume Mux0 (0x70)
I2CAddress(I2CSubBus subBus, uint8_t deviceAddress) :
I2CAddress(I2CMux_0, subBus, deviceAddress) {}
// Conversion from uint8_t to I2CAddress
// For I2CAddress in form "0x23"
// (device assumed to be on the main I2C bus).
I2CAddress(const uint8_t deviceAddress) :
I2CAddress(I2CMux_None, SubBus_None, deviceAddress) {}
// Conversion from uint8_t to I2CAddress
// For I2CAddress in form "{I2CBus_1, 0x23}"
// (device not connected via multiplexer).
I2CAddress(const I2CBus bus, const uint8_t deviceAddress) :
I2CAddress(bus, I2CMux_None, SubBus_None, deviceAddress) {}
// For I2CAddress in form "{I2CMux_0, SubBus_0}" (mux selector)
I2CAddress(const I2CMux muxNumber, const I2CSubBus subBus) :
I2CAddress(muxNumber, subBus, 0x00) {}
// For I2CAddress in form "{i2cAddress, deviceAddress}"
// where deviceAddress is to be on the same subbus as i2cAddress.
I2CAddress(I2CAddress firstAddress, uint8_t newDeviceAddress) :
I2CAddress(firstAddress._muxNumber, firstAddress._subBus, newDeviceAddress) {}
// Conversion operator from I2CAddress to uint8_t
// For "uint8_t address = i2cAddress;" syntax
// (device assumed to be on the main I2C bus or on a currently selected subbus.
operator uint8_t () const { return _deviceAddress; }
// Conversion from I2CAddress to char* (uses static storage so only
// one conversion can be done at a time). So don't call it twice in a
// single DIAG statement for example.
const char* toString() {
char *ptr = addressBuffer;
if (_muxNumber != I2CMux_None) {
strcpy_P(ptr, (const char*)F("{I2CMux_"));
ptr += 8;
*ptr++ = '0' + _muxNumber;
strcpy_P(ptr, (const char*)F(",Subbus_"));
ptr += 8;
if (_subBus == SubBus_None) {
strcpy_P(ptr, (const char*)F("None"));
ptr += 4;
} else if (_subBus == SubBus_All) {
strcpy_P(ptr, (const char*)F("All"));
ptr += 3;
} else
*ptr++ = '0' + _subBus;
*ptr++ = ',';
}
toHex(_deviceAddress, ptr);
ptr += 4;
if (_muxNumber != I2CMux_None)
*ptr++ = '}';
*ptr = 0; // terminate string
return addressBuffer;
}
// Comparison operator
int operator == (I2CAddress &a) const {
if (_deviceAddress != a._deviceAddress)
return false; // Different device address so no match
if (_muxNumber == I2CMux_None || a._muxNumber == I2CMux_None)
return true; // Same device address, one or other on main bus
if (_subBus == SubBus_None || a._subBus == SubBus_None)
return true; // Same device address, one or other on main bus
if (_muxNumber != a._muxNumber)
return false; // Connected to a subbus on a different mux
if (_subBus != a._subBus)
return false; // different subbus
return true; // Same address on same mux and same subbus
}
// Field accessors
I2CMux muxNumber() { return _muxNumber; }
I2CSubBus subBus() { return _subBus; }
uint8_t deviceAddress() { return _deviceAddress; }
private:
// Helper function for converting byte to four-character hex string (e.g. 0x23).
void toHex(const uint8_t value, char *buffer);
};
#else
struct I2CAddress {
private:
uint8_t _deviceAddress;
static char addressBuffer[];
public:
// Constructors
I2CAddress(const uint8_t deviceAddress) {
_deviceAddress = deviceAddress;
}
I2CAddress(I2CMux, I2CSubBus, const uint8_t deviceAddress) {
addressWarning();
_deviceAddress = deviceAddress;
}
I2CAddress(I2CSubBus, const uint8_t deviceAddress) {
addressWarning();
_deviceAddress = deviceAddress;
}
// Basic constructor
I2CAddress() : I2CAddress(0) {}
// Conversion operator from I2CAddress to uint8_t
// For "uint8_t address = i2cAddress;" syntax
operator uint8_t () const { return _deviceAddress; }
// Conversion from I2CAddress to char* (uses static storage so only
// one conversion can be done at a time). So don't call it twice in a
// single DIAG statement for example.
const char* toString () {
char *ptr = addressBuffer;
// Just display hex value, two digits.
toHex(_deviceAddress, ptr);
ptr += 4;
*ptr = 0; // terminate string
return addressBuffer;
}
// Comparison operator
int operator == (I2CAddress &a) const {
if (_deviceAddress != a._deviceAddress)
return false; // Different device address so no match
return true; // Same address on same mux and same subbus
}
private:
// Helper function for converting byte to four-character hex string (e.g. 0x23).
void toHex(const uint8_t value, char *buffer);
void addressWarning() {
if (!_addressWarningDone) {
DIAG(F("WARNIING: Extended I2C address used but not supported in this configuration"));
_addressWarningDone = true;
}
}
static bool _addressWarningDone;
};
#endif // I2C_EXTENDED_ADDRESS
// Status codes for I2CRB structures.
enum : uint8_t {
// Codes used by Wire and by native drivers
I2C_STATUS_OK=0,
I2C_STATUS_TRUNCATED=1,
I2C_STATUS_NEGATIVE_ACKNOWLEDGE=2,
I2C_STATUS_TRANSMIT_ERROR=3,
I2C_STATUS_TIMEOUT=5,
// Code used by Wire only
I2C_STATUS_OTHER_TWI_ERROR=4, // catch-all error
// Codes used by native drivers only
I2C_STATUS_ARBITRATION_LOST=6,
I2C_STATUS_BUS_ERROR=7,
I2C_STATUS_UNEXPECTED_ERROR=8,
I2C_STATUS_PENDING=253,
};
// Status codes for the state machine (not returned to caller).
enum : uint8_t {
I2C_STATE_ACTIVE=253,
I2C_STATE_FREE=254,
I2C_STATE_CLOSING=255,
I2C_STATE_COMPLETED=252,
};
typedef enum : uint8_t
{
OPERATION_READ = 1,
OPERATION_REQUEST = 2,
OPERATION_SEND = 3,
OPERATION_SEND_P = 4,
OPERATION_NORETRY = 0x80, // OR with operation to suppress retries.
OPERATION_MASK = 0x7f, // mask for extracting the operation code
} OperationEnum;
// Default I2C frequency
#ifndef I2C_FREQ
#define I2C_FREQ 400000L
#endif
// Class defining a request context for an I2C operation.
class I2CRB {
public:
volatile uint8_t status; // Completion status, or pending flag (updated from IRC)
volatile uint8_t nBytes; // Number of bytes read (updated from IRC)
inline I2CRB() { status = I2C_STATUS_OK; };
uint8_t wait();
bool isBusy();
void setReadParams(I2CAddress i2cAddress, uint8_t *readBuffer, uint8_t readLen);
void setRequestParams(I2CAddress i2cAddress, uint8_t *readBuffer, uint8_t readLen, const uint8_t *writeBuffer, uint8_t writeLen);
void setWriteParams(I2CAddress i2cAddress, const uint8_t *writeBuffer, uint8_t writeLen);
void suppressRetries(bool suppress);
uint8_t writeLen;
uint8_t readLen;
uint8_t operation;
I2CAddress i2cAddress;
uint8_t *readBuffer;
const uint8_t *writeBuffer;
#if !defined(I2C_USE_WIRE)
I2CRB *nextRequest; // Used by non-blocking devices for I2CRB queue management.
#endif
};
// I2C Manager
class I2CManagerClass {
public:
I2CManagerClass() {}
// If not already initialised, initialise I2C (wire).
void begin(void);
// Set clock speed to the lowest requested one.
void setClock(uint32_t speed);
// Force clock speed
void forceClock(uint32_t speed);
// setTimeout sets the timout value for I2C transactions (milliseconds).
void setTimeout(unsigned long);
// Check if specified I2C address is responding.
uint8_t checkAddress(I2CAddress address);
inline bool exists(I2CAddress address) {
return checkAddress(address)==I2C_STATUS_OK;
}
// Select/deselect Mux Sub-Bus (if using legacy addresses, just checks address)
// E.g. muxSelectSubBus({I2CMux_0, SubBus_3});
uint8_t muxSelectSubBus(I2CAddress address) {
return checkAddress(address);
}
uint8_t checkAddress(uint8_t address);
bool exists(uint8_t address);
// Write a complete transmission to I2C from an array in RAM
uint8_t write(I2CAddress address, const uint8_t buffer[], uint8_t size);
uint8_t write(I2CAddress address, const uint8_t buffer[], uint8_t size, I2CRB *rb);
uint8_t write(uint8_t address, const uint8_t buffer[], uint8_t size);
// Write a complete transmission to I2C from an array in Flash
uint8_t write_P(I2CAddress address, const uint8_t buffer[], uint8_t size);
uint8_t write_P(I2CAddress address, const uint8_t buffer[], uint8_t size, I2CRB *rb);
uint8_t write_P(uint8_t address, const uint8_t buffer[], uint8_t size);
// Write a transmission to I2C from a list of bytes.
uint8_t write(I2CAddress address, uint8_t nBytes, ...);
uint8_t write(uint8_t address, int nBytes, ...);
// Write a command from an array in RAM and read response
uint8_t read(I2CAddress address, uint8_t readBuffer[], uint8_t readSize,
const uint8_t writeBuffer[]=NULL, uint8_t writeSize=0);
uint8_t read(I2CAddress address, uint8_t readBuffer[], uint8_t readSize,
const uint8_t writeBuffer[], uint8_t writeSize, I2CRB *rb);
uint8_t read(uint8_t address, uint8_t writeBuffer[], uint8_t writeSize,
uint8_t readBuffer[], uint8_t readSize);
// Write a command from an arbitrary list of bytes and read response
uint8_t read(I2CAddress address, uint8_t readBuffer[], uint8_t readSize,
uint8_t read(uint8_t address, uint8_t readBuffer[], uint8_t readSize,
uint8_t writeSize, ...);
void queueRequest(I2CRB *req);
// Function to abort long-running operations.
void checkForTimeout();
// Loop method
void loop();
// Expand error codes into text. Note that they are in flash so
// need to be printed using FSH.
static const FSH *getErrorMessage(uint8_t status);
// Write a null command and read the response.
uint8_t read(uint8_t address, uint8_t readBuffer[], uint8_t readSize);
private:
bool _beginCompleted = false;
bool _clockSpeedFixed = false;
uint8_t retryCounter; // Count of retries
// Clock speed must be no higher than 400kHz on AVR. Higher is possible on 4809, SAMD
// and STM32 but most popular I2C devices are 400kHz so in practice the higher speeds
// will not be useful. The speed can be overridden by I2CManager::forceClock().
uint32_t _clockSpeed = I2C_FREQ;
// Default timeout 100ms on I2C request block completion.
// A full 32-byte transmission takes about 8ms at 100kHz,
// so this value allows lots of headroom.
// It can be modified by calling I2CManager.setTimeout() function.
// When retries are enabled, the timeout applies to each
// try, and failure from timeout does not get retried.
// A value of 0 means disable timeout monitoring.
unsigned long _timeout = 100000UL;
// Finish off request block by waiting for completion and posting status.
uint8_t finishRB(I2CRB *rb, uint8_t status);
void _initialise();
void _setClock(unsigned long);
#if defined(I2C_EXTENDED_ADDRESS)
// Count of I2C multiplexers found when initialising. If there is only one
// MUX then the subbus does not need de-selecting after use; however, if there
// are two or more, then the subbus must be deselected to avoid multiple
// sub-bus legs on different multiplexers being accessible simultaneously.
private:
uint8_t _muxCount = 0;
public:
uint8_t getMuxCount() { return _muxCount; }
#endif
#if !defined(I2C_USE_WIRE)
// I2CRB structs are queued on the following two links.
// If there are no requests, both are NULL.
// If there is only one request, then queueHead and queueTail both point to it.
// Otherwise, queueHead is the pointer to the first request in the queue and
// queueTail is the pointer to the last request in the queue.
// Within the queue, each request's nextRequest field points to the
// next request, or NULL.
// Mark volatile as they are updated by IRC and read/written elsewhere.
private:
I2CRB * volatile queueHead = NULL;
I2CRB * volatile queueTail = NULL;
// State is set to I2C_STATE_FREE when the interrupt handler has finished
// the current request and is ready to complete.
uint8_t state = I2C_STATE_FREE;
// CompletionStatus may be set by the interrupt handler at any time but is
// not written to the I2CRB until the state is I2C_STATE_FREE.
uint8_t completionStatus = I2C_STATUS_OK;
uint8_t overallStatus = I2C_STATUS_OK;
I2CRB * currentRequest = NULL;
uint8_t txCount = 0;
uint8_t rxCount = 0;
uint8_t bytesToSend = 0;
uint8_t bytesToReceive = 0;
uint8_t operation = 0;
unsigned long startTime = 0;
uint8_t muxPhase = 0;
uint8_t muxAddress = 0;
uint8_t muxData[1];
uint8_t deviceAddress;
const uint8_t *sendBuffer;
uint8_t *receiveBuffer;
volatile uint32_t pendingClockSpeed = 0;
void startTransaction();
// Low-level hardware manipulation functions.
void I2C_init();
void I2C_setClock(unsigned long i2cClockSpeed);
void I2C_handleInterrupt();
void I2C_sendStart();
void I2C_sendStop();
void I2C_close();
public:
// handleInterrupt needs to be public to be called from the ISR function!
void handleInterrupt();
#endif
uint32_t _clockSpeed = 400000L; // 400kHz max on Arduino.
};
// Pointer to class instance (Note: if there is more than one bus, each will have
// its own instance of I2CManager, selected by the queueRequest function from
// the I2CBus field within the request block's I2CAddress).
extern I2CManagerClass I2CManager;
#endif
#endif

View File

@@ -1,230 +0,0 @@
/*
* © 2023, Neil McKechnie. All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef I2CMANAGER_AVR_H
#define I2CMANAGER_AVR_H
#include <Arduino.h>
#include "I2CManager.h"
#include "I2CManager_NonBlocking.h" // to satisfy intellisense
#include <avr/io.h>
#include <avr/interrupt.h>
/****************************************************************************
TWI State codes
****************************************************************************/
// General TWI Master staus codes
#define TWI_START 0x08 // START has been transmitted
#define TWI_REP_START 0x10 // Repeated START has been transmitted
#define TWI_ARB_LOST 0x38 // Arbitration lost
// TWI Master Transmitter staus codes
#define TWI_MTX_ADR_ACK 0x18 // SLA+W has been tramsmitted and ACK received
#define TWI_MTX_ADR_NACK 0x20 // SLA+W has been tramsmitted and NACK received
#define TWI_MTX_DATA_ACK 0x28 // Data byte has been tramsmitted and ACK received
#define TWI_MTX_DATA_NACK 0x30 // Data byte has been tramsmitted and NACK received
// TWI Master Receiver staus codes
#define TWI_MRX_ADR_ACK 0x40 // SLA+R has been tramsmitted and ACK received
#define TWI_MRX_ADR_NACK 0x48 // SLA+R has been tramsmitted and NACK received
#define TWI_MRX_DATA_ACK 0x50 // Data byte has been received and ACK tramsmitted
#define TWI_MRX_DATA_NACK 0x58 // Data byte has been received and NACK tramsmitted
// TWI Miscellaneous status codes
#define TWI_NO_STATE 0xF8 // No relevant state information available
#define TWI_BUS_ERROR 0x00 // Bus error due to an illegal START or STOP condition
#define TWI_TWBR ((F_CPU / I2C_FREQ) - 16) / 2 // TWI Bit rate Register setting.
#if defined(I2C_USE_INTERRUPTS)
#define ENABLE_TWI_INTERRUPT (1<<TWIE)
#else
#define ENABLE_TWI_INTERRUPT 0
#endif
/***************************************************************************
* Set I2C clock speed register.
***************************************************************************/
void I2CManagerClass::I2C_setClock(unsigned long i2cClockSpeed) {
unsigned long temp = ((F_CPU / i2cClockSpeed) - 16) / 2;
for (uint8_t preScaler = 0; preScaler<=3; preScaler++) {
if (temp <= 255) {
TWBR = temp;
TWSR = (TWSR & 0xfc) | preScaler;
return;
} else
temp /= 4;
}
// Set slowest speed ~= 500 bits/sec
TWBR = 255;
TWSR |= 0x03;
}
/***************************************************************************
* Initialise I2C registers.
***************************************************************************/
void I2CManagerClass::I2C_init()
{
TWSR = 0;
TWBR = TWI_TWBR; // Set bit rate register (Baudrate). Defined in header file.
TWDR = 0xFF; // Default content = SDA released.
TWCR = (1<<TWINT); // Clear interrupt flag
pinMode(SDA, INPUT_PULLUP);
pinMode(SCL, INPUT_PULLUP);
}
/***************************************************************************
* Initiate a start bit for transmission.
***************************************************************************/
void I2CManagerClass::I2C_sendStart() {
rxCount = 0;
txCount = 0;
// We may have already triggered a stop bit in the same run as this. To avoid
// clearing that bit before the stop bit has been sent, we can either wait for
// it to complete or we can OR the bit onto the existing bits.
TWCR |= (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT)|(1<<TWEA)|(1<<TWSTA); // Send Start
}
/***************************************************************************
* Initiate a stop bit for transmission (does not interrupt)
***************************************************************************/
void I2CManagerClass::I2C_sendStop() {
TWDR = 0xff; // Default condition = SDA released
TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWSTO); // Send Stop
}
/***************************************************************************
* Close I2C down
***************************************************************************/
void I2CManagerClass::I2C_close() {
// disable TWI
TWCR = (1<<TWINT); // clear any interrupt and stop twi.
delayMicroseconds(10); // Wait for things to stabilise (hopefully)
}
/***************************************************************************
* Main state machine for I2C, called from interrupt handler or,
* if I2C_USE_INTERRUPTS isn't defined, from the I2CManagerClass::loop() function
* (and therefore, indirectly, from I2CRB::wait() and I2CRB::isBusy()).
***************************************************************************/
void I2CManagerClass::I2C_handleInterrupt() {
if (!(TWCR & (1<<TWINT))) return; // Nothing to do.
uint8_t twsr = TWSR & 0xF8;
// Main I2C interrupt handler, used for the device communications.
// The following variables are used:
// bytesToSend, bytesToReceive (R/W)
// txCount, rxCount (W)
// deviceAddress (R)
// sendBuffer, receiveBuffer (R)
// operation (R)
// state, completionStatus (W)
//
// Cases are ordered so that the most frequently used ones are tested first.
switch (twsr) {
case TWI_MTX_DATA_ACK: // Data byte has been transmitted and ACK received
case TWI_MTX_ADR_ACK: // SLA+W has been transmitted and ACK received
if (bytesToSend) { // Send first.
if (operation == OPERATION_SEND_P)
TWDR = GETFLASH(sendBuffer + (txCount++));
else
TWDR = sendBuffer[txCount++];
bytesToSend--;
TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT);
} else if (bytesToReceive) { // All sent, anything to receive?
// Don't need to wait for stop, as the interface won't send the start until
// any in-progress stop condition from previous interrupts has been sent.
TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT)|(1<<TWSTA); // Send Start
} else {
// Nothing left to send or receive
TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWEA)|(1<<TWSTO); // Send Stop
state = I2C_STATE_COMPLETED;
}
break;
case TWI_MRX_DATA_ACK: // Data byte has been received and ACK transmitted
if (bytesToReceive > 0) {
receiveBuffer[rxCount++] = TWDR;
bytesToReceive--;
}
/* fallthrough */
case TWI_MRX_ADR_ACK: // SLA+R has been sent and ACK received
if (bytesToReceive <= 1) {
TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT); // Send NACK after next reception
} else {
// send ack
TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT)|(1<<TWEA);
}
break;
case TWI_MRX_DATA_NACK: // Data byte has been received and NACK transmitted
if (bytesToReceive > 0) {
receiveBuffer[rxCount++] = TWDR;
bytesToReceive--;
}
TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWEA)|(1<<TWSTO); // Send Stop
state = I2C_STATE_COMPLETED;
break;
case TWI_START: // START has been transmitted
case TWI_REP_START: // Repeated START has been transmitted
// Set up address and R/W
if (operation == OPERATION_READ || (operation==OPERATION_REQUEST && !bytesToSend))
TWDR = (deviceAddress << 1) | 1; // SLA+R
else
TWDR = (deviceAddress << 1) | 0; // SLA+W
TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT)|(1<<TWEA);
break;
case TWI_MTX_ADR_NACK: // SLA+W has been transmitted and NACK received
case TWI_MRX_ADR_NACK: // SLA+R has been transmitted and NACK received
case TWI_MTX_DATA_NACK: // Data byte has been transmitted and NACK received
TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWEA)|(1<<TWSTO); // Send Stop
completionStatus = I2C_STATUS_NEGATIVE_ACKNOWLEDGE;
state = I2C_STATE_COMPLETED;
break;
case TWI_ARB_LOST: // Arbitration lost
// Restart transaction from start.
I2C_sendStart();
break;
case TWI_BUS_ERROR: // Bus error due to an illegal START or STOP condition
default:
TWDR = 0xff; // Default condition = SDA released
TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWEA)|(1<<TWSTO); // Send Stop
completionStatus = I2C_STATUS_TRANSMIT_ERROR;
state = I2C_STATE_COMPLETED;
}
}
#if defined(I2C_USE_INTERRUPTS)
ISR(TWI_vect) {
I2CManager.handleInterrupt();
}
#endif
#endif /* I2CMANAGER_AVR_H */

View File

@@ -1,159 +0,0 @@
/*
* © 2023, Neil McKechnie. All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef I2CMANAGER_MEGA4809_H
#define I2CMANAGER_MEGA4809_H
#include <Arduino.h>
#include "I2CManager.h"
/***************************************************************************
* Set I2C clock speed register.
***************************************************************************/
void I2CManagerClass::I2C_setClock(unsigned long i2cClockSpeed) {
uint16_t t_rise;
if (i2cClockSpeed < 200000)
t_rise = 1000;
else if (i2cClockSpeed < 800000)
t_rise = 300;
else
t_rise = 120;
if (t_rise == 120)
TWI0.CTRLA |= TWI_FMPEN_bm;
else
TWI0.CTRLA &= ~TWI_FMPEN_bm;
uint32_t baud = (F_CPU_CORRECTED / i2cClockSpeed - F_CPU_CORRECTED / 1000 / 1000
* t_rise / 1000 - 10) / 2;
if (baud > 255) baud = 255; // ~30kHz
TWI0.MBAUD = (uint8_t)baud;
}
/***************************************************************************
* Initialise I2C registers.
***************************************************************************/
void I2CManagerClass::I2C_init()
{
pinMode(PIN_WIRE_SDA, INPUT_PULLUP);
pinMode(PIN_WIRE_SCL, INPUT_PULLUP);
PORTMUX.TWISPIROUTEA |= TWI_MUX;
I2C_setClock(I2C_FREQ);
#if defined(I2C_USE_INTERRUPTS)
TWI0.MCTRLA = TWI_RIEN_bm | TWI_WIEN_bm | TWI_ENABLE_bm;
#else
TWI0.MCTRLA = TWI_ENABLE_bm;
#endif
TWI0.MSTATUS = TWI_BUSSTATE_IDLE_gc;
}
/***************************************************************************
* Initiate a start bit for transmission, followed by address and R/W
***************************************************************************/
void I2CManagerClass::I2C_sendStart() {
txCount = 0;
rxCount = 0;
// If anything to send, initiate write. Otherwise initiate read.
if (operation == OPERATION_READ || ((operation == OPERATION_REQUEST) && !bytesToSend))
TWI0.MADDR = (currentRequest->i2cAddress << 1) | 1;
else
TWI0.MADDR = (currentRequest->i2cAddress << 1) | 0;
}
/***************************************************************************
* Initiate a stop bit for transmission.
***************************************************************************/
void I2CManagerClass::I2C_sendStop() {
TWI0.MCTRLB = TWI_MCMD_STOP_gc;
}
/***************************************************************************
* Close I2C down
***************************************************************************/
void I2CManagerClass::I2C_close() {
TWI0.MCTRLA &= ~(TWI_RIEN_bm | TWI_WIEN_bm | TWI_ENABLE_bm); // Switch off I2C
TWI0.MSTATUS = TWI_BUSSTATE_UNKNOWN_gc;
delayMicroseconds(10); // Wait for things to stabilise (hopefully)
}
/***************************************************************************
* Main state machine for I2C, called from interrupt handler.
***************************************************************************/
void I2CManagerClass::I2C_handleInterrupt() {
uint8_t currentStatus = TWI0.MSTATUS;
if (currentStatus & TWI_ARBLOST_bm) {
// Arbitration lost, restart
TWI0.MSTATUS = currentStatus; // clear all flags
I2C_sendStart(); // Reinitiate request
} else if (currentStatus & TWI_BUSERR_bm) {
// Bus error
completionStatus = I2C_STATUS_BUS_ERROR;
state = I2C_STATE_COMPLETED;
TWI0.MSTATUS = currentStatus; // clear all flags
} else if (currentStatus & TWI_WIF_bm) {
// Master write completed
if (currentStatus & TWI_RXACK_bm) {
// Nacked, send stop.
TWI0.MCTRLB = TWI_MCMD_STOP_gc;
completionStatus = I2C_STATUS_NEGATIVE_ACKNOWLEDGE;
state = I2C_STATE_COMPLETED;
} else if (bytesToSend) {
// Acked, so send next byte (don't need to use GETFLASH)
TWI0.MDATA = sendBuffer[txCount++];
bytesToSend--;
} else if (bytesToReceive) {
// Last sent byte acked and no more to send. Send repeated start, address and read bit.
TWI0.MADDR = (deviceAddress << 1) | 1;
} else {
// No more data to send/receive. Initiate a STOP condition.
TWI0.MCTRLB = TWI_MCMD_STOP_gc;
state = I2C_STATE_COMPLETED;
}
} else if (currentStatus & TWI_RIF_bm) {
// Master read completed without errors
if (bytesToReceive) {
receiveBuffer[rxCount++] = TWI0.MDATA; // Store received byte
bytesToReceive--;
}
if (bytesToReceive) {
// More bytes to receive, issue ack and start another read
TWI0.MCTRLB = TWI_MCMD_RECVTRANS_gc;
} else {
// Transaction finished, issue NACK and STOP.
TWI0.MCTRLB = TWI_ACKACT_bm | TWI_MCMD_STOP_gc;
state = I2C_STATE_COMPLETED;
}
}
}
/***************************************************************************
* Interrupt handler.
***************************************************************************/
ISR(TWI0_TWIM_vect) {
I2CManager.handleInterrupt();
}
#endif

View File

@@ -1,382 +0,0 @@
/*
* © 2023, Neil McKechnie
* © 2022 Paul M Antoine
* All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef I2CMANAGER_NONBLOCKING_H
#define I2CMANAGER_NONBLOCKING_H
#include <Arduino.h>
#include "I2CManager.h"
// Support for atomic isolation (i.e. a block with interrupts disabled).
// E.g.
// ATOMIC_BLOCK() {
// doSomethingWithInterruptsDisabled();
// }
// This has the advantage over simple noInterrupts/Interrupts that the
// original interrupt state is restored when the block finishes.
//
// (This should really be defined in an include file somewhere more global, so
// it can replace use of noInterrupts/interrupts in other parts of DCC-EX.
//
static inline uint8_t _deferInterrupts(void) {
noInterrupts();
return 1;
}
static inline void _conditionalEnableInterrupts(bool *wasEnabled) {
if (*wasEnabled) interrupts();
}
#define ATOMIC_BLOCK(x) \
for (bool _int_saved __attribute__((__cleanup__(_conditionalEnableInterrupts))) \
=_getInterruptState(),_ToDo=_deferInterrupts(); _ToDo; _ToDo=0)
#if defined(__AVR__) // Nano, Uno, Mega2580, NanoEvery, etc.
static inline bool _getInterruptState(void) {
return bitRead(SREG, SREG_I); // true if enabled, false if disabled
}
#elif defined(__arm__) // STM32, SAMD, Teensy
static inline bool _getInterruptState( void ) {
uint32_t reg;
__asm__ __volatile__ ("MRS %0, primask" : "=r" (reg) );
return !(reg & 1); // true if interrupts enabled, false otherwise
}
#else
#warning "ATOMIC_BLOCK() not defined for this target type, I2C interrupts disabled"
#define ATOMIC_BLOCK(x) // expand to nothing.
#ifdef I2C_USE_INTERRUPTS
#undef I2C_USE_INTERRUPTS
#endif
#endif
// This module is only compiled if I2C_USE_WIRE is not defined, so undefine it here
// to get intellisense to work correctly.
#if defined(I2C_USE_WIRE)
#undef I2C_USE_WIRE
#endif
enum MuxPhase: uint8_t {
MuxPhase_OFF = 0,
MuxPhase_PROLOG,
MuxPhase_PAYLOAD,
MuxPhase_EPILOG,
} ;
/***************************************************************************
* Initialise the I2CManagerAsync class.
***************************************************************************/
void I2CManagerClass::_initialise()
{
queueHead = queueTail = NULL;
state = I2C_STATE_FREE;
I2C_init();
_setClock(_clockSpeed);
}
/***************************************************************************
* Set I2C clock speed. Normally 100000 (Standard) or 400000 (Fast)
* on Arduino. Mega4809 supports 1000000 (Fast+) too.
* This function saves the desired clock speed and the startTransaction
* function acts on it before a new transaction, to avoid speed changes
* during an I2C transaction.
***************************************************************************/
void I2CManagerClass::_setClock(unsigned long i2cClockSpeed) {
pendingClockSpeed = i2cClockSpeed;
}
/***************************************************************************
* Start an I2C transaction, if the I2C interface is free and
* there is a queued request to be processed.
* If there's an I2C clock speed change pending, then implement it before
* starting the operation.
***************************************************************************/
void I2CManagerClass::startTransaction() {
ATOMIC_BLOCK() {
if ((state == I2C_STATE_FREE) && (queueHead != NULL)) {
state = I2C_STATE_ACTIVE;
completionStatus = I2C_STATUS_OK;
// Check for pending clock speed change
if (pendingClockSpeed) {
// We're about to start a new I2C transaction, so set clock now.
I2C_setClock(pendingClockSpeed);
pendingClockSpeed = 0;
}
startTime = micros();
currentRequest = queueHead;
rxCount = txCount = 0;
// Start the I2C process going.
#if defined(I2C_EXTENDED_ADDRESS)
I2CMux muxNumber = currentRequest->i2cAddress.muxNumber();
if (muxNumber != I2CMux_None) {
muxPhase = MuxPhase_PROLOG;
uint8_t subBus = currentRequest->i2cAddress.subBus();
muxData[0] = (subBus == SubBus_All) ? 0xff :
(subBus == SubBus_None) ? 0x00 :
#if defined(I2CMUX_PCA9547)
0x08 | subBus;
#elif defined(I2CMUX_PCA9542) || defined(I2CMUX_PCA9544)
0x04 | subBus; // NB Only 2 or 4 subbuses respectively
#else
// Default behaviour for most MUXs is to use a mask
// with a bit set for the subBus to be enabled
1 << subBus;
#endif
deviceAddress = I2C_MUX_BASE_ADDRESS + muxNumber;
sendBuffer = &muxData[0];
bytesToSend = 1;
bytesToReceive = 0;
operation = OPERATION_SEND;
} else {
// Send/receive payload for device only.
muxPhase = MuxPhase_OFF;
deviceAddress = currentRequest->i2cAddress;
sendBuffer = currentRequest->writeBuffer;
bytesToSend = currentRequest->writeLen;
receiveBuffer = currentRequest->readBuffer;
bytesToReceive = currentRequest->readLen;
operation = currentRequest->operation & OPERATION_MASK;
}
#else
deviceAddress = currentRequest->i2cAddress;
sendBuffer = currentRequest->writeBuffer;
bytesToSend = currentRequest->writeLen;
receiveBuffer = currentRequest->readBuffer;
bytesToReceive = currentRequest->readLen;
operation = currentRequest->operation & OPERATION_MASK;
#endif
I2C_sendStart();
}
}
}
/***************************************************************************
* Function to queue a request block and initiate operations.
***************************************************************************/
void I2CManagerClass::queueRequest(I2CRB *req) {
req->status = I2C_STATUS_PENDING;
req->nextRequest = NULL;
ATOMIC_BLOCK() {
if (!queueTail)
queueHead = queueTail = req; // Only item on queue
else
queueTail = queueTail->nextRequest = req; // Add to end
startTransaction();
}
}
/***************************************************************************
* Initiate a write to an I2C device (non-blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::write(I2CAddress i2cAddress, const uint8_t *writeBuffer, uint8_t writeLen, I2CRB *req) {
// Make sure previous request has completed.
req->wait();
req->setWriteParams(i2cAddress, writeBuffer, writeLen);
queueRequest(req);
return I2C_STATUS_OK;
}
/***************************************************************************
* Initiate a write from PROGMEM (flash) to an I2C device (non-blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::write_P(I2CAddress i2cAddress, const uint8_t * writeBuffer, uint8_t writeLen, I2CRB *req) {
// Make sure previous request has completed.
req->wait();
req->setWriteParams(i2cAddress, writeBuffer, writeLen);
req->operation = OPERATION_SEND_P;
queueRequest(req);
return I2C_STATUS_OK;
}
/***************************************************************************
* Initiate a read from the I2C device, optionally preceded by a write
* (non-blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::read(I2CAddress i2cAddress, uint8_t *readBuffer, uint8_t readLen,
const uint8_t *writeBuffer, uint8_t writeLen, I2CRB *req)
{
// Make sure previous request has completed.
req->wait();
req->setRequestParams(i2cAddress, readBuffer, readLen, writeBuffer, writeLen);
queueRequest(req);
return I2C_STATUS_OK;
}
/***************************************************************************
* Set I2C timeout value in microseconds. The timeout applies to the entire
* I2CRB request, e.g. where a write+read is performed, the timer is not
* reset before the read.
***************************************************************************/
void I2CManagerClass::setTimeout(unsigned long value) {
_timeout = value;
};
/***************************************************************************
* checkForTimeout() function, called from isBusy() and wait() to cancel
* requests that are taking too long to complete. Such faults
* may be caused by an I2C wire short for example.
***************************************************************************/
void I2CManagerClass::checkForTimeout() {
ATOMIC_BLOCK() {
I2CRB *t = queueHead;
if (state==I2C_STATE_ACTIVE && t!=0 && t==currentRequest && _timeout > 0) {
// Check for timeout
unsigned long elapsed = micros() - startTime;
if (elapsed > _timeout) {
#ifdef DIAG_IO
//DIAG(F("I2CManager Timeout on %s"), t->i2cAddress.toString());
#endif
// Excessive time. Dequeue request
queueHead = t->nextRequest;
if (!queueHead) queueTail = NULL;
currentRequest = NULL;
bytesToReceive = bytesToSend = 0;
// Post request as timed out.
t->status = I2C_STATUS_TIMEOUT;
// Reset TWI interface so it is able to continue
// Try close and init, not entirely satisfactory but sort of works...
I2C_close(); // Shutdown and restart twi interface
// If SDA is stuck low, issue up to 9 clock pulses to attempt to free it.
pinMode(SCL, INPUT_PULLUP);
pinMode(SDA, INPUT_PULLUP);
for (int i=0; !digitalRead(SDA) && i<9; i++) {
digitalWrite(SCL, 0);
pinMode(SCL, OUTPUT); // Force clock low
delayMicroseconds(10); // ... for 5us
pinMode(SCL, INPUT_PULLUP); // ... then high
delayMicroseconds(10); // ... for 5us (100kHz Clock)
}
// Whether that's succeeded or not, now try reinitialising.
I2C_init();
_setClock(_clockSpeed);
state = I2C_STATE_FREE;
// Initiate next queued request if any.
startTransaction();
}
}
}
}
/***************************************************************************
* Loop function, for general background work
***************************************************************************/
void I2CManagerClass::loop() {
#if !defined(I2C_USE_INTERRUPTS)
handleInterrupt();
#endif
// Call function to monitor for stuck I2C operations.
checkForTimeout();
}
/***************************************************************************
* Interupt handler. Call I2C state machine, and dequeue request
* if completed.
***************************************************************************/
void I2CManagerClass::handleInterrupt() {
// Update hardware state machine
I2C_handleInterrupt();
// Check if current request has completed. If there's a current request
// and state isn't active then state contains the completion status of the request.
if (state == I2C_STATE_COMPLETED && currentRequest != NULL) {
// Operation has completed.
if (completionStatus == I2C_STATUS_OK || ++retryCounter > MAX_I2C_RETRIES
|| currentRequest->operation & OPERATION_NORETRY)
{
// Status is OK, or has failed and retry count exceeded, or retries disabled.
#if defined(I2C_EXTENDED_ADDRESS)
if (muxPhase == MuxPhase_PROLOG ) {
overallStatus = completionStatus;
uint8_t rbAddress = currentRequest->i2cAddress.deviceAddress();
if (completionStatus == I2C_STATUS_OK && rbAddress != 0) {
// Mux request OK, start handling application request.
muxPhase = MuxPhase_PAYLOAD;
deviceAddress = rbAddress;
sendBuffer = currentRequest->writeBuffer;
bytesToSend = currentRequest->writeLen;
receiveBuffer = currentRequest->readBuffer;
bytesToReceive = currentRequest->readLen;
operation = currentRequest->operation & OPERATION_MASK;
state = I2C_STATE_ACTIVE;
I2C_sendStart();
return;
}
} else if (muxPhase == MuxPhase_PAYLOAD) {
// Application request completed, now send epilogue to mux
overallStatus = completionStatus;
currentRequest->nBytes = rxCount; // Save number of bytes read into rb
if (_muxCount == 1) {
// Only one MUX, don't need to deselect subbus
muxPhase = MuxPhase_OFF;
} else {
muxPhase = MuxPhase_EPILOG;
deviceAddress = I2C_MUX_BASE_ADDRESS + currentRequest->i2cAddress.muxNumber();
muxData[0] = 0x00;
sendBuffer = &muxData[0];
bytesToSend = 1;
bytesToReceive = 0;
operation = OPERATION_SEND;
state = I2C_STATE_ACTIVE;
I2C_sendStart();
return;
}
} else if (muxPhase == MuxPhase_EPILOG) {
// Epilog finished, ignore completionStatus
muxPhase = MuxPhase_OFF;
} else
overallStatus = completionStatus;
#else
overallStatus = completionStatus;
currentRequest->nBytes = rxCount;
#endif
// Remove completed request from head of queue
I2CRB * t = queueHead;
if (t == currentRequest) {
queueHead = t->nextRequest;
if (!queueHead) queueTail = queueHead;
t->status = overallStatus;
// I2C state machine is now free for next request
currentRequest = NULL;
state = I2C_STATE_FREE;
}
retryCounter = 0;
} else {
// Status is failed and retry permitted.
// Retry previous request.
state = I2C_STATE_FREE;
}
}
if (state == I2C_STATE_FREE && queueHead != NULL) {
// Allow any pending interrupts before starting the next request.
//interrupts();
// Start next request
I2CManager.startTransaction();
}
}
#endif

View File

@@ -1,247 +0,0 @@
/*
* © 2022 Paul M Antoine
* © 2023, Neil McKechnie
* All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef I2CMANAGER_SAMD_H
#define I2CMANAGER_SAMD_H
#include <Arduino.h>
#include "I2CManager.h"
//#include <avr/io.h>
//#include <avr/interrupt.h>
#include <wiring_private.h>
/***************************************************************************
* Interrupt handler.
* IRQ handler for SERCOM3 which is the default I2C definition for Arduino Zero
* compatible variants such as the Sparkfun SAMD21 Dev Breakout etc.
* Later we may wish to allow use of an alternate I2C bus, or more than one I2C
* bus on the SAMD architecture
***************************************************************************/
#if defined(I2C_USE_INTERRUPTS) && defined(ARDUINO_SAMD_ZERO)
void SERCOM3_Handler() {
I2CManager.handleInterrupt();
}
#endif
// Assume SERCOM3 for now - default I2C bus on Arduino Zero and variants of same
Sercom *s = SERCOM3;
/***************************************************************************
* Set I2C clock speed register. This should only be called outside of
* a transmission. The I2CManagerClass::_setClock() function ensures
* that it is only called at the beginning of an I2C transaction.
***************************************************************************/
void I2CManagerClass::I2C_setClock(uint32_t i2cClockSpeed) {
// Calculate a rise time appropriate to the requested bus speed
int t_rise;
if (i2cClockSpeed < 200000L) {
i2cClockSpeed = 100000L; // NB: this overrides a "force clock" of lower than 100KHz!
t_rise = 1000;
} else if (i2cClockSpeed < 800000L) {
i2cClockSpeed = 400000L;
t_rise = 300;
} else if (i2cClockSpeed < 1200000L) {
i2cClockSpeed = 1000000L;
t_rise = 120;
} else {
i2cClockSpeed = 100000L;
t_rise = 1000;
}
// Wait while the bus is busy
while (s->I2CM.STATUS.bit.BUSSTATE != 0x1);
// Disable the I2C master mode and wait for sync
s->I2CM.CTRLA.bit.ENABLE = 0 ;
while (s->I2CM.SYNCBUSY.bit.ENABLE != 0);
// Calculate baudrate - using a rise time appropriate for the speed
s->I2CM.BAUD.bit.BAUD = SystemCoreClock / (2 * i2cClockSpeed) - 5 - (((SystemCoreClock / 1000000) * t_rise) / (2 * 1000));
// Enable the I2C master mode and wait for sync
s->I2CM.CTRLA.bit.ENABLE = 1 ;
while (s->I2CM.SYNCBUSY.bit.ENABLE != 0);
// Setting bus idle mode and wait for sync
s->I2CM.STATUS.bit.BUSSTATE = 1 ;
while (s->I2CM.SYNCBUSY.bit.SYSOP != 0);
}
/***************************************************************************
* Initialise I2C registers.
***************************************************************************/
void I2CManagerClass::I2C_init()
{
//Setting clock
GCLK->CLKCTRL.reg = GCLK_CLKCTRL_ID(GCM_SERCOM3_CORE) | // Generic Clock 0 (SERCOM3)
GCLK_CLKCTRL_GEN_GCLK0 | // Generic Clock Generator 0 is source
GCLK_CLKCTRL_CLKEN ;
/* Wait for peripheral clock synchronization */
while ( GCLK->STATUS.reg & GCLK_STATUS_SYNCBUSY );
// Software reset the SERCOM
s->I2CM.CTRLA.bit.SWRST = 1;
//Wait both bits Software Reset from CTRLA and SYNCBUSY are equal to 0
while(s->I2CM.CTRLA.bit.SWRST || s->I2CM.SYNCBUSY.bit.SWRST);
// Set master mode and enable SCL Clock Stretch mode (stretch after ACK bit)
s->I2CM.CTRLA.reg = SERCOM_I2CM_CTRLA_MODE( I2C_MASTER_OPERATION )/* |
SERCOM_I2CM_CTRLA_SCLSM*/ ;
// Enable Smart mode (but not Quick Command)
s->I2CM.CTRLB.reg = SERCOM_I2CM_CTRLB_SMEN;
#if defined(I2C_USE_INTERRUPTS)
// Setting NVIC
NVIC_EnableIRQ(SERCOM3_IRQn);
NVIC_SetPriority (SERCOM3_IRQn, SERCOM_NVIC_PRIORITY); // Match default SERCOM priorities
// NVIC_SetPriority (SERCOM3_IRQn, 0); // Set highest priority
// Enable all interrupts
s->I2CM.INTENSET.reg = SERCOM_I2CM_INTENSET_MB | SERCOM_I2CM_INTENSET_SB | SERCOM_I2CM_INTENSET_ERROR;
#endif
// Calculate baudrate and set default rate for now
s->I2CM.BAUD.bit.BAUD = SystemCoreClock / ( 2 * I2C_FREQ) - 7 / (2 * 1000);
// Enable the I2C master mode and wait for sync
s->I2CM.CTRLA.bit.ENABLE = 1 ;
while (s->I2CM.SYNCBUSY.bit.ENABLE != 0);
// Setting bus idle mode and wait for sync
s->I2CM.STATUS.bit.BUSSTATE = 1 ;
while (s->I2CM.SYNCBUSY.bit.SYSOP != 0);
// Set SDA/SCL pins as outputs and enable pullups, at present we assume these are
// the default ones for SERCOM3 (see assumption above)
pinPeripheral(PIN_WIRE_SDA, g_APinDescription[PIN_WIRE_SDA].ulPinType);
pinPeripheral(PIN_WIRE_SCL, g_APinDescription[PIN_WIRE_SCL].ulPinType);
// Enable the SCL and SDA pins on the sercom: includes increased driver strength,
// pull-up resistors and pin multiplexer
PORT->Group[g_APinDescription[PIN_WIRE_SCL].ulPort].PINCFG[g_APinDescription[PIN_WIRE_SCL].ulPin].reg =
PORT_PINCFG_DRVSTR | PORT_PINCFG_PULLEN | PORT_PINCFG_PMUXEN;
PORT->Group[g_APinDescription[PIN_WIRE_SDA].ulPort].PINCFG[g_APinDescription[PIN_WIRE_SDA].ulPin].reg =
PORT_PINCFG_DRVSTR | PORT_PINCFG_PULLEN | PORT_PINCFG_PMUXEN;
}
/***************************************************************************
* Initiate a start bit for transmission.
***************************************************************************/
void I2CManagerClass::I2C_sendStart() {
// Set counters here in case this is a retry.
txCount = 0;
rxCount = 0;
// On a single-master I2C bus, the start bit won't be sent until the bus
// state goes to IDLE so we can request it without waiting. On a
// multi-master bus, the bus may be BUSY under control of another master,
// in which case we can avoid some arbitration failures by waiting until
// the bus state is IDLE. We don't do that here.
// If anything to send, initiate write. Otherwise initiate read.
if (operation == OPERATION_READ || ((operation == OPERATION_REQUEST) && !bytesToSend))
{
// Send start and address with read flag (1) or'd in
s->I2CM.ADDR.bit.ADDR = (deviceAddress << 1) | 1;
}
else {
// Send start and address with write flag (0) or'd in
s->I2CM.ADDR.bit.ADDR = (deviceAddress << 1ul) | 0;
}
}
/***************************************************************************
* Initiate a stop bit for transmission (does not interrupt)
***************************************************************************/
void I2CManagerClass::I2C_sendStop() {
s->I2CM.CTRLB.bit.CMD = 3; // Stop condition
}
/***************************************************************************
* Close I2C down
***************************************************************************/
void I2CManagerClass::I2C_close() {
I2C_sendStop();
// Disable the I2C master mode and wait for sync
s->I2CM.CTRLA.bit.ENABLE = 0 ;
// Wait for up to 500us only.
unsigned long startTime = micros();
while (s->I2CM.SYNCBUSY.bit.ENABLE != 0) {
if (micros() - startTime >= 500UL) break;
}
}
/***************************************************************************
* Main state machine for I2C, called from interrupt handler or,
* if I2C_USE_INTERRUPTS isn't defined, from the I2CManagerClass::loop() function
* (and therefore, indirectly, from I2CRB::wait() and I2CRB::isBusy()).
***************************************************************************/
void I2CManagerClass::I2C_handleInterrupt() {
if (s->I2CM.STATUS.bit.ARBLOST) {
// Arbitration lost, restart
I2C_sendStart(); // Reinitiate request
} else if (s->I2CM.STATUS.bit.BUSERR) {
// Bus error
completionStatus = I2C_STATUS_BUS_ERROR;
state = I2C_STATE_COMPLETED; // Completed with error
} else if (s->I2CM.INTFLAG.bit.MB) {
// Master write completed
if (s->I2CM.STATUS.bit.RXNACK) {
// Nacked, send stop.
I2C_sendStop();
completionStatus = I2C_STATUS_NEGATIVE_ACKNOWLEDGE;
state = I2C_STATE_COMPLETED; // Completed with error
} else if (bytesToSend) {
// Acked, so send next byte
s->I2CM.DATA.bit.DATA = sendBuffer[txCount++];
bytesToSend--;
} else if (bytesToReceive) {
// Last sent byte acked and no more to send. Send repeated start, address and read bit.
s->I2CM.ADDR.bit.ADDR = (deviceAddress << 1) | 1;
} else {
// No more data to send/receive. Initiate a STOP condition
I2C_sendStop();
state = I2C_STATE_COMPLETED; // Completed OK
}
} else if (s->I2CM.INTFLAG.bit.SB) {
// Master read completed without errors
if (bytesToReceive == 1) {
s->I2CM.CTRLB.bit.ACKACT = 1; // NAK final byte
I2C_sendStop(); // send stop
receiveBuffer[rxCount++] = s->I2CM.DATA.bit.DATA; // Store received byte
bytesToReceive = 0;
state = I2C_STATE_COMPLETED; // Completed OK
} else if (bytesToReceive) {
s->I2CM.CTRLB.bit.ACKACT = 0; // ACK all but final byte
receiveBuffer[rxCount++] = s->I2CM.DATA.bit.DATA; // Store received byte
bytesToReceive--;
}
}
}
#endif /* I2CMANAGER_SAMD_H */

View File

@@ -1,312 +0,0 @@
/*
* © 2022-23 Paul M Antoine
* © 2023, Neil McKechnie
* All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef I2CMANAGER_STM32_H
#define I2CMANAGER_STM32_H
#include <Arduino.h>
#include "I2CManager.h"
#include "I2CManager_NonBlocking.h" // to satisfy intellisense
//#include <avr/io.h>
//#include <avr/interrupt.h>
#include <wiring_private.h>
/***************************************************************************
* Interrupt handler.
* IRQ handler for SERCOM3 which is the default I2C definition for Arduino Zero
* compatible variants such as the Sparkfun SAMD21 Dev Breakout etc.
* Later we may wish to allow use of an alternate I2C bus, or more than one I2C
* bus on the SAMD architecture
***************************************************************************/
#if defined(I2C_USE_INTERRUPTS) && defined(ARDUINO_ARCH_STM32)
void I2C1_IRQHandler() {
I2CManager.handleInterrupt();
}
#endif
// Assume I2C1 for now - default I2C bus on Nucleo-F411RE and likely Nucleo-64 variants
I2C_TypeDef *s = I2C1;
#define I2C_IRQn I2C1_EV_IRQn
#define I2C_BUSFREQ 16
// I2C SR1 Status Register #1 bit definitions for convenience
// #define I2C_SR1_SMBALERT (1<<15) // SMBus alert
// #define I2C_SR1_TIMEOUT (1<<14) // Timeout of Tlow error
// #define I2C_SR1_PECERR (1<<12) // PEC error in reception
// #define I2C_SR1_OVR (1<<11) // Overrun/Underrun error
// #define I2C_SR1_AF (1<<10) // Acknowledge failure
// #define I2C_SR1_ARLO (1<<9) // Arbitration lost (master mode)
// #define I2C_SR1_BERR (1<<8) // Bus error (misplaced start or stop condition)
// #define I2C_SR1_TxE (1<<7) // Data register empty on transmit
// #define I2C_SR1_RxNE (1<<6) // Data register not empty on receive
// #define I2C_SR1_STOPF (1<<4) // Stop detection (slave mode)
// #define I2C_SR1_ADD10 (1<<3) // 10 bit header sent
// #define I2C_SR1_BTF (1<<2) // Byte transfer finished - data transfer done
// #define I2C_SR1_ADDR (1<<1) // Address sent (master) or matched (slave)
// #define I2C_SR1_SB (1<<0) // Start bit (master mode) 1=start condition generated
// I2C CR1 Control Register #1 bit definitions for convenience
// #define I2C_CR1_SWRST (1<<15) // Software reset - places peripheral under reset
// #define I2C_CR1_ALERT (1<<13) // SMBus alert assertion
// #define I2C_CR1_PEC (1<<12) // Packet Error Checking transfer in progress
// #define I2C_CR1_POS (1<<11) // Acknowledge/PEC Postion (for data reception in PEC mode)
// #define I2C_CR1_ACK (1<<10) // Acknowledge enable - ACK returned after byte is received (address or data)
// #define I2C_CR1_STOP (1<<9) // STOP generated
// #define I2C_CR1_START (1<<8) // START generated
// #define I2C_CR1_NOSTRETCH (1<<7) // Clock stretching disable (slave mode)
// #define I2C_CR1_ENGC (1<<6) // General call (broadcast) enable (address 00h is ACKed)
// #define I2C_CR1_ENPEC (1<<5) // PEC Enable
// #define I2C_CR1_ENARP (1<<4) // ARP enable (SMBus)
// #define I2C_CR1_SMBTYPE (1<<3) // SMBus type, 1=host, 0=device
// #define I2C_CR1_SMBUS (1<<1) // SMBus mode, 1=SMBus, 0=I2C
// #define I2C_CR1_PE (1<<0) // I2C Peripheral enable
/***************************************************************************
* Set I2C clock speed register. This should only be called outside of
* a transmission. The I2CManagerClass::_setClock() function ensures
* that it is only called at the beginning of an I2C transaction.
***************************************************************************/
void I2CManagerClass::I2C_setClock(uint32_t i2cClockSpeed) {
// Calculate a rise time appropriate to the requested bus speed
// Use 10x the rise time spec to enable integer divide of 62.5ns clock period
uint16_t t_rise;
uint32_t ccr_freq;
if (i2cClockSpeed < 200000L) {
// i2cClockSpeed = 100000L;
t_rise = 0x11; // (1000ns /62.5ns) + 1;
}
else if (i2cClockSpeed < 800000L)
{
i2cClockSpeed = 400000L;
t_rise = 0x06; // (300ns / 62.5ns) + 1;
// } else if (i2cClockSpeed < 1200000L) {
// i2cClockSpeed = 1000000L;
// t_rise = 120;
}
else
{
i2cClockSpeed = 100000L;
t_rise = 0x11; // (1000ns /62.5ns) + 1;
}
// Enable the I2C master mode
s->CR1 &= ~(I2C_CR1_PE); // Enable I2C
// Software reset the I2C peripheral
// s->CR1 |= I2C_CR1_SWRST; // reset the I2C
// Release reset
// s->CR1 &= ~(I2C_CR1_SWRST); // Normal operation
// Calculate baudrate - using a rise time appropriate for the speed
ccr_freq = I2C_BUSFREQ * 1000000 / i2cClockSpeed / 2;
// Bit 15: I2C Master mode, 0=standard, 1=Fast Mode
// Bit 14: Duty, fast mode duty cycle
// Bit 11-0: FREQR = 16MHz => TPCLK1 = 62.5ns, so CCR divisor must be 0x50 (80 * 62.5ns = 5000ns)
s->CCR = (uint16_t)ccr_freq;
// Configure the rise time register
s->TRISE = t_rise; // 1000 ns / 62.5 ns = 16 + 1
// Enable the I2C master mode
s->CR1 |= I2C_CR1_PE; // Enable I2C
}
/***************************************************************************
* Initialise I2C registers.
***************************************************************************/
void I2CManagerClass::I2C_init()
{
//Setting up the clocks
RCC->APB1ENR |= (1<<21); // Enable I2C CLOCK
RCC->AHB1ENR |= (1<<1); // Enable GPIOB CLOCK for PB8/PB9
// Standard I2C pins are SCL on PB8 and SDA on PB9
// Bits (17:16)= 1:0 --> Alternate Function for Pin PB8;
// Bits (19:18)= 1:0 --> Alternate Function for Pin PB9
GPIOB->MODER |= (2<<(8*2)) | (2<<(9*2)); // PB8 and PB9 set to ALT function
GPIOB->OTYPER |= (1<<8) | (1<<9); // PB8 and PB9 set to open drain output capability
GPIOB->OSPEEDR |= (3<<(8*2)) | (3<<(9*2)); // PB8 and PB9 set to High Speed mode
GPIOB->PUPDR |= (1<<(8*2)) | (1<<(9*2)); // PB8 and PB9 set to pull-up capability
// Alt Function High register routing pins PB8 and PB9 for I2C1:
// Bits (3:2:1:0) = 0:1:0:0 --> AF4 for pin PB8
// Bits (7:6:5:4) = 0:1:0:0 --> AF4 for pin PB9
GPIOB->AFR[1] |= (4<<0) | (4<<4); // PB8 on low nibble, PB9 on next nibble up
// Software reset the I2C peripheral
s->CR1 |= I2C_CR1_SWRST; // reset the I2C
s->CR1 &= ~(I2C_CR1_SWRST); // Normal operation
// Program the peripheral input clock in CR2 Register in order to generate correct timings
s->CR2 |= I2C_BUSFREQ; // PCLK1 FREQUENCY in MHz
#if defined(I2C_USE_INTERRUPTS)
// Setting NVIC
NVIC_SetPriority(I2C_IRQn, 1); // Match default priorities
NVIC_EnableIRQ(I2C_IRQn);
// CR2 Interrupt Settings
// Bit 15-13: reserved
// Bit 12: LAST - DMA last transfer
// Bit 11: DMAEN - DMA enable
// Bit 10: ITBUFEN - Buffer interrupt enable
// Bit 9: ITEVTEN - Event interrupt enable
// Bit 8: ITERREN - Error interrupt enable
// Bit 7-6: reserved
// Bit 5-0: FREQ - Peripheral clock frequency (max 50MHz)
// s->CR2 |= 0x0700; // Enable Buffer, Event and Error interrupts
s->CR2 |= 0x0300; // Enable Event and Error interrupts
#endif
// Calculate baudrate and set default rate for now
// Configure the Clock Control Register for 100KHz SCL frequency
// Bit 15: I2C Master mode, 0=standard, 1=Fast Mode
// Bit 14: Duty, fast mode duty cycle
// Bit 11-0: FREQR = 16MHz => TPCLK1 = 62.5ns, so CCR divisor must be 0x50 (80 * 62.5ns = 5000ns)
s->CCR = 0x0050;
// Configure the rise time register - max allowed in 1000ns
s->TRISE = 0x0011; // 1000 ns / 62.5 ns = 16 + 1
// Enable the I2C master mode
s->CR1 |= I2C_CR1_PE; // Enable I2C
// Setting bus idle mode and wait for sync
}
/***************************************************************************
* Initiate a start bit for transmission.
***************************************************************************/
void I2CManagerClass::I2C_sendStart() {
// Set counters here in case this is a retry.
rxCount = txCount = 0;
uint8_t temp;
// On a single-master I2C bus, the start bit won't be sent until the bus
// state goes to IDLE so we can request it without waiting. On a
// multi-master bus, the bus may be BUSY under control of another master,
// in which case we can avoid some arbitration failures by waiting until
// the bus state is IDLE. We don't do that here.
// If anything to send, initiate write. Otherwise initiate read.
if (operation == OPERATION_READ || ((operation == OPERATION_REQUEST) && !bytesToSend))
{
// Send start for read operation
s->CR1 |= I2C_CR1_ACK; // Enable the ACK
s->CR1 |= I2C_CR1_START; // Generate START
// Send address with read flag (1) or'd in
s->DR = (deviceAddress << 1) | 1; // send the address
while (!(s->SR1 && I2C_SR1_ADDR)); // wait for ADDR bit to set
// Special case for 1 byte reads!
if (bytesToReceive == 1)
{
s->CR1 &= ~I2C_CR1_ACK; // clear the ACK bit
temp = I2C1->SR1 | I2C1->SR2; // read SR1 and SR2 to clear the ADDR bit.... EV6 condition
s->CR1 |= I2C_CR1_STOP; // Stop I2C
}
else
temp = s->SR1 | s->SR2; // read SR1 and SR2 to clear the ADDR bit
}
else {
// Send start for write operation
s->CR1 |= I2C_CR1_ACK; // Enable the ACK
s->CR1 |= I2C_CR1_START; // Generate START
// Send address with write flag (0) or'd in
s->DR = (deviceAddress << 1) | 0; // send the address
while (!(s->SR1 && I2C_SR1_ADDR)); // wait for ADDR bit to set
temp = s->SR1 | s->SR2; // read SR1 and SR2 to clear the ADDR bit
}
}
/***************************************************************************
* Initiate a stop bit for transmission (does not interrupt)
***************************************************************************/
void I2CManagerClass::I2C_sendStop() {
s->CR1 |= I2C_CR1_STOP; // Stop I2C
}
/***************************************************************************
* Close I2C down
***************************************************************************/
void I2CManagerClass::I2C_close() {
I2C_sendStop();
// Disable the I2C master mode and wait for sync
s->CR1 &= ~I2C_CR1_PE; // Disable I2C peripheral
// Should never happen, but wait for up to 500us only.
unsigned long startTime = micros();
while ((s->CR1 && I2C_CR1_PE) != 0) {
if (micros() - startTime >= 500UL) break;
}
}
/***************************************************************************
* Main state machine for I2C, called from interrupt handler or,
* if I2C_USE_INTERRUPTS isn't defined, from the I2CManagerClass::loop() function
* (and therefore, indirectly, from I2CRB::wait() and I2CRB::isBusy()).
***************************************************************************/
void I2CManagerClass::I2C_handleInterrupt() {
if (s->SR1 && I2C_SR1_ARLO) {
// Arbitration lost, restart
I2C_sendStart(); // Reinitiate request
} else if (s->SR1 && I2C_SR1_BERR) {
// Bus error
completionStatus = I2C_STATUS_BUS_ERROR;
state = I2C_STATE_COMPLETED;
} else if (s->SR1 && I2C_SR1_TXE) {
// Master write completed
if (s->SR1 && (1<<10)) {
// Nacked, send stop.
I2C_sendStop();
completionStatus = I2C_STATUS_NEGATIVE_ACKNOWLEDGE;
state = I2C_STATE_COMPLETED;
} else if (bytesToSend) {
// Acked, so send next byte
s->DR = sendBuffer[txCount++];
bytesToSend--;
} else if (bytesToReceive) {
// Last sent byte acked and no more to send. Send repeated start, address and read bit.
// s->I2CM.ADDR.bit.ADDR = (deviceAddress << 1) | 1;
} else {
// Check both TxE/BTF == 1 before generating stop
while (!(s->SR1 && I2C_SR1_TXE)); // Check TxE
while (!(s->SR1 && I2C_SR1_BTF)); // Check BTF
// No more data to send/receive. Initiate a STOP condition and finish
I2C_sendStop();
state = I2C_STATE_COMPLETED;
}
} else if (s->SR1 && I2C_SR1_RXNE) {
// Master read completed without errors
if (bytesToReceive == 1) {
// s->I2CM.CTRLB.bit.ACKACT = 1; // NAK final byte
I2C_sendStop(); // send stop
receiveBuffer[rxCount++] = s->DR; // Store received byte
bytesToReceive = 0;
state = I2C_STATE_COMPLETED;
} else if (bytesToReceive) {
// s->I2CM.CTRLB.bit.ACKACT = 0; // ACK all but final byte
receiveBuffer[rxCount++] = s->DR; // Store received byte
bytesToReceive--;
}
}
}
#endif /* I2CMANAGER_STM32_H */

View File

@@ -1,234 +0,0 @@
/*
* © 2023, Neil McKechnie. All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef I2CMANAGER_WIRE_H
#define I2CMANAGER_WIRE_H
#include <Arduino.h>
#include <Wire.h>
#include "I2CManager.h"
// This module is only compiled if I2C_USE_WIRE is defined, so define it here
// to get intellisense to work correctly.
#if !defined(I2C_USE_WIRE)
#define I2C_USE_WIRE
#endif
// Older versions of Wire don't have setWireTimeout function. AVR does.
#ifdef ARDUINO_ARCH_AVR
#define WIRE_HAS_TIMEOUT
#endif
/***************************************************************************
* Initialise I2C interface software
***************************************************************************/
void I2CManagerClass::_initialise() {
Wire.begin();
#if defined(WIRE_HAS_TIMEOUT)
Wire.setWireTimeout(_timeout, true);
#endif
}
/***************************************************************************
* Set I2C clock speed. Normally 100000 (Standard) or 400000 (Fast)
* on Arduino. Mega4809 supports 1000000 (Fast+) too.
***************************************************************************/
void I2CManagerClass::_setClock(unsigned long i2cClockSpeed) {
Wire.setClock(i2cClockSpeed);
}
/***************************************************************************
* Set I2C timeout value in microseconds. The timeout applies to each
* Wire call separately, i.e. in a write+read, the timer is reset before the
* read is started.
***************************************************************************/
void I2CManagerClass::setTimeout(unsigned long value) {
_timeout = value;
#if defined(WIRE_HAS_TIMEOUT)
Wire.setWireTimeout(value, true);
#endif
}
/********************************************************
* Helper function for I2C Multiplexer operations
********************************************************/
#ifdef I2C_EXTENDED_ADDRESS
static uint8_t muxSelect(I2CAddress address) {
// Select MUX sub bus.
I2CMux muxNo = address.muxNumber();
I2CSubBus subBus = address.subBus();
if (muxNo != I2CMux_None) {
Wire.beginTransmission(I2C_MUX_BASE_ADDRESS+muxNo);
uint8_t data = (subBus == SubBus_All) ? 0xff :
(subBus == SubBus_None) ? 0x00 :
#if defined(I2CMUX_PCA9547)
0x08 | subBus;
#elif defined(I2CMUX_PCA9542) || defined(I2CMUX_PCA9544)
0x04 | subBus; // NB Only 2 or 4 subbuses respectively
#else
// Default behaviour for most MUXs is to use a mask
// with a bit set for the subBus to be enabled
1 << subBus;
#endif
Wire.write(&data, 1);
return Wire.endTransmission(true); // have to release I2C bus for it to work
}
return I2C_STATUS_OK;
}
#endif
/***************************************************************************
* Initiate a write to an I2C device (blocking operation on Wire)
***************************************************************************/
uint8_t I2CManagerClass::write(I2CAddress address, const uint8_t buffer[], uint8_t size, I2CRB *rb) {
uint8_t status, muxStatus;
uint8_t retryCount = 0;
// If request fails, retry up to the defined limit, unless the NORETRY flag is set
// in the request block.
do {
status = muxStatus = I2C_STATUS_OK;
#ifdef I2C_EXTENDED_ADDRESS
if (address.muxNumber() != I2CMux_None)
muxStatus = muxSelect(address);
#endif
// Only send new transaction if address is non-zero.
if (muxStatus == I2C_STATUS_OK && address != 0) {
Wire.beginTransmission(address);
if (size > 0) Wire.write(buffer, size);
status = Wire.endTransmission();
}
#ifdef I2C_EXTENDED_ADDRESS
// Deselect MUX if there's more than one MUX present, to avoid having multiple ones selected
if (_muxCount > 1 && muxStatus == I2C_STATUS_OK
&& address.deviceAddress() != 0 && address.muxNumber() != I2CMux_None) {
muxSelect({address.muxNumber(), SubBus_None});
}
if (muxStatus != I2C_STATUS_OK) status = muxStatus;
#endif
} while (!(status == I2C_STATUS_OK
|| ++retryCount > MAX_I2C_RETRIES || rb->operation & OPERATION_NORETRY));
rb->status = status;
return I2C_STATUS_OK;
}
/***************************************************************************
* Initiate a write from PROGMEM (flash) to an I2C device (blocking operation on Wire)
***************************************************************************/
uint8_t I2CManagerClass::write_P(I2CAddress address, const uint8_t buffer[], uint8_t size, I2CRB *rb) {
uint8_t ramBuffer[size];
const uint8_t *p1 = buffer;
for (uint8_t i=0; i<size; i++)
ramBuffer[i] = GETFLASH(p1++);
return write(address, ramBuffer, size, rb);
}
/***************************************************************************
* Initiate a write (optional) followed by a read from the I2C device (blocking operation on Wire)
* If fewer than the number of requested bytes are received, status is I2C_STATUS_TRUNCATED.
***************************************************************************/
uint8_t I2CManagerClass::read(I2CAddress address, uint8_t readBuffer[], uint8_t readSize,
const uint8_t writeBuffer[], uint8_t writeSize, I2CRB *rb)
{
uint8_t status, muxStatus;
uint8_t nBytes = 0;
uint8_t retryCount = 0;
// If request fails, retry up to the defined limit, unless the NORETRY flag is set
// in the request block.
do {
status = muxStatus = I2C_STATUS_OK;
#ifdef I2C_EXTENDED_ADDRESS
if (address.muxNumber() != I2CMux_None) {
muxStatus = muxSelect(address);
}
#endif
// Only start new transaction if address is non-zero.
if (muxStatus == I2C_STATUS_OK && address != 0) {
if (writeSize > 0) {
Wire.beginTransmission(address);
Wire.write(writeBuffer, writeSize);
status = Wire.endTransmission(false); // Don't free bus yet
}
if (status == I2C_STATUS_OK) {
#ifdef WIRE_HAS_TIMEOUT
Wire.clearWireTimeoutFlag();
Wire.requestFrom(address, (size_t)readSize);
if (!Wire.getWireTimeoutFlag()) {
while (Wire.available() && nBytes < readSize)
readBuffer[nBytes++] = Wire.read();
if (nBytes < readSize) status = I2C_STATUS_TRUNCATED;
} else {
status = I2C_STATUS_TIMEOUT;
}
#else
Wire.requestFrom(address, (size_t)readSize);
while (Wire.available() && nBytes < readSize)
readBuffer[nBytes++] = Wire.read();
if (nBytes < readSize) status = I2C_STATUS_TRUNCATED;
#endif
}
}
#ifdef I2C_EXTENDED_ADDRESS
// Deselect MUX if there's more than one MUX present, to avoid having multiple ones selected
if (_muxCount > 1 && muxStatus == I2C_STATUS_OK && address != 0 && address.muxNumber() != I2CMux_None) {
muxSelect({address.muxNumber(), SubBus_None});
}
if (muxStatus != I2C_STATUS_OK) status = muxStatus;
#endif
} while (!((status == I2C_STATUS_OK)
|| ++retryCount > MAX_I2C_RETRIES || rb->operation & OPERATION_NORETRY));
rb->nBytes = nBytes;
rb->status = status;
return I2C_STATUS_OK;
}
/***************************************************************************
* Function to queue a request block and initiate operations.
*
* For the Wire version, this executes synchronously.
* The read/write/write_P functions return I2C_STATUS_OK always, and the
* completion status of the operation is in the request block, as for
* the non-blocking version.
***************************************************************************/
void I2CManagerClass::queueRequest(I2CRB *req) {
switch (req->operation & OPERATION_MASK) {
case OPERATION_READ:
read(req->i2cAddress, req->readBuffer, req->readLen, NULL, 0, req);
break;
case OPERATION_SEND:
write(req->i2cAddress, req->writeBuffer, req->writeLen, req);
break;
case OPERATION_SEND_P:
write_P(req->i2cAddress, req->writeBuffer, req->writeLen, req);
break;
case OPERATION_REQUEST:
read(req->i2cAddress, req->readBuffer, req->readLen, req->writeBuffer, req->writeLen, req);
break;
}
}
/***************************************************************************
* Loop function, for general background work
***************************************************************************/
void I2CManagerClass::loop() {}
#endif

View File

@@ -1,585 +0,0 @@
/*
* © 2021 Neil McKechnie
* © 2021 Harald Barth
* All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#include <Arduino.h>
#include "IODevice.h"
#include "DIAG.h"
#include "FSH.h"
#include "IO_MCP23017.h"
#include "DCCTimer.h"
#if defined(ARDUINO_ARCH_AVR) || defined(ARDUINO_ARCH_MEGAAVR)
#define USE_FAST_IO
#endif
// Link to halSetup function. If not defined, the function reference will be NULL.
extern __attribute__((weak)) void halSetup();
extern __attribute__((weak)) void exrailHalSetup();
//==================================================================================================================
// Static methods
//------------------------------------------------------------------------------------------------------------------
// Static functions
// Static method to initialise the IODevice subsystem.
#if !defined(IO_NO_HAL)
// Create any standard device instances that may be required, such as the Arduino pins
// and PCA9685.
void IODevice::begin() {
// Initialise the IO subsystem defaults
ArduinoPins::create(2, NUM_DIGITAL_PINS-2); // Reserve pins for direct access
// Call user's halSetup() function (if defined in the build in myHal.cpp).
// The contents will depend on the user's system hardware configuration.
// The myHal.cpp file is a standard C++ module so has access to all of the DCC++EX APIs.
// This is done early so that the subsequent defaults will detect an overlap and not
// create something that conflicts with the user's vpin definitions.
if (halSetup)
halSetup();
// include any HAL devices defined in exrail.
if (exrailHalSetup)
exrailHalSetup();
// Predefine two PCA9685 modules 0x40-0x41 if no conflicts
// Allocates 32 pins 100-131
if (checkNoOverlap(100, 16, 0x40)) {
PCA9685::create(100, 16, 0x40);
} else {
DIAG(F("Default PCA9685 at I2C 0x40 disabled due to configured user device"));
}
if (checkNoOverlap(116, 16, 0x41)) {
PCA9685::create(116, 16, 0x41);
} else {
DIAG(F("Default PCA9685 at I2C 0x41 disabled due to configured user device"));
}
// Predefine two MCP23017 module 0x20/0x21 if no conflicts
// Allocates 32 pins 164-195
if (checkNoOverlap(164, 16, 0x20)) {
MCP23017::create(164, 16, 0x20);
} else {
DIAG(F("Default MCP23017 at I2C 0x20 disabled due to configured user device"));
}
if (checkNoOverlap(180, 16, 0x21)) {
MCP23017::create(180, 16, 0x21);
} else {
DIAG(F("Default MCP23017 at I2C 0x21 disabled due to configured user device"));
}
}
// reset() function to reinitialise all devices
void IODevice::reset() {
unsigned long currentMicros = micros();
for (IODevice *dev = _firstDevice; dev != NULL; dev = dev->_nextDevice) {
dev->_deviceState = DEVSTATE_DORMANT;
// First ensure that _loop isn't delaying
dev->delayUntil(currentMicros);
// Then invoke _begin to restart driver
dev->_begin();
}
}
// Overarching static loop() method for the IODevice subsystem. Works through the
// list of installed devices and calls their individual _loop() method.
// Devices may or may not implement this, but if they do it is useful for things like animations
// or flashing LEDs.
// The current value of micros() is passed as a parameter, so the called loop function
// doesn't need to invoke it.
void IODevice::loop() {
unsigned long currentMicros = micros();
IODevice *lastLoopDevice = _nextLoopDevice; // So we know when to stop...
// Loop through devices until we find one ready to be serviced.
do {
if (!_nextLoopDevice) _nextLoopDevice = _firstDevice;
if (_nextLoopDevice) {
if (_nextLoopDevice->_deviceState != DEVSTATE_FAILED
&& ((long)(currentMicros - _nextLoopDevice->_nextEntryTime)) >= 0) {
// Found one ready to run, so invoke its _loop method.
_nextLoopDevice->_nextEntryTime = currentMicros;
_nextLoopDevice->_loop(currentMicros);
_nextLoopDevice = _nextLoopDevice->_nextDevice;
break;
}
// Not this one, move to next one
_nextLoopDevice = _nextLoopDevice->_nextDevice;
}
} while (_nextLoopDevice != lastLoopDevice); // Stop looking when we've done all.
// Report loop time if diags enabled
#if defined(DIAG_LOOPTIMES)
unsigned long diagMicros = micros();
static unsigned long lastMicros = 0;
// Measure time since HAL's loop() method started.
unsigned long halElapsed = diagMicros - currentMicros;
// Measure time between loop() method entries (excluding this diagnostic).
unsigned long elapsed = diagMicros - lastMicros;
static unsigned long maxElapsed = 0, maxHalElapsed = 0;
static unsigned long lastOutputTime = 0;
static unsigned long halTotal = 0, total = 0;
static unsigned long count = 0;
const unsigned long interval = (unsigned long)5 * 1000 * 1000; // 5 seconds in microsec
// Ignore long loop counts while message is still outputting (~3 milliseconds)
if (currentMicros - lastOutputTime > 3000UL) {
if (elapsed > maxElapsed) maxElapsed = elapsed;
if (halElapsed > maxHalElapsed) maxHalElapsed = halElapsed;
halTotal += halElapsed;
total += elapsed;
count++;
}
if (diagMicros - lastOutputTime > interval) {
if (lastOutputTime > 0)
DIAG(F("Loop Total:%lus (%lus max) HAL:%lus (%lus max)"),
total/count, maxElapsed, halTotal/count, maxHalElapsed);
maxElapsed = maxHalElapsed = total = halTotal = count = 0;
lastOutputTime = diagMicros;
}
// Read microsecond count after calculations, so they aren't
// included in the overall timings.
lastMicros = micros();
#endif
}
// Display a list of all the devices on the diagnostic stream.
void IODevice::DumpAll() {
for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) {
dev->_display();
}
}
// Determine if the specified vpin is allocated to a device.
bool IODevice::exists(VPIN vpin) {
return findDevice(vpin) != NULL;
}
// check whether the pin supports notification. If so, then regular _read calls are not required.
bool IODevice::hasCallback(VPIN vpin) {
IODevice *dev = findDevice(vpin);
if (!dev) return false;
return dev->_hasCallback;
}
// Display (to diagnostics) details of the device.
void IODevice::_display() {
DIAG(F("Unknown device Vpins:%u-%u %S"),
(int)_firstVpin, (int)_firstVpin+_nPins-1, _deviceState==DEVSTATE_FAILED ? F("OFFLINE") : F(""));
}
// Find device associated with nominated Vpin and pass configuration values on to it.
// Return false if not found.
bool IODevice::configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) {
IODevice *dev = findDevice(vpin);
if (dev) return dev->_configure(vpin, configType, paramCount, params);
#ifdef DIAG_IO
DIAG(F("IODevice::configure(): VPIN %u not found!"), (int)vpin);
#endif
return false;
}
// Read value from virtual pin.
int IODevice::read(VPIN vpin) {
for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) {
if (dev->owns(vpin))
return dev->_read(vpin);
}
#ifdef DIAG_IO
DIAG(F("IODevice::read(): VPIN %u not found!"), (int)vpin);
#endif
return false;
}
// Read analogue value from virtual pin.
int IODevice::readAnalogue(VPIN vpin) {
for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) {
if (dev->owns(vpin))
return dev->_readAnalogue(vpin);
}
#ifdef DIAG_IO
DIAG(F("IODevice::readAnalogue(): VPIN %u not found!"), (int)vpin);
#endif
return -1023;
}
int IODevice::configureAnalogIn(VPIN vpin) {
for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) {
if (dev->owns(vpin))
return dev->_configureAnalogIn(vpin);
}
#ifdef DIAG_IO
DIAG(F("IODevice::configureAnalogIn(): VPIN %u not found!"), (int)vpin);
#endif
return -1023;
}
// Write value to virtual pin(s). If multiple devices are allocated the same pin
// then only the first one found will be used.
void IODevice::write(VPIN vpin, int value) {
IODevice *dev = findDevice(vpin);
if (dev) {
dev->_write(vpin, value);
return;
}
#ifdef DIAG_IO
DIAG(F("IODevice::write(): VPIN %u not found!"), (int)vpin);
#endif
}
// Write analogue value to virtual pin(s). If multiple devices are allocated
// the same pin then only the first one found will be used.
//
// The significance of param1 and param2 may vary from device to device.
// For servo controllers, param1 is the profile of the transition and param2
// the duration, i.e. the time that the operation is to be animated over
// in deciseconds (0-3276 sec)
//
void IODevice::writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) {
IODevice *dev = findDevice(vpin);
if (dev) {
dev->_writeAnalogue(vpin, value, param1, param2);
return;
}
#ifdef DIAG_IO
DIAG(F("IODevice::writeAnalogue(): VPIN %u not found!"), (int)vpin);
#endif
}
// isBusy, when called for a device pin is always a digital output or analogue output,
// returns input feedback state of the pin, i.e. whether the pin is busy performing
// an animation or fade over a period of time.
bool IODevice::isBusy(VPIN vpin) {
IODevice *dev = findDevice(vpin);
if (dev)
return dev->_read(vpin);
else
return false;
}
void IODevice::setGPIOInterruptPin(int16_t pinNumber) {
if (pinNumber >= 0)
pinMode(pinNumber, INPUT_PULLUP);
_gpioInterruptPin = pinNumber;
}
// Helper function to add a new device to the device chain. If
// slaveDevice is NULL then the device is added to the end of the chain.
// Otherwise, the chain is searched for slaveDevice and the new device linked
// in front of it (to support filter devices that share the same VPIN range
// as the devices they control). If slaveDevice isn't found, then the
// device is linked to the end of the chain.
void IODevice::addDevice(IODevice *newDevice, IODevice *slaveDevice /* = NULL */) {
if (slaveDevice == _firstDevice) {
newDevice->_nextDevice = _firstDevice;
_firstDevice = newDevice;
} else {
for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) {
if (dev->_nextDevice == slaveDevice || dev->_nextDevice == NULL) {
// Link new device between dev and slaveDevice (or at end of chain)
newDevice->_nextDevice = dev->_nextDevice;
dev->_nextDevice = newDevice;
break;
}
}
}
newDevice->_begin();
}
// Private helper function to locate a device by VPIN. Returns NULL if not found.
// This is performance-critical, so minimises the calculation and function calls necessary.
IODevice *IODevice::findDevice(VPIN vpin) {
for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) {
VPIN firstVpin = dev->_firstVpin;
if (vpin >= firstVpin && vpin < firstVpin+dev->_nPins)
return dev;
}
return NULL;
}
// Instance helper function for filter devices (layered over others). Looks for
// a device that is further down the chain than the current device.
IODevice *IODevice::findDeviceFollowing(VPIN vpin) {
for (IODevice *dev = _nextDevice; dev != 0; dev = dev->_nextDevice) {
VPIN firstVpin = dev->_firstVpin;
if (vpin >= firstVpin && vpin < firstVpin+dev->_nPins)
return dev;
}
return NULL;
}
// Private helper function to check for vpin overlap. Run during setup only.
// returns true if pins DONT overlap with existing device
// TODO: Move the I2C address reservation and checks into the I2CManager code.
// That will enable non-HAL devices to reserve I2C addresses too.
bool IODevice::checkNoOverlap(VPIN firstPin, uint8_t nPins, I2CAddress i2cAddress) {
#ifdef DIAG_IO
DIAG(F("Check no overlap %u %u %s"), firstPin,nPins,i2cAddress.toString());
#endif
VPIN lastPin=firstPin+nPins-1;
for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) {
if (nPins > 0 && dev->_nPins > 0) {
// check for pin range overlaps (verbose but compiler will fix that)
VPIN firstDevPin=dev->_firstVpin;
VPIN lastDevPin=firstDevPin+dev->_nPins-1;
bool noOverlap= firstPin>lastDevPin || lastPin<firstDevPin;
if (!noOverlap) {
DIAG(F("WARNING HAL Overlap, redefinition of Vpins %u to %u ignored."),
firstPin, lastPin);
return false;
}
}
// Check for overlapping I2C address
if (i2cAddress && dev->_I2CAddress==i2cAddress) {
DIAG(F("WARNING HAL Overlap. i2c Addr %s ignored."),i2cAddress.toString());
return false;
}
}
return true; // no overlaps... OK to go on with constructor
}
//==================================================================================================================
// Static data
//------------------------------------------------------------------------------------------------------------------
// Chain of callback blocks (identifying registered callback functions for state changes)
IONotifyCallback *IONotifyCallback::first = 0;
// Start and end of chain of devices.
IODevice *IODevice::_firstDevice = 0;
// Reference to next device to be called on _loop() method.
IODevice *IODevice::_nextLoopDevice = 0;
//==================================================================================================================
// Instance members
//------------------------------------------------------------------------------------------------------------------
// Method to check whether the id corresponds to this device
bool IODevice::owns(VPIN id) {
return (id >= _firstVpin && id < _firstVpin + _nPins);
}
#else // !defined(IO_NO_HAL)
// Minimal implementations of public HAL interface, to support Arduino pin I/O and nothing more.
void IODevice::begin() { DIAG(F("NO HAL CONFIGURED!")); }
bool IODevice::configure(VPIN pin, ConfigTypeEnum configType, int nParams, int p[]) {
if (configType!=CONFIGURE_INPUT || nParams!=1 || pin >= NUM_DIGITAL_PINS) return false;
#ifdef DIAG_IO
DIAG(F("Arduino _configurePullup pin:%d Val:%d"), pin, p[0]);
#endif
pinMode(pin, p[0] ? INPUT_PULLUP : INPUT);
return true;
}
void IODevice::write(VPIN vpin, int value) {
if (vpin >= NUM_DIGITAL_PINS) return;
digitalWrite(vpin, value);
pinMode(vpin, OUTPUT);
}
void IODevice::writeAnalogue(VPIN, int, uint8_t, uint16_t) {}
bool IODevice::isBusy(VPIN) { return false; }
bool IODevice::hasCallback(VPIN) { return false; }
int IODevice::read(VPIN vpin) {
if (vpin >= NUM_DIGITAL_PINS) return 0;
return !digitalRead(vpin); // Return inverted state (5v=0, 0v=1)
}
int IODevice::readAnalogue(VPIN vpin) {
return ADCee::read(vpin);
}
int IODevice::configureAnalogIn(VPIN vpin) {
return ADCee::init(vpin);
}
void IODevice::loop() {}
void IODevice::DumpAll() {
DIAG(F("NO HAL CONFIGURED!"));
}
bool IODevice::exists(VPIN vpin) { return (vpin > 2 && vpin < NUM_DIGITAL_PINS); }
void IODevice::setGPIOInterruptPin(int16_t) {}
// Chain of callback blocks (identifying registered callback functions for state changes)
// Not used in IO_NO_HAL but must be declared.
IONotifyCallback *IONotifyCallback::first = 0;
#endif // IO_NO_HAL
/////////////////////////////////////////////////////////////////////////////////////////////////////
// Constructor
ArduinoPins::ArduinoPins(VPIN firstVpin, int nPins) {
_firstVpin = firstVpin;
_nPins = nPins;
int arrayLen = (_nPins+7)/8;
_pinPullups = (uint8_t *)calloc(3, arrayLen);
_pinModes = (&_pinPullups[0]) + arrayLen;
_pinInUse = (&_pinPullups[0]) + 2*arrayLen;
for (int i=0; i<arrayLen; i++) {
_pinPullups[i] = 0xff; // default to pullup on, for inputs
_pinModes[i] = 0;
_pinInUse[i] = 0;
}
}
// Device-specific pin configuration. Configure should be called infrequently so simplify
// code by using the standard pinMode function.
bool ArduinoPins::_configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) {
if (configType != CONFIGURE_INPUT) return false;
if (paramCount != 1) return false;
bool pullup = params[0];
int pin = vpin;
#ifdef DIAG_IO
DIAG(F("Arduino _configurePullup Pin:%d Val:%d"), pin, pullup);
#endif
uint8_t mask = 1 << ((pin-_firstVpin) % 8);
uint8_t index = (pin-_firstVpin) / 8;
_pinModes[index] &= ~mask; // set to input mode
if (pullup) {
_pinPullups[index] |= mask;
pinMode(pin, INPUT_PULLUP);
} else {
_pinPullups[index] &= ~mask;
pinMode(pin, INPUT);
}
_pinInUse[index] |= mask;
return true;
}
// Device-specific write function.
void ArduinoPins::_write(VPIN vpin, int value) {
int pin = vpin;
#ifdef DIAG_IO
DIAG(F("Arduino Write Pin:%d Val:%d"), pin, value);
#endif
uint8_t mask = 1 << ((pin-_firstVpin) % 8);
uint8_t index = (pin-_firstVpin) / 8;
// First update the output state, then set into write mode if not already.
fastWriteDigital(pin, value);
if (!(_pinModes[index] & mask)) {
// Currently in read mode, change to write mode
_pinModes[index] |= mask;
// Since mode changes should be infrequent, use standard pinMode function
pinMode(pin, OUTPUT);
_pinInUse[index] |= mask;
}
}
// Device-specific read function (digital input).
int ArduinoPins::_read(VPIN vpin) {
int pin = vpin;
uint8_t mask = 1 << ((pin-_firstVpin) % 8);
uint8_t index = (pin-_firstVpin) / 8;
if ((_pinModes[index] | ~_pinInUse[index]) & mask) {
// Currently in write mode or not initialised, change to read mode
_pinModes[index] &= ~mask;
// Since mode changes should be infrequent, use standard pinMode function
if (_pinPullups[index] & mask)
pinMode(pin, INPUT_PULLUP);
else
pinMode(pin, INPUT);
_pinInUse[index] |= mask;
}
int value = !fastReadDigital(pin); // Invert (5v=0, 0v=1)
#ifdef DIAG_IO
//DIAG(F("Arduino Read Pin:%d Value:%d"), pin, value);
#endif
return value;
}
// Device-specific readAnalogue function (analogue input)
int ArduinoPins::_readAnalogue(VPIN vpin) {
if (vpin > 255) return -1023;
uint8_t pin = vpin;
int value = ADCee::read(pin);
#ifdef DIAG_IO
DIAG(F("Arduino Read Pin:%d Value:%d"), pin, value);
#endif
return value;
}
int ArduinoPins::_configureAnalogIn(VPIN vpin) {
if (vpin > 255) return -1023;
uint8_t pin = vpin;
uint8_t mask = 1 << ((pin-_firstVpin) % 8);
uint8_t index = (pin-_firstVpin) / 8;
if (_pinModes[index] & mask) {
// Currently in write mode, change to read mode
_pinModes[index] &= ~mask;
// Since mode changes should be infrequent, use standard pinMode function
if (_pinPullups[index] & mask)
pinMode(pin, INPUT_PULLUP);
else
pinMode(pin, INPUT);
}
int value = ADCee::init(pin);
#ifdef DIAG_IO
DIAG(F("configureAnalogIn Pin:%d Value:%d"), pin, value);
#endif
return value;
}
void ArduinoPins::_display() {
DIAG(F("Arduino Vpins:%u-%u"), (int)_firstVpin, (int)_firstVpin+_nPins-1);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////
void ArduinoPins::fastWriteDigital(uint8_t pin, uint8_t value) {
#if defined(USE_FAST_IO)
if (pin >= NUM_DIGITAL_PINS) return;
uint8_t mask = digitalPinToBitMask(pin);
uint8_t port = digitalPinToPort(pin);
volatile uint8_t *outPortAdr = portOutputRegister(port);
noInterrupts();
if (value)
*outPortAdr |= mask;
else
*outPortAdr &= ~mask;
interrupts();
#else
digitalWrite(pin, value);
#endif
}
bool ArduinoPins::fastReadDigital(uint8_t pin) {
#if defined(USE_FAST_IO)
if (pin >= NUM_DIGITAL_PINS) return false;
uint8_t mask = digitalPinToBitMask(pin);
uint8_t port = digitalPinToPort(pin);
volatile uint8_t *inPortAdr = portInputRegister(port);
// read input
bool result = (*inPortAdr & mask) != 0;
#else
bool result = digitalRead(pin);
#endif
return result;
}

View File

@@ -1,547 +0,0 @@
/*
* © 2023, Paul Antoine, Discord user @ADUBOURG
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef iodevice_h
#define iodevice_h
// Define symbol DIAG_IO to enable diagnostic output
//#define DIAG_IO Y
// Define symbol DIAG_LOOPTIMES to enable CS loop execution time to be reported
//#define DIAG_LOOPTIMES
// Define symbol IO_NO_HAL to reduce FLASH footprint when HAL features not required
// The HAL is disabled by default on Nano and Uno platforms, because of limited flash space.
#if defined(ARDUINO_AVR_NANO) || defined(ARDUINO_AVR_UNO)
#define IO_NO_HAL
#endif
// Define symbol IO_SWITCH_OFF_SERVO to set the PCA9685 output to 0 when an
// animation has completed. This switches off the servo motor, preventing
// the continuous buzz sometimes found on servos, and reducing the
// power consumption of the servo when inactive.
// It is recommended to enable this, unless it causes you problems.
#define IO_SWITCH_OFF_SERVO
#include "DIAG.h"
#include "FSH.h"
#include "I2CManager.h"
#include "inttypes.h"
typedef uint16_t VPIN;
// Limit VPIN number to max 32767. Above this number, printing often gives negative values.
// This should be enough for 99% of users.
#define VPIN_MAX 32767
#define VPIN_NONE 65535
/*
* Callback support for state change notification from an IODevice subclass to a
* handler, e.g. Sensor object handling.
*/
class IONotifyCallback {
public:
typedef void IONotifyCallbackFunction(VPIN vpin, int value);
static void add(IONotifyCallbackFunction *function) {
IONotifyCallback *blk = new IONotifyCallback(function);
if (first) blk->next = first;
first = blk;
}
static void invokeAll(VPIN vpin, int value) {
for (IONotifyCallback *blk = first; blk != NULL; blk = blk->next)
blk->invoke(vpin, value);
}
static bool hasCallback() {
return first != NULL;
}
private:
IONotifyCallback(IONotifyCallbackFunction *function) { invoke = function; };
IONotifyCallback *next = 0;
IONotifyCallbackFunction *invoke = 0;
static IONotifyCallback *first;
};
/*
* IODevice class
*
* This class is the basis of the Hardware Abstraction Layer (HAL) for
* the DCC++EX Command Station. All device classes derive from this.
*
*/
class IODevice {
public:
// Parameter values to identify type of call to IODevice::configure.
typedef enum : uint8_t {
CONFIGURE_INPUT = 1,
CONFIGURE_SERVO = 2,
CONFIGURE_OUTPUT = 3,
CONFIGURE_ANALOGOUTPUT = 4,
CONFIGURE_ANALOGINPUT = 5,
} ConfigTypeEnum;
typedef enum : uint8_t {
DEVSTATE_DORMANT = 0,
DEVSTATE_PROBING = 1,
DEVSTATE_INITIALISING = 2,
DEVSTATE_NORMAL = 3,
DEVSTATE_SCANNING = 4,
DEVSTATE_FAILED = 5,
} DeviceStateEnum;
// Static functions to find the device and invoke its member functions
// begin is invoked to create any standard IODevice subclass instances.
// Also, the _begin method of any existing instances is called from here.
static void begin();
// reset function to invoke all driver's _begin() methods again, to
// reset the state of the devices and reinitialise.
static void reset();
// configure is used invoke an IODevice instance's _configure method
static bool configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]);
// User-friendly function for configuring an input pin.
inline static bool configureInput(VPIN vpin, bool pullupEnable) {
int params[] = {pullupEnable};
return IODevice::configure(vpin, CONFIGURE_INPUT, 1, params);
}
// User-friendly function for configuring a servo pin.
inline static bool configureServo(VPIN vpin, uint16_t activePosition, uint16_t inactivePosition, uint8_t profile=0, uint16_t duration=0, uint8_t initialState=0) {
int params[] = {(int)activePosition, (int)inactivePosition, profile, (int)duration, initialState};
return IODevice::configure(vpin, CONFIGURE_SERVO, 5, params);
}
// write invokes the IODevice instance's _write method.
static void write(VPIN vpin, int value);
// write invokes the IODevice instance's _writeAnalogue method (not applicable for digital outputs)
static void writeAnalogue(VPIN vpin, int value, uint8_t profile=0, uint16_t duration=0);
// isBusy returns true if the device is currently in an animation of some sort, e.g. is changing
// the output over a period of time.
static bool isBusy(VPIN vpin);
// check whether the pin supports notification. If so, then regular _read calls are not required.
static bool hasCallback(VPIN vpin);
// read invokes the IODevice instance's _read method.
static int read(VPIN vpin);
// read invokes the IODevice instance's _readAnalogue method.
static int readAnalogue(VPIN vpin);
static int configureAnalogIn(VPIN vpin);
// loop invokes the IODevice instance's _loop method.
static void loop();
static void DumpAll();
// exists checks whether there is a device owning the specified vpin
static bool exists(VPIN vpin);
// Enable shared interrupt on specified pin for GPIO extender modules. The extender module
// should pull down this pin when requesting a scan. The pin may be shared by multiple modules.
// Without the shared interrupt, input states are scanned periodically to detect changes on
// GPIO extender pins. If a shared interrupt pin is configured, then input states are scanned
// only when the shared interrupt pin is pulled low. The external GPIO module releases the pin
// once the GPIO port concerned has been read.
void setGPIOInterruptPin(int16_t pinNumber);
// Method to check if pins will overlap before creating new device.
static bool checkNoOverlap(VPIN firstPin, uint8_t nPins=1, I2CAddress i2cAddress=0);
// Method used by IODevice filters to locate slave pins that may be overlayed by their own
// pin range.
IODevice *findDeviceFollowing(VPIN vpin);
// Method to write new state (optionally implemented within device class)
virtual void _write(VPIN vpin, int value) {
(void)vpin; (void)value;
};
// Method to write an 'analogue' value (optionally implemented within device class)
virtual void _writeAnalogue(VPIN vpin, int value, uint8_t param1=0, uint16_t param2=0) {
(void)vpin; (void)value; (void) param1; (void)param2;
};
// Method to read digital pin state (optionally implemented within device class)
virtual int _read(VPIN vpin) {
(void)vpin;
return 0;
};
// Method to read analogue pin state (optionally implemented within device class)
virtual int _readAnalogue(VPIN vpin) {
(void)vpin;
return 0;
};
protected:
// Constructor
IODevice(VPIN firstVpin=0, int nPins=0) {
_firstVpin = firstVpin;
_nPins = nPins;
_nextEntryTime = 0;
_I2CAddress=0;
}
// Method to perform initialisation of the device (optionally implemented within device class)
virtual void _begin() {}
// Method to check whether the vpin corresponds to this device
bool owns(VPIN vpin);
// Method to configure device (optionally implemented within device class)
virtual bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) {
(void)vpin; (void)configType; (void)paramCount; (void)params; // Suppress compiler warning.
return false;
};
virtual int _configureAnalogIn(VPIN vpin) {
(void)vpin;
return 0;
};
// Method to perform updates on an ongoing basis (optionally implemented within device class)
virtual void _loop(unsigned long currentMicros) {
delayUntil(currentMicros + 0x7fffffff); // Largest time in the future! Effectively disable _loop calls.
};
// Method for displaying info on DIAG output (optionally implemented within device class)
virtual void _display();
// Destructor
virtual ~IODevice() {};
// Non-virtual function
void delayUntil(unsigned long futureMicrosCount) {
_nextEntryTime = futureMicrosCount;
}
// Common object fields.
VPIN _firstVpin;
int _nPins;
I2CAddress _I2CAddress;
// Flag whether the device supports callbacks.
bool _hasCallback = false;
// Pin number of interrupt pin for GPIO extender devices. The extender module will pull this
// pin low if an input changes state.
int16_t _gpioInterruptPin = -1;
// Static support function for subclass creation
static void addDevice(IODevice *newDevice, IODevice *slaveDevice = NULL);
// Method to find device handling Vpin
static IODevice *findDevice(VPIN vpin);
// Current state of device
DeviceStateEnum _deviceState = DEVSTATE_DORMANT;
private:
IODevice *_nextDevice = 0;
unsigned long _nextEntryTime;
static IODevice *_firstDevice;
static IODevice *_nextLoopDevice;
};
/////////////////////////////////////////////////////////////////////////////////////////////////////
/*
* IODevice subclass for PCA9685 16-channel PWM module.
*/
class PCA9685 : public IODevice {
public:
static void create(VPIN vpin, int nPins, I2CAddress i2cAddress, uint16_t frequency = 50);
enum ProfileType : uint8_t {
Instant = 0, // Moves immediately between positions (if duration not specified)
UseDuration = 0, // Use specified duration
Fast = 1, // Takes around 500ms end-to-end
Medium = 2, // 1 second end-to-end
Slow = 3, // 2 seconds end-to-end
Bounce = 4, // For semaphores/turnouts with a bit of bounce!!
NoPowerOff = 0x80, // Flag to be ORed in to suppress power off after move.
};
private:
// Constructor
PCA9685(VPIN vpin, int nPins, I2CAddress i2cAddress, uint16_t frequency);
// Device-specific initialisation
void _begin() override;
bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override;
// Device-specific write functions.
void _write(VPIN vpin, int value) override;
void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override;
int _read(VPIN vpin) override; // returns the digital state or busy status of the device
void _loop(unsigned long currentMicros) override;
void updatePosition(uint8_t pin);
void writeDevice(uint8_t pin, int value);
void _display() override;
struct ServoData {
uint16_t activePosition : 12; // Config parameter
uint16_t inactivePosition : 12; // Config parameter
uint16_t currentPosition : 12;
uint16_t fromPosition : 12;
uint16_t toPosition : 12;
uint8_t profile; // Config parameter
uint16_t stepNumber; // Index of current step (starting from 0)
uint16_t numSteps; // Number of steps in animation, or 0 if none in progress.
uint8_t currentProfile; // profile being used for current animation.
uint16_t duration; // time (tenths of a second) for animation to complete.
}; // 14 bytes per element, i.e. per pin in use
struct ServoData *_servoData [16];
static const uint8_t _catchupSteps = 5; // number of steps to wait before switching servo off
static const uint8_t FLASH _bounceProfile[30];
const unsigned int refreshInterval = 50; // refresh every 50ms
// structures for setting up non-blocking writes to servo controller
I2CRB requestBlock;
uint8_t outputBuffer[5];
uint8_t prescaler; // clock prescaler for setting PWM frequency
};
/////////////////////////////////////////////////////////////////////////////////////////////////////
/*
* IODevice subclass for DCC accessory decoder.
*/
class DCCAccessoryDecoder: public IODevice {
public:
static void create(VPIN firstVpin, int nPins, int DCCAddress, int DCCSubaddress);
private:
// Constructor
DCCAccessoryDecoder(VPIN firstVpin, int nPins, int DCCAddress, int DCCSubaddress);
// Device-specific write function.
void _begin() override;
void _write(VPIN vpin, int value) override;
void _display() override;
int _packedAddress;
};
/////////////////////////////////////////////////////////////////////////////////////////////////////
/*
* IODevice subclass for arduino input/output pins.
*/
class ArduinoPins: public IODevice {
public:
static void create(VPIN firstVpin, int nPins) {
addDevice(new ArduinoPins(firstVpin, nPins));
}
static void fastWriteDigital(uint8_t pin, uint8_t value);
static bool fastReadDigital(uint8_t pin);
private:
// Constructor
ArduinoPins(VPIN firstVpin, int nPins);
// Device-specific pin configuration
bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override;
// Device-specific write function.
void _write(VPIN vpin, int value) override;
// Device-specific read functions.
int _read(VPIN vpin) override;
int _readAnalogue(VPIN vpin) override;
int _configureAnalogIn(VPIN vpin) override;
void _display() override;
uint8_t *_pinPullups;
uint8_t *_pinModes; // each bit is 1 for output, 0 for input
uint8_t *_pinInUse;
};
/////////////////////////////////////////////////////////////////////////////////////////////////////
/*
* IODevice subclass for EX-Turntable.
*/
class EXTurntable : public IODevice {
public:
static void create(VPIN firstVpin, int nPins, I2CAddress I2CAddress);
// Constructor
EXTurntable(VPIN firstVpin, int nPins, I2CAddress I2CAddress);
enum ActivityNumber : uint8_t {
Turn = 0, // Rotate turntable, maintain phase
Turn_PInvert = 1, // Rotate turntable, invert phase
Home = 2, // Initiate homing
Calibrate = 3, // Initiate calibration sequence
LED_On = 4, // Turn LED on
LED_Slow = 5, // Set LED to a slow blink
LED_Fast = 6, // Set LED to a fast blink
LED_Off = 7, // Turn LED off
Acc_On = 8, // Turn accessory pin on
Acc_Off = 9, // Turn accessory pin off
};
private:
// Device-specific write function.
void _begin() override;
void _loop(unsigned long currentMicros) override;
int _read(VPIN vpin) override;
void _writeAnalogue(VPIN vpin, int value, uint8_t activity, uint16_t duration) override;
void _display() override;
uint8_t _stepperStatus;
};
/////////////////////////////////////////////////////////////////////////////////////////////////////
// IODevice framework for invoking user-written functions.
// To use, define a function that you want to be regularly
// invoked, and then create an instance of UserAddin.
// For example, you can show the status, on screen 3, of the first eight
// locos in the speed table:
//
// void updateLocoScreen() {
// for (int i=0; i<8; i++) {
// if (DCC::speedTable[i].loco > 0) {
// int speed = DCC::speedTable[i].speedCode;
// SCREEN(3, i, F("Loco:%4d %3d %c"), DCC::speedTable[i].loco,
// speed & 0x7f, speed & 0x80 ? 'R' : 'F');
// }
// }
// }
//
// void halSetup() {
// ...
// UserAddin(updateLocoScreen, 1000); // Update every 1000ms
// ...
// }
//
class UserAddin : public IODevice {
private:
void (*_invokeUserFunction)();
int _delay; // milliseconds
public:
UserAddin(void (*func)(), int delay) {
_invokeUserFunction = func;
_delay = delay;
addDevice(this);
}
// userFunction has no return value, no parameter. delay is in milliseconds.
static void create(void (*userFunction)(), int delay) {
new UserAddin(userFunction, delay);
}
protected:
void _begin() { _display(); }
void _loop(unsigned long currentMicros) override {
_invokeUserFunction();
// _loop won't be called again until _delay ms have elapsed.
delayUntil(currentMicros + _delay * 1000UL);
}
void _display() override {
DIAG(F("UserAddin run every %dms"), _delay);
}
};
/////////////////////////////////////////////////////////////////////////////////////////////////////
//
// This HAL device driver is intended for communication in automation
// sequences. A VPIN can be SET or RESET within a sequence, and its
// current state checked elsewhere using IF, IFNOT, AT etc. or monitored
// from JMRI using a Sensor object (DCC-EX <S ...> command).
// Alternatively, the flag can be set from JMRI and other interfaces
// using the <Z ...> command, to enable or disable actions within a sequence.
//
// Example of configuration in halSetup.h:
//
// FLAGS::create(32000, 128);
//
// or in myAutomation.h:
//
// HAL(FLAGS, 32000, 128);
//
// Both create 128 flags numbered with VPINs 32000-32127.
//
//
class FLAGS : IODevice {
private:
uint8_t *_states = NULL;
public:
static void create(VPIN firstVpin, unsigned int nPins) {
if (checkNoOverlap(firstVpin, nPins))
new FLAGS(firstVpin, nPins);
}
protected:
// Constructor performs static initialisation of the device object
FLAGS (VPIN firstVpin, int nPins) {
_firstVpin = firstVpin;
_nPins = nPins;
_states = (uint8_t *)calloc(1, (_nPins+7)/8);
if (!_states) {
DIAG(F("FLAGS: ERROR Memory Allocation Failure"));
return;
}
addDevice(this);
}
int _read(VPIN vpin) override {
int pin = vpin - _firstVpin;
if (pin >= _nPins || pin < 0) return 0;
uint8_t mask = 1 << (pin & 7);
return (_states[pin>>3] & mask) ? 1 : 0;
}
void _write(VPIN vpin, int value) override {
int pin = vpin - _firstVpin;
if (pin >= _nPins || pin < 0) return;
uint8_t mask = 1 << (pin & 7);
if (value)
_states[pin>>3] |= mask;
else
_states[pin>>3] &= ~mask;
}
void _display() override {
DIAG(F("FLAGS configured on VPINs %u-%u"),
_firstVpin, _firstVpin+_nPins-1);
}
};
#include "IO_MCP23008.h"
#include "IO_MCP23017.h"
#include "IO_PCF8574.h"
#include "IO_PCF8575.h"
#include "IO_duinoNodes.h"
#include "IO_EXIOExpander.h"
#endif // iodevice_h

View File

@@ -1,169 +0,0 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef io_analogueinputs_h
#define io_analogueinputs_h
// Uncomment following line to slow the scan cycle down to 1second ADC samples, with
// diagnostic output of scanned values.
//#define IO_ANALOGUE_SLOW
#include "IODevice.h"
#include "I2CManager.h"
#include "DIAG.h"
#include "FSH.h"
/**********************************************************************************************
* ADS111x class for I2C-connected analogue input modules ADS1113, ADS1114 and ADS1115.
*
* ADS1113 and ADS1114 are restricted to 1 input. ADS1115 has a multiplexer which allows
* any of four input pins to be read by its ADC.
*
* The driver polls the device in accordance with the constant 'scanInterval' below. On first loop
* entry, the multiplexer is set to pin A0 and the ADC is triggered. On second and subsequent
* entries, the analogue value is read from the conversion register and then the multiplexer and
* ADC are set up to read the next pin.
*
* The ADS111x is set up as follows:
* Single-shot scan
* Data rate 128 samples/sec (7.8ms/sample, but scanned every 10ms)
* Comparator off
* Gain FSR=6.144V
* The gain means that the maximum input voltage of 5V (when Vss=5V) gives a reading
* of 32767*(5.0/6.144) = 26666.
*
* A device is configured by the following:
* ADS111x::create(firstVpin, nPins, i2cAddress);
* for example
* ADS111x::create(300, 1, 0x48); // single-input ADS1113
* ADS111x::create(300, 4, 0x48); // four-input ADS1115
*
* Note: The device is simple and does not need initial configuration, so it should recover from
* temporary loss of communications or power.
**********************************************************************************************/
class ADS111x: public IODevice {
public:
static void create(VPIN firstVpin, int nPins, I2CAddress i2cAddress) {
if (checkNoOverlap(firstVpin,nPins,i2cAddress)) new ADS111x(firstVpin, nPins, i2cAddress);
}
private:
ADS111x(VPIN firstVpin, int nPins, I2CAddress i2cAddress) {
_firstVpin = firstVpin;
_nPins = (nPins > 4) ? 4 : nPins;
_I2CAddress = i2cAddress;
_currentPin = 0;
for (int8_t i=0; i<_nPins; i++)
_value[i] = -1;
addDevice(this);
}
void _begin() {
// Initialise I2C
I2CManager.begin();
// ADS111x support high-speed I2C (4.3MHz) but that requires special
// processing. So stick to fast mode (400kHz maximum).
I2CManager.setClock(400000);
// Initialise ADS device
if (I2CManager.exists(_I2CAddress)) {
_nextState = STATE_STARTSCAN;
#ifdef DIAG_IO
_display();
#endif
} else {
DIAG(F("ADS111x device not found, I2C:%s"), _I2CAddress.toString());
_deviceState = DEVSTATE_FAILED;
}
}
void _loop(unsigned long currentMicros) override {
// Check that previous non-blocking write has completed, if not then wait
uint8_t status = _i2crb.status;
if (status == I2C_STATUS_PENDING) return; // Busy, so don't do anything.
if (status == I2C_STATUS_OK) {
switch (_nextState) {
case STATE_STARTSCAN:
// Configure ADC and multiplexer for next scan. See ADS111x datasheet for details
// of configuration register settings.
_outBuffer[0] = 0x01; // Config register address
_outBuffer[1] = 0xC0 + (_currentPin << 4); // Trigger single-shot, channel n
_outBuffer[2] = 0xA3; // 250 samples/sec, comparator off
// Write command, without waiting for completion.
I2CManager.write(_I2CAddress, _outBuffer, 3, &_i2crb);
delayUntil(currentMicros + scanInterval);
_nextState = STATE_STARTREAD;
break;
case STATE_STARTREAD:
// Reading the pin value
_outBuffer[0] = 0x00; // Conversion register address
I2CManager.read(_I2CAddress, _inBuffer, 2, _outBuffer, 1, &_i2crb); // Read register
_nextState = STATE_GETVALUE;
break;
case STATE_GETVALUE:
_value[_currentPin] = ((uint16_t)_inBuffer[0] << 8) + (uint16_t)_inBuffer[1];
#ifdef IO_ANALOGUE_SLOW
DIAG(F("ADS111x VPIN:%u value:%d"), _currentPin, _value[_currentPin]);
#endif
// Move to next pin
if (++_currentPin >= _nPins) _currentPin = 0;
_nextState = STATE_STARTSCAN;
break;
default:
break;
}
} else { // error status
DIAG(F("ADS111x I2C:%s Error:%d %S"), _I2CAddress.toString(), status, I2CManager.getErrorMessage(status));
_deviceState = DEVSTATE_FAILED;
}
}
int _readAnalogue(VPIN vpin) override {
int pin = vpin - _firstVpin;
return _value[pin];
}
void _display() override {
DIAG(F("ADS111x I2C:%s Configured on Vpins:%u-%u %S"), _I2CAddress.toString(), _firstVpin, _firstVpin+_nPins-1,
_deviceState == DEVSTATE_FAILED ? F("OFFLINE") : F(""));
}
// ADC conversion rate is 250SPS, or 4ms per conversion. Set the period between updates to 10ms.
// This is enough to allow the conversion to reliably complete in time.
#ifndef IO_ANALOGUE_SLOW
const unsigned long scanInterval = 10000UL; // Period between successive ADC scans in microseconds.
#else
const unsigned long scanInterval = 1000000UL; // Period between successive ADC scans in microseconds.
#endif
enum : uint8_t {
STATE_STARTSCAN,
STATE_STARTREAD,
STATE_GETVALUE,
};
uint16_t _value[4];
uint8_t _outBuffer[3];
uint8_t _inBuffer[2];
uint8_t _currentPin; // ADC pin currently being scanned
I2CRB _i2crb;
uint8_t _nextState;
};
#endif // io_analogueinputs_h

View File

@@ -1,68 +0,0 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#include "DCC.h"
#include "IODevice.h"
#include "DIAG.h"
#include "defines.h"
#define PACKEDADDRESS(addr, subaddr) (((addr) << 2) + (subaddr))
#define ADDRESS(packedaddr) ((packedaddr) >> 2)
#define SUBADDRESS(packedaddr) ((packedaddr) % 4)
void DCCAccessoryDecoder::create(VPIN firstVpin, int nPins, int DCCAddress, int DCCSubaddress) {
if (checkNoOverlap(firstVpin,nPins)) new DCCAccessoryDecoder(firstVpin, nPins, DCCAddress, DCCSubaddress);
}
// Constructors
DCCAccessoryDecoder::DCCAccessoryDecoder(VPIN vpin, int nPins, int DCCAddress, int DCCSubaddress) {
_firstVpin = vpin;
_nPins = nPins;
_packedAddress = PACKEDADDRESS(DCCAddress, DCCSubaddress);
addDevice(this);
}
void DCCAccessoryDecoder::_begin() {
#if defined(DIAG_IO)
_display();
#endif
}
// Device-specific write function. State 1=closed, 0=thrown. Adjust for RCN-213 compliance
void DCCAccessoryDecoder::_write(VPIN id, int state) {
int packedAddress = _packedAddress + id - _firstVpin;
#if defined(HAL_ACCESSORY_COMMAND_REVERSE)
state = !state;
#ifdef DIAG_IO
DIAG(F("DCC Write Linear Address:%d State:%d (inverted)"), packedAddress, state);
#endif
#else
#ifdef DIAG_IO
DIAG(F("DCC Write Linear Address:%d State:%d"), packedAddress, state);
#endif
#endif
DCC::setAccessory(ADDRESS(packedAddress), SUBADDRESS(packedAddress), state);
}
void DCCAccessoryDecoder::_display() {
int endAddress = _packedAddress + _nPins - 1;
DIAG(F("DCCAccessoryDecoder Configured on Vpins:%u-%u Addresses %d/%d-%d/%d)"), _firstVpin, _firstVpin+_nPins-1,
ADDRESS(_packedAddress), SUBADDRESS(_packedAddress), ADDRESS(endAddress), SUBADDRESS(endAddress));
}

View File

@@ -1,359 +0,0 @@
/*
* © 2023, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* DFPlayer is an MP3 player module with an SD card holder. It also has an integrated
* amplifier, so it only needs a power supply and a speaker.
*
* This driver allows the device to be controlled through IODevice::write() and
* IODevice::writeAnalogue() calls.
*
* The driver is configured as follows:
*
* DFPlayer::create(firstVpin, nPins, Serialn);
*
* Where firstVpin is the first vpin reserved for reading the device,
* nPins is the number of pins to be allocated (max 5)
* and Serialn is the name of the Serial port connected to the DFPlayer (e.g. Serial1).
*
* Example:
* In halSetup function within myHal.cpp:
* DFPlayer::create(3500, 5, Serial1);
* or in myAutomation.h:
* HAL(DFPlayer, 3500, 5, Serial1)
*
* Writing an analogue value 1-2999 to the first pin (3500) will play the numbered file from the
* SD card; e.g. a value of 1 will play the first file, 2 for the second file etc.
* Writing an analogue value 0 to the first pin (3500) will stop the file playing;
* Writing an analogue value 0-30 to the second pin (3501) will set the volume;
* Writing a digital value of 1 to a pin will play the file corresponding to that pin, e.g.
the first file will be played by setting pin 3500, the second by setting pin 3501 etc.;
* Writing a digital value of 0 to any pin will stop the player;
* Reading a digital value from any pin will return true(1) if the player is playing, false(0) otherwise.
*
* From EX-RAIL, the following commands may be used:
* SET(3500) -- starts playing the first file (file 1) on the SD card
* SET(3501) -- starts playing the second file (file 2) on the SD card
* etc.
* RESET(3500) -- stops all playing on the player
* WAITFOR(3500) -- wait for the file currently being played by the player to complete
* SERVO(3500,2,Instant) -- plays file 2 at current volume
* SERVO(3501,20,Instant) -- Sets the volume to 20
*
* NB The DFPlayer's serial lines are not 5V safe, so connecting the Arduino TX directly
* to the DFPlayer's RX terminal will cause lots of noise over the speaker, or worse.
* A 1k resistor in series with the module's RX terminal will alleviate this.
*
* Files on the SD card are numbered according to their order in the directory on the
* card (as listed by the DIR command in Windows). This may not match the order of the files
* as displayed by Windows File Manager, which sorts the file names. It is suggested that
* files be copied into an empty SDcard in the desired order, one at a time.
*
* The driver now polls the device for its current status every second. Should the device
* fail to respond it will be marked off-line and its busy indicator cleared, to avoid
* lock-ups in automation scripts that are executing for a WAITFOR().
*/
#ifndef IO_DFPlayer_h
#define IO_DFPlayer_h
#include "IODevice.h"
class DFPlayer : public IODevice {
private:
const uint8_t MAXVOLUME=30;
HardwareSerial *_serial;
bool _playing = false;
uint8_t _inputIndex = 0;
unsigned long _commandSendTime; // Time (us) that last transmit took place.
unsigned long _timeoutTime;
uint8_t _recvCMD; // Last received command code byte
bool _awaitingResponse = false;
uint8_t _requestedVolumeLevel = MAXVOLUME;
uint8_t _currentVolume = MAXVOLUME;
int _requestedSong = -1; // -1=none, 0=stop, >0=file number
public:
static void create(VPIN firstVpin, int nPins, HardwareSerial &serial) {
if (checkNoOverlap(firstVpin,nPins)) new DFPlayer(firstVpin, nPins, serial);
}
protected:
// Constructor
DFPlayer(VPIN firstVpin, int nPins, HardwareSerial &serial) :
IODevice(firstVpin, nPins),
_serial(&serial)
{
addDevice(this);
}
void _begin() override {
_serial->begin(9600, SERIAL_8N1); // 9600baud, no parity, 1 stop bit
// Flush any data in input queue
while (_serial->available()) _serial->read();
_deviceState = DEVSTATE_INITIALISING;
// Send a query to the device to see if it responds
sendPacket(0x42);
_timeoutTime = micros() + 5000000UL; // 5 second timeout
_awaitingResponse = true;
}
void _loop(unsigned long currentMicros) override {
// Read responses from device
processIncoming();
// Check if a command sent to device has timed out. Allow 0.5 second for response
if (_awaitingResponse && (int32_t)(currentMicros - _timeoutTime) > 0) {
DIAG(F("DFPlayer device not responding on serial port"));
_deviceState = DEVSTATE_FAILED;
_awaitingResponse = false;
_playing = false;
}
// Send any commands that need to go.
processOutgoing(currentMicros);
delayUntil(currentMicros + 10000); // Only enter every 10ms
}
// Check for incoming data on _serial, and update busy flag and other state accordingly
void processIncoming() {
// Expected message is in the form "7E FF 06 3D xx xx xx xx xx EF"
bool ok = false;
while (_serial->available()) {
int c = _serial->read();
switch (_inputIndex) {
case 0:
if (c == 0x7E) ok = true;
break;
case 1:
if (c == 0xFF) ok = true;
break;
case 2:
if (c== 0x06) ok = true;
break;
case 3:
_recvCMD = c; // CMD byte
ok = true;
break;
case 6:
switch (_recvCMD) {
case 0x42:
// Response to status query
_playing = (c != 0);
// Mark the device online and cancel timeout
if (_deviceState==DEVSTATE_INITIALISING) {
_deviceState = DEVSTATE_NORMAL;
#ifdef DIAG_IO
_display();
#endif
}
_awaitingResponse = false;
break;
case 0x3d:
// End of play
if (_playing) {
#ifdef DIAG_IO
DIAG(F("DFPlayer: Finished"));
#endif
_playing = false;
}
break;
case 0x40:
// Error code
DIAG(F("DFPlayer: Error %d returned from device"), c);
_playing = false;
break;
}
ok = true;
break;
case 4: case 5: case 7: case 8:
ok = true; // Skip over these bytes in message.
break;
case 9:
if (c==0xef) {
// Message finished
}
break;
default:
break;
}
if (ok)
_inputIndex++; // character as expected, so increment index
else
_inputIndex = 0; // otherwise reset.
}
}
// Send any commands that need to be sent
void processOutgoing(unsigned long currentMicros) {
// When two commands are sent in quick succession, the device will often fail to
// execute one. Testing has indicated that a delay of 100ms or more is required
// between successive commands to get reliable operation.
// If 100ms has elapsed since the last thing sent, then check if there's some output to do.
if (((int32_t)currentMicros - _commandSendTime) > 100000) {
if (_currentVolume > _requestedVolumeLevel) {
// Change volume before changing song if volume is reducing.
_currentVolume = _requestedVolumeLevel;
sendPacket(0x06, _currentVolume);
} else if (_requestedSong > 0) {
// Change song
sendPacket(0x03, _requestedSong);
_requestedSong = -1;
} else if (_requestedSong == 0) {
sendPacket(0x16); // Stop playing
_requestedSong = -1;
} else if (_currentVolume < _requestedVolumeLevel) {
// Change volume after changing song if volume is increasing.
_currentVolume = _requestedVolumeLevel;
sendPacket(0x06, _currentVolume);
} else if ((int32_t)currentMicros - _commandSendTime > 1000000) {
// Poll device every second that other commands aren't being sent,
// to check if it's still connected and responding.
sendPacket(0x42);
if (!_awaitingResponse) {
_timeoutTime = currentMicros + 5000000UL; // Timeout if no response within 5 seconds
_awaitingResponse = true;
}
}
}
}
// Write with value 1 starts playing a song. The relative pin number is the file number.
// Write with value 0 stops playing.
void _write(VPIN vpin, int value) override {
if (_deviceState == DEVSTATE_FAILED) return;
int pin = vpin - _firstVpin;
if (value) {
// Value 1, start playing
#ifdef DIAG_IO
DIAG(F("DFPlayer: Play %d"), pin+1);
#endif
_requestedSong = pin+1;
_playing = true;
} else {
// Value 0, stop playing
#ifdef DIAG_IO
DIAG(F("DFPlayer: Stop"));
#endif
_requestedSong = 0; // No song
_playing = false;
}
}
// WriteAnalogue on first pin uses the nominated value as a file number to start playing, if file number > 0.
// Volume may be specified as second parameter to writeAnalogue.
// If value is zero, the player stops playing.
// WriteAnalogue on second pin sets the output volume.
//
void _writeAnalogue(VPIN vpin, int value, uint8_t volume=0, uint16_t=0) override {
if (_deviceState == DEVSTATE_FAILED) return;
uint8_t pin = vpin - _firstVpin;
#ifdef DIAG_IO
DIAG(F("DFPlayer: VPIN:%u FileNo:%d Volume:%d"), vpin, value, volume);
#endif
// Validate parameter.
if (volume > MAXVOLUME) volume = MAXVOLUME;
if (pin == 0) {
// Play track
if (value > 0) {
if (volume > 0)
_requestedVolumeLevel = volume;
_requestedSong = value;
_playing = true;
} else {
_requestedSong = 0; // stop playing
_playing = false;
}
} else if (pin == 1) {
// Set volume (0-30)
_requestedVolumeLevel = value;
}
}
// A read on any pin indicates whether the player is still playing.
int _read(VPIN) override {
if (_deviceState == DEVSTATE_FAILED) return false;
return _playing;
}
void _display() override {
DIAG(F("DFPlayer Configured on Vpins:%u-%u %S"), _firstVpin, _firstVpin+_nPins-1,
(_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F(""));
}
private:
// 7E FF 06 0F 00 01 01 xx xx EF
// 0 -> 7E is start code
// 1 -> FF is version
// 2 -> 06 is length
// 3 -> 0F is command
// 4 -> 00 is no receive
// 5~6 -> 01 01 is argument
// 7~8 -> checksum = 0 - ( FF+06+0F+00+01+01 )
// 9 -> EF is end code
void sendPacket(uint8_t command, uint16_t arg = 0)
{
uint8_t out[] = { 0x7E,
0xFF,
06,
command,
00,
static_cast<uint8_t>(arg >> 8),
static_cast<uint8_t>(arg & 0x00ff),
00,
00,
0xEF };
setChecksum(out);
// Output the command
_serial->write(out, sizeof(out));
_commandSendTime = micros();
}
uint16_t calcChecksum(uint8_t* packet)
{
uint16_t sum = 0;
for (int i = 1; i < 7; i++)
{
sum += packet[i];
}
return -sum;
}
void setChecksum(uint8_t* out)
{
uint16_t sum = calcChecksum(out);
out[7] = (sum >> 8);
out[8] = (sum & 0xff);
}
};
#endif // IO_DFPlayer_h

View File

@@ -1,125 +0,0 @@
/*
* © 2022, Colin Murdoch. All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* The IO_EXFastclock device driver is used to interface the standalone fast clock and receive time data.
*
* The EX-fastClock code lives in a separate repo (https://github.com/DCC-EX/EX-Fastclock) and contains the clock logic.
*
*
*/
#ifndef IO_EXFastclock_h
#define IO_EXFastclock_h
#include "IODevice.h"
#include "I2CManager.h"
#include "DIAG.h"
#include "EXRAIL2.h"
#include "CommandDistributor.h"
bool FAST_CLOCK_EXISTS = true;
class EXFastClock : public IODevice {
public:
// Constructor
EXFastClock(I2CAddress i2cAddress){
_I2CAddress = i2cAddress;
addDevice(this);
}
static void create(I2CAddress i2cAddress) {
DIAG(F("Checking for Clock"));
// Start by assuming we will find the clock
// Check if specified I2C address is responding (blocking operation)
// Returns I2C_STATUS_OK (0) if OK, or error code.
uint8_t _checkforclock = I2CManager.checkAddress(i2cAddress);
DIAG(F("Clock check result - %d"), _checkforclock);
// XXXX change thistosave2 bytes
if (_checkforclock == 0) {
FAST_CLOCK_EXISTS = true;
//DIAG(F("I2C Fast Clock found at %s"), i2cAddress.toString());
new EXFastClock(i2cAddress);
}
else {
FAST_CLOCK_EXISTS = false;
//DIAG(F("No Fast Clock found"));
LCD(6,F("CLOCK NOT FOUND"));
}
}
private:
// Initialisation of Fastclock
void _begin() override {
if (FAST_CLOCK_EXISTS == true) {
I2CManager.begin();
if (I2CManager.exists(_I2CAddress)) {
_deviceState = DEVSTATE_NORMAL;
#ifdef DIAG_IO
_display();
#endif
} else {
_deviceState = DEVSTATE_FAILED;
//LCD(6,F("CLOCK NOT FOUND"));
DIAG(F("Fast Clock Not Found at address %s"), _I2CAddress.toString());
}
}
}
// Processing loop to obtain clock time
void _loop(unsigned long currentMicros) override{
if (FAST_CLOCK_EXISTS==true) {
uint8_t readBuffer[3];
byte a,b;
#ifdef EXRAIL_ACTIVE
I2CManager.read(_I2CAddress, readBuffer, 3);
// XXXX change this to save a few bytes
a = readBuffer[0];
b = readBuffer[1];
//_clocktime = (a << 8) + b;
//_clockrate = readBuffer[2];
CommandDistributor::setClockTime(((a << 8) + b), readBuffer[2], 1);
//setClockTime(int16_t clocktime, int8_t clockrate, byte opt);
// As the minimum clock increment is 2 seconds delay a bit - say 1 sec.
// Clock interval is 60/ clockspeed i.e 60/b seconds
delayUntil(currentMicros + ((60/b) * 1000000));
#endif
}
}
// Display EX-FastClock device driver info.
void _display() override {
DIAG(F("FastCLock on I2C:%s - %S"), _I2CAddress.toString(), (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F(""));
}
};
#endif

View File

@@ -1,400 +0,0 @@
/*
* © 2022, Peter Cole. All rights reserved.
*
* This file is part of EX-CommandStation
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* The IO_EXIOExpander.h device driver integrates with one or more EX-IOExpander devices.
* This device driver will configure the device on startup, along with
* interacting with the device for all input/output duties.
*
* To create EX-IOExpander devices, these are defined in myHal.cpp:
* (Note the device driver is included by default)
*
* void halSetup() {
* // EXIOExpander::create(vpin, num_vpins, i2c_address);
* EXIOExpander::create(800, 18, 0x65);
* }
*
* All pins on an EX-IOExpander device are allocated according to the pin map for the specific
* device in use. There is no way for the device driver to sanity check pins are used for the
* correct purpose, however the EX-IOExpander device's pin map will prevent pins being used
* incorrectly (eg. A6/7 on Nano cannot be used for digital input/output).
*
* The total number of pins cannot exceed 256 because of the communications packet format.
* The number of analogue inputs cannot exceed 16 because of a limit on the maximum
* I2C packet size of 32 bytes (in the Wire library).
*/
#ifndef IO_EX_IOEXPANDER_H
#define IO_EX_IOEXPANDER_H
#include "IODevice.h"
#include "I2CManager.h"
#include "DIAG.h"
#include "FSH.h"
/////////////////////////////////////////////////////////////////////////////////////////////////////
/*
* IODevice subclass for EX-IOExpander.
*/
class EXIOExpander : public IODevice {
public:
enum ProfileType : uint8_t {
Instant = 0, // Moves immediately between positions (if duration not specified)
UseDuration = 0, // Use specified duration
Fast = 1, // Takes around 500ms end-to-end
Medium = 2, // 1 second end-to-end
Slow = 3, // 2 seconds end-to-end
Bounce = 4, // For semaphores/turnouts with a bit of bounce!!
NoPowerOff = 0x80, // Flag to be ORed in to suppress power off after move.
};
static void create(VPIN vpin, int nPins, I2CAddress i2cAddress) {
if (checkNoOverlap(vpin, nPins, i2cAddress)) new EXIOExpander(vpin, nPins, i2cAddress);
}
private:
// Constructor
EXIOExpander(VPIN firstVpin, int nPins, I2CAddress i2cAddress) {
_firstVpin = firstVpin;
// Number of pins cannot exceed 256 (1 byte) because of I2C message structure.
if (nPins > 256) nPins = 256;
_nPins = nPins;
_I2CAddress = i2cAddress;
addDevice(this);
}
void _begin() {
uint8_t status;
// Initialise EX-IOExander device
I2CManager.begin();
if (I2CManager.exists(_I2CAddress)) {
// Send config, if EXIOPINS returned, we're good, setup pin buffers, otherwise go offline
// NB The I2C calls here are done as blocking calls, as they're not time-critical
// during initialisation and the reads require waiting for a response anyway.
// Hence we can allocate I/O buffers from the stack.
uint8_t receiveBuffer[3];
uint8_t commandBuffer[4] = {EXIOINIT, (uint8_t)_nPins, (uint8_t)(_firstVpin & 0xFF), (uint8_t)(_firstVpin >> 8)};
status = I2CManager.read(_I2CAddress, receiveBuffer, sizeof(receiveBuffer), commandBuffer, sizeof(commandBuffer));
if (status == I2C_STATUS_OK) {
if (receiveBuffer[0] == EXIOPINS) {
_numDigitalPins = receiveBuffer[1];
_numAnaloguePins = receiveBuffer[2];
// See if we already have suitable buffers assigned
size_t digitalBytesNeeded = (_numDigitalPins + 7) / 8;
if (_digitalPinBytes < digitalBytesNeeded) {
// Not enough space, free any existing buffer and allocate a new one
if (_digitalPinBytes > 0) free(_digitalInputStates);
_digitalInputStates = (byte*) calloc(_digitalPinBytes, 1);
_digitalPinBytes = digitalBytesNeeded;
}
size_t analogueBytesNeeded = _numAnaloguePins * 2;
if (_analoguePinBytes < analogueBytesNeeded) {
// Free any existing buffers and allocate new ones.
if (_analoguePinBytes > 0) {
free(_analogueInputBuffer);
free(_analogueInputStates);
free(_analoguePinMap);
}
_analogueInputStates = (uint8_t*) calloc(analogueBytesNeeded, 1);
_analogueInputBuffer = (uint8_t*) calloc(analogueBytesNeeded, 1);
_analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1);
_analoguePinBytes = analogueBytesNeeded;
}
} else {
DIAG(F("EX-IOExpander I2C:%s ERROR configuring device"), _I2CAddress.toString());
_deviceState = DEVSTATE_FAILED;
return;
}
}
// We now need to retrieve the analogue pin map
if (status == I2C_STATUS_OK) {
commandBuffer[0] = EXIOINITA;
status = I2CManager.read(_I2CAddress, _analoguePinMap, _numAnaloguePins, commandBuffer, 1);
}
if (status == I2C_STATUS_OK) {
// Attempt to get version, if we don't get it, we don't care, don't go offline
uint8_t versionBuffer[3];
commandBuffer[0] = EXIOVER;
if (I2CManager.read(_I2CAddress, versionBuffer, sizeof(versionBuffer), commandBuffer, 1) == I2C_STATUS_OK) {
_majorVer = versionBuffer[0];
_minorVer = versionBuffer[1];
_patchVer = versionBuffer[2];
}
DIAG(F("EX-IOExpander device found, I2C:%s, Version v%d.%d.%d"),
_I2CAddress.toString(), _majorVer, _minorVer, _patchVer);
#ifdef DIAG_IO
_display();
#endif
}
if (status != I2C_STATUS_OK)
reportError(status);
} else {
DIAG(F("EX-IOExpander I2C:%s device not found"), _I2CAddress.toString());
_deviceState = DEVSTATE_FAILED;
}
}
// Digital input pin configuration, used to enable on EX-IOExpander device and set pullups if requested.
// Configuration isn't done frequently so we can use blocking I2C calls here, and so buffers can
// be allocated from the stack to reduce RAM allocation.
bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override {
if (paramCount != 1) return false;
int pin = vpin - _firstVpin;
if (configType == CONFIGURE_INPUT) {
uint8_t pullup = params[0];
uint8_t outBuffer[] = {EXIODPUP, (uint8_t)pin, pullup};
uint8_t responseBuffer[1];
uint8_t status = I2CManager.read(_I2CAddress, responseBuffer, sizeof(responseBuffer),
outBuffer, sizeof(outBuffer));
if (status == I2C_STATUS_OK) {
if (responseBuffer[0] == EXIORDY) {
return true;
} else {
DIAG(F("EXIOVpin %u cannot be used as a digital input pin"), (int)vpin);
}
} else
reportError(status);
} else if (configType == CONFIGURE_ANALOGINPUT) {
// TODO: Consider moving code from _configureAnalogIn() to here and remove _configureAnalogIn
// from IODevice class definition. Not urgent, but each virtual function defined
// means increasing the RAM requirement of every HAL device driver, whether it's relevant
// to the driver or not.
return false;
}
return false;
}
// Analogue input pin configuration, used to enable an EX-IOExpander device.
// Use I2C blocking calls and allocate buffers from stack to save RAM.
int _configureAnalogIn(VPIN vpin) override {
int pin = vpin - _firstVpin;
uint8_t commandBuffer[] = {EXIOENAN, (uint8_t)pin};
uint8_t responseBuffer[1];
uint8_t status = I2CManager.read(_I2CAddress, responseBuffer, sizeof(responseBuffer),
commandBuffer, sizeof(commandBuffer));
if (status == I2C_STATUS_OK) {
if (responseBuffer[0] == EXIORDY) {
return true;
} else {
DIAG(F("EX-IOExpander: Vpin %u cannot be used as an analogue input pin"), (int)vpin);
}
} else
reportError(status);
return false;
}
// Main loop, collect both digital and analogue pin states continuously (faster sensor/input reads)
void _loop(unsigned long currentMicros) override {
if (_deviceState == DEVSTATE_FAILED) return; // If device failed, return
// Request block is used for analogue and digital reads from the IOExpander, which are performed
// on a cyclic basis. Writes are performed synchronously as and when requested.
if (_readState != RDS_IDLE) {
if (_i2crb.isBusy()) return; // If I2C operation still in progress, return
uint8_t status = _i2crb.status;
if (status == I2C_STATUS_OK) { // If device request ok, read input data
// First check if we need to process received data
if (_readState == RDS_ANALOGUE) {
// Read of analogue values was in progress, so process received values
// Here we need to copy the values from input buffer to the analogue value array. We need to
// do this to avoid tearing of the values (i.e. one byte of a two-byte value being changed
// while the value is being read).
memcpy(_analogueInputStates, _analogueInputBuffer, _analoguePinBytes); // Copy I2C input buffer to states
} else if (_readState == RDS_DIGITAL) {
// Read of digital states was in progress, so process received values
// The received digital states are placed directly into the digital buffer on receipt,
// so don't need any further processing at this point (unless we want to check for
// changes and notify them to subscribers, to avoid the need for polling - see IO_GPIOBase.h).
}
} else
reportError(status, false); // report eror but don't go offline.
_readState = RDS_IDLE;
}
// If we're not doing anything now, check to see if a new input transfer is due.
if (_readState == RDS_IDLE) {
if (currentMicros - _lastDigitalRead > _digitalRefresh) { // Delay for digital read refresh
// Issue new read request for digital states. As the request is non-blocking, the buffer has to
// be allocated from heap (object state).
_readCommandBuffer[0] = EXIORDD;
I2CManager.read(_I2CAddress, _digitalInputStates, (_numDigitalPins+7)/8, _readCommandBuffer, 1, &_i2crb);
// non-blocking read
_lastDigitalRead = currentMicros;
_readState = RDS_DIGITAL;
} else if (currentMicros - _lastAnalogueRead > _analogueRefresh) { // Delay for analogue read refresh
// Issue new read for analogue input states
_readCommandBuffer[0] = EXIORDAN;
I2CManager.read(_I2CAddress, _analogueInputBuffer,
_numAnaloguePins * 2, _readCommandBuffer, 1, &_i2crb);
_lastAnalogueRead = currentMicros;
_readState = RDS_ANALOGUE;
}
}
}
// Obtain the correct analogue input value, with reference to the analogue
// pin map.
// Obtain the correct analogue input value
int _readAnalogue(VPIN vpin) override {
if (_deviceState == DEVSTATE_FAILED) return 0;
int pin = vpin - _firstVpin;
for (uint8_t aPin = 0; aPin < _numAnaloguePins; aPin++) {
if (_analoguePinMap[aPin] == pin) {
uint8_t _pinLSBByte = aPin * 2;
uint8_t _pinMSBByte = _pinLSBByte + 1;
return (_analogueInputStates[_pinMSBByte] << 8) + _analogueInputStates[_pinLSBByte];
}
}
return -1; // pin not found in table
}
// Obtain the correct digital input value
int _read(VPIN vpin) override {
if (_deviceState == DEVSTATE_FAILED) return 0;
int pin = vpin - _firstVpin;
uint8_t pinByte = pin / 8;
bool value = bitRead(_digitalInputStates[pinByte], pin - pinByte * 8);
return value;
}
// Write digital value. We could have an output buffer of states, that is periodically
// written to the device if there are any changes; this would reduce the I2C overhead
// if lots of output requests are being made. We could also cache the last value
// sent so that we don't write the same value over and over to the output.
// However, for the time being, we just write the current value (blocking I2C) to the
// IOExpander node. As it is a blocking request, we can use buffers allocated from
// the stack to save RAM allocation.
void _write(VPIN vpin, int value) override {
uint8_t digitalOutBuffer[3];
uint8_t responseBuffer[1];
if (_deviceState == DEVSTATE_FAILED) return;
int pin = vpin - _firstVpin;
digitalOutBuffer[0] = EXIOWRD;
digitalOutBuffer[1] = pin;
digitalOutBuffer[2] = value;
uint8_t status = I2CManager.read(_I2CAddress, responseBuffer, 1, digitalOutBuffer, 3);
if (status != I2C_STATUS_OK) {
reportError(status);
} else {
if (responseBuffer[0] != EXIORDY) {
DIAG(F("Vpin %u cannot be used as a digital output pin"), (int)vpin);
}
}
}
// Write analogue (integer) value. Write the parameters (blocking I2C) to the
// IOExpander node. As it is a blocking request, we can use buffers allocated from
// the stack to reduce RAM allocation.
void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override {
uint8_t servoBuffer[7];
uint8_t responseBuffer[1];
if (_deviceState == DEVSTATE_FAILED) return;
int pin = vpin - _firstVpin;
#ifdef DIAG_IO
DIAG(F("Servo: WriteAnalogue Vpin:%u Value:%d Profile:%d Duration:%d %S"),
vpin, value, profile, duration, _deviceState == DEVSTATE_FAILED?F("DEVSTATE_FAILED"):F(""));
#endif
servoBuffer[0] = EXIOWRAN;
servoBuffer[1] = pin;
servoBuffer[2] = value & 0xFF;
servoBuffer[3] = value >> 8;
servoBuffer[4] = profile;
servoBuffer[5] = duration & 0xFF;
servoBuffer[6] = duration >> 8;
uint8_t status = I2CManager.read(_I2CAddress, responseBuffer, 1, servoBuffer, 7);
if (status != I2C_STATUS_OK) {
DIAG(F("EX-IOExpander I2C:%s Error:%d %S"), _I2CAddress.toString(), status, I2CManager.getErrorMessage(status));
_deviceState = DEVSTATE_FAILED;
} else {
if (responseBuffer[0] != EXIORDY) {
DIAG(F("Vpin %u cannot be used as a servo/PWM pin"), (int)vpin);
}
}
}
// Display device information and status.
void _display() override {
DIAG(F("EX-IOExpander I2C:%s v%d.%d.%d Vpins %u-%u %S"),
_I2CAddress.toString(), _majorVer, _minorVer, _patchVer,
(int)_firstVpin, (int)_firstVpin+_nPins-1,
_deviceState == DEVSTATE_FAILED ? F("OFFLINE") : F(""));
}
// Helper function for error handling
void reportError(uint8_t status, bool fail=true) {
DIAG(F("EX-IOExpander I2C:%s Error:%d (%S)"), _I2CAddress.toString(),
status, I2CManager.getErrorMessage(status));
if (fail)
_deviceState = DEVSTATE_FAILED;
}
uint8_t _numDigitalPins = 0;
uint8_t _numAnaloguePins = 0;
uint8_t _majorVer = 0;
uint8_t _minorVer = 0;
uint8_t _patchVer = 0;
uint8_t* _digitalInputStates;
uint8_t* _analogueInputStates;
uint8_t* _analogueInputBuffer; // buffer for I2C input transfers
uint8_t _readCommandBuffer[1];
uint8_t _digitalPinBytes = 0; // Size of allocated memory buffer (may be longer than needed)
uint8_t _analoguePinBytes = 0; // Size of allocated memory buffers (may be longer than needed)
uint8_t* _analoguePinMap;
I2CRB _i2crb;
enum {RDS_IDLE, RDS_DIGITAL, RDS_ANALOGUE}; // Read operation states
uint8_t _readState = RDS_IDLE;
unsigned long _lastDigitalRead = 0;
unsigned long _lastAnalogueRead = 0;
const unsigned long _digitalRefresh = 10000UL; // Delay refreshing digital inputs for 10ms
const unsigned long _analogueRefresh = 50000UL; // Delay refreshing analogue inputs for 50ms
// EX-IOExpander protocol flags
enum {
EXIOINIT = 0xE0, // Flag to initialise setup procedure
EXIORDY = 0xE1, // Flag we have completed setup procedure, also for EX-IO to ACK setup
EXIODPUP = 0xE2, // Flag we're sending digital pin pullup configuration
EXIOVER = 0xE3, // Flag to get version
EXIORDAN = 0xE4, // Flag to read an analogue input
EXIOWRD = 0xE5, // Flag for digital write
EXIORDD = 0xE6, // Flag to read digital input
EXIOENAN = 0xE7, // Flag to enable an analogue pin
EXIOINITA = 0xE8, // Flag we're receiving analogue pin mappings
EXIOPINS = 0xE9, // Flag we're receiving pin counts for buffers
EXIOWRAN = 0xEA, // Flag we're sending an analogue write (PWM)
EXIOERR = 0xEF, // Flag we've received an error
};
};
#endif

View File

@@ -1,121 +0,0 @@
/*
* © 2021, Peter Cole. All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* The IO_EXTurntable device driver is used to control a turntable via an Arduino with a stepper motor over I2C.
*
* The EX-Turntable code lives in a separate repo (https://github.com/DCC-EX/Turntable-EX) and contains the stepper motor logic.
*
* This device driver sends a step position to Turntable-EX to indicate the step position to move to using either of these commands:
* <D TT vpin steps activity> in the serial console
* MOVETT(vpin, steps, activity) in EX-RAIL
* Refer to the documentation for further information including the valid activities.
*/
#ifndef IO_EXTurntable_h
#define IO_EXTurntable_h
#include "IODevice.h"
#include "I2CManager.h"
#include "DIAG.h"
void EXTurntable::create(VPIN firstVpin, int nPins, I2CAddress I2CAddress) {
new EXTurntable(firstVpin, nPins, I2CAddress);
}
// Constructor
EXTurntable::EXTurntable(VPIN firstVpin, int nPins, I2CAddress I2CAddress) {
_firstVpin = firstVpin;
_nPins = nPins;
_I2CAddress = I2CAddress;
addDevice(this);
}
// Initialisation of EXTurntable
void EXTurntable::_begin() {
I2CManager.begin();
I2CManager.setClock(1000000);
if (I2CManager.exists(_I2CAddress)) {
#ifdef DIAG_IO
_display();
#endif
} else {
_deviceState = DEVSTATE_FAILED;
}
}
// Processing loop to obtain status of stepper
// 0 = finished moving and in correct position
// 1 = still moving
void EXTurntable::_loop(unsigned long currentMicros) {
uint8_t readBuffer[1];
I2CManager.read(_I2CAddress, readBuffer, 1);
_stepperStatus = readBuffer[0];
// DIAG(F("Turntable-EX returned status: %d"), _stepperStatus);
delayUntil(currentMicros + 500000); // Wait 500ms before checking again, turntables turn slowly
}
// Read returns status as obtained in our loop.
// Return false if our status value is invalid.
int EXTurntable::_read(VPIN vpin) {
if (_deviceState == DEVSTATE_FAILED) return 0;
// DIAG(F("_read status: %d"), _stepperStatus);
if (_stepperStatus > 1) {
return false;
} else {
return _stepperStatus;
}
}
// writeAnalogue to send the steps and activity to Turntable-EX.
// Sends 3 bytes containing the MSB and LSB of the step count, and activity.
// value contains the steps, bit shifted to MSB + LSB.
// activity contains the activity flag as per this list:
//
// Turn = 0, // Rotate turntable, maintain phase
// Turn_PInvert = 1, // Rotate turntable, invert phase
// Home = 2, // Initiate homing
// Calibrate = 3, // Initiate calibration sequence
// LED_On = 4, // Turn LED on
// LED_Slow = 5, // Set LED to a slow blink
// LED_Fast = 6, // Set LED to a fast blink
// LED_Off = 7, // Turn LED off
// Acc_On = 8, // Turn accessory pin on
// Acc_Off = 9 // Turn accessory pin off
void EXTurntable::_writeAnalogue(VPIN vpin, int value, uint8_t activity, uint16_t duration) {
if (_deviceState == DEVSTATE_FAILED) return;
uint8_t stepsMSB = value >> 8;
uint8_t stepsLSB = value & 0xFF;
#ifdef DIAG_IO
DIAG(F("EX-Turntable WriteAnalogue VPIN:%u Value:%d Activity:%d Duration:%d"),
vpin, value, activity, duration);
DIAG(F("I2CManager write I2C Address:%d stepsMSB:%d stepsLSB:%d activity:%d"),
_I2CAddress.toString(), stepsMSB, stepsLSB, activity);
#endif
_stepperStatus = 1; // Tell the device driver Turntable-EX is busy
I2CManager.write(_I2CAddress, 3, stepsMSB, stepsLSB, activity);
}
// Display Turnetable-EX device driver info.
void EXTurntable::_display() {
DIAG(F("EX-Turntable I2C:%s Configured on Vpins:%u-%u %S"), _I2CAddress.toString(), (int)_firstVpin,
(int)_firstVpin+_nPins-1, (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F(""));
}
#endif

View File

@@ -1,165 +0,0 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* To declare a device instance,
* IO_ExampleSerial myDevice(1000, 10, Serial3, 9600);
* or to create programmatically,
* IO_ExampleSerial::create(1000, 10, Serial3, 9600);
*
* (uses VPINs 1000-1009, talke on Serial 3 at 9600 baud.)
*
* See IO_ExampleSerial.cpp for the protocol used over the serial line.
*
*/
#ifndef IO_EXAMPLESERIAL_H
#define IO_EXAMPLESERIAL_H
#include "IODevice.h"
class IO_ExampleSerial : public IODevice {
private:
// Here we define the device-specific variables.
HardwareSerial *_serial;
uint8_t _inputState = 0;
int _inputIndex = 0;
int _inputValue = 0;
uint16_t *_pinValues; // Pointer to block of memory containing pin values
unsigned long _baud;
public:
// Static function to handle "IO_ExampleSerial::create(...)" calls.
static void create(VPIN firstVpin, int nPins, HardwareSerial *serial, unsigned long baud) {
if (checkNoOverlap(firstVpin,nPins)) new IO_ExampleSerial(firstVpin, nPins, serial, baud);
}
protected:
// Constructor. This should initialise variables etc. but not call other objects yet
// (e.g. Serial, I2CManager, and other parts of the CS functionality).
// defer those until the _begin() function. The 'addDevice' call is required unless
// the device is not to be added (e.g. because of incorrect parameters).
IO_ExampleSerial(VPIN firstVpin, int nPins, HardwareSerial *serial, unsigned long baud) {
_firstVpin = firstVpin;
_nPins = nPins;
_pinValues = (uint16_t *)calloc(_nPins, sizeof(uint16_t));
_baud = baud;
// Save reference to serial port driver
_serial = serial;
addDevice(this);
}
// Device-specific initialisation
void _begin() override {
_serial->begin(_baud);
#if defined(DIAG_IO)
_display();
#endif
// Send a few # characters to the output
for (uint8_t i=0; i<3; i++)
_serial->write('#');
}
// Device-specific write function. Write a string in the form "#Wm,n#"
// where m is the vpin number, and n is the value.
void _write(VPIN vpin, int value) {
int pin = vpin -_firstVpin;
#ifdef DIAG_IO
DIAG(F("IO_ExampleSerial::_write VPIN:%u Value:%d"), (int)vpin, value);
#endif
// Send a command string over the serial line
_serial->print('#');
_serial->print('W');
_serial->print(pin);
_serial->print(',');
_serial->print(value);
_serial->println('#');
DIAG(F("ExampleSerial Sent command, p1=%d, p2=%d"), vpin, value);
}
// Device-specific read function.
int _read(VPIN vpin) {
// Return a value for the specified vpin.
int result = _pinValues[vpin-_firstVpin];
return result;
}
// Loop function to do background scanning of the input port. State
// machine parses the incoming command as it is received. Command
// is in the form "#Nm,n#" where m is the index and n is the value.
void _loop(unsigned long currentMicros) {
(void)currentMicros; // Suppress compiler warnings
if (_serial->available()) {
// Input data available to read. Read a character.
char c = _serial->read();
switch (_inputState) {
case 0: // Waiting for start of command
if (c == '#') // Start of command received.
_inputState = 1;
break;
case 1: // Expecting command character
if (c == 'N') { // 'Notify' character received
_inputState = 2;
_inputValue = _inputIndex = 0;
} else
_inputState = 0; // Unexpected char, reset
break;
case 2: // reading first parameter (index)
if (isdigit(c))
_inputIndex = _inputIndex * 10 + (c-'0');
else if (c==',')
_inputState = 3;
else
_inputState = 0; // Unexpected char, reset
break;
case 3: // reading reading second parameter (value)
if (isdigit(c))
_inputValue = _inputValue * 10 - (c-'0');
else if (c=='#') { // End of command
// Complete command received, do something with it.
DIAG(F("ExampleSerial Received command, p1=%d, p2=%d"), _inputIndex, _inputValue);
if (_inputIndex >= 0 && _inputIndex < _nPins) { // Store value
_pinValues[_inputIndex] = _inputValue;
}
_inputState = 0; // Done, start again.
} else
_inputState = 0; // Unexpected char, reset
break;
}
}
}
// Display information about the device, and perhaps its current condition (e.g. active, disabled etc).
// Here we display the current values held for the pins.
void _display() {
DIAG(F("IO_ExampleSerial Configured on Vpins:%u-%u"), (int)_firstVpin,
(int)_firstVpin+_nPins-1);
for (int i=0; i<_nPins; i++)
DIAG(F(" VPin %2u: %d"), _firstVpin+i, _pinValues[i]);
}
};
#endif // IO_EXAMPLESERIAL_H

View File

@@ -1,250 +0,0 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef IO_GPIOBASE_H
#define IO_GPIOBASE_H
#include "IODevice.h"
#include "I2CManager.h"
#include "DIAG.h"
// GPIOBase is defined as a class template. This allows it to be instantiated by
// subclasses with different types, according to the number of pins on the GPIO module.
// For example, GPIOBase<uint8_t> for 8 pins, GPIOBase<uint16_t> for 16 pins etc.
// A module with up to 64 pins can be handled in this way (uint64_t).
template <class T>
class GPIOBase : public IODevice {
protected:
// Constructor
GPIOBase(FSH *deviceName, VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin);
// Device-specific initialisation
void _begin() override;
// Device-specific pin configuration function.
bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) override;
// Pin write function.
void _write(VPIN vpin, int value) override;
// Pin read function.
int _read(VPIN vpin) override;
void _display() override;
void _loop(unsigned long currentMicros) override;
// Data fields
// Allocate enough space for all input pins
T _portInputState; // 1=high (inactive), 0=low (activated)
T _portOutputState; // 1 =high, 0=low
T _portMode; // 0=input, 1=output
T _portPullup; // 0=nopullup, 1=pullup
T _portInUse; // 0=not in use, 1=in use
// Target interval between refreshes of each input port
static const int _portTickTime = 4000; // 4ms
// Virtual functions for interfacing with I2C GPIO Device
virtual void _writeGpioPort() = 0;
virtual void _readGpioPort(bool immediate=true) = 0;
virtual void _writePullups() {};
virtual void _writePortModes() {};
virtual void _setupDevice() {};
virtual void _processCompletion(uint8_t status) {
(void)status; // Suppress compiler warning
};
I2CRB requestBlock;
FSH *_deviceName;
};
// Because class GPIOBase is a template, the implementation (below) must be contained within the same
// file as the class declaration (above). Otherwise it won't compile!
// Constructor
template <class T>
GPIOBase<T>::GPIOBase(FSH *deviceName, VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin) :
IODevice(firstVpin, nPins)
{
if (_nPins > (int)sizeof(T)*8) _nPins = sizeof(T)*8; // Ensure nPins is consistent with the number of bits in T
_deviceName = deviceName;
_I2CAddress = i2cAddress;
_gpioInterruptPin = interruptPin;
_hasCallback = true;
// Add device to list of devices.
addDevice(this);
_portMode = 0; // default to input mode
_portPullup = -1; // default to pullup enabled
_portInputState = -1; // default to all inputs high (inactive)
_portInUse = 0; // No ports in use initially.
}
template <class T>
void GPIOBase<T>::_begin() {
// Configure pin used for GPIO extender notification of change (if allocated)
if (_gpioInterruptPin >= 0)
pinMode(_gpioInterruptPin, INPUT_PULLUP);
I2CManager.begin();
I2CManager.setClock(400000);
if (I2CManager.exists(_I2CAddress)) {
#if defined(DIAG_IO)
_display();
#endif
_setupDevice();
_deviceState = DEVSTATE_NORMAL;
} else {
DIAG(F("%S I2C:%s Device not detected"), _deviceName, _I2CAddress.toString());
_deviceState = DEVSTATE_FAILED;
}
}
// Configuration parameters for inputs:
// params[0]: enable pullup
template <class T>
bool GPIOBase<T>::_configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) {
if (configType != CONFIGURE_INPUT) return false;
if (paramCount == 0 || paramCount > 1) return false;
bool pullup = params[0];
int pin = vpin - _firstVpin;
#ifdef DIAG_IO
DIAG(F("%S I2C:%s Config Pin:%d Val:%d"), _deviceName, _I2CAddress.toString(), pin, pullup);
#endif
uint16_t mask = 1 << pin;
if (pullup)
_portPullup |= mask;
else
_portPullup &= ~mask;
// Mark that port has been accessed
_portInUse |= mask;
// Set input mode
_portMode &= ~mask;
// Call subclass's virtual function to write to device
_writePortModes();
_writePullups();
// Port change will be notified on next loop entry.
return true;
}
// Periodically read the input port
template <class T>
void GPIOBase<T>::_loop(unsigned long currentMicros) {
T lastPortStates = _portInputState;
if (_deviceState == DEVSTATE_SCANNING && !requestBlock.isBusy()) {
uint8_t status = requestBlock.status;
if (status == I2C_STATUS_OK) {
_deviceState = DEVSTATE_NORMAL;
} else {
_deviceState = DEVSTATE_FAILED;
DIAG(F("%S I2C:%s Error:%d %S"), _deviceName, _I2CAddress.toString(), status,
I2CManager.getErrorMessage(status));
}
_processCompletion(status);
// Set unused pin and write mode pin value to 1
_portInputState |= ~_portInUse | _portMode;
// Scan for changes in input states and invoke callback (if present)
T differences = lastPortStates ^ _portInputState;
if (differences && IONotifyCallback::hasCallback()) {
// Scan for differences bit by bit
T mask = 1;
for (int pin=0; pin<_nPins; pin++) {
if (differences & mask) {
// Change detected.
IONotifyCallback::invokeAll(_firstVpin+pin, (_portInputState & mask) == 0);
}
mask <<= 1;
}
}
#ifdef DIAG_IO
if (differences)
DIAG(F("%S I2C:%s PortStates:%x"), _deviceName, _I2CAddress.toString(), _portInputState);
#endif
}
// Check if interrupt configured. If not, or if it is active (pulled down), then
// initiate a scan.
if (_gpioInterruptPin < 0 || !digitalRead(_gpioInterruptPin)) {
// TODO: Could suppress reads if there are no pins configured as inputs!
// Read input
if (_deviceState == DEVSTATE_NORMAL) {
_readGpioPort(false); // Initiate non-blocking read
_deviceState= DEVSTATE_SCANNING;
}
}
// Delay next entry until tick elapsed.
delayUntil(currentMicros + _portTickTime);
}
template <class T>
void GPIOBase<T>::_display() {
DIAG(F("%S I2C:%s Configured on Vpins:%u-%u %S"), _deviceName, _I2CAddress.toString(),
_firstVpin, _firstVpin+_nPins-1, (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F(""));
}
template <class T>
void GPIOBase<T>::_write(VPIN vpin, int value) {
int pin = vpin - _firstVpin;
T mask = 1 << pin;
#ifdef DIAG_IO
DIAG(F("%S I2C:%s Write Pin:%d Val:%d"), _deviceName, _I2CAddress.toString(), pin, value);
#endif
// Set port mode output if currently not output mode
if (!(_portMode & mask)) {
_portInUse |= mask;
_portMode |= mask;
_writePortModes();
}
// Update port output state
if (value)
_portOutputState |= mask;
else
_portOutputState &= ~mask;
// Call subclass's virtual function to write to device.
return _writeGpioPort();
}
template <class T>
int GPIOBase<T>::_read(VPIN vpin) {
int pin = vpin - _firstVpin;
T mask = 1 << pin;
// Set port mode to input if currently output or first use
if ((_portMode | ~_portInUse) & mask) {
_portMode &= ~mask;
_portInUse |= mask;
_writePullups();
_writePortModes();
// Port won't have been read yet, so read it now.
_readGpioPort();
// Set unused pin and write mode pin value to 1
_portInputState |= ~_portInUse | _portMode;
#ifdef DIAG_IO
DIAG(F("%S I2C:%s PortStates:%x"), _deviceName, _I2CAddress.toString(), _portInputState);
#endif
}
return (_portInputState & mask) ? 0 : 1; // Invert state (5v=0, 0v=1)
}
#endif

View File

@@ -1,262 +0,0 @@
/*
* © 2023, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* This driver provides a more immediate interface into the OLED display
* than the one installed through the config.h file. When an LCD(...) call
* is made, the text is output immediately to the specified display line,
* without waiting for the next 2.5 second refresh. However, if the line
* specified is off the screen then the text in the bottom line will be
* overwritten. There is however a special case that if line 255 is specified,
* the existing text will scroll up and the new line added to the bottom
* line of the screen.
*
* To install, use the following command in myHal.cpp:
*
* HALDisplay<OLED>::create(address, width, height);
*
* where address is the I2C address of the OLED display (0x3c or 0x3d),
* width is the width in pixels, and height is the height in pixels.
*
* Valid width and height are 128x32 (SSD1306 controller),
* 128x64 (SSD1306) and 132x64 (SH1106). The driver uses
* a 5x7 character set in a 6x8 pixel cell.
*
* OR
*
* HALDisplay<LiquidCrystal>::create(address, width, height);
*
* where address is the I2C address of the LCD display (0x27 typically),
* width is the width in characters (16 or 20 typically),
* and height is the height in characters (2 or 4 typically).
*/
#ifndef IO_HALDisplay_H
#define IO_HALDisplay_H
#include "IODevice.h"
#include "DisplayInterface.h"
#include "SSD1306Ascii.h"
#include "LiquidCrystal_I2C.h"
#include "version.h"
typedef SSD1306AsciiWire OLED;
typedef LiquidCrystal_I2C LiquidCrystal;
template <class T>
class HALDisplay : public IODevice, public DisplayInterface {
private:
// Here we define the device-specific variables.
uint8_t _height; // in pixels
uint8_t _width; // in pixels
T *_displayDriver;
uint8_t _rowNo = 0; // Row number being written by caller
uint8_t _colNo = 0; // Position in line being written by caller
uint8_t _numRows;
uint8_t _numCols;
char *_buffer = NULL;
uint8_t *_rowGeneration = NULL;
uint8_t *_lastRowGeneration = NULL;
uint8_t _rowNoToScreen = 0;
uint8_t _charPosToScreen = 0;
bool _startAgain = false;
DisplayInterface *_nextDisplay = NULL;
public:
// Static function to handle "HALDisplay::create(...)" calls.
static void create(I2CAddress i2cAddress, int width, int height) {
if (checkNoOverlap(0, 0, i2cAddress)) new HALDisplay(0, i2cAddress, width, height);
}
static void create(uint8_t displayNo, I2CAddress i2cAddress, int width, int height) {
if (checkNoOverlap(0, 0, i2cAddress)) new HALDisplay(displayNo, i2cAddress, width, height);
}
protected:
// Constructor
HALDisplay(uint8_t displayNo, I2CAddress i2cAddress, int width, int height) {
_displayDriver = new T(i2cAddress, width, height);
if (!_displayDriver) return; // Check for memory allocation failure
_I2CAddress = i2cAddress;
_width = width;
_height = height;
_numCols = _displayDriver->getNumCols();
_numRows = _displayDriver->getNumRows();
_charPosToScreen = _numCols;
// Allocate arrays
_buffer = (char *)calloc(_numRows*_numCols, sizeof(char));
if (!_buffer) return; // Check for memory allocation failure
_rowGeneration = (uint8_t *)calloc(_numRows, sizeof(uint8_t));
if (!_rowGeneration) return; // Check for memory allocation failure
_lastRowGeneration = (uint8_t *)calloc(_numRows, sizeof(uint8_t));
if (!_lastRowGeneration) return; // Check for memory allocation failure
// Fill buffer with spaces
memset(_buffer, ' ', _numCols*_numRows);
_displayDriver->clearNative();
// Add device to list of HAL devices (not necessary but allows
// status to be displayed using <D HAL SHOW> and device to be
// reinitialised using <D HAL RESET>).
IODevice::addDevice(this);
// Also add this display to list of display handlers
DisplayInterface::addDisplay(displayNo);
// Is this the system display (0)?
if (displayNo == 0) {
// Set first two lines on screen
this->setRow(displayNo, 0);
print(F("DCC-EX v"));
print(F(VERSION));
setRow(displayNo, 1);
print(F("Lic GPLv3"));
}
}
void screenUpdate() {
// Loop through the buffer and if a row has changed
// (rowGeneration[row] is changed) then start writing the
// characters from the buffer, one character per entry,
// to the screen until that row has been refreshed.
// First check if the OLED driver is still busy from a previous
// call. If so, don't do anything until the next entry.
if (!_displayDriver->isBusy()) {
// Check if we've just done the end of a row
if (_charPosToScreen >= _numCols) {
// Move to next line
if (++_rowNoToScreen >= _numRows || _startAgain) {
_rowNoToScreen = 0; // Wrap to first row
_startAgain = false;
}
if (_rowGeneration[_rowNoToScreen] != _lastRowGeneration[_rowNoToScreen]) {
// Row content has changed, so start outputting it
_lastRowGeneration[_rowNoToScreen] = _rowGeneration[_rowNoToScreen];
_displayDriver->setRowNative(_rowNoToScreen);
_charPosToScreen = 0; // Prepare to output first character on next entry
} else {
// Row not changed, don't bother writing it.
}
} else {
// output character at current position
_displayDriver->writeNative(_buffer[_rowNoToScreen*_numCols+_charPosToScreen++]);
}
}
return;
}
/////////////////////////////////////////////////
// IODevice Class Member Overrides
/////////////////////////////////////////////////
// Device-specific initialisation
void _begin() override {
// Initialise device
if (_displayDriver->begin()) {
_display();
// Force all rows to be redrawn
for (uint8_t row=0; row<_numRows; row++)
_rowGeneration[row]++;
// Start with top line (looks better).
// The numbers will wrap round on the first loop2 entry.
_rowNoToScreen = _numRows;
_charPosToScreen = _numCols;
}
}
void _loop(unsigned long) override {
screenUpdate();
}
// Display information about the device.
void _display() {
DIAG(F("HALDisplay %d configured on addr %s"), _displayNo, _I2CAddress.toString());
}
/////////////////////////////////////////////////
// DisplayInterface functions
//
/////////////////////////////////////////////////
public:
void _displayLoop() override {
screenUpdate();
}
// Position on nominated line number (0 to number of lines -1)
// Clear the line in the buffer ready for updating
// The displayNo referenced here is remembered and any following
// calls to write() will be directed to that display.
void _setRow(byte line) override {
if (line == 255) {
// LCD(255,"xxx") or SCREEN(displayNo,255, "xxx") -
// scroll the contents of the buffer and put the new line
// at the bottom of the screen
for (int row=1; row<_numRows; row++) {
strncpy(&_buffer[(row-1)*_numCols], &_buffer[row*_numCols], _numCols);
_rowGeneration[row-1]++;
}
line = _numRows-1;
} else if (line >= _numRows)
line = _numRows - 1; // Overwrite bottom line.
_rowNo = line;
// Fill line with blanks
for (_colNo = 0; _colNo < _numCols; _colNo++)
_buffer[_rowNo*_numCols+_colNo] = ' ';
_colNo = 0;
// Mark that the buffer has been touched. It will start being
// sent to the screen on the next loop entry, by which time
// the line should have been written to the buffer.
_rowGeneration[_rowNo]++;
// Indicate that the output loop is to start updating the screen again from
// row 0. Otherwise, on a full screen rewrite the bottom part may be drawn
// before the top part!
_startAgain = true;
}
// Write one character to the screen referenced in the last setRow() call.
virtual size_t _write(uint8_t c) override {
// Write character to buffer (if there's space)
if (_colNo < _numCols) {
_buffer[_rowNo*_numCols+_colNo++] = c;
}
return 1;
}
// Write blanks to all of the screen buffer
void _clear() {
// Clear buffer
memset(_buffer, ' ', _numCols*_numRows);
_colNo = 0;
_rowNo = 0;
}
};
#endif // IO_HALDisplay_H

View File

@@ -1,243 +0,0 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* The HC-SR04 module has an ultrasonic transmitter (40kHz) and a receiver.
* It is operated through two signal pins. When the transmit pin is set to 1
* for 10us, on the falling edge the transmitter sends a short transmission of
* 8 pulses (like a sonar 'ping'). This is reflected off objects and received
* by the receiver. A pulse is sent on the receive pin whose length is equal
* to the delay between the transmission of the pulse and the detection of
* its echo. The distance of the reflecting object is calculated by halving
* the time (to allow for the out and back distance), then multiplying by the
* speed of sound (assumed to be constant).
*
* This driver polls the HC-SR04 by sending the trigger pulse and then measuring
* the length of the received pulse. If the calculated distance is less than
* the threshold, the output _state returned by a read() call changes to 1. If
* the distance is greater than the threshold plus a hysteresis margin, the
* output changes to 0. The device also supports readAnalogue(), which returns
* the measured distance in cm, or 32767 if the distance exceeds the
* offThreshold.
*
* It might be thought that the measurement would be more reliable if interrupts
* were disabled while the pulse is being timed. However, this would affect
* other functions in the CS so the measurement is being performed with
* interrupts enabled. Also, we could use an interrupt pin in the Arduino for
* the timing, but the same consideration applies. In any case, the DCC
* interrupt occurs once every 58us, so any IRC code is much faster than that.
* And 58us corresponds to 1cm in the calculation, so the effect of
* interrupts is negligible.
*
* Note: The timing accuracy required for measuring the pulse length means that
* the pins have to be direct Arduino pins; GPIO pins on an IO Extender cannot
* provide the required accuracy.
*
* Example configuration:
* HCSR04::create(23000, 32, 33, 80, 85);
*
* Where 23000 is the VPIN allocated,
* 32 is the pin connected to the HCSR04 trigger terminal,
* 33 is the pin connected to the HCSR04 echo terminal,
* 80 is the distance in cm below which pin 23000 will be active,
* and 85 is the distance in cm above which pin 23000 will be inactive.
*
* Alternative configuration, which hogs the processor until the measurement is complete
* (old behaviour, more accurate but higher impact on other CS tasks):
* HCSR04::create(23000, 32, 33, 80, 85, HCSR04::LOOP);
*
*/
#ifndef IO_HCSR04_H
#define IO_HCSR04_H
#include "IODevice.h"
class HCSR04 : public IODevice {
private:
// pins must be arduino GPIO pins, not extender pins or HAL pins.
int _trigPin = -1;
int _echoPin = -1;
// Thresholds for setting active _state in cm.
uint8_t _onThreshold; // cm
uint8_t _offThreshold; // cm
// Last measured distance in cm.
uint16_t _distance;
// Active=1/inactive=0 _state
uint8_t _value = 0;
// Factor for calculating the distance (cm) from echo time (us).
// Based on a speed of sound of 345 metres/second.
const uint16_t factor = 58; // us/cm
// Limit the time spent looping by dropping out when the expected
// worst case threshold value is greater than an arbitrary value.
const uint16_t maxPermittedLoopTime = 10 * factor; // max in us
unsigned long _startTime = 0;
unsigned long _maxTime = 0;
enum {DORMANT, MEASURING}; // _state values
uint8_t _state = DORMANT;
uint8_t _counter = 0;
uint16_t _options = 0;
public:
enum Options {
LOOP = 1, // Option HCSR04::LOOP reinstates old behaviour, i.e. complete measurement in one loop entry.
};
// Static create function provides alternative way to create object
static void create(VPIN vpin, int trigPin, int echoPin, uint16_t onThreshold, uint16_t offThreshold, uint16_t options = 0) {
if (checkNoOverlap(vpin))
new HCSR04(vpin, trigPin, echoPin, onThreshold, offThreshold, options);
}
protected:
// Constructor performs static initialisation of the device object
HCSR04 (VPIN vpin, int trigPin, int echoPin, uint16_t onThreshold, uint16_t offThreshold, uint16_t options) {
_firstVpin = vpin;
_nPins = 1;
_trigPin = trigPin;
_echoPin = echoPin;
_onThreshold = onThreshold;
_offThreshold = offThreshold;
_options = options;
addDevice(this);
}
// _begin function called to perform dynamic initialisation of the device
void _begin() override {
_state = 0;
pinMode(_trigPin, OUTPUT);
pinMode(_echoPin, INPUT);
ArduinoPins::fastWriteDigital(_trigPin, 0);
#if defined(DIAG_IO)
_display();
#endif
}
// _read function - just return _value (calculated in _loop).
int _read(VPIN vpin) override {
(void)vpin; // avoid compiler warning
return _value;
}
int _readAnalogue(VPIN vpin) override {
(void)vpin; // avoid compiler warning
return _distance;
}
// _loop function - read HC-SR04 once every 100 milliseconds.
void _loop(unsigned long currentMicros) override {
unsigned long waitTime;
switch(_state) {
case DORMANT: // Issue pulse
// If receive pin is still set on from previous call, do nothing till next entry.
if (ArduinoPins::fastReadDigital(_echoPin)) return;
// Send 10us pulse to trigger transmitter
ArduinoPins::fastWriteDigital(_trigPin, 1);
delayMicroseconds(10);
ArduinoPins::fastWriteDigital(_trigPin, 0);
// Wait, with timeout, for echo pin to become set.
// Measured time delay is just under 500us, so
// wait for max of 1000us.
_startTime = micros();
_maxTime = 1000;
while (!ArduinoPins::fastReadDigital(_echoPin)) {
// Not set yet, see if we've timed out.
waitTime = micros() - _startTime;
if (waitTime > _maxTime) {
// Timeout waiting for pulse start, abort the read and start again
_state = DORMANT;
return;
}
}
// Echo pulse started, so wait for echo pin to reset, and measure length of pulse
_startTime = micros();
_maxTime = factor * _offThreshold;
_state = MEASURING;
// If maximum measurement time is high, then skip until next loop entry before
// starting to look for pulse end.
// This gives better accuracy at shorter distance thresholds but without extending
// loop execution time for longer thresholds. If LOOP option is set on, then
// the entire measurement will be done in one loop entry, i.e. the code will fall
// through into the measuring phase.
if (!(_options & LOOP) && _maxTime > maxPermittedLoopTime) break;
/* fallthrough */
case MEASURING: // Check if echo pulse has finished
do {
waitTime = micros() - _startTime;
if (!ArduinoPins::fastReadDigital(_echoPin)) {
// Echo pulse completed; check if pulse length is below threshold and if so set value.
if (waitTime <= factor * _onThreshold) {
// Measured time is within the onThreshold, so value is one.
_value = 1;
// If the new distance value is less than the current, use it immediately.
// But if the new distance value is longer, then it may be erroneously long
// (because of extended loop times delays), so apply a delay to distance increases.
uint16_t estimatedDistance = waitTime / factor;
if (estimatedDistance < _distance)
_distance = estimatedDistance;
else
_distance += 1; // Just increase distance slowly.
_counter = 0;
//DIAG(F("HCSR04: Pulse Len=%l Distance=%d"), waitTime, _distance);
}
_state = DORMANT;
} else {
// Echo pulse hasn't finished, so check if maximum time has elapsed
// If pulse is too long then set return value to zero,
// and finish without waiting for end of pulse.
if (waitTime > _maxTime) {
// Pulse length longer than maxTime, value is provisionally zero.
// But don't change _value unless provisional value is zero for 10 consecutive measurements
if (_value == 1) {
if (++_counter >= 10) {
_value = 0;
_distance = 32767;
_counter = 0;
}
}
_state = DORMANT; // start again
}
}
// If there's lots of time remaining before the expected completion time,
// then exit and wait for next loop entry. Otherwise, loop until we finish.
// If option LOOP is set, then we loop until finished anyway.
uint32_t remainingTime = _maxTime - waitTime;
if (!(_options & LOOP) && remainingTime < maxPermittedLoopTime) return;
} while (_state == MEASURING) ;
break;
}
// Datasheet recommends a wait of at least 60ms between measurement cycles
if (_state == DORMANT)
delayUntil(currentMicros+60000UL); // wait 60ms till next measurement
}
void _display() override {
DIAG(F("HCSR04 Configured on VPIN:%u TrigPin:%d EchoPin:%d On:%dcm Off:%dcm"),
_firstVpin, _trigPin, _echoPin, _onThreshold, _offThreshold);
}
};
#endif //IO_HCSR04_H

View File

@@ -1,101 +0,0 @@
/*
* © 2022 Paul M Antoine
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef IO_MCP23008_H
#define IO_MCP23008_H
#include "IO_GPIOBase.h"
class MCP23008 : public GPIOBase<uint8_t> {
public:
static void create(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) {
if (checkNoOverlap(firstVpin, nPins,i2cAddress)) new MCP23008(firstVpin, nPins, i2cAddress, interruptPin);
}
private:
// Constructor
MCP23008(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1)
: GPIOBase<uint8_t>((FSH *)F("MCP23008"), firstVpin, nPins, i2cAddress, interruptPin) {
requestBlock.setRequestParams(_I2CAddress, inputBuffer, sizeof(inputBuffer),
outputBuffer, sizeof(outputBuffer));
outputBuffer[0] = REG_GPIO;
}
void _writeGpioPort() override {
I2CManager.write(_I2CAddress, 2, REG_GPIO, _portOutputState);
}
void _writePullups() override {
// Set pullups only for in-use pins. This prevents pullup being set for a pin that
// is intended for use as an output but hasn't been written to yet.
I2CManager.write(_I2CAddress, 2, REG_GPPU, _portPullup & _portInUse);
}
void _writePortModes() override {
// Write 0 to IODIR for in-use pins that are outputs, 1 for others.
uint8_t temp = ~(_portMode & _portInUse);
I2CManager.write(_I2CAddress, 2, REG_IODIR, temp);
// Enable interrupt-on-change for in-use pins that are inputs (_portMode=0)
temp = ~_portMode & _portInUse;
I2CManager.write(_I2CAddress, 2, REG_INTCON, 0x00);
I2CManager.write(_I2CAddress, 2, REG_GPINTEN, temp);
}
void _readGpioPort(bool immediate) override {
if (immediate) {
uint8_t buffer;
I2CManager.read(_I2CAddress, &buffer, 1, 1, REG_GPIO);
_portInputState = buffer | _portMode;
} else {
// Queue new request
requestBlock.wait(); // Wait for preceding operation to complete
// Issue new request to read GPIO register
I2CManager.queueRequest(&requestBlock);
}
}
// This function is invoked when an I/O operation on the requestBlock completes.
void _processCompletion(uint8_t status) override {
if (status == I2C_STATUS_OK)
_portInputState = inputBuffer[0] | _portMode;
else
_portInputState = 0xff;
}
void _setupDevice() override {
// IOCON is set ODR=1 (open drain shared interrupt pin), INTPOL=0 (active-Low)
I2CManager.write(_I2CAddress, 2, REG_IOCON, 0x04);
_writePortModes();
_writePullups();
_writeGpioPort();
}
uint8_t inputBuffer[1];
uint8_t outputBuffer[1];
enum {
// Register definitions for MCP23008
REG_IODIR=0x00,
REG_GPINTEN=0x02,
REG_INTCON=0x04,
REG_IOCON=0x05,
REG_GPPU=0x06,
REG_GPIO=0x09,
};
};
#endif

View File

@@ -1,111 +0,0 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef io_mcp23017_h
#define io_mcp23017_h
#include "IO_GPIOBase.h"
#include "FSH.h"
/////////////////////////////////////////////////////////////////////////////////////////////////////
/*
* IODevice subclass for MCP23017 16-bit I/O expander.
*/
class MCP23017 : public GPIOBase<uint16_t> {
public:
static void create(VPIN vpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) {
if (checkNoOverlap(vpin, nPins, i2cAddress)) new MCP23017(vpin, nPins, i2cAddress, interruptPin);
}
private:
// Constructor
MCP23017(VPIN vpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1)
: GPIOBase<uint16_t>((FSH *)F("MCP23017"), vpin, nPins, i2cAddress, interruptPin)
{
requestBlock.setRequestParams(_I2CAddress, inputBuffer, sizeof(inputBuffer),
outputBuffer, sizeof(outputBuffer));
outputBuffer[0] = REG_GPIOA;
}
void _writeGpioPort() override {
I2CManager.write(_I2CAddress, 3, REG_GPIOA, _portOutputState, _portOutputState>>8);
}
void _writePullups() override {
// Set pullups only for in-use pins. This prevents pullup being set for a pin that
// is intended for use as an output but hasn't been written to yet.
uint16_t temp = _portPullup & _portInUse;
I2CManager.write(_I2CAddress, 3, REG_GPPUA, temp, temp>>8);
}
void _writePortModes() override {
// Write 0 to IODIR for in-use pins that are outputs, 1 for others.
uint16_t temp = ~(_portMode & _portInUse);
I2CManager.write(_I2CAddress, 3, REG_IODIRA, temp, temp>>8);
// Enable interrupt for in-use pins which are inputs (_portMode=0)
temp = ~_portMode & _portInUse;
I2CManager.write(_I2CAddress, 3, REG_INTCONA, 0x00, 0x00);
I2CManager.write(_I2CAddress, 3, REG_GPINTENA, temp, temp>>8);
}
void _readGpioPort(bool immediate) override {
if (immediate) {
uint8_t buffer[2];
I2CManager.read(_I2CAddress, buffer, 2, 1, REG_GPIOA);
_portInputState = ((uint16_t)buffer[1]<<8) | buffer[0] | _portMode;
} else {
// Queue new request
requestBlock.wait(); // Wait for preceding operation to complete
// Issue new request to read GPIO register
I2CManager.queueRequest(&requestBlock);
}
}
// This function is invoked when an I/O operation on the requestBlock completes.
void _processCompletion(uint8_t status) override {
if (status == I2C_STATUS_OK)
_portInputState = (((uint16_t)inputBuffer[1]<<8) | inputBuffer[0]) | _portMode;
else
_portInputState = 0xffff;
}
void _setupDevice() override {
// IOCON is set MIRROR=1, ODR=1 (open drain shared interrupt pin)
I2CManager.write(_I2CAddress, 2, REG_IOCON, 0x44);
_writePortModes();
_writePullups();
_writeGpioPort();
}
uint8_t inputBuffer[2];
uint8_t outputBuffer[1];
enum {
REG_IODIRA = 0x00,
REG_IODIRB = 0x01,
REG_GPINTENA = 0x04,
REG_GPINTENB = 0x05,
REG_INTCONA = 0x08,
REG_INTCONB = 0x09,
REG_IOCON = 0x0A,
REG_GPPUA = 0x0C,
REG_GPPUB = 0x0D,
REG_GPIOA = 0x12,
REG_GPIOB = 0x13,
};
};
#endif

View File

@@ -1,279 +0,0 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#include "IODevice.h"
#include "I2CManager.h"
#include "DIAG.h"
// REGISTER ADDRESSES
static const byte PCA9685_MODE1=0x00; // Mode Register
static const byte PCA9685_FIRST_SERVO=0x06; /** low byte first servo register ON*/
static const byte PCA9685_PRESCALE=0xFE; /** Prescale register for PWM output frequency */
// MODE1 bits
static const byte MODE1_SLEEP=0x10; /**< Low power mode. Oscillator off */
static const byte MODE1_AI=0x20; /**< Auto-Increment enabled */
static const byte MODE1_RESTART=0x80; /**< Restart enabled */
static const float FREQUENCY_OSCILLATOR=25000000.0; /** Accurate enough for our purposes */
static const uint32_t MAX_I2C_SPEED = 1000000L; // PCA9685 rated up to 1MHz I2C clock speed
// Predeclare helper function
static void writeRegister(byte address, byte reg, byte value);
// Create device driver instance.
void PCA9685::create(VPIN firstVpin, int nPins, I2CAddress i2cAddress, uint16_t frequency) {
if (checkNoOverlap(firstVpin, nPins,i2cAddress)) new PCA9685(firstVpin, nPins, i2cAddress, frequency);
}
// Configure a port on the PCA9685.
bool PCA9685::_configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) {
if (configType != CONFIGURE_SERVO) return false;
if (paramCount != 5) return false;
#ifdef DIAG_IO
DIAG(F("PCA9685 Configure VPIN:%u Apos:%d Ipos:%d Profile:%d Duration:%d state:%d"),
vpin, params[0], params[1], params[2], params[3], params[4]);
#endif
int8_t pin = vpin - _firstVpin;
struct ServoData *s = _servoData[pin];
if (s == NULL) {
_servoData[pin] = (struct ServoData *)calloc(1, sizeof(struct ServoData));
s = _servoData[pin];
if (!s) return false; // Check for failed memory allocation
}
s->activePosition = params[0];
s->inactivePosition = params[1];
s->profile = params[2];
s->duration = params[3];
int state = params[4];
if (state != -1) {
// Position servo to initial state
_writeAnalogue(vpin, state ? s->activePosition : s->inactivePosition, 0, 0);
}
return true;
}
// Constructor
PCA9685::PCA9685(VPIN firstVpin, int nPins, I2CAddress i2cAddress, uint16_t frequency) {
_firstVpin = firstVpin;
_nPins = (nPins > 16) ? 16 : nPins;
_I2CAddress = i2cAddress;
// Calculate prescaler value for PWM clock
if (frequency > 1526) frequency = 1526;
else if (frequency < 24) frequency = 24;
prescaler = FREQUENCY_OSCILLATOR / 4096 / frequency;
// To save RAM, space for servo configuration is not allocated unless a pin is used.
// Initialise the pointers to NULL.
for (int i=0; i<_nPins; i++)
_servoData[i] = NULL;
addDevice(this);
// Initialise structure used for setting pulse rate
requestBlock.setWriteParams(_I2CAddress, outputBuffer, sizeof(outputBuffer));
}
// Device-specific initialisation
void PCA9685::_begin() {
I2CManager.begin();
I2CManager.setClock(1000000); // Nominally able to run up to 1MHz on I2C
// In reality, other devices including the Arduino will limit
// the clock speed to a lower rate.
// Initialise I/O module here.
if (I2CManager.exists(_I2CAddress)) {
writeRegister(_I2CAddress, PCA9685_MODE1, MODE1_SLEEP | MODE1_AI);
writeRegister(_I2CAddress, PCA9685_PRESCALE, prescaler);
writeRegister(_I2CAddress, PCA9685_MODE1, MODE1_AI);
writeRegister(_I2CAddress, PCA9685_MODE1, MODE1_RESTART | MODE1_AI);
// In theory, we should wait 500us before sending any other commands to each device, to allow
// the PWM oscillator to get running. However, we don't do any specific wait, as there's
// plenty of other stuff to do before we will send a command.
#if defined(DIAG_IO)
_display();
#endif
} else
_deviceState = DEVSTATE_FAILED;
}
// Device-specific write function, invoked from IODevice::write().
// For this function, the configured profile is used.
void PCA9685::_write(VPIN vpin, int value) {
#ifdef DIAG_IO
DIAG(F("PCA9685 Write VPIN:%u Value:%d"), vpin, value);
#endif
int pin = vpin - _firstVpin;
if (value) value = 1;
struct ServoData *s = _servoData[pin];
if (s != NULL) {
// Use configured parameters
_writeAnalogue(vpin, value ? s->activePosition : s->inactivePosition, s->profile, s->duration);
} else {
/* simulate digital pin on PWM */
_writeAnalogue(vpin, value ? 4095 : 0, Instant | NoPowerOff, 0);
}
}
// Device-specific writeAnalogue function, invoked from IODevice::writeAnalogue().
// Profile is as follows:
// Bit 7: 0=Set PWM to 0% to power off servo motor when finished
// 1=Keep PWM pulses on (better when using PWM to drive an LED)
// Bits 6-0: 0 Use specified duration (defaults to 0 deciseconds)
// 1 (Fast) Move servo in 0.5 seconds
// 2 (Medium) Move servo in 1.0 seconds
// 3 (Slow) Move servo in 2.0 seconds
// 4 (Bounce) Servo 'bounces' at extremes.
//
void PCA9685::_writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) {
#ifdef DIAG_IO
DIAG(F("PCA9685 WriteAnalogue VPIN:%u Value:%d Profile:%d Duration:%d %S"),
vpin, value, profile, duration, _deviceState == DEVSTATE_FAILED?F("DEVSTATE_FAILED"):F(""));
#endif
if (_deviceState == DEVSTATE_FAILED) return;
int pin = vpin - _firstVpin;
if (value > 4095) value = 4095;
else if (value < 0) value = 0;
struct ServoData *s = _servoData[pin];
if (s == NULL) {
// Servo pin not configured, so configure now using defaults
s = _servoData[pin] = (struct ServoData *) calloc(sizeof(struct ServoData), 1);
if (s == NULL) return; // Check for memory allocation failure
s->activePosition = 4095;
s->inactivePosition = 0;
s->currentPosition = value;
s->profile = Instant | NoPowerOff; // Use instant profile (but not this time)
}
// Animated profile. Initiate the appropriate action.
s->currentProfile = profile;
uint8_t profileValue = profile & ~NoPowerOff; // Mask off 'don't-power-off' bit.
s->numSteps = profileValue==Fast ? 10 : // 0.5 seconds
profileValue==Medium ? 20 : // 1.0 seconds
profileValue==Slow ? 40 : // 2.0 seconds
profileValue==Bounce ? sizeof(_bounceProfile)-1 : // ~ 1.5 seconds
duration * 2 + 1; // Convert from deciseconds (100ms) to refresh cycles (50ms)
s->stepNumber = 0;
s->toPosition = value;
s->fromPosition = s->currentPosition;
}
// _read returns true if the device is currently in executing an animation,
// changing the output over a period of time.
int PCA9685::_read(VPIN vpin) {
if (_deviceState == DEVSTATE_FAILED) return 0;
int pin = vpin - _firstVpin;
struct ServoData *s = _servoData[pin];
if (s == NULL)
return false; // No structure means no animation!
else
return (s->stepNumber < s->numSteps);
}
void PCA9685::_loop(unsigned long currentMicros) {
for (int pin=0; pin<_nPins; pin++) {
updatePosition(pin);
}
delayUntil(currentMicros + refreshInterval * 1000UL);
}
// Private function to reposition servo
// TODO: Could calculate step number from elapsed time, to allow for erratic loop timing.
void PCA9685::updatePosition(uint8_t pin) {
struct ServoData *s = _servoData[pin];
if (s == NULL) return; // No pin configuration/state data
if (s->numSteps == 0) return; // No animation in progress
if (s->stepNumber == 0 && s->fromPosition == s->toPosition) {
// Go straight to end of sequence, output final position.
s->stepNumber = s->numSteps-1;
}
if (s->stepNumber < s->numSteps) {
// Animation in progress, reposition servo
s->stepNumber++;
if ((s->currentProfile & ~NoPowerOff) == Bounce) {
// Retrieve step positions from array in flash
byte profileValue = GETFLASH(&_bounceProfile[s->stepNumber]);
s->currentPosition = map(profileValue, 0, 100, s->fromPosition, s->toPosition);
} else {
// All other profiles - calculate step by linear interpolation between from and to positions.
s->currentPosition = map(s->stepNumber, 0, s->numSteps, s->fromPosition, s->toPosition);
}
// Send servo command
writeDevice(pin, s->currentPosition);
} else if (s->stepNumber < s->numSteps + _catchupSteps) {
// We've finished animation, wait a little to allow servo to catch up
s->stepNumber++;
} else if (s->stepNumber == s->numSteps + _catchupSteps
&& s->currentPosition != 0) {
#ifdef IO_SWITCH_OFF_SERVO
if ((s->currentProfile & NoPowerOff) == 0) {
// Wait has finished, so switch off PWM to prevent annoying servo buzz
writeDevice(pin, 0);
}
#endif
s->numSteps = 0; // Done now.
}
}
// writeDevice takes a pin in range 0 to _nPins-1 within the device, and a value
// between 0 and 4095 for the PWM mark-to-period ratio, with 4095 being 100%.
void PCA9685::writeDevice(uint8_t pin, int value) {
#ifdef DIAG_IO
DIAG(F("PCA9685 I2C:%s WriteDevice Pin:%d Value:%d"), _I2CAddress.toString(), pin, value);
#endif
// Wait for previous request to complete
uint8_t status = requestBlock.wait();
if (status != I2C_STATUS_OK) {
_deviceState = DEVSTATE_FAILED;
DIAG(F("PCA9685 I2C:%s failed %S"), _I2CAddress.toString(), I2CManager.getErrorMessage(status));
} else {
// Set up new request.
outputBuffer[0] = PCA9685_FIRST_SERVO + 4 * pin;
outputBuffer[1] = 0;
outputBuffer[2] = (value == 4095 ? 0x10 : 0); // 4095=full on
outputBuffer[3] = value & 0xff;
outputBuffer[4] = value >> 8;
I2CManager.queueRequest(&requestBlock);
}
}
// Display details of this device.
void PCA9685::_display() {
DIAG(F("PCA9685 I2C:%s Configured on Vpins:%u-%u %S"), _I2CAddress.toString(), (int)_firstVpin,
(int)_firstVpin+_nPins-1, (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F(""));
}
// Internal helper function for this device
static void writeRegister(byte address, byte reg, byte value) {
I2CManager.write(address, 2, reg, value);
}
// Profile for a bouncing signal or turnout
// The profile below is in the range 0-100% and should be combined with the desired limits
// of the servo set by _activePosition and _inactivePosition. The profile is symmetrical here,
// i.e. the bounce is the same on the down action as on the up action. First entry isn't used.
const uint8_t FLASH PCA9685::_bounceProfile[30] =
{0,2,3,7,13,33,50,83,100,83,75,70,65,60,60,65,74,84,100,83,75,70,70,72,75,80,87,92,97,100};

View File

@@ -1,170 +0,0 @@
/*
* © 2023, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* This driver performs the basic interface between the HAL and an
* I2C-connected PCA9685 16-channel PWM module. When requested, it
* commands the device to set the PWM mark-to-period ratio accordingly.
* The call to IODevice::writeAnalogue(vpin, value) specifies the
* desired value in the range 0-4095 (0=0% and 4095=100%).
*
* This driver can be used for simple servo control by writing values between
* about 102 and 450 (extremes of movement for 9g micro servos) or 150 to 250
* for a more restricted range (corresponding to 1.5ms to 2.5ms pulse length).
* A value of zero will switch off the servo. To create the device, use
* the following syntax:
*
* PCA9685_basic::create(vpin, npins, i2caddress);
*
* For LED control, a value of 0 is fully off, and 4095 is fully on. It is
* recommended, to reduce flicker of LEDs, that the frequency be configured
* to a value higher than the default of 50Hz. To do this, create the device
* as follows, for a frequency of 200Hz.:
*
* PCA9685_basic::create(vpin, npins, i2caddress, 200);
*
*/
#ifndef PCA9685_BASIC_H
#define PCA9685_BASIC_H
#include "IODevice.h"
#include "I2CManager.h"
#include "DIAG.h"
/*
* IODevice subclass for PCA9685 16-channel PWM module.
*/
class PCA9685pwm : public IODevice {
public:
// Create device driver instance.
static void create(VPIN firstVpin, int nPins, I2CAddress i2cAddress, uint16_t frequency = 50) {
if (checkNoOverlap(firstVpin, nPins, i2cAddress)) new PCA9685pwm(firstVpin, nPins, i2cAddress, frequency);
}
private:
// structures for setting up non-blocking writes to PWM controller
I2CRB requestBlock;
uint8_t outputBuffer[5];
uint16_t prescaler;
// REGISTER ADDRESSES
const uint8_t PCA9685_MODE1=0x00; // Mode Register
const uint8_t PCA9685_FIRST_SERVO=0x06; /** low uint8_t first PWM register ON*/
const uint8_t PCA9685_PRESCALE=0xFE; /** Prescale register for PWM output frequency */
// MODE1 bits
const uint8_t MODE1_SLEEP=0x10; /**< Low power mode. Oscillator off */
const uint8_t MODE1_AI=0x20; /**< Auto-Increment enabled */
const uint8_t MODE1_RESTART=0x80; /**< Restart enabled */
const uint32_t FREQUENCY_OSCILLATOR=25000000; /** Accurate enough for our purposes */
const uint8_t PRESCALE_50HZ = (uint8_t)(((FREQUENCY_OSCILLATOR / (50.0 * 4096.0)) + 0.5) - 1);
const uint32_t MAX_I2C_SPEED = 1000000L; // PCA9685 rated up to 1MHz I2C clock speed
// Constructor
PCA9685pwm(VPIN firstVpin, int nPins, I2CAddress i2cAddress, uint16_t frequency) {
_firstVpin = firstVpin;
_nPins = (nPins>16) ? 16 : nPins;
_I2CAddress = i2cAddress;
if (frequency > 1526) frequency = 1526;
else if (frequency < 24) frequency = 24;
prescaler = FREQUENCY_OSCILLATOR / 4096 / frequency;
addDevice(this);
// Initialise structure used for setting pulse rate
requestBlock.setWriteParams(_I2CAddress, outputBuffer, sizeof(outputBuffer));
}
// Device-specific initialisation
void _begin() override {
I2CManager.begin();
I2CManager.setClock(1000000); // Nominally able to run up to 1MHz on I2C
// In reality, other devices including the Arduino will limit
// the clock speed to a lower rate.
// Initialise I/O module here.
if (I2CManager.exists(_I2CAddress)) {
writeRegister(_I2CAddress, PCA9685_MODE1, MODE1_SLEEP | MODE1_AI);
writeRegister(_I2CAddress, PCA9685_PRESCALE, prescaler);
writeRegister(_I2CAddress, PCA9685_MODE1, MODE1_AI);
writeRegister(_I2CAddress, PCA9685_MODE1, MODE1_RESTART | MODE1_AI);
// In theory, we should wait 500us before sending any other commands to each device, to allow
// the PWM oscillator to get running. However, we don't do any specific wait, as there's
// plenty of other stuff to do before we will send a command.
#if defined(DIAG_IO)
_display();
#endif
} else
_deviceState = DEVSTATE_FAILED;
}
// Device-specific writeAnalogue function, invoked from IODevice::writeAnalogue().
//
void _writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) override {
(void)param1; (void)param2; // suppress compiler warning
#ifdef DIAG_IO
DIAG(F("PCA9685pwm WriteAnalogue VPIN:%u Value:%d %S"),
vpin, value, _deviceState == DEVSTATE_FAILED?F("DEVSTATE_FAILED"):F(""));
#endif
if (_deviceState == DEVSTATE_FAILED) return;
int pin = vpin - _firstVpin;
if (value > 4095) value = 4095;
else if (value < 0) value = 0;
writeDevice(pin, value);
}
// Display details of this device.
void _display() override {
DIAG(F("PCA9685pwm I2C:%s Configured on Vpins:%u-%u %S"), _I2CAddress.toString(), (int)_firstVpin,
(int)_firstVpin+_nPins-1, (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F(""));
}
// writeDevice (helper function) takes a pin in range 0 to _nPins-1 within the device, and a value
// between 0 and 4095 for the PWM mark-to-period ratio, with 4095 being 100%.
void writeDevice(uint8_t pin, int value) {
#ifdef DIAG_IO
DIAG(F("PCA9685pwm I2C:%s WriteDevice Pin:%d Value:%d"), _I2CAddress.toString(), pin, value);
#endif
// Wait for previous request to complete
uint8_t status = requestBlock.wait();
if (status != I2C_STATUS_OK) {
_deviceState = DEVSTATE_FAILED;
DIAG(F("PCA9685pwm I2C:%s failed %S"), _I2CAddress.toString(), I2CManager.getErrorMessage(status));
} else {
// Set up new request.
outputBuffer[0] = PCA9685_FIRST_SERVO + 4 * pin;
outputBuffer[1] = 0;
outputBuffer[2] = (value == 4095 ? 0x10 : 0); // 4095=full on
outputBuffer[3] = value & 0xff;
outputBuffer[4] = value >> 8;
I2CManager.queueRequest(&requestBlock);
}
}
// Internal helper function for this device
static void writeRegister(I2CAddress address, uint8_t reg, uint8_t value) {
I2CManager.write(address, 2, reg, value);
}
};
#endif

View File

@@ -1,104 +0,0 @@
/*
* © 2022 Paul M Antoine
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* The PCF8574 is a simple device; it only has one register. The device
* input/output mode and pullup are configured through this, and the
* output state is written and the input state read through it too.
*
* This is accomplished by having a weak resistor in series with the output,
* and a read-back of the other end of the resistor. As an output, the
* pin state is set to 1 or 0, and the output voltage goes to +5V or 0V
* (through the weak resistor).
*
* In order to use the pin as an input, the output is written as
* a '1' in order to pull up the resistor. Therefore the input will be
* 1 unless the pin is pulled down externally, in which case it will be 0.
*
* As a consequence of this approach, it is not possible to use the device for
* inputs without pullups.
*/
#ifndef IO_PCF8574_H
#define IO_PCF8574_H
#include "IO_GPIOBase.h"
class PCF8574 : public GPIOBase<uint8_t> {
public:
static void create(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) {
if (checkNoOverlap(firstVpin, nPins, i2cAddress)) new PCF8574(firstVpin, nPins, i2cAddress, interruptPin);
}
private:
PCF8574(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1)
: GPIOBase<uint8_t>((FSH *)F("PCF8574"), firstVpin, nPins, i2cAddress, interruptPin)
{
requestBlock.setReadParams(_I2CAddress, inputBuffer, 1);
}
// The PCF8574 handles inputs by applying a weak pull-up when output is driven to '1'.
// The pin state is driven '1' if the pin is an input, or if it is an output set to 1.
// Unused pins are driven '0'.
void _writeGpioPort() override {
I2CManager.write(_I2CAddress, 1, (_portOutputState | ~_portMode) & _portInUse);
}
// The PCF8574 handles inputs by applying a weak pull-up when output is driven to '1'.
// Therefore, writing '1' in _writePortModes is enough to set the module to input mode
// and enable pull-up.
void _writePullups() override { }
void _writePortModes() override {
_writeGpioPort();
}
// In immediate mode, _readGpioPort reads the device GPIO port and updates _portInputState accordingly.
// When not in immediate mode, it initiates a request using the request block and returns.
// When the request completes, _processCompletion finishes the operation.
void _readGpioPort(bool immediate) override {
if (immediate) {
uint8_t buffer[1];
I2CManager.read(_I2CAddress, buffer, 1);
_portInputState = buffer[0] | _portMode;
} else {
requestBlock.wait(); // Wait for preceding operation to complete
// Issue new request to read GPIO register
I2CManager.queueRequest(&requestBlock);
}
}
// This function is invoked when an I/O operation on the requestBlock completes.
void _processCompletion(uint8_t status) override {
if (status == I2C_STATUS_OK)
_portInputState = inputBuffer[0] | _portMode;
else
_portInputState = 0xff;
}
// Set up device ports
void _setupDevice() override {
_writePortModes();
}
uint8_t inputBuffer[1];
};
#endif

View File

@@ -1,109 +0,0 @@
/*
* © 2023, Paul Antoine, and Discord user @ADUBOURG
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* The PCF8575 is a simple device; it only has one register. The device
* input/output mode and pullup are configured through this, and the
* output state is written and the input state read through it too.
*
* This is accomplished by having a weak resistor in series with the output,
* and a read-back of the other end of the resistor. As an output, the
* pin state is set to 1 or 0, and the output voltage goes to +5V or 0V
* (through the weak resistor).
*
* In order to use the pin as an input, the output is written as
* a '1' in order to pull up the resistor. Therefore the input will be
* 1 unless the pin is pulled down externally, in which case it will be 0.
*
* As a consequence of this approach, it is not possible to use the device for
* inputs without pullups.
*/
#ifndef IO_PCF8575_H
#define IO_PCF8575_H
#include "IO_GPIOBase.h"
#include "FSH.h"
class PCF8575 : public GPIOBase<uint16_t> {
public:
static void create(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) {
if (checkNoOverlap(firstVpin, nPins, i2cAddress)) new PCF8575(firstVpin, nPins, i2cAddress, interruptPin);
}
private:
PCF8575(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1)
: GPIOBase<uint16_t>((FSH *)F("PCF8575"), firstVpin, nPins, i2cAddress, interruptPin)
{
requestBlock.setReadParams(_I2CAddress, inputBuffer, sizeof(inputBuffer));
}
// The PCF8575 handles inputs by applying a weak pull-up when output is driven to '1'.
// The pin state is driven '1' if the pin is an input, or if it is an output set to 1.
// Unused pins are driven '0'.
void _writeGpioPort() override {
uint16_t bits = (_portOutputState | ~_portMode) & _portInUse;
I2CManager.write(_I2CAddress, 2, bits, bits>>8);
}
// The PCF8575 handles inputs by applying a weak pull-up when output is driven to '1'.
// Therefore, writing '1' in _writePortModes is enough to set the module to input mode
// and enable pull-up.
void _writePullups() override { }
// The pin state is '1' if the pin is an input or if it is an output set to 1. Zero otherwise.
void _writePortModes() override {
_writeGpioPort();
}
// In immediate mode, _readGpioPort reads the device GPIO port and updates _portInputState accordingly.
// When not in immediate mode, it initiates a request using the request block and returns.
// When the request completes, _processCompletion finishes the operation.
void _readGpioPort(bool immediate) override {
if (immediate) {
uint8_t buffer[2];
I2CManager.read(_I2CAddress, buffer, 2);
_portInputState = (((uint16_t)buffer[1]<<8) | buffer[0]) | _portMode;
} else {
requestBlock.wait(); // Wait for preceding operation to complete
// Issue new request to read GPIO register
I2CManager.queueRequest(&requestBlock);
}
}
// This function is invoked when an I/O operation on the requestBlock completes.
void _processCompletion(uint8_t status) override {
if (status == I2C_STATUS_OK)
_portInputState = (((uint16_t)inputBuffer[1]<<8) | inputBuffer[0]) | _portMode;
else
_portInputState = 0xffff;
}
// Set up device ports
void _setupDevice() override {
_writePortModes();
_writeGpioPort();
_writePullups();
}
uint8_t inputBuffer[2];
};
#endif

View File

@@ -1,127 +0,0 @@
/*
* © 2022, Peter Cole. All rights reserved.
*
* This file is part of EX-CommandStation
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* The IO_RotaryEncoder device driver is used to receive positions from a rotary encoder connected to an Arduino via I2C.
*
* There is separate code required for the Arduino the rotary encoder is connected to, which is located here:
* https://github.com/peteGSX-Projects/dcc-ex-rotary-encoder
*
* This device driver receives the rotary encoder position when the rotary encoder button is pushed, and these positions
* can be tested in EX-RAIL with:
* ONCHANGE(vpin) - flag when the rotary encoder position has changed from the previous position
* IFRE(vpin, position) - test to see if specified rotary encoder position has been received
*
* Further to this, feedback can be sent to the rotary encoder by using 2 Vpins, and sending a SET()/RESET() to the second Vpin.
* A SET(vpin) will flag that a turntable (or anything else) is in motion, and a RESET(vpin) that the motion has finished.
*
* Refer to the documentation for further information including the valid activities and examples.
*/
#ifndef IO_ROTARYENCODER_H
#define IO_ROTARYENCODER_H
#include "EXRAIL2.h"
#include "IODevice.h"
#include "I2CManager.h"
#include "DIAG.h"
class RotaryEncoder : public IODevice {
public:
// Constructor
RotaryEncoder(VPIN firstVpin, int nPins, I2CAddress i2cAddress){
_firstVpin = firstVpin;
_nPins = nPins;
_I2CAddress = i2cAddress;
addDevice(this);
}
static void create(VPIN firstVpin, int nPins, I2CAddress i2cAddress) {
if (checkNoOverlap(firstVpin, nPins, i2cAddress)) new RotaryEncoder(firstVpin, nPins, i2cAddress);
}
private:
// Initiate the device
void _begin() {
I2CManager.begin();
if (I2CManager.exists(_I2CAddress)) {
byte _getVersion[1] = {RE_VER};
I2CManager.read(_I2CAddress, _versionBuffer, 3, _getVersion, 1);
_majorVer = _versionBuffer[0];
_minorVer = _versionBuffer[1];
_patchVer = _versionBuffer[2];
_buffer[0] = RE_OP;
I2CManager.write(_I2CAddress, _buffer, 1);
#ifdef DIAG_IO
_display();
#endif
} else {
_deviceState = DEVSTATE_FAILED;
}
}
void _loop(unsigned long currentMicros) override {
I2CManager.read(_I2CAddress, _buffer, 1);
_position = _buffer[0];
// This here needs to have a change check, ie. position is a different value.
#if defined(EXRAIL_ACTIVE)
if (_position != _previousPosition) {
_previousPosition = _position;
RMFT2::changeEvent(_firstVpin,1);
} else {
RMFT2::changeEvent(_firstVpin,0);
}
#endif
delayUntil(currentMicros + 100000);
}
// Device specific read function
int _readAnalogue(VPIN vpin) override {
if (_deviceState == DEVSTATE_FAILED) return 0;
return _position;
}
void _write(VPIN vpin, int value) override {
if (vpin == _firstVpin + 1) {
if (value != 0) value = 0x01;
byte _feedbackBuffer[2] = {RE_OP, (byte)value};
I2CManager.write(_I2CAddress, _feedbackBuffer, 2);
}
}
void _display() override {
DIAG(F("Rotary Encoder I2C:%s v%d.%d.%d Configured on VPIN:%u-%d %S"), _I2CAddress.toString(), _majorVer, _minorVer, _patchVer,
(int)_firstVpin, _firstVpin+_nPins-1, (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F(""));
}
int8_t _position;
int8_t _previousPosition = 0;
uint8_t _versionBuffer[3];
uint8_t _buffer[1];
uint8_t _majorVer = 0;
uint8_t _minorVer = 0;
uint8_t _patchVer = 0;
enum {
RE_VER = 0xA0, // Flag to retrieve rotary encoder version from the device
RE_OP = 0xA1, // Flag for normal operation
};
};
#endif

View File

@@ -1,33 +0,0 @@
/*
* © 2023, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#include "IO_Servo.h"
#include "FSH.h"
// Profile for a bouncing signal or turnout
// The profile below is in the range 0-100% and should be combined with the desired limits
// of the servo set by _activePosition and _inactivePosition. The profile is symmetrical here,
// i.e. the bounce is the same on the down action as on the up action. First entry isn't used.
//
// Note: This has been put into its own .CPP file to ensure that duplicates aren't created
// if the IO_Servo.h library is #include'd in multiple source files.
//
const uint8_t FLASH Servo::_bounceProfile[30] =
{0,2,3,7,13,33,50,83,100,83,75,70,65,60,60,65,74,84,100,83,75,70,70,72,75,80,87,92,97,100};

View File

@@ -1,298 +0,0 @@
/*
* © 2023, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* This device is a layered device which is designed to sit on top of another
* device. The underlying device class is expected to accept writeAnalogue calls
* which will normally cause some physical movement of something. The device may be a servo,
* a motor or some other kind of positioner, and the something might be a turnout,
* a semaphore signal or something else. One user has used this capability for
* moving a figure along the platform on their layout!
*
* Example of use:
* In myHal.cpp,
*
* #include "IO_Servo.h"
* ...
* PCA9685::create(100,16,0x40); // First create the hardware interface device
* Servo::create(300,16,100); // Then create the higher level device which
* // references pins 100-115 or a subset of them.
*
* Then any reference to pins 300-315 will cause the servo driver to send output
* PWM commands to the corresponding PCA9685 driver pins 100-115. The PCA9685 driver may
* be substituted with any other driver which provides analogue output
* capability, e.g. EX-IOExpander devices, as long as they are capable of interpreting
* the writeAnalogue() function calls.
*/
#include "IODevice.h"
#ifndef IO_SERVO_H
#define IO_SERVO_H
#include "I2CManager.h"
#include "DIAG.h"
class Servo : IODevice {
public:
enum ProfileType : uint8_t {
Instant = 0, // Moves immediately between positions (if duration not specified)
UseDuration = 0, // Use specified duration
Fast = 1, // Takes around 500ms end-to-end
Medium = 2, // 1 second end-to-end
Slow = 3, // 2 seconds end-to-end
Bounce = 4, // For semaphores/turnouts with a bit of bounce!!
NoPowerOff = 0x80, // Flag to be ORed in to suppress power off after move.
};
// Create device driver instance.
static void create(VPIN firstVpin, int nPins, VPIN firstSlavePin=VPIN_NONE) {
new Servo(firstVpin, nPins, firstSlavePin);
}
private:
VPIN _firstSlavePin;
IODevice *_slaveDevice = NULL;
struct ServoData {
uint16_t activePosition : 12; // Config parameter
uint16_t inactivePosition : 12; // Config parameter
uint16_t currentPosition : 12;
uint16_t fromPosition : 12;
uint16_t toPosition : 12;
uint8_t profile; // Config parameter
uint16_t stepNumber; // Index of current step (starting from 0)
uint16_t numSteps; // Number of steps in animation, or 0 if none in progress.
uint8_t currentProfile; // profile being used for current animation.
uint16_t duration; // time (tenths of a second) for animation to complete.
}; // 14 bytes per element, i.e. per pin in use
struct ServoData *_servoData [16];
static const uint8_t _catchupSteps = 5; // number of steps to wait before switching servo off
static const uint8_t FLASH _bounceProfile[30];
const unsigned int refreshInterval = 50; // refresh every 50ms
// Configure a port on the Servo.
bool _configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) {
if (_deviceState == DEVSTATE_FAILED) return false;
if (configType != CONFIGURE_SERVO) return false;
if (paramCount != 5) return false;
#ifdef DIAG_IO
DIAG(F("Servo: Configure VPIN:%u Apos:%d Ipos:%d Profile:%d Duration:%d state:%d"),
vpin, params[0], params[1], params[2], params[3], params[4]);
#endif
int8_t pin = vpin - _firstVpin;
struct ServoData *s = _servoData[pin];
if (s == NULL) {
_servoData[pin] = (struct ServoData *)calloc(1, sizeof(struct ServoData));
s = _servoData[pin];
if (!s) return false; // Check for failed memory allocation
}
s->activePosition = params[0];
s->inactivePosition = params[1];
s->profile = params[2];
s->duration = params[3];
int state = params[4];
if (state != -1) {
// Position servo to initial state
writeAnalogue(vpin, state ? s->activePosition : s->inactivePosition);
}
return true;
}
// Constructor
Servo(VPIN firstVpin, int nPins, VPIN firstSlavePin = VPIN_NONE) {
_firstVpin = firstVpin;
_nPins = (nPins > 16) ? 16 : nPins;
if (firstSlavePin == VPIN_NONE)
_firstSlavePin = firstVpin;
else
_firstSlavePin = firstSlavePin;
// To save RAM, space for servo configuration is not allocated unless a pin is used.
// Initialise the pointers to NULL.
for (int i=0; i<_nPins; i++)
_servoData[i] = NULL;
// Get reference to slave device.
_slaveDevice = findDevice(_firstSlavePin);
if (!_slaveDevice) {
DIAG(F("Servo: Slave device not found on Vpins %u-%u"),
_firstSlavePin, _firstSlavePin+_nPins-1);
_deviceState = DEVSTATE_FAILED;
}
if (_slaveDevice != findDevice(_firstSlavePin+_nPins-1)) {
DIAG(F("Servo: Slave device does not cover all Vpins %u-%u"),
_firstSlavePin, _firstSlavePin+_nPins-1);
_deviceState = DEVSTATE_FAILED;
}
addDevice(this, _slaveDevice); // Link device ahead of slave device to intercept requests
}
// Device-specific initialisation
void _begin() override {
#if defined(DIAG_IO)
_display();
#endif
}
// Device-specific write function, invoked from IODevice::write().
// For this function, the configured profile is used.
void _write(VPIN vpin, int value) override {
if (_deviceState == DEVSTATE_FAILED) return;
#ifdef DIAG_IO
DIAG(F("Servo Write VPIN:%u Value:%d"), vpin, value);
#endif
int pin = vpin - _firstVpin;
if (value) value = 1;
struct ServoData *s = _servoData[pin];
if (s != NULL) {
// Use configured parameters
writeAnalogue(vpin, value ? s->activePosition : s->inactivePosition, s->profile, s->duration);
} else {
/* simulate digital pin on PWM */
writeAnalogue(vpin, value ? 4095 : 0, Instant | NoPowerOff, 0);
}
}
// Device-specific writeAnalogue function, invoked from IODevice::writeAnalogue().
// Profile is as follows:
// Bit 7: 0=Set output to 0% to power off servo motor when finished
// 1=Keep output at final position (better with LEDs, which will stay lit)
// Bits 6-0: 0 Use specified duration (defaults to 0 deciseconds)
// 1 (Fast) Move servo in 0.5 seconds
// 2 (Medium) Move servo in 1.0 seconds
// 3 (Slow) Move servo in 2.0 seconds
// 4 (Bounce) Servo 'bounces' at extremes.
// Duration is in deciseconds (tenths of a second) and defaults to 0.
//
void _writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) override {
#ifdef DIAG_IO
DIAG(F("Servo: WriteAnalogue VPIN:%u Value:%d Profile:%d Duration:%d %S"),
vpin, value, profile, duration, _deviceState == DEVSTATE_FAILED?F("DEVSTATE_FAILED"):F(""));
#endif
if (_deviceState == DEVSTATE_FAILED) return;
int pin = vpin - _firstVpin;
if (value > 4095) value = 4095;
else if (value < 0) value = 0;
struct ServoData *s = _servoData[pin];
if (s == NULL) {
// Servo pin not configured, so configure now using defaults
s = _servoData[pin] = (struct ServoData *) calloc(sizeof(struct ServoData), 1);
if (s == NULL) return; // Check for memory allocation failure
s->activePosition = 4095;
s->inactivePosition = 0;
s->currentPosition = value;
s->profile = Instant | NoPowerOff; // Use instant profile (but not this time)
}
// Animated profile. Initiate the appropriate action.
s->currentProfile = profile;
uint8_t profileValue = profile & ~NoPowerOff; // Mask off 'don't-power-off' bit.
s->numSteps = profileValue==Fast ? 10 : // 0.5 seconds
profileValue==Medium ? 20 : // 1.0 seconds
profileValue==Slow ? 40 : // 2.0 seconds
profileValue==Bounce ? sizeof(_bounceProfile)-1 : // ~ 1.5 seconds
duration * 2 + 1; // Convert from deciseconds (100ms) to refresh cycles (50ms)
s->stepNumber = 0;
s->toPosition = value;
s->fromPosition = s->currentPosition;
}
// _read returns true if the device is currently in executing an animation,
// changing the output over a period of time.
int _read(VPIN vpin) override {
if (_deviceState == DEVSTATE_FAILED) return 0;
int pin = vpin - _firstVpin;
struct ServoData *s = _servoData[pin];
if (s == NULL)
return false; // No structure means no animation!
else
return (s->stepNumber < s->numSteps);
}
void _loop(unsigned long currentMicros) override {
if (_deviceState == DEVSTATE_FAILED) return;
for (int pin=0; pin<_nPins; pin++) {
updatePosition(pin);
}
delayUntil(currentMicros + refreshInterval * 1000UL);
}
// Private function to reposition servo
// TODO: Could calculate step number from elapsed time, to allow for erratic loop timing.
void updatePosition(uint8_t pin) {
struct ServoData *s = _servoData[pin];
if (s == NULL) return; // No pin configuration/state data
if (s->numSteps == 0) return; // No animation in progress
if (s->stepNumber == 0 && s->fromPosition == s->toPosition) {
// Go straight to end of sequence, output final position.
s->stepNumber = s->numSteps-1;
}
if (s->stepNumber < s->numSteps) {
// Animation in progress, reposition servo
s->stepNumber++;
if ((s->currentProfile & ~NoPowerOff) == Bounce) {
// Retrieve step positions from array in flash
uint8_t profileValue = GETFLASH(&_bounceProfile[s->stepNumber]);
s->currentPosition = map(profileValue, 0, 100, s->fromPosition, s->toPosition);
} else {
// All other profiles - calculate step by linear interpolation between from and to positions.
s->currentPosition = map(s->stepNumber, 0, s->numSteps, s->fromPosition, s->toPosition);
}
// Send servo command to output driver
_slaveDevice->_writeAnalogue(_firstSlavePin+pin, s->currentPosition);
} else if (s->stepNumber < s->numSteps + _catchupSteps) {
// We've finished animation, wait a little to allow servo to catch up
s->stepNumber++;
} else if (s->stepNumber == s->numSteps + _catchupSteps
&& s->currentPosition != 0) {
#ifdef IO_SWITCH_OFF_SERVO
if ((s->currentProfile & NoPowerOff) == 0) {
// Wait has finished, so switch off output driver to avoid servo buzz.
_slaveDevice->_writeAnalogue(_firstSlavePin+pin, 0);
}
#endif
s->numSteps = 0; // Done now.
}
}
// Display details of this device.
void _display() override {
DIAG(F("Servo Configured on Vpins:%u-%u, slave pins:%d-%d %S"),
(int)_firstVpin, (int)_firstVpin+_nPins-1,
(int)_firstSlavePin, (int)_firstSlavePin+_nPins-1,
(_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F(""));
}
};
#endif

View File

@@ -1,134 +0,0 @@
/*
* © 2023, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* Driver for capacitative touch-pad based on the TTP229-B chip with serial
* (not I2C) output. The touchpad has 16 separate pads in a 4x4 matrix,
* numbered 1-16. The communications with the pad are via a clock signal sent
* from the controller to the device, and a data signal sent back by the device.
* The pins clockPin and dataPin must be local pins, not external (GPIO Expander)
* pins.
*
* To use,
* TouchKeypad::create(firstVpin, 16, clockPin, dataPin);
*
* NOTE: Most of these keypads ship with only 8 pads enabled. To enable all
* sixteen pads, locate the area of the board labelled P1 (four pairs of
* holes labelled 1 to 4 from the left); solder a jumper link between the pair
* labelled 3 (connected to pin TP2 on the chip). When this link is connected,
* the pins OUT1 to OUT8 are not used but all sixteen touch pads are operational.
*
* TODO: Allow a list of datapins to be provided so that multiple keypads can
* be read simultaneously by the one device driver and the one shared clock signal.
* As it stands, we can configure multiple driver instances, one for each keypad,
* and it will work fine. The clock will be driven to all devices but only one
* driver will be reading the responses from its corresponding device at a time.
*/
#ifndef IO_TOUCHKEYPAD_H
#define IO_TOUCHKEYPAD_H
#include "IODevice.h"
class TouchKeypad : public IODevice {
private:
// Here we define the device-specific variables.
uint16_t _inputStates = 0;
VPIN _clockPin;
VPIN _dataPin;
public:
// Static function to handle create calls.
static void create(VPIN firstVpin, int nPins, VPIN clockPin, VPIN dataPin) {
if (checkNoOverlap(firstVpin,nPins)) new TouchKeypad(firstVpin, nPins, clockPin, dataPin);
}
protected:
// Constructor.
TouchKeypad(VPIN firstVpin, int nPins, VPIN clockPin, VPIN dataPin) {
_firstVpin = firstVpin;
_nPins = (nPins > 16) ? 16 : nPins; // Maximum of 16 pads per device
_clockPin = clockPin;
_dataPin = dataPin;
addDevice(this);
}
// Device-specific initialisation
void _begin() override {
#if defined(DIAG_IO)
_display();
#endif
// Set clock pin as output, initially high, and data pin as input.
// Enable pullup on the input so that the default (not connected) state is
// 'keypad not pressed'.
ArduinoPins::fastWriteDigital(_clockPin, 1);
pinMode(_clockPin, OUTPUT);
pinMode(_dataPin, INPUT_PULLUP); // Force defined state when no connection
}
// Device-specific read function.
int _read(VPIN vpin) {
if (vpin < _firstVpin || vpin >= _firstVpin + _nPins) return 0;
// Return a value for the specified vpin.
return _inputStates & (1<<(vpin-_firstVpin)) ? 1 : 0;
}
// Loop function to do background scanning of the keyboard.
// The TTP229 device requires clock pulses to be sent to it,
// and the data bits can be read on the rising edge of the clock.
// By default the clock and data are inverted (active-low).
// A gap of more than 2ms is advised between successive read
// cycles, we wait for 100ms between reads of the keyboard as this
// provide a good enough response time.
// Maximum clock frequency is 512kHz, so put a 1us delay
// between clock transitions.
//
void _loop(unsigned long currentMicros) {
// Clock 16 bits from the device
uint16_t data = 0, maskBit = 0x01;
for (uint8_t pad=0; pad<16; pad++) {
ArduinoPins::fastWriteDigital(_clockPin, 0);
delayMicroseconds(1);
ArduinoPins::fastWriteDigital(_clockPin, 1);
data |= (ArduinoPins::fastReadDigital(_dataPin) ? 0 : maskBit);
maskBit <<= 1;
delayMicroseconds(1);
}
_inputStates = data;
#ifdef DIAG_IO
static uint16_t lastData = 0;
if (data != lastData) DIAG(F("KeyPad: %x"), data);
lastData = data;
#endif
delayUntil(currentMicros + 100000); // read every 100ms
}
// Display information about the device, and perhaps its current condition (e.g. active, disabled etc).
void _display() {
DIAG(F("TouchKeypad Configured on Vpins:%u-%u SCL=%d SDO=%d"), (int)_firstVpin,
(int)_firstVpin+_nPins-1, _clockPin, _dataPin);
}
};
#endif // IO_TOUCHKEYPAD_H

View File

@@ -1,348 +0,0 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* The VL53L0X Time-Of-Flight sensor operates by sending a short laser pulse and detecting
* the reflection of the pulse. The time between the pulse and the receipt of reflections
* is measured and used to determine the distance to the reflecting object.
*
* For economy of memory and processing time, this driver includes only part of the code
* that ST provide in their API. Also, the API code isn't very clear and it is not easy
* to identify what operations are useful and what are not.
* The operation shown here doesn't include any calibration, so is probably not as accurate
* as using the full driver, but it's probably accurate enough for the purpose.
*
* The device driver allocates up to 3 vpins to the device. A digital read on the first pin
* will return a value that indicates whether the object is within the threshold range (1)
* or not (0). An analogue read on the first pin returns the last measured distance (in mm),
* the second pin returns the signal strength, and the third pin returns detected
* ambient light level. By default the device takes around 60ms to complete a ranging
* operation, so we do a 100ms cycle (10 samples per second).
*
* The VL53L0X is initially set to respond to I2C address 0x29. If you only have one module,
* you can use this address. However, the address can be modified by software. If
* you select another address, that address will be written to the device and used until the device is reset.
*
* If you have more than one module, then you will need to specify a digital VPIN (Arduino
* digital output or I/O extender pin) which you connect to the module's XSHUT pin. Now,
* when the device driver starts, the XSHUT pin is set LOW to turn the module off. Once
* all VL53L0X modules are turned off, the driver works through each module in turn,
* setting XSHUT to HIGH to turn that module on, then writing that module's desired I2C address.
* In this way, many VL53L0X modules can be connected to the one I2C bus, each one
* using a distinct I2C address. The process is described in ST Microelectronics application
* note AN4846.
*
* WARNING: If the device's XSHUT pin is not connected, then it may be prone to noise,
* and the device may reset spontaneously or when handled and the device will stop responding
* on its allocated address. If you're not using XSHUT, then tie it to +5V via a resistor
* (should be tied to +2.8V strictly). Some manufacturers (Adafruit and Polulu for example)
* include a pull-up on the module, but others don't.
*
* The driver is configured as follows:
*
* Single VL53L0X module:
* VL53L0X::create(firstVpin, nPins, i2cAddress, lowThreshold, highThreshold);
* Where firstVpin is the first vpin reserved for reading the device,
* nPins is 1, 2 or 3,
* i2cAddress is the address of the device (normally 0x29),
* lowThreshold is the distance at which the digital vpin state is set to 1 (in mm),
* and highThreshold is the distance at which the digital vpin state is set to 0 (in mm).
*
* Multiple VL53L0X modules:
* VL53L0X::create(firstVpin, nPins, i2cAddress, lowThreshold, highThreshold, xshutPin);
* ...
* Where firstVpin is the first vpin reserved for reading the device,
* nPins is 1, 2 or 3,
* i2cAddress is the address of the device (any valid address except 0x29),
* lowThreshold is the distance at which the digital vpin state is set to 1 (in mm),
* highThreshold is the distance at which the digital vpin state is set to 0 (in mm),
* and xshutPin is the VPIN number corresponding to a digital output that is connected to the
* XSHUT terminal on the module. The digital output may be an Arduino pin or an
* I/O extender pin.
*
* Example:
* In mySetup function within mySetup.cpp:
* VL53L0X::create(4000, 3, 0x29, 200, 250);
* Sensor::create(4000, 4000, 0); // Create a sensor
*
* When an object comes within 200mm of the sensor, a message
* <Q 4000>
* will be sent over the serial USB, and when the object moves more than 250mm from the sensor,
* a message
* <q 4000>
* will be sent.
*
*/
#ifndef IO_VL53L0X_h
#define IO_VL53L0X_h
#include "IODevice.h"
class VL53L0X : public IODevice {
private:
uint16_t _ambient;
uint16_t _distance;
uint16_t _signal;
uint16_t _onThreshold;
uint16_t _offThreshold;
VPIN _xshutPin;
bool _value;
uint8_t _nextState = STATE_INIT;
I2CRB _rb;
uint8_t _inBuffer[12];
uint8_t _outBuffer[2];
static bool _addressConfigInProgress;
// State machine states.
enum : uint8_t {
STATE_INIT,
STATE_RESTARTMODULE,
STATE_CONFIGUREADDRESS,
STATE_CONFIGUREDEVICE,
STATE_INITIATESCAN,
STATE_CHECKSTATUS,
STATE_GETRESULTS,
STATE_DECODERESULTS,
STATE_FAILED,
};
// Register addresses
enum : uint8_t {
VL53L0X_REG_SYSRANGE_START=0x00,
VL53L0X_REG_RESULT_INTERRUPT_STATUS=0x13,
VL53L0X_REG_RESULT_RANGE_STATUS=0x14,
VL53L0X_CONFIG_PAD_SCL_SDA__EXTSUP_HV=0x89,
VL53L0X_REG_I2C_SLAVE_DEVICE_ADDRESS=0x8A,
};
const uint8_t VL53L0X_I2C_DEFAULT_ADDRESS=0x29;
public:
static void create(VPIN firstVpin, int nPins, I2CAddress i2cAddress, uint16_t onThreshold, uint16_t offThreshold, VPIN xshutPin = VPIN_NONE) {
if (checkNoOverlap(firstVpin, nPins,i2cAddress)) new VL53L0X(firstVpin, nPins, i2cAddress, onThreshold, offThreshold, xshutPin);
}
protected:
VL53L0X(VPIN firstVpin, int nPins, I2CAddress i2cAddress, uint16_t onThreshold, uint16_t offThreshold, VPIN xshutPin = VPIN_NONE) {
_firstVpin = firstVpin;
_nPins = (nPins > 3) ? 3 : nPins;
_I2CAddress = i2cAddress;
_onThreshold = onThreshold;
_offThreshold = offThreshold;
_xshutPin = xshutPin;
_value = 0;
addDevice(this);
}
void _begin() override {
// If there's only one device, then the XSHUT pin need not be connected. However,
// the device will not respond on its default address if it has
// already been changed. Therefore, we skip the address configuration if the
// desired address is already responding on the I2C bus.
_nextState = STATE_INIT;
_addressConfigInProgress = false;
}
void _loop(unsigned long currentMicros) override {
uint8_t status;
switch (_nextState) {
case STATE_INIT:
if (I2CManager.exists(_I2CAddress)) {
// Device already present on the nominated address, so skip the address initialisation.
_nextState = STATE_CONFIGUREDEVICE;
} else {
// On first entry to loop, reset this module by pulling XSHUT low. Each module
// will be addressed in turn, until all are in the reset state.
// If no XSHUT pin is configured, then only one device is supported.
if (_xshutPin != VPIN_NONE) IODevice::write(_xshutPin, 0);
_nextState = STATE_RESTARTMODULE;
delayUntil(currentMicros+10000);
}
break;
case STATE_RESTARTMODULE:
// On second entry, set XSHUT pin high to allow this module to restart.
// I've observed that the device tends to randomly reset if the XSHUT
// pin is set high from a 5V arduino, even through a pullup resistor.
// Assume that there will be a pull-up on the XSHUT pin to +2.8V as
// recommended in the device datasheet. Then we only need to
// turn our output pin high-impedence (by making it an input) and the
// on-board pullup will do its job.
// Ensure XSHUT is set for only one module at a time by using a
// shared flag accessible to all device instances.
if (!_addressConfigInProgress) {
_addressConfigInProgress = true;
// Configure XSHUT pin (if connected) to bring the module out of sleep mode.
if (_xshutPin != VPIN_NONE) IODevice::configureInput(_xshutPin, false);
// Allow the module time to restart
delayUntil(currentMicros+10000);
_nextState = STATE_CONFIGUREADDRESS;
}
break;
case STATE_CONFIGUREADDRESS:
// Then write the desired I2C address to the device, while this is the only
// module responding to the default address.
{
#if defined(I2C_EXTENDED_ADDRESS)
// Add subbus reference for desired address to the device default address.
I2CAddress defaultAddress = {_I2CAddress, VL53L0X_I2C_DEFAULT_ADDRESS};
status = I2CManager.write(defaultAddress, 2, VL53L0X_REG_I2C_SLAVE_DEVICE_ADDRESS, _I2CAddress.deviceAddress());
#else
status = I2CManager.write(VL53L0X_I2C_DEFAULT_ADDRESS, 2, VL53L0X_REG_I2C_SLAVE_DEVICE_ADDRESS, _I2CAddress);
#endif
if (status != I2C_STATUS_OK) {
reportError(status);
}
}
delayUntil(currentMicros+10000);
_nextState = STATE_CONFIGUREDEVICE;
break;
case STATE_CONFIGUREDEVICE:
// Allow next VL53L0X device to be configured
_addressConfigInProgress = false;
// Now check if device address has been set.
if (I2CManager.exists(_I2CAddress)) {
#ifdef DIAG_IO
_display();
#endif
// Set 2.8V mode
status = write_reg(VL53L0X_CONFIG_PAD_SCL_SDA__EXTSUP_HV,
read_reg(VL53L0X_CONFIG_PAD_SCL_SDA__EXTSUP_HV) | 0x01);
if (status != I2C_STATUS_OK) {
reportError(status);
} else
_nextState = STATE_INITIATESCAN;
} else {
DIAG(F("VL53L0X I2C:%s device not responding"), _I2CAddress.toString());
_deviceState = DEVSTATE_FAILED;
_nextState = STATE_FAILED;
}
break;
case STATE_INITIATESCAN:
// Not scanning, so initiate a scan
_outBuffer[0] = VL53L0X_REG_SYSRANGE_START;
_outBuffer[1] = 0x01;
I2CManager.write(_I2CAddress, _outBuffer, 2, &_rb);
_nextState = STATE_CHECKSTATUS;
break;
case STATE_CHECKSTATUS:
status = _rb.status;
if (status == I2C_STATUS_PENDING) return; // try next time
if (status != I2C_STATUS_OK) {
reportError(status);
} else
_nextState = STATE_GETRESULTS;
delayUntil(currentMicros + 95000); // wait for 95 ms before checking.
break;
case STATE_GETRESULTS:
// Ranging completed. Request results
_outBuffer[0] = VL53L0X_REG_RESULT_RANGE_STATUS;
I2CManager.read(_I2CAddress, _inBuffer, 12, _outBuffer, 1, &_rb);
delayUntil(currentMicros + 5000); // Allow 5ms to get data
_nextState = STATE_DECODERESULTS;
break;
case STATE_DECODERESULTS:
// If I2C write still busy, return.
status = _rb.status;
if (status == I2C_STATUS_PENDING) return; // try again next time
if (status == I2C_STATUS_OK) {
if (!(_inBuffer[0] & 1)) return; // device still busy
uint8_t deviceRangeStatus = ((_inBuffer[0] & 0x78) >> 3);
if (deviceRangeStatus == 0x0b) {
// Range status OK, so use data
_ambient = makeuint16(_inBuffer[7], _inBuffer[6]);
_signal = makeuint16(_inBuffer[9], _inBuffer[8]);
_distance = makeuint16(_inBuffer[11], _inBuffer[10]);
if (_distance <= _onThreshold)
_value = true;
else if (_distance > _offThreshold)
_value = false;
}
// Completed. Restart scan on next loop entry.
_nextState = STATE_INITIATESCAN;
} else {
reportError(status);
}
break;
case STATE_FAILED:
// Do nothing.
delayUntil(currentMicros+1000000UL);
break;
default:
break;
}
}
// Function to report a failed I2C operation.
void reportError(uint8_t status) {
DIAG(F("VL53L0X I2C:%s Error:%d %S"), _I2CAddress.toString(), status, I2CManager.getErrorMessage(status));
_deviceState = DEVSTATE_FAILED;
_value = false;
}
// For analogue read, first pin returns distance, second pin is signal strength, and third is ambient level.
int _readAnalogue(VPIN vpin) override {
int pin = vpin - _firstVpin;
switch (pin) {
case 0:
return _distance;
case 1:
return _signal;
case 2:
return _ambient;
default:
return -1;
}
}
// For digital read, return zero for all but first pin.
int _read(VPIN vpin) override {
if (vpin == _firstVpin)
return _value;
else
return 0;
}
void _display() override {
DIAG(F("VL53L0X I2C:%s Configured on Vpins:%u-%u On:%dmm Off:%dmm %S"),
_I2CAddress.toString(), _firstVpin, _firstVpin+_nPins-1, _onThreshold, _offThreshold,
(_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F(""));
}
private:
inline uint16_t makeuint16(byte lsb, byte msb) {
return (((uint16_t)msb) << 8) | lsb;
}
uint8_t write_reg(uint8_t reg, uint8_t data) {
// write byte to register
uint8_t outBuffer[2];
outBuffer[0] = reg;
outBuffer[1] = data;
return I2CManager.write(_I2CAddress, outBuffer, 2);
}
uint8_t read_reg(uint8_t reg) {
// read byte from register and return value
I2CManager.read(_I2CAddress, _inBuffer, 1, &reg, 1);
return _inBuffer[0];
}
};
bool VL53L0X::_addressConfigInProgress = false;
#endif // IO_VL53L0X_h

View File

@@ -1,173 +0,0 @@
/*
* © 2022, Chris Harlow. All rights reserved.
* Based on original by: Robin Simonds, Beagle Bay Inc
*
* This file is part of DCC-EX API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef IO_duinoNodes_h
#define IO_duinoNodes_h
#include <Arduino.h>
#include "defines.h"
#include "IODevice.h"
#define DN_PIN_MASK(bit) (0x80>>(bit%8))
#define DN_GET_BIT(x) (_pinValues[(x)/8] & DN_PIN_MASK((x)) )
#define DN_SET_BIT(x) _pinValues[(x)/8] |= DN_PIN_MASK((x))
#define DN_CLR_BIT(x) _pinValues[(x)/8] &= ~DN_PIN_MASK((x))
class IO_duinoNodes : public IODevice {
public:
IO_duinoNodes(VPIN firstVpin, int nPins,
byte clockPin, byte latchPin, byte dataPin,
const byte* pinmap) :
IODevice(firstVpin, nPins) {
_latchPin=latchPin;
_clockPin=clockPin;
_dataPin=dataPin;
_pinMap=pinmap;
_nShiftBytes=(nPins+7)/8; // rounded up to multiples of 8 bits
_pinValues=(byte*) calloc(_nShiftBytes,1);
// Connect to HAL so my _write, _read and _loop will be called as required.
IODevice::addDevice(this);
}
// Called by HAL to start handling this device
void _begin() override {
_deviceState = DEVSTATE_NORMAL;
pinMode(_latchPin,OUTPUT);
pinMode(_clockPin,OUTPUT);
pinMode(_dataPin,_pinMap?INPUT_PULLUP:OUTPUT);
_display();
if (!_pinMap) _loopOutput();
}
// loop called by HAL supervisor
void _loop(unsigned long currentMicros) override {
if (_pinMap) _loopInput(currentMicros);
else if (_xmitPending) _loopOutput();
}
void _loopInput(unsigned long currentMicros) {
if (currentMicros-_prevMicros < POLL_MICROS) return; // Nothing to do
_prevMicros=currentMicros;
//set latch to HIGH to freeze & store parallel data
ArduinoPins::fastWriteDigital(_latchPin, HIGH);
delayMicroseconds(1);
//set latch to LOW to enable the data to be transmitted serially
ArduinoPins::fastWriteDigital(_latchPin, LOW);
// stream in the bitmap using mapping order provided at constructor
for (int xmitByte=0;xmitByte<_nShiftBytes; xmitByte++) {
byte newByte=0;
for (int xmitBit=0;xmitBit<8; xmitBit++) {
ArduinoPins::fastWriteDigital(_clockPin, LOW);
delayMicroseconds(1);
bool data = ArduinoPins::fastReadDigital(_dataPin);
byte map=_pinMap[xmitBit];
if (data) newByte |= map;
else newByte &= ~map;
ArduinoPins::fastWriteDigital(_clockPin, HIGH);
delayMicroseconds(1);
}
_pinValues[xmitByte]=newByte;
// DIAG(F("DIN %x=%x"),xmitByte, newByte);
}
}
void _loopOutput() {
// stream out the bitmap (highest pin first)
_xmitPending=false;
ArduinoPins::fastWriteDigital(_latchPin, LOW);
for (int xmitBit=_nShiftBytes*8 -1; xmitBit>=0; xmitBit--) {
ArduinoPins::fastWriteDigital(_dataPin,DN_GET_BIT(xmitBit));
ArduinoPins::fastWriteDigital(_clockPin,HIGH);
ArduinoPins::fastWriteDigital(_clockPin,LOW);
}
ArduinoPins::fastWriteDigital(_latchPin, HIGH);
}
int _read(VPIN vpin) override {
int pin=vpin - _firstVpin;
bool b=DN_GET_BIT(pin);
return b?1:0;
}
void _write(VPIN vpin, int value) override {
int pin = vpin - _firstVpin;
bool oldval=DN_GET_BIT(pin);
bool newval=value!=0;
if (newval==oldval) return; // no change
if (newval) DN_SET_BIT(pin);
else DN_CLR_BIT(pin);
_xmitPending=true; // shift register will be sent on next _loop()
}
void _display() override {
DIAG(F("IO_duinoNodes %SPUT Configured on Vpins:%u-%u shift=%d"),
_pinMap?F("IN"):F("OUT"),
(int)_firstVpin,
(int)_firstVpin+_nPins-1, _nShiftBytes*8);
}
private:
static const unsigned long POLL_MICROS=100000; // 10 / S
unsigned long _prevMicros;
int _nShiftBytes=0;
VPIN _latchPin,_clockPin,_dataPin;
byte* _pinValues;
bool _xmitPending; // Only relevant in output mode
const byte* _pinMap; // NULL in output mode
};
class IO_DNIN8 {
public:
static void create(VPIN firstVpin, int nPins, byte clockPin, byte latchPin, byte dataPin )
{
// input arrives as board pin 0,7,6,5,1,2,3,4
static const byte pinmap[8]={0x80,0x01,0x02,0x04,0x40,0x20,0x10,0x08};
if (IODevice::checkNoOverlap(firstVpin,nPins))
new IO_duinoNodes( firstVpin, nPins, clockPin, latchPin, dataPin,pinmap);
}
};
class IO_DNIN8K {
public:
static void create(VPIN firstVpin, int nPins, byte clockPin, byte latchPin, byte dataPin )
{
// input arrives as board pin 0, 1, 2, 3, 4, 5, 6, 7
static const byte pinmap[8]={0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80};
if (IODevice::checkNoOverlap(firstVpin,nPins))
new IO_duinoNodes( firstVpin, nPins, clockPin, latchPin, dataPin,pinmap);
}
};
class IO_DNOU8 {
public:
static void create(VPIN firstVpin, int nPins, byte clockPin, byte latchPin, byte dataPin )
{
if (IODevice::checkNoOverlap(firstVpin,nPins))
new IO_duinoNodes( firstVpin, nPins, clockPin, latchPin, dataPin,NULL);
}
};
#endif

162
LCDDisplay.cpp Normal file
View File

@@ -0,0 +1,162 @@
/*
* © 2021, Chris Harlow, Neil McKechnie. All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
// CAUTION: the device dependent parts of this class are created in the .ini
// using LCD_Implementation.h
/* The strategy for drawing the screen is as follows.
* 1) There are up to eight rows of text to be displayed.
* 2) Blank rows of text are ignored.
* 3) If there are more non-blank rows than screen lines,
* then all of the rows are displayed, with the rest of the
* screen being blank.
* 4) If there are fewer non-blank rows than screen lines,
* then a scrolling strategy is adopted so that, on each screen
* refresh, a different subset of the rows is presented.
* 5) On each entry into loop2(), a single operation is sent to the
* screen; this may be a position command or a character for
* display. This spreads the onerous work of updating the screen
* and ensures that other loop() functions in the application are
* not held up significantly. The exception to this is when
* the loop2() function is called with force=true, where
* a screen update is executed to completion. This is normally
* only done during start-up.
* The scroll mode is selected by defining SCROLLMODE as 0, 1 or 2
* in the config.h.
* #define SCROLLMODE 0 is scroll continuous (fill screen if poss),
* #define SCROLLMODE 1 is by page (alternate between pages),
* #define SCROLLMODE 2 is by row (move up 1 row at a time).
*/
#include "LCDDisplay.h"
void LCDDisplay::clear() {
clearNative();
for (byte row = 0; row < MAX_LCD_ROWS; row++) rowBuffer[row][0] = '\0';
topRow = -1; // loop2 will fill from row 0
}
void LCDDisplay::setRow(byte line) {
hotRow = line;
hotCol = 0;
}
size_t LCDDisplay::write(uint8_t b) {
if (hotRow >= MAX_LCD_ROWS || hotCol >= MAX_LCD_COLS) return -1;
rowBuffer[hotRow][hotCol] = b;
hotCol++;
rowBuffer[hotRow][hotCol] = 0;
return 1;
}
void LCDDisplay::loop() {
if (!lcdDisplay) return;
lcdDisplay->loop2(false);
}
LCDDisplay *LCDDisplay::loop2(bool force) {
if (!lcdDisplay) return NULL;
unsigned long currentMillis = millis();
if (!force) {
// See if we're in the time between updates
if ((currentMillis - lastScrollTime) < LCD_SCROLL_TIME)
return NULL;
} else {
// force full screen update from the beginning.
rowFirst = -1;
rowNext = 0;
bufferPointer = 0;
done = false;
slot = 0;
}
do {
if (bufferPointer == 0) {
// Find a line of data to write to the screen.
if (rowFirst < 0) rowFirst = rowNext;
skipBlankRows();
if (!done) {
// Non-blank line found, so copy it.
for (uint8_t i = 0; i < sizeof(buffer); i++)
buffer[i] = rowBuffer[rowNext][i];
} else
buffer[0] = '\0'; // Empty line
setRowNative(slot); // Set position for display
charIndex = 0;
bufferPointer = &buffer[0];
} else {
// Write next character, or a space to erase current position.
char ch = *bufferPointer;
if (ch) {
writeNative(ch);
bufferPointer++;
} else
writeNative(' ');
if (++charIndex >= MAX_LCD_COLS) {
// Screen slot completed, move to next slot on screen
slot++;
bufferPointer = 0;
if (!done) {
moveToNextRow();
skipBlankRows();
}
}
if (slot >= lcdRows) {
// Last slot finished, reset ready for next screen update.
#if SCROLLMODE==2
if (!done) {
// On next refresh, restart one row on from previous start.
rowNext = rowFirst;
moveToNextRow();
skipBlankRows();
}
#endif
done = false;
slot = 0;
rowFirst = -1;
lastScrollTime = currentMillis;
return NULL;
}
}
} while (force);
return NULL;
}
void LCDDisplay::moveToNextRow() {
rowNext = (rowNext + 1) % MAX_LCD_ROWS;
#if SCROLLMODE == 1
// Finished if we've looped back to row 0
if (rowNext == 0) done = true;
#else
// Finished if we're back to the first one shown
if (rowNext == rowFirst) done = true;
#endif
}
void LCDDisplay::skipBlankRows() {
while (!done && rowBuffer[rowNext][0] == 0)
moveToNextRow();
}

80
LCDDisplay.h Normal file
View File

@@ -0,0 +1,80 @@
/*
* © 2021, Chris Harlow, Neil McKechnie. All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef LCDDisplay_h
#define LCDDisplay_h
#include <Arduino.h>
#if __has_include ( "config.h")
#include "config.h"
#endif
// Allow maximum message length to be overridden from config.h
#if !defined(MAX_MSG_SIZE)
#define MAX_MSG_SIZE 16
#endif
// This class is created in LCDisplay_Implementation.h
class LCDDisplay : public Print {
public:
static const int MAX_LCD_ROWS = 8;
static const int MAX_LCD_COLS = MAX_MSG_SIZE;
static const long LCD_SCROLL_TIME = 3000; // 3 seconds
static LCDDisplay* lcdDisplay;
LCDDisplay();
void interfake(int p1, int p2, int p3);
// Internally handled functions
static void loop();
LCDDisplay* loop2(bool force);
void setRow(byte line);
void clear();
virtual size_t write(uint8_t b);
using Print::write;
private:
void moveToNextRow();
void skipBlankRows();
// Relay functions to the live driver
void clearNative();
void displayNative();
void setRowNative(byte line);
void writeNative(char b);
unsigned long lastScrollTime = 0;
int8_t hotRow = 0;
int8_t hotCol = 0;
int8_t topRow = 0;
uint8_t lcdRows;
uint8_t lcdCols;
int8_t slot = 0;
int8_t rowFirst = -1;
int8_t rowNext = 0;
int8_t charIndex = 0;
char buffer[MAX_LCD_COLS + 1];
char* bufferPointer = 0;
bool done = false;
char rowBuffer[MAX_LCD_ROWS][MAX_LCD_COLS + 1];
};
#endif

View File

@@ -27,34 +27,29 @@
#ifndef LCD_Implementation_h
#define LCD_Implementation_h
#include "DisplayInterface.h"
#include "SSD1306Ascii.h"
#include "LiquidCrystal_I2C.h"
#include <Wire.h>
#include "LCDDisplay.h"
LCDDisplay * LCDDisplay::lcdDisplay=0;
// Implement the LCDDisplay shim class as a singleton.
// Notice that the LCDDisplay class declaration (LCDDisplay.h) is independent of the library
// but the implementation is compiled here with dependencies on LCDDriver which is
// specific to the library in use.
// Thats the workaround to the drivers not all implementing a common interface.
#if defined(OLED_DRIVER)
#include "LCD_OLED.h"
#define CONDITIONAL_LCD_START for (LCDDisplay * dummy=new LCDDisplay();dummy!=NULL; dummy=dummy->loop2(true))
// Implement the Display shim class as a singleton.
// The DisplayInterface class implements a display handler with no code (null device);
// The Display class sub-classes DisplayInterface to provide the common display code;
// Then Display class talks to the specific device type classes:
// SSD1306AsciiWire for I2C OLED driver with SSD1306 or SH1106 controllers;
// LiquidCrystal_I2C for I2C LCD driver for HD44780 with PCF8574 'backpack'.
#if defined(OLED_DRIVER)
#define DISPLAY_START(xxx) { \
DisplayInterface *t = new Display(new SSD1306AsciiWire(OLED_DRIVER)); \
t->begin(); \
xxx; \
t->refresh(); \
}
#elif defined(LCD_DRIVER)
#define DISPLAY_START(xxx) { \
DisplayInterface *t = new Display(new LiquidCrystal_I2C(LCD_DRIVER)); \
t->begin(); \
xxx; \
t->refresh();}
#else
#define DISPLAY_START(xxx) {}
#elif defined(LCD_DRIVER)
#include "LCD_LCD.h"
#define CONDITIONAL_LCD_START for (LCDDisplay * dummy=new LCDDisplay();dummy!=NULL; dummy=dummy->loop2(true))
#else
#include "LCD_NONE.h"
#define CONDITIONAL_LCD_START if (true) /* NO LCD CONFIG, but do the LCD macros to get DIAGS */
#endif
#endif // LCD_Implementation_h

33
LCD_LCD.h Normal file
View File

@@ -0,0 +1,33 @@
/*
* © 2021, Chris Harlow, Neil McKechnie. All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#include "LiquidCrystal_I2C.h"
LiquidCrystal_I2C LCDDriver(LCD_DRIVER); // set the LCD address, cols, rows
// DEVICE SPECIFIC LCDDisplay Implementation for LCD_DRIVER
LCDDisplay::LCDDisplay() {
lcdDisplay=this;
LCDDriver.init();
LCDDriver.backlight();
interfake(LCD_DRIVER);
clear();
}
void LCDDisplay::interfake(int p1, int p2, int p3) {(void)p1; (void)p2; lcdRows=p3; }
void LCDDisplay::clearNative() {LCDDriver.clear();}
void LCDDisplay::setRowNative(byte row) { LCDDriver.setCursor(0, row); }
void LCDDisplay::writeNative(char b){ LCDDriver.write(b); }
void LCDDisplay::displayNative() { LCDDriver.display(); }

View File

@@ -1,6 +1,6 @@
/*
* © 2021, Harald Barth.
*
* © 2021, Chris Harlow, Neil McKechnie. All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
@@ -17,23 +17,11 @@
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#if defined(ARDUINO_ARCH_ESP32)
#ifndef WifiESP32_h
#define WifiESP32_h
#include "FSH.h"
class WifiESP
{
public:
static bool setup(const char *wifiESSID,
const char *wifiPassword,
const char *hostname,
const int port,
const byte channel);
static void loop();
private:
};
#endif //WifiESP8266_h
#endif //ESP8266
// dummy LCD shim to keep linker happy
LCDDisplay::LCDDisplay() {}
void LCDDisplay::interfake(int p1, int p2, int p3) {(void)p1; (void)p2; (void)p3;}
void LCDDisplay::setRowNative(byte row) { (void)row;}
void LCDDisplay::clearNative() {}
void LCDDisplay::writeNative(char b){ (void)b;} //
void LCDDisplay::displayNative(){}

73
LCD_OLED.h Normal file
View File

@@ -0,0 +1,73 @@
/*
* © 2021, Chris Harlow, Neil McKechnie. All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
// OLED Implementation of LCDDisplay class
// Note: this file is optionally included by LCD_Implementation.h
// It is NOT a .cpp file to prevent it being compiled and demanding libraries
// even when not needed.
#include "I2CManager.h"
#include "SSD1306Ascii.h"
SSD1306AsciiWire LCDDriver;
// DEVICE SPECIFIC LCDDisplay Implementation for OLED
LCDDisplay::LCDDisplay() {
// Scan for device on 0x3c and 0x3d.
I2CManager.begin();
I2CManager.setClock(400000L); // Set max supported I2C speed
for (byte address = 0x3c; address <= 0x3d; address++) {
if (I2CManager.exists(address)) {
// Device found
DIAG(F("OLED display found at 0x%x"), address);
interfake(OLED_DRIVER, 0);
const DevType *devType;
if (lcdCols == 132)
devType = &SH1106_128x64; // Actually 132x64 but treated as 128x64
else if (lcdCols == 128 && lcdRows == 4)
devType = &Adafruit128x32;
else
devType = &Adafruit128x64;
LCDDriver.begin(devType, address);
lcdDisplay = this;
LCDDriver.setFont(System5x7); // Normal 1:1 pixel scale, 8 bits high
clear();
return;
}
}
DIAG(F("OLED display not found"));
}
void LCDDisplay::interfake(int p1, int p2, int p3) {
lcdCols = p1;
lcdRows = p2 / 8;
(void)p3;
}
void LCDDisplay::clearNative() { LCDDriver.clear(); }
void LCDDisplay::setRowNative(byte row) {
// Positions text write to start of row 1..n
int y = row;
LCDDriver.setCursor(0, y);
}
void LCDDisplay::writeNative(char b) { LCDDriver.write(b); }
void LCDDisplay::displayNative() {}

18
LCN.cpp
View File

@@ -43,25 +43,23 @@ void LCN::loop() {
while (stream->available()) {
int ch = stream->read();
if (ch >= '0' && ch <= '9') { // accumulate id value
if (ch >= 0 && ch <= '9') { // accumulate id value
id = 10 * id + ch - '0';
}
else if (ch == 't' || ch == 'T') { // Turnout opcodes
if (Diag::LCN) DIAG(F("LCN IN %d%c"),id,(char)ch);
if (!Turnout::exists(id)) LCNTurnout::create(id);
Turnout::setClosedStateOnly(id,ch=='t');
id = 0;
}
else if (ch == 'y' || ch == 'Y') { // Turnout opcodes
if (Diag::LCN) DIAG(F("LCN IN %d%c"),id,(char)ch);
Turnout::setClosed(id,ch=='y');
Turnout * tt = Turnout::get(id);
if (!tt) Turnout::create(id, LCN_TURNOUT_ADDRESS, 0);
if (ch == 't') tt->data.tStatus |= STATUS_ACTIVE;
else tt->data.tStatus &= ~STATUS_ACTIVE;
Turnout::turnoutlistHash++; // signals ED update of turnout data
id = 0;
}
else if (ch == 'S' || ch == 's') {
if (Diag::LCN) DIAG(F("LCN IN %d%c"),id,(char)ch);
Sensor * ss = Sensor::get(id);
if (!ss) ss = Sensor::create(id, VPIN_NONE, 0); // impossible pin
ss->setState(ch == 'S');
if (!ss) ss = Sensor::create(id, 255,0); // impossible pin
ss->active = ch == 'S';
id = 0;
}
else id = 0; // ignore any other garbage from LCN

20
LCN.h
View File

@@ -1,23 +1,3 @@
/*
* © 2021 Harald Barth
* © 2021 Fred Decker
* All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef LCN_h
#define LCN_h
#include <Arduino.h>

View File

@@ -20,7 +20,7 @@
#include <Arduino.h>
#include "LiquidCrystal_I2C.h"
#include "DIAG.h"
#include "I2CManager.h"
// When the display powers up, it is configured as follows:
//
@@ -41,34 +41,33 @@
// can't assume that its in that state when a sketch starts (and the
// LiquidCrystal constructor is called).
LiquidCrystal_I2C::LiquidCrystal_I2C(I2CAddress lcd_Addr, uint8_t lcd_cols,
LiquidCrystal_I2C::LiquidCrystal_I2C(uint8_t lcd_Addr, uint8_t lcd_cols,
uint8_t lcd_rows) {
_Addr = lcd_Addr;
lcdRows = lcd_rows; // Number of character rows (typically 2 or 4).
lcdCols = lcd_cols; // Number of character columns (typically 16 or 20)
_backlightval = 0;
}
_cols = lcd_cols;
_rows = lcd_rows;
_backlightval = LCD_NOBACKLIGHT;
}
bool LiquidCrystal_I2C::begin() {
void LiquidCrystal_I2C::init() { init_priv(); }
void LiquidCrystal_I2C::init_priv() {
I2CManager.begin();
I2CManager.setClock(100000L); // PCF8574 is spec'd to 100kHz.
if (I2CManager.exists(_Addr)) {
DIAG(F("%dx%d LCD configured on I2C:%s"), (int)lcdCols, (int)lcdRows, _Addr.toString());
_displayfunction = LCD_4BITMODE | LCD_1LINE | LCD_5x8DOTS;
backlight();
} else {
DIAG(F("LCD not found on I2C:%s"), _Addr.toString());
return false;
}
_displayfunction = LCD_4BITMODE | LCD_1LINE | LCD_5x8DOTS;
begin(_cols, _rows);
}
if (lcdRows > 1) {
void LiquidCrystal_I2C::begin(uint8_t cols, uint8_t lines) {
if (lines > 1) {
_displayfunction |= LCD_2LINE;
}
_numlines = lines;
(void)cols; // Suppress compiler warning.
// according to datasheet, we need at least 40ms after power rises above 2.7V
// before sending commands. Arduino can turn on way before 4.5V so we'll allow
// before sending commands. Arduino can turn on way befer 4.5V so we'll allow
// 100 milliseconds after pulling both RS and R/W and backlight pin low
expanderWrite(
_backlightval); // reset expander and turn backlight off (Bit 8 =1)
@@ -79,19 +78,19 @@ bool LiquidCrystal_I2C::begin() {
// figure 24, pg 46
// we start in 8bit mode, try to set 4 bit mode
write4bits(0x03);
delayMicroseconds(5000); // wait min 4.1ms
write4bits(0x03 << 4);
delayMicroseconds(4500); // wait min 4.1ms
// second try
write4bits(0x03);
delayMicroseconds(5000); // wait min 4.1ms
write4bits(0x03 << 4);
delayMicroseconds(4500); // wait min 4.1ms
// third go!
write4bits(0x03);
delayMicroseconds(5000);
write4bits(0x03 << 4);
delayMicroseconds(150);
// finally, set to 4-bit interface
write4bits(0x02);
write4bits(0x02 << 4);
// set # lines, font size, etc.
command(LCD_FUNCTIONSET | _displayfunction);
@@ -100,27 +99,36 @@ bool LiquidCrystal_I2C::begin() {
_displaycontrol = LCD_DISPLAYON | LCD_CURSOROFF | LCD_BLINKOFF;
display();
// clear it off
clear();
// Initialize to default text direction (for roman languages)
_displaymode = LCD_ENTRYLEFT | LCD_ENTRYSHIFTDECREMENT;
// set the entry mode
command(LCD_ENTRYMODESET | _displaymode);
return true;
setCursor(0, 0);
}
/********** high level commands, for the user! */
void LiquidCrystal_I2C::clearNative() {
void LiquidCrystal_I2C::clear() {
command(LCD_CLEARDISPLAY); // clear display, set cursor position to zero
delayMicroseconds(2000); // this command takes 1.52ms but allow plenty
delayMicroseconds(2000); // this command takes 1.52ms
}
void LiquidCrystal_I2C::setRowNative(byte row) {
uint8_t row_offsets[] = {0x00, 0x40, 0x14, 0x54};
if (row >= lcdRows) {
row = lcdRows - 1; // we count rows starting w/0
void LiquidCrystal_I2C::setCursor(uint8_t col, uint8_t row) {
int row_offsets[] = {0x00, 0x40, 0x14, 0x54};
if (row > _numlines) {
row = _numlines - 1; // we count rows starting w/0
}
command(LCD_SETDDRAMADDR | (row_offsets[row]));
command(LCD_SETDDRAMADDR | (col + row_offsets[row]));
}
// Turn the display on/off (quickly)
void LiquidCrystal_I2C::noDisplay() {
_displaycontrol &= ~LCD_DISPLAYON;
command(LCD_DISPLAYCONTROL | _displaycontrol);
}
void LiquidCrystal_I2C::display() {
@@ -130,7 +138,7 @@ void LiquidCrystal_I2C::display() {
// Turn the (optional) backlight off/on
void LiquidCrystal_I2C::noBacklight(void) {
_backlightval &= ~LCD_BACKLIGHT;
_backlightval = LCD_NOBACKLIGHT;
expanderWrite(0);
}
@@ -139,15 +147,11 @@ void LiquidCrystal_I2C::backlight(void) {
expanderWrite(0);
}
size_t LiquidCrystal_I2C::writeNative(uint8_t value) {
size_t LiquidCrystal_I2C::write(uint8_t value) {
send(value, Rs);
return 1;
}
bool LiquidCrystal_I2C::isBusy() {
return rb.isBusy();
}
/*********** mid level commands, for sending data/cmds */
inline void LiquidCrystal_I2C::command(uint8_t value) {
@@ -190,35 +194,25 @@ inline void LiquidCrystal_I2C::command(uint8_t value) {
// a single I2C transmission.
void LiquidCrystal_I2C::send(uint8_t value, uint8_t mode) {
mode |= _backlightval;
uint8_t highnib = (((value >> 4) & 0x0f) << BACKPACK_DATA_BITS) | mode;
uint8_t lownib = ((value & 0x0f) << BACKPACK_DATA_BITS) | mode;
uint8_t highnib = (value & 0xf0) | mode;
uint8_t lownib = ((value << 4) & 0xf0) | mode;
// Send both nibbles
uint8_t len = 0;
rb.wait();
outputBuffer[len++] = highnib|En;
outputBuffer[len++] = highnib;
outputBuffer[len++] = lownib|En;
outputBuffer[len++] = lownib;
I2CManager.write(_Addr, outputBuffer, len, &rb); // Write command asynchronously
byte buffer[] = {(byte)(highnib|En), highnib, (byte)(lownib|En), lownib};
I2CManager.write(_Addr, buffer, sizeof(buffer));
}
// write 4 data bits to the HD44780 LCD controller.
// write 4 bits to the HD44780 LCD controller.
void LiquidCrystal_I2C::write4bits(uint8_t value) {
uint8_t _data = ((value & 0x0f) << BACKPACK_DATA_BITS) | _backlightval;
uint8_t _data = value | _backlightval;
// Enable must be set/reset for at least 450ns. This is well within the
// I2C clock cycle time of 2.5us at 400kHz. Data is clocked in to the
// HD44780 on the trailing edge of the Enable pin.
uint8_t len = 0;
rb.wait();
outputBuffer[len++] = _data|En;
outputBuffer[len++] = _data;
I2CManager.write(_Addr, outputBuffer, len, &rb); // Write command asynchronously
byte buffer[] = {(byte)(_data|En), _data};
I2CManager.write(_Addr, buffer, sizeof(buffer));
}
// write a byte to the PCF8574 I2C interface. We don't need to set
// the enable pin for this.
void LiquidCrystal_I2C::expanderWrite(uint8_t value) {
rb.wait();
outputBuffer[0] = value | _backlightval;
I2CManager.write(_Addr, outputBuffer, 1, &rb); // Write command asynchronously
I2CManager.write(_Addr, 1, value | _backlightval);
}

View File

@@ -22,13 +22,13 @@
#define LiquidCrystal_I2C_h
#include <Arduino.h>
#include "Display.h"
#include "I2CManager.h"
// commands
#define LCD_CLEARDISPLAY 0x01
#define LCD_RETURNHOME 0x02
#define LCD_ENTRYMODESET 0x04
#define LCD_DISPLAYCONTROL 0x08
#define LCD_CURSORSHIFT 0x10
#define LCD_FUNCTIONSET 0x20
#define LCD_SETCGRAMADDR 0x40
#define LCD_SETDDRAMADDR 0x80
@@ -41,59 +41,62 @@
// flags for display on/off control
#define LCD_DISPLAYON 0x04
#define LCD_DISPLAYOFF 0x00
#define LCD_CURSORON 0x02
#define LCD_CURSOROFF 0x00
#define LCD_BLINKON 0x01
#define LCD_BLINKOFF 0x00
// flags for display/cursor shift
#define LCD_DISPLAYMOVE 0x08
#define LCD_CURSORMOVE 0x00
#define LCD_MOVERIGHT 0x04
#define LCD_MOVELEFT 0x00
// flags for function set
#define LCD_8BITMODE 0x10
#define LCD_4BITMODE 0x00
#define LCD_2LINE 0x08
#define LCD_1LINE 0x00
#define LCD_5x10DOTS 0x04
#define LCD_5x8DOTS 0x00
// Bit mapping onto PCF8574 port
#define BACKPACK_Rs_BIT 0
#define BACKPACK_Rw_BIT 1
#define BACKPACK_En_BIT 2
#define BACKPACK_BACKLIGHT_BIT 3
#define BACKPACK_DATA_BITS 4 // Bits 4-7
// Equivalent mask bits
#define LCD_BACKLIGHT (1 << BACKPACK_BACKLIGHT_BIT) // Backlight enable
#define En (1 << BACKPACK_En_BIT) // Enable bit
#define Rw (1 << BACKPACK_Rw_BIT) // Read/Write bit
#define Rs (1 << BACKPACK_Rs_BIT) // Register select bit
// flags for backlight control
#define LCD_BACKLIGHT 0x08
#define LCD_NOBACKLIGHT 0x00
class LiquidCrystal_I2C : public DisplayDevice {
#define En 0b00000100 // Enable bit
#define Rw 0b00000010 // Read/Write bit
#define Rs 0b00000001 // Register select bit
class LiquidCrystal_I2C : public Print {
public:
LiquidCrystal_I2C(I2CAddress lcd_Addr,uint8_t lcd_cols,uint8_t lcd_rows);
bool begin() override;
void clearNative() override;
void setRowNative(byte line) override;
size_t writeNative(uint8_t c) override;
// I/O is synchronous, so if this is called we're not busy!
bool isBusy() override;
LiquidCrystal_I2C(uint8_t lcd_Addr,uint8_t lcd_cols,uint8_t lcd_rows);
void begin(uint8_t cols, uint8_t rows);
void clear();
void noDisplay();
void display();
void noBacklight();
void backlight();
void setCursor(uint8_t, uint8_t);
virtual size_t write(uint8_t);
void command(uint8_t);
uint16_t getNumCols() { return lcdCols; }
uint16_t getNumRows() { return lcdRows; }
void init();
private:
void init_priv();
void send(uint8_t, uint8_t);
void write4bits(uint8_t);
void expanderWrite(uint8_t);
uint8_t lcdCols=0, lcdRows=0;
I2CAddress _Addr;
uint8_t _Addr;
uint8_t _displayfunction;
uint8_t _displaycontrol;
uint8_t _displaymode;
uint8_t _backlightval = 0;
uint8_t outputBuffer[4];
I2CRB rb;
uint8_t _numlines;
uint8_t _cols;
uint8_t _rows;
uint8_t _backlightval;
};
#endif

42
MQTTBrokers.h Normal file
View File

@@ -0,0 +1,42 @@
/*
* © 2021, Gregor Baues, All rights reserved.
*
* This file is part of DCC-EX/CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*
*/
#ifndef _MQTTBrokers_h_
#define _MQTTBrokers_h_
// Defines preconfigured mqtt broker configurations
// EthernetShields / Arduino do not support secure transport i.e. on either port 443 or 8883 for MQTTS on most broker installations
// Once we support the ESP / Wifi as Transport medium we may get TLS capabilities for data in transit i.e. can use the 443/8883 ports
#define MQPWD F(MQTT_PWD)
#define MQUID F(MQTT_USER)
#define MQPREFIX F(MQTT_PREFIX)
// Cloud server provided by the DccEX team for testing purposes; apply for a uid/pwd on discord
#define DCCEX_MQTT_BROKER F("DccexMQ"), new MQTTBroker( 9883, F("dcclms.modelrailroad.ovh"), MQUID, MQPWD, MQPREFIX)
// Mosquitto test server
#define DCCEX_MOSQUITTO F("Mosquitto"), new MQTTBroker(1883, F("test.mosquitto.org"))
// HiveMQ test server
#define DCCEX_HIVEMQ F("HiveMQ"), new MQTTBroker(1883, F("broker.hivemq.com"))
#endif

180
MQTTCallbackHandlers.cpp Normal file
View File

@@ -0,0 +1,180 @@
/*
* © 2021, Gregor Baues, All rights reserved.
*
* This file is part of DCC-EX/CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*
*/
#if __has_include("config.h")
#include "config.h"
#else
#warning config.h not found. Using defaults from config.example.h
#include "config.example.h"
#endif
#include "defines.h"
#include <errno.h>
#include <limits.h>
#include "MQTTInterface.h"
// Fwd decl for the callback handlers
void mqttDCCEXCallback(MQTTInterface *mqtt, csmsg_t &tm);
void mqttProtocolCallback(MQTTInterface *mqtt, csmsg_t &tm);
void mqttMCallback(MQTTInterface *mqtt, csmsg_t &tm);
typedef void (*CallbackFunc)(MQTTInterface *mqtt, csmsg_t &tm);
template<class M, class N>
struct CallbackFunction {
M first;
N second;
};
using CallbackFunctions = CallbackFunction<char, CallbackFunc>[MAX_CALLBACKS];
// lookup table for the protocol handle functions
constexpr CallbackFunctions vec = {
{'<', mqttDCCEXCallback},
{'{', mqttProtocolCallback},
{'m', mqttMCallback}
};
long cantorEncode(long a, long b)
{
return (((a + b) * (a + b + 1)) / 2) + b;
}
void cantorDecode(int32_t c, int *a, int *b)
{
int w = floor((sqrt(8 * c + 1) - 1) / 2);
int t = (w * (w + 1)) / 2;
*b = c - t;
*a = w - *b;
}
/**
* @brief lookup of the proper function for < or { based commands
*
* @param c
* @return CallbackFunc
*/
auto protocolDistributor(const char c) -> CallbackFunc {
for (auto &&f : vec)
{
if (f.first == c)
return f.second;
}
return nullptr;
}
void protocolHandler(MQTTInterface *mqtt, csmsg_t &tm) {
protocolDistributor(tm.cmd[0])(mqtt, tm);
}
/**
* @brief Callback for handling 'm' MQTT Protocol commands (deprecated)
* @deprecated to be replaced by '{' commands in simple JSON format
*/
void mqttMCallback(MQTTInterface *mqtt, csmsg_t &tm)
{
auto clients = mqtt->getClients();
// DIAG(F("MQTT m - Callback"));
switch (tm.cmd[1])
{
case 'i': // Inital handshake message to create the tunnel
{
char buffer[MAXPAYLOAD];
char *tmp = tm.cmd + 3;
auto length = strlen(tm.cmd);
strlcpy(buffer, tmp, length);
buffer[length - 4] = '\0';
// DIAG(F("MQTT buffer %s - %s - %s - %d"), tm.cmd, tmp, buffer, length);
auto distantid = strtol(buffer, NULL, 10);
if (errno == ERANGE || distantid > UCHAR_MAX)
{
DIAG(F("MQTT Invalid Handshake ID; must be between 0 and 255"));
return;
}
if (distantid == 0)
{
DIAG(F("MQTT Invalid Handshake ID"));
return;
}
// Create a new MQTT client
auto subscriberid = mqtt->obtainSubscriberID(); // to be used in the parsing process for the clientid in the ringbuffer
if (subscriberid == 0)
{
DIAG(F("MQTT no more connections are available"));
return;
}
auto topicid = cantorEncode((long)subscriberid, (long)distantid);
DIAG(F("MQTT Client connected : subscriber [%d] : distant [%d] : topic: [%d]"), subscriberid, (int)distantid, topicid);
// extract the number delivered from & initalize the new mqtt client object
clients[subscriberid] = {(int)distantid, subscriberid, topicid, false}; // set to true once the channels are available
auto sq = mqtt->getSubscriptionQueue();
sq->push(subscriberid);
return;
}
default:
{
return;
}
}
}
/**
* @brief Callback for handling '{' MQTT Protocol commands
*/
void mqttProtocolCallback(MQTTInterface *mqtt, csmsg_t &tm)
{
// DIAG(F("MQTT Protocol - Callback"));
}
/**
* @brief Callback for handling '<' DccEX commands
*/
void mqttDCCEXCallback(MQTTInterface *mqtt, csmsg_t &tm)
// void mqttDCCEXCallback(MQTTInterface *mqtt, char *topic, char *payload, unsigned int length)
{
// DIAG(F("MQTT DCCEX - Callback"));
if (!tm.mqsocket)
{
DIAG(F("MQTT Can't identify sender; command send on wrong topic"));
return;
}
int idx = mqtt->getPool()->setItem(tm); // Add the recieved command to the pool
if (idx == -1)
{
DIAG(F("MQTT Command pool full. Could not handle recieved command."));
return;
}
mqtt->getIncomming()->push(idx); // Add the index of the pool item to the incomming queue
// don't show the topic as we would have to save it also just like the payload
if (Diag::MQTT)
DIAG(F("MQTT Message arrived: [%s]"), tm.cmd);
}

540
MQTTInterface.cpp Normal file
View File

@@ -0,0 +1,540 @@
/*
* © 2021, Gregor Baues, All rights reserved.
*
* This file is part of DCC-EX/CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*
*/
#if __has_include("config.h")
#include "config.h"
#else
#warning config.h not found. Using defaults from config.example.h
#include "config.example.h"
#endif
#include "defines.h"
#include <errno.h>
#include <limits.h>
#include "MQTTInterface.h"
#include "MQTTBrokers.h"
#include "DCCTimer.h"
#include "CommandDistributor.h"
#include "freeMemory.h"
MQTTInterface *MQTTInterface::singleton = NULL;
void protocolHandler(MQTTInterface *mqtt, csmsg_t &tm);
/**
* @brief callback used from DIAG to send diag messages to the broker / clients
*
* @param msg
* @param length
*/
void mqttDiag(const char *msg, const int length)
{
if (MQTTInterface::get()->getState() == CONNECTED)
{
// if not connected all goes only to Serial;
// if CONNECTED we have at least the root topic subscribed to
auto mqSocket = MQTTInterface::get()->getActive();
char topic[MAXTSTR];
memset(topic, 0, MAXTSTR);
if (mqSocket == 0)
{ // send to root topic of the commandstation as it doen't concern a specific client at this point
sprintf(topic, "%s", MQTTInterface::get()->getClientID());
}
else
{
sprintf(topic, "%s/%ld/diag", MQTTInterface::get()->getClientID(), MQTTInterface::get()->getClients()[mqSocket].topic);
}
// Serial.print(" ---- MQTT pub to: ");
// Serial.print(topic);
// Serial.print(" Msg: ");
// Serial.print(msg);
MQTTInterface::get()->publish(topic, msg);
}
}
void MQTTInterface::setup()
{
DiagLogger::get().addDiagWriter(mqttDiag);
singleton = new MQTTInterface();
if (!singleton->connected)
{
singleton = NULL;
}
if (Diag::MQTT)
DIAG(F("MQTT Interface instance: [%x] - Setup done"), singleton);
};
MQTTInterface::MQTTInterface()
{
this->connected = this->setupNetwork();
if (!this->connected)
{
DIAG(F("Network setup failed"));
}
else
{
this->setup(CSMQTTBROKER);
}
this->outboundRing = new RingStream(OUT_BOUND_SIZE);
};
/**
* @brief determine the mqsocket from a topic
*
* @return byte the mqsocketid for the message recieved
*/
byte senderMqSocket(MQTTInterface *mqtt, char *topic)
{
// list of all available clients from which we can determine the mqsocket
auto clients = mqtt->getClients();
const char s[2] = "/"; // topic delimiter is /
char *token;
byte mqsocket = 0;
/* get the first token = ClientID */
token = strtok(topic, s);
/* get the second token = topicID */
token = strtok(NULL, s);
if (token != NULL) // topic didn't contain any topicID
{
auto topicid = atoi(token);
// verify that there is a MQTT client with that topic id connected
// check in the array of clients if we have one with the topicid
// start at 1 as 0 is not allocated as mqsocket
for (int i = 1; i <= mqtt->getClientSize(); i++)
{
if (clients[i].topic == topicid)
{
mqsocket = i;
break; // we are done
}
}
// if we get here we have a topic but no associated client
}
// if mqsocket == 0 here we haven't got any Id in the topic string
return mqsocket;
}
/**
* @brief MQTT Interface callback recieving all incomming messages from the PubSubClient
*
* @param topic
* @param payload
* @param length
*/
void mqttCallback(char *topic, byte *pld, unsigned int length)
{
// it's a bounced diag message ignore in all cases
// but it should not be necessary here .. that means the active mqsocket is wrong when sending to diag message
if ((pld[0] == '<') && (pld[1] == '*'))
{
return;
}
// ignore anything above the PAYLOAD limit of 64 char which should be enough
// in general things rejected here is the bounce of the inital messages setting up the chnanel etc
if (length >= MAXPAYLOAD)
{
return;
}
MQTTInterface *mqtt = MQTTInterface::get();
csmsg_t tm; // topic message
// FOR DIAGS and MQTT ON in the callback we need to copy the payload buffer
// as during the publish of the diag messages the original payload gets destroyed
// so we setup the csmsg_t now to save yet another buffer
// if tm not used it will just be discarded at the end of the function call
memset(tm.cmd, 0, MAXPAYLOAD); // Clean up the cmd buffer - should not be necessary
strlcpy(tm.cmd, (char *)pld, length + 1); // Message payload
tm.mqsocket = senderMqSocket(mqtt, topic); // On which socket did we recieve the mq message
mqtt->setActive(tm.mqsocket); // connection from where we recieved the command is active now
if (Diag::MQTT)
DIAG(F("MQTT Callback:[%s/%d] [%s] [%d] on interface [%x]"), topic, tm.mqsocket, tm.cmd, length, mqtt);
protocolHandler(mqtt, tm);
}
/**
* @brief Copies an byte array to a hex representation as string; used for generating the unique Arduino ID
*
* @param array array containing bytes
* @param len length of the array
* @param buffer buffer to which the string will be written; make sure the buffer has appropriate length
*/
static void array_to_string(byte array[], unsigned int len, char buffer[])
{
for (unsigned int i = 0; i < len; i++)
{
byte nib1 = (array[i] >> 4) & 0x0F;
byte nib2 = (array[i] >> 0) & 0x0F;
buffer[i * 2 + 0] = nib1 < 0xA ? '0' + nib1 : 'A' + nib1 - 0xA;
buffer[i * 2 + 1] = nib2 < 0xA ? '0' + nib2 : 'A' + nib2 - 0xA;
}
buffer[len * 2] = '\0';
}
/**
* @brief Connect to the MQTT broker; Parameters for this function are defined in
* like the motoshield configurations there are mqtt broker configurations in config.h
*
* @param id Name provided to the broker configuration
* @param b MQTT broker object containing the main configuration parameters
*/
void MQTTInterface::setup(const FSH *id, MQTTBroker *b)
{
//Create the MQTT environment and establish inital connection to the Broker
broker = b;
DIAG(F("[%d] MQTT Connect to %S at %S/%d.%d.%d.%d:%d"), freeMemory(), id, broker->domain, broker->ip[0], broker->ip[1], broker->ip[2], broker->ip[3], broker->port);
// initalize MQ Broker
mqttClient = new PubSubClient(broker->ip, broker->port, mqttCallback, ethClient);
if (Diag::MQTT)
DIAG(F("MQTT Client created ok..."));
array_to_string(mac, CLIENTIDSIZE, clientID);
DIAG(F("[%d] MQTT Client ID : %s"), freeMemory(), clientID);
connect(); // inital connection as well as reconnects
}
/**
* @brief MQTT broker connection / reconnection
*
*/
void MQTTInterface::connect()
{
int reconnectCount = 0;
connectID[0] = '\0';
// Build the connect ID : Prefix + clientID
if (broker->prefix != nullptr)
{
strcpy_P(connectID, (const char *)broker->prefix);
}
strcat(connectID, clientID);
// Connect to the broker
DIAG(F("[%d] MQTT %s (re)connecting ..."), freeMemory(), connectID);
while (!mqttClient->connected() && reconnectCount < MAXRECONNECT)
{
switch (broker->cType)
{
// no uid no pwd
case 1:
{ // port(p), ip(i), domain(d),
DIAG(F("[%d] MQTT Broker connecting anonymous ..."), freeMemory());
if (mqttClient->connect(connectID))
{
DIAG(F("[%d] MQTT Broker connected ..."),freeMemory());
auto sub = subscribe(clientID); // set up the main subscription on which we will recieve the intal mi message from a subscriber
if (Diag::MQTT)
DIAG(F("MQTT subscriptons %s..."), sub ? "ok" : "failed");
mqState = CONNECTED;
}
else
{
DIAG(F("MQTT broker connection failed, rc=%d, trying to reconnect"), mqttClient->state());
reconnectCount++;
}
break;
}
// with uid passwd
case 2:
{
DIAG(F("MQTT Broker connecting with uid/pwd ..."));
char user[strlen_P((const char *)broker->user)];
char pwd[strlen_P((const char *)broker->pwd)];
// need to copy from progmem to lacal
strcpy_P(user, (const char *)broker->user);
strcpy_P(pwd, (const char *)broker->pwd);
if (mqttClient->connect(connectID, user, pwd))
{
DIAG(F("MQTT Broker connected ..."));
auto sub = subscribe(clientID); // set up the main subscription on which we will recieve the intal mi message from a subscriber
if (Diag::MQTT)
DIAG(F("MQTT subscriptons %s..."), sub ? "ok" : "failed");
mqState = CONNECTED;
}
else
{
DIAG(F("MQTT broker connection failed, rc=%d, trying to reconnect"), mqttClient->state());
reconnectCount++;
}
break;
// ! add last will messages for the client
// (connectID, MQTT_BROKER_USER, MQTT_BROKER_PASSWD, "$connected", 0, true, "0", 0))
}
}
if (reconnectCount == MAXRECONNECT)
{
DIAG(F("MQTT Connection aborted after %d tries"), MAXRECONNECT);
mqState = CONNECTION_FAILED;
}
}
}
/**
* @brief for the time being only one topic at the root
* which is the unique clientID from the MCU
* QoS is 0 by default
*
* @param topic to subsribe to
* @return boolean true if successful false otherwise
*/
boolean MQTTInterface::subscribe(const char *topic)
{
auto res = mqttClient->subscribe(topic);
return res;
}
void MQTTInterface::publish(const char *topic, const char *payload)
{
mqttClient->publish(topic, payload);
}
/**
* @brief Connect the Ethernet network;
*
* @return true if connections was successful
*/
bool MQTTInterface::setupNetwork()
{
// setup Ethernet connection first
DIAG(F("[%d] Starting network setup ... "), freeMemory());
DCCTimer::getSimulatedMacAddress(mac);
#ifdef IP_ADDRESS
Ethernet.begin(mac, IP_ADDRESS);
#else
if (Ethernet.begin(mac) == 0)
{
DIAG(F("Ethernet.begin FAILED"));
return false;
}
#endif
DIAG(F("[%d] Ethernet.begin OK"), freeMemory());
if (Ethernet.hardwareStatus() == EthernetNoHardware)
{
DIAG(F("Ethernet shield not found"));
return false;
}
// For slower cards like the ENC courtesy @PaulS
// wait max 5 sec before bailing out on the connection
unsigned long startmilli = millis();
while ((millis() - startmilli) < 5500)
{
if (Ethernet.linkStatus() == LinkON)
break;
DIAG(F("Ethernet waiting for link (1sec) "));
delay(1000);
}
if (Ethernet.linkStatus() == LinkOFF)
{
DIAG(F("Ethernet cable not connected"));
return false;
}
DIAG(F("[%d] Ethernet link is up"),freeMemory());
IPAddress ip = Ethernet.localIP(); // reassign the obtained ip address
DIAG(F("IP: %d.%d.%d.%d"), ip[0], ip[1], ip[2], ip[3]);
DIAG(F("Port:%d"), IP_PORT);
return true;
}
/**
* @brief handle the incomming queue in the loop
*
*/
void inLoop(Queue<int> &in, ObjectPool<csmsg_t, MAXPOOLSIZE> &pool, RingStream *outboundRing)
{
bool state;
if (in.count() > 0)
{
// pop a command index from the incomming queue and get the command from the pool
int idx = in.pop();
csmsg_t *c = pool.getItem(idx, &state);
MQTTInterface::get()->setActive(c->mqsocket); // connection from where we recieved the command is active now
// execute the command and collect results
outboundRing->mark((uint8_t)c->mqsocket);
CommandDistributor::parse(c->mqsocket, (byte *)c->cmd, outboundRing);
outboundRing->commit();
// free the slot in the command pool
pool.returnItem(idx);
}
}
/**
* @brief handle the outgoing messages in the loop
*
*/
void outLoop(PubSubClient *mq)
{
// handle at most 1 outbound transmission
MQTTInterface *mqtt = MQTTInterface::get();
auto clients = mqtt->getClients();
auto outboundRing = mqtt->getRingStream();
int mqSocket = outboundRing->read();
if (mqSocket >= 0) // mqsocket / clientid can't be 0 ....
{
int count = outboundRing->count();
char buffer[MAXTSTR];
buffer[0] = '\0';
sprintf(buffer, "%s/%d/result", mqtt->getClientID(), (int)clients[mqSocket].topic);
if (Diag::MQTT)
DIAG(F("MQTT publish to mqSocket=%d, count=:%d on topic %s"), mqSocket, count, buffer);
if (mq->beginPublish(buffer, count, false))
{
for (; count > 0; count--)
{
mq->write(outboundRing->read());
}
}
else
{
DIAG(F("MQTT error start publishing result)"));
};
if (!mq->endPublish())
{
DIAG(F("MQTT error finalizing published result)"));
};
}
}
/**
* @brief check if there are new subscribers connected and create the channels
*
* @param sq if the callback captured a client there will be an entry in the sq with the subscriber number
* @param clients the clients array where we find the info to setup the subsciptions and print out the publish topics for info
*/
void checkSubscribers(Queue<int> &sq, csmqttclient_t *clients)
{
MQTTInterface *mqtt = MQTTInterface::get();
if (sq.count() > 0)
{
// new subscriber
auto s = sq.pop();
char tbuffer[(CLIENTIDSIZE * 2) + 1 + MAXTSTR];
sprintf(tbuffer, "%s/%ld/cmd", mqtt->getClientID(), clients[s].topic);
auto ok = mqtt->subscribe(tbuffer);
if (Diag::MQTT)
DIAG(F("MQTT new subscriber topic: %s %s"), tbuffer, ok ? "OK" : "NOK");
// send the topic on which the CS will listen for commands and the ones on which it will publish for the connecting
// client to pickup. Once the connecting client has setup other topic setup messages on the main channel shall be
// ignored
// JSON message { init: <number> channels: {result: <string>, diag: <string> }}
char buffer[MAXPAYLOAD * 2];
memset(buffer, 0, MAXPAYLOAD * 2);
// sprintf(buffer, "mc(%d,%ld)", (int)clients[s].distant, clients[s].topic);
sprintf(buffer, "{ \"init\": %d, \"subscribeto\": {\"result\": \"%s/%ld/result\" , \"diag\": \"%s/%ld/diag\" }, \"publishto\": {\"cmd\": \"%s/%ld/cmd\" } }",
(int)clients[s].distant,
mqtt->getClientID(),
clients[s].topic,
mqtt->getClientID(),
clients[s].topic,
mqtt->getClientID(),
clients[s].topic);
if (Diag::MQTT)
DIAG(F("MQTT channel setup message: [%s]"), buffer);
mqtt->publish(mqtt->getClientID(), buffer);
// on the cs side all is set and we declare that the cs is open for business
clients[s].open = true;
}
}
void MQTTInterface::loop()
{
if (!singleton)
return;
singleton->loop2();
}
bool showonce = false;
auto s = millis();
void loopPing(int interval)
{
auto c = millis();
if (c - s > 2000)
{
DIAG(F("loop alive")); // ping every 2 sec
s = c;
}
}
void MQTTInterface::loop2()
{
// loopPing(2000); // ping every 2 sec
// Connection impossible so just don't do anything
if (singleton->mqState == CONNECTION_FAILED)
{
if (!showonce)
{
DIAG(F("MQTT connection failed..."));
showonce = true;
}
return;
}
if (!mqttClient->connected())
{
DIAG(F("MQTT no connection trying to reconnect ..."));
connect();
}
if (!mqttClient->loop())
{
DIAG(F("mqttClient returned with error; state: %d"), mqttClient->state());
return;
};
checkSubscribers(subscriberQueue, clients);
inLoop(in, pool, outboundRing);
outLoop(mqttClient);
}

225
MQTTInterface.h Normal file
View File

@@ -0,0 +1,225 @@
/*
* © 2021, Gregor Baues, All rights reserved.
*
* This file is part of DCC-EX/CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*
*/
#ifndef _MQTTInterface_h_
#define _MQTTInterface_h_
#if __has_include("config.h")
#include "config.h"
#else
#warning config.h not found. Using defaults from config.example.h
#include "config.example.h"
#endif
#include "defines.h"
#include <Arduino.h>
#include <Ethernet.h>
#include <Dns.h>
#include <PubSubClient.h>
#include "DCCEXParser.h"
#include "Queue.h"
#include "ObjectPool.h"
// #include "MemoryFree.h"
#include "freeMemory.h"
#define MAXPAYLOAD 64 // max length of a payload recieved
#define MAXDOMAINLENGTH 32 // domain name length for the broker e.g. test.mosquitto.org
#define MAXTBUF 64 //!< max length of the buffer for building the topic name ;to be checked
#define MAXTMSG 64 //!< max length of the messages for a topic ;to be checked PROGMEM ?
#define MAXTSTR 32 //!< max length of a topic string
#define MAXCONNECTID 32 // broker connection id length incl possible prefixes
#define CLIENTIDSIZE 6 // max length of the clientid used for connection to the broker
#define MAXRECONNECT 5 // reconnection tries before final failure
#define MAXMQTTCONNECTIONS 20 // maximum number of unique tpoics available for subscribers
#define OUT_BOUND_SIZE 128 // Size of the RingStream used to provide results from the parser and publish
#define MAX_POOL_SIZE 16 // recieved command store size
#define MAX_CALLBACKS 10
// extern int freeMemory();
struct MQTTBroker
{
int port;
IPAddress ip;
const FSH *domain = nullptr;
const FSH *user = nullptr;
const FSH *pwd = nullptr;
const FSH *prefix = nullptr;
byte cType; // connection type to identify valid params
IPAddress resovleBroker(const FSH *d)
{
DNSClient dns;
IPAddress bip;
char domain[MAXDOMAINLENGTH];
strcpy_P(domain, (const char *)d);
dns.begin(Ethernet.dnsServerIP());
if (dns.getHostByName(domain, bip) == 1)
{
DIAG(F("[%d] MQTT Broker: %s = %d.%d.%d.%d"), freeMemory(), domain, bip[0], bip[1], bip[2], bip[3]);
}
else
{
DIAG(F("MQTT Dns lookup for %s failed"), domain);
}
return bip;
}
// all boils down to the ip address type = 1 without user authentication 2 with user authentication
// no ssl support !
// port & ip address
MQTTBroker(int p, IPAddress i) : port(p), ip(i), cType(1){};
// port & domain name
MQTTBroker(int p, const FSH *d) : port(p), domain(d), cType(1)
{
ip = resovleBroker(d);
};
// port & ip & prefix
MQTTBroker(int p, IPAddress i, const FSH *pfix) : port(p), ip(i), prefix(pfix), cType(1){};
// port & domain & prefix
MQTTBroker(int p, const FSH *d, const FSH *pfix) : port(p), domain(d), prefix(pfix), cType(1)
{
ip = resovleBroker(d);
};
// port & ip & user & pwd
MQTTBroker(int p, IPAddress i, const FSH *uid, const FSH *pass) : port(p), ip(i), user(uid), pwd(pass), cType(2){};
// port & domain & user & pwd
MQTTBroker(int p, const FSH *d, const FSH *uid, const FSH *pass) : port(p), domain(d), user(uid), pwd(pass), cType(2)
{
ip = resovleBroker(d);
};
// port & ip & user & pwd & prefix
MQTTBroker(int p, IPAddress i, const FSH *uid, const FSH *pass, const FSH *pfix) : port(p), ip(i), user(uid), pwd(pass), prefix(pfix), cType(2){};
// port & domain & user & pwd & prefix
MQTTBroker(int p, const FSH *d, const FSH *uid, const FSH *pass, const FSH *pfix) : port(p), domain(d), user(uid), pwd(pass), prefix(pfix), cType(2)
{
ip = resovleBroker(d);
};
};
/**
* @brief dcc-ex command as recieved via MQ
*
*/
typedef struct csmsg_t
{
char cmd[MAXPAYLOAD]; // recieved command message
byte mqsocket; // from which mqsocket / subscriberid
} csmsg_t;
typedef struct csmqttclient_t
{
int distant; // random int number recieved from the subscriber
byte mqsocket; // mqtt socket = subscriberid provided by the cs
long topic; // cantor(subscriber,cs) encoded tpoic used to send / recieve commands
bool open; // true as soon as we have send the id to the mq broker for the client to pickup
} csmqttclient_t;
enum MQTTInterfaceState
{
INIT,
CONFIGURED, // server/client objects set
CONNECTED, // mqtt broker is connected
CONNECTION_FAILED // Impossible to get the connection set after MAXRECONNECT tries
};
class MQTTInterface
{
private:
// Methods
MQTTInterface();
MQTTInterface(const MQTTInterface &); // non construction-copyable
MQTTInterface &operator=(const MQTTInterface &); // non copyable
void setup(const FSH *id, MQTTBroker *broker); // instantiates the broker
void connect(); // (re)connects to the broker
bool setupNetwork(); // sets up the network connection for the PubSub system
void loop2();
// Members
static MQTTInterface *singleton; // unique instance of the MQTTInterface object
EthernetClient ethClient; // TCP Client object for the MQ Connection
byte mac[6]; // simulated mac address
IPAddress server; // MQTT server object
MQTTBroker *broker; // Broker configuration object as set in config.h
ObjectPool<csmsg_t, MAXPOOLSIZE> pool; // Pool of commands recieved for the CS
Queue<int> in; // Queue of indexes into the pool according to incomming cmds
Queue<int> subscriberQueue; // Queue for incomming subscribers; push the subscriber into the queue for setup in a loop cycle
char clientID[(CLIENTIDSIZE * 2) + 1]; // unique ID of the commandstation; not to confused with the connectionID
csmqttclient_t clients[MAXMQTTCONNECTIONS]; // array of connected mqtt clients
char connectID[MAXCONNECTID]; // clientId plus possible prefix if required by the broker
byte subscriberid = 0; // id assigned to a mqtt client when recieving the inital handshake; +1 at each connection
byte activeSubscriber = 0; // if its 0 no active Subscriber; set as soon as we recieve a command of go into processing on the CS
bool connected = false; // set to true if the ethernet connection is available
MQTTInterfaceState mqState = INIT; // Status of the MQBroker connection
RingStream *outboundRing; // Buffer for collecting the results from the command parser
PubSubClient *mqttClient; // PubSub Endpoint for data exchange
public:
static MQTTInterface *get() noexcept { return singleton;}
boolean subscribe(const char *topic);
void publish(const char *topic, const char *payload);
ObjectPool<csmsg_t, MAXPOOLSIZE> *getPool() { return &pool; };
Queue<int> *getIncomming() { return &in; };
Queue<int> *getSubscriptionQueue() { return &subscriberQueue; };
MQTTInterfaceState getState() { return mqState; };
byte getActive() { return activeSubscriber; };
void setActive(byte mqSocket) { activeSubscriber = mqSocket; };
char *getClientID() { return clientID; };
uint8_t getClientSize() { return subscriberid; };
// initalized to 0 so that the first id comming back is 1
// index 0 in the clients array is not used therefore
uint8_t obtainSubscriberID()
{
if (subscriberid == MAXMQTTCONNECTIONS)
{
return 0; // no more subscriber id available
}
return (++subscriberid);
}
csmqttclient_t *getClients() { return clients; };
RingStream *getRingStream() { return outboundRing; };
static void setup();
static void loop();
~MQTTInterface() = default;
};
#endif

View File

@@ -1,12 +1,7 @@
/*
* © 2022-2023 Paul M Antoine
* © 2021 Mike S
* © 2021 Fred Decker
* © 2020-2023 Harald Barth
* © 2020-2021 Chris Harlow
* All rights reserved.
* © 2020, Chris Harlow. All rights reserved.
*
* This file is part of CommandStation-EX
* This file is part of Asbelos DCC API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -23,71 +18,32 @@
*/
#include <Arduino.h>
#include "MotorDriver.h"
#include "DCCWaveform.h"
#include "DCCTimer.h"
#include "DIAG.h"
#define setHIGH(fastpin) *fastpin.inout |= fastpin.maskHIGH
#define setLOW(fastpin) *fastpin.inout &= fastpin.maskLOW
#define isHIGH(fastpin) (*fastpin.inout & fastpin.maskHIGH)
#define isLOW(fastpin) (!isHIGH(fastpin))
bool MotorDriver::usePWM=false;
bool MotorDriver::commonFaultPin=false;
volatile portreg_t shadowPORTA;
volatile portreg_t shadowPORTB;
volatile portreg_t shadowPORTC;
MotorDriver::MotorDriver(int16_t power_pin, byte signal_pin, byte signal_pin2, int8_t brake_pin,
byte current_pin, float sense_factor, unsigned int trip_milliamps, int8_t fault_pin) {
MotorDriver::MotorDriver(byte power_pin, byte signal_pin, byte signal_pin2, int8_t brake_pin,
byte current_pin, float sense_factor, unsigned int trip_milliamps, byte fault_pin) {
powerPin=power_pin;
invertPower=power_pin < 0;
if (invertPower) {
powerPin = 0-power_pin;
IODevice::write(powerPin,HIGH);// set to OUTPUT and off
} else {
powerPin = power_pin;
IODevice::write(powerPin,LOW);// set to OUTPUT and off
}
getFastPin(F("POWER"),powerPin,fastPowerPin);
pinMode(powerPin, OUTPUT);
signalPin=signal_pin;
getFastPin(F("SIG"),signalPin,fastSignalPin);
pinMode(signalPin, OUTPUT);
fastSignalPin.shadowinout = NULL;
if (HAVE_PORTA(fastSignalPin.inout == &PORTA)) {
DIAG(F("Found PORTA pin %d"),signalPin);
fastSignalPin.shadowinout = fastSignalPin.inout;
fastSignalPin.inout = &shadowPORTA;
}
if (HAVE_PORTB(fastSignalPin.inout == &PORTB)) {
DIAG(F("Found PORTB pin %d"),signalPin);
fastSignalPin.shadowinout = fastSignalPin.inout;
fastSignalPin.inout = &shadowPORTB;
}
if (HAVE_PORTC(fastSignalPin.inout == &PORTC)) {
DIAG(F("Found PORTC pin %d"),signalPin);
fastSignalPin.shadowinout = fastSignalPin.inout;
fastSignalPin.inout = &shadowPORTC;
}
signalPin2=signal_pin2;
if (signalPin2!=UNUSED_PIN) {
dualSignal=true;
getFastPin(F("SIG2"),signalPin2,fastSignalPin2);
pinMode(signalPin2, OUTPUT);
fastSignalPin2.shadowinout = NULL;
if (HAVE_PORTA(fastSignalPin2.inout == &PORTA)) {
DIAG(F("Found PORTA pin %d"),signalPin2);
fastSignalPin2.shadowinout = fastSignalPin2.inout;
fastSignalPin2.inout = &shadowPORTA;
}
if (HAVE_PORTB(fastSignalPin2.inout == &PORTB)) {
DIAG(F("Found PORTB pin %d"),signalPin2);
fastSignalPin2.shadowinout = fastSignalPin2.inout;
fastSignalPin2.inout = &shadowPORTB;
}
if (HAVE_PORTC(fastSignalPin2.inout == &PORTC)) {
DIAG(F("Found PORTC pin %d"),signalPin2);
fastSignalPin2.shadowinout = fastSignalPin2.inout;
fastSignalPin2.inout = &shadowPORTC;
}
}
else dualSignal=false;
@@ -96,58 +52,32 @@ MotorDriver::MotorDriver(int16_t power_pin, byte signal_pin, byte signal_pin2, i
invertBrake=brake_pin < 0;
brakePin=invertBrake ? 0-brake_pin : brake_pin;
getFastPin(F("BRAKE"),brakePin,fastBrakePin);
// if brake is used for railcom cutout we need to do PORTX register trick here as well
pinMode(brakePin, OUTPUT);
setBrake(true); // start with brake on in case we hace DC stuff going on
setBrake(false);
}
else brakePin=UNUSED_PIN;
currentPin=current_pin;
if (currentPin!=UNUSED_PIN) ADCee::init(currentPin);
senseOffset=0; // value can not be obtained until waveform is activated
if (currentPin!=UNUSED_PIN) {
pinMode(currentPin, INPUT);
senseOffset=analogRead(currentPin); // value of sensor at zero current
}
faultPin=fault_pin;
if (faultPin != UNUSED_PIN) {
invertFault=fault_pin < 0;
faultPin=invertFault ? 0-fault_pin : fault_pin;
DIAG(F("Fault pin = %d invert %d"), faultPin, invertFault);
getFastPin(F("FAULT"),faultPin, 1 /*input*/, fastFaultPin);
pinMode(faultPin, INPUT);
}
// This conversion performed at compile time so the remainder of the code never needs
// float calculations or libraray code.
senseFactorInternal=sense_factor * senseScale;
senseFactor=sense_factor;
tripMilliamps=trip_milliamps;
rawCurrentTripValue=mA2raw(trip_milliamps);
if (rawCurrentTripValue + senseOffset > ADCee::ADCmax()) {
// This would mean that the values obtained from the ADC never
// can reach the trip value. So independent of the current, the
// short circuit protection would never trip. So we adjust the
// trip value so that it is tiggered when the ADC reports it's
// maximum value instead.
// DIAG(F("Changing short detection value from %d to %d mA"),
// raw2mA(rawCurrentTripValue), raw2mA(ADCee::ADCmax()-senseOffset));
rawCurrentTripValue=ADCee::ADCmax()-senseOffset;
}
rawCurrentTripValue=(int)(trip_milliamps / sense_factor);
if (currentPin==UNUSED_PIN)
DIAG(F("** WARNING ** No current or short detection"));
else {
DIAG(F("Track %c, TripValue=%d"), trackLetter, rawCurrentTripValue);
// self testing diagnostic for the non-float converters... may be removed when happy
// DIAG(F("senseFactorInternal=%d raw2mA(1000)=%d mA2Raw(1000)=%d"),
// senseFactorInternal, raw2mA(1000),mA2raw(1000));
}
// prepare values for current detection
sampleDelay = 0;
lastSampleTaken = millis();
progTripValue = mA2raw(TRIP_CURRENT_PROG);
DIAG(F("MotorDriver ** WARNING ** No current or short detection"));
else
DIAG(F("MotorDriver currentPin=A%d, senseOffset=%d, rawCurentTripValue(relative to offset)=%d"),
currentPin-A0, senseOffset,rawCurrentTripValue);
}
bool MotorDriver::isPWMCapable() {
@@ -155,23 +85,15 @@ bool MotorDriver::isPWMCapable() {
}
void MotorDriver::setPower(POWERMODE mode) {
bool on=mode==POWERMODE::ON;
void MotorDriver::setPower(bool on) {
if (on) {
// when switching a track On, we need to check the crrentOffset with the pin OFF
if (powerMode==POWERMODE::OFF && currentPin!=UNUSED_PIN) {
senseOffset = ADCee::read(currentPin);
DIAG(F("Track %c sensOffset=%d"),trackLetter,senseOffset);
}
IODevice::write(powerPin,invertPower ? LOW : HIGH);
if (isProgTrack)
DCCWaveform::progTrack.clearResets();
// toggle brake before turning power on - resets overcurrent error
// on the Pololu board if brake is wired to ^D2.
setBrake(true);
setBrake(false);
setHIGH(fastPowerPin);
}
else {
IODevice::write(powerPin,invertPower ? HIGH : LOW);
}
powerMode=mode;
else setLOW(fastPowerPin);
}
// setBrake applies brake if on == true. So to get
@@ -182,171 +104,80 @@ void MotorDriver::setPower(POWERMODE mode) {
// (HIGH == release brake) and setBrake does
// compensate for that.
//
void MotorDriver::setBrake(bool on, bool interruptContext) {
void MotorDriver::setBrake(bool on) {
if (brakePin == UNUSED_PIN) return;
if (!interruptContext) {noInterrupts();}
if (on ^ invertBrake)
setHIGH(fastBrakePin);
else
setLOW(fastBrakePin);
if (!interruptContext) {interrupts();}
if (on ^ invertBrake) setHIGH(fastBrakePin);
else setLOW(fastBrakePin);
}
void MotorDriver::setSignal( bool high) {
if (usePWM) {
DCCTimer::setPWM(signalPin,high);
}
else {
if (high) {
setHIGH(fastSignalPin);
if (dualSignal) setLOW(fastSignalPin2);
}
else {
setLOW(fastSignalPin);
if (dualSignal) setHIGH(fastSignalPin2);
}
}
}
#if defined(ARDUINO_TEENSY32) || defined(ARDUINO_TEENSY35)|| defined(ARDUINO_TEENSY36)
volatile unsigned int overflow_count=0;
#endif
bool MotorDriver::canMeasureCurrent() {
return currentPin!=UNUSED_PIN;
}
/*
* Return the current reading as pin reading 0 to 1023. If the fault
* pin is activated return a negative current to show active fault pin.
* As there is no -0, cheat a little and return -1 in that case.
* As there is no -0, create a little and return -1 in that case.
*
* senseOffset handles the case where a shield returns values above or below
* a central value depending on direction.
*
* Bool fromISR should be adjusted dependent how function is called
*/
int MotorDriver::getCurrentRaw(bool fromISR) {
(void)fromISR;
int MotorDriver::getCurrentRaw() {
if (currentPin==UNUSED_PIN) return 0;
int current;
current = ADCee::read(currentPin, fromISR);
// here one can diag raw value
// if (fromISR == false) DIAG(F("%c: %d"), trackLetter, current);
current = current-senseOffset; // adjust with offset
#if defined(ARDUINO_TEENSY40) || defined(ARDUINO_TEENSY41)
bool irq = disableInterrupts();
current = analogRead(currentPin)-senseOffset;
enableInterrupts(irq);
#elif defined(ARDUINO_TEENSY32) || defined(ARDUINO_TEENSY35)|| defined(ARDUINO_TEENSY36)
unsigned char sreg_backup;
sreg_backup = SREG; /* save interrupt enable/disable state */
cli();
current = analogRead(currentPin)-senseOffset;
overflow_count = 0;
SREG = sreg_backup; /* restore interrupt state */
#else
current = analogRead(currentPin)-senseOffset;
#endif
if (current<0) current=0-current;
if ((faultPin != UNUSED_PIN) && powerMode==POWERMODE::ON) {
if (invertFault && isLOW(fastFaultPin))
if ((faultPin != UNUSED_PIN) && isLOW(fastFaultPin) && isHIGH(fastPowerPin))
return (current == 0 ? -1 : -current);
if (!invertFault && !isLOW(fastFaultPin))
return (current == 0 ? -1 : -current);
}
return current;
}
#ifdef ANALOG_READ_INTERRUPT
/*
* This should only be called in interrupt context
* Copies current value from HW to cached value in
* Motordriver.
*/
#pragma GCC push_options
#pragma GCC optimize ("-O3")
bool MotorDriver::sampleCurrentFromHW() {
byte low, high;
//if (!bit_is_set(ADCSRA, ADIF))
if (bit_is_set(ADCSRA, ADSC))
return false;
// if ((ADMUX & mask) != (currentPin - A0))
// return false;
low = ADCL; //must read low before high
high = ADCH;
bitSet(ADCSRA, ADIF);
sampleCurrent = (high << 8) | low;
sampleCurrentTimestamp = millis();
return true;
}
void MotorDriver::startCurrentFromHW() {
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
const byte mask = 7;
#else
const byte mask = 31;
#endif
ADMUX=(1<<REFS0)|((currentPin-A0) & mask); //select AVCC as reference and set MUX
bitSet(ADCSRA,ADSC); // start conversion
}
#pragma GCC pop_options
#endif //ANALOG_READ_INTERRUPT
#if defined(ARDUINO_ARCH_ESP32)
uint16_t taurustones[28] = { 165, 175, 196, 220,
247, 262, 294, 330,
349, 392, 440, 494,
523, 587, 659, 698,
494, 440, 392, 249,
330, 284, 262, 247,
220, 196, 175, 165 };
#endif
void MotorDriver::setDCSignal(byte speedcode) {
if (brakePin == UNUSED_PIN)
return;
#if defined(ARDUINO_AVR_UNO)
TCCR2B = (TCCR2B & B11111000) | B00000110; // set divisor on timer 2 to result in (approx) 122.55Hz
#endif
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
TCCR2B = (TCCR2B & B11111000) | B00000110; // set divisor on timer 2 to result in (approx) 122.55Hz
TCCR4B = (TCCR4B & B11111000) | B00000100; // same for timer 4 but maxcount and thus divisor differs
#endif
// spedcoode is a dcc speed & direction
byte tSpeed=speedcode & 0x7F; // DCC Speed with 0,1 stop and speed steps 2 to 127
byte tDir=speedcode & 0x80;
byte brake;
#if defined(ARDUINO_ARCH_ESP32)
{
int f = 131;
if (tSpeed > 2) {
if (tSpeed <= 58) {
f = taurustones[ (tSpeed-2)/2 ] ;
}
}
DCCTimer::DCCEXanalogWriteFrequency(brakePin, f); // set DC PWM frequency to 100Hz XXX May move to setup
}
#endif
if (tSpeed <= 1) brake = 255;
else if (tSpeed >= 127) brake = 0;
else brake = 2 * (128-tSpeed);
if (invertBrake)
brake=255-brake;
#if defined(ARDUINO_ARCH_ESP32)
DCCTimer::DCCEXanalogWrite(brakePin,brake);
#else
analogWrite(brakePin,brake);
#endif
//DIAG(F("DCSignal %d"), speedcode);
if (HAVE_PORTA(fastSignalPin.shadowinout == &PORTA)) {
noInterrupts();
HAVE_PORTA(shadowPORTA=PORTA);
setSignal(tDir);
HAVE_PORTA(PORTA=shadowPORTA);
interrupts();
} else if (HAVE_PORTB(fastSignalPin.shadowinout == &PORTB)) {
noInterrupts();
HAVE_PORTB(shadowPORTB=PORTB);
setSignal(tDir);
HAVE_PORTB(PORTB=shadowPORTB);
interrupts();
} else if (HAVE_PORTC(fastSignalPin.shadowinout == &PORTC)) {
noInterrupts();
HAVE_PORTC(shadowPORTC=PORTC);
setSignal(tDir);
HAVE_PORTC(PORTC=shadowPORTC);
interrupts();
} else {
noInterrupts();
setSignal(tDir);
interrupts();
}
// IMPORTANT: This function can be called in Interrupt() time within the 56uS timer
// The default analogRead takes ~100uS which is catastrphic
// so DCCTimer has set the sample time to be much faster.
}
unsigned int MotorDriver::raw2mA( int raw) {
//DIAG(F("%d = %d * %d / %d"), (int32_t)raw * senseFactorInternal / senseScale, raw, senseFactorInternal, senseScale);
return (int32_t)raw * senseFactorInternal / senseScale;
return (unsigned int)(raw * senseFactor);
}
unsigned int MotorDriver::mA2raw( unsigned int mA) {
//DIAG(F("%d = %d * %d / %d"), (int32_t)mA * senseScale / senseFactorInternal, mA, senseScale, senseFactorInternal);
return (int32_t)mA * senseScale / senseFactorInternal;
int MotorDriver::mA2raw( unsigned int mA) {
return (int)(mA / senseFactor);
}
void MotorDriver::getFastPin(const FSH* type,int pin, bool input, FASTPIN & result) {
// DIAG(F("MotorDriver %S Pin=%d,"),type,pin);
(void) type; // avoid compiler warning if diag not used above.
#if defined(ARDUINO_ARCH_SAMD)
PortGroup *port = digitalPinToPort(pin);
#elif defined(ARDUINO_ARCH_STM32)
GPIO_TypeDef *port = digitalPinToPort(pin);
#else
(void) type; // avoid compiler warning if diag not used above.
uint8_t port = digitalPinToPort(pin);
#endif
if (input)
result.inout = portInputRegister(port);
else
@@ -355,65 +186,3 @@ void MotorDriver::getFastPin(const FSH* type,int pin, bool input, FASTPIN & res
result.maskLOW = ~result.maskHIGH;
// DIAG(F(" port=0x%x, inoutpin=0x%x, isinput=%d, mask=0x%x"),port, result.inout,input,result.maskHIGH);
}
void MotorDriver::checkPowerOverload(bool useProgLimit, byte trackno) {
if (millis() - lastSampleTaken < sampleDelay) return;
lastSampleTaken = millis();
int tripValue= useProgLimit?progTripValue:getRawCurrentTripValue();
// Trackname for diag messages later
switch (powerMode) {
case POWERMODE::OFF:
sampleDelay = POWER_SAMPLE_OFF_WAIT;
break;
case POWERMODE::ON:
// Check current
lastCurrent=getCurrentRaw();
if (lastCurrent < 0) {
// We have a fault pin condition to take care of
lastCurrent = -lastCurrent;
setPower(POWERMODE::OVERLOAD); // Turn off, decide later how fast to turn on again
if (commonFaultPin) {
if (lastCurrent < tripValue) {
setPower(POWERMODE::ON); // maybe other track
}
// Write this after the fact as we want to turn on as fast as possible
// because we don't know which output actually triggered the fault pin
DIAG(F("COMMON FAULT PIN ACTIVE: POWERTOGGLE TRACK %c"), trackno + 'A');
} else {
DIAG(F("TRACK %c FAULT PIN ACTIVE - OVERLOAD"), trackno + 'A');
if (lastCurrent < tripValue) {
lastCurrent = tripValue; // exaggerate
}
}
}
if (lastCurrent < tripValue) {
sampleDelay = POWER_SAMPLE_ON_WAIT;
if(power_good_counter<100)
power_good_counter++;
else
if (power_sample_overload_wait>POWER_SAMPLE_OVERLOAD_WAIT) power_sample_overload_wait=POWER_SAMPLE_OVERLOAD_WAIT;
} else {
setPower(POWERMODE::OVERLOAD);
unsigned int mA=raw2mA(lastCurrent);
unsigned int maxmA=raw2mA(tripValue);
power_good_counter=0;
sampleDelay = power_sample_overload_wait;
DIAG(F("TRACK %c POWER OVERLOAD %dmA (limit %dmA) shutdown for %dms"), trackno + 'A', mA, maxmA, sampleDelay);
if (power_sample_overload_wait >= 10000)
power_sample_overload_wait = 10000;
else
power_sample_overload_wait *= 2;
}
break;
case POWERMODE::OVERLOAD:
// Try setting it back on after the OVERLOAD_WAIT
setPower(POWERMODE::ON);
sampleDelay = POWER_SAMPLE_ON_WAIT;
// Debug code....
DIAG(F("TRACK %c POWER RESTORE (check %dms)"), trackno + 'A', sampleDelay);
break;
default:
sampleDelay = 999; // cant get here..meaningless statement to avoid compiler warning.
}
}

View File

@@ -1,12 +1,7 @@
/*
* © 2022 Paul M Antoine
* © 2021 Mike S
* © 2021 Fred Decker
* © 2020 Chris Harlow
* © 2022 Harald Barth
* All rights reserved.
* © 2020, Chris Harlow. All rights reserved.
*
* This file is part of CommandStation-EX
* This file is part of Asbelos DCC API
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -24,52 +19,6 @@
#ifndef MotorDriver_h
#define MotorDriver_h
#include "FSH.h"
#include "IODevice.h"
#include "DCCTimer.h"
#define setHIGH(fastpin) *fastpin.inout |= fastpin.maskHIGH
#define setLOW(fastpin) *fastpin.inout &= fastpin.maskLOW
#define isHIGH(fastpin) (*fastpin.inout & fastpin.maskHIGH)
#define isLOW(fastpin) (!isHIGH(fastpin))
#define TOKENPASTE(x, y) x ## y
#define TOKENPASTE2(x, y) TOKENPASTE(x, y)
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
#define HAVE_PORTA(X) X
#define HAVE_PORTB(X) X
#define HAVE_PORTC(X) X
#endif
#if defined(ARDUINO_AVR_UNO)
#define HAVE_PORTB(X) X
#endif
#if defined(ARDUINO_ARCH_SAMD)
#define PORTA REG_PORT_OUT0
#define HAVE_PORTA(X) X
#define PORTB REG_PORT_OUT1
#define HAVE_PORTB(X) X
#endif
#if defined(ARDUINO_ARCH_STM32)
#define PORTA GPIOA->ODR
#define HAVE_PORTA(X) X
#define PORTB GPIOB->ODR
#define HAVE_PORTB(X) X
#define PORTC GPIOC->ODR
#define HAVE_PORTC(X) X
#endif
// if macros not defined as pass-through we define
// them here as someting that is valid as a
// statement and evaluates to false.
#ifndef HAVE_PORTA
#define HAVE_PORTA(X) byte TOKENPASTE2(Unique_, __LINE__) __attribute__((unused)) =0
#endif
#ifndef HAVE_PORTB
#define HAVE_PORTB(X) byte TOKENPASTE2(Unique_, __LINE__) __attribute__((unused)) =0
#endif
#ifndef HAVE_PORTC
#define HAVE_PORTC(X) byte TOKENPASTE2(Unique_, __LINE__) __attribute__((unused)) =0
#endif
// Virtualised Motor shield 1-track hardware Interface
@@ -77,166 +26,63 @@
#define UNUSED_PIN 127 // inside int8_t
#endif
class pinpair {
public:
pinpair(byte p1, byte p2) {
pin = p1;
invpin = p2;
};
byte pin = UNUSED_PIN;
byte invpin = UNUSED_PIN;
};
#if defined(__IMXRT1062__) || defined(ARDUINO_ARCH_ESP8266) || defined(ARDUINO_ARCH_ESP32) || defined(ARDUINO_ARCH_SAMD) || defined(ARDUINO_ARCH_STM32)
typedef uint32_t portreg_t;
#else
typedef uint8_t portreg_t;
#endif
#if defined(__IMXRT1062__)
struct FASTPIN {
volatile portreg_t *inout;
portreg_t maskHIGH;
portreg_t maskLOW;
volatile portreg_t *shadowinout;
volatile uint32_t *inout;
uint32_t maskHIGH;
uint32_t maskLOW;
};
// The port registers that are shadowing
// the real port registers. These are
// defined in Motordriver.cpp
extern volatile portreg_t shadowPORTA;
extern volatile portreg_t shadowPORTB;
extern volatile portreg_t shadowPORTC;
enum class POWERMODE : byte { OFF, ON, OVERLOAD };
#else
struct FASTPIN {
volatile uint8_t *inout;
uint8_t maskHIGH;
uint8_t maskLOW;
};
#endif
class MotorDriver {
public:
MotorDriver(int16_t power_pin, byte signal_pin, byte signal_pin2, int8_t brake_pin,
byte current_pin, float senseFactor, unsigned int tripMilliamps, int8_t fault_pin);
void setPower( POWERMODE mode);
POWERMODE getPower() { return powerMode;}
// as the port registers can be shadowed to get syncronized DCC signals
// we need to take care of that and we have to turn off interrupts if
// we setSignal() or setBrake() or setPower() during that time as
// otherwise the call from interrupt context can undo whatever we do
// from outside interrupt
void setBrake( bool on, bool interruptContext=false);
__attribute__((always_inline)) inline void setSignal( bool high) {
if (trackPWM) {
DCCTimer::setPWM(signalPin,high);
}
else {
if (high) {
setHIGH(fastSignalPin);
if (dualSignal) setLOW(fastSignalPin2);
}
else {
setLOW(fastSignalPin);
if (dualSignal) setHIGH(fastSignalPin2);
}
}
};
inline void enableSignal(bool on) {
if (on)
pinMode(signalPin, OUTPUT);
else
pinMode(signalPin, INPUT);
};
inline pinpair getSignalPin() { return pinpair(signalPin,signalPin2); };
void setDCSignal(byte speedByte);
inline void detachDCSignal() {
#if defined(__arm__)
pinMode(brakePin, OUTPUT);
#elif defined(ARDUINO_ARCH_ESP32)
ledcDetachPin(brakePin);
#else
setDCSignal(128);
#endif
};
int getCurrentRaw(bool fromISR=false);
unsigned int raw2mA( int raw);
unsigned int mA2raw( unsigned int mA);
inline bool brakeCanPWM() {
#if defined(ARDUINO_ARCH_ESP32) || defined(__arm__)
// TODO: on ARM we can use digitalPinHasPWM, and may wish/need to
return true;
#else
#ifdef digitalPinToTimer
return ((brakePin!=UNUSED_PIN) && (digitalPinToTimer(brakePin)));
#else
return (brakePin<14 && brakePin >1);
#endif //digitalPinToTimer
#endif //ESP32/ARM
}
MotorDriver(byte power_pin, byte signal_pin, byte signal_pin2, int8_t brake_pin,
byte current_pin, float senseFactor, unsigned int tripMilliamps, byte faultPin);
virtual void setPower( bool on);
virtual void setSignal( bool high);
virtual void setBrake( bool on);
virtual int getCurrentRaw();
virtual unsigned int raw2mA( int raw);
virtual int mA2raw( unsigned int mA);
inline int getRawCurrentTripValue() {
return rawCurrentTripValue;
}
bool isPWMCapable();
bool canMeasureCurrent();
bool trackPWM = false; // this track uses PWM timer to generate the DCC waveform
static bool usePWM;
static bool commonFaultPin; // This is a stupid motor shield which has only a common fault pin for both outputs
inline byte getFaultPin() {
return faultPin;
}
inline void makeProgTrack(bool on) { // let this output know it's a prog track.
isProgTrack = on;
}
void checkPowerOverload(bool useProgLimit, byte trackno);
inline void setTrackLetter(char c) {
trackLetter = c;
};
#ifdef ANALOG_READ_INTERRUPT
bool sampleCurrentFromHW();
void startCurrentFromHW();
#endif
private:
char trackLetter = '?';
bool isProgTrack = false; // tells us if this is a prog track
void getFastPin(const FSH* type,int pin, bool input, FASTPIN & result);
void getFastPin(const FSH* type,int pin, FASTPIN & result) {
getFastPin(type, pin, 0, result);
}
VPIN powerPin;
byte signalPin, signalPin2, currentPin, faultPin, brakePin;
FASTPIN fastSignalPin, fastSignalPin2, fastBrakePin,fastFaultPin;
byte powerPin, signalPin, signalPin2, currentPin, faultPin, brakePin;
FASTPIN fastPowerPin,fastSignalPin, fastSignalPin2, fastBrakePin,fastFaultPin;
bool dualSignal; // true to use signalPin2
bool invertBrake; // brake pin passed as negative means pin is inverted
bool invertPower; // power pin passed as negative means pin is inverted
bool invertFault; // fault pin passed as negative means pin is inverted
// Raw to milliamp conversion factors avoiding float data types.
// Milliamps=rawADCreading * sensefactorInternal / senseScale
//
// senseScale is chosen as 256 to give enough scale for 2 decimal place
// raw->mA conversion with an ultra fast optimised integer multiplication
int senseFactorInternal; // set to senseFactor * senseScale
static const int senseScale=256;
float senseFactor;
int senseOffset;
unsigned int tripMilliamps;
int rawCurrentTripValue;
// current sampling
POWERMODE powerMode;
unsigned long lastSampleTaken;
unsigned int sampleDelay;
int progTripValue;
int lastCurrent;
#ifdef ANALOG_READ_INTERRUPT
volatile unsigned long sampleCurrentTimestamp;
volatile uint16_t sampleCurrent;
#if defined(ARDUINO_TEENSY40) || defined(ARDUINO_TEENSY41)
static bool disableInterrupts() {
uint32_t primask;
__asm__ volatile("mrs %0, primask\n" : "=r" (primask)::);
__disable_irq();
return (primask == 0) ? true : false;
}
static void enableInterrupts(bool doit) {
if (doit) __enable_irq();
}
#endif
int maxmA;
int tripmA;
// Wait times for power management. Unit: milliseconds
static const int POWER_SAMPLE_ON_WAIT = 100;
static const int POWER_SAMPLE_OFF_WAIT = 1000;
static const int POWER_SAMPLE_OVERLOAD_WAIT = 20;
// Trip current for programming track, 250mA. Change only if you really
// need to be non-NMRA-compliant because of decoders that are not either.
static const int TRIP_CURRENT_PROG=250;
unsigned long power_sample_overload_wait = POWER_SAMPLE_OVERLOAD_WAIT;
unsigned int power_good_counter = 0;
};
#endif

View File

@@ -1,27 +1,3 @@
/*
* © 2022-2023 Paul M. Antoine
* © 2021 Fred Decker
* © 2020-2023 Harald Barth
* (c) 2020 Chris Harlow. All rights reserved.
* (c) 2021 Fred Decker. All rights reserved.
* (c) 2020 Harald Barth. All rights reserved.
* (c) 2020 Anthony W - Dayton. All rights reserved.
*
* This file is part of CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*/
#ifndef MotorDrivers_h
#define MotorDrivers_h
#include <Arduino.h>
@@ -39,75 +15,17 @@
#define UNUSED_PIN 127 // inside int8_t
#endif
// The MotorDriver definition is:
//
// MotorDriver(byte power_pin, byte signal_pin, byte signal_pin2, int8_t brake_pin, byte current_pin,
// float senseFactor, unsigned int tripMilliamps, byte faultPin);
//
// power_pin: Turns the board on/off. Often called ENABLE or PWM on the shield
// signal_pin: Where the DCC signal goes in. Often called DIR on the shield
// signal_pin2: Inverse of signal_pin. A few shields need this as well, can be replace by hardware inverter
// brake_pin: When tuned on, brake is set - output shortened (*)
// current_pin: Current sense voltage pin from shield to ADC
// senseFactor: Relation between volts on current_pin and actual output current
// tripMilliamps: Short circuit trip limit in milliampere, max 32767 (32.767A)
// faultPin: Some shields have a pin to to report a fault condition to the uCPU. High when fault occurs
//
// (*) If the brake_pin is negative that means the sense
// If the brakePin is negative that means the sense
// of the brake pin on the motor bridge is inverted
// (HIGH == release brake)
// Arduino STANDARD Motor Shield, used on different architectures:
#if defined(ARDUINO_ARCH_SAMD) || defined(ARDUINO_ARCH_STM32)
// Standard Motor Shield definition for 3v3 processors (other than the ESP32)
// Setup for SAMD21 Sparkfun DEV board MUST use Arduino Motor Shield R3 (MUST be R3
// for 3v3 compatibility!!) senseFactor for 3.3v systems is 1.95 as calculated when using
// 10-bit A/D samples, and for 12-bit samples it's more like 0.488, but we probably need
// to tweak both these
//
// Arduino standard Motor Shield
#define STANDARD_MOTOR_SHIELD F("STANDARD_MOTOR_SHIELD"), \
new MotorDriver(3, 12, UNUSED_PIN, 9, A0, 0.488, 1500, UNUSED_PIN), \
new MotorDriver(11, 13, UNUSED_PIN, 8, A1, 0.488, 1500, UNUSED_PIN)
#define SAMD_STANDARD_MOTOR_SHIELD STANDARD_MOTOR_SHIELD
#define STM32_STANDARD_MOTOR_SHIELD STANDARD_MOTOR_SHIELD
// EX 8874 based shield connected to a 3V3 system with 12-bit (4096) ADC
#define EX8874_SHIELD F("EX8874"), \
new MotorDriver( 3, 12, UNUSED_PIN, 9, A0, 1.27, 5000, -A4), \
new MotorDriver(11, 13, UNUSED_PIN, 8, A1, 1.27, 5000, -A5)
#elif defined(ARDUINO_ARCH_ESP32)
// STANDARD shield on an ESPDUINO-32 (ESP32 in Uno form factor). The shield must be eiter the
// 3.3V compatible R3 version or it has to be modified to not supply more than 3.3V to the
// analog inputs. Here we use analog inputs A2 and A3 as A0 and A1 are wired in a way so that
// they are not useable at the same time as WiFi (what a bummer). The numbers below are the
// actual GPIO numbers. In comments the numbers the pins have on an Uno.
#define STANDARD_MOTOR_SHIELD F("STANDARD_MOTOR_SHIELD"), \
new MotorDriver(25/* 3*/, 19/*12*/, UNUSED_PIN, 13/*9*/, 35/*A2*/, 0.70, 1500, UNUSED_PIN), \
new MotorDriver(23/*11*/, 18/*13*/, UNUSED_PIN, 12/*8*/, 34/*A3*/, 0.70, 1500, UNUSED_PIN)
// EX 8874 based shield connected to a 3.3V system (like ESP32) and 12bit (4096) ADC
// numbers are GPIO numbers. comments are UNO form factor shield pin numbers
#define EX8874_SHIELD F("EX8874"),\
new MotorDriver(25/* 3*/, 19/*12*/, UNUSED_PIN, 13/*9*/, 35/*A2*/, 1.27, 5000, -36 /*-A4*/), \
new MotorDriver(23/*11*/, 18/*13*/, UNUSED_PIN, 12/*8*/, 34/*A3*/, 1.27, 5000, -39 /*-A5*/)
#else
// STANDARD shield on any Arduino Uno or Mega compatible with the original specification.
#define STANDARD_MOTOR_SHIELD F("STANDARD_MOTOR_SHIELD"), \
new MotorDriver(3, 12, UNUSED_PIN, 9, A0, 2.99, 1500, UNUSED_PIN), \
new MotorDriver(11, 13, UNUSED_PIN, 8, A1, 2.99, 1500, UNUSED_PIN)
#define BRAKE_PWM_SWAPPED_MOTOR_SHIELD F("BPS_MOTOR_SHIELD"), \
new MotorDriver(-9 , 12, UNUSED_PIN, -3, A0, 2.99, 1500, UNUSED_PIN), \
new MotorDriver(-8 , 13, UNUSED_PIN,-11, A1, 2.99, 1500, UNUSED_PIN)
// EX 8874 based shield connected to a 5V system (like Arduino) and 10bit (1024) ADC
#define EX8874_SHIELD F("EX8874"), \
new MotorDriver( 3, 12, UNUSED_PIN, 9, A0, 5.08, 5000, -A4), \
new MotorDriver(11, 13, UNUSED_PIN, 8, A1, 5.08, 5000, -A5)
#endif
new MotorDriver(3, 12, UNUSED_PIN, UNUSED_PIN, A0, 2.99, 2000, UNUSED_PIN), \
new MotorDriver(11, 13, UNUSED_PIN, UNUSED_PIN, A2, 2.99, 2000, UNUSED_PIN)
// Pololu Motor Shield
#define POLOLU_MOTOR_SHIELD F("POLOLU_MOTOR_SHIELD"), \
@@ -124,17 +42,6 @@
// new MotorDriver(2, 8, UNUSED_PIN, -10, A1, 18, 3000, 12)
// See Pololu dial_mc33926_shield_schematic.pdf and truth table on page 17 of the MC33926 data sheet.
// Pololu Dual TB9051FTG Motor Shield
// This is the shield without modifications. Unfortunately the TB9051FTG driver chip on
// the shield makes short delays when direction is switched. That means that the chip
// can NOT provide a standard conformant DCC signal independent how hard we try. If your
// Decoders tolerate that signal, use it by all mean but it is not recommended. Without
// modifications it uses the following pins below which means no HA waveform and no
// RailCom on an Arduino Mega 2560 but the DCC signal is broken anyway.
#define POLOLU_TB9051FTG F("POLOLU_TB9051FTG"), \
new MotorDriver(2, 7, UNUSED_PIN, -9, A0, 10, 2500, 6), \
new MotorDriver(4, 8, UNUSED_PIN, -10, A1, 10, 2500, 12)
// Firebox Mk1
#define FIREBOX_MK1 F("FIREBOX_MK1"), \
new MotorDriver(3, 6, 7, UNUSED_PIN, A5, 9.766, 5500, UNUSED_PIN), \
@@ -147,61 +54,12 @@
// FunduMoto Motor Shield
#define FUNDUMOTO_SHIELD F("FUNDUMOTO_SHIELD"), \
new MotorDriver(10, 12, UNUSED_PIN, UNUSED_PIN, A0, 2.99, 1500, UNUSED_PIN), \
new MotorDriver(11, 13, UNUSED_PIN, UNUSED_PIN, A1, 2.99, 1500, UNUSED_PIN)
new MotorDriver(10, 12, UNUSED_PIN, 9, A0, 2.99, 2000, UNUSED_PIN), \
new MotorDriver(11, 13, UNUSED_PIN, UNUSED_PIN, A1, 2.99, 2000, UNUSED_PIN)
// IBT_2 Motor Board for Main and Arduino Motor Shield for Prog
#define IBT_2_WITH_ARDUINO F("IBT_2_WITH_ARDUINO_SHIELD"), \
new MotorDriver(4, 5, 6, UNUSED_PIN, A5, 41.54, 5000, UNUSED_PIN), \
new MotorDriver(11, 13, UNUSED_PIN, UNUSED_PIN, A1, 2.99, 1500, UNUSED_PIN)
// YFROBOT Motor Shield (V3.1)
#define YFROBOT_MOTOR_SHIELD F("YFROBOT_MOTOR_SHIELD"), \
new MotorDriver(5, 4, UNUSED_PIN, UNUSED_PIN, A0, 2.99, 1500, UNUSED_PIN), \
new MotorDriver(6, 7, UNUSED_PIN, UNUSED_PIN, A1, 2.99, 1500, UNUSED_PIN)
new MotorDriver(11, 13, UNUSED_PIN, UNUSED_PIN, A1, 2.99, 2000, UNUSED_PIN)
// Makeblock ORION UNO like sized board with integrated motor driver
// This is like an Uno with H-bridge and RJ12 contacts instead of pin rows.
// No current sense. Barrel connector max 12V, Vmotor max 15V. 1.1A polyfuse as output protection.
// Main is marked M1 and near RJ12 #5
// Prog is marked M2 and near RJ12 #4
// For details see
// http://docs.makeblock.com/diy-platform/en/electronic-modules/main-control-boards/makeblock-orion.html
#define ORION_UNO_INTEGRATED_SHIELD F("ORION_UNO_INTEGRATED_SHIELD"), \
new MotorDriver(6, 7, UNUSED_PIN, UNUSED_PIN, UNUSED_PIN, 1.0, 1100, UNUSED_PIN), \
new MotorDriver(5, 4, UNUSED_PIN, UNUSED_PIN, UNUSED_PIN, 1.0, 1100, UNUSED_PIN)
// This is an example how to setup a motor shield definition for a motor shield connected
// to an NANO EVERY board. You have to make the connectons from the shield to the board
// as in this example or adjust the values yourself.
#define NANOEVERY_EXAMPLE F("NANOEVERY_EXAMPLE"), \
new MotorDriver(5, 6, UNUSED_PIN, UNUSED_PIN, A0, 2.99, 1500, UNUSED_PIN),\
new MotorDriver(9, 10, UNUSED_PIN, UNUSED_PIN, A1, 2.99, 1500, UNUSED_PIN)
// This is an example how to stack two standard motor shields. The upper shield
// needs pins 3 8 9 11 12 13 A0 A1 disconnected from the lower shield and
// jumpered instead like this: 2-3 6-8 7-9 4-13 5-11 10-12 A0-A4 A1-A5
// Pin assigment table:
// 2 Enable C jumpered
// 3 Enable A direct
// 4 Dir D jumpered
// 5 Enable D jumpered
// 6 Brake D jumpered
// 7 Brake C jumpered
// 8 Brake B direct
// 9 Brake A direct
// 10 Dir C jumpered
// 11 Enable B direct
// 12 Dir A direct
// 13 Dir B direct
// A0 Sense A direct
// A1 Sense B direct
// A4 Sense C jumpered
// A5 Sense D jumpered
//
#define STACKED_MOTOR_SHIELD F("STACKED_MOTOR_SHIELD"),\
new MotorDriver( 3, 12, UNUSED_PIN, 9, A0, 2.99, 1500, UNUSED_PIN), \
new MotorDriver(11, 13, UNUSED_PIN, 8, A1, 2.99, 1500, UNUSED_PIN), \
new MotorDriver( 2, 10, UNUSED_PIN, 7, A4, 2.99, 1500, UNUSED_PIN), \
new MotorDriver( 5, 4, UNUSED_PIN, 6, A5, 2.99, 1500, UNUSED_PIN)
//
#endif

110
ObjectPool.h Normal file
View File

@@ -0,0 +1,110 @@
/*
* © 2021, Gregor Baues, All rights reserved.
*
* This file is part of DCC-EX/CommandStation-EX
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* It is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
*
*/
#ifndef _ObjectPool_h_
#define _ObjectPool_h_
#include <DIAG.h>
#define MAXPOOLSIZE 32
template <typename T, int length>
class ObjectPool
{
// just make sure that we don't create a pool eating up all memory @compiletime
static_assert(length <= MAXPOOLSIZE);
struct item
{
T i;
bool free = true; // boolean 1 free i.e. i can be reused; 0 occupied
};
private:
item p[length]; // MAXPOOLSIZE items of struct item
const int size = length; // size of the pool
int findFreeIdx()
{ // find the first free index or return -1 if there is none
for (int i = 0; i < length; i++)
{
if (p[i].free)
{
return i;
}
}
return -1; // if we are here there is no free slot available
}
public:
int setItem(T i)
{ // add an item to the pool at a free slot
int idx = findFreeIdx();
if (idx != -1)
{
p[idx].i = i;
p[idx].free = false;
}
return idx;
}
/**
* @brief returns the slot for an object to the pool i.e. frees the slot for reuse of the data member and
* clears out the memory
*
* @param idx
* @return true if the return is ok
* @return false otherwise
*/
bool returnItem(int idx)
{ // clear item at pool index idx
if (idx > size)
{ // can't return an item outside of the pool size; returns false;
return false;
}
memset(&p[idx].i, 0, sizeof(T)); // clear out the memory but keep the allocation for reuse
p[idx].free = true;
return true; // set the free flag
}
/**
* @brief Obtain a pool item
* @note This should only be used for debugging.
* It allows to change actually the content of the pool item where this should only be allowed for the setItem method.
* @param idx Index of the pool item to retrieve
* @param state State of the pool item ( 1 available, 0 occupied)
* @return T* returns the pointer to the pool item
*/
T *getItem(int idx, bool *state)
{
*state = p[idx].free;
return &p[idx].i;
}
int getSize()
{
return size;
}
ObjectPool() = default;
~ObjectPool() = default;
};
#endif

View File

@@ -1,9 +1,5 @@
/*
* © 2021 Neil McKechnie
* © 2021 Harald Barth
* © 2020-2021 Fred Decker
* © 2020-2021 Chris Harlow
* All rights reserved.
* © 2020, Chris Harlow. All rights reserved.
*
* This file is part of Asbelos DCC API
*
@@ -86,50 +82,32 @@ the state of any outputs being monitored or controlled by a separate interface o
**********************************************************************/
#include "Outputs.h"
#ifndef DISABLE_EEPROM
#include "EEStore.h"
#endif
#include "StringFormatter.h"
#include "IODevice.h"
///////////////////////////////////////////////////////////////////////////////
// Static function to print all output states to stream in the form "<Y id state>"
// print all output states to stream
void Output::printAll(Print *stream){
for (Output *tt = Output::firstOutput; tt != NULL; tt = tt->nextOutput)
StringFormatter::send(stream, F("<Y %d %d>\n"), tt->data.id, tt->data.active);
StringFormatter::send(stream, F("<Y %d %d>\n"), tt->data.id, tt->data.oStatus);
} // Output::printAll
///////////////////////////////////////////////////////////////////////////////
// Object method to activate / deactivate the Output state.
void Output::activate(uint16_t s){
s = (s>0); // Make 0 or 1
data.active = s; // if s>0, set status to active, else inactive
// set state of output pin to HIGH or LOW depending on whether bit zero of iFlag is set to 0 (ACTIVE=HIGH) or 1 (ACTIVE=LOW)
IODevice::write(data.pin, s ^ data.invert);
#ifndef DISABLE_EEPROM
// Update EEPROM if output has been stored.
if(EEStore::eeStore->data.nOutputs > 0 && num > 0)
EEPROM.put(num, data.oStatus);
#endif
void Output::activate(int s){
data.oStatus=(s>0); // if s>0, set status to active, else inactive
digitalWrite(data.pin,data.oStatus ^ bitRead(data.iFlag,0)); // set state of output pin to HIGH or LOW depending on whether bit zero of iFlag is set to 0 (ACTIVE=HIGH) or 1 (ACTIVE=LOW)
if(num>0)
EEPROM.put(num,data.oStatus);
}
///////////////////////////////////////////////////////////////////////////////
// Static function to locate Output object specified by ID 'n'.
// Return NULL if not found.
Output* Output::get(uint16_t n){
Output* Output::get(int n){
Output *tt;
for(tt=firstOutput;tt!=NULL && tt->data.id!=n;tt=tt->nextOutput);
return(tt);
}
///////////////////////////////////////////////////////////////////////////////
// Static function to delete Output object specified by ID 'n'.
// Return false if not found.
bool Output::remove(uint16_t n){
bool Output::remove(int n){
Output *tt,*pp=NULL;
for(tt=firstOutput;tt!=NULL && tt->data.id!=n;pp=tt,tt=tt->nextOutput);
@@ -147,26 +125,23 @@ bool Output::remove(uint16_t n){
}
///////////////////////////////////////////////////////////////////////////////
// Static function to load configuration and state of all Outputs from EEPROM
#ifndef DISABLE_EEPROM
void Output::load(){
struct OutputData data;
Output *tt;
for(uint16_t i=0;i<EEStore::eeStore->data.nOutputs;i++){
for(int i=0;i<EEStore::eeStore->data.nOutputs;i++){
EEPROM.get(EEStore::pointer(),data);
// Create new object, set current state to default or to saved state from eeprom.
tt=create(data.id, data.pin, data.flags);
uint8_t state = data.setDefault ? data.defaultValue : data.active;
tt->activate(state);
if (tt) tt->num=EEStore::pointer() + offsetof(OutputData, oStatus); // Save pointer to flags within EEPROM
tt=create(data.id,data.pin,data.iFlag);
tt->data.oStatus=bitRead(tt->data.iFlag,1)?bitRead(tt->data.iFlag,2):data.oStatus; // restore status to EEPROM value is bit 1 of iFlag=0, otherwise set to value of bit 2 of iFlag
digitalWrite(tt->data.pin,tt->data.oStatus ^ bitRead(tt->data.iFlag,0));
pinMode(tt->data.pin,OUTPUT);
tt->num=EEStore::pointer();
EEStore::advance(sizeof(tt->data));
}
}
///////////////////////////////////////////////////////////////////////////////
// Static function to store configuration and state of all Outputs to EEPROM
void Output::store(){
Output *tt;
@@ -175,26 +150,19 @@ void Output::store(){
EEStore::eeStore->data.nOutputs=0;
while(tt!=NULL){
tt->num=EEStore::pointer();
EEPROM.put(EEStore::pointer(),tt->data);
tt->num=EEStore::pointer() + offsetof(OutputData, oStatus); // Save pointer to flags within EEPROM
EEStore::advance(sizeof(tt->data));
tt=tt->nextOutput;
EEStore::eeStore->data.nOutputs++;
}
}
#endif
///////////////////////////////////////////////////////////////////////////////
// Static function to create an Output object
// The obscurely named parameter 'v' is 0 if called from the load() function
// and 1 if called from the <Z> command processing.
Output *Output::create(uint16_t id, VPIN pin, int iFlag, int v){
Output *Output::create(int id, int pin, int iFlag, int v){
Output *tt;
if (pin > VPIN_MAX) return NULL;
if(firstOutput==NULL){
firstOutput=(Output *)calloc(1,sizeof(Output));
tt=firstOutput;
@@ -207,21 +175,20 @@ Output *Output::create(uint16_t id, VPIN pin, int iFlag, int v){
}
if(tt==NULL) return tt;
tt->num = 0; // make sure new object doesn't get written to EEPROM until store() command
tt->data.id=id;
tt->data.pin=pin;
tt->data.flags=iFlag;
tt->data.iFlag=iFlag;
tt->data.oStatus=0;
if(v==1){
// sets status to 0 (INACTIVE) is bit 1 of iFlag=0, otherwise set to value of bit 2 of iFlag
if (tt->data.setDefault)
tt->data.active = tt->data.defaultValue;
else
tt->data.active = 0;
tt->data.oStatus=bitRead(tt->data.iFlag,1)?bitRead(tt->data.iFlag,2):0; // sets status to 0 (INACTIVE) is bit 1 of iFlag=0, otherwise set to value of bit 2 of iFlag
digitalWrite(tt->data.pin,tt->data.oStatus ^ bitRead(tt->data.iFlag,0));
pinMode(tt->data.pin,OUTPUT);
}
IODevice::write(tt->data.pin, tt->data.active ^ tt->data.invert);
return(tt);
}
///////////////////////////////////////////////////////////////////////////////

View File

@@ -1,8 +1,5 @@
/*
* © 2021 Harald Barth
* © 2021 Fred Decker
* © 2020 Chris Harlow
* All rights reserved.
* © 2020, Chris Harlow. All rights reserved.
*
* This file is part of Asbelos DCC API
*
@@ -23,45 +20,28 @@
#define Outputs_h
#include <Arduino.h>
#include "IODevice.h"
struct OutputData {
union {
uint8_t oStatus; // (Bit 0=Invert, Bit 1=Set state to default, Bit 2=default state, Bit 7=active)
struct {
unsigned int flags : 7; // Bit 0=Invert, Bit 1=Set state to default, Bit 2=default state
unsigned int : 1;
};
struct {
unsigned int invert : 1;
unsigned int setDefault : 1;
unsigned int defaultValue : 1;
unsigned int: 4;
unsigned int active : 1;
};
};
uint16_t id;
VPIN pin;
uint8_t oStatus;
uint8_t id;
uint8_t pin;
uint8_t iFlag;
};
class Output{
public:
void activate(uint16_t s);
bool isActive();
static Output* get(uint16_t);
static bool remove(uint16_t);
#ifndef DISABLE_EEPROM
public:
void activate(int s);
static Output* get(int);
static bool remove(int);
static void load();
static void store();
#endif
static Output *create(uint16_t, VPIN, int, int=0);
static Output *create(int, int, int, int=0);
static Output *firstOutput;
struct OutputData data;
Output *nextOutput;
static void printAll(Print *);
private:
uint16_t num; // EEPROM address of oStatus in OutputData struct, or zero if not stored.
private:
int num; // Chris has no idea what this is all about!
}; // Output

97
PWMServoDriver.cpp Normal file
View File

@@ -0,0 +1,97 @@
/*!
* @file PWMServoDriver.cpp
*
* @mainpage Adafruit 16-channel PWM & Servo driver, based on Adafruit_PWMServoDriver
*
* @section intro_sec Introduction
*
* This is a library for the 16-channel PWM & Servo driver.
*
* Designed specifically to work with the Adafruit PWM & Servo driver.
* This class contains a very small subset of the Adafruit version which
* is relevant to driving simple servos at 50Hz through a number of chained
* servo driver boards (ie servos 0-15 on board 0x40, 16-31 on board 0x41 etc.)
*
* @section author Author
* Chris Harlow (TPL)
* original by Limor Fried/Ladyada (Adafruit Industries).
*
* @section license License
*
* BSD license, all text above must be included in any redistribution
*/
#include <Arduino.h>
#include "PWMServoDriver.h"
#include "DIAG.h"
#include "I2CManager.h"
// REGISTER ADDRESSES
const byte PCA9685_MODE1=0x00; // Mode Register
const byte PCA9685_FIRST_SERVO=0x06; /** low byte first servo register ON*/
const byte PCA9685_PRESCALE=0xFE; /** Prescale register for PWM output frequency */
// MODE1 bits
const byte MODE1_SLEEP=0x10; /**< Low power mode. Oscillator off */
const byte MODE1_AI=0x20; /**< Auto-Increment enabled */
const byte MODE1_RESTART=0x80; /**< Restart enabled */
const byte PCA9685_I2C_ADDRESS=0x40; /** First PCA9685 I2C Slave Address */
const float FREQUENCY_OSCILLATOR=25000000.0; /** Accurate enough for our purposes */
const uint8_t PRESCALE_50HZ = (uint8_t)(((FREQUENCY_OSCILLATOR / (50.0 * 4096.0)) + 0.5) - 1);
const uint32_t MAX_I2C_SPEED = 1000000L; // PCA9685 rated up to 1MHz I2C clock speed
/*!
* @brief Sets the PWM frequency for a chip to 50Hz for servos
*/
byte PWMServoDriver::setupFlags=0; // boards that have been initialised
byte PWMServoDriver::failFlags=0; // boards that have faild initialisation
bool PWMServoDriver::setup(int board) {
if (board>3 || (failFlags & (1<<board))) return false;
if (setupFlags & (1<<board)) return true;
I2CManager.begin();
I2CManager.setClock(MAX_I2C_SPEED);
uint8_t i2caddr=PCA9685_I2C_ADDRESS + board;
// Test if device is available
byte error = I2CManager.checkAddress(i2caddr);
if (error) {
DIAG(F("I2C Servo device 0x%x Not Found %d"),i2caddr, error);
failFlags|=1<<board;
return false;
}
//DIAG(F("PWMServoDriver::setup %x prescale=%d"),i2caddr,PRESCALE_50HZ);
writeRegister(i2caddr,PCA9685_MODE1, MODE1_SLEEP | MODE1_AI);
writeRegister(i2caddr,PCA9685_PRESCALE, PRESCALE_50HZ);
writeRegister(i2caddr,PCA9685_MODE1,MODE1_AI);
writeRegister(i2caddr,PCA9685_MODE1, MODE1_RESTART | MODE1_AI);
setupFlags|=1<<board;
return true;
}
/*!
* @brief Sets the PWM output to a servo
*/
void PWMServoDriver::setServo(byte servoNum, uint16_t value) {
int board=servoNum/16;
int pin=servoNum%16;
if (setup(board)) {
DIAG(F("SetServo %d %d"),servoNum,value);
uint8_t buffer[] = {(uint8_t)(PCA9685_FIRST_SERVO + 4 * pin), // 4 registers per pin
0, 0, (uint8_t)(value & 0xff), (uint8_t)(value >> 8)};
if (value == 4095) buffer[2] = 0x10; // Full on
byte error=I2CManager.write(PCA9685_I2C_ADDRESS + board, buffer, sizeof(buffer));
if (error!=0) DIAG(F("SetServo error %d"),error);
}
}
void PWMServoDriver::writeRegister(uint8_t i2caddr,uint8_t hardwareRegister, uint8_t d) {
I2CManager.write(i2caddr, 2, hardwareRegister, d);
}

21
PWMServoDriver.h Normal file
View File

@@ -0,0 +1,21 @@
/*!
* @file PWMServoDriver.h
*
* Used to set servo positions on an I2C bus with 1 or more PCA96685 boards.
*/
#ifndef PWMServoDriver_H
#define PWMServoDriver_H
class PWMServoDriver {
public:
static void setServo(byte servoNum, uint16_t pos);
private:
static byte setupFlags;
static byte failFlags;
static bool setup(int board);
static void writeRegister(uint8_t i2caddr,uint8_t hardwareRegister, uint8_t d);
};
#endif

Some files were not shown because too many files have changed in this diff Show More