1
0
mirror of https://github.com/DCC-EX/CommandStation-EX.git synced 2024-11-22 23:56:13 +01:00

Merge pull request #312 from DCC-EX/devel-nmck

Merge Devel-nmck:
I2C Multiplexer support through Extended Addresses added for Wire, 4209 and AVR I2C drivers.
I2C retries when an operation fails.
I2C timeout handling and recovery completed.
I2C SAMD Driver Read code completed.
PCF8575 I2C GPIO driver added.
EX-RAIL ANOUT function for triggering analogue HAL drivers (e.g. analogue outputs, DFPlayer, PWM).
EX-RAIL SCREEN function for writing to screens other than the primary one.
Installable HALDisplay Driver, with support for multiple displays.
Layered HAL Drivers PCA9685pwm and Servo added for native PWM on PCA9685 module and for animations of servo movement via PCA9685pwm. This is intended to support EXIOExpander and also replace the existing PCA9685 driver.
Add <D HAL RESET> to reinitialise failed drivers (it calls the _begin method of all HAL drivers to reinitialise them).
Add UserAddin facility to allow a user-written C++ function to be declared in myHal.cpp, to be called at a user-specified frequency.
Add ability to configure clock speed of PCA9685 drivers (to allow flicker-free LED control).
Improve stability of VL53L0X driver when XSHUT pin connected.
Enable DCC high accuracy mode for STM32 on standard motor shield (pins D12/D13).
Incorporate improvements to ADC scanning performance (courtesy of HABA).
This commit is contained in:
Neil McKechnie 2023-02-23 11:20:17 +00:00 committed by GitHub
commit 39d953ed29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
68 changed files with 4738 additions and 1364 deletions

View File

@ -48,7 +48,7 @@ template<typename... Targs> void CommandDistributor::broadcastReply(clientType t
// 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(&Serial, msg...);
StringFormatter::send(&USB_SERIAL, msg...);
}
#endif

View File

@ -49,6 +49,7 @@
*/
#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
@ -74,11 +75,11 @@ void setup()
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));
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
@ -160,7 +161,8 @@ void loop()
LCN::loop();
#endif
LCDDisplay::loop(); // ignored if LCD not in use
// Display refresh
DisplayInterface::loop();
// Handle/update IO devices.
IODevice::loop();

View File

@ -40,7 +40,7 @@
#if ETHERNET_ON == true
#include "EthernetInterface.h"
#endif
#include "LCD_Implementation.h"
#include "Display_Implementation.h"
#include "LCN.h"
#include "IODevice.h"
#include "Turnouts.h"

View File

@ -190,9 +190,9 @@ void DCCEXParser::setAtCommandCallback(AT_COMMAND_CALLBACK callback)
// Parse an F() string
void DCCEXParser::parse(const FSH * cmd) {
DIAG(F("SETUP(\"%S\")"),cmd);
int size=strlen_P((char *)cmd)+1;
int size=STRLEN_P((char *)cmd)+1;
char buffer[size];
strcpy_P(buffer,(char *)cmd);
STRCPY_P(buffer,(char *)cmd);
parse(&USB_SERIAL,(byte *)buffer,NULL);
}
@ -944,10 +944,12 @@ bool DCCEXParser::parseD(Print *stream, int16_t params, int16_t p[])
DIAG(F("VPIN=%d value=%d"), p[1], IODevice::readAnalogue(p[1]));
break;
#if !defined(IO_MINIMAL_HAL)
#if !defined(IO_NO_HAL)
case HASH_KEYWORD_HAL:
if (p[1] == HASH_KEYWORD_SHOW)
IODevice::DumpAll();
else if (p[1] == HASH_KEYWORD_RESET)
IODevice::reset();
break;
#endif

View File

