diff --git a/.gitignore b/.gitignore index 7226a7072..b1b54c6d3 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ bootloaders/*/build/ # Personal agent skills are provided by the user's global installation. .agents/ skills-lock.json + +.venv/ +build/ diff --git a/cores/arduino/DmacCrc.cpp b/cores/arduino/DmacCrc.cpp index 020054cac..1af7cdaf4 100644 --- a/cores/arduino/DmacCrc.cpp +++ b/cores/arduino/DmacCrc.cpp @@ -2,6 +2,8 @@ #include namespace { +enum class Source : uint8_t { None, Dma, Input }; +Source source = Source::None; uint8_t owner = 0xff; class CriticalSection { uint32_t mask; @@ -13,12 +15,14 @@ class CriticalSection { #define CRC_CONTROL DMAC_REGS->DMAC_CRCCTRL #define CRC_CHECKSUM DMAC_REGS->DMAC_CRCCHKSUM #define CRC_STATUS DMAC_REGS->DMAC_CRCSTATUS +#define CRC_INPUT DMAC_REGS->DMAC_CRCDATAIN #define CRC_BUSY DMAC_CRCSTATUS_CRCBUSY_Msk #define DMA_CONTROL DMAC_REGS->DMAC_CTRL #else #define CRC_CONTROL DMAC->CRCCTRL.reg #define CRC_CHECKSUM DMAC->CRCCHKSUM.reg #define CRC_STATUS DMAC->CRCSTATUS.reg +#define CRC_INPUT DMAC->CRCDATAIN.reg #define CRC_BUSY DMAC_CRCSTATUS_CRCBUSY #define DMA_CONTROL DMAC->CTRL.reg #endif @@ -49,7 +53,7 @@ DmacCrc::Status DmacCrc::begin(uint8_t channel, Polynomial polynomial, uint32_t (polynomial != Polynomial::Crc16 && polynomial != Polynomial::Crc32)) return Status::InvalidArgument; CriticalSection critical; - if (owner != 0xff || CRC_CONTROL != 0 || (CRC_STATUS & CRC_BUSY) != 0) + if (source != Source::None || CRC_CONTROL != 0 || (CRC_STATUS & CRC_BUSY) != 0) return Status::Busy; #if defined(__SAME53__) || defined(__SAME54__) if ((DMA_CONTROL & DMAC_CTRL_DMAENABLE_Msk) == 0) @@ -71,12 +75,13 @@ DmacCrc::Status DmacCrc::begin(uint8_t channel, Polynomial polynomial, uint32_t DMA_CONTROL |= DMAC_CTRL_CRCENABLE; #endif owner = channel; + source = Source::Dma; return Status::Ok; } DmacCrc::Status DmacCrc::finish(uint8_t channel, bool transferSucceeded, uint32_t& checksum) { CriticalSection critical; - if (owner == 0xff || owner != channel) + if (source != Source::Dma || owner != channel) return Status::NotOwner; if (channelEnabled(channel) || (CRC_STATUS & CRC_BUSY) != 0) return Status::Busy; @@ -86,5 +91,61 @@ DmacCrc::Status DmacCrc::finish(uint8_t channel, bool transferSucceeded, uint32_ checksum = CRC_CHECKSUM; disableCrc(); owner = 0xff; + source = Source::None; + return transferSucceeded ? Status::Ok : Status::TransferFailed; +} + +DmacCrc::Status DmacCrc::beginInput(uint8_t client, Polynomial polynomial, uint32_t seed) { + if (polynomial != Polynomial::Crc16 && polynomial != Polynomial::Crc32) + return Status::InvalidArgument; + CriticalSection critical; +#if defined(__SAME53__) || defined(__SAME54__) + MCLK_REGS->MCLK_AHBMASK |= MCLK_AHBMASK_DMAC_Msk; +#elif defined(__SAMD51__) + MCLK->AHBMASK.reg |= MCLK_AHBMASK_DMAC; +#else + PM->AHBMASK.reg |= PM_AHBMASK_DMAC; + PM->APBBMASK.reg |= PM_APBBMASK_DMAC; +#endif + if (source != Source::None || CRC_CONTROL != 0 || (CRC_STATUS & CRC_BUSY) != 0) + return Status::Busy; +#if !defined(__SAMD51__) && !defined(__SAME53__) && !defined(__SAME54__) + if ((DMA_CONTROL & DMAC_CTRL_CRCENABLE) != 0) + return Status::Busy; +#endif + disableCrc(); + CRC_CHECKSUM = polynomial == Polynomial::Crc16 ? (seed & 0xffffu) : seed; + CRC_CONTROL = DMAC_CRCCTRL_CRCPOLY(polynomial == Polynomial::Crc32 ? 1u : 0u) | + DMAC_CRCCTRL_CRCSRC(1u); +#if !defined(__SAMD51__) && !defined(__SAME53__) && !defined(__SAME54__) + DMA_CONTROL |= DMAC_CTRL_CRCENABLE; +#endif + owner = client; + source = Source::Input; + return Status::Ok; +} + +DmacCrc::Status DmacCrc::writeInput(uint8_t client, uint8_t byte) { + CriticalSection critical; + if (source != Source::Input || owner != client) + return Status::NotOwner; + // I/O CRCBUSY is sticky, cleared by W1C before another byte (D21 20.8.5, + // D5x/E5x 22.8.5). It must not be polled for hardware self-clear. + CRC_STATUS = CRC_BUSY; + CRC_INPUT = byte; + return Status::Ok; +} + +DmacCrc::Status DmacCrc::finishInput(uint8_t client, bool transferSucceeded, + uint32_t& checksum) { + CriticalSection critical; + if (source != Source::Input || owner != client) + return Status::NotOwner; + CRC_STATUS = CRC_BUSY; + if (transferSucceeded) + checksum = CRC_CHECKSUM; + disableCrc(); + owner = 0xff; + source = Source::None; return transferSucceeded ? Status::Ok : Status::TransferFailed; } diff --git a/cores/arduino/DmacCrc.h b/cores/arduino/DmacCrc.h index c422856f1..c41d4bcda 100644 --- a/cores/arduino/DmacCrc.h +++ b/cores/arduino/DmacCrc.h @@ -10,4 +10,9 @@ class DmacCrc { // Call after completion or abort. Busy retains ownership until the channel stops. // Only the successful DMA completion path may pass transferSucceeded=true. static Status finish(uint8_t channel, bool transferSucceeded, uint32_t& checksum); + // Uses the hardware CRC input register when bytes are transferred without DMA. + // Enables the DMAC clock without allocating a DMA channel; owner retains its token. + static Status beginInput(uint8_t owner, Polynomial polynomial, uint32_t seed); + static Status writeInput(uint8_t owner, uint8_t byte); + static Status finishInput(uint8_t owner, bool transferSucceeded, uint32_t& checksum); }; diff --git a/cores/arduino/SERCOM.cpp b/cores/arduino/SERCOM.cpp index 090836104..ef2c6f8d6 100644 --- a/cores/arduino/SERCOM.cpp +++ b/cores/arduino/SERCOM.cpp @@ -821,6 +821,12 @@ uint8_t SERCOM::calculateBaudrateSynchronous(uint32_t baudrate) */ void SERCOM::resetWIRE() { +#ifdef USE_ZERODMA + dmaAbortTx(); + dmaAbortRx(); +#endif + uint32_t ignored = 0; + finishCrcWIRE(false, ignored); clearQueueWIRE(); // Drain pending transactions from queue resetSERCOM(); // SWRST: hardware reset to default state _wire = WireConfig{}; // Reset software state @@ -945,6 +951,11 @@ void SERCOM::deferReceiveWIRE(void) { if (_dmaRxActive) return; #endif // USE_ZERODMA + // The preceding receive owns retirement, even after its DMA buffer fills. + // Its completion dispatch also starts this follow-up; do not queue it twice. + if (_wire.slaveTransactionActive && _wire.currentTxn && + !(_wire.currentTxn->config & I2C_CFG_READ)) + return; setPending((uint8_t)getSercomIndex()); } @@ -959,6 +970,11 @@ void SERCOM::deferRequestWIRE(void) { if (_wireRxSuspendPending || _dmaRxActive) return; #endif // USE_ZERODMA + // The preceding receive owns retirement, even after its DMA buffer fills. + // Its completion dispatch also starts this follow-up; do not queue it twice. + if (_wire.slaveTransactionActive && _wire.currentTxn && + !(_wire.currentTxn->config & I2C_CFG_READ)) + return; setPending((uint8_t)getSercomIndex()); } @@ -1117,6 +1133,87 @@ void SERCOM::setBaudrateWIRE(uint32_t baudrate) } +bool SERCOM::beginCrcWIRE() { + uint32_t ignored = 0; + if (_wire.crcActive && !finishCrcWIRE(false, ignored)) + return false; + _wire.crcRetired = false; + SercomTxn *txn = _wire.currentTxn; + if (!txn || !(txn->config & I2C_CFG_CRC)) + return true; + _wire.crcTransmit = isSlaveWIRE() == ((txn->config & I2C_CFG_READ) != 0); + _wire.crcDma = isDmaWIRE(); + DmacCrc::Status status; +#ifdef USE_ZERODMA + if (_wire.crcDma) { + if (!_dmaConfigured || !_dmaTx || !_dmaRx) + return false; + _wire.crcOwner = (_wire.crcTransmit ? _dmaTx : _dmaRx)->getChannel(); + status = DmacCrc::begin(_wire.crcOwner, DmacCrc::Polynomial::Crc32, 0xFFFFFFFFu); + } else +#endif + { + _wire.crcOwner = static_cast(getSercomIndex()); + status = DmacCrc::beginInput(_wire.crcOwner, DmacCrc::Polynomial::Crc32, 0xFFFFFFFFu); + } + _wire.crcActive = status == DmacCrc::Status::Ok; + return _wire.crcActive; +} + +bool SERCOM::finishCrcWIRE(bool success, uint32_t &checksum) { + if (!_wire.crcActive) + return false; + const DmacCrc::Status status = _wire.crcDma + ? DmacCrc::finish(_wire.crcOwner, success, checksum) + : DmacCrc::finishInput(_wire.crcOwner, success, checksum); + if (status != DmacCrc::Status::Busy) + _wire.crcActive = false; + return success ? status == DmacCrc::Status::Ok + : status == DmacCrc::Status::TransferFailed; +} + +bool SERCOM::prepareCrcTxWIRE() { + if (_wire.txnIndex != _wire.currentTxn->length || !_wire.crcActive) + return true; + uint32_t checksum = 0; + if (!finishCrcWIRE(true, checksum)) + return false; + for (uint8_t i = 0; i < 4; ++i) + _wire.crcTail[i] = static_cast(checksum >> (8 * i)); + return true; +} + +SercomWireError SERCOM::retireCrcWIRE(SercomWireError error) { + SercomTxn *txn = _wire.currentTxn; + if (!txn || !(txn->config & I2C_CFG_CRC) || _wire.crcRetired) + return error; + const size_t wireLength = _wire.txnIndex; + uint32_t checksum = 0; + if (error == SercomWireError::SUCCESS && !_wire.crcTransmit && wireLength == 0) { + // Address-only discovery probes carry no data or CRC trailer. + finishCrcWIRE(false, checksum); + _wire.crcRetired = true; + _wire.txnLength = txn->length; + return error; + } + if (error == SercomWireError::SUCCESS) { + if (_wire.crcTransmit) { + if (wireLength != _wire.txnLength || _wire.crcActive) + error = SercomWireError::CRC_ERROR; + } else if (!finishCrcWIRE(true, checksum) || wireLength < 4 || + checksum != 0x2144DF1Cu) { + error = SercomWireError::CRC_ERROR; + } + } + if (_wire.crcActive) + finishCrcWIRE(false, checksum); + _wire.crcRetired = true; + // Failed input is never made available as a partial protobuf message. + _wire.txnIndex = error == SercomWireError::SUCCESS ? wireLength - 4 : 0; + _wire.txnLength = txn->length; + return error; +} + SercomTxn* SERCOM::startTransmissionWIRE( void ) { // Writing ADDR.ADDR drives different behavior based on BUSSTATE: @@ -1133,6 +1230,28 @@ SercomTxn* SERCOM::startTransmissionWIRE( void ) SercomTxn *txn = _wire.currentTxn; if (txn == nullptr || txn->length == 0) return nullptr; + const auto failSlave = [this](SercomWireError error) -> SercomTxn * { +#ifdef USE_ZERODMA + dmaAbortTx(); + dmaAbortRx(); +#endif + uint32_t ignored = 0; + finishCrcWIRE(false, ignored); + prepareNackBitWIRE(); + prepareSlaveCommandBitsWIRE(WIRE_SLAVE_ACT_COMPLETE); + deferStopWIRE(error); + return nullptr; + }; +#ifdef USE_ZERODMA + if (isDmaWIRE()) { + if (!_dmaConfigured) + dmaInit(getSercomIndex()); + if (!_dmaConfigured || !_dmaTx || !_dmaRx) + return failSlave(SercomWireError::DMA_ERROR); + } +#endif + if (!beginCrcWIRE()) + return failSlave(SercomWireError::CRC_BUSY); const bool slaveTransmit = (txn->config & I2C_CFG_READ) != 0; const auto clearAmatch = [this]() { #if defined(__SAME53__) || defined(__SAME54__) @@ -1146,17 +1265,12 @@ SercomTxn* SERCOM::startTransmissionWIRE( void ) // Mirror the master engine selection: eligible transactions use DMA; // larger transactions remain on the interrupt-driven DATA path. if (isDmaWIRE()) { - if (!_dmaConfigured) - dmaInit(getSercomIndex()); - if (!_dmaConfigured || !_dmaTx || !_dmaRx) - return nullptr; - // AMATCH holds SCL low while software prepares the transfer. Arm the // DATA-triggered DMA channel before releasing AMATCH so the first DRDY // request cannot arrive before DMAC is ready. const bool started = slaveTransmit ? sendDataWIRE() : readDataWIRE(); if (!started) - return nullptr; + return failSlave(SercomWireError::DMA_ERROR); #if defined(__SAME53__) || defined(__SAME54__) enableInterrupts(SERCOM_I2CS_INTENSET_DRDY_Msk | SERCOM_I2CS_INTENSET_PREC_Msk); @@ -1209,7 +1323,7 @@ SercomTxn* SERCOM::startTransmissionWIRE( void ) _wire.currentTxn = txn; _wire.txnIndex = 0; - _wire.txnLength = txn->length; + _wire.txnLength = txn->length + ((txn->config & I2C_CFG_CRC) ? 4u : 0u); setDmaWIRE(false); // Reset DMA mode - let code below decide if DMA is used const bool read = (txn->config & I2C_CFG_READ) != 0; @@ -1241,7 +1355,7 @@ SercomTxn* SERCOM::startTransmissionWIRE( void ) stopTransmissionWIRE(SercomWireError::OTHER); return nullptr; #endif // !USE_ZERODMA - if (txn->length > 255) { + if (_wire.txnLength > 255) { stopTransmissionWIRE(SercomWireError::DATA_TOO_LONG); return nullptr; } @@ -1262,7 +1376,7 @@ SercomTxn* SERCOM::startTransmissionWIRE( void ) } #ifdef USE_ZERODMA else { - setDmaWIRE(txn->length > 0 && txn->length < 256 && + setDmaWIRE(_wire.txnLength > 0 && _wire.txnLength < 256 && (txn->config & I2C_CFG_STOP) && !(txn->config & I2C_CFG_NODMA)); } @@ -1278,13 +1392,18 @@ SercomTxn* SERCOM::startTransmissionWIRE( void ) } #if defined(__SAME53__) || defined(__SAME54__) - addrReg |= SERCOM_I2CM_ADDR_LENEN_Msk | SERCOM_I2CM_ADDR_LEN((uint8_t)txn->length); + addrReg |= SERCOM_I2CM_ADDR_LENEN_Msk | SERCOM_I2CM_ADDR_LEN((uint8_t)_wire.txnLength); #else - addrReg |= SERCOM_I2CM_ADDR_LENEN | SERCOM_I2CM_ADDR_LEN((uint8_t)txn->length); + addrReg |= SERCOM_I2CM_ADDR_LENEN | SERCOM_I2CM_ADDR_LEN((uint8_t)_wire.txnLength); #endif // __SAME53__ / __SAME54__ } #endif // USE_ZERODMA + if (!beginCrcWIRE()) { + stopTransmissionWIRE(SercomWireError::CRC_BUSY); + return nullptr; + } + // Send address (non-blocking; ISR handles ERROR/MB/SB) _wire.active = true; #if defined(__SAME53__) || defined(__SAME54__) @@ -1419,6 +1538,8 @@ void SERCOM::deferBusErrorRecoveryWIRE(bool arbitrationLost, dmaAbortRx(); setDmaWIRE(false); #endif + uint32_t ignored = 0; + finishCrcWIRE(false, ignored); _wire.busErrorRecoveryPending = true; _wire.busErrorRecoveryStartedUs = micros(); if (!_wire.busErrorRecoveryDeadlineActive) { @@ -1610,7 +1731,8 @@ SercomTxn* SERCOM::stopTransmissionWIRE( SercomWireError error ) // A master NACK is the expected physical end of a slave response. Keep // NACK_ON_DATA as the stop/release reason, but publish successful request // completion after the slave has released the bus. - if (error == SercomWireError::NACK_ON_DATA) + if (error == SercomWireError::NACK_ON_DATA && + (!(txn->config & I2C_CFG_CRC) || _wire.txnIndex == _wire.txnLength)) completionError = SercomWireError::SUCCESS; } @@ -1628,6 +1750,8 @@ SercomTxn* SERCOM::stopTransmissionWIRE( SercomWireError error ) dmaAbortTx(); dmaAbortRx(); #endif // USE_ZERODMA + completionError = retireCrcWIRE(completionError); + report.error = completionError; retireSlaveTransactionWIRE(followupPending); refreshCompletionReport(); if (txn->onComplete) @@ -1721,6 +1845,14 @@ SercomTxn* SERCOM::stopTransmissionWIRE( SercomWireError error ) setMasterWIRE(); } } +#ifdef USE_ZERODMA + if (txn && (txn->config & I2C_CFG_CRC)) { + dmaAbortTx(); + dmaAbortRx(); + } +#endif + completionError = retireCrcWIRE(completionError); + report.error = completionError; refreshCompletionReport(); // Preserve the tested async-DMA master retirement order: publish the // callback before inspecting chainNext, dequeuing, or restoring slave mode. diff --git a/cores/arduino/SERCOM.h b/cores/arduino/SERCOM.h index 39fa92aa0..8b7ebcedb 100644 --- a/cores/arduino/SERCOM.h +++ b/cores/arduino/SERCOM.h @@ -22,6 +22,7 @@ #include #include "SERCOM_PinMux.h" #include "SERCOM_Txn.h" +#include "DmacCrc.h" #include "RingBuffer.h" #include @@ -496,6 +497,10 @@ class SERCOM uint32_t division(uint32_t dividend, uint32_t divisor) ; void initClockNVIC( void ) ; void initWIRE(void) ; + bool beginCrcWIRE(); + bool finishCrcWIRE(bool success, uint32_t &checksum); + bool prepareCrcTxWIRE(); + SercomWireError retireCrcWIRE(SercomWireError error); // Cached I2C master/slave configuration for fast role switching. // This can be expanded to support additional configuration options @@ -533,6 +538,12 @@ class SERCOM size_t txnIndex = 0; size_t txnLength = 0; size_t dmaBlockLength = 0; + bool crcActive = false; + bool crcDma = false; + bool crcTransmit = false; + bool crcRetired = false; + uint8_t crcOwner = 0; + uint8_t crcTail[4] = {}; SercomWireCompletionReport lastCompletionReport = {}; } _wire; diff --git a/cores/arduino/SERCOM_Txn.h b/cores/arduino/SERCOM_Txn.h index aec16dc66..5ce2427db 100644 --- a/cores/arduino/SERCOM_Txn.h +++ b/cores/arduino/SERCOM_Txn.h @@ -55,6 +55,8 @@ enum class SercomWireError : uint8_t LENGTH_ERROR = 12, // LENERR when LEN/LENEN mismatch (STATUS.LENERR) UNKNOWN_ERROR = 13, // Error flag set but no specific bit matched DMA_ERROR = 14, // DMAC transfer/descriptor bus error + CRC_ERROR = 16, // Invalid or incomplete CRC-32 frame + CRC_BUSY = 17, // Shared hardware CRC engine unavailable BUS_RELEASE_TIMEOUT = 15 // STOP issued but BUSSTATE did not return to IDLE }; diff --git a/cores/arduino/SERCOM_inline.h b/cores/arduino/SERCOM_inline.h index fcc6f3199..66012330c 100644 --- a/cores/arduino/SERCOM_inline.h +++ b/cores/arduino/SERCOM_inline.h @@ -87,20 +87,29 @@ inline bool SERCOM::sendDataWIRE( void ) { SercomTxn* txn = _wire.currentTxn; if (txn == nullptr || txn->txPtr == nullptr) return false; + const bool crc = (txn->config & I2C_CFG_CRC) != 0; + if (crc && !prepareCrcTxWIRE()) { + abortWIRE(SercomWireError::CRC_ERROR); + return false; + } + const bool trailer = crc && _wire.txnIndex >= txn->length; + const uint8_t *source = trailer + ? _wire.crcTail + (_wire.txnIndex - txn->length) + : txn->txPtr + _wire.txnIndex; #ifdef USE_ZERODMA if (isDmaWIRE()) { DmaStatus value = DmaStatus::StartFailed; if (!_dmaTxActive && !_dmaRxActive) { - const size_t remaining = _wire.txnLength - _wire.txnIndex; + const size_t remaining = (crc && !trailer ? txn->length : _wire.txnLength) - _wire.txnIndex; _wire.dmaBlockLength = isSlaveWIRE() && remaining > 255u ? 255u : remaining; #if defined(__SAME53__) || defined(__SAME54__) - value = dmaStartTx(txn->txPtr + _wire.txnIndex, + value = dmaStartTx(source, &sercom->I2CM.SERCOM_DATA, _wire.dmaBlockLength); #else - value = dmaStartTx(txn->txPtr + _wire.txnIndex, + value = dmaStartTx(source, &sercom->I2CM.DATA.reg, _wire.dmaBlockLength); #endif // __SAME53__ / __SAME54__ @@ -109,11 +118,17 @@ inline bool SERCOM::sendDataWIRE( void ) } #endif // USE_ZERODMA + if (crc && !trailer && + DmacCrc::writeInput(_wire.crcOwner, *source) != DmacCrc::Status::Ok) { + abortWIRE(SercomWireError::CRC_ERROR); + return false; + } + ++_wire.txnIndex; // Wait for DATA to sync out of the ISR and clear MB #if defined(__SAME53__) || defined(__SAME54__) - sercom->I2CM.SERCOM_DATA = txn->txPtr[_wire.txnIndex++]; + sercom->I2CM.SERCOM_DATA = *source; #else - sercom->I2CM.DATA.reg = txn->txPtr[_wire.txnIndex++]; + sercom->I2CM.DATA.reg = *source; #endif // __SAME53__ / __SAME54__ // Return false when the last byte has been consumed so the caller can @@ -151,20 +166,25 @@ inline bool SERCOM::readDataWIRE( void ) { SercomTxn* txn = _wire.currentTxn; if (txn == nullptr || txn->rxPtr == nullptr) return false; + const bool crc = (txn->config & I2C_CFG_CRC) != 0; + const bool tail = crc && _wire.txnIndex >= txn->length; + uint8_t *destination = tail + ? _wire.crcTail + (_wire.txnIndex - txn->length) + : txn->rxPtr + _wire.txnIndex; #ifdef USE_ZERODMA if (isDmaWIRE()) { DmaStatus value = DmaStatus::StartFailed; if (!_dmaRxActive && !_dmaTxActive) { - const size_t remaining = _wire.txnLength - _wire.txnIndex; + const size_t remaining = (crc && !tail ? txn->length : _wire.txnLength) - _wire.txnIndex; _wire.dmaBlockLength = isSlaveWIRE() && remaining > 255u ? 255u : remaining; #if defined(__SAME53__) || defined(__SAME54__) - value = dmaStartRx(txn->rxPtr + _wire.txnIndex, + value = dmaStartRx(destination, &sercom->I2CM.SERCOM_DATA, _wire.dmaBlockLength); #else - value = dmaStartRx(txn->rxPtr + _wire.txnIndex, + value = dmaStartRx(destination, &sercom->I2CM.DATA.reg, _wire.dmaBlockLength); #endif // __SAME53__ / __SAME54__ @@ -202,11 +222,16 @@ inline bool SERCOM::readDataWIRE( void ) // Read DATA register (accesses auto-trigger bus operation based on ACKACT/SMEN) #if defined(__SAME53__) || defined(__SAME54__) - txn->rxPtr[_wire.txnIndex++] = sercom->I2CM.SERCOM_DATA; + *destination = sercom->I2CM.SERCOM_DATA; #else - txn->rxPtr[_wire.txnIndex++] = sercom->I2CM.DATA.reg; + *destination = sercom->I2CM.DATA.reg; #endif // __SAME53__ / __SAME54__ + ++_wire.txnIndex; + if (crc && DmacCrc::writeInput(_wire.crcOwner, *destination) != DmacCrc::Status::Ok) { + abortWIRE(SercomWireError::CRC_ERROR); + return false; + } return (_wire.txnIndex < _wire.txnLength); } @@ -276,7 +301,7 @@ inline void SERCOM::setTxnWIRE(SercomTxn* txn) _wire.returnValue = SercomWireError::SUCCESS; if (txn) - _wire.txnLength = txn->length; + _wire.txnLength = txn->length + ((txn->config & I2C_CFG_CRC) ? 4u : 0u); #ifdef USE_ZERODMA #if defined(__SAME53__) || defined(__SAME54__) const bool sclsm = @@ -393,11 +418,13 @@ inline void SERCOM::dmaTxCallbackWIRE(Adafruit_ZeroDMA* dma) // is generated automatically and STATUS.LENERR is raised with INTFLAG.ERROR. inst->_dmaTxActive = false; - if (inst->isSlaveWIRE()) { + if (inst->isSlaveWIRE() || (inst->_wire.currentTxn->config & I2C_CFG_CRC)) { inst->_wire.txnIndex += inst->_wire.dmaBlockLength; if (inst->_wire.txnIndex < inst->_wire.txnLength) { if (!inst->sendDataWIRE()) inst->abortWIRE(SercomWireError::DMA_ERROR); + } else if (!inst->isSlaveWIRE()) { + inst->deferStopWIRE(SercomWireError::SUCCESS); } } else { inst->_wire.txnIndex = inst->_wire.txnLength; @@ -423,10 +450,18 @@ inline void SERCOM::dmaRxCallbackWIRE(Adafruit_ZeroDMA* dma) // the abandoned suspend intent suppress suspension of the next RX phase. inst->_wireRxSuspendPending = false; inst->_wire.txnIndex += inst->_wire.dmaBlockLength; - if (inst->isSlaveWIRE() && + if ((inst->isSlaveWIRE() || (inst->_wire.currentTxn->config & I2C_CFG_CRC)) && inst->_wire.txnIndex < inst->_wire.txnLength) { if (!inst->readDataWIRE()) inst->abortWIRE(SercomWireError::DMA_ERROR); + } else if (inst->isSlaveWIRE() && (inst->_wire.currentTxn->config & I2C_CFG_CRC)) { + // Capacity is not a frame boundary. STOP or repeated START retires RX. + // Re-enable DRDY so an excess byte can be NACKed as overflow. +#if defined(__SAME53__) || defined(__SAME54__) + inst->enableInterrupts(SERCOM_I2CS_INTENSET_DRDY_Msk); +#else + inst->enableInterrupts(SERCOM_I2CS_INTENSET_DRDY); +#endif } else { inst->deferStopWIRE(SercomWireError::SUCCESS); } diff --git a/cspell.config.json b/cspell.config.json index b4c2aa1e1..0ed2ca56b 100644 --- a/cspell.config.json +++ b/cspell.config.json @@ -7,33 +7,83 @@ "AACKEN", "ACKACT", "Adafruit", + "AHBMASK", + "APBADIV", + "APBAMASK", + "APBASEL", + "APBBDIV", + "APBBSEL", + "APBCDIV", + "APBCMASK", + "APBCSEL", "ARBLOST", "Atmel", "atsame", + "atsuo", "BAUDLOW", + "BEIEN", + "BIASCAL", + "biascomp", + "biasr", + "biasref", + "BIASREFBUF", + "bootloaders", + "BPLCKC", "BTCNT", "BUFOVF", + "CACHEDIS", + "CALIB", + "CCDIS", + "CDWBN", "CHIPNAME", "CLKCTRL", "CLKEN", "CLKHOLD", + "CLKRDY", + "CMCC", "CMSIS", "Codegen", "CPHA", + "CRYSTALLESS", "CTRLA", "CTRLB", + "CYCCNT", + "CYCCNTENA", "Datasheet", "datasheets", "DBGCTRL", + "DEMCR", + "DFLL", + "DFLLCTRL", + "DFLLCTRLA", + "DFLLCTRLB", + "DFLLLCKC", + "DFLLLCKF", + "DFLLMUL", + "DFLLRDY", + "DFLLSYNC", + "DFLLVAL", "DIPO", "DMAC", + "DMIEN", "DOPO", "DORD", + "DPLL", + "DPLLCTRLA", + "DPLLCTRLB", + "DPLLRATIO", + "DPLLSTATUS", + "DPLLSYNCBUSY", "DRDY", + "ECIEN", + "FDPLL", "FLASHDRIVER", "GCEN", "GCLK", + "GCLKMAIN", "GENCEN", + "GENCTRL", + "GENEN", "HARDSER", "HSBAUD", "HSBAUDLOW", @@ -48,12 +98,16 @@ "itfnum", "jlink", "KCTRL", + "KRDY", + "LBYPASS", + "LDRFRAC", "LENEN", "LENERR", "LINUXBRIDGE", "LOWTOUT", "LSBFIRST", "MAINOSC", + "MANW", "Mclk", "MEXTTOUT", "microcontrollers", @@ -66,18 +120,31 @@ "NVIC", "NVMCTRL", "OSCCTRL", + "OSCM", + "OSCULP", "outclr", + "PADCAL", "PCHCTRL", + "PCLKSR", "pendsv", "PENDSVSET", "PERIPH", "PINMUX", "PORTA", "PORTB", + "PRESC", "QCEN", + "QLDIS", "QSPI", "Qwiic", + "RAWRMDIS", + "rctrl", + "REFCLK", "RFTM", + "RHIEN", + "RMDIS", + "RMEN", + "rnext", "ROREG", "rxbuf", "RXEN", @@ -98,21 +165,36 @@ "SWDIO", "SWRST", "SYNCBUSY", + "SYSCTRL", + "tannewt", "tenbit", "TENBITEN", "Thach", "TINYUSB", + "TRANSN", + "TRCENA", "TRCPT", + "TRSIZE", "TXEN", "TXPO", + "UALGO", "UART", + "UIHASH", + "UIHVAL", + "URAD", "USART", "USBCDC", + "USBCRM", "USBD", + "usbtransn", + "usbtrim", "VECTACTIVE", + "venv", "VREFA", "VREFB", + "WRBA", "WRBADDR", + "XTALEN", "ZERODMA" ], "ignoreWords": [], diff --git a/docs/research/harmony-hardware-crc.md b/docs/research/harmony-hardware-crc.md new file mode 100644 index 000000000..d2702cf01 --- /dev/null +++ b/docs/research/harmony-hardware-crc.md @@ -0,0 +1,238 @@ +# Harmony hardware CRC on SAM D21 and SAM D5x/E5x + +## Question and sources + +Research date: 2026-09-27. How does Microchip Harmony 3 implement hardware CRC +on the SAM D21 and SAM E54, and what can the Framework reuse? This is source +and datasheet research, not a hardware validation or a protocol CRC selection. + +Primary source revisions: + +- Harmony CSP templates: [`e846e68aa557b450d9fff03f00b000c50fb97f63`](https://github.com/Microchip-MPLAB-Harmony/csp/tree/e846e68aa557b450d9fff03f00b000c50fb97f63). +- SAM D21 examples: [`9331e79cf2937d2b3166813c6d2886b2481162e3`](https://github.com/Microchip-MPLAB-Harmony/csp_apps_sam_d21_da1/tree/9331e79cf2937d2b3166813c6d2886b2481162e3). +- SAM E54 examples: [`4a38194d468e94d4710c31bbfd67fbfe9f5d2096`](https://github.com/Microchip-MPLAB-Harmony/csp_apps_sam_d5x_e5x/tree/4a38194d468e94d4710c31bbfd67fbfe9f5d2096). +- Locally archived Microchip [D21 datasheet, DS40001882K (2025)][d21], + [D5x/E5x datasheet, DS60001507M (2024)][e54], + [D21 errata, DS80000760M (2025)][d21-errata], and + [D5x/E5x errata, DS80000748T (2023)][e54-errata]. Section numbers below refer + to these archived editions. The parsed local datasheets were read before + interpreting registers; later online revisions may differ. + +Downloaded source excerpts are under `build/harmony-crc-research/`. Framework +inspection used `5cec3c8817048fe598677d7349c5c55218fd988f` and its ZeroDMA +submodule `9215aa0848a092960fc1ba4e02cb4989af2f46e6`. + +## Findings + +Harmony provides two distinct routes on these chips: CRC integrated into the +**DMAC peripheral library**, and a **DSU peripheral library** for memory CRC32. +The DMAC route supports both CPU-fed data and a selected DMA channel. Its +asynchronous route uses ordinary DMA completion/error handling. The DSU wrapper +is blocking. Neither supplies a complete queued CRC service for this +Framework's SPI, Serial, and Wire transactions. [D21 DMAC implementation][d21-dmac], +[E54 DMAC implementation][e54-dmac], [D21 DSU implementation][d21-dsu], +[E54 DSU implementation][e54-dsu]. + +| Property | SAM D21 DMAC | SAM D5x/E5x DMAC | DSU on both families | +| --- | --- | --- | --- | +| Hardware polynomial | CRC16 `0x1021` or CRC32 `0x04C11DB7` | Same | CRC32, reversed representation `0xEDB88320` | +| Input | CPU writes or one selected DMA channel | Same, plus memory generation/monitor modes | Word-aligned address and byte length, both multiples of four | +| Initial value | Caller-provided seed | Caller-provided seed; memory modes take seed through descriptor destination field | Caller-provided seed | +| Harmony completion | CPU routine blocks; DMA channel callback or status | Same | Polls `DONE`, checks bus error | +| Independent CRC resources | One shared DMAC CRC engine | One shared DMAC CRC engine | Separate DSU engine | + +Sources: [D21 DMAC header][d21-h], [E54 DMAC header][e54-h]; datasheets [D21][d21] +sections 13.11.3, 20.6.3, 20.8.2-20.8.5 and [D5x/E5x][e54] sections 12.11.3, +22.6.3.8-22.6.3.10, 22.8.2-22.8.5. + +## The actual Harmony DMAC sequence + +The SAM implementations expose these operations: + +```c +void DMAC_ChannelCRCSetup(DMAC_CHANNEL channel, DMAC_CRC_SETUP setup); +uint32_t DMAC_CRCRead(void); +uint32_t DMAC_CRCCalculate(void *buffer, uint32_t length, DMAC_CRC_SETUP setup); +void DMAC_CRCDisable(void); +``` + +D21's setup contains polynomial selection and seed. D5x/E5x adds `crc_mode`. +There are no caller-selectable polynomial coefficients, input/output reflection, +or final-XOR fields in these SAM structures. [D21 header][d21-h], +[E54 header][e54-h]. + +For the channel path, Harmony disables the CRC engine, seeds it, selects the +polynomial and channel (`CRCSRC = 0x20 + channel`), then lets the caller start +the DMA transfer. D21 separately enables `CTRL.CRCENABLE`; D5x/E5x enables CRC +by selecting the source. This configures the single engine to observe that +channel; it does not give every channel its own CRC accumulator. Both setup +routines overwrite global CRC state without a busy rejection or software +reservation. [D21 implementation][d21-dmac], [E54 implementation][e54-dmac]. + +Microchip's [D21 example][d21-example] and [E54 example][e54-example] register a +DMA callback, calculate a software reference, run the CPU-fed CRC, then configure +CRC for channel 0 and launch a memory-to-memory transfer. On successful DMA +completion they check the copied data and compare `DMAC_CRCRead()` with the +software result. Errors are separate callback events. The example tests nine +literal bytes, `123456789`, seeded with `0xFFFFFFFF`, expecting `0xCBF43926`. +It does not append zero padding or convert the seed to a non-direct form. + +That last point matters: the [generic Harmony `DMAC_ChannelCRCSetup` page][api] +combines examples from different device families. Its programmable-polynomial, +non-direct-seed and zero-padding material must not be applied to these SAM +implementations. Harmony's separate [`crc_02672` header][other-crc] has reflection, +polynomial-length and final-XOR fields, but it is not the DMAC/DSU interface used +by the D21 and E54 examples. + +### CPU feed, alignment, and ordering + +`DMAC_CRCCalculate()` also uses the DMAC CRC engine, but consumes no DMA channel +for data transfer. It chooses word beats when length is divisible by four, +halfword beats for other even lengths, otherwise byte beats. It reads the +buffer through matching integer pointers, writes `CRCDATAIN`, waits until +`CRCBUSY` is set, and clears that flag with a write-one-to-clear operation after +each beat. The loop and result return are synchronous; there is no timeout or +error result. It chooses alignment from **length only**, without checking the +buffer address. These details are visible in both [D21][d21-dmac] and +[E54][e54-dmac] implementations and should not be copied without reviewing the +Framework's unaligned byte-buffer requirements. + +The hardware processes bytes internally; DMA beat width comes from the channel, +while CPU feed uses `CRCBEATSIZE`. The manuals describe the I/O busy flag as +software-cleared, unlike DMA mode where enabling/disabling the channel controls +it. Harmony therefore must not be translated into a conventional "wait until +busy clears" loop. [D21][d21] sections 20.6.3 and 20.8.5; +[D5x/E5x][e54] sections 22.6.3.8 and 22.8.5. + +For completed CRC32, `CRCCHKSUM` reads are automatically bit-reversed and +complemented. While busy, the register exposes the internal value instead; +CRC16 reads are untransformed. A completed CRC32 result consequently is not +interchangeable with an internal continuation seed. The fixed reflection +behavior for the demonstrated CRC32 configuration is also checked by +Microchip's software reference. [D21][d21] section 20.8.4; +[D5x/E5x][e54] section 22.8.4; [D21 example][d21-example], +[E54 example][e54-example]. + +The nine-byte example does **not** establish equivalence of byte, halfword and +word beats, fragmented buffers, or arbitrary seeds. The reviewed prose does not +unambiguously specify every byte-lane ordering case. Those remain explicit +hardware-vector tests before defining a general API. CRC-register read order, +byte order within a beat, and CRC byte order on the wire are separate questions; +Harmony's example does not define a packet format. + +### Completion, cancellation, and E54 memory modes + +Harmony uses the existing channel interrupt handler: `TCMPL` produces a complete +event and `TERR` produces an error event; both clear its channel-busy state. +`CRCRead()` only reads the register, without checking completion. An aborted +transfer can therefore have a partial CRC, not a valid complete-frame result. +The caller must retain that distinction and reset the CRC seed before a fresh +calculation. This follows the [E54 implementation][e54-dmac] and [D5x/E5x][e54] +section 22.6.3.8. DMA completion is evidence of DMA data movement, not proof that +a UART/SPI shift register or I2C transaction has completed on the wire. + +E54 additionally has `CRCGEN` and `CRCMON`. In memory generation mode a descriptor's +`DSTADDR` is an initial checksum value, not a normal destination pointer. The +hardware calculates directly over source memory and reports DMA completion. +Memory monitor mode checks the block including its expected checksum, reports +CRC mismatch through channel error status/`TERR`, and can repeat descriptor +lists. These modes are not present in the D21 setup type. They are specialized +memory operations, not automatic packet append/verify modes for every SERCOM. +[D5x/E5x][e54] sections 22.6.3.9-22.6.3.10; +[E54 header][e54-h] and descriptor construction in [E54 implementation][e54-dmac]. + +## DSU is a different option + +Both Harmony DSU implementations take a start address, byte length, seed, and +output pointer. They write `ADDR`, `LENGTH`, and `DATA`, clear status, start +`CTRL.CRC`, poll `STATUSA.DONE`, and return failure on `BERR`. They reject zero +length and a null output pointer, but do not check word alignment or bound the +wait. They return the raw `DATA` register; the wrapper does not apply the final +complement. [D21 DSU][d21-dsu], [E54 DSU][e54-dsu]. + +The hardware accepts a memory region with word-aligned address and length. +Raw results can seed another region; complement the last result for the standard +CRC32 value described in the manual. A running operation can be cancelled with +DSU software reset. Harmony's E54 example clears DSU PAC write protection before +calling the PLIB. This is useful evidence for a memory-check facility, but not +an arbitrary-length streaming CRC API. [D21][d21] section 13.11.3; +[D5x/E5x][e54] section 12.11.3; [E54 DSU example][dsu-example]. + +## Silicon errata that affect the design + +- D21 revisions A-F: consecutive instruction writes to `CRCDATAIN` may calculate + incorrectly; the documented workaround inserts a NOP between writes. + Harmony's polling/clear sequence contains intervening accesses, but any tighter + CPU loop needs this requirement checked in its generated instructions. + [D21 errata][d21-errata] section 1.7.1. +- D21 revisions A-D: DSU CRC32 over RAM needs the documented workaround around + the operation (`0x41007058`, clear `0x30000`, then set `0x20000`). The reviewed + Harmony DSU wrapper does not implement it. [D21 errata][d21-errata] section 1.8.3; + [D21 DSU source][d21-dsu]. +- D5x/E5x revisions A, D, F, G: DSU CRC32 over NVM never completes when the NVM + cache is disabled; ensure that cache is enabled. A blocking wrapper without a + timeout will not recover from that condition. [D5x/E5x errata][e54-errata] + section 2.7.1; [E54 DSU source][e54-dsu]. + +These are findings from the archived errata editions, not a claim about the +connected board's revision. Read its device revision and check applicable errata +before hardware acceptance. Existing linked-descriptor DMA errata also remain +relevant when adding CRC DMA jobs; CRC does not bypass the underlying DMAC. +[D21 errata][d21-errata] section 1.7.2; [D5x/E5x errata][e54-errata] section 2.10.1. + +## What this means for the Framework + +The Framework already queues `SercomTxn*` pointers and holds private transmit +and receive ZeroDMA objects. Transaction headers define `I2C_CFG_CRC`, +`SPI_CFG_CRC`, and `UART_CFG_CRC`, but the inspected core, ZeroDMA, SPI and Wire +sources contain no CRC peripheral implementation; those flags are not an +existing CRC service. [SERCOM.h](../../cores/arduino/SERCOM.h), +[SERCOM_Txn.h](../../cores/arduino/SERCOM_Txn.h). + +ZeroDMA owns the controller's allocation and lifetime. Its first allocated +channel resets the entire controller and installs descriptor/writeback tables; +freeing its final channel disables the controller, interrupts and clock. Harmony's +`DMAC_Initialize()` installs its own tables. Importing that initialization as a +second runtime would conflict with the Framework, and even CPU-fed DMAC CRC +must participate in the existing clock/reset lifetime. ZeroDMA already exposes +`getChannel()`, which can identify a channel to the CRC hardware without another +allocator. [ZeroDMA implementation][zerodma], [ZeroDMA header][zerodma-h], +[Harmony initialization][e54-dmac]. + +The reusable Harmony pattern is therefore **configure the existing hardware +engine, start work through the existing DMA owner, consume completion/error, +then read the result**. What still needs Framework design and validation is +serialization of that single CRC accumulator across overlapping clients, +transaction/buffer lifetime, cancellation and retries, continuation semantics, +and the non-DMA path. This is an integration conclusion from the sources above, +not a requirement to introduce an additional transport or ownership protocol. +Selecting one CRC source does not require stopping unrelated DMA channels; the +shared limitation concerns simultaneous independent CRC calculations. + +A useful next investigation is a Framework-level hardware harness on D21 and +E54: reproduce Microchip's known vector, compare CPU and channel feed for byte, +halfword and word transfers, vary address alignment and lengths, test seeds and +split buffers, then exercise abort/retry and overlapping SERCOM work. Native +regressions can check scheduling and lifetime, but only hardware tests establish +peripheral byte ordering and silicon behavior. These results should inform the +CRC library contract before or alongside the protocol's polynomial and wire +format decision. No implementation, protocol choice, firmware flash, commit or +PR was made for this research. + +[d21]: ../../../SimIODevice/docs/datasheets/SAM-D21-DA1-Family-Data-Sheet-DS40001882.pdf +[e54]: ../../../SimIODevice/docs/datasheets/SAM-D5x-E5x-Family-Data-Sheet-DS60001507.pdf +[d21-errata]: ../../../SimIODevice/docs/datasheets/SAM-D21DA1-Family-Silicon-Errata-and-Data-Sheet-Clarification-DS80000760.pdf +[e54-errata]: ../../../SimIODevice/docs/datasheets/SAM-D5x-E5x-Family-Silicon-Errata-and-Data-Sheet-Clarification-DS80000748.pdf +[d21-dmac]: https://github.com/Microchip-MPLAB-Harmony/csp/blob/e846e68aa557b450d9fff03f00b000c50fb97f63/peripheral/dmac_u2223/templates/plib_dmac.c.ftl#L544-L656 +[e54-dmac]: https://github.com/Microchip-MPLAB-Harmony/csp/blob/e846e68aa557b450d9fff03f00b000c50fb97f63/peripheral/dmac_u2503/templates/plib_dmac.c.ftl +[d21-h]: https://github.com/Microchip-MPLAB-Harmony/csp/blob/e846e68aa557b450d9fff03f00b000c50fb97f63/peripheral/dmac_u2223/templates/plib_dmac.h.ftl#L107-L178 +[e54-h]: https://github.com/Microchip-MPLAB-Harmony/csp/blob/e846e68aa557b450d9fff03f00b000c50fb97f63/peripheral/dmac_u2503/templates/plib_dmac.h.ftl#L117-L184 +[d21-dsu]: https://github.com/Microchip-MPLAB-Harmony/csp/blob/e846e68aa557b450d9fff03f00b000c50fb97f63/peripheral/dsu_u2209/templates/plib_dsu.c.ftl#L57-L92 +[e54-dsu]: https://github.com/Microchip-MPLAB-Harmony/csp/blob/e846e68aa557b450d9fff03f00b000c50fb97f63/peripheral/dsu_u2410/templates/plib_dsu.c.ftl#L57-L92 +[d21-example]: https://github.com/Microchip-MPLAB-Harmony/csp_apps_sam_d21_da1/blob/9331e79cf2937d2b3166813c6d2886b2481162e3/apps/dmac/dmac_crc32_generate/firmware/src/main.c +[e54-example]: https://github.com/Microchip-MPLAB-Harmony/csp_apps_sam_d5x_e5x/blob/4a38194d468e94d4710c31bbfd67fbfe9f5d2096/apps/dmac/dmac_crc32_generate/firmware/src/main.c +[dsu-example]: https://github.com/Microchip-MPLAB-Harmony/csp_apps_sam_d5x_e5x/blob/4a38194d468e94d4710c31bbfd67fbfe9f5d2096/apps/dsu/dsu_crc32_generate/firmware/src/main.c#L238-L250 +[api]: https://onlinedocs.microchip.com/oxy/GUID-450989FA-38E4-4D68-AB61-15ADB29AD718-en-US-6/GUID-17431D0E-B538-41B9-8FA9-861BAC4C365E.html +[other-crc]: https://github.com/Microchip-MPLAB-Harmony/csp/blob/e846e68aa557b450d9fff03f00b000c50fb97f63/peripheral/crc_02672/templates/plib_crc.h.ftl#L76-L98 +[zerodma]: https://github.com/crabel99/Adafruit_ZeroDMA/blob/9215aa0848a092960fc1ba4e02cb4989af2f46e6/Adafruit_ZeroDMA.cpp#L380-L574 +[zerodma-h]: https://github.com/crabel99/Adafruit_ZeroDMA/blob/9215aa0848a092960fc1ba4e02cb4989af2f46e6/Adafruit_ZeroDMA.h#L175 diff --git a/libraries/Wire/Wire.cpp b/libraries/Wire/Wire.cpp index b69c223a1..854e90d01 100644 --- a/libraries/Wire/Wire.cpp +++ b/libraries/Wire/Wire.cpp @@ -249,7 +249,8 @@ uint8_t TwoWire::requestFrom(uint8_t address, size_t quantity, bool stopBit, uin } loader.config = I2C_CFG_READ | (stopBit ? I2C_CFG_STOP : 0) | - (replaySafe ? I2C_CFG_REPLAY_SAFE : 0); + (replaySafe ? I2C_CFG_REPLAY_SAFE : 0) | + (crcEnabled ? I2C_CFG_CRC : 0); loader.address = address; // Allocate fresh transaction from pool and copy loader data @@ -344,7 +345,8 @@ uint8_t TwoWire::endTransmission(bool stopBit, // Set parameters that weren't known during beginTransmission/write txn->config = (loader.config & I2C_CFG_REPLAY_SAFE) | - (stopBit ? I2C_CFG_STOP : 0); + (stopBit ? I2C_CFG_STOP : 0) | + (crcEnabled && txn->length != 0 ? I2C_CFG_CRC : 0); if (onComplete) { txn->onComplete = onComplete; txn->user = (user == nullptr) ? txn : user; @@ -575,7 +577,7 @@ void TwoWire::prepareSlaveReceive() { slaveTxn = SercomTxn{}; slaveTxn.rxPtr = rxBufferPtr; slaveTxn.length = rxBufferCapacity; - slaveTxn.config = 0; + slaveTxn.config = crcEnabled ? I2C_CFG_CRC : 0; slaveTxn.onComplete = &TwoWire::onSlaveReceiveComplete; slaveTxn.user = this; sercom->setSlaveTxnWIRE(&slaveTxn); @@ -588,10 +590,7 @@ void TwoWire::onDeferredReceive(void *user) { self->prepareSlaveReceive(); self->sercom->setTxnWIRE(&self->slaveTxn); self->sercom->markSlaveTransactionActiveWIRE(); - if (!self->sercom->startTransmissionWIRE()) { - self->sercom->deferStopWIRE(SercomWireError::DMA_ERROR); - return; - } + self->sercom->startTransmissionWIRE(); } void TwoWire::onDeferredRequest(void *user) { @@ -610,15 +609,13 @@ void TwoWire::onDeferredRequest(void *user) { return; } self->slaveTxn = self->loader; - self->slaveTxn.config |= I2C_CFG_READ; + self->slaveTxn.config |= I2C_CFG_READ | + (self->crcEnabled ? I2C_CFG_CRC : 0); self->slaveTxn.onComplete = &TwoWire::onSlaveRequestComplete; self->slaveTxn.user = self; self->sercom->setTxnWIRE(&self->slaveTxn); self->sercom->markSlaveTransactionActiveWIRE(); - if (!self->sercom->startTransmissionWIRE()) { - self->sercom->deferStopWIRE(SercomWireError::DMA_ERROR); - return; - } + self->sercom->startTransmissionWIRE(); } void TwoWire::onSlaveReceiveComplete(void *user, int status) { diff --git a/libraries/Wire/Wire.h b/libraries/Wire/Wire.h index 54e54358a..a9c607635 100644 --- a/libraries/Wire/Wire.h +++ b/libraries/Wire/Wire.h @@ -48,6 +48,10 @@ class TwoWire : public Stream bool enable10Bit = false); void end(); void setClock(uint32_t); + // Opt-in CRC-32/IEEE in both bus roles, seed 0xFFFFFFFF, little-endian trailer. + // Configure before begin(); payload capacity excludes the four CRC bytes. + // Empty address probes carry no trailer. Other devices on this bus must agree. + void setCrcEnabled(bool enabled) { crcEnabled = enabled; } void beginTransmission(uint8_t); void setTransactionReplaySafe(bool replaySafe = true); @@ -110,6 +114,7 @@ class TwoWire : public Stream uint8_t _uc_pinSCL; bool transmissionBegun; + bool crcEnabled = false; // RX/TX buffers (sync compatibility, async staging) static constexpr size_t WIRE_BUFFER_LENGTH = 255; @@ -464,6 +469,15 @@ inline void TwoWire::onService(void) return; } + if (slaveDrdy && activeTxn && (activeTxn->config & I2C_CFG_CRC) && + !(activeTxn->config & I2C_CFG_READ) && + sercom->getTxnIndexWIRE() >= sercom->getTxnLengthWIRE()) { + sercom->prepareNackBitWIRE(); + sercom->prepareSlaveCommandBitsWIRE(WIRE_SLAVE_ACT_COMPLETE); + sercom->deferStopWIRE(SercomWireError::DATA_TOO_LONG); + return; + } + // Preserve the interrupt-driven slave byte engine both when DMA is not // compiled and when this transaction is not DMA-eligible. if (slaveDrdy && !amatch && !sercom->isDmaWIRE()) {