From e7d8d320bdbb7cb933ce853124d6c5466f4d1fd7 Mon Sep 17 00:00:00 2001 From: pmantoine Date: Sat, 6 Aug 2022 17:51:13 +0800 Subject: [PATCH] SAMD21 I2C native interrupt capable driver --- I2CManager.cpp | 7 +- I2CManager_NonBlocking.h | 43 ++++++- I2CManager_SAMD.h | 271 +++++++++++++++++++++++---------------- defines.h | 6 +- 4 files changed, 212 insertions(+), 115 deletions(-) diff --git a/I2CManager.cpp b/I2CManager.cpp index a3ea611..a951a87 100644 --- a/I2CManager.cpp +++ b/I2CManager.cpp @@ -1,5 +1,7 @@ /* - * © 2021, Neil McKechnie. All rights reserved. + * © 2022 Paul M Antoine + * © 2021, Neil McKechnie + * All rights reserved. * * This file is part of CommandStation-EX * @@ -30,6 +32,9 @@ #elif defined(ARDUINO_ARCH_MEGAAVR) #include "I2CManager_NonBlocking.h" #include "I2CManager_Mega4809.h" // NanoEvery/UnoWifi +#elif defined(ARDUINO_ARCH_SAMD) +#include "I2CManager_NonBlocking.h" +#include "I2CManager_SAMD.h" // SAMD21 for now... SAMD51 as well later #else #define I2C_USE_WIRE #include "I2CManager_Wire.h" // Other platforms diff --git a/I2CManager_NonBlocking.h b/I2CManager_NonBlocking.h index 112e51e..fbcb98a 100644 --- a/I2CManager_NonBlocking.h +++ b/I2CManager_NonBlocking.h @@ -1,5 +1,7 @@ /* - * © 2021, Neil McKechnie. All rights reserved. + * © 2022 Paul M Antoine + * © 2021, Neil McKechnie + * All rights reserved. * * This file is part of CommandStation-EX * @@ -23,7 +25,46 @@ #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; +} + +static __inline__ uint8_t my_iCliRetVal(void) +{ + __asm__ __volatile__ ("cpsid i" ::); + return 1; +} + +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 #else #define ATOMIC_BLOCK(x) #define ATOMIC_RESTORESTATE diff --git a/I2CManager_SAMD.h b/I2CManager_SAMD.h index aeabd6c..4dd2790 100644 --- a/I2CManager_SAMD.h +++ b/I2CManager_SAMD.h @@ -1,5 +1,7 @@ /* - * © 2021, Neil McKechnie. All rights reserved. + * © 2022 Paul M Antoine + * © 2021, Neil McKechnie + * All rights reserved. * * This file is part of CommandStation-EX * @@ -25,31 +27,61 @@ //#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_SAMD_ZERO) -// PMA - IRQ handler, based on SERCOM3 being used for I2C, as per Arduino Zero & Sparkfun SAMD21 -// TODO: test void SERCOM3_Handler() { I2CManagerClass::handleInterrupt(); } #endif +// Assume SERCOM3 for now - default I2C bus on Arduino Zero and variants of same +Sercom *s = SERCOM3; + /*************************************************************************** * Set I2C clock speed register. ***************************************************************************/ -void I2CManagerClass::I2C_setClock(unsigned long i2cClockSpeed) { - unsigned long temp = ((F_CPU / i2cClockSpeed) - 16) / 2; - for (uint8_t preScaler = 0; preScaler<=3; preScaler++) { - if (temp <= 255) { - TWBR = temp; - TWSR = (TWSR & 0xfc) | preScaler; - return; - } else - temp /= 4; +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; + t_rise = 1000; + } else if (i2cClockSpeed < 800000L) { + i2cClockSpeed = 400000L; + t_rise = 300; + } else if (i2cClockSpeed < 1200000L) { + i2cClockSpeed = 1000000L; + t_rise = 120; + } else { + i2cClockSpeed = 100000L; + t_rise = 1000; } - // Set slowest speed ~= 500 bits/sec - TWBR = 255; - TWSR |= 0x03; + + // Disable the I2C master mode and wait for sync + s->I2CM.CTRLA.bit.ENABLE = 0 ; + while (s->I2CM.SYNCBUSY.bit.ENABLE != 0); + + // Calculate baudrate - using a rise time appropriate for the speed + s->I2CM.BAUD.bit.BAUD = SystemCoreClock / (2 * i2cClockSpeed) - 5 - (((SystemCoreClock / 1000000) * t_rise) / (2 * 1000)); + + // Enable the I2C master mode and wait for sync + s->I2CM.CTRLA.bit.ENABLE = 1 ; + while (s->I2CM.SYNCBUSY.bit.ENABLE != 0); + + // Setting bus idle mode and wait for sync + s->I2CM.STATUS.bit.BUSSTATE = 1 ; + while (s->I2CM.SYNCBUSY.bit.SYSOP != 0); + + return; } /*************************************************************************** @@ -57,16 +89,58 @@ void I2CManagerClass::I2C_setClock(unsigned long i2cClockSpeed) { ***************************************************************************/ void I2CManagerClass::I2C_init() { - // PMA - broadly we do the following - initialise the clock - initialise the NVIC - software reset the I2C for the sercom - set master mode - do we need smart mode and quick command?? - configure interrupt handlers - enable interrupts - set default baud rate - set SDA/SCL pins as outputs and enable pullups + //Setting clock + GCLK->CLKCTRL.reg = GCLK_CLKCTRL_ID(GCM_SERCOM3_CORE) | // Generic Clock 0 (SERCOM3) + GCLK_CLKCTRL_GEN_GCLK0 | // Generic Clock Generator 0 is source + GCLK_CLKCTRL_CLKEN ; + + /* Wait for peripheral clock synchronization */ + while ( GCLK->STATUS.reg & GCLK_STATUS_SYNCBUSY ); + + // Software reset the SERCOM + s->I2CM.CTRLA.bit.SWRST = 1; + + //Wait both bits Software Reset from CTRLA and SYNCBUSY are equal to 0 + while(s->I2CM.CTRLA.bit.SWRST || s->I2CM.SYNCBUSY.bit.SWRST); + + // Set master mode and enable SCL Clock Stretch mode (stretch after ACK bit) + s->I2CM.CTRLA.reg = SERCOM_I2CM_CTRLA_MODE( I2C_MASTER_OPERATION )/* | + SERCOM_I2CM_CTRLA_SCLSM*/ ; + + // Enable Smart mode and Quick Command + s->I2CM.CTRLB.reg = SERCOM_I2CM_CTRLB_SMEN | SERCOM_I2CM_CTRLB_QCEN; + +#if defined(I2C_USE_INTERRUPTS) + // Setting NVIC + NVIC_EnableIRQ(SERCOM3_IRQn); + NVIC_SetPriority (SERCOM3_IRQn, 0); /* set Priority */ + + // Enable all interrupts + s->I2CM.INTENSET.reg = SERCOM_I2CM_INTENSET_MB | SERCOM_I2CM_INTENSET_SB | SERCOM_I2CM_INTENSET_ERROR; +#endif + + // Calculate baudrate and set default rate for now + s->I2CM.BAUD.bit.BAUD = SystemCoreClock / ( 2 * I2C_FREQ) - 7 / (2 * 1000); + + // Enable the I2C master mode and wait for sync + s->I2CM.CTRLA.bit.ENABLE = 1 ; + while (s->I2CM.SYNCBUSY.bit.ENABLE != 0); + + // Setting bus idle mode and wait for sync + s->I2CM.STATUS.bit.BUSSTATE = 1 ; + while (s->I2CM.SYNCBUSY.bit.SYSOP != 0); + + // Set SDA/SCL pins as outputs and enable pullups, at present we assume these are + // the default ones for SERCOM3 (see assumption above) + pinPeripheral(PIN_WIRE_SDA, g_APinDescription[PIN_WIRE_SDA].ulPinType); + pinPeripheral(PIN_WIRE_SCL, g_APinDescription[PIN_WIRE_SCL].ulPinType); + + // Enable the SCL and SDA pins on the sercom: includes increased driver strength, + // pull-up resistors and pin multiplexer + PORT->Group[g_APinDescription[PIN_WIRE_SCL].ulPort].PINCFG[g_APinDescription[PIN_WIRE_SCL].ulPin].reg = + PORT_PINCFG_DRVSTR | PORT_PINCFG_PULLEN | PORT_PINCFG_PMUXEN; + PORT->Group[g_APinDescription[PIN_WIRE_SDA].ulPort].PINCFG[g_APinDescription[PIN_WIRE_SDA].ulPin].reg = + PORT_PINCFG_DRVSTR | PORT_PINCFG_PULLEN | PORT_PINCFG_PMUXEN; } /*************************************************************************** @@ -75,28 +149,36 @@ void I2CManagerClass::I2C_init() 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<I2CM.STATUS.bit.BUSSTATE == 0x2); + + // 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; + } + else { + // Wait while the I2C bus is BUSY + while (s->I2CM.STATUS.bit.BUSSTATE != 0x1); + s->I2CM.ADDR.bit.ADDR = (currentRequest->i2cAddress << 1ul) | 0; + } } /*************************************************************************** * Initiate a stop bit for transmission (does not interrupt) ***************************************************************************/ void I2CManagerClass::I2C_sendStop() { - TWDR = 0xff; // Default condition = SDA released - TWCR = (1<I2CM.CTRLB.bit.CMD = 3; // Stop condition } /*************************************************************************** * Close I2C down ***************************************************************************/ void I2CManagerClass::I2C_close() { - // disable TWI I2C_sendStop(); - while (TWCR & (1<writeBuffer + (txCount++)); - else - TWDR = currentRequest->writeBuffer[txCount++]; - bytesToSend--; - TWCR = (1< 0) { - currentRequest->readBuffer[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; - bytesToReceive--; - } - TWCR = (1<i2cAddress << 1) | 1; // SLA+R - else - TWDR = (currentRequest->i2cAddress << 1) | 0; // SLA+W - TWCR = (1<I2CM.STATUS.bit.ARBLOST) { + // Arbitration lost, restart + I2C_sendStart(); // Reinitiate request + } else if (s->I2CM.STATUS.bit.BUSERR) { + // Bus error + state = I2C_STATUS_BUS_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; - break; - case TWI_ARB_LOST: // Arbitration lost - // Restart transaction from start. - I2C_sendStart(); - break; - case TWI_BUS_ERROR: // Bus error due to an illegal START or STOP condition - default: - TWDR = 0xff; // Default condition = SDA released - TWCR = (1<operation == OPERATION_SEND_P) + s->I2CM.DATA.bit.DATA = GETFLASH(currentRequest->writeBuffer + (txCount++)); + else + s->I2CM.DATA.bit.DATA = currentRequest->writeBuffer[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; + } else { + // No more data to send/receive. Initiate a STOP condition. + I2C_sendStop(); + state = I2C_STATUS_OK; // Done + } + } 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 + 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; + } } } -#if defined(I2C_USE_INTERRUPTS) -ISR(TWI_vect) { - I2CManagerClass::handleInterrupt(); -} -#endif - -#endif /* I2CMANAGER_AVR_H */ +#endif /* I2CMANAGER_SAMD_H */ diff --git a/defines.h b/defines.h index fa8dd17..ba96b4e 100644 --- a/defines.h +++ b/defines.h @@ -120,14 +120,10 @@ #define ARDUINO_TYPE "SAMD21" #undef USB_SERIAL #define USB_SERIAL SerialUSB -// STM32 no EEPROM by default +// SAMD no EEPROM by default #ifndef DISABLE_EEPROM #define DISABLE_EEPROM #endif -// SAMD support for native I2C is awaiting development -#ifndef I2C_NO_INTERRUPTS - #define I2C_NO_INTERRUPTS -#endif #elif defined(ARDUINO_ARCH_STM32) #define ARDUINO_TYPE "STM32" // STM32 no EEPROM by default