@ -1,5 +1,5 @@
/*
* © 2022-2023 Paul M. Antoine
* © 2022 Paul M. Antoine
* © 2021 Mike S
* © 2021-2022 Harald Barth
* © 2021 Fred Decker
@ -102,14 +102,9 @@ private:
// that an offset can be initialized.
class ADCee {
public:
// begin is called for any setup that must be done before
// **init** can be called. On some architectures this involves ADC
// initialisation and clock routing, sampling times etc.
static void begin();
// init adds the pin to the list of scanned pins (if this
// init does add the pin to the list of scanned pins (if this
// platform's implementation scans pins) and returns the first
// read value (which is why it required begin to have been called first!)
// It must be called before the regular scan is started.
// 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
@ -118,6 +113,9 @@ public:
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.
@ -126,6 +124,8 @@ private:
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;
};

View File

@ -123,13 +123,14 @@ void DCCTimer::reset() {
}
#if defined(ARDUINO_AVR_MEGA) || defined(ARDUINO_AVR_MEGA2560)
#define NUM_ADC_INPUTS 15
#define NUM_ADC_INPUTS 16
#else
#define NUM_ADC_INPUTS 7
#define NUM_ADC_INPUTS 8
#endif
uint16_t ADCee::usedpins = 0;
int * ADCee::analogvals = NULL;
bool ADCusesHighPort = false;
byte *ADCee::idarr = NULL;
static bool ADCusesHighPort = false;
/*
* Register a new pin to be scanned
@ -138,16 +139,28 @@ bool ADCusesHighPort = false;
*/
int ADCee::init(uint8_t pin) {
uint8_t id = pin - A0;
if (id > NUM_ADC_INPUTS)
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+1, sizeof(int));
analogvals[id] = value;
usedpins |= (1<<id);
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() {
@ -159,11 +172,13 @@ int16_t ADCee::ADCmax() {
int ADCee::read(uint8_t pin, bool fromISR) {
(void)fromISR; // AVR does ignore this arg
uint8_t id = pin - A0;
if ((usedpins & (1<<id) ) == 0)
return -1023;
int a;
// we do not need to check (analogvals == NULL)
// because usedpins would still be 0 in that case
return analogvals[id];
noInterrupts();
a = analogvals[id];
interrupts();
return a;
}
/*
* Scan function that is called from interrupt
@ -171,8 +186,7 @@ int ADCee::read(uint8_t pin, bool fromISR) {
#pragma GCC push_options
#pragma GCC optimize ("-O3")
void ADCee::scan() {
static byte 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 byte num = 0; // index into id array
static bool waiting = false;
if (waiting) {
@ -184,45 +198,26 @@ void ADCee::scan() {
low = ADCL; //must read low before high
high = ADCH;
bitSet(ADCSRA, ADIF);
analogvals[id] = (high << 8) | low;
// advance at least one track
// for scope debug TrackManager::track[1]->setBrake(0);
analogvals[idarr[num]] = (high << 8) | low;
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
// 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 (id > 7) // if we use a high ADC pin
bitSet(ADCSRB, MUX5); // set MUX5 bit
else
bitClear(ADCSRB, MUX5);
}
#endif
ADMUX=(1<<REFS0)|(id & 0x07); //select AVCC as reference and set MUX
bitSet(ADCSRA,ADSC); // start conversion
// 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;
}
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

View File

@ -1,4 +1,5 @@
/*
* © 2023 Neil McKechnie
* © 2022 Paul M. Antoine
* © 2021 Mike S
* © 2021 Harald Barth
@ -50,13 +51,16 @@ HardwareSerial Serial1(PG9, PG14); // Rx=PG9, Tx=PG14 -- D0, D1 - F412ZG/F446ZE
#endif
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);
// 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);
// Timer IRQ handler
void Timer11_Handler() {
void Timer_Handler() {
interruptHandler();
}
@ -66,31 +70,59 @@ void DCCTimer::begin(INTERRUPT_CALLBACK callback) {
// adc_set_sample_rate(ADC_SAMPLETIME_480CYCLES);
timer.pause();
timerAux.pause();
timer.setPrescaleFactor(1);
// timer.setOverflow(CLOCK_CYCLES * 2);
timer.setOverflow(DCC_SIGNAL_TIME, MICROSEC_FORMAT);
timer.attachInterrupt(Timer11_Handler);
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) {
//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;
// Timer 2 Channel 1 controls pin D13, and Timer3 Channel 1 controls D12.
// Enable the appropriate timer channel.
switch (pin) {
case 12:
timerAux.setMode(1, TIMER_OUTPUT_COMPARE_INACTIVE, D12);
return true;
case 13:
timer.setMode(1, TIMER_OUTPUT_COMPARE_INACTIVE, D13);
return true;
default:
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;
// 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 (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 (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() {
return;
timer.setMode(1, TIMER_OUTPUT_COMPARE_INACTIVE, NC);
timerAux.setMode(1, TIMER_OUTPUT_COMPARE_INACTIVE, NC);
}
void DCCTimer::getSimulatedMacAddress(byte mac[6]) {

1
DIAG.h
View File

@ -24,4 +24,5 @@
#include "StringFormatter.h"
#define DIAG StringFormatter::diag
#define LCD StringFormatter::lcd
#define SCREEN StringFormatter::lcd2
#endif

196
Display.cpp Normal file
View File

@ -0,0 +1,196 @@
/*
* © 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 noMoreRowsToDisplay 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).
numCharacterColumns = _deviceDriver->getNumCols();
numCharacterRows = _deviceDriver->getNumRows();;
for (uint8_t row = 0; row < MAX_CHARACTER_ROWS; row++)
rowBuffer[row][0] = '\0';
topRow = ROW_INITIAL; // loop2 will fill from row 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';
topRow = ROW_INITIAL; // loop2 will fill from row 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 = ROW_INITIAL;
rowNext = ROW_INITIAL;
bufferPointer = 0;
noMoreRowsToDisplay = false;
slot = 0;
}
do {
if (bufferPointer == 0) {
// Find a line of data to write to the screen.
if (rowFirst == ROW_INITIAL) rowFirst = rowNext;
if (findNextNonBlankRow()) {
// Non-blank line found, so copy it (including terminator)
for (uint8_t i = 0; i <= MAX_CHARACTER_COLS; i++)
buffer[i] = rowBuffer[rowNext][i];
} else {
// No non-blank lines left, so draw a blank line
buffer[0] = 0;
}
_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 slot on screen
bufferPointer = 0;
slot++;
if (slot >= numCharacterRows) {
// Last slot on screen written, reset ready for next screen update.
#if SCROLLMODE==2
if (!noMoreRowsToDisplay) {
// On next refresh, restart one row on from previous start.
rowNext = rowFirst;
findNextNonBlankRow();
}
#endif
noMoreRowsToDisplay = false;
slot = 0;
rowFirst = ROW_INITIAL;
lastScrollTime = currentMillis;
return NULL;
}
}
}
} while (force);
return NULL;
}
bool Display::findNextNonBlankRow() {
while (!noMoreRowsToDisplay) {
if (rowNext == ROW_INITIAL)
rowNext = 0;
else
rowNext = rowNext + 1;
if (rowNext >= MAX_CHARACTER_ROWS) rowNext = ROW_INITIAL;
#if SCROLLMODE == 1
// Finished if we've looped back to start
if (rowNext == ROW_INITIAL) {
noMoreRowsToDisplay = true;
return false;
}
#else
// Finished if we're back to the first one shown
if (rowNext == rowFirst) {
noMoreRowsToDisplay = true;
return false;
}
#endif
if (rowBuffer[rowNext][0] != 0) {
// Found non-blank row
return true;
}
}
return false;
}

76
Display.h Normal file
View File

@ -0,0 +1,76 @@
/*
* © 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
static const uint8_t ROW_INITIAL = 255;
private:
DisplayDevice *_deviceDriver;
unsigned long lastScrollTime = 0;
uint8_t hotRow = 0;
uint8_t hotCol = 0;
uint8_t topRow = 0;
uint8_t slot = 0;
uint8_t rowFirst = ROW_INITIAL;
uint8_t rowNext = ROW_INITIAL;
uint8_t charIndex = 0;
char buffer[MAX_CHARACTER_COLS + 1];
char* bufferPointer = 0;
bool noMoreRowsToDisplay = false;
uint16_t numCharacterRows;
uint16_t numCharacterColumns = 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 findNextNonBlankRow();
};
#endif

View File

@ -21,4 +21,7 @@
#include "DisplayInterface.h"
DisplayInterface *DisplayInterface::lcdDisplay = 0;
// Install null display driver initially - will be replaced if required.
DisplayInterface *DisplayInterface::_displayHandler = new DisplayInterface();
uint8_t DisplayInterface::_selectedDisplayNo = 255;

View File

@ -25,13 +25,75 @@
// Definition of base class for displays. The base class does nothing.
class DisplayInterface : public Print {
public:
virtual DisplayInterface* loop2(bool force) { (void)force; return NULL; };
virtual void setRow(byte line) { (void)line; };
virtual void clear() {};
virtual size_t write(uint8_t c) { (void)c; return 0; };
protected:
static DisplayInterface *_displayHandler;
static uint8_t _selectedDisplayNo; // Nothing selected.
DisplayInterface *_nextHandler = NULL;
uint8_t _displayNo = 0;
static DisplayInterface *lcdDisplay;
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

@ -27,27 +27,34 @@
#ifndef LCD_Implementation_h
#define LCD_Implementation_h
#include "LCDDisplay.h"
#include "DisplayInterface.h"
#include "SSD1306Ascii.h"
#include "LiquidCrystal_I2C.h"
// Implement the LCDDisplay shim class as a singleton.
// The DisplayInterface class implements a displayy handler with no code (null device);
// The LCDDisplay class sub-classes DisplayInterface to provide the common display code;
// Then LCDDisplay class is subclassed to the specific device type classes:
// 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 CONDITIONAL_LCD_START for (DisplayInterface * dummy=new SSD1306AsciiWire(OLED_DRIVER);dummy!=NULL; dummy=dummy->loop2(true))
#define DISPLAY_START(xxx) { \
DisplayInterface *t = new Display(new SSD1306AsciiWire(OLED_DRIVER)); \
t->begin(); \
xxx; \
t->refresh(); \
}
#elif defined(LCD_DRIVER)
#define CONDITIONAL_LCD_START for (DisplayInterface * dummy=new LiquidCrystal_I2C(LCD_DRIVER);dummy!=NULL; dummy=dummy->loop2(true))
#define DISPLAY_START(xxx) { \
DisplayInterface *t = new Display(new LiquidCrystal_I2C(LCD_DRIVER)); \
t->begin(); \
xxx; \
t->refresh();}
#else
// Create null display handler just in case someone calls lcdDisplay->something without checking if lcdDisplay is NULL!
#define CONDITIONAL_LCD_START { new DisplayInterface(); }
#endif
#define DISPLAY_START(xxx) {}
#endif
#endif // LCD_Implementation_h

View File

@ -1165,11 +1165,11 @@ void RMFT2::thrungeString(uint32_t strfar, thrunger mode, byte id) {
// Find out where the string is going
switch (mode) {
case thrunge_print:
StringFormatter::send(&Serial,F("<* EXRAIL(%d) "),loco);
stream=&Serial;
StringFormatter::send(&USB_SERIAL,F("<* EXRAIL(%d) "),loco);
stream=&USB_SERIAL;
break;
case thrunge_serial: stream=&Serial; break;
case thrunge_serial: stream=&USB_SERIAL; break;
case thrunge_serial1:
#ifdef SERIAL1_COMMANDS
stream=&Serial1;
@ -1200,7 +1200,6 @@ void RMFT2::thrungeString(uint32_t strfar, thrunger mode, byte id) {
stream=&Serial6;
#endif
break;
// TODO more serials for SAMx case thrunge_serial4: stream=&Serial4; break;
case thrunge_lcn:
#if defined(LCN_SERIAL)
stream=&LCN_SERIAL;
@ -1209,6 +1208,7 @@ void RMFT2::thrungeString(uint32_t strfar, thrunger mode, byte id) {
case thrunge_parse:
case thrunge_broadcast:
case thrunge_lcd:
default: // thrunge_lcd+1, ...
if (!buffer) buffer=new StringBuffer();
buffer->flush();
stream=buffer;
@ -1232,11 +1232,11 @@ void RMFT2::thrungeString(uint32_t strfar, thrunger mode, byte id) {
// and decide what to do next
switch (mode) {
case thrunge_print:
StringFormatter::send(&Serial,F(" *>\n"));
StringFormatter::send(&USB_SERIAL,F(" *>\n"));
break;
// TODO more serials for SAMx case thrunge_serial4: stream=&Serial4; break;
case thrunge_parse:
DCCEXParser::parseOne(&Serial,(byte*)buffer->getString(),NULL);
DCCEXParser::parseOne(&USB_SERIAL,(byte*)buffer->getString(),NULL);
break;
case thrunge_broadcast:
// TODO CommandDistributor::broadcastText(buffer->getString());
@ -1244,7 +1244,9 @@ void RMFT2::thrungeString(uint32_t strfar, thrunger mode, byte id) {
case thrunge_lcd:
LCD(id,F("%s"),buffer->getString());
break;
default: break;
default: // thrunge_lcd+1, ...
if (mode > thrunge_lcd)
SCREEN(mode-thrunge_lcd, id, F("%s"),buffer->getString()); // print to other display
break;
}
}

View File

@ -73,11 +73,15 @@ enum OPCODE : byte {OPCODE_THROW,OPCODE_CLOSE,
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_serial,thrunge_parse,
thrunge_serial1, thrunge_serial2, thrunge_serial3,
thrunge_serial4, thrunge_serial5, thrunge_serial6,
thrunge_lcd, thrunge_lcn};
thrunge_lcn,
thrunge_lcd, // Must be last!!
};

View File

@ -28,6 +28,7 @@
#undef AFTER
#undef ALIAS
#undef AMBER
#undef ANOUT
#undef AT
#undef ATGTE
#undef ATLT
@ -79,6 +80,7 @@
#undef KILLALL
#undef LATCH
#undef LCD
#undef SCREEN
#undef LCN
#undef MOVETT
#undef ONACTIVATE
@ -146,6 +148,7 @@
#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)
@ -197,6 +200,7 @@
#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)

View File

@ -45,7 +45,7 @@
// 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) and LCD(row,msg) is implemented in a separate pass to create
// 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
@ -143,6 +143,15 @@ const int StringMacroTracker1=__COUNTER__;
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;\
}
void RMFT2::printMessage(uint16_t id) {
thrunger tmode;
@ -246,6 +255,7 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = {
#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),
@ -297,6 +307,7 @@ const HIGHFLASH int16_t RMFT2::SignalDefinitions[] = {
#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),
@ -367,6 +378,8 @@ const HIGHFLASH byte RMFT2::RouteCode[] = {
// Restore normal code LCD & SERIAL macro
#undef LCD
#define LCD StringFormatter::lcd
#undef SCREEN
#define SCREEN StringFormatter::lcd2
#undef SERIAL
#define SERIAL 0x0
#endif

11
FSH.h
View File

@ -47,7 +47,11 @@
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
@ -80,5 +84,10 @@ typedef char FSH;
#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
#endif
#endif

View File

@ -1,6 +1,6 @@
/*
* © 2023, Neil McKechnie
* © 2022 Paul M Antoine
* © 2021, Neil McKechnie
* All rights reserved.
*
* This file is part of CommandStation-EX
@ -35,28 +35,120 @@
#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
void I2CManagerClass::begin(void) {
//setTimeout(25000); // 25 millisecond timeout
if (!_beginCompleted) {
_beginCompleted = true;
_initialise();
// Probe and list devices.
#if defined(I2C_USE_WIRE)
DIAG(F("I2CManager: Using Wire library"));
#endif
// Check for short-circuits on I2C
if (!digitalRead(SDA))
DIAG(F("WARNING: Possible short-circuit on I2C SDA line"));
if (!digitalRead(SCL))
DIAG(F("WARNING: Possible short-circuit on I2C SCL line"));
// 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 (byte addr=1; addr<127; addr++) {
for (uint8_t addr=0x08; addr<0x78; addr++) {
if (exists(addr)) {
found = true;
DIAG(F("I2C Device found at x%x"), addr);
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
}
}
@ -65,31 +157,35 @@ 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);
}
_setClock(_clockSpeed);
}
// Force clock speed to that specified. It can then only
// be overridden by calling Wire.setClock directly.
// Force clock speed to that specified.
void I2CManagerClass::forceClock(uint32_t speed) {
if (!_clockSpeedFixed) {
_clockSpeed = speed;
_clockSpeedFixed = true;
_setClock(_clockSpeed);
}
_clockSpeed = speed;
_clockSpeedFixed = true;
_setClock(_clockSpeed);
DIAG(F("I2C clock speed forced to %l Hz"), _clockSpeed);
}
// Check if specified I2C address is responding (blocking operation)
// Returns I2C_STATUS_OK (0) if OK, or error code.
uint8_t I2CManagerClass::checkAddress(uint8_t address) {
return write(address, NULL, 0);
// 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();
}
/***************************************************************************
* Write a transmission to I2C using a list of data (blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::write(uint8_t address, uint8_t nBytes, ...) {
uint8_t I2CManagerClass::write(I2CAddress address, uint8_t nBytes, ...) {
uint8_t buffer[nBytes];
va_list args;
va_start(args, nBytes);
@ -102,7 +198,7 @@ uint8_t I2CManagerClass::write(uint8_t address, uint8_t nBytes, ...) {
/***************************************************************************
* Initiate a write to an I2C device (blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::write(uint8_t i2cAddress, const uint8_t writeBuffer[], uint8_t writeLen) {
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);
@ -111,7 +207,7 @@ uint8_t I2CManagerClass::write(uint8_t i2cAddress, const uint8_t writeBuffer[],
/***************************************************************************
* Initiate a write from PROGMEM (flash) to an I2C device (blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::write_P(uint8_t i2cAddress, const uint8_t * data, uint8_t dataLen) {
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);
@ -120,7 +216,7 @@ uint8_t I2CManagerClass::write_P(uint8_t i2cAddress, const uint8_t * data, uint8
/***************************************************************************
* Initiate a write (optional) followed by a read from the I2C device (blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::read(uint8_t i2cAddress, uint8_t *readBuffer, uint8_t readLen,
uint8_t I2CManagerClass::read(I2CAddress i2cAddress, uint8_t *readBuffer, uint8_t readLen,
const uint8_t *writeBuffer, uint8_t writeLen)
{
I2CRB req;
@ -131,7 +227,7 @@ uint8_t I2CManagerClass::read(uint8_t i2cAddress, uint8_t *readBuffer, uint8_t r
/***************************************************************************
* Overload of read() to allow command to be specified as a series of bytes (blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::read(uint8_t address, uint8_t readBuffer[], uint8_t readSize,
uint8_t I2CManagerClass::read(I2CAddress address, uint8_t readBuffer[], uint8_t readSize,
uint8_t writeSize, ...) {
va_list args;
// Copy the series of bytes into an array.
@ -162,7 +258,7 @@ const FSH *I2CManagerClass::getErrorMessage(uint8_t status) {
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("Timeout");
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");
@ -176,46 +272,40 @@ const FSH *I2CManagerClass::getErrorMessage(uint8_t status) {
***************************************************************************/
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 block to complete, and return completion status.
* Since such a loop could potentially last for ever if the RB status doesn't
* change, we set a high limit (1sec, 1000ms) on the wait time and, if it
* hasn't changed by that time we assume it's not going to, and just return
* a timeout status. This means that CS will not lock up.
* Block waiting for request to complete, and return completion status.
* Timeout monitoring is performed in the I2CManager.loop() function.
***************************************************************************/
uint8_t I2CRB::wait() {
unsigned long waitStart = millis();
do {
while (status==I2C_STATUS_PENDING) {
I2CManager.loop();
// Rather than looping indefinitely, let's set a very high timeout (1s).
if ((millis() - waitStart) > 1000UL) {
DIAG(F("I2C TIMEOUT I2C:x%x I2CRB:x%x"), i2cAddress, this);
status = I2C_STATUS_TIMEOUT;
// Note that, although the timeout is posted, the request may yet complete.
// TODO: Ideally we would like to cancel the request.
return status;
}
} while (status==I2C_STATUS_PENDING);
};
return status;
}
/***************************************************************************
* Check whether request is still in progress.
* Timeout monitoring is performed in the I2CManager.loop() function.
***************************************************************************/
bool I2CRB::isBusy() {
I2CManager.loop();
return (status==I2C_STATUS_PENDING);
if (status==I2C_STATUS_PENDING) {
I2CManager.loop();
return true;
} else
return false;
}
/***************************************************************************
* Helper functions to fill the I2CRequest structure with parameters.
***************************************************************************/
void I2CRB::setReadParams(uint8_t i2cAddress, uint8_t *readBuffer, uint8_t readLen) {
void I2CRB::setReadParams(I2CAddress i2cAddress, uint8_t *readBuffer, uint8_t readLen) {
this->i2cAddress = i2cAddress;
this->writeLen = 0;
this->readBuffer = readBuffer;
@ -224,7 +314,7 @@ void I2CRB::setReadParams(uint8_t i2cAddress, uint8_t *readBuffer, uint8_t readL
this->status = I2C_STATUS_OK;
}
void I2CRB::setRequestParams(uint8_t i2cAddress, uint8_t *readBuffer, uint8_t readLen,
void I2CRB::setRequestParams(I2CAddress i2cAddress, uint8_t *readBuffer, uint8_t readLen,
const uint8_t *writeBuffer, uint8_t writeLen) {
this->i2cAddress = i2cAddress;
this->writeBuffer = writeBuffer;
@ -235,7 +325,7 @@ void I2CRB::setRequestParams(uint8_t i2cAddress, uint8_t *readBuffer, uint8_t re
this->status = I2C_STATUS_OK;
}
void I2CRB::setWriteParams(uint8_t i2cAddress, const uint8_t *writeBuffer, uint8_t writeLen) {
void I2CRB::setWriteParams(I2CAddress i2cAddress, const uint8_t *writeBuffer, uint8_t writeLen) {
this->i2cAddress = i2cAddress;
this->writeBuffer = writeBuffer;
this->writeLen = writeLen;
@ -244,3 +334,28 @@ void I2CRB::setWriteParams(uint8_t i2cAddress, const uint8_t *writeBuffer, uint8
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

View File

@ -1,6 +1,6 @@
/*
* © 2023, Neil McKechnie. All rights reserved.
* © 2022 Paul M Antoine
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of CommandStation-EX
*
@ -23,13 +23,15 @@
#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.
*
* 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
@ -76,6 +78,8 @@
* 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.
*
*/
@ -102,7 +106,7 @@
*
* Non-interrupting I2C:
*
* I2C may be operated without interrupts (undefine I2C_USE_INTERRUPTS). Instead, the I2C state
* 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.
@ -111,6 +115,13 @@
*
*/
// 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
@ -122,6 +133,237 @@
#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.
//
// WARNING: When I2CAddress is passed to formatting commands such as DIAG, LCD etc,
// it should be cast to (int) to ensure that the address value is passed rather than
// the struct.
//#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
};
// 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
@ -144,6 +386,7 @@ enum : uint8_t {
I2C_STATE_ACTIVE=253,
I2C_STATE_FREE=254,
I2C_STATE_CLOSING=255,
I2C_STATE_COMPLETED=252,
};
typedef enum : uint8_t
@ -152,6 +395,8 @@ typedef enum : uint8_t
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;
@ -170,18 +415,19 @@ public:
uint8_t wait();
bool isBusy();
void setReadParams(uint8_t i2cAddress, uint8_t *readBuffer, uint8_t readLen);
void setRequestParams(uint8_t i2cAddress, uint8_t *readBuffer, uint8_t readLen, const uint8_t *writeBuffer, uint8_t writeLen);
void setWriteParams(uint8_t i2cAddress, const uint8_t *writeBuffer, uint8_t writeLen);
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;
uint8_t i2cAddress;
I2CAddress i2cAddress;
uint8_t *readBuffer;
const uint8_t *writeBuffer;
#if !defined(I2C_USE_WIRE)
I2CRB *nextRequest;
I2CRB *nextRequest; // Used by non-blocking devices for I2CRB queue management.
#endif
};
@ -195,26 +441,33 @@ public:
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(uint8_t address);
inline bool exists(uint8_t address) {
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);
}
// Write a complete transmission to I2C from an array in RAM
uint8_t write(uint8_t address, const uint8_t buffer[], uint8_t size);
uint8_t write(uint8_t address, const uint8_t buffer[], uint8_t size, I2CRB *rb);
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);
// Write a complete transmission to I2C from an array in Flash
uint8_t write_P(uint8_t address, const uint8_t buffer[], uint8_t size);
uint8_t write_P(uint8_t address, const uint8_t buffer[], uint8_t size, I2CRB *rb);
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);
// Write a transmission to I2C from a list of bytes.
uint8_t write(uint8_t address, uint8_t nBytes, ...);
uint8_t write(I2CAddress address, uint8_t nBytes, ...);
// Write a command from an array in RAM and read response
uint8_t read(uint8_t address, uint8_t readBuffer[], uint8_t readSize,
uint8_t read(I2CAddress address, uint8_t readBuffer[], uint8_t readSize,
const uint8_t writeBuffer[]=NULL, uint8_t writeSize=0);
uint8_t read(uint8_t address, uint8_t readBuffer[], uint8_t readSize,
uint8_t read(I2CAddress address, uint8_t readBuffer[], uint8_t readSize,
const uint8_t writeBuffer[], uint8_t writeSize, I2CRB *rb);
// Write a command from an arbitrary list of bytes and read response
uint8_t read(uint8_t address, uint8_t readBuffer[], uint8_t readSize,
uint8_t read(I2CAddress address, uint8_t readBuffer[], uint8_t readSize,
uint8_t writeSize, ...);
void queueRequest(I2CRB *req);
@ -231,11 +484,19 @@ public:
private:
bool _beginCompleted = false;
bool _clockSpeedFixed = false;
#if defined(__arm__)
uint32_t _clockSpeed = 32000000L; // 3.2MHz max on SAMD and STM32
#else
uint32_t _clockSpeed = 400000L; // 400kHz max on Arduino.
#endif
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);
@ -243,6 +504,15 @@ private:
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 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.
uint8_t _muxCount = 0;
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.
@ -252,42 +522,56 @@ private:
// 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.
static I2CRB * volatile queueHead;
static I2CRB * volatile queueTail;
static volatile uint8_t state;
I2CRB * volatile queueHead = NULL;
I2CRB * volatile queueTail = NULL;
static I2CRB * volatile currentRequest;
static volatile uint8_t txCount;
static volatile uint8_t rxCount;
static volatile uint8_t bytesToSend;
static volatile uint8_t bytesToReceive;
static volatile uint8_t operation;
static volatile unsigned long startTime;
// 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;
static unsigned long timeout; // Transaction timeout in microseconds. 0=disabled.
// 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.
static void I2C_init();
static void I2C_setClock(unsigned long i2cClockSpeed);
static void I2C_handleInterrupt();
static void I2C_sendStart();
static void I2C_sendStop();
static void I2C_close();
void I2C_init();
void I2C_setClock(unsigned long i2cClockSpeed);
void I2C_handleInterrupt();
void I2C_sendStart();
void I2C_sendStop();
void I2C_close();
public:
// setTimeout sets the timout value for I2C transactions.
// TODO: Get I2C timeout working before uncommenting the code below.
void setTimeout(unsigned long value) { (void)value; /* timeout = value; */ };
// handleInterrupt needs to be public to be called from the ISR function!
static void handleInterrupt();
void handleInterrupt();
#endif
};
// 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

View File

@ -1,5 +1,5 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
* © 2023, Neil McKechnie. All rights reserved.
*
* This file is part of CommandStation-EX
*
@ -22,6 +22,7 @@
#include <Arduino.h>
#include "I2CManager.h"
#include "I2CManager_NonBlocking.h" // to satisfy intellisense
#include <avr/io.h>
#include <avr/interrupt.h>
@ -94,12 +95,13 @@ void I2CManagerClass::I2C_init()
* Initiate a start bit for transmission.
***************************************************************************/
void I2CManagerClass::I2C_sendStart() {
bytesToSend = currentRequest->writeLen;
bytesToReceive = currentRequest->readLen;
// We may have initiated a stop bit before this without waiting for it.
// Wait for stop bit to be sent before sending start.
while (TWCR & (1<<TWSTO)) {}
TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT)|(1<<TWEA)|(1<<TWSTA); // Send Start
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
}
/***************************************************************************
@ -107,7 +109,7 @@ void I2CManagerClass::I2C_sendStart() {
***************************************************************************/
void I2CManagerClass::I2C_sendStop() {
TWDR = 0xff; // Default condition = SDA released
TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWEA)|(1<<TWSTO); // Send Stop
TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWSTO); // Send Stop
}
/***************************************************************************
@ -115,9 +117,8 @@ void I2CManagerClass::I2C_sendStop() {
***************************************************************************/
void I2CManagerClass::I2C_close() {
// disable TWI
I2C_sendStop();
while (TWCR & (1<<TWSTO)) {}
TWCR = (1<<TWINT); // clear any interrupt and stop twi.
delayMicroseconds(10); // Wait for things to stabilise (hopefully)
}
/***************************************************************************
@ -125,37 +126,51 @@ void I2CManagerClass::I2C_close() {
* 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(currentRequest->writeBuffer + (txCount++));
TWDR = GETFLASH(sendBuffer + (txCount++));
else
TWDR = currentRequest->writeBuffer[txCount++];
TWDR = sendBuffer[txCount++];
bytesToSend--;
TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT)|(1<<TWEA);
TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT);
} else if (bytesToReceive) { // All sent, anything to receive?
while (TWCR & (1<<TWSTO)) {} // Wait for stop to be sent
TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT)|(1<<TWEA)|(1<<TWSTA); // Send Start
} else { // Nothing left to send or receive
TWDR = 0xff; // Default condition = SDA released
// 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_STATUS_OK;
state = I2C_STATE_COMPLETED;
}
break;
case TWI_MRX_DATA_ACK: // Data byte has been received and ACK transmitted
if (bytesToReceive > 0) {
currentRequest->readBuffer[rxCount++] = TWDR;
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
@ -164,45 +179,54 @@ void I2CManagerClass::I2C_handleInterrupt() {
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) {
currentRequest->readBuffer[rxCount++] = TWDR;
receiveBuffer[rxCount++] = TWDR;
bytesToReceive--;
}
TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWEA)|(1<<TWSTO); // Send Stop
state = I2C_STATUS_OK;
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 = (currentRequest->i2cAddress << 1) | 1; // SLA+R
TWDR = (deviceAddress << 1) | 1; // SLA+R
else
TWDR = (currentRequest->i2cAddress << 1) | 0; // SLA+W
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
TWDR = 0xff; // Default condition = SDA released
TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWEA)|(1<<TWSTO); // Send Stop
state = I2C_STATUS_NEGATIVE_ACKNOWLEDGE;
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
state = I2C_STATUS_TRANSMIT_ERROR;
completionStatus = I2C_STATUS_TRANSMIT_ERROR;
state = I2C_STATE_COMPLETED;
}
}
#if defined(I2C_USE_INTERRUPTS)
ISR(TWI_vect) {
I2CManagerClass::handleInterrupt();
// pinMode(2,OUTPUT);
// digitalWrite(2,1);
I2CManager.handleInterrupt();
// digitalWrite(2,0);
}
#endif

View File

@ -1,5 +1,5 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
* © 2023, Neil McKechnie. All rights reserved.
*
* This file is part of CommandStation-EX
*
@ -28,21 +28,21 @@
***************************************************************************/
void I2CManagerClass::I2C_setClock(unsigned long i2cClockSpeed) {
uint16_t t_rise;
if (i2cClockSpeed < 200000) {
i2cClockSpeed = 100000;
if (i2cClockSpeed < 200000)
t_rise = 1000;
} else if (i2cClockSpeed < 800000) {
i2cClockSpeed = 400000;
else if (i2cClockSpeed < 800000)
t_rise = 300;
} else if (i2cClockSpeed < 1200000) {
i2cClockSpeed = 1000000;
else
t_rise = 120;
} else {
i2cClockSpeed = 100000;
t_rise = 1000;
}
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;
}
@ -54,13 +54,13 @@ 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
I2C_setClock(I2C_FREQ);
TWI0.MSTATUS = TWI_BUSSTATE_IDLE_gc;
}
@ -68,8 +68,8 @@ void I2CManagerClass::I2C_init()
* Initiate a start bit for transmission, followed by address and R/W
***************************************************************************/
void I2CManagerClass::I2C_sendStart() {
bytesToSend = currentRequest->writeLen;
bytesToReceive = currentRequest->readLen;
txCount = 0;
rxCount = 0;
// If anything to send, initiate write. Otherwise initiate read.
if (operation == OPERATION_READ || ((operation == OPERATION_REQUEST) && !bytesToSend))
@ -89,7 +89,10 @@ void I2CManagerClass::I2C_sendStop() {
* Close I2C down
***************************************************************************/
void I2CManagerClass::I2C_close() {
I2C_sendStop();
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)
}
/***************************************************************************
@ -105,38 +108,34 @@ void I2CManagerClass::I2C_handleInterrupt() {
I2C_sendStart(); // Reinitiate request
} else if (currentStatus & TWI_BUSERR_bm) {
// Bus error
state = I2C_STATUS_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;
state = I2C_STATUS_NEGATIVE_ACKNOWLEDGE;
completionStatus = I2C_STATUS_NEGATIVE_ACKNOWLEDGE;
state = I2C_STATE_COMPLETED;
} else if (bytesToSend) {
// Acked, so send next byte
if (currentRequest->operation == OPERATION_SEND_P)
TWI0.MDATA = GETFLASH(currentRequest->writeBuffer + (txCount++));
else
TWI0.MDATA = currentRequest->writeBuffer[txCount++];
// 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 = (currentRequest->i2cAddress << 1) | 1;
// 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_STATUS_OK; // Done
state = I2C_STATE_COMPLETED;
}
} else if (currentStatus & TWI_RIF_bm) {
// Master read completed without errors
if (bytesToReceive) {
currentRequest->readBuffer[rxCount++] = TWI0.MDATA; // Store received byte
receiveBuffer[rxCount++] = TWI0.MDATA; // Store received byte
bytesToReceive--;
} else {
// Buffer full, issue nack/stop
TWI0.MCTRLB = TWI_ACKACT_bm | TWI_MCMD_STOP_gc;
state = I2C_STATUS_OK;
}
if (bytesToReceive) {
// More bytes to receive, issue ack and start another read
@ -144,7 +143,7 @@ void I2CManagerClass::I2C_handleInterrupt() {
} else {
// Transaction finished, issue NACK and STOP.
TWI0.MCTRLB = TWI_ACKACT_bm | TWI_MCMD_STOP_gc;
state = I2C_STATUS_OK;
state = I2C_STATE_COMPLETED;
}
}
}
@ -154,7 +153,7 @@ void I2CManagerClass::I2C_handleInterrupt() {
* Interrupt handler.
***************************************************************************/
ISR(TWI0_TWIM_vect) {
I2CManagerClass::handleInterrupt();
I2CManager.handleInterrupt();
}
#endif

View File

@ -1,6 +1,6 @@
/*
* © 2023, Neil McKechnie
* © 2022 Paul M Antoine
* © 2021, Neil McKechnie
* All rights reserved.
*
* This file is part of CommandStation-EX
@ -24,58 +24,62 @@
#include <Arduino.h>
#include "I2CManager.h"
#if defined(I2C_USE_INTERRUPTS)
// atomic.h isn't available on SAMD, and likely others too...
#if defined(__AVR__)
#include <util/atomic.h>
#elif defined(__arm__)
// Helper assembly language functions
static __inline__ uint8_t my_iSeiRetVal(void)
{
__asm__ __volatile__ ("cpsie i" ::);
return 1;
// 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__ uint8_t my_iCliRetVal(void)
{
__asm__ __volatile__ ("cpsid i" ::);
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)
static __inline__ void my_iRestore(const uint32_t *__s)
{
uint32_t res = *__s;
__asm__ __volatile__ ("MSR primask, %0" : : "r" (res) );
}
static __inline__ uint32_t my_iGetIReg( void )
{
uint32_t reg;
__asm__ __volatile__ ("MRS %0, primask" : "=r" (reg) );
return reg;
}
// Macros for atomic isolation
#define MY_ATOMIC_RESTORESTATE uint32_t _sa_saved \
__attribute__((__cleanup__(my_iRestore))) = my_iGetIReg()
#define ATOMIC() \
for ( MY_ATOMIC_RESTORESTATE, _done = my_iCliRetVal(); \
_done; _done = 0 )
#define ATOMIC_BLOCK(x) ATOMIC()
#define ATOMIC_RESTORESTATE
#endif
#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
#define ATOMIC_BLOCK(x)
#define ATOMIC_RESTORESTATE
#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.
***************************************************************************/
@ -84,31 +88,82 @@ 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) {
I2C_setClock(i2cClockSpeed);
pendingClockSpeed = i2cClockSpeed;
}
/***************************************************************************
* Helper function to start operations, if the I2C interface is free and
* 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(ATOMIC_RESTORESTATE) {
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;
// Copy key fields to static data for speed.
operation = currentRequest->operation;
// 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();
startTime = micros();
}
}
}
@ -119,8 +174,7 @@ void I2CManagerClass::startTransaction() {
void I2CManagerClass::queueRequest(I2CRB *req) {
req->status = I2C_STATUS_PENDING;
req->nextRequest = NULL;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
ATOMIC_BLOCK() {
if (!queueTail)
queueHead = queueTail = req; // Only item on queue
else
@ -133,7 +187,7 @@ void I2CManagerClass::queueRequest(I2CRB *req) {
/***************************************************************************
* Initiate a write to an I2C device (non-blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::write(uint8_t i2cAddress, const uint8_t *writeBuffer, uint8_t writeLen, I2CRB *req) {
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);
@ -144,7 +198,7 @@ uint8_t I2CManagerClass::write(uint8_t i2cAddress, const uint8_t *writeBuffer, u
/***************************************************************************
* Initiate a write from PROGMEM (flash) to an I2C device (non-blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::write_P(uint8_t i2cAddress, const uint8_t * writeBuffer, uint8_t writeLen, I2CRB *req) {
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);
@ -157,7 +211,7 @@ uint8_t I2CManagerClass::write_P(uint8_t i2cAddress, const uint8_t * writeBuffer
* Initiate a read from the I2C device, optionally preceded by a write
* (non-blocking operation)
***************************************************************************/
uint8_t I2CManagerClass::read(uint8_t i2cAddress, uint8_t *readBuffer, uint8_t readLen,
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.
@ -167,31 +221,54 @@ uint8_t I2CManagerClass::read(uint8_t i2cAddress, uint8_t *readBuffer, uint8_t r
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.
* This function doesn't fully work as intended so is not currently called.
* Instead we check for an I2C hang-up and report an error from
* I2CRB::wait(), but we aren't able to recover from the hang-up. Such faults
* requests that are taking too long to complete. Such faults
* may be caused by an I2C wire short for example.
***************************************************************************/
void I2CManagerClass::checkForTimeout() {
unsigned long currentMicros = micros();
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
ATOMIC_BLOCK() {
I2CRB *t = queueHead;
if (state==I2C_STATE_ACTIVE && t!=0 && t==currentRequest && timeout > 0) {
if (state==I2C_STATE_ACTIVE && t!=0 && t==currentRequest && _timeout > 0) {
// Check for timeout
if (currentMicros - startTime > 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.
@ -208,10 +285,8 @@ void I2CManagerClass::loop() {
#if !defined(I2C_USE_INTERRUPTS)
handleInterrupt();
#endif
// Timeout is now reported in I2CRB::wait(), not here.
// I've left the code, commented out, as a reminder to look at this again
// in the future.
//checkForTimeout();
// Call function to monitor for stuck I2C operations.
checkForTimeout();
}
/***************************************************************************
@ -223,43 +298,85 @@ void I2CManagerClass::handleInterrupt() {
// Update hardware state machine
I2C_handleInterrupt();
// Enable interrupts to minimise effect on other interrupt code
interrupts();
// 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_ACTIVE && currentRequest != NULL) {
// Remove completed request from head of queue
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
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 == queueHead) {
if (t == currentRequest) {
queueHead = t->nextRequest;
if (!queueHead) queueTail = queueHead;
t->nBytes = rxCount;
t->status = state;
t->status = overallStatus;
// I2C state machine is now free for next request
currentRequest = NULL;
state = I2C_STATE_FREE;
// Start next request (if any)
I2CManager.startTransaction();
}
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();
}
}
// Fields in I2CManager class specific to Non-blocking implementation.
I2CRB * volatile I2CManagerClass::queueHead = NULL;
I2CRB * volatile I2CManagerClass::queueTail = NULL;
I2CRB * volatile I2CManagerClass::currentRequest = NULL;
volatile uint8_t I2CManagerClass::state = I2C_STATE_FREE;
volatile uint8_t I2CManagerClass::txCount;
volatile uint8_t I2CManagerClass::rxCount;
volatile uint8_t I2CManagerClass::operation;
volatile uint8_t I2CManagerClass::bytesToSend;
volatile uint8_t I2CManagerClass::bytesToReceive;
volatile unsigned long I2CManagerClass::startTime;
unsigned long I2CManagerClass::timeout = 0;
#endif

View File

@ -1,6 +1,6 @@
/*
* © 2022 Paul M Antoine
* © 2021, Neil McKechnie
* © 2023, Neil McKechnie
* All rights reserved.
*
* This file is part of CommandStation-EX
@ -38,7 +38,7 @@
***************************************************************************/
#if defined(I2C_USE_INTERRUPTS) && defined(ARDUINO_SAMD_ZERO)
void SERCOM3_Handler() {
I2CManagerClass::handleInterrupt();
I2CManager.handleInterrupt();
}
#endif
@ -46,14 +46,16 @@ void SERCOM3_Handler() {
Sercom *s = SERCOM3;
/***************************************************************************
* Set I2C clock speed register.
* 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;
i2cClockSpeed = 100000L; // NB: this overrides a "force clock" of lower than 100KHz!
t_rise = 1000;
} else if (i2cClockSpeed < 800000L) {
i2cClockSpeed = 400000L;
@ -66,6 +68,9 @@ void I2CManagerClass::I2C_setClock(uint32_t i2cClockSpeed) {
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);
@ -80,8 +85,6 @@ void I2CManagerClass::I2C_setClock(uint32_t i2cClockSpeed) {
// Setting bus idle mode and wait for sync
s->I2CM.STATUS.bit.BUSSTATE = 1 ;
while (s->I2CM.SYNCBUSY.bit.SYSOP != 0);
return;
}
/***************************************************************************
@ -107,8 +110,8 @@ void I2CManagerClass::I2C_init()
s->I2CM.CTRLA.reg = SERCOM_I2CM_CTRLA_MODE( I2C_MASTER_OPERATION )/* |
SERCOM_I2CM_CTRLA_SCLSM*/ ;
// Enable Smart mode and Quick Command
s->I2CM.CTRLB.reg = SERCOM_I2CM_CTRLB_SMEN | SERCOM_I2CM_CTRLB_QCEN;
// Enable Smart mode (but not Quick Command)
s->I2CM.CTRLB.reg = SERCOM_I2CM_CTRLB_SMEN;
#if defined(I2C_USE_INTERRUPTS)
// Setting NVIC
@ -141,30 +144,33 @@ void I2CManagerClass::I2C_init()
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;
PORT_PINCFG_DRVSTR | PORT_PINCFG_PULLEN | PORT_PINCFG_PMUXEN;
}
/***************************************************************************
* Initiate a start bit for transmission.
***************************************************************************/
void I2CManagerClass::I2C_sendStart() {
bytesToSend = currentRequest->writeLen;
bytesToReceive = currentRequest->readLen;
// We may have initiated a stop bit before this without waiting for it.
// Wait for stop bit to be sent before sending start.
while (s->I2CM.STATUS.bit.BUSSTATE == 0x2);
// 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/write flag or'd in
s->I2CM.ADDR.bit.ADDR = (currentRequest->i2cAddress << 1) | 1;
// Send start and address with read flag (1) or'd in
s->I2CM.ADDR.bit.ADDR = (deviceAddress << 1) | 1;
}
else {
// Wait while the I2C bus is BUSY
while (s->I2CM.STATUS.bit.BUSSTATE != 0x1);
s->I2CM.ADDR.bit.ADDR = (currentRequest->i2cAddress << 1ul) | 0;
// Send start and address with write flag (0) or'd in
s->I2CM.ADDR.bit.ADDR = (deviceAddress << 1ul) | 0;
}
}
@ -180,6 +186,13 @@ void I2CManagerClass::I2C_sendStop() {
***************************************************************************/
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;
}
}
/***************************************************************************
@ -194,49 +207,39 @@ void I2CManagerClass::I2C_handleInterrupt() {
I2C_sendStart(); // Reinitiate request
} else if (s->I2CM.STATUS.bit.BUSERR) {
// Bus error
state = I2C_STATUS_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();
state = I2C_STATUS_NEGATIVE_ACKNOWLEDGE;
completionStatus = I2C_STATUS_NEGATIVE_ACKNOWLEDGE;
state = I2C_STATE_COMPLETED; // Completed with error
} else if (bytesToSend) {
// Acked, so send next byte
if (currentRequest->operation == OPERATION_SEND_P)
s->I2CM.DATA.bit.DATA = GETFLASH(currentRequest->writeBuffer + (txCount++));
else
s->I2CM.DATA.bit.DATA = currentRequest->writeBuffer[txCount++];
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 = (currentRequest->i2cAddress << 1) | 1;
s->I2CM.ADDR.bit.ADDR = (deviceAddress << 1) | 1;
} else {
// No more data to send/receive. Initiate a STOP condition.
// No more data to send/receive. Initiate a STOP condition
I2C_sendStop();
state = I2C_STATUS_OK; // Done
state = I2C_STATE_COMPLETED; // Completed OK
}
} else if (s->I2CM.INTFLAG.bit.SB) {
// Master read completed without errors
if (bytesToReceive) {
currentRequest->readBuffer[rxCount++] = s->I2CM.DATA.bit.DATA; // Store received byte
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--;
} else {
// Buffer full, issue nack/stop
s->I2CM.CTRLB.bit.ACKACT = 1;
I2C_sendStop();
state = I2C_STATUS_OK;
}
if (bytesToReceive) {
// PMA - I think Smart Mode means we have nothing to do...
// More bytes to receive, issue ack and start another read
}
else
{
// Transaction finished, issue NACK and STOP.
s->I2CM.CTRLB.bit.ACKACT = 1;
I2C_sendStop();
state = I2C_STATUS_OK;
}
}
}

312
I2CManager_STM32.h Normal file
View File

@ -0,0 +1,312 @@
/*
* © 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,5 +1,5 @@
/*
* © 2021, Neil McKechnie. All rights reserved.
* © 2023, Neil McKechnie. All rights reserved.
*
* This file is part of CommandStation-EX
*
@ -30,11 +30,19 @@
#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
}
/***************************************************************************
@ -45,20 +53,77 @@ 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 :
(1 << subBus);
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(uint8_t address, const uint8_t buffer[], uint8_t size, I2CRB *rb) {
Wire.beginTransmission(address);
if (size > 0) Wire.write(buffer, size);
rb->status = Wire.endTransmission();
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(uint8_t address, const uint8_t buffer[], uint8_t size, I2CRB *rb) {
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++)
@ -70,27 +135,64 @@ uint8_t I2CManagerClass::write_P(uint8_t address, const uint8_t buffer[], uint8_
* 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(uint8_t address, uint8_t readBuffer[], uint8_t readSize,
uint8_t I2CManagerClass::read(I2CAddress address, uint8_t readBuffer[], uint8_t readSize,
const uint8_t writeBuffer[], uint8_t writeSize, I2CRB *rb)
{
uint8_t status = I2C_STATUS_OK;
uint8_t status, muxStatus;
uint8_t nBytes = 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) {
Wire.requestFrom(address, (size_t)readSize);
while (Wire.available() && nBytes < readSize)
readBuffer[nBytes++] = Wire.read();
if (nBytes < readSize) status = I2C_STATUS_TRUNCATED;
}
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.
*
@ -100,7 +202,7 @@ uint8_t I2CManagerClass::read(uint8_t address, uint8_t readBuffer[], uint8_t rea
* the non-blocking version.
***************************************************************************/
void I2CManagerClass::queueRequest(I2CRB *req) {
switch (req->operation) {
switch (req->operation & OPERATION_MASK) {
case OPERATION_READ:
read(req->i2cAddress, req->readBuffer, req->readLen, NULL, 0, req);
break;
@ -121,8 +223,4 @@ void I2CManagerClass::queueRequest(I2CRB *req) {
***************************************************************************/
void I2CManagerClass::loop() {}
// Loop function
void I2CManagerClass::checkForTimeout() {}
#endif

View File

@ -48,12 +48,14 @@ extern __attribute__((weak)) void exrailHalSetup();
// 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 first so that the following defaults will detect an overlap and not
// create something that conflicts with the users vpin definitions.
// 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();
@ -61,8 +63,6 @@ void IODevice::begin() {
if (exrailHalSetup)
exrailHalSetup();
// Initialise the IO subsystem defaults
ArduinoPins::create(2, NUM_DIGITAL_PINS-2); // Reserve pins for direct access
// Predefine two PCA9685 modules 0x40-0x41
// Allocates 32 pins 100-131
PCA9685::create(100, 16, 0x40);
@ -72,12 +72,18 @@ void IODevice::begin() {
// Allocates 32 pins 164-195
MCP23017::create(164, 16, 0x20);
MCP23017::create(180, 16, 0x21);
}
// Call the begin() methods of each configured device in turn
for (IODevice *dev=_firstDevice; dev!=NULL; dev = dev->_nextDevice) {
// 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();
}
_initPhase = false;
}
// Overarching static loop() method for the IODevice subsystem. Works through the
@ -109,18 +115,19 @@ void IODevice::loop() {
// Report loop time if diags enabled
#if defined(DIAG_LOOPTIMES)
unsigned long diagMicros = micros();
static unsigned long lastMicros = 0;
// Measure time since loop() method started.
unsigned long halElapsed = micros() - currentMicros;
// Measure time between loop() method entries.
unsigned long elapsed = currentMicros - lastMicros;
// 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
// 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;
@ -128,14 +135,16 @@ void IODevice::loop() {
total += elapsed;
count++;
}
if (currentMicros - lastOutputTime > interval) {
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 = currentMicros;
lastOutputTime = diagMicros;
}
lastMicros = currentMicros;
// Read microsecond count after calculations, so they aren't
// included in the overall timings.
lastMicros = micros();
#endif
}
@ -258,25 +267,27 @@ void IODevice::setGPIOInterruptPin(int16_t pinNumber) {
_gpioInterruptPin = pinNumber;
}
// Private helper function to add a device to the chain of devices.
void IODevice::addDevice(IODevice *newDevice) {
// Link new object to the end of the chain. Thereby, the first devices to be declared/created
// will be located faster by findDevice than those which are created later.
// Ideally declare/create the digital IO pins first, then servos, then more esoteric devices.
IODevice *lastDevice;
if (_firstDevice == 0)
// 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)
lastDevice = dev;
lastDevice->_nextDevice = 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->_nextDevice = 0;
// If the IODevice::begin() method has already been called, initialise device here. If not,
// the device's _begin() method will be called by IODevice::begin().
if (!_initPhase)
newDevice->_begin();
newDevice->_begin();
}
// Private helper function to locate a device by VPIN. Returns NULL if not found.
@ -290,28 +301,40 @@ IODevice *IODevice::findDevice(VPIN vpin) {
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
bool IODevice::checkNoOverlap(VPIN firstPin, uint8_t nPins, uint8_t i2cAddress) {
bool IODevice::checkNoOverlap(VPIN firstPin, uint8_t nPins, I2CAddress i2cAddress) {
#ifdef DIAG_IO
DIAG(F("Check no overlap %d %d 0x%x"), firstPin,nPins,i2cAddress);
DIAG(F("Check no overlap %d %d %s"), firstPin,nPins,i2cAddress.toString());
#endif
VPIN lastPin=firstPin+nPins-1;
for (IODevice *dev = _firstDevice; dev != 0; dev = dev->_nextDevice) {
// 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 definition of pins %d to %d ignored."),
firstPin, lastPin);
return false;
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 definition of pins %d to %d ignored."),
firstPin, lastPin);
return false;
}
}
// Check for overlapping I2C address
if (i2cAddress && dev->_I2CAddress==i2cAddress) {
DIAG(F("WARNING HAL Overlap. i2c Addr 0x%x ignored."),i2cAddress);
DIAG(F("WARNING HAL Overlap. i2c Addr %s ignored."),i2cAddress.toString());
return false;
}
}
@ -326,15 +349,12 @@ bool IODevice::checkNoOverlap(VPIN firstPin, uint8_t nPins, uint8_t i2cAddress)
// Chain of callback blocks (identifying registered callback functions for state changes)
IONotifyCallback *IONotifyCallback::first = 0;
// Start of chain of devices.
// 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;
// Flag which is reset when IODevice::begin has been called.
bool IODevice::_initPhase = true;
//==================================================================================================================
// Instance members

View File

@ -1,4 +1,5 @@
/*
* © 2023, Paul Antoine, Discord user @ADUBOURG
* © 2021, Neil McKechnie. All rights reserved.
*
* This file is part of DCC++EX API
@ -93,6 +94,8 @@ public:
CONFIGURE_INPUT = 1,
CONFIGURE_SERVO = 2,
CONFIGURE_OUTPUT = 3,
CONFIGURE_ANALOGOUTPUT = 4,
CONFIGURE_ANALOGINPUT = 5,
} ConfigTypeEnum;
typedef enum : uint8_t {
@ -110,6 +113,10 @@ public:
// 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[]);
@ -161,27 +168,12 @@ public:
// 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, uint8_t i2cAddress=0);
// Method to check if pins will overlap before creating new device.
static bool checkNoOverlap(VPIN firstPin, uint8_t nPins=1, I2CAddress i2cAddress=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 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;
};
// 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) {
@ -189,7 +181,7 @@ protected:
};
// Method to write an 'analogue' value (optionally implemented within device class)
virtual void _writeAnalogue(VPIN vpin, int value, uint8_t param1, uint16_t param2) {
virtual void _writeAnalogue(VPIN vpin, int value, uint8_t param1=0, uint16_t param2=0) {
(void)vpin; (void)value; (void) param1; (void)param2;
};
@ -204,6 +196,29 @@ protected:
(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;
@ -228,7 +243,7 @@ protected:
// Common object fields.
VPIN _firstVpin;
int _nPins;
uint8_t _I2CAddress;
I2CAddress _I2CAddress;
// Flag whether the device supports callbacks.
bool _hasCallback = false;
@ -237,22 +252,20 @@ protected:
int16_t _gpioInterruptPin = -1;
// Static support function for subclass creation
static void addDevice(IODevice *newDevice);
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:
// Method to check whether the vpin corresponds to this device
bool owns(VPIN vpin);
// Method to find device handling Vpin
static IODevice *findDevice(VPIN vpin);
IODevice *_nextDevice = 0;
unsigned long _nextEntryTime;
static IODevice *_firstDevice;
static IODevice *_nextLoopDevice;
static bool _initPhase;
};
@ -263,7 +276,7 @@ private:
class PCA9685 : public IODevice {
public:
static void create(VPIN vpin, int nPins, uint8_t I2CAddress);
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
@ -276,7 +289,7 @@ public:
private:
// Constructor
PCA9685(VPIN vpin, int nPins, uint8_t I2CAddress);
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;
@ -306,13 +319,14 @@ private:
struct ServoData *_servoData [16];
static const uint8_t _catchupSteps = 5; // number of steps to wait before switching servo off
static const byte FLASH _bounceProfile[30];
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
};
/////////////////////////////////////////////////////////////////////////////////////////////////////
@ -376,9 +390,9 @@ private:
class EXTurntable : public IODevice {
public:
static void create(VPIN firstVpin, int nPins, uint8_t I2CAddress);
static void create(VPIN firstVpin, int nPins, I2CAddress I2CAddress);
// Constructor
EXTurntable(VPIN firstVpin, int nPins, uint8_t I2CAddress);
EXTurntable(VPIN firstVpin, int nPins, I2CAddress I2CAddress);
enum ActivityNumber : uint8_t {
Turn = 0, // Rotate turntable, maintain phase
Turn_PInvert = 1, // Rotate turntable, invert phase
@ -404,9 +418,59 @@ private:
/////////////////////////////////////////////////////////////////////////////////////////////////////
// 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);
}
};
#include "IO_MCP23008.h"
#include "IO_MCP23017.h"
#include "IO_PCF8574.h"
#include "IO_PCF8575.h"
#include "IO_duinoNodes.h"
#include "IO_EXIOExpander.h"

View File

@ -59,28 +59,33 @@
**********************************************************************************************/
class ADS111x: public IODevice {
public:
static void create(VPIN firstVpin, int nPins, uint8_t i2cAddress) {
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, uint8_t i2cAddress) {
ADS111x(VPIN firstVpin, int nPins, I2CAddress i2cAddress) {
_firstVpin = firstVpin;
_nPins = min(nPins,4);
_i2cAddress = i2cAddress;
_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)) {
if (I2CManager.exists(_I2CAddress)) {
_nextState = STATE_STARTSCAN;
#ifdef DIAG_IO
_display();
#endif
} else {
DIAG(F("ADS111x device not found, I2C:%x"), _i2cAddress);
DIAG(F("ADS111x device not found, I2C:%s"), _I2CAddress.toString());
_deviceState = DEVSTATE_FAILED;
}
}
@ -98,7 +103,7 @@ private:
_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);
I2CManager.write(_I2CAddress, _outBuffer, 3, &_i2crb);
delayUntil(currentMicros + scanInterval);
_nextState = STATE_STARTREAD;
@ -107,7 +112,7 @@ private:
case STATE_STARTREAD:
// Reading the pin value
_outBuffer[0] = 0x00; // Conversion register address
I2CManager.read(_i2cAddress, _inBuffer, 2, _outBuffer, 1, &_i2crb); // Read register
I2CManager.read(_I2CAddress, _inBuffer, 2, _outBuffer, 1, &_i2crb); // Read register
_nextState = STATE_GETVALUE;
break;
@ -126,7 +131,7 @@ private:
break;
}
} else { // error status
DIAG(F("ADS111x I2C:x%d Error:%d %S"), _i2cAddress, status, I2CManager.getErrorMessage(status));
DIAG(F("ADS111x I2C:%s Error:%d %S"), _I2CAddress.toString(), status, I2CManager.getErrorMessage(status));
_deviceState = DEVSTATE_FAILED;
}
}
@ -137,7 +142,7 @@ private:
}
void _display() override {
DIAG(F("ADS111x I2C:x%x Configured on Vpins:%d-%d %S"), _i2cAddress, _firstVpin, _firstVpin+_nPins-1,
DIAG(F("ADS111x I2C:%s Configured on Vpins:%d-%d %S"), _I2CAddress.toString(), _firstVpin, _firstVpin+_nPins-1,
_deviceState == DEVSTATE_FAILED ? F("OFFLINE") : F(""));
}
@ -154,7 +159,6 @@ private:
STATE_GETVALUE,
};
uint16_t _value[4];
uint8_t _i2cAddress;
uint8_t _outBuffer[3];
uint8_t _inBuffer[2];
uint8_t _currentPin; // ADC pin currently being scanned

View File

@ -70,10 +70,12 @@
class DFPlayer : public IODevice {
private:
const uint8_t MAXVOLUME=30;
HardwareSerial *_serial;
bool _playing = false;
uint8_t _inputIndex = 0;
unsigned long _commandSendTime; // Allows timeout processing
uint8_t _lastVolumeLevel = MAXVOLUME;
// When two commands are sent in quick succession, the device sometimes
// fails to execute one. A delay is required between successive commands.
@ -179,41 +181,45 @@ protected:
// 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.
// If starting a new file and setting volume, then avoid a short burst of loud noise by
// the following strategy:
// - If the volume is increasing, start playing the song before setting the volume,
// - If the volume is decreasing, decrease it and then start playing.
//
void _writeAnalogue(VPIN vpin, int value, uint8_t volume=0, uint16_t=0) override {
uint8_t pin = vpin - _firstVpin;
#ifdef DIAG_IO
DIAG(F("DFPlayer: VPIN:%d FileNo:%d Volume:%d"), vpin, value, volume);
#endif
// Validate parameter.
volume = min((uint8_t)30,volume);
if (volume > MAXVOLUME) volume = MAXVOLUME;
if (pin == 0) {
// Play track
if (value > 0) {
#ifdef DIAG_IO
DIAG(F("DFPlayer: Play %d"), value);
#endif
sendPacket(0x03, value); // Play track
_playing = true;
if (volume > 0) {
#ifdef DIAG_IO
DIAG(F("DFPlayer: Volume %d"), volume);
#endif
sendPacket(0x06, volume); // Set volume
if (volume != 0) {
if (volume <= _lastVolumeLevel)
sendPacket(0x06, volume); // Set volume before starting
sendPacket(0x03, value); // Play track
_playing = true;
if (volume > _lastVolumeLevel)
sendPacket(0x06, volume); // Set volume after starting
_lastVolumeLevel = volume;
} else {
// Volume not changed, just play
sendPacket(0x03, value);
_playing = true;
}
} else {
#ifdef DIAG_IO
DIAG(F("DFPlayer: Stop"));
#endif
sendPacket(0x16); // Stop play
_playing = false;
}
} else if (pin == 1) {
// Set volume (0-30)
if (value > 30) value = 30;
else if (value < 0) value = 0;
#ifdef DIAG_IO
DIAG(F("DFPlayer: Volume %d"), value);
#endif
sendPacket(0x06, value);
_lastVolumeLevel = volume;
}
}
@ -261,7 +267,7 @@ private:
// Output some pad characters to add an
// artificial delay between commands
for (int i=0; i<numPadCharacters; i++)
_serial->write(0);
_serial->write((uint8_t)0);
}
// Now output the command

View File

@ -56,7 +56,7 @@ static void create(uint8_t _I2CAddress) {
// XXXX change thistosave2 bytes
if (_checkforclock == 0) {
FAST_CLOCK_EXISTS = true;
//DIAG(F("I2C Fast Clock found at x%x"), _I2CAddress);
//DIAG(F("I2C Fast Clock found at %s"), _I2CAddress.toString());
new EXFastClock(_I2CAddress);
}
else {
@ -68,7 +68,6 @@ static void create(uint8_t _I2CAddress) {
}
private:
uint8_t _I2CAddress;
// Initialisation of Fastclock
@ -84,7 +83,7 @@ void _begin() override {
} else {
_deviceState = DEVSTATE_FAILED;
//LCD(6,F("CLOCK NOT FOUND"));
DIAG(F("Fast Clock Not Found at address %d"), _I2CAddress);
DIAG(F("Fast Clock Not Found at address %s"), _I2CAddress.toString());
}
}
}
@ -111,16 +110,14 @@ void _loop(unsigned long currentMicros) override{
// Clock interval is 60/ clockspeed i.e 60/b seconds
delayUntil(currentMicros + ((60/b) * 1000000));
}
#endif
}
}
// Display EX-FastClock device driver info.
void _display() {
DIAG(F("FastCLock on I2C:x%x - %S"), _I2CAddress, (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F(""));
void _display() override {
DIAG(F("FastCLock on I2C:%s - %S"), _I2CAddress.toString(), (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F(""));
}
};

View File

@ -60,13 +60,13 @@ public:
NoPowerOff = 0x80, // Flag to be ORed in to suppress power off after move.
};
static void create(VPIN vpin, int nPins, uint8_t i2cAddress) {
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, uint8_t i2cAddress) {
EXIOExpander(VPIN firstVpin, int nPins, I2CAddress i2cAddress) {
_firstVpin = firstVpin;
_nPins = nPins;
_i2cAddress = i2cAddress;
@ -92,7 +92,7 @@ private:
_analogueInputStates = (byte*) calloc(_analoguePinBytes, 1);
_analoguePinMap = (uint8_t*) calloc(_numAnaloguePins, 1);
} else {
DIAG(F("ERROR configuring EX-IOExpander device, I2C:x%x"), _i2cAddress);
DIAG(F("ERROR configuring EX-IOExpander device, I2C:%s"), _i2cAddress.toString());
_deviceState = DEVSTATE_FAILED;
return;
}
@ -105,13 +105,13 @@ private:
_majorVer = _versionBuffer[0];
_minorVer = _versionBuffer[1];
_patchVer = _versionBuffer[2];
DIAG(F("EX-IOExpander device found, I2C:x%x, Version v%d.%d.%d"),
_i2cAddress, _versionBuffer[0], _versionBuffer[1], _versionBuffer[2]);
DIAG(F("EX-IOExpander device found, I2C:%s, Version v%d.%d.%d"),
_I2CAddress.toString(), _versionBuffer[0], _versionBuffer[1], _versionBuffer[2]);
#ifdef DIAG_IO
_display();
#endif
} else {
DIAG(F("EX-IOExpander device not found, I2C:x%x"), _i2cAddress);
DIAG(F("EX-IOExpander device not found, I2C:%s"), _I2CAddress.toString());
_deviceState = DEVSTATE_FAILED;
}
}
@ -218,13 +218,13 @@ private:
}
void _display() override {
DIAG(F("EX-IOExpander I2C:x%x v%d.%d.%d Vpins %d-%d %S"),
_i2cAddress, _majorVer, _minorVer, _patchVer,
DIAG(F("EX-IOExpander I2C:%s v%d.%d.%d Vpins %d-%d %S"),
_i2cAddress.toString(), _majorVer, _minorVer, _patchVer,
(int)_firstVpin, (int)_firstVpin+_nPins-1,
_deviceState == DEVSTATE_FAILED ? F("OFFLINE") : F(""));
}
uint8_t _i2cAddress;
I2CAddress _i2cAddress;
uint8_t _numDigitalPins = 0;
uint8_t _numAnaloguePins = 0;
byte _digitalOutBuffer[3];

View File

@ -35,12 +35,12 @@
#include "I2CManager.h"
#include "DIAG.h"
void EXTurntable::create(VPIN firstVpin, int nPins, uint8_t I2CAddress) {
void EXTurntable::create(VPIN firstVpin, int nPins, I2CAddress I2CAddress) {
new EXTurntable(firstVpin, nPins, I2CAddress);
}
// Constructor
EXTurntable::EXTurntable(VPIN firstVpin, int nPins, uint8_t I2CAddress) {
EXTurntable::EXTurntable(VPIN firstVpin, int nPins, I2CAddress I2CAddress) {
_firstVpin = firstVpin;
_nPins = nPins;
_I2CAddress = I2CAddress;
@ -106,7 +106,7 @@ void EXTurntable::_writeAnalogue(VPIN vpin, int value, uint8_t activity, uint16_
DIAG(F("EX-Turntable WriteAnalogue Vpin:%d Value:%d Activity:%d Duration:%d"),
vpin, value, activity, duration);
DIAG(F("I2CManager write I2C Address:%d stepsMSB:%d stepsLSB:%d activity:%d"),
_I2CAddress, stepsMSB, stepsLSB, activity);
_I2CAddress.toString(), stepsMSB, stepsLSB, activity);
#endif
_stepperStatus = 1; // Tell the device driver Turntable-EX is busy
I2CManager.write(_I2CAddress, 3, stepsMSB, stepsLSB, activity);
@ -114,7 +114,7 @@ void EXTurntable::_writeAnalogue(VPIN vpin, int value, uint8_t activity, uint16_
// Display Turnetable-EX device driver info.
void EXTurntable::_display() {
DIAG(F("EX-Turntable I2C:x%x Configured on Vpins:%d-%d %S"), _I2CAddress, (int)_firstVpin,
DIAG(F("EX-Turntable I2C:%s Configured on Vpins:%d-%d %S"), _I2CAddress.toString(), (int)_firstVpin,
(int)_firstVpin+_nPins-1, (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F(""));
}

View File

@ -1,129 +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 <Arduino.h>
#include "IO_ExampleSerial.h"
#include "FSH.h"
// Constructor
IO_ExampleSerial::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);
}
// Static create method for one module.
void IO_ExampleSerial::create(VPIN firstVpin, int nPins, HardwareSerial *serial, unsigned long baud) {
if (checkNoOverlap(firstVpin,nPins)) new IO_ExampleSerial(firstVpin, nPins, serial, baud);
}
// Device-specific initialisation
void IO_ExampleSerial::_begin() {
_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 IO_ExampleSerial::_write(VPIN vpin, int value) {
int pin = vpin -_firstVpin;
#ifdef DIAG_IO
DIAG(F("IO_ExampleSerial::_write Pin:%d 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 IO_ExampleSerial::_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 IO_ExampleSerial::_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 < _nPins) { // Store value
_pinValues[_inputIndex] = _inputValue;
}
_inputState = 0; // Done, start again.
} else
_inputState = 0; // Unexpected char, reset
break;
}
}
}
void IO_ExampleSerial::_display() {
DIAG(F("IO_ExampleSerial Configured on VPins:%d-%d"), (int)_firstVpin,
(int)_firstVpin+_nPins-1);
}

View File

@ -35,24 +35,131 @@
#include "IODevice.h"
class IO_ExampleSerial : public IODevice {
public:
static void create(VPIN firstVpin, int nPins, HardwareSerial *serial, unsigned long baud);
protected:
IO_ExampleSerial(VPIN firstVpin, int nPins, HardwareSerial *serial, unsigned long baud);
void _begin() override;
void _loop(unsigned long currentMicros) override;
void _write(VPIN vpin, int value) override;
int _read(VPIN vpin) override;
void _display() override;
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 Pin:%d 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:%d-%d"), (int)_firstVpin,
(int)_firstVpin+_nPins-1);
for (int i=0; i<_nPins; i++)
DIAG(F(" VPin %2d: %d"), _firstVpin+i, _pinValues[i]);
}
};
#endif // IO_EXAMPLESERIAL_H

141
IO_ExternalEEPROM.h Normal file
View File

@ -0,0 +1,141 @@
/*
* © 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 driver monitors the state of turnout objects and writes updates,
* on change of state, to an external 24C128 (16kByte) or 24C256 (32kByte)
* EEPROM device connected via I2C.
*
* When the device is restarted, it repositions the turnouts in accordance
* with the last saved position.
*
* To create a device instance,
* IO_ExternalEEPROM::create(0, 0, i2cAddress);
*
*
*/
#ifndef IO_EXTERNALEEPROM_H
#define IO_EXTERNALEEPROM_H
#include "IODevice.h"
#include "I2CManager.h"
#include "Turnouts.h"
class ExternalEEPROM : public IODevice {
private:
// Here we define the device-specific variables.
int _sizeInKBytes = 128;
Turnout *_turnout = 0;
int _lastTurnoutHash = 0;
I2CRB _rb;
uint8_t _buffer[32]; // 32 is max for Wire write
public:
// Static function to handle "IO_ExampleSerial::create(...)" calls.
static void create(I2CAddress i2cAddress, int sizeInKBytes) {
if (checkNoOverlap(0, 0, i2cAddress)) new ExternalEEPROM(i2cAddress, sizeInKBytes);
}
protected:
// Constructor.
ExternalEEPROM(I2CAddress i2cAddress, int sizeInKBytes) {
_I2CAddress = i2cAddress;
_sizeInKBytes = sizeInKBytes;
// Set up I2C structures.
_rb.setWriteParams(_I2CAddress, _buffer, 32);
addDevice(this);
}
// Device-specific initialisation
void _begin() override {
I2CManager.begin();
I2CManager.setClock(1000000); // Max supported speed
if (I2CManager.exists(_I2CAddress)) {
// Initialise or read contents of EEPROM
// and set turnout states accordingly.
// Read 32 bytes from address 0x0000.
I2CManager.read(_I2CAddress, _buffer, 32, 2, 0, 0);
// Dump data
DIAG(F("EEPROM First 32 bytes:"));
for (int i=0; i<32; i+=8)
DIAG(F("%d: %x %x %x %x %x %x %x %x"),
i, _buffer[i], _buffer[i+1], _buffer[i+2], _buffer[i+3],
_buffer[i+4], _buffer[i+5], _buffer[i+6], _buffer[i+7]);
#if defined(DIAG_IO)
_display();
#endif
} else {
DIAG(F("ExternalEEPROM not found, I2C:%s"), _I2CAddress.toString());
_deviceState = DEVSTATE_FAILED;
}
}
// Loop function to do background scanning of the turnouts
void _loop(unsigned long currentMicros) {
(void)currentMicros; // Suppress compiler warnings
if (_rb.isBusy()) return; // Can't do anything until previous request has completed.
if (_rb.status == I2C_STATUS_NEGATIVE_ACKNOWLEDGE) {
// Device not responding, probably still writing data, so requeue request
I2CManager.queueRequest(&_rb);
return;
}
if (_lastTurnoutHash != Turnout::turnoutlistHash) {
_lastTurnoutHash = Turnout::turnoutlistHash;
// Turnout list has changed, so pointer held from last run may be invalid
_turnout = 0; // Start at the beginning of the list again.
//#if defined(DIAG_IO)
DIAG(F("Turnout Hash Changed!"));
//#endif
}
// Locate next turnout, or first one if there is no current one.
if (_turnout)
_turnout = _turnout->next();
else
_turnout = Turnout::first();
// Retrieve turnout state
int turnoutID = _turnout->getId();
int turnoutState = _turnout->isThrown();
(void)turnoutID; // Suppress compiler warning
(void)turnoutState; // Suppress compiler warning
// TODO: Locate turnoutID in EEPROM (or EEPROM copy) and check if state has changed.
// TODO: If it has, then initiate a write of the updated state to EEPROM
delayUntil(currentMicros+5000); // Write cycle time is 5ms max for FT24C256
}
// Display information about the device.
void _display() {
DIAG(F("ExternalEEPROM %dkBytes I2C:%s %S"), _sizeInKBytes, _I2CAddress.toString(),
_deviceState== DEVSTATE_FAILED ? F("OFFLINE") : F(""));
}
};
#endif // IO_EXTERNALEEPROM_H

View File

@ -34,7 +34,7 @@ class GPIOBase : public IODevice {
protected:
// Constructor
GPIOBase(FSH *deviceName, VPIN firstVpin, uint8_t nPins, uint8_t I2CAddress, int interruptPin);
GPIOBase(FSH *deviceName, VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin);
// Device-specific initialisation
void _begin() override;
// Device-specific pin configuration function.
@ -49,13 +49,13 @@ protected:
// Data fields
// Allocate enough space for all input pins
T _portInputState;
T _portOutputState;
T _portMode;
T _portPullup;
T _portInUse;
// Interval between refreshes of each input port
static const int _portTickTime = 4000;
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;
@ -69,10 +69,6 @@ protected:
I2CRB requestBlock;
FSH *_deviceName;
#if defined(ARDUINO_ARCH_ESP32)
// workaround: Has somehow no min function for all types
static inline T min(T a, int b) { return a < b ? a : b; };
#endif
};
// Because class GPIOBase is a template, the implementation (below) must be contained within the same
@ -80,15 +76,21 @@ protected:
// Constructor
template <class T>
GPIOBase<T>::GPIOBase(FSH *deviceName, VPIN firstVpin, uint8_t nPins, uint8_t I2CAddress, int interruptPin) :
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;
_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>
@ -103,21 +105,16 @@ void GPIOBase<T>::_begin() {
#if defined(DIAG_IO)
_display();
#endif
_portMode = 0; // default to input mode
_portPullup = -1; // default to pullup enabled
_portInputState = -1;
_portInUse = 0;
_setupDevice();
_deviceState = DEVSTATE_NORMAL;
} else {
DIAG(F("%S I2C:x%x Device not detected"), _deviceName, _I2CAddress);
DIAG(F("%S I2C:%s Device not detected"), _deviceName, _I2CAddress.toString());
_deviceState = DEVSTATE_FAILED;
}
}
// Configuration parameters for inputs:
// params[0]: enable pullup
// params[1]: invert input (optional)
template <class T>
bool GPIOBase<T>::_configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) {
if (configType != CONFIGURE_INPUT) return false;
@ -125,7 +122,7 @@ bool GPIOBase<T>::_configure(VPIN vpin, ConfigTypeEnum configType, int paramCoun
bool pullup = params[0];
int pin = vpin - _firstVpin;
#ifdef DIAG_IO
DIAG(F("%S I2C:x%x Config Pin:%d Val:%d"), _deviceName, _I2CAddress, pin, pullup);
DIAG(F("%S I2C:%s Config Pin:%d Val:%d"), _deviceName, _I2CAddress.toString(), pin, pullup);
#endif
uint16_t mask = 1 << pin;
if (pullup)
@ -155,7 +152,7 @@ void GPIOBase<T>::_loop(unsigned long currentMicros) {
_deviceState = DEVSTATE_NORMAL;
} else {
_deviceState = DEVSTATE_FAILED;
DIAG(F("%S I2C:x%x Error:%d %S"), _deviceName, _I2CAddress, status,
DIAG(F("%S I2C:%s Error:%d %S"), _deviceName, _I2CAddress.toString(), status,
I2CManager.getErrorMessage(status));
}
_processCompletion(status);
@ -178,7 +175,7 @@ void GPIOBase<T>::_loop(unsigned long currentMicros) {
#ifdef DIAG_IO
if (differences)
DIAG(F("%S I2C:x%x PortStates:%x"), _deviceName, _I2CAddress, _portInputState);
DIAG(F("%S I2C:%s PortStates:%x"), _deviceName, _I2CAddress.toString(), _portInputState);
#endif
}
@ -199,7 +196,7 @@ void GPIOBase<T>::_loop(unsigned long currentMicros) {
template <class T>
void GPIOBase<T>::_display() {
DIAG(F("%S I2C:x%x Configured on Vpins:%d-%d %S"), _deviceName, _I2CAddress,
DIAG(F("%S I2C:%s Configured on Vpins:%d-%d %S"), _deviceName, _I2CAddress.toString(),
_firstVpin, _firstVpin+_nPins-1, (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F(""));
}
@ -208,7 +205,7 @@ void GPIOBase<T>::_write(VPIN vpin, int value) {
int pin = vpin - _firstVpin;
T mask = 1 << pin;
#ifdef DIAG_IO
DIAG(F("%S I2C:x%x Write Pin:%d Val:%d"), _deviceName, _I2CAddress, pin, value);
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
@ -244,7 +241,7 @@ int GPIOBase<T>::_read(VPIN vpin) {
// Set unused pin and write mode pin value to 1
_portInputState |= ~_portInUse | _portMode;
#ifdef DIAG_IO
DIAG(F("%S I2C:x%x PortStates:%x"), _deviceName, _I2CAddress, _portInputState);
DIAG(F("%S I2C:%s PortStates:%x"), _deviceName, _I2CAddress.toString(), _portInputState);
#endif
}
return (_portInputState & mask) ? 0 : 1; // Invert state (5v=0, 0v=1)

250
IO_HALDisplay.h Normal file
View File

@ -0,0 +1,250 @@
/*
* © 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;
DisplayInterface *_nextDisplay = NULL;
public:
// Static function to handle "HALDisplay::create(...)" calls.
static void create(I2CAddress i2cAddress, int width, int height) {
/* if (checkNoOverlap(i2cAddress)) */ new HALDisplay(0, i2cAddress, width, height);
}
static void create(uint8_t displayNo, I2CAddress i2cAddress, int width, int height) {
/* if (checkNoOverlap(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);
_I2CAddress = i2cAddress;
_width = width;
_height = height;
_numCols = _displayDriver->getNumCols();
_numRows = _displayDriver->getNumRows();
_charPosToScreen = _numCols;
// Allocate arrays
_buffer = (char *)calloc(_numRows*_numCols, sizeof(char));
_rowGeneration = (uint8_t *)calloc(_numRows, sizeof(uint8_t));
_lastRowGeneration = (uint8_t *)calloc(_numRows, sizeof(uint8_t));
// 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 main display?
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 to 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)
_rowNoToScreen = 0; // Wrap to first row
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 be
// sent to the screen on the next loop entry, by which time
// the line should have been written to the buffer.
_rowGeneration[_rowNo]++;
}
// 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

@ -25,14 +25,14 @@
class MCP23008 : public GPIOBase<uint8_t> {
public:
static void create(VPIN firstVpin, uint8_t nPins, uint8_t I2CAddress, int interruptPin=-1) {
if (checkNoOverlap(firstVpin, nPins,I2CAddress)) new MCP23008(firstVpin, nPins, I2CAddress, interruptPin);
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, uint8_t I2CAddress, int interruptPin=-1)
: GPIOBase<uint8_t>((FSH *)F("MCP23008"), firstVpin, min(nPins, (uint8_t)8), I2CAddress, interruptPin) {
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));
@ -60,7 +60,7 @@ private:
if (immediate) {
uint8_t buffer;
I2CManager.read(_I2CAddress, &buffer, 1, 1, REG_GPIO);
_portInputState = buffer;
_portInputState = buffer | _portMode;
} else {
// Queue new request
requestBlock.wait(); // Wait for preceding operation to complete
@ -71,7 +71,7 @@ private:
// 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];
_portInputState = inputBuffer[0] | _portMode;
else
_portInputState = 0xff;
}

View File

@ -30,14 +30,14 @@
class MCP23017 : public GPIOBase<uint16_t> {
public:
static void create(VPIN vpin, int nPins, uint8_t I2CAddress, int interruptPin=-1) {
if (checkNoOverlap(vpin, nPins, I2CAddress)) new MCP23017(vpin, min(nPins,16), I2CAddress, interruptPin);
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, int nPins, uint8_t I2CAddress, int interruptPin=-1)
: GPIOBase<uint16_t>((FSH *)F("MCP23017"), vpin, nPins, I2CAddress, interruptPin)
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));
@ -65,7 +65,7 @@ private:
if (immediate) {
uint8_t buffer[2];
I2CManager.read(_I2CAddress, buffer, 2, 1, REG_GPIOA);
_portInputState = ((uint16_t)buffer[1]<<8) | buffer[0];
_portInputState = ((uint16_t)buffer[1]<<8) | buffer[0] | _portMode;
} else {
// Queue new request
requestBlock.wait(); // Wait for preceding operation to complete
@ -76,7 +76,7 @@ private:
// 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];
_portInputState = (((uint16_t)inputBuffer[1]<<8) | inputBuffer[0]) | _portMode;
else
_portInputState = 0xffff;
}

View File

@ -31,22 +31,21 @@ 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 uint8_t PRESCALE_50HZ = (uint8_t)(((FREQUENCY_OSCILLATOR / (50.0 * 4096.0)) + 0.5) - 1);
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, uint8_t I2CAddress) {
if (checkNoOverlap(firstVpin, nPins,I2CAddress)) new PCA9685(firstVpin, nPins, I2CAddress);
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
#if DIAG_IO >= 3
DIAG(F("PCA9685 Configure VPIN:%d Apos:%d Ipos:%d Profile:%d Duration:%d state:%d"),
vpin, params[0], params[1], params[2], params[3], params[4]);
#endif
@ -73,10 +72,14 @@ bool PCA9685::_configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, i
}
// Constructor
PCA9685::PCA9685(VPIN firstVpin, int nPins, uint8_t I2CAddress) {
PCA9685::PCA9685(VPIN firstVpin, int nPins, I2CAddress i2cAddress, uint16_t frequency) {
_firstVpin = firstVpin;
_nPins = min(nPins, 16);
_I2CAddress = I2CAddress;
_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++)
@ -98,7 +101,7 @@ void PCA9685::_begin() {
// Initialise I/O module here.
if (I2CManager.exists(_I2CAddress)) {
writeRegister(_I2CAddress, PCA9685_MODE1, MODE1_SLEEP | MODE1_AI);
writeRegister(_I2CAddress, PCA9685_PRESCALE, PRESCALE_50HZ); // 50Hz clock, 20ms pulse period.
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
@ -114,7 +117,7 @@ void PCA9685::_begin() {
// 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
#if DIAG_IO >= 3
DIAG(F("PCA9685 Write Vpin:%d Value:%d"), vpin, value);
#endif
int pin = vpin - _firstVpin;
@ -141,7 +144,7 @@ void PCA9685::_write(VPIN vpin, int value) {
// 4 (Bounce) Servo 'bounces' at extremes.
//
void PCA9685::_writeAnalogue(VPIN vpin, int value, uint8_t profile, uint16_t duration) {
#ifdef DIAG_IO
#if DIAG_IO >= 3
DIAG(F("PCA9685 WriteAnalogue Vpin:%d Value:%d Profile:%d Duration:%d %S"),
vpin, value, profile, duration, _deviceState == DEVSTATE_FAILED?F("DEVSTATE_FAILED"):F(""));
#endif
@ -239,13 +242,13 @@ void PCA9685::updatePosition(uint8_t pin) {
// 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:x%x WriteDevice Pin:%d Value:%d"), _I2CAddress, pin, value);
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:x%x failed %S"), _I2CAddress, I2CManager.getErrorMessage(status));
DIAG(F("PCA9685 I2C:%s failed %S"), _I2CAddress.toString(), I2CManager.getErrorMessage(status));
} else {
// Set up new request.
outputBuffer[0] = PCA9685_FIRST_SERVO + 4 * pin;
@ -259,7 +262,7 @@ void PCA9685::writeDevice(uint8_t pin, int value) {
// Display details of this device.
void PCA9685::_display() {
DIAG(F("PCA9685 I2C:x%x Configured on Vpins:%d-%d %S"), _I2CAddress, (int)_firstVpin,
DIAG(F("PCA9685 I2C:%s Configured on Vpins:%d-%d %S"), _I2CAddress.toString(), (int)_firstVpin,
(int)_firstVpin+_nPins-1, (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F(""));
}
@ -272,5 +275,5 @@ static void writeRegister(byte address, byte reg, byte value) {
// 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 byte FLASH PCA9685::_bounceProfile[30] =
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};

170
IO_PCA9685pwm.h Normal file
View File

@ -0,0 +1,170 @@
/*
* © 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
#if DIAG_IO >= 3
DIAG(F("PCA9685pwm WriteAnalogue Vpin:%d 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:%d-%d %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) {
#if DIAG_IO >= 3
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

@ -43,20 +43,22 @@
class PCF8574 : public GPIOBase<uint8_t> {
public:
static void create(VPIN firstVpin, uint8_t nPins, uint8_t I2CAddress, int interruptPin=-1) {
if (checkNoOverlap(firstVpin, nPins,I2CAddress)) new PCF8574(firstVpin, nPins, I2CAddress, interruptPin);
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, uint8_t I2CAddress, int interruptPin=-1)
: GPIOBase<uint8_t>((FSH *)F("PCF8574"), firstVpin, min(nPins, (uint8_t)8), I2CAddress, interruptPin)
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 pin state is '1' if the pin is an input or if it is an output set to 1. Zero otherwise.
// 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);
I2CManager.write(_I2CAddress, 1, (_portOutputState | ~_portMode) & _portInUse);
}
// The PCF8574 handles inputs by applying a weak pull-up when output is driven to '1'.
@ -64,9 +66,8 @@ private:
// 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 {
I2CManager.write(_I2CAddress, 1, _portOutputState | ~_portMode);
_writeGpioPort();
}
// In immediate mode, _readGpioPort reads the device GPIO port and updates _portInputState accordingly.
@ -76,7 +77,7 @@ private:
if (immediate) {
uint8_t buffer[1];
I2CManager.read(_I2CAddress, buffer, 1);
_portInputState = buffer[0];
_portInputState = buffer[0] | _portMode;
} else {
requestBlock.wait(); // Wait for preceding operation to complete
// Issue new request to read GPIO register
@ -87,7 +88,7 @@ private:
// 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];
_portInputState = inputBuffer[0] | _portMode;
else
_portInputState = 0xff;
}

109
IO_PCF8575.h Normal file
View File

@ -0,0 +1,109 @@
/*
* © 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

@ -104,7 +104,7 @@ private:
}
void _display() override {
DIAG(F("Rotary Encoder I2C:x%x v%d.%d.%d Configured on Vpin:%d-%d %S"), _I2CAddress, _majorVer, _minorVer, _patchVer,
DIAG(F("Rotary Encoder I2C:%s v%d.%d.%d Configured on Vpin:%d-%d %S"), _I2CAddress.toString(), _majorVer, _minorVer, _patchVer,
(int)_firstVpin, _firstVpin+_nPins-1, (_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F(""));
}

33
IO_Servo.cpp Normal file
View File

@ -0,0 +1,33 @@
/*
* © 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};

298
IO_Servo.h Normal file
View File

@ -0,0 +1,298 @@
/*
* © 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:%d 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 pins %d-%d"),
_firstSlavePin, _firstSlavePin+_nPins-1);
_deviceState = DEVSTATE_FAILED;
}
if (_slaveDevice != findDevice(_firstSlavePin+_nPins-1)) {
DIAG(F("Servo: Slave device does not cover all pins %d-%d"),
_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:%d 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:%d 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:%d-%d, slave pins:%d-%d %S"),
(int)_firstVpin, (int)_firstVpin+_nPins-1,
(int)_firstSlavePin, (int)_firstSlavePin+_nPins-1,
(_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F(""));
}
};
#endif

256
IO_TFTDisplay.h Normal file
View File

@ -0,0 +1,256 @@
/*
* © 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 way of driving a ST7735 TFT display through SCREEN(disp,line,"text").
* 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:
* TFTDisplay::create(address, width, height);
*
* where address is the I2C address (0x3c or 0x3d),
* width is the width in pixels of the display, and
* height is the height in pixels of the display.
*
*/
#ifndef IO_TFTDISPLAY_H
#define IO_TFTDDISPLAY_H
#include "IODevice.h"
#include "DisplayInterface.h"
#include "version.h"
template <class T>
class TFTDisplay : public IODevice, public DisplayInterface {
private:
uint8_t _displayNo = 0;
// 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;
DisplayInterface *_nextDisplay = NULL;
uint8_t _selectedDisplayNo = 0;
public:
// Static function to handle "TFTDisplay::create(...)" calls.
static void create(I2CAddress i2cAddress, int width = 128, int height=64) {
/* if (checkNoOverlap(i2cAddress)) */ new TFTDisplay(0, i2cAddress, width, height);
}
static void create(uint8_t displayNo, I2CAddress i2cAddress, int width = 128, int height=64) {
/* if (checkNoOverlap(i2cAddress)) */ new TFTDisplay(displayNo, i2cAddress, width, height);
}
protected:
// Constructor
TFTDisplay(uint8_t displayNo, I2CAddress i2cAddress, int width, int height) {
_displayDriver = new T(i2cAddress, width, height);
_displayNo = displayNo;
_I2CAddress = i2cAddress;
_width = width;
_height = height;
_numCols = (_width+5) / 6; // character block 6 x 8, round up
_numRows = _height / 8; // Round down
_charPosToScreen = _numCols;
// Allocate arrays
_buffer = (char *)calloc(_numRows*_numCols, sizeof(char));
_rowGeneration = (uint8_t *)calloc(_numRows, sizeof(uint8_t));
_lastRowGeneration = (uint8_t *)calloc(_numRows, sizeof(uint8_t));
// Fill buffer with spaces
memset(_buffer, ' ', _numCols*_numRows);
_displayDriver->clearNative();
// Is this the main display?
if (_displayNo == 0) {
// Set first two lines on screen
this->setRow(0, 0);
print(F("DCC-EX v"));
print(F(VERSION));
setRow(0, 1);
print(F("Lic GPLv3"));
}
// Store pointer to this object into CS display hook, so that we
// will intercept any subsequent calls to displayHandler methods.
// Make a note of the existing display reference, to that we can
// pass on anything we're not interested in.
_nextDisplay = DisplayInterface::displayHandler;
DisplayInterface::displayHandler = this;
addDevice(this);
}
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 to anything until the next entry.
if (!_displayDriver->isBusy()) {
// Check if we've just done the end of a row or just started
if (_charPosToScreen >= _numCols) {
// Move to next line
if (++_rowNoToScreen >= _numRows)
_rowNoToScreen = 0; // Wrap to first row
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()) {
DIAG(F("TFTDisplay installed on address %s as screen %d"),
_I2CAddress.toString(), _displayNo);
// Force all rows to be redrawn
for (uint8_t row=0; row<_numRows; row++)
_rowGeneration[row]++;
// Start with top line (looks better)
_rowNoToScreen = _numRows;
_charPosToScreen = _numCols;
}
}
void _loop(unsigned long) override {
screenUpdate();
}
/////////////////////////////////////////////////
// DisplayInterface functions
//
/////////////////////////////////////////////////
public:
void loop() override {
screenUpdate();
if (_nextDisplay)
_nextDisplay->loop(); // continue to next display
return;
}
// 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(uint8_t displayNo, byte line) override {
_selectedDisplayNo = displayNo;
if (displayNo == _displayNo) {
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 be
// sent to the screen on the next loop entry, by which time
// the line should have been written to the buffer.
_rowGeneration[_rowNo]++;
}
if (_nextDisplay)
_nextDisplay->setRow(displayNo, line); // Pass to next display
}
// Write one character to the screen referenced in the last setRow() call.
size_t write(uint8_t c) override {
if (_selectedDisplayNo == _displayNo) {
// Write character to buffer (if there's space)
if (_colNo < _numCols) {
_buffer[_rowNo*_numCols+_colNo++] = c;
}
}
if (_nextDisplay)
_nextDisplay->write(c);
return 1;
}
// Write blanks to all of the screen (blocks until complete)
void clear (uint8_t displayNo) override {
if (displayNo == _displayNo) {
// Clear buffer
for (_rowNo = 0; _rowNo < _numRows; _rowNo++) {
setRow(displayNo, _rowNo);
}
_rowNo = 0;
}
if (_nextDisplay)
_nextDisplay->clear(displayNo); // Pass to next display
}
// Display information about the device.
void _display() {
DIAG(F("TFTDisplay %d Configured addr %s"), _displayNo, _I2CAddress.toString());
}
};
#endif // IO_TFTDDISPLAY_H

View File

@ -42,14 +42,17 @@
* 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 by
* setting XSHUT to HIGH to turn the module on,, then writing the module's desired I2C address.
* 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.
* 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 is very prone to noise,
* and the device may even reset when handled. If you're not using XSHUT, then it's
* best to tie it to +5V.
* 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:
*
@ -70,7 +73,8 @@
* 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.
* 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:
@ -93,7 +97,6 @@
class VL53L0X : public IODevice {
private:
uint8_t _i2cAddress;
uint16_t _ambient;
uint16_t _distance;
uint16_t _signal;
@ -101,20 +104,23 @@ private:
uint16_t _offThreshold;
VPIN _xshutPin;
bool _value;
uint8_t _nextState = 0;
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 = 0,
STATE_CONFIGUREADDRESS = 1,
STATE_SKIP = 2,
STATE_CONFIGUREDEVICE = 3,
STATE_INITIATESCAN = 4,
STATE_CHECKSTATUS = 5,
STATE_GETRESULTS = 6,
STATE_DECODERESULTS = 7,
STATE_INIT,
STATE_RESTARTMODULE,
STATE_CONFIGUREADDRESS,
STATE_CONFIGUREDEVICE,
STATE_INITIATESCAN,
STATE_CHECKSTATUS,
STATE_GETRESULTS,
STATE_DECODERESULTS,
STATE_FAILED,
};
// Register addresses
@ -129,15 +135,15 @@ private:
public:
static void create(VPIN firstVpin, int nPins, uint8_t i2cAddress, uint16_t onThreshold, uint16_t offThreshold, VPIN xshutPin = VPIN_NONE) {
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, uint8_t i2cAddress, uint16_t onThreshold, uint16_t offThreshold, VPIN xshutPin = VPIN_NONE) {
VL53L0X(VPIN firstVpin, int nPins, I2CAddress i2cAddress, uint16_t onThreshold, uint16_t offThreshold, VPIN xshutPin = VPIN_NONE) {
_firstVpin = firstVpin;
_nPins = min(nPins, 3);
_i2cAddress = i2cAddress;
_nPins = (nPins > 3) ? 3 : nPins;
_I2CAddress = i2cAddress;
_onThreshold = onThreshold;
_offThreshold = offThreshold;
_xshutPin = xshutPin;
@ -145,81 +151,108 @@ protected:
addDevice(this);
}
void _begin() override {
if (_xshutPin == VPIN_NONE) {
// Check if device is already responding on the nominated address.
if (I2CManager.exists(_i2cAddress)) {
// Yes, it's already on this address, so skip the address initialisation.
_nextState = STATE_CONFIGUREDEVICE;
} else {
_nextState = STATE_INIT;
}
}
// 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:
// On first entry to loop, reset this module by pulling XSHUT low. All modules
// will be reset in turn.
if (_xshutPin != VPIN_NONE) IODevice::write(_xshutPin, 0);
_nextState = STATE_CONFIGUREADDRESS;
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:
// On second entry, set XSHUT pin high to allow the module to restart.
// On the module, there is a diode in series with the XSHUT pin to
// protect the low-voltage pin against +5V.
if (_xshutPin != VPIN_NONE) IODevice::write(_xshutPin, 1);
// Allow the module time to restart
delay(10);
// Then write the desired I2C address to the device, while this is the only
// module responding to the default address.
I2CManager.write(VL53L0X_I2C_DEFAULT_ADDRESS, 2, VL53L0X_REG_I2C_SLAVE_DEVICE_ADDRESS, _i2cAddress);
_nextState = STATE_SKIP;
break;
case STATE_SKIP:
// Do nothing on the third entry.
{
#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:
// On next entry, check if device address has been set.
if (I2CManager.exists(_i2cAddress)) {
// 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
write_reg(VL53L0X_CONFIG_PAD_SCL_SDA__EXTSUP_HV,
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:x%x device not responding"), _i2cAddress);
DIAG(F("VL53L0X I2C:%s device not responding"), _I2CAddress.toString());
_deviceState = DEVSTATE_FAILED;
_nextState = STATE_FAILED;
}
_nextState = STATE_INITIATESCAN;
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);
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) {
DIAG(F("VL53L0X I2C:x%x Error:%d %S"), _i2cAddress, status, I2CManager.getErrorMessage(status));
_deviceState = DEVSTATE_FAILED;
_value = false;
reportError(status);
} else
_nextState = 2;
_nextState = STATE_GETRESULTS;
delayUntil(currentMicros + 95000); // wait for 95 ms before checking.
_nextState = STATE_GETRESULTS;
break;
case STATE_GETRESULTS:
// Ranging completed. Request results
_outBuffer[0] = VL53L0X_REG_RESULT_RANGE_STATUS;
I2CManager.read(_i2cAddress, _inBuffer, 12, _outBuffer, 1, &_rb);
_nextState = 3;
I2CManager.read(_I2CAddress, _inBuffer, 12, _outBuffer, 1, &_rb);
delayUntil(currentMicros + 5000); // Allow 5ms to get data
_nextState = STATE_DECODERESULTS;
break;
@ -240,15 +273,28 @@ protected:
else if (_distance > _offThreshold)
_value = false;
}
// Completed. Restart scan on next loop entry.
_nextState = STATE_INITIATESCAN;
} else {
reportError(status);
}
// Completed. Restart scan on next loop entry.
_nextState = STATE_INITIATESCAN;
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;
@ -273,8 +319,8 @@ protected:
}
void _display() override {
DIAG(F("VL53L0X I2C:x%x Configured on Vpins:%d-%d On:%dmm Off:%dmm %S"),
_i2cAddress, _firstVpin, _firstVpin+_nPins-1, _onThreshold, _offThreshold,
DIAG(F("VL53L0X I2C:%s Configured on Vpins:%d-%d On:%dmm Off:%dmm %S"),
_I2CAddress.toString(), _firstVpin, _firstVpin+_nPins-1, _onThreshold, _offThreshold,
(_deviceState==DEVSTATE_FAILED) ? F("OFFLINE") : F(""));
}
@ -288,13 +334,15 @@ private:
uint8_t outBuffer[2];
outBuffer[0] = reg;
outBuffer[1] = data;
return I2CManager.write(_i2cAddress, outBuffer, 2);
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);
I2CManager.read(_I2CAddress, _inBuffer, 1, &reg, 1);
return _inBuffer[0];
}
};
bool VL53L0X::_addressConfigInProgress = false;
#endif // IO_VL53L0X_h

136
IO_Wire.h Normal file
View File

@ -0,0 +1,136 @@
/*
* © 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/>.
*/
/*
* The purpose of this module is to provide an interface to the DCC
* I2CManager that is compatible with code written for the Arduino
* 'Wire' interface.
*
* To use it, just replace
* #include "Wire.h" or #include <Wire.h>
* with
* #include "IO_Wire.h"
*
* Note that the CS only supports I2C master mode, so the calls related to
* slave mode are not implemented here.
*
*/
#ifndef IO_WIRE
#define IO_WIRE
#include "IODevice.h"
#ifndef I2C_USE_WIRE
class IO_Wire : public IODevice, public Stream {
public:
IO_Wire() {
addDevice(this);
};
void begin() {
I2CManager.begin();
}
void setClock(uint32_t speed) {
I2CManager.setClock(speed);
}
void beginTransmission(uint8_t address) {
i2cAddress = address;
outputLength = 0;
}
size_t write(byte value) override {
if (outputLength < sizeof(outputBuffer)) {
outputBuffer[outputLength++] = value;
return 1;
} else
return 0;
}
size_t write(const uint8_t *buffer, size_t size) override {
for (size_t i=0; i<size; i++) {
if (!write(buffer[i])) return i;
}
return size;
}
uint8_t endTransmission(bool) {
// As this software doesn't run in a multi-master environment, there
// is no advantage to holding the bus between transactions. Therefore,
// for simplicity, a stop condition is always sent.
return I2CManager.write(i2cAddress, outputBuffer, outputLength);
}
uint8_t requestFrom(uint8_t address, uint8_t readSize, uint8_t sendStop) {
(void)sendStop; // suppress compiler warning
uint8_t status = I2CManager.read(address, inputBuffer, readSize);
inputPos = 0;
inputLength = readSize;
return status;
}
uint8_t requestFrom(uint8_t address, uint8_t quantity)
{
return requestFrom((uint8_t)address, (uint8_t)quantity, (uint8_t)true);
}
uint8_t requestFrom(int address, int quantity)
{
return requestFrom((uint8_t)address, (uint8_t)quantity, (uint8_t)true);
}
uint8_t requestFrom(int address, int quantity, int sendStop)
{
return requestFrom((uint8_t)address, (uint8_t)quantity, (uint8_t)sendStop);
}
int read() override {
if (inputPos < inputLength)
return inputBuffer[inputPos++];
else
return -1;
}
int available() override {
return (inputPos < inputLength);
}
int peek() override {
if (inputPos < inputLength)
return inputBuffer[inputPos];
else
return -1;
}
uint8_t endTransmission() {
return endTransmission(true);
}
static IO_Wire Wire();
protected:
void _begin() { }
void _display() {
DIAG(F("I2CManager Wire Interface"));
}
private:
uint8_t outputBuffer[32];
uint8_t outputLength = 0;
uint8_t inputBuffer[32];
uint8_t inputLength = 0;
uint8_t inputPos = 0;
uint8_t i2cAddress;
};
static IO_Wire Wire;
#else
#include <Wire.h>
#endif
#endif

View File

@ -1,167 +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 "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;
// If output device is busy, don't do anything on this loop
// This avoids blocking while waiting for the device to complete.
if (isBusy()) 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();
}

View File

@ -1,81 +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 LCDDisplay_h
#define LCDDisplay_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 LCDisplay_Implementation.h
class LCDDisplay : public DisplayInterface {
public:
LCDDisplay() {};
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
// Internally handled functions
static void loop();
LCDDisplay* loop2(bool force) override;
void setRow(byte line) override;
void clear() override;
size_t write(uint8_t b) override;
protected:
uint8_t lcdRows;
uint8_t lcdCols;
private:
void moveToNextRow();
void skipBlankRows();
// Relay functions to the live driver in the subclass
virtual void clearNative() = 0;
virtual void setRowNative(byte line) = 0;
virtual size_t writeNative(uint8_t b) = 0;
virtual bool isBusy() = 0;
unsigned long lastScrollTime = 0;
int8_t hotRow = 0;
int8_t hotCol = 0;
int8_t topRow = 0;
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

@ -41,27 +41,28 @@
// can't assume that its in that state when a sketch starts (and the
// LiquidCrystal constructor is called).
LiquidCrystal_I2C::LiquidCrystal_I2C(uint8_t lcd_Addr, uint8_t lcd_cols,
LiquidCrystal_I2C::LiquidCrystal_I2C(I2CAddress lcd_Addr, uint8_t lcd_cols,
uint8_t lcd_rows) {
_Addr = lcd_Addr;
lcdRows = lcd_rows;
lcdCols = lcd_cols;
lcdRows = lcd_rows; // Number of character rows (typically 2 or 4).
lcdCols = lcd_cols; // Number of character columns (typically 16 or 20)
_backlightval = 0;
}
bool LiquidCrystal_I2C::begin() {
I2CManager.begin();
I2CManager.setClock(100000L); // PCF8574 is spec'd to 100kHz.
if (I2CManager.exists(lcd_Addr)) {
DIAG(F("%dx%d LCD configured on I2C:x%x"), (int)lcd_cols, (int)lcd_rows, (int)lcd_Addr);
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;
begin();
backlight();
lcdDisplay = this;
} else {
DIAG(F("LCD not found on I2C:%s"), _Addr.toString());
return false;
}
}
void LiquidCrystal_I2C::begin() {
if (lcdRows > 1) {
_displayfunction |= LCD_2LINE;
}
@ -79,15 +80,15 @@ void LiquidCrystal_I2C::begin() {
// we start in 8bit mode, try to set 4 bit mode
write4bits(0x03);
delayMicroseconds(4500); // wait min 4.1ms
delayMicroseconds(5000); // wait min 4.1ms
// second try
write4bits(0x03);
delayMicroseconds(4500); // wait min 4.1ms
delayMicroseconds(5000); // wait min 4.1ms
// third go!
write4bits(0x03);
delayMicroseconds(150);
delayMicroseconds(5000);
// finally, set to 4-bit interface
write4bits(0x02);
@ -99,26 +100,23 @@ void 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);
setRowNative(0);
return true;
}
/********** high level commands, for the user! */
void LiquidCrystal_I2C::clearNative() {
command(LCD_CLEARDISPLAY); // clear display, set cursor position to zero
delayMicroseconds(2000); // this command takes 1.52ms
delayMicroseconds(2000); // this command takes 1.52ms but allow plenty
}
void LiquidCrystal_I2C::setRowNative(byte row) {
int row_offsets[] = {0x00, 0x40, 0x14, 0x54};
uint8_t row_offsets[] = {0x00, 0x40, 0x14, 0x54};
if (row >= lcdRows) {
row = lcdRows - 1; // we count rows starting w/0
}
@ -146,6 +144,10 @@ size_t LiquidCrystal_I2C::writeNative(uint8_t value) {
return 1;
}
bool LiquidCrystal_I2C::isBusy() {
return rb.isBusy();
}
/*********** mid level commands, for sending data/cmds */
inline void LiquidCrystal_I2C::command(uint8_t value) {
@ -192,11 +194,12 @@ void LiquidCrystal_I2C::send(uint8_t value, uint8_t mode) {
uint8_t lownib = ((value & 0x0f) << BACKPACK_DATA_BITS) | 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); // Write command synchronously
I2CManager.write(_Addr, outputBuffer, len, &rb); // Write command asynchronously
}
// write 4 data bits to the HD44780 LCD controller.
@ -206,14 +209,16 @@ void LiquidCrystal_I2C::write4bits(uint8_t value) {
// 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); // Write command synchronously
I2CManager.write(_Addr, outputBuffer, len, &rb); // Write command asynchronously
}
// 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); // Write command synchronously
I2CManager.write(_Addr, outputBuffer, 1, &rb); // Write command asynchronously
}

View File

@ -22,7 +22,7 @@
#define LiquidCrystal_I2C_h
#include <Arduino.h>
#include "LCDDisplay.h"
#include "Display.h"
#include "I2CManager.h"
// commands
@ -62,33 +62,38 @@
#define Rw (1 << BACKPACK_Rw_BIT) // Read/Write bit
#define Rs (1 << BACKPACK_Rs_BIT) // Register select bit
class LiquidCrystal_I2C : public LCDDisplay {
class LiquidCrystal_I2C : public DisplayDevice {
public:
LiquidCrystal_I2C(uint8_t lcd_Addr,uint8_t lcd_cols,uint8_t lcd_rows);
void begin();
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;
void display();
void noBacklight();
void backlight();
void command(uint8_t);
uint16_t getNumCols() { return lcdCols; }
uint16_t getNumRows() { return lcdRows; }
private:
void send(uint8_t, uint8_t);
void write4bits(uint8_t);
void expanderWrite(uint8_t);
uint8_t _Addr;
uint8_t lcdCols=0, lcdRows=0;
I2CAddress _Addr;
uint8_t _displayfunction;
uint8_t _displaycontrol;
uint8_t _displaymode;
uint8_t _backlightval;
uint8_t _backlightval = 0;
uint8_t outputBuffer[4];
// I/O is synchronous, so if this is called we're not busy!
bool isBusy() override { return false; }
I2CRB rb;
};
#endif

View File

@ -144,16 +144,12 @@ bool MotorDriver::isPWMCapable() {
void MotorDriver::setPower(POWERMODE mode) {
bool on=mode==POWERMODE::ON;
if (on) {
noInterrupts();
IODevice::write(powerPin,invertPower ? LOW : HIGH);
interrupts();
if (isProgTrack)
DCCWaveform::progTrack.clearResets();
}
else {
noInterrupts();
IODevice::write(powerPin,invertPower ? HIGH : LOW);
interrupts();
}
powerMode=mode;
}

View File

@ -485,10 +485,10 @@
<text x="32.86" y="598.05" class="st4" v:langID="1033"><v:paragraph v:horizAlign="1"/><v:tabList/>Accessories <v:newlineChar/><tspan
x="30.42" dy="1.2em" class="st5">(</tspan>Output.cpp)</text> </g>
</a>
<a xlink:href="https://github.com/DCC-EX/CommandStation-EX/blob/master/LCDDisplay.cpp">
<a xlink:href="https://github.com/DCC-EX/CommandStation-EX/blob/master/Display.cpp">
<g id="shape14-81" v:mID="14" v:groupContext="shape" v:layerMember="0" transform="translate(288,-116.156)">
<title>Process.14</title>
<desc>Other Utilities (LCDDisplay.cpp)</desc>
<desc>Other Utilities (Display.cpp)</desc>
<v:custProps>
<v:cp v:nameU="Cost" v:lbl="Cost" v:prompt="" v:type="7" v:format="@" v:sortKey="" v:invis="false"
v:ask="false" v:langID="1033" v:cal="0"/>
@ -522,7 +522,7 @@
</g>
<rect x="0" y="580.5" width="90" height="31.5" rx="13.5" ry="13.5" class="st3"/>
<text x="19.29" y="593.55" class="st4" v:langID="1033"><v:paragraph v:horizAlign="1"/><v:tabList/>Other Utilities<v:newlineChar/><tspan
x="14.29" dy="1.2em" class="st5">(</tspan>LCDDisplay.cpp)</text> </g>
x="14.29" dy="1.2em" class="st5">(</tspan>Display.cpp)</text> </g>
</a>
<g id="shape3-88" v:mID="3" v:groupContext="shape" v:layerMember="1" transform="translate(108,-443.812)">
<title>Dynamic connector</title>

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

View File

@ -83,7 +83,7 @@ size_t RingStream::printFlash(const FSH * flashBuffer) {
// Establish the actual length of the progmem string.
char * flash=(char *)flashBuffer;
int16_t plength=strlen_P(flash);
int16_t plength=STRLEN_P(flash);
if (plength==0) return 0; // just ignore empty string
// Retain the buffer count as it will be modified by the marker+address insert

View File

@ -143,56 +143,69 @@ const uint8_t FLASH SSD1306AsciiWire::SH1106_132x64init[] = {
// SSD1306AsciiWire Method Definitions
//------------------------------------------------------------------------------
// Constructor
SSD1306AsciiWire::SSD1306AsciiWire(int width, int height) {
// Auto-detect address
SSD1306AsciiWire::SSD1306AsciiWire(int width, int height)
: SSD1306AsciiWire(0, width, height) { }
// Constructor with explicit address
SSD1306AsciiWire::SSD1306AsciiWire(I2CAddress address, int width, int height) {
m_i2cAddr = address;
m_displayWidth = width;
m_displayHeight = height;
// Set size in characters in base class
lcdRows = height / 8;
lcdCols = width / 6;
// Set size in characters
m_charsPerColumn = m_displayHeight / fontHeight;
m_charsPerRow = (m_displayWidth+fontWidth-1) / fontWidth; // Round up
}
bool SSD1306AsciiWire::begin() {
I2CManager.begin();
I2CManager.setClock(400000L); // Set max supported I2C speede
if (m_i2cAddr == 0) {
// Probe for I2C device on 0x3c and 0x3d.
for (uint8_t address = 0x3c; address <= 0x3d; address++) {
if (I2CManager.exists(address)) {
m_i2cAddr = address;
break;
}
}
if (m_i2cAddr == 0)
DIAG(F("OLED display not found"));
}
m_col = 0;
m_row = 0;
m_colOffset = 0;
I2CManager.begin();
I2CManager.setClock(400000L); // Set max supported I2C speed
for (byte address = 0x3c; address <= 0x3d; address++) {
if (I2CManager.exists(address)) {
m_i2cAddr = address;
if (m_displayWidth==132 && m_displayHeight==64) {
// SH1106 display. This uses 128x64 centered within a 132x64 OLED.
m_colOffset = 2;
I2CManager.write_P(address, SH1106_132x64init, sizeof(SH1106_132x64init));
} else if (m_displayWidth==128 && (m_displayHeight==64 || m_displayHeight==32)) {
// SSD1306 128x64 or 128x32
I2CManager.write_P(address, Adafruit128xXXinit, sizeof(Adafruit128xXXinit));
if (m_displayHeight == 32)
I2CManager.write(address, 5, 0, // Set command mode
SSD1306_SETMULTIPLEX, 0x1F, // ratio 32
SSD1306_SETCOMPINS, 0x02); // sequential COM pins, disable remap
} else {
DIAG(F("OLED configuration option not recognised"));
return;
}
// Device found
DIAG(F("%dx%d OLED display configured on I2C:x%x"), width, height, address);
// Set singleton address
lcdDisplay = this;
clear();
return;
}
if (m_displayWidth==132 && m_displayHeight==64) {
// SH1106 display. This uses 128x64 centered within a 132x64 OLED.
m_colOffset = 2;
I2CManager.write_P(m_i2cAddr, SH1106_132x64init, sizeof(SH1106_132x64init));
} else if (m_displayWidth==128 && (m_displayHeight==64 || m_displayHeight==32)) {
// SSD1306 or SSD1309 128x64 or 128x32
I2CManager.write_P(m_i2cAddr, Adafruit128xXXinit, sizeof(Adafruit128xXXinit));
if (m_displayHeight == 32)
I2CManager.write(m_i2cAddr, 5, 0, // Set command mode
SSD1306_SETMULTIPLEX, 0x1F, // ratio 32
SSD1306_SETCOMPINS, 0x02); // sequential COM pins, disable remap
} else {
DIAG(F("OLED configuration option not recognised"));
return false;
}
DIAG(F("OLED display not found"));
// Device found
DIAG(F("%dx%d OLED display configured on I2C:%s"), m_displayWidth, m_displayHeight, m_i2cAddr.toString());
return true;
}
/* Clear screen by writing blank pixels. */
void SSD1306AsciiWire::clearNative() {
const int maxBytes = sizeof(blankPixels); // max number of bytes sendable over Wire
const int maxBytes = sizeof(blankPixels) - 1; // max number of pixel columns (bytes) per transmission
for (uint8_t r = 0; r <= m_displayHeight/8 - 1; r++) {
setRowNative(r); // Position at start of row to be erased
for (uint8_t c = 0; c <= m_displayWidth - 1; c += maxBytes-1) {
uint8_t len = min(m_displayWidth-c, maxBytes-1) + 1;
I2CManager.write_P(m_i2cAddr, blankPixels, len); // Write a number of blank columns
for (uint8_t c = 0; c < m_displayWidth; c += maxBytes) {
uint8_t len = m_displayWidth-c; // Number of pixel columns remaining
if (len > maxBytes) len = maxBytes;
I2CManager.write_P(m_i2cAddr, blankPixels, len+1); // Write command + 'len' blank columns
}
}
}
@ -245,127 +258,262 @@ size_t SSD1306AsciiWire::writeNative(uint8_t ch) {
outputBuffer[0] = 0x40; // set SSD1306 controller to data mode
uint8_t bufferPos = 1;
// Copy character pixel columns
for (uint8_t i = 0; i < fontWidth; i++)
outputBuffer[bufferPos++] = GETFLASH(base++);
// Add blank pixels between letters
for (uint8_t i = 0; i < letterSpacing; i++)
outputBuffer[bufferPos++] = 0;
for (uint8_t i = 0; i < fontWidth; i++) {
if (m_col++ < m_displayWidth)
outputBuffer[bufferPos++] = GETFLASH(base++);
}
// Write the data to I2C display
I2CManager.write(m_i2cAddr, outputBuffer, bufferPos, &requestBlock);
m_col += fontWidth + letterSpacing;
return 1;
}
//------------------------------------------------------------------------------
// Font characters, 5x7 pixels, 0x61 characters starting at 0x20.
// Font characters, 6x8 pixels, starting at 0x20.
// Lower case characters optionally omitted.
const uint8_t FLASH SSD1306AsciiWire::System5x7[] = {
const uint8_t FLASH SSD1306AsciiWire::System6x8[] = {
// Fixed width; char width table not used !!!!
// or with lowercase character omitted.
// font data
0x00, 0x00, 0x00, 0x00, 0x00, // (space)
0x00, 0x00, 0x5F, 0x00, 0x00, // !
0x00, 0x07, 0x00, 0x07, 0x00, // "
0x14, 0x7F, 0x14, 0x7F, 0x14, // #
0x24, 0x2A, 0x7F, 0x2A, 0x12, // $
0x23, 0x13, 0x08, 0x64, 0x62, // %
0x36, 0x49, 0x55, 0x22, 0x50, // &
0x00, 0x05, 0x03, 0x00, 0x00, // '
0x00, 0x1C, 0x22, 0x41, 0x00, // (
0x00, 0x41, 0x22, 0x1C, 0x00, // )
0x08, 0x2A, 0x1C, 0x2A, 0x08, // *
0x08, 0x08, 0x3E, 0x08, 0x08, // +
0x00, 0x50, 0x30, 0x00, 0x00, // ,
0x08, 0x08, 0x08, 0x08, 0x08, // -
0x00, 0x60, 0x60, 0x00, 0x00, // .
0x20, 0x10, 0x08, 0x04, 0x02, // /
0x3E, 0x51, 0x49, 0x45, 0x3E, // 0
0x00, 0x42, 0x7F, 0x40, 0x00, // 1
0x42, 0x61, 0x51, 0x49, 0x46, // 2
0x21, 0x41, 0x45, 0x4B, 0x31, // 3
0x18, 0x14, 0x12, 0x7F, 0x10, // 4
0x27, 0x45, 0x45, 0x45, 0x39, // 5
0x3C, 0x4A, 0x49, 0x49, 0x30, // 6
0x01, 0x71, 0x09, 0x05, 0x03, // 7
0x36, 0x49, 0x49, 0x49, 0x36, // 8
0x06, 0x49, 0x49, 0x29, 0x1E, // 9
0x00, 0x36, 0x36, 0x00, 0x00, // :
0x00, 0x56, 0x36, 0x00, 0x00, // ;
0x00, 0x08, 0x14, 0x22, 0x41, // <
0x14, 0x14, 0x14, 0x14, 0x14, // =
0x41, 0x22, 0x14, 0x08, 0x00, // >
0x02, 0x01, 0x51, 0x09, 0x06, // ?
0x32, 0x49, 0x79, 0x41, 0x3E, // @
0x7E, 0x11, 0x11, 0x11, 0x7E, // A
0x7F, 0x49, 0x49, 0x49, 0x36, // B
0x3E, 0x41, 0x41, 0x41, 0x22, // C
0x7F, 0x41, 0x41, 0x22, 0x1C, // D
0x7F, 0x49, 0x49, 0x49, 0x41, // E
0x7F, 0x09, 0x09, 0x01, 0x01, // F
0x3E, 0x41, 0x41, 0x51, 0x32, // G
0x7F, 0x08, 0x08, 0x08, 0x7F, // H
0x00, 0x41, 0x7F, 0x41, 0x00, // I
0x20, 0x40, 0x41, 0x3F, 0x01, // J
0x7F, 0x08, 0x14, 0x22, 0x41, // K
0x7F, 0x40, 0x40, 0x40, 0x40, // L
0x7F, 0x02, 0x04, 0x02, 0x7F, // M
0x7F, 0x04, 0x08, 0x10, 0x7F, // N
0x3E, 0x41, 0x41, 0x41, 0x3E, // O
0x7F, 0x09, 0x09, 0x09, 0x06, // P
0x3E, 0x41, 0x51, 0x21, 0x5E, // Q
0x7F, 0x09, 0x19, 0x29, 0x46, // R
0x46, 0x49, 0x49, 0x49, 0x31, // S
0x01, 0x01, 0x7F, 0x01, 0x01, // T
0x3F, 0x40, 0x40, 0x40, 0x3F, // U
0x1F, 0x20, 0x40, 0x20, 0x1F, // V
0x7F, 0x20, 0x18, 0x20, 0x7F, // W
0x63, 0x14, 0x08, 0x14, 0x63, // X
0x03, 0x04, 0x78, 0x04, 0x03, // Y
0x61, 0x51, 0x49, 0x45, 0x43, // Z
0x00, 0x00, 0x7F, 0x41, 0x41, // [
0x02, 0x04, 0x08, 0x10, 0x20, // "\"
0x41, 0x41, 0x7F, 0x00, 0x00, // ]
0x04, 0x02, 0x01, 0x02, 0x04, // ^
0x40, 0x40, 0x40, 0x40, 0x40, // _
0x00, 0x01, 0x02, 0x04, 0x00, // `
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // (space) (20)
0x00, 0x00, 0x5F, 0x00, 0x00, 0x00, // ! (21)
0x00, 0x07, 0x00, 0x07, 0x00, 0x00, // "
0x14, 0x7F, 0x14, 0x7F, 0x14, 0x00, // #
0x24, 0x2A, 0x7F, 0x2A, 0x12, 0x00, // $
0x23, 0x13, 0x08, 0x64, 0x62, 0x00, // %
0x36, 0x49, 0x55, 0x22, 0x50, 0x00, // &
0x00, 0x05, 0x03, 0x00, 0x00, 0x00, // '
0x00, 0x1C, 0x22, 0x41, 0x00, 0x00, // (
0x00, 0x41, 0x22, 0x1C, 0x00, 0x00, // )
0x08, 0x2A, 0x1C, 0x2A, 0x08, 0x00, // *
0x08, 0x08, 0x3E, 0x08, 0x08, 0x00, // +
0x00, 0x50, 0x30, 0x00, 0x00, 0x00, // ,
0x08, 0x08, 0x08, 0x08, 0x08, 0x00, // -
0x00, 0x60, 0x60, 0x00, 0x00, 0x00, // .
0x20, 0x10, 0x08, 0x04, 0x02, 0x00, // / (47)
0x3E, 0x51, 0x49, 0x45, 0x3E, 0x00, // 0 (48)
0x00, 0x42, 0x7F, 0x40, 0x00, 0x00, // 1
0x42, 0x61, 0x51, 0x49, 0x46, 0x00, // 2
0x21, 0x41, 0x45, 0x4B, 0x31, 0x00, // 3
0x18, 0x14, 0x12, 0x7F, 0x10, 0x00, // 4
0x27, 0x45, 0x45, 0x45, 0x39, 0x00, // 5
0x3C, 0x4A, 0x49, 0x49, 0x30, 0x00, // 6
0x01, 0x71, 0x09, 0x05, 0x03, 0x00, // 7
0x36, 0x49, 0x49, 0x49, 0x36, 0x00, // 8
0x06, 0x49, 0x49, 0x29, 0x1E, 0x00, // 9 (57)
0x00, 0x36, 0x36, 0x00, 0x00, 0x00, // :
0x00, 0x56, 0x36, 0x00, 0x00, 0x00, // ;
0x00, 0x08, 0x14, 0x22, 0x41, 0x00, // <
0x14, 0x14, 0x14, 0x14, 0x14, 0x00, // =
0x41, 0x22, 0x14, 0x08, 0x00, 0x00, // >
0x02, 0x01, 0x51, 0x09, 0x06, 0x00, // ?
0x32, 0x49, 0x79, 0x41, 0x3E, 0x00, // @ (64)
0x7E, 0x11, 0x11, 0x11, 0x7E, 0x00, // A (65)
0x7F, 0x49, 0x49, 0x49, 0x36, 0x00, // B
0x3E, 0x41, 0x41, 0x41, 0x22, 0x00, // C
0x7F, 0x41, 0x41, 0x22, 0x1C, 0x00, // D
0x7F, 0x49, 0x49, 0x49, 0x41, 0x00, // E
0x7F, 0x09, 0x09, 0x01, 0x01, 0x00, // F
0x3E, 0x41, 0x41, 0x51, 0x32, 0x00, // G
0x7F, 0x08, 0x08, 0x08, 0x7F, 0x00, // H
0x00, 0x41, 0x7F, 0x41, 0x00, 0x00, // I
0x20, 0x40, 0x41, 0x3F, 0x01, 0x00, // J
0x7F, 0x08, 0x14, 0x22, 0x41, 0x00, // K
0x7F, 0x40, 0x40, 0x40, 0x40, 0x00, // L
0x7F, 0x02, 0x04, 0x02, 0x7F, 0x00, // M
0x7F, 0x04, 0x08, 0x10, 0x7F, 0x00, // N
0x3E, 0x41, 0x41, 0x41, 0x3E, 0x00, // O
0x7F, 0x09, 0x09, 0x09, 0x06, 0x00, // P
0x3E, 0x41, 0x51, 0x21, 0x5E, 0x00, // Q
0x7F, 0x09, 0x19, 0x29, 0x46, 0x00, // R
0x46, 0x49, 0x49, 0x49, 0x31, 0x00, // S
0x01, 0x01, 0x7F, 0x01, 0x01, 0x00, // T
0x3F, 0x40, 0x40, 0x40, 0x3F, 0x00, // U
0x1F, 0x20, 0x40, 0x20, 0x1F, 0x00, // V
0x7F, 0x20, 0x18, 0x20, 0x7F, 0x00, // W
0x63, 0x14, 0x08, 0x14, 0x63, 0x00, // X
0x03, 0x04, 0x78, 0x04, 0x03, 0x00, // Y
0x61, 0x51, 0x49, 0x45, 0x43, 0x00, // Z (90)
0x00, 0x00, 0x7F, 0x41, 0x41, 0x00, // [
0x02, 0x04, 0x08, 0x10, 0x20, 0x00, // "\"
0x41, 0x41, 0x7F, 0x00, 0x00, 0x00, // ]
0x04, 0x02, 0x01, 0x02, 0x04, 0x00, // ^
0x40, 0x40, 0x40, 0x40, 0x40, 0x00, // _
0x00, 0x01, 0x02, 0x04, 0x00, 0x00, // ' (96)
#ifndef NOLOWERCASE
0x20, 0x54, 0x54, 0x54, 0x78, // a
0x7F, 0x48, 0x44, 0x44, 0x38, // b
0x38, 0x44, 0x44, 0x44, 0x20, // c
0x38, 0x44, 0x44, 0x48, 0x7F, // d
0x38, 0x54, 0x54, 0x54, 0x18, // e
0x08, 0x7E, 0x09, 0x01, 0x02, // f
0x08, 0x14, 0x54, 0x54, 0x3C, // g
0x7F, 0x08, 0x04, 0x04, 0x78, // h
0x00, 0x44, 0x7D, 0x40, 0x00, // i
0x20, 0x40, 0x44, 0x3D, 0x00, // j
0x00, 0x7F, 0x10, 0x28, 0x44, // k
0x00, 0x41, 0x7F, 0x40, 0x00, // l
0x7C, 0x04, 0x18, 0x04, 0x78, // m
0x7C, 0x08, 0x04, 0x04, 0x78, // n
0x38, 0x44, 0x44, 0x44, 0x38, // o
0x7C, 0x14, 0x14, 0x14, 0x08, // p
0x08, 0x14, 0x14, 0x18, 0x7C, // q
0x7C, 0x08, 0x04, 0x04, 0x08, // r
0x48, 0x54, 0x54, 0x54, 0x20, // s
0x04, 0x3F, 0x44, 0x40, 0x20, // t
0x3C, 0x40, 0x40, 0x20, 0x7C, // u
0x1C, 0x20, 0x40, 0x20, 0x1C, // v
0x3C, 0x40, 0x30, 0x40, 0x3C, // w
0x44, 0x28, 0x10, 0x28, 0x44, // x
0x0C, 0x50, 0x50, 0x50, 0x3C, // y
0x44, 0x64, 0x54, 0x4C, 0x44, // z
0x20, 0x54, 0x54, 0x54, 0x78, 0x00, // a (97)
0x7F, 0x48, 0x44, 0x44, 0x38, 0x00, // b
0x38, 0x44, 0x44, 0x44, 0x20, 0x00, // c
0x38, 0x44, 0x44, 0x48, 0x7F, 0x00, // d
0x38, 0x54, 0x54, 0x54, 0x18, 0x00, // e
0x08, 0x7E, 0x09, 0x01, 0x02, 0x00, // f
0x08, 0x14, 0x54, 0x54, 0x3C, 0x00, // g
0x7F, 0x08, 0x04, 0x04, 0x78, 0x00, // h
0x00, 0x44, 0x7D, 0x40, 0x00, 0x00, // i
0x20, 0x40, 0x44, 0x3D, 0x00, 0x00, // j
0x00, 0x7F, 0x10, 0x28, 0x44, 0x00, // k
0x00, 0x41, 0x7F, 0x40, 0x00, 0x00, // l
0x7C, 0x04, 0x18, 0x04, 0x78, 0x00, // m
0x7C, 0x08, 0x04, 0x04, 0x78, 0x00, // n
0x38, 0x44, 0x44, 0x44, 0x38, 0x00, // o
0x7C, 0x14, 0x14, 0x14, 0x08, 0x00, // p
0x08, 0x14, 0x14, 0x18, 0x7C, 0x00, // q
0x7C, 0x08, 0x04, 0x04, 0x08, 0x00, // r
0x48, 0x54, 0x54, 0x54, 0x20, 0x00, // s
0x04, 0x3F, 0x44, 0x40, 0x20, 0x00, // t
0x3C, 0x40, 0x40, 0x20, 0x7C, 0x00, // u
0x1C, 0x20, 0x40, 0x20, 0x1C, 0x00, // v
0x3C, 0x40, 0x30, 0x40, 0x3C, 0x00, // w
0x44, 0x28, 0x10, 0x28, 0x44, 0x00, // x
0x0C, 0x50, 0x50, 0x50, 0x3C, 0x00, // y
0x44, 0x64, 0x54, 0x4C, 0x44, 0x00, // z (122)
#endif
0x00, 0x08, 0x36, 0x41, 0x00, // {
0x00, 0x00, 0x7F, 0x00, 0x00, // |
0x00, 0x41, 0x36, 0x08, 0x00, // }
0x08, 0x08, 0x2A, 0x1C, 0x08, // ->
0x08, 0x1C, 0x2A, 0x08, 0x08, // <-
0x00, 0x06, 0x09, 0x09, 0x06 // degree symbol
0x00, 0x08, 0x36, 0x41, 0x00, 0x00, // { (123)
0x00, 0x00, 0x7F, 0x00, 0x00, 0x00, // |
0x00, 0x41, 0x36, 0x08, 0x00, 0x00, // }
0x08, 0x08, 0x2A, 0x1C, 0x08, 0x00, // ->
0x08, 0x1C, 0x2A, 0x08, 0x08, 0x00, // <- (127)
#ifndef NO_EXTENDED_CHARACTERS
// Extended characters - based on "DOS Western Europe" characters
// International characters not yet implemented.
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented 0x80
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented 0x90
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented 0xa0
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
// Extended characters 176-180
0x92, 0x00, 0x49, 0x00, 0x24, 0x00, // Light grey 0xb0
0xcc, 0x55, 0xcc, 0x55, 0xcc, 0x55, // Mid grey 0xb1
0x6a, 0xff, 0xb6, 0xff, 0xdb, 0xff, // Dark grey 0xb2
0x00, 0x00, 0x00, 0xff, 0x00, 0x00, // Vertical line 0xb3
0x08, 0x08, 0x08, 0xff, 0x00, 0x00, // Vertical line with left spur 0xb4
0x14, 0x14, 0xfe, 0x00, 0xff, 0x00, // Vertical line with double left spur 0xb9
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented Double vertical line with single left spur
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
// Extended characters 185-190
0x28, 0x28, 0xef, 0x00, 0xff, 0x00, // Double vertical line with double left spur 0xb9
0x00, 0x00, 0xff, 0x00, 0xff, 0x00, // Double vertical line 0xba
0x14, 0x14, 0xf4, 0x04, 0xfc, 0x00, // Double top right corner 0xbb
0x14, 0x14, 0x17, 0x10, 0x1f, 0x00, // Double bottom right corner 0xbc
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented 0xbd
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented 0xbe
// Extended characters 191-199
0x08, 0x08, 0x08, 0xf8, 0x00, 0x00, // Top right corner 0xbf
0x00, 0x00, 0x00, 0x0f, 0x08, 0x08, // Bottom left corner 0xc0
0x08, 0x08, 0x08, 0x0f, 0x08, 0x08, // Horizontal line with upward spur 0xc1
0x08, 0x08, 0x08, 0xf8, 0x08, 0x08, // Horizontal line with downward spur 0xc2
0x00, 0x00, 0x00, 0xff, 0x08, 0x08, // Vertical line with right spur 0xc3
0x08, 0x08, 0x08, 0x08, 0x08, 0x08, // Horizontal line 0xc4
0x08, 0x08, 0x08, 0xff, 0x08, 0x08, // Cross 0xc5
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
// Extended characters 200-206
0x00, 0x00, 0x1f, 0x10, 0x17, 0x14, // Double bottom left corner 0xc8
0x00, 0x00, 0xfc, 0x04, 0xf4, 0x14, // Double top left corner 0xc9
0x14, 0x14, 0x17, 0x10, 0x17, 0x14, // Double horizontal with double upward spur 0xca
0x14, 0x14, 0xf4, 0x04, 0xf4, 0x14, // Double horizontal with double downward spur 0xcb
0x00, 0x00, 0xff, 0x00, 0xf7, 0x14, // Double vertical line with double right spur 0xcc
0x14, 0x14, 0x14, 0x14, 0x14, 0x14, // Double horizontal line 0xcd
0x14, 0x14, 0xf7, 0x00, 0xf7, 0x14, // Double cross 0xce
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented 0xd0
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
// Extended characters 217-223
0x08, 0x08, 0x08, 0x0f, 0x00, 0x00, // Bottom right corner 0xd9
0x00, 0x00, 0x00, 0xf8, 0x08, 0x08, // Top left corner 0xda
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // Solid block 0xdb
0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, // Bottom half block 0xdc
0xff, 0xff, 0xff, 0x00, 0x00, 0x00, // Left half block 0xdd
0x00, 0x00, 0x00, 0xff, 0xff, 0xff, // Right half block 0xde
0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f, // Top half block 0xdf
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented 0xe0
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented 0xf0
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Not implemented
// Extended character 248
0x00, 0x06, 0x09, 0x09, 0x06, 0x00, // degree symbol 0xf8
#endif
0x00
};
const uint8_t SSD1306AsciiWire::m_fontCharCount = sizeof(System6x8) / 6;

View File

@ -23,24 +23,28 @@
#include "Arduino.h"
#include "FSH.h"
#include "LCDDisplay.h"
#include "Display.h"
#include "I2CManager.h"
#include "DIAG.h"
#include "DisplayInterface.h"
// Uncomment to remove lower-case letters to save 108 bytes of flash
//#define NOLOWERCASE
//------------------------------------------------------------------------------
// Constructor
class SSD1306AsciiWire : public LCDDisplay {
class SSD1306AsciiWire : public DisplayDevice {
public:
// Constructor
SSD1306AsciiWire(int width, int height);
// Constructors
SSD1306AsciiWire(int width, int height); // Auto-detects I2C address
SSD1306AsciiWire(I2CAddress address, int width, int height);
// Initialize the display controller.
void begin(uint8_t i2cAddr);
bool begin();
// Clear the display and set the cursor to (0, 0).
void clearNative() override;
@ -52,6 +56,8 @@ class SSD1306AsciiWire : public LCDDisplay {
size_t writeNative(uint8_t c) override;
bool isBusy() override { return requestBlock.isBusy(); }
uint16_t getNumCols() { return m_charsPerRow; }
uint16_t getNumRows() { return m_charsPerColumn; }
private:
// Cursor column.
@ -62,26 +68,31 @@ class SSD1306AsciiWire : public LCDDisplay {
uint8_t m_displayWidth;
// Display height.
uint8_t m_displayHeight;
// Display width in characters
uint8_t m_charsPerRow;
// Display height in characters
uint8_t m_charsPerColumn;
// Column offset RAM to SEG.
uint8_t m_colOffset = 0;
// Current font.
const uint8_t* const m_font = System5x7;
const uint8_t* const m_font = System6x8;
// Flag to prevent calling begin() twice
uint8_t m_initialised = false;
// Only fixed size 5x7 fonts in a 6x8 cell are supported.
static const uint8_t fontWidth = 5;
static const uint8_t fontHeight = 7;
static const uint8_t letterSpacing = 1;
// Only fixed size 6x8 fonts in a 6x8 cell are supported.
static const uint8_t fontWidth = 6;
static const uint8_t fontHeight = 8;
static const uint8_t m_fontFirstChar = 0x20;
static const uint8_t m_fontCharCount = 0x61;
static const uint8_t m_fontCharCount;
uint8_t m_i2cAddr;
I2CAddress m_i2cAddr = 0;
I2CRB requestBlock;
uint8_t outputBuffer[fontWidth+letterSpacing+1];
uint8_t outputBuffer[fontWidth+1];
static const uint8_t blankPixels[];
static const uint8_t System5x7[];
static const uint8_t System6x8[];
static const uint8_t FLASH Adafruit128xXXinit[];
static const uint8_t FLASH SH1106_132x64init[];
};

517
ST7735-TFT.h Normal file
View File

@ -0,0 +1,517 @@
/* Tiny TFT Graphics Library v5 - see http://www.technoblogy.com/show?3WAI
David Johnson-Davies - www.technoblogy.com - 26th October 2022
CC BY 4.0
Licensed under a Creative Commons Attribution 4.0 International license:
http://creativecommons.org/licenses/by/4.0/
*/
#include "FSH.h"
#include "DisplayInterface.h"
#if defined(MEGATINYCORE)
// ATtiny402/412 PORTA positions. Change these for the chip you're using
int const dc = 7;
int const mosi = 1;
int const sck = 3;
int const cs = 6;
// ATtiny 0-, 1-, and 2-series port manipulations - assumes all pins in same port
#define PORT_TOGGLE(x) PORTA.OUTTGL = (x)
#define PORT_LOW(x) PORTA.OUTCLR = (x)
#define PORT_HIGH(x) PORTA.OUTSET = (x)
#define PORT_OUTPUT(x) PORTA.DIRSET = (x)
#else
// ATtiny45/85 PORTB positions. Change these for the chip you're using
int const dc = 0;
int const mosi = 1;
int const sck = 2;
int const cs = 3;
// Classic ATtiny port manipulations - assumes all pins in same port
#define PORT_TOGGLE(x) PINB = (x)
#define PORT_LOW(x) PORTB = PORTB & ~((x));
#define PORT_HIGH(x) PORTB = PORTB | ((x))
#define PORT_OUTPUT(x) DDRB = (x)
#endif
// Display parameters - uncomment the line for the one you want to use
// Adafruit 1.44" 128x128 display
// int const xsize = 128, ysize = 128, xoff = 2, yoff = 1, invert = 0, rotate = 3, bgr = 1;
// AliExpress 1.44" 128x128 display
// int const xsize = 128, ysize = 128, xoff = 2, yoff = 1, invert = 0, rotate = 3, bgr = 1;
// Adafruit 0.96" 160x80 display
// int const xsize = 160, ysize = 80, xoff = 0, yoff = 24, invert = 0, rotate = 6, bgr = 0;
// AliExpress 0.96" 160x80 display
// int const xsize = 160, ysize = 80, xoff = 1, yoff = 26, invert = 1, rotate = 0, bgr = 1;
// Adafruit 1.8" 160x128 display
// int const xsize = 160, ysize = 128, xoff = 0, yoff = 0, invert = 0, rotate = 0, bgr = 1;
// AliExpress 1.8" 160x128 display (red PCB)
int const xsize = 160, ysize = 128, xoff = 0, yoff = 0, invert = 0, rotate = 0, bgr = 1;
// AliExpress 1.8" 160x128 display (blue PCB)
// int const xsize = 160, ysize = 128, xoff = 0, yoff = 0, invert = 0, rotate = 6, bgr = 0;
// Adafruit 1.14" 240x135 display
// int const xsize = 240, ysize = 135, xoff = 40, yoff = 53, invert = 1, rotate = 6, bgr = 0;
// AliExpress 1.14" 240x135 display
// int const xsize = 240, ysize = 135, xoff = 40, yoff = 52, invert = 1, rotate = 0, bgr = 0;
// Adafruit 1.3" 240x240 display
// int const xsize = 240, ysize = 240, xoff = 0, yoff = 80, invert = 1, rotate = 5, bgr = 0;
// Adafruit 1.54" 240x240 display
// int const xsize = 240, ysize = 240, xoff = 0, yoff = 80, invert = 1, rotate = 5, bgr = 0;
// AliExpress 1.54" 240x240 display
// int const xsize = 240, ysize = 240, xoff = 0, yoff = 80, invert = 1, rotate = 5, bgr = 0;
// Adafruit 1.9" 320x170 display
// int const xsize = 320, ysize = 170, xoff = 0, yoff = 35, invert = 1, rotate = 0, bgr = 0;
// AliExpress 1.9" 320x170 display
// int const xsize = 320, ysize = 170, xoff = 0, yoff = 35, invert = 1, rotate = 0, bgr = 0;
// Adafruit 1.47" 320x172 rounded rectangle display
// int const xsize = 320, ysize = 172, xoff = 0, yoff = 34, invert = 1, rotate = 0, bgr = 0;
// AliExpress 1.47" 320x172 rounded rectangle display
// int const xsize = 320, ysize = 172, xoff = 0, yoff = 34, invert = 1, rotate = 0, bgr = 0;
// Adafruit 2.0" 320x240 display
// int const xsize = 320, ysize = 240, xoff = 0, yoff = 0, invert = 1, rotate = 6, bgr = 0;
// AliExpress 2.0" 320x240 display
// int const xsize = 320, ysize = 240, xoff = 0, yoff = 0, invert = 1, rotate = 0, bgr = 0;
// Adafruit 2.2" 320x240 display
// int const xsize = 320, ysize = 240, xoff = 0, yoff = 0, invert = 0, rotate = 4, bgr = 1;
// AliExpress 2.4" 320x240 display
// int const xsize = 320, ysize = 240, xoff = 0, yoff = 0, invert = 0, rotate = 2, bgr = 1;
// Character set for text - stored in program memory
const uint8_t CharMap[96][6] FLASH = {
{ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
{ 0x00, 0x00, 0x5F, 0x00, 0x00, 0x00 },
{ 0x00, 0x07, 0x00, 0x07, 0x00, 0x00 },
{ 0x14, 0x7F, 0x14, 0x7F, 0x14, 0x00 },
{ 0x24, 0x2A, 0x7F, 0x2A, 0x12, 0x00 },
{ 0x23, 0x13, 0x08, 0x64, 0x62, 0x00 },
{ 0x36, 0x49, 0x56, 0x20, 0x50, 0x00 },
{ 0x00, 0x08, 0x07, 0x03, 0x00, 0x00 },
{ 0x00, 0x1C, 0x22, 0x41, 0x00, 0x00 },
{ 0x00, 0x41, 0x22, 0x1C, 0x00, 0x00 },
{ 0x2A, 0x1C, 0x7F, 0x1C, 0x2A, 0x00 },
{ 0x08, 0x08, 0x3E, 0x08, 0x08, 0x00 },
{ 0x00, 0x80, 0x70, 0x30, 0x00, 0x00 },
{ 0x08, 0x08, 0x08, 0x08, 0x08, 0x00 },
{ 0x00, 0x00, 0x60, 0x60, 0x00, 0x00 },
{ 0x20, 0x10, 0x08, 0x04, 0x02, 0x00 },
{ 0x3E, 0x51, 0x49, 0x45, 0x3E, 0x00 },
{ 0x00, 0x42, 0x7F, 0x40, 0x00, 0x00 },
{ 0x72, 0x49, 0x49, 0x49, 0x46, 0x00 },
{ 0x21, 0x41, 0x49, 0x4D, 0x33, 0x00 },
{ 0x18, 0x14, 0x12, 0x7F, 0x10, 0x00 },
{ 0x27, 0x45, 0x45, 0x45, 0x39, 0x00 },
{ 0x3C, 0x4A, 0x49, 0x49, 0x31, 0x00 },
{ 0x41, 0x21, 0x11, 0x09, 0x07, 0x00 },
{ 0x36, 0x49, 0x49, 0x49, 0x36, 0x00 },
{ 0x46, 0x49, 0x49, 0x29, 0x1E, 0x00 },
{ 0x00, 0x00, 0x14, 0x00, 0x00, 0x00 },
{ 0x00, 0x40, 0x34, 0x00, 0x00, 0x00 },
{ 0x00, 0x08, 0x14, 0x22, 0x41, 0x00 },
{ 0x14, 0x14, 0x14, 0x14, 0x14, 0x00 },
{ 0x00, 0x41, 0x22, 0x14, 0x08, 0x00 },
{ 0x02, 0x01, 0x59, 0x09, 0x06, 0x00 },
{ 0x3E, 0x41, 0x5D, 0x59, 0x4E, 0x00 },
{ 0x7C, 0x12, 0x11, 0x12, 0x7C, 0x00 },
{ 0x7F, 0x49, 0x49, 0x49, 0x36, 0x00 },
{ 0x3E, 0x41, 0x41, 0x41, 0x22, 0x00 },
{ 0x7F, 0x41, 0x41, 0x41, 0x3E, 0x00 },
{ 0x7F, 0x49, 0x49, 0x49, 0x41, 0x00 },
{ 0x7F, 0x09, 0x09, 0x09, 0x01, 0x00 },
{ 0x3E, 0x41, 0x41, 0x51, 0x73, 0x00 },
{ 0x7F, 0x08, 0x08, 0x08, 0x7F, 0x00 },
{ 0x00, 0x41, 0x7F, 0x41, 0x00, 0x00 },
{ 0x20, 0x40, 0x41, 0x3F, 0x01, 0x00 },
{ 0x7F, 0x08, 0x14, 0x22, 0x41, 0x00 },
{ 0x7F, 0x40, 0x40, 0x40, 0x40, 0x00 },
{ 0x7F, 0x02, 0x1C, 0x02, 0x7F, 0x00 },
{ 0x7F, 0x04, 0x08, 0x10, 0x7F, 0x00 },
{ 0x3E, 0x41, 0x41, 0x41, 0x3E, 0x00 },
{ 0x7F, 0x09, 0x09, 0x09, 0x06, 0x00 },
{ 0x3E, 0x41, 0x51, 0x21, 0x5E, 0x00 },
{ 0x7F, 0x09, 0x19, 0x29, 0x46, 0x00 },
{ 0x26, 0x49, 0x49, 0x49, 0x32, 0x00 },
{ 0x03, 0x01, 0x7F, 0x01, 0x03, 0x00 },
{ 0x3F, 0x40, 0x40, 0x40, 0x3F, 0x00 },
{ 0x1F, 0x20, 0x40, 0x20, 0x1F, 0x00 },
{ 0x3F, 0x40, 0x38, 0x40, 0x3F, 0x00 },
{ 0x63, 0x14, 0x08, 0x14, 0x63, 0x00 },
{ 0x03, 0x04, 0x78, 0x04, 0x03, 0x00 },
{ 0x61, 0x59, 0x49, 0x4D, 0x43, 0x00 },
{ 0x00, 0x7F, 0x41, 0x41, 0x41, 0x00 },
{ 0x02, 0x04, 0x08, 0x10, 0x20, 0x00 },
{ 0x00, 0x41, 0x41, 0x41, 0x7F, 0x00 },
{ 0x04, 0x02, 0x01, 0x02, 0x04, 0x00 },
{ 0x40, 0x40, 0x40, 0x40, 0x40, 0x00 },
{ 0x00, 0x03, 0x07, 0x08, 0x00, 0x00 },
{ 0x20, 0x54, 0x54, 0x78, 0x40, 0x00 },
{ 0x7F, 0x28, 0x44, 0x44, 0x38, 0x00 },
{ 0x38, 0x44, 0x44, 0x44, 0x28, 0x00 },
{ 0x38, 0x44, 0x44, 0x28, 0x7F, 0x00 },
{ 0x38, 0x54, 0x54, 0x54, 0x18, 0x00 },
{ 0x00, 0x08, 0x7E, 0x09, 0x02, 0x00 },
{ 0x18, 0xA4, 0xA4, 0x9C, 0x78, 0x00 },
{ 0x7F, 0x08, 0x04, 0x04, 0x78, 0x00 },
{ 0x00, 0x44, 0x7D, 0x40, 0x00, 0x00 },
{ 0x20, 0x40, 0x40, 0x3D, 0x00, 0x00 },
{ 0x7F, 0x10, 0x28, 0x44, 0x00, 0x00 },
{ 0x00, 0x41, 0x7F, 0x40, 0x00, 0x00 },
{ 0x7C, 0x04, 0x78, 0x04, 0x78, 0x00 },
{ 0x7C, 0x08, 0x04, 0x04, 0x78, 0x00 },
{ 0x38, 0x44, 0x44, 0x44, 0x38, 0x00 },
{ 0xFC, 0x18, 0x24, 0x24, 0x18, 0x00 },
{ 0x18, 0x24, 0x24, 0x18, 0xFC, 0x00 },
{ 0x7C, 0x08, 0x04, 0x04, 0x08, 0x00 },
{ 0x48, 0x54, 0x54, 0x54, 0x24, 0x00 },
{ 0x04, 0x04, 0x3F, 0x44, 0x24, 0x00 },
{ 0x3C, 0x40, 0x40, 0x20, 0x7C, 0x00 },
{ 0x1C, 0x20, 0x40, 0x20, 0x1C, 0x00 },
{ 0x3C, 0x40, 0x30, 0x40, 0x3C, 0x00 },
{ 0x44, 0x28, 0x10, 0x28, 0x44, 0x00 },
{ 0x4C, 0x90, 0x90, 0x90, 0x7C, 0x00 },
{ 0x44, 0x64, 0x54, 0x4C, 0x44, 0x00 },
{ 0x00, 0x08, 0x36, 0x41, 0x00, 0x00 },
{ 0x00, 0x00, 0x77, 0x00, 0x00, 0x00 },
{ 0x00, 0x41, 0x36, 0x08, 0x00, 0x00 },
{ 0x00, 0x06, 0x09, 0x06, 0x00, 0x00 }, // degree symbol = '~'
{ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00 }
};
// TFT colour display **********************************************
int const CASET = 0x2A; // Define column address
int const RASET = 0x2B; // Define row address
int const RAMWR = 0x2C; // Write to display RAM
int const White = 0xFFFF;
int const Black = 0;
// Current plot position and colours
int xpos, ypos;
int fore = White;
int back = Black;
int scale = 1; // Text scale
// Send a byte to the display
void Data (uint8_t d) {
for (uint8_t bit = 0x80; bit; bit >>= 1) {
PORT_TOGGLE(1<<sck);
if (d & bit) PORT_HIGH(1<<mosi); else PORT_LOW(1<<mosi);
PORT_TOGGLE(1<<sck);
}
}
// Send a command to the display
void Command (uint8_t c) {
PORT_TOGGLE(1<<dc);
Data(c);
PORT_TOGGLE(1<<dc);
}
// Send a command followed by two data words
void Command2 (uint8_t c, uint16_t d1, uint16_t d2) {
PORT_TOGGLE(1<<dc);
Data(c);
PORT_TOGGLE(1<<dc);
Data(d1>>8); Data(d1); Data(d2>>8); Data(d2);
}
void InitDisplay () {
PORT_OUTPUT(1<<dc | 1<<cs | 1<<mosi | 1<<sck); // All outputs
PORT_HIGH(1<<dc | 1<<cs | 1<<sck); // Outputs high
PORT_TOGGLE(1<<cs);
Command(0x01); // Software reset
delay(250); // delay 250 ms
Command(0x36); Data(rotate<<5 | bgr<<3); // Set orientation and rgb/bgr
Command(0x3A); Data(0x55); // Set color mode - 16-bit color
Command(0x20+invert); // Invert
Command(0x11); // Out of sleep mode
delay(150);
PORT_TOGGLE(1<<cs);
}
void DisplayOn () {
PORT_TOGGLE(1<<cs);
Command(0x29); // Display on
delay(150);
PORT_TOGGLE(1<<cs);
}
void ClearDisplay () {
PORT_TOGGLE(1<<cs);
Command2(CASET, yoff, yoff + ysize - 1);
Command2(RASET, xoff, xoff + xsize - 1);
Command(0x3A); Data(0x03); // 12-bit colour
Command(RAMWR); // Leaves mosi low
for (int i=0; i<xsize*4; i++) {
for (int j=0; j<ysize*3; j++) {
PORT_TOGGLE(1<<sck);
PORT_TOGGLE(1<<sck);
}
}
Command(0x3A); Data(0x05); // Back to 16-bit colour
PORT_TOGGLE(1<<cs);
}
unsigned int Colour (int r, int g, int b) {
return (r & 0xf8)<<8 | (g & 0xfc)<<3 | b>>3;
}
// Move current plot position to x,y
void MoveTo (int x, int y) {
xpos = x; ypos = y;
}
// Plot point at x,y
void PlotPoint (int x, int y) {
PORT_TOGGLE(1<<cs);
Command2(CASET, yoff+y, yoff+y);
Command2(RASET, xoff+x, xoff+x);
Command(RAMWR); Data(fore>>8); Data(fore & 0xff);
PORT_TOGGLE(1<<cs);
}
// Draw a line to x,y
void DrawTo (int x, int y) {
int sx, sy, e2, err;
int dx = abs(x - xpos);
int dy = abs(y - ypos);
if (xpos < x) sx = 1; else sx = -1;
if (ypos < y) sy = 1; else sy = -1;
err = dx - dy;
for (;;) {
PlotPoint(xpos, ypos);
if (xpos==x && ypos==y) return;
e2 = err<<1;
if (e2 > -dy) { err = err - dy; xpos = xpos + sx; }
if (e2 < dx) { err = err + dx; ypos = ypos + sy; }
}
}
void FillRect (int w, int h) {
PORT_TOGGLE(1<<cs);
Command2(CASET, ypos+yoff, ypos+yoff+h-1);
Command2(RASET, xpos+xoff, xpos+xoff+w-1);
Command(RAMWR);
uint8_t hi = fore>>8;
uint8_t lo = fore & 0xff;
for (int i=0; i<w; i++) {
for (int j=0; j<h; j++) {
Data(hi); Data(lo);
}
}
PORT_TOGGLE(1<<cs);
}
void DrawRect (int w, int h) {
int x1 = xpos, y1 = ypos;
FillRect(w-1, 1); MoveTo(x1, y1+1);
FillRect(1, h-1); MoveTo(x1+1, y1+h-1);
FillRect(w-1, 1); MoveTo(x1+w-1, y1);
FillRect(1, h-1);
xpos = x1; ypos = y1;
}
void FillCircle (int radius) {
int x1 = xpos, y1 = ypos, dx = 1, dy = 1;
int x = radius - 1, y = 0;
int err = dx - (radius<<1);
while (x >= y) {
MoveTo(x1-x, y1+y); FillRect(x<<1, 1);
MoveTo(x1-y, y1+x); FillRect(y<<1, 1);
MoveTo(x1-y, y1-x); FillRect(y<<1, 1);
MoveTo(x1-x, y1-y); FillRect(x<<1, 1);
if (err > 0) {
x = x - 1; dx = dx + 2;
err = err - (radius<<1) + dx;
} else {
y = y + 1; err = err + dy;
dy = dy + 2;
}
}
xpos = x1; ypos = y1;
}
void DrawCircle (int radius) {
int x1 = xpos, y1 = ypos, dx = 1, dy = 1;
int x = radius - 1, y = 0;
int err = dx - (radius<<1);
while (x >= y) {
PlotPoint(x1-x, y1+y); PlotPoint(x1+x, y1+y);
PlotPoint(x1-y, y1+x); PlotPoint(x1+y, y1+x);
PlotPoint(x1-y, y1-x); PlotPoint(x1+y, y1-x);
PlotPoint(x1-x, y1-y); PlotPoint(x1+x, y1-y);
if (err > 0) {
x = x - 1; dx = dx + 2;
err = err - (radius<<1) + dx;
} else {
y = y + 1; err = err + dy;
dy = dy + 2;
}
}
}
// Plot an ASCII character with bottom left corner at x,y
void PlotChar (char c) {
int colour;
PORT_TOGGLE(1<<cs);
Command2(CASET, yoff+ypos, yoff+ypos+8*scale-1);
Command2(RASET, xoff+xpos, xoff+xpos+6*scale-1);
Command(RAMWR);
for (int xx=0; xx<6; xx++) {
int bits = pgm_read_byte(&CharMap[c-32][xx]);
for (int xr=0; xr<scale; xr++) {
for (int yy=0; yy<8; yy++) {
if (bits>>(7-yy) & 1) colour = fore; else colour = back;
for (int yr=0; yr<scale; yr++) {
Data(colour>>8); Data(colour & 0xFF);
}
}
}
}
PORT_TOGGLE(1<<cs);
xpos = xpos + 6*scale;
}
// Plot text starting at the current plot position
void PlotText(PGM_P p) {
while (1) {
char c = pgm_read_byte(p++);
if (c == 0) return;
PlotChar(c);
}
}
void PlotInt(int n) {
bool lead = false;
for (int d=10000; d>0; d = d/10) {
char j = (n/d) % 10;
if (j!=0 || lead || d==1) { PlotChar(j + '0'); lead = true; }
}
}
void TestChart () {
DrawRect(xsize, ysize);
scale = 8;
fore = Colour(255, 0, 0);
MoveTo((xsize-40)/2, (ysize-64)/2); PlotChar('F');
scale = 1;
}
// Demos **********************************************
void BarChart () {
int x0 = 0, y0 = 0, w = xsize, h = ysize, x1 = 15, y1 = 11;
MoveTo(x0+(w-x1-90)/2+x1, y0+h-8); PlotText(PSTR("Sensor Readings"));
// Horizontal axis
int xinc = (w-x1)/20;
MoveTo(x0+x1, y0+y1); DrawTo(x0+w-1, y0+y1);
for (int i=0; i<=20; i=i+4) {
int mark = x1+i*xinc;
MoveTo(x0+mark, y0+y1); DrawTo(x0+mark, y0+y1-2);
// Draw histogram
if (i != 20) {
int bar = xinc*4/3;
for (int b=2; b>=0; b--) {
fore = Colour(255, 127*b, 0); // Red, Orange, Yellow
MoveTo(x0+mark+bar*b-b+1, y0+y1+1); FillRect(bar, 5+random(h-y1-20));
}
fore = White;
}
if (i > 9) MoveTo(x0+mark-7, y0+y1-11); else MoveTo(x0+mark-3, y0+y1-11);
PlotInt(i);
}
// Vertical axis
int yinc = (h-y1)/20;
MoveTo(x0+x1, y0+y1); DrawTo(x0+x1, y0+h-1);
for (int i=0; i<=20; i=i+5) {
int mark = y1+i*yinc;
MoveTo(x0+x1, y0+mark); DrawTo(x0+x1-2, y0+mark);
if (i > 9) MoveTo(x0+x1-15, y0+mark-4); else MoveTo(x0+x1-9, y0+mark-4);
PlotInt(i);
}
}
void Waterfall () {
int x0 = 0, y0 = 0, w = xsize, h = ysize, x1 = 15, y1 = 11;
int factor = 5160/h*10;
MoveTo(x0+(w-x1-60)/2+x1, y0+h-8); PlotText(PSTR("Luminance"));
// Horizontal axis
int xinc = (w-x1-15)/30;
MoveTo(x0+x1, y0+y1); DrawTo(x0+x1+xinc*20, y0+y1);
for (int i=0; i<=20; i=i+5) {
int mark = x1+i*xinc;
MoveTo(x0+mark, y0+y1); DrawTo(x0+mark, y0+y1-2);
if (i > 9) MoveTo(x0+mark-7, y0+y1-11); else MoveTo(x0+mark-3, y0+y1-11);
PlotInt(i);
}
// Vertical axis
int yinc = (h-y1)/20;
MoveTo(x0+x1, y0+y1); DrawTo(x0+x1, y0+h-1);
for (int i=0; i<=20; i=i+5) {
int mark = y1+i*yinc;
MoveTo(x0+x1, y0+mark); DrawTo(x0+x1-2, y0+mark);
if (i > 9) MoveTo(x0+x1-15, y0+mark-4); else MoveTo(x0+x1-9, y0+mark-4);
PlotInt(i);
}
// Diagonal axis
yinc = xinc/2;
// MoveTo(x0+x1, y0+y1); DrawTo(x0+x1+10*xinc, y0+y1+10*xinc);
MoveTo(x0+x1+20*xinc, y0+y1); DrawTo(x0+x1+30*xinc, y0+y1+10*xinc);
for (int i=0; i<=20; i=i+5) {
MoveTo(x0+x1+20*xinc+i*xinc/2, y0+y1+i*xinc/2);
DrawTo(x0+x1+20*xinc+i*xinc/2+3, y0+y1+i*xinc/2);
MoveTo(x0+x1+20*xinc+i*xinc/2+6, y0+y1+i*xinc/2-4); PlotInt(i);
}
// Plot data
for (int y=20; y>=0; y--) {
for (int i=0; i<=20; i++) {
int fn0 = 180-(i-10)*(i-10)-(y-10)*(y-10);
int fn1 = 180-(i+1-10)*(i+1-10)-(y-10)*(y-10);
fore = Colour(255, 255, 0);
MoveTo(x0+x1+y*yinc+i*xinc, y0+y1+y*yinc+fn0*fn0/factor);
DrawTo(x0+x1+y*yinc+(i+1)*xinc, y0+y1+y*yinc+fn1*fn1/factor);
fore = White;
}
}
}
// Setup **********************************************
void setup() {
InitDisplay();
ClearDisplay();
DisplayOn();
MoveTo(0,0);
// TestChart();
}
void loop () {
BarChart();
// Waterfall();
for (;;);
}

View File

@ -18,7 +18,7 @@
*/
#include "StringFormatter.h"
#include <stdarg.h>
#include "LCDDisplay.h"
#include "DisplayInterface.h"
bool Diag::ACK=false;
bool Diag::CMD=false;
@ -45,10 +45,17 @@ void StringFormatter::lcd(byte row, const FSH* input...) {
send2(&USB_SERIAL,input,args);
send(&USB_SERIAL,F(" *>\n"));
if (!LCDDisplay::lcdDisplay) return;
LCDDisplay::lcdDisplay->setRow(row);
DisplayInterface::setRow(row);
va_start(args, input);
send2(LCDDisplay::lcdDisplay,input,args);
send2(DisplayInterface::getDisplayHandler(),input,args);
}
void StringFormatter::lcd2(uint8_t display, byte row, const FSH* input...) {
va_list args;
DisplayInterface::setRow(display, row);
va_start(args, input);
send2(DisplayInterface::getDisplayHandler(),input,args);
}
void StringFormatter::send(Print * stream, const FSH* input...) {
@ -108,7 +115,8 @@ void StringFormatter::send2(Print * stream,const FSH* format, va_list args) {
case 'l': printPadded(stream,va_arg(args, long), formatWidth, formatLeft); break;
case 'b': stream->print(va_arg(args, int), BIN); break;
case 'o': stream->print(va_arg(args, int), OCT); break;
case 'x': stream->print(va_arg(args, int), HEX); break;
case 'x': stream->print((unsigned int)va_arg(args, unsigned int), HEX); break;
case 'X': stream->print((unsigned long)va_arg(args, unsigned long), HEX); break;
//case 'f': stream->print(va_arg(args, double), 2); break;
//format width prefix
case '-':

View File

@ -21,7 +21,7 @@
#include <Arduino.h>
#include "FSH.h"
#include "RingStream.h"
#include "LCDDisplay.h"
#include "Display.h"
class Diag {
public:
static bool ACK;
@ -46,6 +46,7 @@ class StringFormatter
// DIAG support
static void diag( const FSH* input...);
static void lcd(byte row, const FSH* input...);
static void lcd2(uint8_t display, byte row, const FSH* input...);
static void printEscapes(char * input);
static void printEscape( char c);

View File

@ -207,7 +207,7 @@
}
#ifdef EESTOREDEBUG
printAll(&Serial);
printAll(&USB_SERIAL);
#endif
return tt;
}

View File

@ -184,8 +184,8 @@ wifiSerialState WifiInterface::setup2(const FSH* SSid, const FSH* password,
checkForOK(1000, true); // Not always OK, sometimes "no change"
const char *yourNetwork = "Your network ";
if (strncmp_P(yourNetwork, (const char*)SSid, 13) == 0 || strncmp_P("", (const char*)SSid, 13) == 0) {
if (strncmp_P(yourNetwork, (const char*)password, 13) == 0) {
if (STRNCMP_P(yourNetwork, (const char*)SSid, 13) == 0 || STRNCMP_P("", (const char*)SSid, 13) == 0) {
if (STRNCMP_P(yourNetwork, (const char*)password, 13) == 0) {
// If the source code looks unconfigured, check if the
// ESP8266 is preconfigured in station mode.
// We check the first 13 chars of the SSid and the password
@ -258,7 +258,7 @@ wifiSerialState WifiInterface::setup2(const FSH* SSid, const FSH* password,
i=0;
do {
if (strncmp_P(yourNetwork, (const char*)password, 13) == 0) {
if (STRNCMP_P(yourNetwork, (const char*)password, 13) == 0) {
// unconfigured
StringFormatter::send(wifiStream, F("AT+CWSAP%s=\"DCCEX_%s\",\"PASS_%s\",%d,4\r\n"),
oldCmd ? "" : "_CUR", macTail, macTail, channel);

175
defines.h
View File

@ -43,110 +43,119 @@
#undef USB_SERIAL // Teensy has this defined by default...
#define USB_SERIAL Serial
// Include extended addresses unless specifically excluded
#define I2C_EXTENDED_ADDRESS
#if defined(ARDUINO_AVR_UNO)
#define ARDUINO_TYPE "UNO"
#undef HAS_ENOUGH_MEMORY
#define ARDUINO_TYPE "UNO"
#undef HAS_ENOUGH_MEMORY
#define NO_EXTENDED_CHARACTERS
#undef I2C_EXTENDED_ADDRESS
#elif defined(ARDUINO_AVR_NANO)
#define ARDUINO_TYPE "NANO"
#undef HAS_ENOUGH_MEMORY
#define ARDUINO_TYPE "NANO"
#undef HAS_ENOUGH_MEMORY
#define NO_EXTENDED_CHARACTERS
#undef I2C_EXTENDED_ADDRESS
#elif defined(ARDUINO_AVR_MEGA)
#define ARDUINO_TYPE "MEGA"
#define ARDUINO_TYPE "MEGA"
#elif defined(ARDUINO_AVR_MEGA2560)
#define ARDUINO_TYPE "MEGA"
#define ARDUINO_TYPE "MEGA"
#elif defined(ARDUINO_ARCH_MEGAAVR)
#define ARDUINO_TYPE "MEGAAVR"
#undef HAS_ENOUGH_MEMORY
#define ARDUINO_TYPE "MEGAAVR"
#undef HAS_ENOUGH_MEMORY
#define NO_EXTENDED_CHARACTERS
#undef I2C_EXTENDED_ADDRESS
#elif defined(ARDUINO_TEENSY31)
#define ARDUINO_TYPE "TEENSY3132"
#undef USB_SERIAL
#define USB_SERIAL SerialUSB
#ifndef DISABLE_EEPROM
#define DISABLE_EEPROM
#endif
// Teensy support for native I2C is awaiting development
#ifndef I2C_NO_INTERRUPTS
#define I2C_NO_INTERRUPTS
#endif
#define ARDUINO_TYPE "TEENSY3132"
#undef USB_SERIAL
#define USB_SERIAL SerialUSB
#ifndef DISABLE_EEPROM
#define DISABLE_EEPROM
#endif
// Teensy support for native I2C is awaiting development
#ifndef I2C_USE_WIRE
#define I2C_USE_WIRE
#endif
#elif defined(ARDUINO_TEENSY35)
#define ARDUINO_TYPE "TEENSY35"
#undef USB_SERIAL
#define USB_SERIAL SerialUSB
// Teensy support for I2C is awaiting development
#ifndef DISABLE_EEPROM
#define DISABLE_EEPROM
#endif
// Teensy support for native I2C is awaiting development
#ifndef I2C_NO_INTERRUPTS
#define I2C_NO_INTERRUPTS
#endif
#define ARDUINO_TYPE "TEENSY35"
#undef USB_SERIAL
#define USB_SERIAL SerialUSB
// Teensy support for I2C is awaiting development
#ifndef DISABLE_EEPROM
#define DISABLE_EEPROM
#endif
// Teensy support for native I2C is awaiting development
#ifndef I2C_USE_WIRE
#define I2C_USE_WIRE
#endif
#elif defined(ARDUINO_TEENSY36)
#define ARDUINO_TYPE "TEENSY36"
#undef USB_SERIAL
#define USB_SERIAL SerialUSB
#ifndef DISABLE_EEPROM
#define DISABLE_EEPROM
#endif
// Teensy support for native I2C is awaiting development
#ifndef I2C_NO_INTERRUPTS
#define I2C_NO_INTERRUPTS
#endif
#define ARDUINO_TYPE "TEENSY36"
#undef USB_SERIAL
#define USB_SERIAL SerialUSB
#ifndef DISABLE_EEPROM
#define DISABLE_EEPROM
#endif
// Teensy support for native I2C is awaiting development
#ifndef I2C_USE_WIRE
#define I2C_USE_WIRE
#endif
#elif defined(ARDUINO_TEENSY40)
#define ARDUINO_TYPE "TEENSY40"
#undef USB_SERIAL
#define USB_SERIAL SerialUSB
#ifndef DISABLE_EEPROM
#define DISABLE_EEPROM
#endif
// Teensy support for native I2C is awaiting development
#ifndef I2C_NO_INTERRUPTS
#define I2C_NO_INTERRUPTS
#endif
#define ARDUINO_TYPE "TEENSY40"
#undef USB_SERIAL
#define USB_SERIAL SerialUSB
#ifndef DISABLE_EEPROM
#define DISABLE_EEPROM
#endif
// Teensy support for native I2C is awaiting development
#ifndef I2C_USE_WIRE
#define I2C_USE_WIRE
#endif
#elif defined(ARDUINO_TEENSY41)
#define ARDUINO_TYPE "TEENSY41"
#undef USB_SERIAL
#define USB_SERIAL SerialUSB
#ifndef DISABLE_EEPROM
#define DISABLE_EEPROM
#endif
// Teensy support for native I2C is awaiting development
#ifndef I2C_NO_INTERRUPTS
#define I2C_NO_INTERRUPTS
#endif
#define ARDUINO_TYPE "TEENSY41"
#undef USB_SERIAL
#define USB_SERIAL SerialUSB
#ifndef DISABLE_EEPROM
#define DISABLE_EEPROM
#endif
// Teensy support for native I2C is awaiting development
#ifndef I2C_USE_WIRE
#define I2C_USE_WIRE
#endif
#elif defined(ARDUINO_ARCH_ESP8266)
#define ARDUINO_TYPE "ESP8266"
#warning "ESP8266 platform untested, you are on your own"
#define ARDUINO_TYPE "ESP8266"
#warning "ESP8266 platform untested, you are on your own"
#elif defined(ARDUINO_ARCH_ESP32)
#define ARDUINO_TYPE "ESP32"
#ifndef DISABLE_EEPROM
#define DISABLE_EEPROM
#endif
#define ARDUINO_TYPE "ESP32"
#ifndef DISABLE_EEPROM
#define DISABLE_EEPROM
#endif
#elif defined(ARDUINO_ARCH_SAMD)
#define ARDUINO_TYPE "SAMD21"
#undef USB_SERIAL
#define USB_SERIAL SerialUSB
// SAMD no EEPROM by default
#ifndef DISABLE_EEPROM
#define DISABLE_EEPROM
#endif
#define ARDUINO_TYPE "SAMD21"
#undef USB_SERIAL
#define USB_SERIAL SerialUSB
// SAMD no EEPROM by default
#ifndef DISABLE_EEPROM
#define DISABLE_EEPROM
#endif
#elif defined(ARDUINO_ARCH_STM32)
#define ARDUINO_TYPE "STM32"
// STM32 no EEPROM by default
#ifndef DISABLE_EEPROM
#define DISABLE_EEPROM
#endif
// STM32 support for native I2C is awaiting development
#ifndef I2C_NO_INTERRUPTS
#define I2C_NO_INTERRUPTS
#endif
#define ARDUINO_TYPE "STM32"
// STM32 no EEPROM by default
#ifndef DISABLE_EEPROM
#define DISABLE_EEPROM
#endif
// STM32 support for native I2C is awaiting development
#ifndef I2C_USE_WIRE
#define I2C_USE_WIRE
#endif
/* TODO when ready
#elif defined(ARDUINO_ARCH_RP2040)
#define ARDUINO_TYPE "RP2040"
#define ARDUINO_TYPE "RP2040"
*/
#else
#define CPU_TYPE_ERROR
#define CPU_TYPE_ERROR
#endif
// replace board type if provided by compiler

View File

@ -88,6 +88,21 @@ void halSetup() {
//PCF8574::create(200, 8, 0x23, 40);
//=======================================================================
// The following directive defines a PCF8575 16-port I2C GPIO Extender module.
//=======================================================================
// The parameters are:
// First Vpin=200
// Number of VPINs=16 (numbered 200-215)
// I2C address of module=0x23
//PCF8575::create(200, 16, 0x23);
// Alternative form using INT pin (see above)
//PCF8575::create(200, 16, 0x23, 40);
//=======================================================================
// The following directive defines an HCSR04 ultrasonic ranging module.
//=======================================================================

View File

@ -30,6 +30,8 @@ include_dir = .
[env]
build_flags = -Wall -Wextra
monitor_filters = time
; lib_deps = adafruit/Adafruit ST7735 and ST7789 Library @ ^1.10.0
[env:samd21-dev-usb]
platform = atmelsam
@ -51,6 +53,15 @@ monitor_speed = 115200
monitor_echo = yes
build_flags = -std=c++17
[env:Arduino M0]
platform = atmelsam
board = mzeroUSB
framework = arduino
lib_deps = ${env.lib_deps}
monitor_speed = 115200
monitor_echo = yes
build_flags = -std=c++17 ; -DI2C_USE_WIRE -DDIAG_LOOPTIMES -DDIAG_IO
[env:mega2560-debug]
platform = atmelavr
board = megaatmega2560
@ -61,7 +72,7 @@ lib_deps =
SPI
monitor_speed = 115200
monitor_echo = yes
build_flags = -DDIAG_IO -DDIAG_LOOPTIMES
build_flags = -DDIAG_IO=2 -DDIAG_LOOPTIMES
[env:mega2560-no-HAL]
platform = atmelavr
@ -97,7 +108,7 @@ lib_deps =
SPI
monitor_speed = 115200
monitor_echo = yes
build_flags = -mcall-prologues
build_flags = ; -DDIAG_LOOPTIMES
[env:mega328]
platform = atmelavr
@ -133,7 +144,7 @@ lib_deps =
monitor_speed = 115200
monitor_echo = yes
upload_speed = 19200
build_flags = -DDIAG_IO
build_flags =
[env:uno]
platform = atmelavr
@ -177,7 +188,7 @@ platform = ststm32
board = nucleo_f446re
framework = arduino
lib_deps = ${env.lib_deps}
build_flags = -std=c++17 -Os -g2 -Wunused-variable
build_flags = -std=c++17 -Os -g2 -Wunused-variable -DDIAG_LOOPTIMES ; -DDIAG_IO
monitor_speed = 115200
monitor_echo = yes

View File

@ -4,7 +4,32 @@
#include "StringFormatter.h"
#define VERSION "4.2.17"
#define VERSION "4.2.18"
// 4.2.18 - I2C Multiplexer support through Extended Addresses,
// added for Wire, 4209 and AVR I2C drivers.
// - I2C retries when an operation fails.
// - I2C timeout handling and recovery completed.
// - I2C SAMD Driver Read code completed.
// - PCF8575 I2C GPIO driver added.
// - EX-RAIL ANOUT function for triggering analogue
// HAL drivers (e.g. analogue outputs, DFPlayer, PWM).
// - EX-RAIL SCREEN function for writing to screens other
// than the primary one.
// - Installable HALDisplay Driver, with support
// for multiple displays.
// - Layered HAL Drivers PCA9685pwm and Servo added for
// native PWM on PCA9685 module and
// for animations of servo movement via PCA9685pwm.
// This is intended to support EXIOExpander and also
// replace the existing PCA9685 driver.
// - Add <D HAL RESET> to reinitialise failed drivers.
// - Add UserAddin facility to allow a user-written C++ function to be
// declared in myHal.cpp, to be called at a user-specified frequency.
// - Add ability to configure clock speed of PCA9685 drivers
// (to allow flicker-free LED control).
// - Improve stability of VL53L0X driver when XSHUT pin connected.
// - Enable DCC high accuracy mode for STM32 on standard motor shield (pins D12/D13).
// - Incorporate improvements to ADC scanning performance (courtesy of HABA).
// 4.2.17 LCN bugfix
// 4.2.16 Move EX-IOExpander servo support to the EX-IOExpander software
// 4.2.15 Add basic experimental PWM support to EX-IOExpander