diff --git a/CommandDistributor.cpp b/CommandDistributor.cpp index 652d852..4b05c68 100644 --- a/CommandDistributor.cpp +++ b/CommandDistributor.cpp @@ -48,7 +48,7 @@ template void CommandDistributor::broadcastReply(clientType t // on a single USB connection config, write direct to Serial and ignore flush/shove template void CommandDistributor::broadcastReply(clientType type, Targs... msg){ (void)type; //shut up compiler warning - StringFormatter::send(&Serial, msg...); + StringFormatter::send(&USB_SERIAL, msg...); } #endif diff --git a/CommandStation-EX.ino b/CommandStation-EX.ino index 67a7e58..3c40bba 100644 --- a/CommandStation-EX.ino +++ b/CommandStation-EX.ino @@ -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(); diff --git a/DCCEX.h b/DCCEX.h index f2eee51..2dc8eb7 100644 --- a/DCCEX.h +++ b/DCCEX.h @@ -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" diff --git a/DCCEXParser.cpp b/DCCEXParser.cpp index ef11763..bcc25d7 100644 --- a/DCCEXParser.cpp +++ b/DCCEXParser.cpp @@ -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 diff --git a/DCCTimer.h b/DCCTimer.h index 75be8cd..2214851 100644 --- a/DCCTimer.h +++ b/DCCTimer.h @@ -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; }; diff --git a/DCCTimerAVR.cpp b/DCCTimerAVR.cpp index 9b16c47..40ce0fb 100644 --- a/DCCTimerAVR.cpp +++ b/DCCTimerAVR.cpp @@ -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<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<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 @@ -236,4 +231,4 @@ void ADCee::begin() { //bitSet(ADCSRA, ADSC); //do not start the ADC yet. Done when we have set the MUX interrupts(); } -#endif +#endif \ No newline at end of file diff --git a/DCCTimerSTM32.cpp b/DCCTimerSTM32.cpp index df66c98..2504a26 100644 --- a/DCCTimerSTM32.cpp +++ b/DCCTimerSTM32.cpp @@ -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]) { diff --git a/DIAG.h b/DIAG.h index c942e59..5aa54aa 100644 --- a/DIAG.h +++ b/DIAG.h @@ -24,4 +24,5 @@ #include "StringFormatter.h" #define DIAG StringFormatter::diag #define LCD StringFormatter::lcd +#define SCREEN StringFormatter::lcd2 #endif diff --git a/Display.cpp b/Display.cpp new file mode 100644 index 0000000..2c44778 --- /dev/null +++ b/Display.cpp @@ -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 . + */ + +// 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; +} \ No newline at end of file diff --git a/Display.h b/Display.h new file mode 100644 index 0000000..be2479d --- /dev/null +++ b/Display.h @@ -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 . + */ +#ifndef Display_h +#define Display_h +#include +#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 diff --git a/DisplayInterface.cpp b/DisplayInterface.cpp index 8fe45d3..f2c144e 100644 --- a/DisplayInterface.cpp +++ b/DisplayInterface.cpp @@ -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; diff --git a/DisplayInterface.h b/DisplayInterface.h index 64fc1de..c5d8e96 100644 --- a/DisplayInterface.h +++ b/DisplayInterface.h @@ -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 diff --git a/LCD_Implementation.h b/Display_Implementation.h similarity index 66% rename from LCD_Implementation.h rename to Display_Implementation.h index 95b0f81..ca19bd7 100644 --- a/LCD_Implementation.h +++ b/Display_Implementation.h @@ -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 diff --git a/EXRAIL2.cpp b/EXRAIL2.cpp index e925d96..3bd8da7 100644 --- a/EXRAIL2.cpp +++ b/EXRAIL2.cpp @@ -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; } } diff --git a/EXRAIL2.h b/EXRAIL2.h index 12bca3b..2c4ad96 100644 --- a/EXRAIL2.h +++ b/EXRAIL2.h @@ -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!! + }; diff --git a/EXRAIL2MacroReset.h b/EXRAIL2MacroReset.h index e5f3ba0..b4ffc6d 100644 --- a/EXRAIL2MacroReset.h +++ b/EXRAIL2MacroReset.h @@ -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) @@ -196,7 +199,8 @@ #define JOIN #define KILLALL #define LATCH(sensor_id) -#define LCD(row,msg) +#define LCD(row,msg) +#define SCREEN(display,row,msg) #define LCN(msg) #define MOVETT(id,steps,activity) #define ONACTIVATE(addr,subaddr) diff --git a/EXRAILMacros.h b/EXRAILMacros.h index fc05e6b..ee20c1f 100644 --- a/EXRAILMacros.h +++ b/EXRAILMacros.h @@ -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 @@ -142,6 +142,15 @@ const int StringMacroTracker1=__COUNTER__; tmode=thrunge_lcd; \ lcdid=id;\ break;\ + } +#undef SCREEN +#define SCREEN(display,id,msg) \ + case (__COUNTER__ - StringMacroTracker1) : {\ + static const char HIGHFLASH thrunge[]=msg;\ + strfar=(uint32_t)GETFARPTR(thrunge);\ + tmode=(thrunger)(thrunge_lcd+display); \ + lcdid=id;\ + break;\ } void RMFT2::printMessage(uint16_t id) { @@ -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 diff --git a/FSH.h b/FSH.h index f4bf47e..d031935 100644 --- a/FSH.h +++ b/FSH.h @@ -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 diff --git a/I2CManager.cpp b/I2CManager.cpp index a951a87..6d5db41 100644 --- a/I2CManager.cpp +++ b/I2CManager.cpp @@ -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 \ No newline at end of file diff --git a/I2CManager.h b/I2CManager.h index 7df0803..016874a 100644 --- a/I2CManager.h +++ b/I2CManager.h @@ -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 #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,18 +484,35 @@ 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); 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; + + // 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; - static unsigned long timeout; // Transaction timeout in microseconds. 0=disabled. - 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 diff --git a/I2CManager_AVR.h b/I2CManager_AVR.h index 6492e00..e75b32f 100644 --- a/I2CManager_AVR.h +++ b/I2CManager_AVR.h @@ -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 #include "I2CManager.h" +#include "I2CManager_NonBlocking.h" // to satisfy intellisense #include #include @@ -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<writeBuffer + (txCount++)); + TWDR = GETFLASH(sendBuffer + (txCount++)); else - TWDR = currentRequest->writeBuffer[txCount++]; + TWDR = sendBuffer[txCount++]; bytesToSend--; - TWCR = (1< 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< 0) { - currentRequest->readBuffer[rxCount++] = TWDR; + receiveBuffer[rxCount++] = TWDR; bytesToReceive--; } TWCR = (1<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< 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,46 +108,42 @@ 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 TWI0.MCTRLB = TWI_MCMD_RECVTRANS_gc; } 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 diff --git a/I2CManager_NonBlocking.h b/I2CManager_NonBlocking.h index fbcb98a..fb5bae5 100644 --- a/I2CManager_NonBlocking.h +++ b/I2CManager_NonBlocking.h @@ -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 #include "I2CManager.h" -#if defined(I2C_USE_INTERRUPTS) -// atomic.h isn't available on SAMD, and likely others too... -#if defined(__AVR__) -#include -#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) { +void I2CManagerClass::startTransaction() { + ATOMIC_BLOCK() { if ((state == I2C_STATE_FREE) && (queueHead != NULL)) { state = I2C_STATE_ACTIVE; + completionStatus = I2C_STATUS_OK; + // Check for pending clock speed change + if (pendingClockSpeed) { + // We're about to start a new I2C transaction, so set clock now. + I2C_setClock(pendingClockSpeed); + pendingClockSpeed = 0; + } + startTime = micros(); currentRequest = queueHead; rxCount = txCount = 0; - // 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 \ No newline at end of file diff --git a/I2CManager_SAMD.h b/I2CManager_SAMD.h index cd57507..76774a9 100644 --- a/I2CManager_SAMD.h +++ b/I2CManager_SAMD.h @@ -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; } } } diff --git a/I2CManager_STM32.h b/I2CManager_STM32.h new file mode 100644 index 0000000..a55fd2e --- /dev/null +++ b/I2CManager_STM32.h @@ -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 . + */ + +#ifndef I2CMANAGER_STM32_H +#define I2CMANAGER_STM32_H + +#include +#include "I2CManager.h" +#include "I2CManager_NonBlocking.h" // to satisfy intellisense + +//#include +//#include +#include + +/*************************************************************************** + * 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 */ diff --git a/I2CManager_Wire.h b/I2CManager_Wire.h index 87152e7..04d99bd 100644 --- a/I2CManager_Wire.h +++ b/I2CManager_Wire.h @@ -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 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 \ No newline at end of file diff --git a/IODevice.cpp b/IODevice.cpp index 812d7ed..e907c23 100644 --- a/IODevice.cpp +++ b/IODevice.cpp @@ -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 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_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 diff --git a/IODevice.h b/IODevice.h index 72beb9e..51b5aa0 100644 --- a/IODevice.h +++ b/IODevice.h @@ -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); - -protected: - - // Constructor - IODevice(VPIN firstVpin=0, int nPins=0) { - _firstVpin = firstVpin; - _nPins = nPins; - _nextEntryTime = 0; - _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); - // 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" diff --git a/IO_AnalogueInputs.h b/IO_AnalogueInputs.h index 1af351d..8ff8683 100644 --- a/IO_AnalogueInputs.h +++ b/IO_AnalogueInputs.h @@ -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 diff --git a/IO_DFPlayer.h b/IO_DFPlayer.h index d4684e7..4439fbc 100644 --- a/IO_DFPlayer.h +++ b/IO_DFPlayer.h @@ -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); + 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; iwrite(0); + _serial->write((uint8_t)0); } // Now output the command diff --git a/IO_EXFastclock.h b/IO_EXFastclock.h index 0e1bf76..923f922 100644 --- a/IO_EXFastclock.h +++ b/IO_EXFastclock.h @@ -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()); } } } @@ -110,17 +109,15 @@ void _loop(unsigned long currentMicros) override{ // As the minimum clock increment is 2 seconds delay a bit - say 1 sec. // Clock interval is 60/ clockspeed i.e 60/b seconds delayUntil(currentMicros + ((60/b) * 1000000)); - - } #endif } - +} // Display EX-FastClock device driver info. - void _display() { - 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("")); } }; diff --git a/IO_EXIOExpander.h b/IO_EXIOExpander.h index a782942..58a92ae 100644 --- a/IO_EXIOExpander.h +++ b/IO_EXIOExpander.h @@ -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]; diff --git a/IO_EXTurntable.h b/IO_EXTurntable.h index 2dc9e6b..02a87e3 100644 --- a/IO_EXTurntable.h +++ b/IO_EXTurntable.h @@ -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("")); } diff --git a/IO_ExampleSerial.cpp b/IO_ExampleSerial.cpp deleted file mode 100644 index 12476db..0000000 --- a/IO_ExampleSerial.cpp +++ /dev/null @@ -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 . - */ - -#include -#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); -} - diff --git a/IO_ExampleSerial.h b/IO_ExampleSerial.h index 9b20399..da421c5 100644 --- a/IO_ExampleSerial.h +++ b/IO_ExampleSerial.h @@ -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 \ No newline at end of file diff --git a/IO_ExternalEEPROM.h b/IO_ExternalEEPROM.h new file mode 100644 index 0000000..d2c90e5 --- /dev/null +++ b/IO_ExternalEEPROM.h @@ -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 . + */ + +/* + * 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 \ No newline at end of file diff --git a/IO_GPIOBase.h b/IO_GPIOBase.h index 1a66b3d..66b9ff6 100644 --- a/IO_GPIOBase.h +++ b/IO_GPIOBase.h @@ -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 -GPIOBase::GPIOBase(FSH *deviceName, VPIN firstVpin, uint8_t nPins, uint8_t I2CAddress, int interruptPin) : +GPIOBase::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 @@ -103,21 +105,16 @@ void GPIOBase::_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 bool GPIOBase::_configure(VPIN vpin, ConfigTypeEnum configType, int paramCount, int params[]) { if (configType != CONFIGURE_INPUT) return false; @@ -125,7 +122,7 @@ bool GPIOBase::_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::_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::_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::_loop(unsigned long currentMicros) { template void GPIOBase::_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::_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::_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) diff --git a/IO_HALDisplay.h b/IO_HALDisplay.h new file mode 100644 index 0000000..3919bbf --- /dev/null +++ b/IO_HALDisplay.h @@ -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 . + */ + +/* + * 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::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::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 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 and device to be + // reinitialised using ). + 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 \ No newline at end of file diff --git a/IO_MCP23008.h b/IO_MCP23008.h index bf4d521..9598a50 100644 --- a/IO_MCP23008.h +++ b/IO_MCP23008.h @@ -25,14 +25,14 @@ class MCP23008 : public GPIOBase { 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((FSH *)F("MCP23008"), firstVpin, min(nPins, (uint8_t)8), I2CAddress, interruptPin) { + MCP23008(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) + : GPIOBase((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; } diff --git a/IO_MCP23017.h b/IO_MCP23017.h index 65769f6..7bdc288 100644 --- a/IO_MCP23017.h +++ b/IO_MCP23017.h @@ -30,14 +30,14 @@ class MCP23017 : public GPIOBase { 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((FSH *)F("MCP23017"), vpin, nPins, I2CAddress, interruptPin) + MCP23017(VPIN vpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) + : GPIOBase((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; } diff --git a/IO_PCA9685.cpp b/IO_PCA9685.cpp index 3d7c347..25b0745 100644 --- a/IO_PCA9685.cpp +++ b/IO_PCA9685.cpp @@ -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}; diff --git a/IO_PCA9685pwm.h b/IO_PCA9685pwm.h new file mode 100644 index 0000000..d950bbf --- /dev/null +++ b/IO_PCA9685pwm.h @@ -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 . + */ + +/* + * 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 \ No newline at end of file diff --git a/IO_PCF8574.h b/IO_PCF8574.h index beeeb7c..d71a32a 100644 --- a/IO_PCF8574.h +++ b/IO_PCF8574.h @@ -43,20 +43,22 @@ class PCF8574 : public GPIOBase { 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((FSH *)F("PCF8574"), firstVpin, min(nPins, (uint8_t)8), I2CAddress, interruptPin) + PCF8574(VPIN firstVpin, uint8_t nPins, I2CAddress i2cAddress, int interruptPin=-1) + : GPIOBase((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; } diff --git a/IO_PCF8575.h b/IO_PCF8575.h new file mode 100644 index 0000000..0674617 --- /dev/null +++ b/IO_PCF8575.h @@ -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 . + */ + +/* + * 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 { +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((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 \ No newline at end of file diff --git a/IO_RotaryEncoder.h b/IO_RotaryEncoder.h index b4d538c..9cf4e65 100644 --- a/IO_RotaryEncoder.h +++ b/IO_RotaryEncoder.h @@ -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("")); } diff --git a/IO_Servo.cpp b/IO_Servo.cpp new file mode 100644 index 0000000..d59c1de --- /dev/null +++ b/IO_Servo.cpp @@ -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 . + */ + +#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}; + diff --git a/IO_Servo.h b/IO_Servo.h new file mode 100644 index 0000000..8f95463 --- /dev/null +++ b/IO_Servo.h @@ -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 . + */ + +/* + * 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 \ No newline at end of file diff --git a/IO_TFTDisplay.h b/IO_TFTDisplay.h new file mode 100644 index 0000000..1de7b85 --- /dev/null +++ b/IO_TFTDisplay.h @@ -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 . + */ + +/* + * 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 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 \ No newline at end of file diff --git a/IO_VL53L0X.h b/IO_VL53L0X.h index 9d51345..7c80518 100644 --- a/IO_VL53L0X.h +++ b/IO_VL53L0X.h @@ -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, ®, 1); + I2CManager.read(_I2CAddress, _inBuffer, 1, ®, 1); return _inBuffer[0]; } }; +bool VL53L0X::_addressConfigInProgress = false; + #endif // IO_VL53L0X_h diff --git a/IO_Wire.h b/IO_Wire.h new file mode 100644 index 0000000..624c911 --- /dev/null +++ b/IO_Wire.h @@ -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 . + */ + +/* + * 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 + * 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 +#endif +#endif \ No newline at end of file diff --git a/LCDDisplay.cpp b/LCDDisplay.cpp deleted file mode 100644 index c6609ce..0000000 --- a/LCDDisplay.cpp +++ /dev/null @@ -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 . - */ - -// 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(); -} diff --git a/LCDDisplay.h b/LCDDisplay.h deleted file mode 100644 index 6e2c69d..0000000 --- a/LCDDisplay.h +++ /dev/null @@ -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 . - */ -#ifndef LCDDisplay_h -#define LCDDisplay_h -#include -#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 diff --git a/LiquidCrystal_I2C.cpp b/LiquidCrystal_I2C.cpp index e036b98..1354024 100644 --- a/LiquidCrystal_I2C.cpp +++ b/LiquidCrystal_I2C.cpp @@ -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 } \ No newline at end of file diff --git a/LiquidCrystal_I2C.h b/LiquidCrystal_I2C.h index 6881a69..650ad15 100644 --- a/LiquidCrystal_I2C.h +++ b/LiquidCrystal_I2C.h @@ -22,7 +22,7 @@ #define LiquidCrystal_I2C_h #include -#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 diff --git a/MotorDriver.cpp b/MotorDriver.cpp index c41420a..e87755b 100644 --- a/MotorDriver.cpp +++ b/MotorDriver.cpp @@ -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; } diff --git a/Release - Architecture Doc/CommandStation-EX-Arch-v1-0.svg b/Release - Architecture Doc/CommandStation-EX-Arch-v1-0.svg index b99995d..481b970 100644 --- a/Release - Architecture Doc/CommandStation-EX-Arch-v1-0.svg +++ b/Release - Architecture Doc/CommandStation-EX-Arch-v1-0.svg @@ -485,10 +485,10 @@ Accessories (Output.cpp) - + Process.14 - Other Utilities (LCDDisplay.cpp) + Other Utilities (Display.cpp) @@ -522,7 +522,7 @@ Other Utilities(LCDDisplay.cpp) + x="14.29" dy="1.2em" class="st5">(Display.cpp) Dynamic connector diff --git a/RingStream.cpp b/RingStream.cpp index 9377a0a..12dbaa1 100644 --- a/RingStream.cpp +++ b/RingStream.cpp @@ -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 diff --git a/SSD1306Ascii.cpp b/SSD1306Ascii.cpp index 2fc23c2..911074b 100644 --- a/SSD1306Ascii.cpp +++ b/SSD1306Ascii.cpp @@ -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; diff --git a/SSD1306Ascii.h b/SSD1306Ascii.h index 312a62f..57427a1 100644 --- a/SSD1306Ascii.h +++ b/SSD1306Ascii.h @@ -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[]; }; diff --git a/ST7735-TFT.h b/ST7735-TFT.h new file mode 100644 index 0000000..5893d3c --- /dev/null +++ b/ST7735-TFT.h @@ -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<>8); Data(d1); Data(d2>>8); Data(d2); +} + +void InitDisplay () { + PORT_OUTPUT(1<>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<>8); Data(fore & 0xff); + PORT_TOGGLE(1< -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<>8; + uint8_t lo = fore & 0xff; + for (int i=0; i= 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<>(7-yy) & 1) colour = fore; else colour = back; + for (int yr=0; yr>8); Data(colour & 0xFF); + } + } + } + } + PORT_TOGGLE(1<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 (;;); +} \ No newline at end of file diff --git a/StringFormatter.cpp b/StringFormatter.cpp index cc78714..f7d9c50 100644 --- a/StringFormatter.cpp +++ b/StringFormatter.cpp @@ -18,7 +18,7 @@ */ #include "StringFormatter.h" #include -#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 '-': diff --git a/StringFormatter.h b/StringFormatter.h index 4787715..6923c10 100644 --- a/StringFormatter.h +++ b/StringFormatter.h @@ -21,7 +21,7 @@ #include #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); diff --git a/Turnouts.cpp b/Turnouts.cpp index 636922d..582ea7d 100644 --- a/Turnouts.cpp +++ b/Turnouts.cpp @@ -207,7 +207,7 @@ } #ifdef EESTOREDEBUG - printAll(&Serial); + printAll(&USB_SERIAL); #endif return tt; } diff --git a/WifiInterface.cpp b/WifiInterface.cpp index bdc8dad..f69cc2f 100644 --- a/WifiInterface.cpp +++ b/WifiInterface.cpp @@ -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); diff --git a/defines.h b/defines.h index 9ad5851..5582e8b 100644 --- a/defines.h +++ b/defines.h @@ -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 diff --git a/myHal.cpp_example.txt b/myHal.cpp_example.txt index d7afe15..f528b18 100644 --- a/myHal.cpp_example.txt +++ b/myHal.cpp_example.txt @@ -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. //======================================================================= diff --git a/platformio.ini b/platformio.ini index 82167f2..1fc7291 100644 --- a/platformio.ini +++ b/platformio.ini @@ -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 @@ -73,7 +84,7 @@ lib_deps = SPI monitor_speed = 115200 monitor_echo = yes -build_flags = -DIO_NO_HAL +build_flags = -DIO_NO_HAL [env:mega2560-I2C-wire] 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 diff --git a/version.h b/version.h index 21c7ad2..f846e0d 100644 --- a/version.h +++ b/version.h @@ -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 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