GCC Code Coverage Report


Directory: lib/fprime-samd/fprime-samd/
File: Drv/I2cDriver/I2cDriver.cpp
Date: 2026-09-26 00:05:09
Exec Total Coverage
Lines: 0 348 0.0%
Functions: 0 18 0.0%
Branches: 0 523 0.0%

Line Branch Exec Source
1 // ======================================================================
2 // \title I2cDriver.cpp
3 // \author tumbar
4 // \brief cpp file for I2cDriver component implementation class
5 // ======================================================================
6
7 #include "fprime-samd/Drv/I2cDriver/I2cDriver.hpp"
8 #include "Fw/Types/Assert.hpp"
9 #include "config/FwAssertArgTypeAliasAc.h"
10 #include "fprime-samd/Drv/I2cDriver/I2cDriverHardware.hpp"
11 #include "fprime-samd/Drv/Types/CriticalSection.hpp"
12 #include "fprime-samd/Drv/Types/Sercom.hpp"
13 #include "fprime-samd/Drv/Types/StatusEnumAc.hpp"
14 #include "fprime-samd/Drv/Types/ThinBuffer.hpp"
15 #include "samd-config/I2cDriverConfig.hpp"
16
17 namespace Samd21 {
18
19 // ----------------------------------------------------------------------
20 // Component construction and destruction
21 // ----------------------------------------------------------------------
22
23 ✗ I2cDriver ::I2cDriver(const char* const compName)
24 : I2cDriverComponentBase(compName),
25 ✗ m_sercom(SercomKind::SERCOM_0),
26 ✗ m_configured(false),
27 ✗ m_state(I2cDriver::State::IDLE),
28 ✗ m_pendingStatus(Drv::I2cStatus::I2C_OK),
29 ✗ m_portNum(),
30 ✗ m_read(),
31 ✗ m_pending_read_address(0),
32 ✗ m_write(),
33 ✗ m_tlmErrors(0),
34 ✗ m_stallTicks(0),
35 ✗ m_stallRecoveryCount(0) {}
36
37 ✗ I2cDriver ::~I2cDriver() {}
38
39 ✗ void I2cDriver::configure(SercomKind sercom,
40 SclLowTimeout scl_low_timeout,
41 InactiveTimeout inactive_timeout,
42 ClockStretchMode clock_stretch_mode,
43 Frequency frequency,
44 ClientSclLowTimeout client_scl_low_timeout,
45 HostSclLowTimeout host_scl_low_timeout,
46 SdaHold sda_hold,
47 PinUsage pin_usage,
48 RunInStandby run_in_standby) {
49 ✗ FW_ASSERT(!this->m_configured, sercom.e);
50 ✗ FW_ASSERT(this->m_state == State::IDLE, static_cast<FwAssertArgType>(this->m_state));
51
52 // Store configuration
53 ✗ this->m_sercom = sercom;
54
55 // Register with the ISR callback table
56 ✗ I2cHardware::I2cHal::registerIsr(sercom, I2cDriver::s_i2cDriverIsrHandler, this);
57
58 // Configure and enable the SERCOM I2C host peripheral.
59 ✗ I2cHardware::I2cHal::configure(sercom, scl_low_timeout, inactive_timeout, clock_stretch_mode, frequency,
60 client_scl_low_timeout, host_scl_low_timeout, sda_hold, pin_usage, run_in_standby);
61
62 ✗ this->m_configured = true;
63 ✗ }
64
65 ✗ void I2cDriver::s_i2cDriverIsrHandler(Fw::PassiveComponentBase* i2cDriverRaw, SercomKind sercom) {
66 ✗ auto i2cDriver = reinterpret_cast<I2cDriver*>(i2cDriverRaw);
67
68 ✗ FW_ASSERT(i2cDriver != nullptr);
69 ✗ FW_ASSERT(sercom == i2cDriver->m_sercom, sercom, i2cDriver->m_sercom);
70 ✗ i2cDriver->isrHandler();
71 ✗ }
72
73 ✗ void I2cDriver ::isrHandler() {
74 // There are two interrupt sources:
75 // 1. Errors on the I2C bus
76 // 2. Post-Tx ACK from the slave during a writeRead
77 //
78 // We need to handle both cases in the same handler
79 ✗ const I2cHardware::I2cInterruptStatus status = I2cHardware::I2cHal::readInterruptStatus(this->m_sercom);
80
81 ✗ if (status.error) {
82 // The interrupt source was an error
83 // The other interrupt source _could_ still be set but we should ack it and ignore it
84
85 // Look up the error(s)
86 ✗ if (status.busError) {
87 ✗ this->m_tlmErrors++;
88 ✗ this->log_WARNING_LO_I2cBusError(this->m_sercom, Samd21::I2cDriver_I2cError::BUS_ERROR);
89 }
90
91 ✗ if (status.arbLost) {
92 ✗ this->m_tlmErrors++;
93 ✗ this->log_WARNING_LO_I2cBusError(this->m_sercom, Samd21::I2cDriver_I2cError::ARBITRATION_LOST);
94 }
95
96 ✗ if (status.lowTimeout) {
97 ✗ this->m_tlmErrors++;
98 ✗ this->log_WARNING_LO_I2cBusError(this->m_sercom, Samd21::I2cDriver_I2cError::SCL_LOW_TIMEOUT);
99 }
100
101 ✗ if (status.masterExtTimeout) {
102 ✗ this->m_tlmErrors++;
103 ✗ this->log_WARNING_LO_I2cBusError(this->m_sercom, Samd21::I2cDriver_I2cError::MASTER_SCL_EXTEND_TIMEOUT);
104 }
105
106 ✗ if (status.slaveExtTimeout) {
107 ✗ this->m_tlmErrors++;
108 ✗ this->log_WARNING_LO_I2cBusError(this->m_sercom, Samd21::I2cDriver_I2cError::SLAVE_SCL_EXTEND_TIMEOUT);
109 }
110
111 ✗ if (status.lengthError) {
112 ✗ this->m_tlmErrors++;
113 ✗ this->log_WARNING_LO_I2cBusError(this->m_sercom, Samd21::I2cDriver_I2cError::LENGTH_ERROR);
114 }
115
116 // Acknowledge the master-on-bus flag (if set) and all error flags.
117 ✗ I2cHardware::I2cHal::acknowledgeErrors(this->m_sercom);
118
119 // Handle the error. The DMA channels are aborted here (the transaction
120 // must stop immediately), but the client reply is NOT issued from this ISR:
121 // the driver moves into the matching COMPLETE_* state and records the
122 // status, and the reply is delivered from activeIn in the main context.
123 ✗ switch (this->m_state) {
124 ✗ case State::IDLE:
125 case State::COMPLETE_READ:
126 case State::COMPLETE_WRITE:
127 case State::COMPLETE_WRITE_READ:
128 // Error IRQ with no in-flight transaction: either genuinely idle,
129 // or a completion is already recorded and waiting for activeIn to
130 // deliver it. Nothing to abort or re-report; just flag it and leave
131 // the pending completion untouched.
132 ✗ this->log_WARNING_HI_UnexpectedInterrupt(this->m_sercom, I2cDriver_I2CInterrupt::BUS_ERROR);
133 ✗ break;
134 ✗ case State::READ:
135 ✗ this->dmaTransactionAbortOut_out(I2cDriver_DmaChannel::READ);
136 ✗ this->m_pendingStatus = Drv::I2cStatus::I2C_READ_ERR;
137 ✗ this->m_state = State::COMPLETE_READ;
138 ✗ break;
139 ✗ case State::WRITE:
140 ✗ this->dmaTransactionAbortOut_out(I2cDriver_DmaChannel::WRITE);
141 ✗ this->m_pendingStatus = Drv::I2cStatus::I2C_WRITE_ERR;
142 ✗ this->m_state = State::COMPLETE_WRITE;
143 ✗ break;
144 ✗ case State::WRITE_READ_WRITING_WAIT:
145 case State::WRITE_READ_WRITING:
146 // WRITE_READ_WRITING_WAIT has the MB interrupt enabled (armed by the
147 // write-DMA completion). Disable it before recovering, otherwise the
148 // stale enable leaks into the next transaction and can latch MB
149 // against an unexpected state. Harmless in WRITE_READ_WRITING (not
150 // yet armed).
151 ✗ if (this->m_state == State::WRITE_READ_WRITING_WAIT) {
152 ✗ I2cHardware::I2cHal::disableMasterOnBusInterrupt(this->m_sercom);
153 }
154
155 ✗ this->dmaTransactionAbortOut_out(I2cDriver_DmaChannel::WRITE);
156 ✗ this->dmaTransactionAbortOut_out(I2cDriver_DmaChannel::READ);
157 ✗ this->m_pendingStatus = Drv::I2cStatus::I2C_WRITE_ERR;
158 ✗ this->m_state = State::COMPLETE_WRITE_READ;
159 ✗ break;
160 ✗ case State::WRITE_READ_READING:
161 // Write already finished, no need to abort it
162 ✗ this->dmaTransactionAbortOut_out(I2cDriver_DmaChannel::READ);
163 ✗ this->m_pendingStatus = Drv::I2cStatus::I2C_READ_ERR;
164 ✗ this->m_state = State::COMPLETE_WRITE_READ;
165 ✗ break;
166 ✗ default:
167 ✗ FW_ASSERT(false, this->m_sercom, static_cast<FwAssertArgType>(this->m_state));
168 }
169 ✗ } else if (!status.masterOnBus) {
170 // Woke with neither ERROR nor MB set: a spurious interrupt. This is
171 // expected. When a write-read handoff is serviced inline from the write-DMA
172 // completion (dmaReplyIn WRITE_READ_WRITING), we enable INTENSET.MB while MB
173 // is already latched, which asserts the SERCOM line and latches the NVIC
174 // pending bit before we ack MB and disable the interrupt. That stale pending
175 // bit still fires one SERCOM interrupt afterwards. MB is already cleared, so
176 // there is nothing to do -- any real pending source (ERROR) is level-
177 // sensitive and would have re-latched into status above.
178 ✗ return;
179 } else {
180 // Master on bus.
181
182 // Ack the interrupt. MB is level-triggered and clearing it here (write-1)
183 // prevents it from re-latching this handler; the transaction is advanced by
184 // the actions taken below (writing ADDR/DATA), not by the ack itself.
185 ✗ I2cHardware::I2cHal::acknowledgeMasterOnBus(this->m_sercom);
186
187 ✗ switch (this->m_state) {
188 ✗ case State::WRITE_READ_WRITING_WAIT:
189 ✗ this->serviceWriteReadMasterOnBus();
190 ✗ break;
191 ✗ default:
192 // MB fired in a state that does not expect it. Ack (done above) is
193 // not enough on its own: leaving the MB interrupt enabled against a
194 // stale state would let it re-fire indefinitely, so disable it here.
195 // Without this, a single mis-ordered handoff could latch MB with the
196 // bus idle and wedge every future write-read (see serviceWriteRead-
197 // MasterOnBus / dmaReplyIn WRITE_READ_WRITING).
198 ✗ I2cHardware::I2cHal::disableMasterOnBusInterrupt(this->m_sercom);
199 ✗ this->log_WARNING_HI_UnexpectedInterrupt(this->m_sercom, I2cDriver_I2CInterrupt::MASTER_ON_BUS);
200 ✗ break;
201 }
202 }
203 }
204
205 ✗ void I2cDriver ::serviceWriteReadMasterOnBus() {
206 ✗ FW_ASSERT(this->m_state == State::WRITE_READ_WRITING_WAIT, static_cast<FwAssertArgType>(this->m_state));
207
208 // We have finished transfering the Tx bytes via DMA and the i2c peripheral
209 // entered a MASTER_ON_BUS mode where it is ready to read from the slave.
210 // The master-on-bus interrupt has served its purpose; disable it.
211 ✗ I2cHardware::I2cHal::disableMasterOnBusInterrupt(this->m_sercom);
212
213 // The write phase of the write-read uses LENEN=0, so a client NACK of
214 // the address or register-pointer byte does not raise LENERR/ERROR. Check
215 // RXNACK here: if the client did not ACK, the write half failed, so abort
216 // the pre-armed read DMA and report the write-read as a write error rather
217 // than issuing a repeated START into a device that never accepted the pointer.
218 ✗ if (I2cHardware::I2cHal::readInterruptStatus(this->m_sercom).rxNack) {
219 ✗ this->dmaTransactionAbortOut_out(I2cDriver_DmaChannel::READ);
220 // Record the write error; activeIn delivers the reply from the main context.
221 ✗ this->m_pendingStatus = Drv::I2cStatus::I2C_WRITE_ERR;
222 ✗ this->m_state = State::COMPLETE_WRITE_READ;
223 ✗ return;
224 }
225
226 // The write of the write/read chain finished
227 // Proceed to read.
228 ✗ this->m_state = State::WRITE_READ_READING;
229
230 ✗ auto buf = this->m_read.getBuffer();
231
232 // Read DMA has already been queued, no need to do it again
233 ✗ this->readImpl(this->m_pending_read_address, buf, /* queueDma */ false);
234 ✗ }
235
236 ✗ void I2cDriver ::recoverFromStall(State stalledState) {
237 ✗ FW_ASSERT(stalledState != State::IDLE, static_cast<FwAssertArgType>(stalledState));
238
239 // The caller has already atomically set m_state = IDLE under a critical
240 // section, so any completion ISR that races in during recovery is dropped by
241 // the IDLE guards rather than acting on half-reset state.
242
243 // Capture the frozen registers BEFORE recovery clears them, so the event
244 // records the actual wedge signature (e.g. MB latched with BUSSTATE=IDLE).
245 ✗ const I2cHardware::I2cRawRegisters regs = I2cHardware::I2cHal::readRawRegisters(this->m_sercom);
246 ✗ this->log_WARNING_HI_StalledTransactionRecovered(this->m_sercom, static_cast<U8>(stalledState), regs.intflag,
247 ✗ regs.status);
248
249 // Abort whichever DMA channels this transaction could have had in flight.
250 ✗ switch (stalledState) {
251 ✗ case State::READ:
252 ✗ this->dmaTransactionAbortOut_out(I2cDriver_DmaChannel::READ);
253 ✗ break;
254 ✗ case State::WRITE:
255 ✗ this->dmaTransactionAbortOut_out(I2cDriver_DmaChannel::WRITE);
256 ✗ break;
257 ✗ case State::WRITE_READ_WRITING:
258 case State::WRITE_READ_WRITING_WAIT:
259 case State::WRITE_READ_READING:
260 ✗ this->dmaTransactionAbortOut_out(I2cDriver_DmaChannel::WRITE);
261 ✗ this->dmaTransactionAbortOut_out(I2cDriver_DmaChannel::READ);
262 ✗ break;
263 ✗ default:
264 ✗ break;
265 }
266
267 // Clear all pending flags, disable the MB interrupt, and force the bus to IDLE.
268 ✗ I2cHardware::I2cHal::recoverBusToIdle(this->m_sercom);
269
270 // Reply to the stuck caller with an error so the client's own state machine
271 // unwinds instead of waiting forever. Map to the port that matches the
272 // transaction kind, mirroring the ISR error paths.
273 ✗ Fw::Buffer read = this->m_read.getBuffer();
274 ✗ Fw::Buffer write = this->m_write.getBuffer();
275 ✗ switch (stalledState) {
276 ✗ case State::READ:
277 ✗ if (this->isConnected_readComplete_OutputPort(this->m_portNum)) {
278 ✗ this->readComplete_out(this->m_portNum, read, Drv::I2cStatus::I2C_OTHER_ERR);
279 }
280 ✗ break;
281 ✗ case State::WRITE:
282 ✗ if (this->isConnected_writeComplete_OutputPort(this->m_portNum)) {
283 ✗ this->writeComplete_out(this->m_portNum, write, Drv::I2cStatus::I2C_OTHER_ERR);
284 }
285 ✗ break;
286 ✗ case State::WRITE_READ_WRITING:
287 case State::WRITE_READ_WRITING_WAIT:
288 case State::WRITE_READ_READING:
289 ✗ if (this->isConnected_writeReadComplete_OutputPort(this->m_portNum)) {
290 ✗ this->writeReadComplete_out(this->m_portNum, write, read, Drv::I2cStatus::I2C_OTHER_ERR);
291 }
292 ✗ break;
293 ✗ default:
294 ✗ break;
295 }
296
297 ✗ this->m_stallRecoveryCount++;
298 ✗ }
299
300 // ----------------------------------------------------------------------
301 // Command handler implementations
302 // ----------------------------------------------------------------------
303
304 ✗ void I2cDriver::CLEAR_ERRORS_cmdHandler(FwOpcodeType opCode, //!< The opcode
305 U32 cmdSeq //!< The command sequence number
306 ) {
307 ✗ this->m_tlmErrors = 0;
308 ✗ this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK);
309 ✗ }
310
311 // ----------------------------------------------------------------------
312 // Handler implementations for typed input ports
313 // ----------------------------------------------------------------------
314
315 ✗ void I2cDriver ::dmaReplyIn_handler(FwIndexType portNum, const Samd21::Dma::Reply& reply) {
316 ✗ FW_ASSERT(reply.get_status() == Samd21::Dma::Status::OK, static_cast<FwAssertArgType>(this->m_sercom),
317 static_cast<FwAssertArgType>(this->m_state), portNum);
318
319 // Validate which DMA channel replied to us
320 ✗ switch (this->m_state) {
321 ✗ case State::IDLE:
322 case State::WRITE_READ_WRITING_WAIT:
323 case State::COMPLETE_READ:
324 case State::COMPLETE_WRITE:
325 case State::COMPLETE_WRITE_READ:
326 // No DMA is expected in these states: either idle, waiting on the MB
327 // interrupt (not a DMA event), or a completion is already recorded and
328 // pending delivery. Drop the reply.
329 ✗ return;
330
331 // Expecting a read reply
332 ✗ case State::READ:
333 case State::WRITE_READ_READING:
334 ✗ switch (portNum) {
335 ✗ case I2cDriver_DmaChannel::WRITE:
336 ✗ this->log_WARNING_HI_InvalidDmaReply(this->m_sercom, portNum, Samd21::I2cDriver_DmaChannel::READ);
337 ✗ return;
338 ✗ case I2cDriver_DmaChannel::READ:
339 ✗ break;
340 ✗ default:
341 ✗ FW_ASSERT(false, portNum);
342 }
343
344 ✗ break;
345 ✗ case State::WRITE:
346 case State::WRITE_READ_WRITING:
347 ✗ switch (portNum) {
348 ✗ case I2cDriver_DmaChannel::WRITE:
349 ✗ break;
350 ✗ case I2cDriver_DmaChannel::READ:
351 ✗ this->log_WARNING_HI_InvalidDmaReply(this->m_sercom, portNum, Samd21::I2cDriver_DmaChannel::WRITE);
352 ✗ return;
353 ✗ default:
354 ✗ FW_ASSERT(false, portNum);
355 }
356
357 ✗ break;
358 ✗ default:
359 ✗ FW_ASSERT(false, static_cast<FwAssertArgType>(this->m_state));
360 }
361
362 ✗ switch (this->m_state) {
363 ✗ case State::IDLE:
364 case State::WRITE_READ_WRITING_WAIT:
365 case State::COMPLETE_READ:
366 case State::COMPLETE_WRITE:
367 case State::COMPLETE_WRITE_READ:
368 // We already handled these cases
369 ✗ this->log_WARNING_HI_InvalidDmaReply(this->m_sercom, portNum, Samd21::I2cDriver_DmaChannel::N);
370 ✗ break;
371 ✗ case State::READ: {
372 ✗ FW_ASSERT(reply.get_status() == Samd21::Dma::Status::OK);
373 // Record the successful read; activeIn delivers the reply from the main context.
374 ✗ this->m_pendingStatus = Drv::I2cStatus::I2C_OK;
375 ✗ this->m_state = State::COMPLETE_READ;
376 ✗ break;
377 }
378 ✗ case State::WRITE: {
379 ✗ FW_ASSERT(reply.get_status() == Samd21::Dma::Status::OK);
380 // Record the successful write; activeIn delivers the reply from the main context.
381 ✗ this->m_pendingStatus = Drv::I2cStatus::I2C_OK;
382 ✗ this->m_state = State::COMPLETE_WRITE;
383 ✗ break;
384 }
385 ✗ case State::WRITE_READ_WRITING: {
386 ✗ FW_ASSERT(reply.get_status() == Samd21::Dma::Status::OK);
387
388 // The DMA has finished transfering the Tx packets
389 // Because we do not enable LENEN in writeRead (write portion),
390 // the I2C peripheral is currently holding the clock until the software
391 // generates another condition.
392 // In reality we may not have finished transfering the final byte so we
393 // need to wait until the MB (master on bus) is set to proceed with the
394 // read.
395 //
396 // MB is a LEVEL condition. it is set the moment the write
397 // address/data byte is ACKed, which may already be true by the time this
398 // DMA-completion ISR runs.
399 //
400 // The critical section keeps m_state and the MB-interrupt enable
401 // consistent as seen by the SERCOM ISR.
402 ✗ Samd21::CriticalSection cs;
403
404 ✗ this->m_state = State::WRITE_READ_WRITING_WAIT;
405
406 // Enable the master on bus interrupt which should trigger once the I2C
407 // master is ready to begin the read operation during the clock hold.
408 ✗ I2cHardware::I2cHal::enableMasterOnBusInterrupt(this->m_sercom);
409
410 // If MB is already latched, no further edge is coming: acknowledge it and
411 // service the handoff now. Otherwise the MB interrupt will drive it later.
412 ✗ if (I2cHardware::I2cHal::readInterruptStatus(this->m_sercom).masterOnBus) {
413 ✗ I2cHardware::I2cHal::acknowledgeMasterOnBus(this->m_sercom);
414 ✗ this->serviceWriteReadMasterOnBus();
415 }
416 ✗ break;
417 ✗ }
418 ✗ case State::WRITE_READ_READING: {
419 ✗ FW_ASSERT(reply.get_status() == Samd21::Dma::Status::OK);
420 // Record the successful write-read; activeIn delivers the reply from the main context.
421 ✗ this->m_pendingStatus = Drv::I2cStatus::I2C_OK;
422 ✗ this->m_state = State::COMPLETE_WRITE_READ;
423 ✗ break;
424 }
425 ✗ default:
426 ✗ FW_ASSERT(false, this->m_sercom, static_cast<FwAssertArgType>(this->m_state));
427 }
428 }
429
430 ✗ bool I2cDriver ::activeIn_handler(FwIndexType portNum, U32 context) {
431 // Snapshot the pending completion under a critical section so a completion ISR
432 // cannot mutate m_state / m_pendingStatus between the read and the transition
433 // back to IDLE. The actual client callback (potentially long) runs OUTSIDE the
434 // critical section.
435 State completed;
436 ✗ Drv::I2cStatus status;
437 {
438 ✗ Samd21::CriticalSection cs;
439 ✗ completed = this->m_state;
440 ✗ switch (completed) {
441 ✗ case State::COMPLETE_READ:
442 case State::COMPLETE_WRITE:
443 case State::COMPLETE_WRITE_READ:
444 ✗ status = this->m_pendingStatus;
445 ✗ this->m_state = State::IDLE;
446 ✗ break;
447 ✗ default:
448 // No completion pending (idle or a transaction still in flight):
449 // nothing to deliver this tick.
450 ✗ return false;
451 }
452 ✗ }
453
454 // Deliver the reply on the port matching the completed transaction kind. The
455 // buffers are still owned by the driver until the callback returns them.
456 ✗ switch (completed) {
457 ✗ case State::COMPLETE_READ: {
458 ✗ Fw::Buffer buf = this->m_read.getBuffer();
459 ✗ if (this->isConnected_readComplete_OutputPort(this->m_portNum)) {
460 ✗ this->readComplete_out(this->m_portNum, buf, status);
461 }
462 ✗ break;
463 ✗ }
464 ✗ case State::COMPLETE_WRITE: {
465 ✗ Fw::Buffer buf = this->m_write.getBuffer();
466 ✗ if (this->isConnected_writeComplete_OutputPort(this->m_portNum)) {
467 ✗ this->writeComplete_out(this->m_portNum, buf, status);
468 }
469 ✗ break;
470 ✗ }
471 ✗ case State::COMPLETE_WRITE_READ: {
472 ✗ Fw::Buffer w_buf = this->m_write.getBuffer();
473 ✗ Fw::Buffer r_buf = this->m_read.getBuffer();
474 ✗ if (this->isConnected_writeReadComplete_OutputPort(this->m_portNum)) {
475 ✗ this->writeReadComplete_out(this->m_portNum, w_buf, r_buf, status);
476 }
477 ✗ break;
478 ✗ }
479 ✗ default:
480 ✗ FW_ASSERT(false, static_cast<FwAssertArgType>(completed));
481 }
482
483 ✗ return true;
484 ✗ }
485
486 ✗ void I2cDriver ::read_handler(FwIndexType portNum, U32 addr, Fw::Buffer& buffer) {
487 ✗ if (this->m_state != State::IDLE) {
488 ✗ if (this->isConnected_readComplete_OutputPort(portNum)) {
489 ✗ this->readComplete_out(portNum, buffer, Drv::I2cStatus::I2C_OTHER_ERR);
490 }
491 ✗ return;
492 }
493
494 ✗ this->m_state = State::READ;
495 ✗ this->m_read = ThinBuffer(buffer);
496 ✗ this->m_portNum = portNum;
497
498 ✗ this->readImpl(addr, buffer, /* queueDma */ true);
499 }
500
501 ✗ void I2cDriver ::reportTelemetryIn_handler(FwIndexType portNum, U32 context) {
502 ✗ auto now = this->getTime();
503
504 // Stall watchdog. A healthy transaction completes in milliseconds, well within
505 // a single telemetry tick. If the driver is still non-IDLE across
506 // I2C_STALL_RECOVERY_TICKS consecutive ticks, its completion interrupt was lost
507 // (e.g. a hardware time-out auto-STOPped the bus but the completion never
508 // propagated) and it will otherwise stay wedged forever. Force-recover it.
509 //
510 // The transaction is *claimed* under a critical section so a completion ISR
511 // cannot drive m_state to IDLE between the test and the claim: either the ISR
512 // wins and we observe IDLE (counter resets), or we win, atomically capture the
513 // stuck state and set m_state = IDLE. The actual recovery (event, DMA aborts,
514 // HAL reset, port replies -- all potentially long) then runs OUTSIDE the
515 // critical section so interrupts are not disabled across downstream calls. Any
516 // ISR that races in after the claim sees IDLE and is dropped by the IDLE guards.
517 ✗ State stalledState = State::IDLE;
518 {
519 ✗ Samd21::CriticalSection cs;
520 // A COMPLETE_* state is not a stall: the transaction already finished in
521 // the ISR, the bus is idle, and the reply is only waiting for the next
522 // activeIn tick. Only the in-flight states (READ/WRITE/WRITE_READ_*) can
523 // wedge, so only they accrue stall ticks.
524 ✗ const bool inFlight = (this->m_state != State::IDLE) && (this->m_state != State::COMPLETE_READ) &&
525 ✗ (this->m_state != State::COMPLETE_WRITE) && (this->m_state != State::COMPLETE_WRITE_READ);
526 ✗ if (!inFlight) {
527 ✗ this->m_stallTicks = 0;
528 } else {
529 ✗ this->m_stallTicks++;
530 ✗ if (this->m_stallTicks >= I2cDriverConfig::I2C_STALL_RECOVERY_TICKS) {
531 ✗ stalledState = this->m_state;
532 ✗ this->m_state = State::IDLE;
533 ✗ this->m_stallTicks = 0;
534 }
535 }
536 ✗ }
537 ✗ if (stalledState != State::IDLE) {
538 ✗ this->recoverFromStall(stalledState);
539 }
540
541 // The bus-error counter is always emitted -- it is the driver's health signal.
542 ✗ this->tlmWrite_BusErrorCount(this->m_tlmErrors, now);
543 ✗ this->tlmWrite_StallRecoveryCount(this->m_stallRecoveryCount, now);
544
545 // The remaining diagnostic channels are compile-time gated. When disabled we
546 // also skip the hardware bus-status read they depend on.
547 if (!I2cDriverConfig::I2C_ENABLE_DEBUG_TELEMETRY) {
548 return;
549 }
550
551 ✗ const I2cHardware::I2cBusStatus status = I2cHardware::I2cHal::readBusStatus(this->m_sercom);
552
553 ✗ I2cDriver_I2cBusState busState;
554 ✗ I2cDriver_DeviceOnBusFlag deviceOnBus;
555
556 ✗ switch (status.busState) { // 2-bits
557 ✗ case 0x0:
558 ✗ busState = I2cDriver_I2cBusState::UNKNOWN;
559 ✗ break;
560 ✗ case 0x1:
561 ✗ busState = I2cDriver_I2cBusState::IDLE;
562 ✗ break;
563 ✗ case 0x2:
564 ✗ busState = I2cDriver_I2cBusState::OWNER;
565 ✗ break;
566 ✗ case 0x3:
567 ✗ busState = I2cDriver_I2cBusState::BUSY;
568 ✗ break;
569 ✗ default:
570 ✗ FW_ASSERT(false, status.busState);
571 }
572
573 ✗ if (status.slaveOnBus && status.masterOnBus) {
574 ✗ deviceOnBus = I2cDriver_DeviceOnBusFlag::MASTER_AND_SLAVE_ON_BUS;
575 ✗ } else if (status.slaveOnBus) {
576 ✗ deviceOnBus = I2cDriver_DeviceOnBusFlag::SLAVE_ON_BUS;
577 ✗ } else if (status.masterOnBus) {
578 ✗ deviceOnBus = I2cDriver_DeviceOnBusFlag::MASTER_ON_BUS;
579 } else {
580 ✗ deviceOnBus = I2cDriver_DeviceOnBusFlag::NONE;
581 }
582
583 ✗ this->tlmWrite_BusState(busState, now);
584 ✗ this->tlmWrite_ClockHold(status.clockHold, now);
585 ✗ this->tlmWrite_ReceiveNotAcknowledged(status.rxNack, now);
586 ✗ this->tlmWrite_DeviceOnBus(deviceOnBus, now);
587 ✗ }
588
589 // I2C DMA is limited to 255 bytes
590 constexpr U32 SAMD21_I2C_MAX_DMA_PAYLOAD_SIZE = 255;
591
592 ✗ void I2cDriver::queueReadDma() {
593 // Queue up a DMA operation to read the data from the device
594 ✗ this->dmaTransactionOut_out(I2cDriver_DmaChannel::READ, SercomUtil::rxDmaTrigger(this->m_sercom),
595 Samd21::Dma::TransactionType::BEAT, Samd21::Dma::Priority::PRIORITY_1,
596 ✗ I2cHardware::I2cHal::getDataRegisterAddress(this->m_sercom),
597 ✗ static_cast<U32>(reinterpret_cast<uintptr_t>(this->m_read.getData())),
598 ✗ this->m_read.getSize(), Samd21::Dma::BeatSize::BYTE,
599 /* incrementSource */ false, /* incrementDestination */ true,
600 Samd21::Dma::AddressIncrementStepSize::SIZE_1, Samd21::Dma::StepSelection::DESTINATION);
601 ✗ }
602
603 ✗ void I2cDriver::queueWriteDma() {
604 ✗ this->dmaTransactionOut_out(I2cDriver_DmaChannel::WRITE, SercomUtil::txDmaTrigger(this->m_sercom),
605 Samd21::Dma::TransactionType::BEAT, Samd21::Dma::Priority::PRIORITY_1,
606 ✗ static_cast<U32>(reinterpret_cast<uintptr_t>(this->m_write.getData())),
607 ✗ I2cHardware::I2cHal::getDataRegisterAddress(this->m_sercom), this->m_write.getSize(),
608 Samd21::Dma::BeatSize::BYTE,
609 /* incrementSource */ true, /* incrementDestination */ false,
610 Samd21::Dma::AddressIncrementStepSize::SIZE_1, Samd21::Dma::StepSelection::SOURCE);
611 ✗ }
612
613 ✗ void I2cDriver::readImpl(U32 addr, Fw::Buffer& buffer, bool queueDma) {
614 ✗ FW_ASSERT(this->m_configured);
615
616 ✗ FW_ASSERT(buffer.getData() != nullptr);
617 ✗ FW_ASSERT(buffer.getSize() <= SAMD21_I2C_MAX_DMA_PAYLOAD_SIZE, buffer.getSize());
618
619 // We only support 7-bit address mode
620 ✗ FW_ASSERT((addr & ~0x7F) == 0, addr);
621
622 ✗ if (queueDma) {
623 ✗ this->queueReadDma();
624 }
625
626 // Kick off the I2C read: sets the read ACK action and writes ADDR with
627 // length mode enabled so the DMA drives the transfer.
628 ✗ I2cHardware::I2cHal::beginRead(this->m_sercom, addr, static_cast<U8>(buffer.getSize()));
629 ✗ }
630
631 ✗ void I2cDriver::writeImpl(U32 addr, Fw::Buffer& buffer, bool generateStopCondition, bool queueReadDma) {
632 ✗ FW_ASSERT(this->m_configured);
633
634 // I2C DMA is limited to 255 bytes
635 ✗ FW_ASSERT(buffer.getData() != nullptr);
636 ✗ FW_ASSERT(buffer.getSize() <= SAMD21_I2C_MAX_DMA_PAYLOAD_SIZE, buffer.getSize());
637 ✗ FW_ASSERT(buffer.getSize() > 0);
638
639 // We only support 7-bit address mode
640 ✗ FW_ASSERT((addr & ~0x7F) == 0, addr);
641
642 // Queue up a DMA operation to write the data to the device
643 ✗ this->queueWriteDma();
644
645 // Queue up a read DMA if we are doing writeRead
646 ✗ if (queueReadDma) {
647 ✗ FW_ASSERT(this->m_read.getData() != nullptr);
648 ✗ FW_ASSERT(this->m_read.getSize() <= SAMD21_I2C_MAX_DMA_PAYLOAD_SIZE, this->m_read.getSize());
649 ✗ FW_ASSERT(this->m_read.getSize() > 0);
650
651 ✗ this->queueReadDma();
652 }
653
654 // Kick off the I2C write. When generateStopCondition is set the peripheral
655 // emits an automatic STOP after the DMA writes all the bytes; otherwise the
656 // clock is held for a repeated START (write-read).
657 ✗ I2cHardware::I2cHal::beginWrite(this->m_sercom, addr, static_cast<U8>(buffer.getSize()), generateStopCondition);
658 ✗ }
659
660 ✗ void I2cDriver ::write_handler(FwIndexType portNum, U32 addr, Fw::Buffer& buffer) {
661 ✗ if (this->m_state != State::IDLE) {
662 ✗ if (this->isConnected_writeComplete_OutputPort(portNum)) {
663 ✗ this->writeComplete_out(portNum, buffer, Drv::I2cStatus::I2C_OTHER_ERR);
664 }
665 ✗ return;
666 }
667
668 ✗ this->m_state = State::WRITE;
669 ✗ this->m_write = ThinBuffer(buffer);
670 ✗ this->m_portNum = portNum;
671
672 ✗ this->writeImpl(addr, buffer, /* generateStopCondition */ true, /* queueReadDma */ false);
673 }
674
675 ✗ void I2cDriver ::writeRead_handler(FwIndexType portNum, U32 addr, Fw::Buffer& writeBuffer, Fw::Buffer& readBuffer) {
676 ✗ if (this->m_state != State::IDLE) {
677 ✗ if (this->isConnected_writeReadComplete_OutputPort(portNum)) {
678 ✗ this->writeReadComplete_out(portNum, writeBuffer, readBuffer, Drv::I2cStatus::I2C_OTHER_ERR);
679 }
680 ✗ return;
681 }
682
683 ✗ this->m_state = State::WRITE_READ_WRITING;
684 ✗ this->m_read = ThinBuffer(readBuffer);
685 ✗ this->m_write = ThinBuffer(writeBuffer);
686 ✗ this->m_portNum = portNum;
687
688 // Stash the address for the read phase: once the write DMA completes,
689 // dmaReplyIn_handler issues readImpl(m_pending_read_address, ...). Without this
690 // the read half of every write-read would target address 0x00.
691 ✗ this->m_pending_read_address = addr;
692
693 ✗ this->writeImpl(addr, writeBuffer, /* generateStopCondition */ false, /* queueReadDma */ true);
694 }
695
696 } // namespace Samd21
697