GCC Code Coverage Report


Directory: lib/fprime-samd/fprime-samd/Drv/I2cDriver/
File: I2cDriver.cpp
Date: 2026-09-26 00:05:14
Exec Total Coverage
Lines: 327 348 94.0%
Functions: 18 18 100.0%
Branches: 265 306 86.6%

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 39 I2cDriver ::I2cDriver(const char* const compName)
24 : I2cDriverComponentBase(compName),
25
1/1
✓ Branch 3 taken 39 times.
39 m_sercom(SercomKind::SERCOM_0),
26 39 m_configured(false),
27 39 m_state(I2cDriver::State::IDLE),
28
1/1
✓ Branch 4 taken 39 times.
39 m_pendingStatus(Drv::I2cStatus::I2C_OK),
29 39 m_portNum(),
30
1/1
✓ Branch 5 taken 39 times.
39 m_read(),
31 39 m_pending_read_address(0),
32
1/1
✓ Branch 5 taken 39 times.
39 m_write(),
33 39 m_tlmErrors(0),
34 39 m_stallTicks(0),
35 78 m_stallRecoveryCount(0) {}
36
37 78 I2cDriver ::~I2cDriver() {}
38
39 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 39 FW_ASSERT(!this->m_configured, sercom.e);
50 39 FW_ASSERT(this->m_state == State::IDLE, static_cast<FwAssertArgType>(this->m_state));
51
52 // Store configuration
53 39 this->m_sercom = sercom;
54
55 // Register with the ISR callback table
56
2/2
✓ Branch 3 taken 39 times.
✓ Branch 6 taken 39 times.
39 I2cHardware::I2cHal::registerIsr(sercom, I2cDriver::s_i2cDriverIsrHandler, this);
57
58 // Configure and enable the SERCOM I2C host peripheral.
59
2/2
✓ Branch 2 taken 39 times.
✓ Branch 5 taken 39 times.
39 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 39 this->m_configured = true;
63 39 }
64
65 14 void I2cDriver::s_i2cDriverIsrHandler(Fw::PassiveComponentBase* i2cDriverRaw, SercomKind sercom) {
66 14 auto i2cDriver = reinterpret_cast<I2cDriver*>(i2cDriverRaw);
67
68 14 FW_ASSERT(i2cDriver != nullptr);
69 14 FW_ASSERT(sercom == i2cDriver->m_sercom, sercom, i2cDriver->m_sercom);
70 14 i2cDriver->isrHandler();
71 14 }
72
73 14 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
2/2
✓ Branch 5 taken 14 times.
✓ Branch 8 taken 14 times.
14 const I2cHardware::I2cInterruptStatus status = I2cHardware::I2cHal::readInterruptStatus(this->m_sercom);
80
81
3/4
✗ Branch 0 not taken.
✓ Branch 1 taken 14 times.
✓ Branch 2 taken 8 times.
✓ Branch 3 taken 6 times.
14 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
3/4
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 3 times.
8 if (status.busError) {
87 5 this->m_tlmErrors++;
88
2/2
✓ Branch 6 taken 5 times.
✓ Branch 13 taken 5 times.
5 this->log_WARNING_LO_I2cBusError(this->m_sercom, Samd21::I2cDriver_I2cError::BUS_ERROR);
89 }
90
91
3/4
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 6 times.
8 if (status.arbLost) {
92 2 this->m_tlmErrors++;
93
2/2
✓ Branch 6 taken 2 times.
✓ Branch 13 taken 2 times.
2 this->log_WARNING_LO_I2cBusError(this->m_sercom, Samd21::I2cDriver_I2cError::ARBITRATION_LOST);
94 }
95
96
3/4
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 6 times.
8 if (status.lowTimeout) {
97 2 this->m_tlmErrors++;
98
2/2
✓ Branch 6 taken 2 times.
✓ Branch 13 taken 2 times.
2 this->log_WARNING_LO_I2cBusError(this->m_sercom, Samd21::I2cDriver_I2cError::SCL_LOW_TIMEOUT);
99 }
100
101
3/4
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 7 times.
8 if (status.masterExtTimeout) {
102 1 this->m_tlmErrors++;
103
2/2
✓ Branch 6 taken 1 times.
✓ Branch 13 taken 1 times.
1 this->log_WARNING_LO_I2cBusError(this->m_sercom, Samd21::I2cDriver_I2cError::MASTER_SCL_EXTEND_TIMEOUT);
104 }
105
106
3/4
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 6 times.
8 if (status.slaveExtTimeout) {
107 2 this->m_tlmErrors++;
108
2/2
✓ Branch 6 taken 2 times.
✓ Branch 13 taken 2 times.
2 this->log_WARNING_LO_I2cBusError(this->m_sercom, Samd21::I2cDriver_I2cError::SLAVE_SCL_EXTEND_TIMEOUT);
109 }
110
111
3/4
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 7 times.
8 if (status.lengthError) {
112 1 this->m_tlmErrors++;
113
2/2
✓ Branch 6 taken 1 times.
✓ Branch 13 taken 1 times.
1 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
2/2
✓ Branch 5 taken 8 times.
✓ Branch 8 taken 8 times.
8 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
5/6
✓ Branch 4 taken 3 times.
✓ Branch 5 taken 1 times.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 2 times.
✓ Branch 8 taken 1 times.
✗ Branch 9 not taken.
8 switch (this->m_state) {
124 3 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
2/2
✓ Branch 6 taken 3 times.
✓ Branch 13 taken 3 times.
3 this->log_WARNING_HI_UnexpectedInterrupt(this->m_sercom, I2cDriver_I2CInterrupt::BUS_ERROR);
133 3 break;
134 1 case State::READ:
135 1 this->dmaTransactionAbortOut_out(I2cDriver_DmaChannel::READ);
136 1 this->m_pendingStatus = Drv::I2cStatus::I2C_READ_ERR;
137 1 this->m_state = State::COMPLETE_READ;
138 1 break;
139 1 case State::WRITE:
140 1 this->dmaTransactionAbortOut_out(I2cDriver_DmaChannel::WRITE);
141 1 this->m_pendingStatus = Drv::I2cStatus::I2C_WRITE_ERR;
142 1 this->m_state = State::COMPLETE_WRITE;
143 1 break;
144 2 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
2/2
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 1 times.
2 if (this->m_state == State::WRITE_READ_WRITING_WAIT) {
152
2/2
✓ Branch 5 taken 1 times.
✓ Branch 8 taken 1 times.
1 I2cHardware::I2cHal::disableMasterOnBusInterrupt(this->m_sercom);
153 }
154
155 2 this->dmaTransactionAbortOut_out(I2cDriver_DmaChannel::WRITE);
156 2 this->dmaTransactionAbortOut_out(I2cDriver_DmaChannel::READ);
157 2 this->m_pendingStatus = Drv::I2cStatus::I2C_WRITE_ERR;
158 2 this->m_state = State::COMPLETE_WRITE_READ;
159 2 break;
160 1 case State::WRITE_READ_READING:
161 // Write already finished, no need to abort it
162 1 this->dmaTransactionAbortOut_out(I2cDriver_DmaChannel::READ);
163 1 this->m_pendingStatus = Drv::I2cStatus::I2C_READ_ERR;
164 1 this->m_state = State::COMPLETE_WRITE_READ;
165 1 break;
166 ✗ default:
167 ✗ FW_ASSERT(false, this->m_sercom, static_cast<FwAssertArgType>(this->m_state));
168 }
169
3/4
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 5 times.
6 } 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 1 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
2/2
✓ Branch 5 taken 5 times.
✓ Branch 8 taken 5 times.
5 I2cHardware::I2cHal::acknowledgeMasterOnBus(this->m_sercom);
186
187
2/2
✓ Branch 4 taken 4 times.
✓ Branch 5 taken 1 times.
5 switch (this->m_state) {
188 4 case State::WRITE_READ_WRITING_WAIT:
189 4 this->serviceWriteReadMasterOnBus();
190 4 break;
191 1 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
2/2
✓ Branch 5 taken 1 times.
✓ Branch 8 taken 1 times.
1 I2cHardware::I2cHal::disableMasterOnBusInterrupt(this->m_sercom);
199
2/2
✓ Branch 6 taken 1 times.
✓ Branch 13 taken 1 times.
1 this->log_WARNING_HI_UnexpectedInterrupt(this->m_sercom, I2cDriver_I2CInterrupt::MASTER_ON_BUS);
200 1 break;
201 }
202 }
203 }
204
205 6 void I2cDriver ::serviceWriteReadMasterOnBus() {
206 6 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
2/2
✓ Branch 5 taken 6 times.
✓ Branch 8 taken 6 times.
6 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
5/6
✓ Branch 5 taken 6 times.
✓ Branch 8 taken 6 times.
✗ Branch 10 not taken.
✓ Branch 11 taken 6 times.
✓ Branch 15 taken 1 times.
✓ Branch 16 taken 5 times.
6 if (I2cHardware::I2cHal::readInterruptStatus(this->m_sercom).rxNack) {
219
1/1
✓ Branch 5 taken 1 times.
1 this->dmaTransactionAbortOut_out(I2cDriver_DmaChannel::READ);
220 // Record the write error; activeIn delivers the reply from the main context.
221
1/1
✓ Branch 6 taken 1 times.
1 this->m_pendingStatus = Drv::I2cStatus::I2C_WRITE_ERR;
222 1 this->m_state = State::COMPLETE_WRITE_READ;
223 1 return;
224 }
225
226 // The write of the write/read chain finished
227 // Proceed to read.
228 5 this->m_state = State::WRITE_READ_READING;
229
230
1/1
✓ Branch 4 taken 5 times.
5 auto buf = this->m_read.getBuffer();
231
232 // Read DMA has already been queued, no need to do it again
233
1/1
✓ Branch 8 taken 5 times.
5 this->readImpl(this->m_pending_read_address, buf, /* queueDma */ false);
234 5 }
235
236 4 void I2cDriver ::recoverFromStall(State stalledState) {
237 4 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
2/2
✓ Branch 5 taken 4 times.
✓ Branch 8 taken 4 times.
4 const I2cHardware::I2cRawRegisters regs = I2cHardware::I2cHal::readRawRegisters(this->m_sercom);
246
1/1
✓ Branch 7 taken 4 times.
8 this->log_WARNING_HI_StalledTransactionRecovered(this->m_sercom, static_cast<U8>(stalledState), regs.intflag,
247 4 regs.status);
248
249 // Abort whichever DMA channels this transaction could have had in flight.
250
3/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
4 switch (stalledState) {
251 2 case State::READ:
252
1/1
✓ Branch 5 taken 2 times.
2 this->dmaTransactionAbortOut_out(I2cDriver_DmaChannel::READ);
253 2 break;
254 1 case State::WRITE:
255
1/1
✓ Branch 5 taken 1 times.
1 this->dmaTransactionAbortOut_out(I2cDriver_DmaChannel::WRITE);
256 1 break;
257 1 case State::WRITE_READ_WRITING:
258 case State::WRITE_READ_WRITING_WAIT:
259 case State::WRITE_READ_READING:
260
1/1
✓ Branch 5 taken 1 times.
1 this->dmaTransactionAbortOut_out(I2cDriver_DmaChannel::WRITE);
261
1/1
✓ Branch 5 taken 1 times.
1 this->dmaTransactionAbortOut_out(I2cDriver_DmaChannel::READ);
262 1 break;
263 ✗ default:
264 ✗ break;
265 }
266
267 // Clear all pending flags, disable the MB interrupt, and force the bus to IDLE.
268
2/2
✓ Branch 5 taken 4 times.
✓ Branch 8 taken 4 times.
4 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
1/1
✓ Branch 4 taken 4 times.
8 Fw::Buffer read = this->m_read.getBuffer();
274
1/1
✓ Branch 4 taken 4 times.
8 Fw::Buffer write = this->m_write.getBuffer();
275
3/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
4 switch (stalledState) {
276 2 case State::READ:
277
2/3
✓ Branch 9 taken 2 times.
✓ Branch 11 taken 2 times.
✗ Branch 12 not taken.
2 if (this->isConnected_readComplete_OutputPort(this->m_portNum)) {
278
2/2
✓ Branch 6 taken 2 times.
✓ Branch 14 taken 2 times.
2 this->readComplete_out(this->m_portNum, read, Drv::I2cStatus::I2C_OTHER_ERR);
279 }
280 2 break;
281 1 case State::WRITE:
282
2/3
✓ Branch 9 taken 1 times.
✓ Branch 11 taken 1 times.
✗ Branch 12 not taken.
1 if (this->isConnected_writeComplete_OutputPort(this->m_portNum)) {
283
2/2
✓ Branch 6 taken 1 times.
✓ Branch 14 taken 1 times.
1 this->writeComplete_out(this->m_portNum, write, Drv::I2cStatus::I2C_OTHER_ERR);
284 }
285 1 break;
286 1 case State::WRITE_READ_WRITING:
287 case State::WRITE_READ_WRITING_WAIT:
288 case State::WRITE_READ_READING:
289
2/3
✓ Branch 9 taken 1 times.
✓ Branch 11 taken 1 times.
✗ Branch 12 not taken.
1 if (this->isConnected_writeReadComplete_OutputPort(this->m_portNum)) {
290
2/2
✓ Branch 6 taken 1 times.
✓ Branch 14 taken 1 times.
1 this->writeReadComplete_out(this->m_portNum, write, read, Drv::I2cStatus::I2C_OTHER_ERR);
291 }
292 1 break;
293 ✗ default:
294 ✗ break;
295 }
296
297 4 this->m_stallRecoveryCount++;
298 4 }
299
300 // ----------------------------------------------------------------------
301 // Command handler implementations
302 // ----------------------------------------------------------------------
303
304 1 void I2cDriver::CLEAR_ERRORS_cmdHandler(FwOpcodeType opCode, //!< The opcode
305 U32 cmdSeq //!< The command sequence number
306 ) {
307 1 this->m_tlmErrors = 0;
308
2/2
✓ Branch 6 taken 1 times.
✓ Branch 9 taken 1 times.
1 this->cmdResponse_out(opCode, cmdSeq, Fw::CmdResponse::OK);
309 1 }
310
311 // ----------------------------------------------------------------------
312 // Handler implementations for typed input ports
313 // ----------------------------------------------------------------------
314
315 20 void I2cDriver ::dmaReplyIn_handler(FwIndexType portNum, const Samd21::Dma::Reply& reply) {
316 20 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
3/4
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 8 times.
✓ Branch 6 taken 11 times.
✗ Branch 7 not taken.
20 switch (this->m_state) {
321 1 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 1 return;
330
331 // Expecting a read reply
332 8 case State::READ:
333 case State::WRITE_READ_READING:
334
2/3
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 7 times.
✗ Branch 2 not taken.
8 switch (portNum) {
335 1 case I2cDriver_DmaChannel::WRITE:
336
3/3
✓ Branch 6 taken 1 times.
✓ Branch 11 taken 1 times.
✓ Branch 18 taken 1 times.
1 this->log_WARNING_HI_InvalidDmaReply(this->m_sercom, portNum, Samd21::I2cDriver_DmaChannel::READ);
337 1 return;
338 7 case I2cDriver_DmaChannel::READ:
339 7 break;
340 ✗ default:
341 ✗ FW_ASSERT(false, portNum);
342 }
343
344 7 break;
345 11 case State::WRITE:
346 case State::WRITE_READ_WRITING:
347
2/3
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
11 switch (portNum) {
348 10 case I2cDriver_DmaChannel::WRITE:
349 10 break;
350 1 case I2cDriver_DmaChannel::READ:
351
3/3
✓ Branch 6 taken 1 times.
✓ Branch 11 taken 1 times.
✓ Branch 18 taken 1 times.
1 this->log_WARNING_HI_InvalidDmaReply(this->m_sercom, portNum, Samd21::I2cDriver_DmaChannel::WRITE);
352 1 return;
353 ✗ default:
354 ✗ FW_ASSERT(false, portNum);
355 }
356
357 10 break;
358 ✗ default:
359 ✗ FW_ASSERT(false, static_cast<FwAssertArgType>(this->m_state));
360 }
361
362
4/6
✗ Branch 4 not taken.
✓ Branch 5 taken 3 times.
✓ Branch 6 taken 2 times.
✓ Branch 7 taken 8 times.
✓ Branch 8 taken 4 times.
✗ Branch 9 not taken.
17 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 3 case State::READ: {
372 3 FW_ASSERT(reply.get_status() == Samd21::Dma::Status::OK);
373 // Record the successful read; activeIn delivers the reply from the main context.
374 3 this->m_pendingStatus = Drv::I2cStatus::I2C_OK;
375 3 this->m_state = State::COMPLETE_READ;
376 3 break;
377 }
378 2 case State::WRITE: {
379 2 FW_ASSERT(reply.get_status() == Samd21::Dma::Status::OK);
380 // Record the successful write; activeIn delivers the reply from the main context.
381 2 this->m_pendingStatus = Drv::I2cStatus::I2C_OK;
382 2 this->m_state = State::COMPLETE_WRITE;
383 2 break;
384 }
385 8 case State::WRITE_READ_WRITING: {
386 8 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
1/1
✓ Branch 2 taken 8 times.
8 Samd21::CriticalSection cs;
403
404 8 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
2/2
✓ Branch 5 taken 8 times.
✓ Branch 8 taken 8 times.
8 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
5/6
✓ Branch 5 taken 8 times.
✓ Branch 8 taken 8 times.
✗ Branch 10 not taken.
✓ Branch 11 taken 8 times.
✓ Branch 15 taken 2 times.
✓ Branch 16 taken 6 times.
8 if (I2cHardware::I2cHal::readInterruptStatus(this->m_sercom).masterOnBus) {
413
2/2
✓ Branch 5 taken 2 times.
✓ Branch 8 taken 2 times.
2 I2cHardware::I2cHal::acknowledgeMasterOnBus(this->m_sercom);
414
1/1
✓ Branch 4 taken 2 times.
2 this->serviceWriteReadMasterOnBus();
415 }
416 8 break;
417 8 }
418 4 case State::WRITE_READ_READING: {
419 4 FW_ASSERT(reply.get_status() == Samd21::Dma::Status::OK);
420 // Record the successful write-read; activeIn delivers the reply from the main context.
421 4 this->m_pendingStatus = Drv::I2cStatus::I2C_OK;
422 4 this->m_state = State::COMPLETE_WRITE_READ;
423 4 break;
424 }
425 ✗ default:
426 ✗ FW_ASSERT(false, this->m_sercom, static_cast<FwAssertArgType>(this->m_state));
427 }
428 }
429
430 16 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
1/1
✓ Branch 2 taken 16 times.
16 Drv::I2cStatus status;
437 {
438
1/1
✓ Branch 2 taken 16 times.
16 Samd21::CriticalSection cs;
439 16 completed = this->m_state;
440
2/2
✓ Branch 0 taken 15 times.
✓ Branch 1 taken 1 times.
16 switch (completed) {
441 15 case State::COMPLETE_READ:
442 case State::COMPLETE_WRITE:
443 case State::COMPLETE_WRITE_READ:
444
1/1
✓ Branch 5 taken 15 times.
15 status = this->m_pendingStatus;
445 15 this->m_state = State::IDLE;
446 15 break;
447 1 default:
448 // No completion pending (idle or a transaction still in flight):
449 // nothing to deliver this tick.
450 1 return false;
451 }
452 16 }
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
3/4
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
15 switch (completed) {
457 4 case State::COMPLETE_READ: {
458
1/1
✓ Branch 4 taken 4 times.
4 Fw::Buffer buf = this->m_read.getBuffer();
459
2/3
✓ Branch 9 taken 4 times.
✓ Branch 11 taken 4 times.
✗ Branch 12 not taken.
4 if (this->isConnected_readComplete_OutputPort(this->m_portNum)) {
460
1/1
✓ Branch 9 taken 4 times.
4 this->readComplete_out(this->m_portNum, buf, status);
461 }
462 4 break;
463 4 }
464 3 case State::COMPLETE_WRITE: {
465
1/1
✓ Branch 4 taken 3 times.
3 Fw::Buffer buf = this->m_write.getBuffer();
466
2/3
✓ Branch 9 taken 3 times.
✓ Branch 11 taken 3 times.
✗ Branch 12 not taken.
3 if (this->isConnected_writeComplete_OutputPort(this->m_portNum)) {
467
1/1
✓ Branch 9 taken 3 times.
3 this->writeComplete_out(this->m_portNum, buf, status);
468 }
469 3 break;
470 3 }
471 8 case State::COMPLETE_WRITE_READ: {
472
1/1
✓ Branch 4 taken 8 times.
8 Fw::Buffer w_buf = this->m_write.getBuffer();
473
1/1
✓ Branch 4 taken 8 times.
8 Fw::Buffer r_buf = this->m_read.getBuffer();
474
2/3
✓ Branch 9 taken 8 times.
✓ Branch 11 taken 8 times.
✗ Branch 12 not taken.
8 if (this->isConnected_writeReadComplete_OutputPort(this->m_portNum)) {
475
1/1
✓ Branch 9 taken 8 times.
8 this->writeReadComplete_out(this->m_portNum, w_buf, r_buf, status);
476 }
477 8 break;
478 8 }
479 ✗ default:
480 ✗ FW_ASSERT(false, static_cast<FwAssertArgType>(completed));
481 }
482
483 15 return true;
484 16 }
485
486 14 void I2cDriver ::read_handler(FwIndexType portNum, U32 addr, Fw::Buffer& buffer) {
487
2/2
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 13 times.
14 if (this->m_state != State::IDLE) {
488
1/2
✓ Branch 5 taken 1 times.
✗ Branch 6 not taken.
1 if (this->isConnected_readComplete_OutputPort(portNum)) {
489
2/2
✓ Branch 6 taken 1 times.
✓ Branch 10 taken 1 times.
1 this->readComplete_out(portNum, buffer, Drv::I2cStatus::I2C_OTHER_ERR);
490 }
491 1 return;
492 }
493
494 13 this->m_state = State::READ;
495
1/1
✓ Branch 3 taken 13 times.
13 this->m_read = ThinBuffer(buffer);
496 13 this->m_portNum = portNum;
497
498 13 this->readImpl(addr, buffer, /* queueDma */ true);
499 }
500
501 35 void I2cDriver ::reportTelemetryIn_handler(FwIndexType portNum, U32 context) {
502
1/1
✓ Branch 3 taken 35 times.
35 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 35 State stalledState = State::IDLE;
518 {
519
1/1
✓ Branch 2 taken 35 times.
35 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
1/2
✓ Branch 8 taken 16 times.
✗ Branch 9 not taken.
51 const bool inFlight = (this->m_state != State::IDLE) && (this->m_state != State::COMPLETE_READ) &&
525
4/6
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 19 times.
✓ Branch 6 taken 16 times.
✗ Branch 7 not taken.
✓ Branch 12 taken 16 times.
✗ Branch 13 not taken.
51 (this->m_state != State::COMPLETE_WRITE) && (this->m_state != State::COMPLETE_WRITE_READ);
526
2/2
✓ Branch 0 taken 19 times.
✓ Branch 1 taken 16 times.
35 if (!inFlight) {
527 19 this->m_stallTicks = 0;
528 } else {
529 16 this->m_stallTicks++;
530
2/2
✓ Branch 4 taken 4 times.
✓ Branch 5 taken 12 times.
16 if (this->m_stallTicks >= I2cDriverConfig::I2C_STALL_RECOVERY_TICKS) {
531 4 stalledState = this->m_state;
532 4 this->m_state = State::IDLE;
533 4 this->m_stallTicks = 0;
534 }
535 }
536 35 }
537
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 31 times.
35 if (stalledState != State::IDLE) {
538
1/1
✓ Branch 4 taken 4 times.
4 this->recoverFromStall(stalledState);
539 }
540
541 // The bus-error counter is always emitted -- it is the driver's health signal.
542
2/2
✓ Branch 6 taken 35 times.
✓ Branch 13 taken 35 times.
35 this->tlmWrite_BusErrorCount(this->m_tlmErrors, now);
543
2/2
✓ Branch 6 taken 35 times.
✓ Branch 13 taken 35 times.
35 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
2/2
✓ Branch 5 taken 35 times.
✓ Branch 8 taken 35 times.
35 const I2cHardware::I2cBusStatus status = I2cHardware::I2cHal::readBusStatus(this->m_sercom);
552
553
1/1
✓ Branch 2 taken 35 times.
35 I2cDriver_I2cBusState busState;
554
1/1
✓ Branch 2 taken 35 times.
35 I2cDriver_DeviceOnBusFlag deviceOnBus;
555
556
4/5
✓ Branch 0 taken 27 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 3 times.
✗ Branch 4 not taken.
35 switch (status.busState) { // 2-bits
557 27 case 0x0:
558
1/1
✓ Branch 2 taken 27 times.
27 busState = I2cDriver_I2cBusState::UNKNOWN;
559 27 break;
560 4 case 0x1:
561
1/1
✓ Branch 2 taken 4 times.
4 busState = I2cDriver_I2cBusState::IDLE;
562 4 break;
563 1 case 0x2:
564
1/1
✓ Branch 2 taken 1 times.
1 busState = I2cDriver_I2cBusState::OWNER;
565 1 break;
566 3 case 0x3:
567
1/1
✓ Branch 2 taken 3 times.
3 busState = I2cDriver_I2cBusState::BUSY;
568 3 break;
569 ✗ default:
570 ✗ FW_ASSERT(false, status.busState);
571 }
572
573
6/8
✗ Branch 0 not taken.
✓ Branch 1 taken 35 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 33 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 2 times.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 1 times.
35 if (status.slaveOnBus && status.masterOnBus) {
574
1/1
✓ Branch 2 taken 1 times.
1 deviceOnBus = I2cDriver_DeviceOnBusFlag::MASTER_AND_SLAVE_ON_BUS;
575
3/4
✗ Branch 0 not taken.
✓ Branch 1 taken 34 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 33 times.
34 } else if (status.slaveOnBus) {
576
1/1
✓ Branch 2 taken 1 times.
1 deviceOnBus = I2cDriver_DeviceOnBusFlag::SLAVE_ON_BUS;
577
3/4
✗ Branch 0 not taken.
✓ Branch 1 taken 33 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 32 times.
33 } else if (status.masterOnBus) {
578
1/1
✓ Branch 2 taken 1 times.
1 deviceOnBus = I2cDriver_DeviceOnBusFlag::MASTER_ON_BUS;
579 } else {
580
1/1
✓ Branch 2 taken 32 times.
32 deviceOnBus = I2cDriver_DeviceOnBusFlag::NONE;
581 }
582
583
2/2
✓ Branch 6 taken 35 times.
✓ Branch 9 taken 35 times.
35 this->tlmWrite_BusState(busState, now);
584
3/4
✓ Branch 6 taken 35 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 35 times.
✓ Branch 11 taken 35 times.
35 this->tlmWrite_ClockHold(status.clockHold, now);
585
3/4
✓ Branch 6 taken 35 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 35 times.
✓ Branch 11 taken 35 times.
35 this->tlmWrite_ReceiveNotAcknowledged(status.rxNack, now);
586
2/2
✓ Branch 6 taken 35 times.
✓ Branch 9 taken 35 times.
35 this->tlmWrite_DeviceOnBus(deviceOnBus, now);
587 35 }
588
589 // I2C DMA is limited to 255 bytes
590 constexpr U32 SAMD21_I2C_MAX_DMA_PAYLOAD_SIZE = 255;
591
592 27 void I2cDriver::queueReadDma() {
593 // Queue up a DMA operation to read the data from the device
594
10/10
✓ Branch 6 taken 27 times.
✓ Branch 11 taken 27 times.
✓ Branch 16 taken 27 times.
✓ Branch 20 taken 27 times.
✓ Branch 23 taken 27 times.
✓ Branch 27 taken 27 times.
✓ Branch 32 taken 27 times.
✓ Branch 41 taken 27 times.
✓ Branch 44 taken 27 times.
✓ Branch 48 taken 27 times.
54 this->dmaTransactionOut_out(I2cDriver_DmaChannel::READ, SercomUtil::rxDmaTrigger(this->m_sercom),
595 Samd21::Dma::TransactionType::BEAT, Samd21::Dma::Priority::PRIORITY_1,
596 27 I2cHardware::I2cHal::getDataRegisterAddress(this->m_sercom),
597
1/1
✓ Branch 3 taken 27 times.
27 static_cast<U32>(reinterpret_cast<uintptr_t>(this->m_read.getData())),
598
1/1
✓ Branch 4 taken 27 times.
27 this->m_read.getSize(), Samd21::Dma::BeatSize::BYTE,
599 /* incrementSource */ false, /* incrementDestination */ true,
600 Samd21::Dma::AddressIncrementStepSize::SIZE_1, Samd21::Dma::StepSelection::DESTINATION);
601 27 }
602
603 22 void I2cDriver::queueWriteDma() {
604
10/10
✓ Branch 6 taken 22 times.
✓ Branch 11 taken 22 times.
✓ Branch 16 taken 22 times.
✓ Branch 20 taken 22 times.
✓ Branch 23 taken 22 times.
✓ Branch 26 taken 22 times.
✓ Branch 31 taken 22 times.
✓ Branch 40 taken 22 times.
✓ Branch 43 taken 22 times.
✓ Branch 47 taken 22 times.
66 this->dmaTransactionOut_out(I2cDriver_DmaChannel::WRITE, SercomUtil::txDmaTrigger(this->m_sercom),
605 Samd21::Dma::TransactionType::BEAT, Samd21::Dma::Priority::PRIORITY_1,
606
1/1
✓ Branch 4 taken 22 times.
22 static_cast<U32>(reinterpret_cast<uintptr_t>(this->m_write.getData())),
607
1/1
✓ Branch 4 taken 22 times.
22 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 22 }
612
613 18 void I2cDriver::readImpl(U32 addr, Fw::Buffer& buffer, bool queueDma) {
614 18 FW_ASSERT(this->m_configured);
615
616 18 FW_ASSERT(buffer.getData() != nullptr);
617 18 FW_ASSERT(buffer.getSize() <= SAMD21_I2C_MAX_DMA_PAYLOAD_SIZE, buffer.getSize());
618
619 // We only support 7-bit address mode
620 18 FW_ASSERT((addr & ~0x7F) == 0, addr);
621
622
2/2
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 5 times.
18 if (queueDma) {
623 13 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
2/2
✓ Branch 9 taken 18 times.
✓ Branch 12 taken 18 times.
18 I2cHardware::I2cHal::beginRead(this->m_sercom, addr, static_cast<U8>(buffer.getSize()));
629 18 }
630
631 22 void I2cDriver::writeImpl(U32 addr, Fw::Buffer& buffer, bool generateStopCondition, bool queueReadDma) {
632 22 FW_ASSERT(this->m_configured);
633
634 // I2C DMA is limited to 255 bytes
635 22 FW_ASSERT(buffer.getData() != nullptr);
636 22 FW_ASSERT(buffer.getSize() <= SAMD21_I2C_MAX_DMA_PAYLOAD_SIZE, buffer.getSize());
637 22 FW_ASSERT(buffer.getSize() > 0);
638
639 // We only support 7-bit address mode
640 22 FW_ASSERT((addr & ~0x7F) == 0, addr);
641
642 // Queue up a DMA operation to write the data to the device
643 22 this->queueWriteDma();
644
645 // Queue up a read DMA if we are doing writeRead
646
2/2
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 8 times.
22 if (queueReadDma) {
647 14 FW_ASSERT(this->m_read.getData() != nullptr);
648 14 FW_ASSERT(this->m_read.getSize() <= SAMD21_I2C_MAX_DMA_PAYLOAD_SIZE, this->m_read.getSize());
649 14 FW_ASSERT(this->m_read.getSize() > 0);
650
651 14 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
2/2
✓ Branch 9 taken 22 times.
✓ Branch 12 taken 22 times.
22 I2cHardware::I2cHal::beginWrite(this->m_sercom, addr, static_cast<U8>(buffer.getSize()), generateStopCondition);
658 22 }
659
660 9 void I2cDriver ::write_handler(FwIndexType portNum, U32 addr, Fw::Buffer& buffer) {
661
2/2
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 8 times.
9 if (this->m_state != State::IDLE) {
662
1/2
✓ Branch 5 taken 1 times.
✗ Branch 6 not taken.
1 if (this->isConnected_writeComplete_OutputPort(portNum)) {
663
2/2
✓ Branch 6 taken 1 times.
✓ Branch 10 taken 1 times.
1 this->writeComplete_out(portNum, buffer, Drv::I2cStatus::I2C_OTHER_ERR);
664 }
665 1 return;
666 }
667
668 8 this->m_state = State::WRITE;
669
1/1
✓ Branch 3 taken 8 times.
8 this->m_write = ThinBuffer(buffer);
670 8 this->m_portNum = portNum;
671
672 8 this->writeImpl(addr, buffer, /* generateStopCondition */ true, /* queueReadDma */ false);
673 }
674
675 15 void I2cDriver ::writeRead_handler(FwIndexType portNum, U32 addr, Fw::Buffer& writeBuffer, Fw::Buffer& readBuffer) {
676
2/2
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 14 times.
15 if (this->m_state != State::IDLE) {
677
1/2
✓ Branch 5 taken 1 times.
✗ Branch 6 not taken.
1 if (this->isConnected_writeReadComplete_OutputPort(portNum)) {
678
2/2
✓ Branch 6 taken 1 times.
✓ Branch 10 taken 1 times.
1 this->writeReadComplete_out(portNum, writeBuffer, readBuffer, Drv::I2cStatus::I2C_OTHER_ERR);
679 }
680 1 return;
681 }
682
683 14 this->m_state = State::WRITE_READ_WRITING;
684
1/1
✓ Branch 3 taken 14 times.
14 this->m_read = ThinBuffer(readBuffer);
685
1/1
✓ Branch 3 taken 14 times.
14 this->m_write = ThinBuffer(writeBuffer);
686 14 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 14 this->m_pending_read_address = addr;
692
693 14 this->writeImpl(addr, writeBuffer, /* generateStopCondition */ false, /* queueReadDma */ true);
694 }
695
696 } // namespace Samd21
697