1
0
mirror of https://github.com/DCC-EX/CommandStation-EX.git synced 2024-12-23 12:51:24 +01:00

SAMD21 I2C native interrupt capable driver

This commit is contained in:
pmantoine 2022-08-06 17:51:13 +08:00
parent 17bdd2d724
commit e7d8d320bd
4 changed files with 212 additions and 115 deletions

View File

@ -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

View File

@ -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 <Arduino.h>
#include "I2CManager.h"
#if defined(I2C_USE_INTERRUPTS)
// atomic.h isn't available on SAMD, and likely others too...
#if defined(__AVR__)
#include <util/atomic.h>
#elif defined(__arm__)
// Helper assembly language functions
static __inline__ uint8_t my_iSeiRetVal(void)
{
__asm__ __volatile__ ("cpsie i" ::);
return 1;
}
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

View File

@ -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 <avr/io.h>
//#include <avr/interrupt.h>
#include <wiring_private.h>
/***************************************************************************
* Interrupt handler.
* IRQ handler for SERCOM3 which is the default I2C definition for Arduino Zero
* compatible variants such as the Sparkfun SAMD21 Dev Breakout etc.
* Later we may wish to allow use of an alternate I2C bus, or more than one I2C
* bus on the SAMD architecture
***************************************************************************/
#if defined(I2C_USE_INTERRUPTS) && defined(ARDUINO_SAMD_ZERO)
// 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<<TWSTO)) {}
TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT)|(1<<TWEA)|(1<<TWSTA); // Send Start
while (s->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<<TWEN)|(1<<TWINT)|(1<<TWEA)|(1<<TWSTO); // Send Stop
s->I2CM.CTRLB.bit.CMD = 3; // Stop condition
}
/***************************************************************************
* Close I2C down
***************************************************************************/
void I2CManagerClass::I2C_close() {
// disable TWI
I2C_sendStop();
while (TWCR & (1<<TWSTO)) {}
TWCR = (1<<TWINT); // clear any interrupt and stop twi.
}
/***************************************************************************
@ -105,84 +187,57 @@ void I2CManagerClass::I2C_close() {
* (and therefore, indirectly, from I2CRB::wait() and I2CRB::isBusy()).
***************************************************************************/
void I2CManagerClass::I2C_handleInterrupt() {
if (!(TWCR & (1<<TWINT))) return; // Nothing to do.
uint8_t twsr = TWSR & 0xF8;
// Cases are ordered so that the most frequently used ones are tested first.
switch (twsr) {
case TWI_MTX_DATA_ACK: // Data byte has been transmitted and ACK received
case TWI_MTX_ADR_ACK: // SLA+W has been transmitted and ACK received
if (bytesToSend) { // Send first.
if (operation == OPERATION_SEND_P)
TWDR = GETFLASH(currentRequest->writeBuffer + (txCount++));
else
TWDR = currentRequest->writeBuffer[txCount++];
bytesToSend--;
TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT)|(1<<TWEA);
} else if (bytesToReceive) { // All sent, anything to receive?
while (TWCR & (1<<TWSTO)) {} // Wait for stop to be sent
TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT)|(1<<TWEA)|(1<<TWSTA); // Send Start
} else { // Nothing left to send or receive
TWDR = 0xff; // Default condition = SDA released
TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWEA)|(1<<TWSTO); // Send Stop
state = I2C_STATUS_OK;
}
break;
case TWI_MRX_DATA_ACK: // Data byte has been received and ACK transmitted
if (bytesToReceive > 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<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT); // Send NACK after next reception
} else {
// send ack
TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT)|(1<<TWEA);
}
break;
case TWI_MRX_DATA_NACK: // Data byte has been received and NACK transmitted
if (bytesToReceive > 0) {
currentRequest->readBuffer[rxCount++] = TWDR;
bytesToReceive--;
}
TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWEA)|(1<<TWSTO); // Send Stop
state = I2C_STATUS_OK;
break;
case TWI_START: // START has been transmitted
case TWI_REP_START: // Repeated START has been transmitted
// Set up address and R/W
if (operation == OPERATION_READ || (operation==OPERATION_REQUEST && !bytesToSend))
TWDR = (currentRequest->i2cAddress << 1) | 1; // SLA+R
else
TWDR = (currentRequest->i2cAddress << 1) | 0; // SLA+W
TWCR = (1<<TWEN)|ENABLE_TWI_INTERRUPT|(1<<TWINT)|(1<<TWEA);
break;
case TWI_MTX_ADR_NACK: // SLA+W has been transmitted and NACK received
case TWI_MRX_ADR_NACK: // SLA+R has been transmitted and NACK received
case TWI_MTX_DATA_NACK: // Data byte has been transmitted and NACK received
TWDR = 0xff; // Default condition = SDA released
TWCR = (1<<TWEN)|(1<<TWINT)|(1<<TWEA)|(1<<TWSTO); // Send Stop
if (s->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<<TWEN)|(1<<TWINT)|(1<<TWEA)|(1<<TWSTO); // Send Stop
state = I2C_STATUS_TRANSMIT_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++];
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 */

View File

@ -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