/* * © 2023, Neil McKechnie. All rights reserved. * © 2022 Paul M Antoine * * 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_H #define I2CMANAGER_H #include #include "FSH.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 * entirely benign as it reinitialises). * * Also helps to avoid the Wire clock from being set, by another device * driver, to a speed which is higher than a device supports. * * Thirdly, it provides a convenient way to check whether there is a * device on a particular I2C address. * * Non-blocking requests are issued by creating an I2C Request Block * (I2CRB) which is then added to the I2C manager's queue. The * application refers to this block to check for completion of the * operation, and for reading completion status. * * Examples: * I2CRB rb; * uint8_t status = I2CManager.write(address, buffer, sizeof(buffer), &rb); * ... * if (!rb.isBusy()) { * status = rb.status; * // Repeat write * I2CManager.queueRequest(&rb); * ... * status = rb.wait(); // Wait for completion and read status * } * ... * I2CRB rb2; * outbuffer[0] = 12; // Register number in I2C device to be read * rb2.setRequestParams(address, inBuffer, 1, outBuffer, 1); * status = I2CManager.queueRequest(&rb2); * if (status == I2C_STATUS_OK) { * status = rb2.wait(); * if (status == I2C_STATUS_OK) { * registerValue = inBuffer[0]; * } * } * ... * * Synchronous (blocking) calls are also possible, e.g. * status = I2CManager.write(address, buffer, sizeof(buffer)); * * When using non-blocking requests, neither the I2CRB nor the input or output * buffers should be modified until the I2CRB is complete (not busy). * * Timeout monitoring is possible, but requires that the following call is made * reasonably frequently in the program's loop() function: * I2CManager.loop(); * So that the application doesn't need to do this explicitly, this call is performed * from the I2CRB::isBusy() or I2CRB::wait() functions. * */ /* * Future enhancement possibility: * * I2C Multiplexer (e.g. TCA9547, TCA9548) * * A multiplexer offers a way of extending the address range of I2C devices. For example, GPIO extenders use address range 0x20-0x27 * to are limited to 8 on a bus. By adding a multiplexer, the limit becomes 8 for each of the multiplexer's 8 sub-buses, i.e. 64. * And a single I2C bus can have up to 8 multiplexers, giving up to 64 sub-buses and, in theory, up to 512 I/O extenders; that's * as many as 8192 input/output pins! * Secondly, the capacitance of the bus is an electrical limiting factor of the length of the bus, speed and number of devices. * The multiplexer isolates each sub-bus from the others, and so reduces the capacitance of the bus. For example, with one * multiplexer and 64 GPIO extenders, only 9 devices are connected to the bus at any time (multiplexer plus 8 extenders). * Thirdly, the multiplexer offers the ability to use mixed-speed devices more effectively, by allowing high-speed devices to be * put on a different bus to low-speed devices, enabling the software to switch the I2C speed on-the-fly between I2C transactions. * * Changes required: Increase the size of the I2CAddress field in the IODevice class from uint8_t to uint16_t. * The most significant byte would contain a '1' bit flag, the multiplexer number (0-7) and bus number (0-7). Then, when performing * an I2C operation, the I2CManager would check this byte and, if zero, do what it currently does. If the byte is non-zero, then * that means the device is connected via a multiplexer so the I2C transaction should be preceded by a select command issued to the * relevant multiplexer. * * Non-interrupting I2C: * * I2C may be operated without interrupts (undefine I2C_USE_INTERRUPTS). Instead, the I2C state * machine handler, currently invoked from the interrupt service routine, is invoked from the loop() function. * The speed at which I2C operations can be performed then becomes highly dependent on the frequency that * the loop() function is called, and may be adequate under some circumstances. * The advantage of NOT using interrupts is that the impact of I2C upon the DCC waveform (when accurate timing mode isn't in use) * becomes almost zero. * */ // Maximum number of retries on an I2C operation. // A value of zero will disable retries. // Maximum value is 254 (unsigned byte counter) // Note that timeout failures are not retried, but any timeout // configured applies to each try separately. #define MAX_I2C_RETRIES 2 // Add following line to config.h to enable Wire library instead of native I2C drivers //#define I2C_USE_WIRE // Add following line to config.h to disable the use of interrupts by the native I2C drivers. //#define I2C_NO_INTERRUPTS // Default to use interrupts within the native I2C drivers. #ifndef I2C_NO_INTERRUPTS #define I2C_USE_INTERRUPTS #endif // I2C Extended Address support I2C Multiplexers and allows various properties to be // associated with an I2C address such as the MUX and SubBus. In the future, this // may be extended to include multiple buses, and other features. // Uncomment to enable extended address. // // 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 // Type to hold I2C address #if defined(I2C_EXTENDED_ADDRESS) ///////////////////////////////////////////////////////////////////////////////////// // Extended I2C Address type to facilitate extended I2C addresses including // I2C multiplexer support. ///////////////////////////////////////////////////////////////////////////////////// // 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, SubBus_2 = 2, SubBus_3 = 3, SubBus_4 = 4, SubBus_5 = 5, SubBus_6 = 6, SubBus_7 = 7, SubBus_None = 254, // Disable all sub-buses on selected mux SubBus_All = 255, // Enable all sub-buses }; // 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 I2CMux _muxNumber; I2CSubBus _subBus; uint8_t _deviceAddress; static char addressBuffer[]; public: // Constructors // For I2CAddress "{Mux_0, SubBus_0, 0x23}" syntax. I2CAddress(const I2CMux muxNumber, const I2CSubBus subBus, const uint8_t deviceAddress) { _muxNumber = muxNumber; _subBus = subBus; _deviceAddress = deviceAddress; } // Basic constructor I2CAddress() : I2CAddress(I2CMux_None, SubBus_None, 0) {} // 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) {} // 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++ = ','; } uint8_t temp = _deviceAddress; *ptr++ = '0'; *ptr++ = 'x'; for (uint8_t index = 0; index<2; index++) { uint8_t bits = (temp >> 4) & 0x0f; *ptr++ = bits > 9 ? bits-10+'A' : bits+'0'; temp <<= 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; } }; #else struct I2CAddress { private: uint8_t _deviceAddress; static char addressBuffer[]; public: // Constructors I2CAddress(const uint8_t deviceAddress) { _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. uint8_t temp = _deviceAddress; *ptr++ = '0'; *ptr++ = 'x'; for (uint8_t index = 0; index<2; index++) { uint8_t bits = (temp >> 4) & 0xf; *ptr++ = bits > 9 ? bits-10+'a' : bits+'0'; temp <<= 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 } }; // Legacy single-byte I2C address type for compact code and smooth changeover. //typedef uint8_t I2CAddress; #endif // I2C_EXTENDED_ADDRESS // Status codes for I2CRB structures. enum : uint8_t { // Codes used by Wire and by native drivers I2C_STATUS_OK=0, I2C_STATUS_TRUNCATED=1, I2C_STATUS_NEGATIVE_ACKNOWLEDGE=2, I2C_STATUS_TRANSMIT_ERROR=3, I2C_STATUS_TIMEOUT=5, // Code used by Wire only I2C_STATUS_OTHER_TWI_ERROR=4, // catch-all error // Codes used by native drivers only I2C_STATUS_ARBITRATION_LOST=6, I2C_STATUS_BUS_ERROR=7, I2C_STATUS_UNEXPECTED_ERROR=8, I2C_STATUS_PENDING=253, }; // Status codes for the state machine (not returned to caller). enum : uint8_t { I2C_STATE_ACTIVE=253, I2C_STATE_FREE=254, I2C_STATE_CLOSING=255, I2C_STATE_COMPLETED=252, }; typedef enum : uint8_t { OPERATION_READ = 1, OPERATION_REQUEST = 2, OPERATION_SEND = 3, OPERATION_SEND_P = 4, OPERATION_NORETRY = 0x80, // OR with operation to suppress retries. OPERATION_MASK = 0x7f, // mask for extracting the operation code } OperationEnum; // Default I2C frequency #ifndef I2C_FREQ #define I2C_FREQ 400000L #endif // Class defining a request context for an I2C operation. class I2CRB { public: volatile uint8_t status; // Completion status, or pending flag (updated from IRC) volatile uint8_t nBytes; // Number of bytes read (updated from IRC) inline I2CRB() { status = I2C_STATUS_OK; }; uint8_t wait(); bool isBusy(); void setReadParams(I2CAddress i2cAddress, uint8_t *readBuffer, uint8_t readLen); void setRequestParams(I2CAddress i2cAddress, uint8_t *readBuffer, uint8_t readLen, const uint8_t *writeBuffer, uint8_t writeLen); void setWriteParams(I2CAddress i2cAddress, const uint8_t *writeBuffer, uint8_t writeLen); void suppressRetries(bool suppress); uint8_t writeLen; uint8_t readLen; uint8_t operation; I2CAddress i2cAddress; uint8_t *readBuffer; const uint8_t *writeBuffer; #if !defined(I2C_USE_WIRE) I2CRB *nextRequest; // Used by non-blocking devices for I2CRB queue management. #endif }; // I2C Manager class I2CManagerClass { public: // If not already initialised, initialise I2C (wire). void begin(void); // Set clock speed to the lowest requested one. void setClock(uint32_t speed); // Force clock speed void forceClock(uint32_t speed); // setTimeout sets the timout value for I2C transactions (milliseconds). void setTimeout(unsigned long); // Check if specified I2C address is responding. uint8_t checkAddress(I2CAddress address); inline bool exists(I2CAddress address) { return checkAddress(address)==I2C_STATUS_OK; } // Select/deselect Mux Sub-Bus (if using legacy addresses, just checks address) // E.g. muxSelectSubBus({I2CMux_0, SubBus_3}); uint8_t muxSelectSubBus(I2CAddress address) { return checkAddress(address); } // Write a complete transmission to I2C from an array in RAM uint8_t write(I2CAddress address, const uint8_t buffer[], uint8_t size); uint8_t write(I2CAddress address, const uint8_t buffer[], uint8_t size, I2CRB *rb); // Write a complete transmission to I2C from an array in Flash uint8_t write_P(I2CAddress address, const uint8_t buffer[], uint8_t size); uint8_t write_P(I2CAddress address, const uint8_t buffer[], uint8_t size, I2CRB *rb); // Write a transmission to I2C from a list of bytes. uint8_t write(I2CAddress address, uint8_t nBytes, ...); // Write a command from an array in RAM and read response uint8_t read(I2CAddress address, uint8_t readBuffer[], uint8_t readSize, const uint8_t writeBuffer[]=NULL, uint8_t writeSize=0); uint8_t read(I2CAddress address, uint8_t readBuffer[], uint8_t readSize, const uint8_t writeBuffer[], uint8_t writeSize, I2CRB *rb); // Write a command from an arbitrary list of bytes and read response uint8_t read(I2CAddress address, uint8_t readBuffer[], uint8_t readSize, uint8_t writeSize, ...); void queueRequest(I2CRB *req); // Function to abort long-running operations. void checkForTimeout(); // Loop method void loop(); // Expand error codes into text. Note that they are in flash so // need to be printed using FSH. static const FSH *getErrorMessage(uint8_t status); private: bool _beginCompleted = false; bool _clockSpeedFixed = false; static 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; static unsigned long timeout; // Transaction timeout in microseconds. 0=disabled. // 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) static uint8_t _muxCount; uint8_t getMuxCount() { return _muxCount; } #endif #if !defined(I2C_USE_WIRE) // I2CRB structs are queued on the following two links. // If there are no requests, both are NULL. // If there is only one request, then queueHead and queueTail both point to it. // Otherwise, queueHead is the pointer to the first request in the queue and // queueTail is the pointer to the last request in the queue. // Within the queue, each request's nextRequest field points to the // next request, or NULL. // Mark volatile as they are updated by IRC and read/written elsewhere. static I2CRB * volatile queueHead; static I2CRB * volatile queueTail; static volatile uint8_t state; static uint8_t completionStatus; 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; static volatile uint8_t muxPhase; volatile uint32_t pendingClockSpeed = 0; void startTransaction(); // Low-level hardware manipulation functions. static void I2C_init(); static void I2C_setClock(unsigned long i2cClockSpeed); static void I2C_handleInterrupt(); static void I2C_sendStart(); static void I2C_sendStop(); static void I2C_close(); public: // handleInterrupt needs to be public to be called from the ISR function! static void handleInterrupt(); #endif }; extern I2CManagerClass I2CManager; #endif