xref: /aosp_15_r20/external/pigweed/pw_spi_mcuxpresso/responder.cc (revision 61c4878ac05f98d0ceed94b57d316916de578985)
1 // Copyright 2024 The Pigweed Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License"); you may not
4 // use this file except in compliance with the License. You may obtain a copy of
5 // the License at
6 //
7 //     https://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 // License for the specific language governing permissions and limitations under
13 // the License.
14 #include "pw_spi_mcuxpresso/responder.h"
15 
16 #include <cinttypes>
17 
18 #include "pw_assert/check.h"
19 #include "pw_log/log.h"
20 #include "pw_status/try.h"
21 
22 // Vendor terminology requires this to be disabled.
23 // inclusive-language: disable
24 
25 namespace pw::spi {
26 namespace {
27 
SpanData(ByteSpan & span)28 uint8_t* SpanData(ByteSpan& span) {
29   static_assert(std::is_same_v<uint8_t, unsigned char>);
30   return reinterpret_cast<uint8_t*>(span.data());
31 }
32 
SpanDataDiscardConst(ConstByteSpan & span)33 uint8_t* SpanDataDiscardConst(ConstByteSpan& span) {
34   static_assert(std::is_same_v<uint8_t, unsigned char>);
35   return const_cast<uint8_t*>(reinterpret_cast<const uint8_t*>(span.data()));
36 }
37 
ToPwStatus(status_t status)38 Status ToPwStatus(status_t status) {
39   switch (status) {
40     // Intentional fall-through
41     case kStatus_Success:
42     case kStatus_SPI_Idle:
43       return OkStatus();
44     case kStatus_ReadOnly:
45       return Status::PermissionDenied();
46     case kStatus_OutOfRange:
47       return Status::OutOfRange();
48     case kStatus_InvalidArgument:
49       return Status::InvalidArgument();
50     case kStatus_Timeout:
51       return Status::DeadlineExceeded();
52     case kStatus_NoTransferInProgress:
53       return Status::FailedPrecondition();
54     // Intentional fall-through
55     case kStatus_Fail:
56     default:
57       PW_LOG_ERROR("Mcuxpresso SPI unknown error code: %d",
58                    static_cast<int>(status));
59       return Status::Unknown();
60   }
61 }
62 
SetSdkConfig(const McuxpressoResponder::Config & config,spi_slave_config_t & sdk_config)63 Status SetSdkConfig(const McuxpressoResponder::Config& config,
64                     spi_slave_config_t& sdk_config) {
65   switch (config.polarity) {
66     case ClockPolarity::kActiveLow:
67       sdk_config.polarity = kSPI_ClockPolarityActiveLow;
68       break;
69     case ClockPolarity::kActiveHigh:
70       sdk_config.polarity = kSPI_ClockPolarityActiveHigh;
71       break;
72     default:
73       return Status::InvalidArgument();
74   }
75 
76   switch (config.phase) {
77     case ClockPhase::kRisingEdge:
78       sdk_config.phase = kSPI_ClockPhaseFirstEdge;
79       break;
80     case ClockPhase::kFallingEdge:
81       sdk_config.phase = kSPI_ClockPhaseSecondEdge;
82       break;
83     default:
84       return Status::InvalidArgument();
85   }
86 
87   switch (config.bit_order) {
88     case BitOrder::kMsbFirst:
89       sdk_config.direction = kSPI_MsbFirst;
90       break;
91     case BitOrder::kLsbFirst:
92       sdk_config.direction = kSPI_LsbFirst;
93       break;
94     default:
95       return Status::InvalidArgument();
96   }
97 
98   switch (config.bits_per_word()) {
99     case 4:
100       sdk_config.dataWidth = kSPI_Data4Bits;
101       break;
102     case 5:
103       sdk_config.dataWidth = kSPI_Data5Bits;
104       break;
105     case 6:
106       sdk_config.dataWidth = kSPI_Data6Bits;
107       break;
108     case 7:
109       sdk_config.dataWidth = kSPI_Data7Bits;
110       break;
111     case 8:
112       sdk_config.dataWidth = kSPI_Data8Bits;
113       break;
114     case 9:
115       sdk_config.dataWidth = kSPI_Data9Bits;
116       break;
117     case 10:
118       sdk_config.dataWidth = kSPI_Data10Bits;
119       break;
120     case 11:
121       sdk_config.dataWidth = kSPI_Data11Bits;
122       break;
123     case 12:
124       sdk_config.dataWidth = kSPI_Data12Bits;
125       break;
126     case 13:
127       sdk_config.dataWidth = kSPI_Data13Bits;
128       break;
129     case 14:
130       sdk_config.dataWidth = kSPI_Data14Bits;
131       break;
132     case 15:
133       sdk_config.dataWidth = kSPI_Data15Bits;
134       break;
135     case 16:
136       sdk_config.dataWidth = kSPI_Data16Bits;
137       break;
138     default:
139       return Status::InvalidArgument();
140   }
141 
142   return OkStatus();
143 }
144 
145 //
146 // Helpful things missing from the SDK
147 //
148 const IRQn_Type spi_irq_map[] = SPI_IRQS;
149 
150 // Enable interrupt on CS asserted / de-asserted.
SPI_EnableSSInterrupt(SPI_Type * base)151 void SPI_EnableSSInterrupt(SPI_Type* base) {
152   base->STAT = SPI_STAT_SSA_MASK | SPI_STAT_SSD_MASK;  // Clear first
153   base->INTENSET = SPI_INTENSET_SSAEN_MASK | SPI_INTENSET_SSDEN_MASK;
154 }
155 
156 // Disable interrupt on CS asserted / de-asserted.
SPI_DisableSSInterrupt(SPI_Type * base)157 void SPI_DisableSSInterrupt(SPI_Type* base) {
158   base->INTENCLR = SPI_INTENSET_SSAEN_MASK | SPI_INTENSET_SSDEN_MASK;
159 }
160 
161 // Empty the TX and RX FIFOs.
SPI_EmptyFifos(SPI_Type * base)162 void SPI_EmptyFifos(SPI_Type* base) {
163   base->FIFOCFG |= SPI_FIFOCFG_EMPTYTX_MASK | SPI_FIFOCFG_EMPTYRX_MASK;
164 }
165 
SPI_RxFifoIsEmpty(SPI_Type * base)166 bool SPI_RxFifoIsEmpty(SPI_Type* base) {
167   // RXNOTEMPTY: Receive FIFO is Not Empty
168   // 0 - The receive FIFO is empty.
169   // 1 - The receive FIFO is not empty, so data can be read.
170   return !(base->FIFOSTAT & SPI_FIFOSTAT_RXNOTEMPTY_MASK);
171 }
172 
SPI_RxError(SPI_Type * base)173 bool SPI_RxError(SPI_Type* base) {
174   return (base->FIFOSTAT & SPI_FIFOSTAT_RXERR_MASK);
175 }
176 
SPI_TxError(SPI_Type * base)177 bool SPI_TxError(SPI_Type* base) {
178   return (base->FIFOSTAT & SPI_FIFOSTAT_TXERR_MASK);
179 }
180 
181 // Non-FIFO interrupt sources
182 enum _spi_interrupt_sources {
183   kSPI_SlaveSelAssertIrq = SPI_INTENSET_SSAEN_MASK,
184   kSPI_SlaveSelDeassertIrq = SPI_INTENSET_SSDEN_MASK,
185   kSPI_MasterIdleIrq = SPI_INTENSET_MSTIDLEEN_MASK,
186 };
187 
188 // Gets a bitmap of active (pending + enabled) interrupts.
189 // Test against _spi_interrupt_sources constants.
SPI_GetActiveInterrupts(SPI_Type * base)190 uint32_t SPI_GetActiveInterrupts(SPI_Type* base) {
191   // Verify that the bits in INTSTAT and INTENSET are the same.
192   static_assert(SPI_INTSTAT_SSA_MASK == SPI_INTENSET_SSAEN_MASK);
193   static_assert(SPI_INTSTAT_SSD_MASK == SPI_INTENSET_SSDEN_MASK);
194   static_assert(SPI_INTSTAT_MSTIDLE_MASK == SPI_INTENSET_MSTIDLEEN_MASK);
195   return base->INTSTAT & base->INTENSET;
196 }
197 
198 // Clears a bitmap of active interrupts.
199 // This acknowledges the interrupt; it does not disable it.
200 // @irqs is either kSPI_SlaveSelAssertIrq or kSPI_SlaveSelDeassertIrq.
SPI_ClearActiveInterrupts(SPI_Type * base,uint32_t irqs)201 void SPI_ClearActiveInterrupts(SPI_Type* base, uint32_t irqs) {
202   // Verify that the bits in STAT match the enum.
203   static_assert(SPI_STAT_SSA_MASK == kSPI_SlaveSelAssertIrq);
204   static_assert(SPI_STAT_SSD_MASK == kSPI_SlaveSelDeassertIrq);
205   PW_CHECK((irqs & ~(kSPI_SlaveSelAssertIrq | kSPI_SlaveSelDeassertIrq)) == 0);
206   base->STAT = irqs;  // write to clear
207 }
208 
209 }  // namespace
210 
Initialize()211 Status McuxpressoResponder::Initialize() {
212   status_t sdk_status;
213   spi_slave_config_t sdk_config;
214   spi_dma_callback_t callback;
215 
216   SPI_SlaveGetDefaultConfig(&sdk_config);
217   PW_TRY(SetSdkConfig(config_, sdk_config));
218 
219   // Hard coded for now, till added to Config
220   sdk_config.sselPol = kSPI_SpolActiveAllLow;
221 
222   sdk_status = SPI_SlaveInit(base_, &sdk_config);
223   if (sdk_status != kStatus_Success) {
224     PW_LOG_ERROR("SPI_SlaveInit failed: %ld", sdk_status);
225     return ToPwStatus(sdk_status);
226   }
227 
228   if (config_.handle_cs) {
229     // Set up the FLEXCOMM IRQ to get CS assertion/deassertion.
230     // See SPI_MasterTransferCreateHandle().
231     // Note that the 'handle' argument can actually be anything.
232     FLEXCOMM_SetIRQHandler(base_, FlexcommSpiIrqHandler, this);
233 
234     // Enable SPI interrupt in NVIC
235     uint32_t instance = SPI_GetInstance(base_);
236     EnableIRQ(spi_irq_map[instance]);
237 
238     // We only use the CS deassertion interrupt to complete transfers.
239     // Don't provide any callback to the SPI driver (to be invoked by DMA IRQ).
240     callback = nullptr;
241 
242     // Disable the DMA channel interrupts.
243     // If we leave them enabled, then the SPI driver could complete a full
244     // transfer, move the state to kSPI_Idle, and prevent
245     // SPI_SlaveTransferGetCountDMA() from working.
246     rx_dma_.DisableInterrupts();
247     tx_dma_.DisableInterrupts();
248   } else {
249     // Without CS deassertion, we use the SPI driver callback (invoked by DMA
250     // IRQ) to complete transfers.
251     callback = McuxpressoResponder::SdkCallback;
252 
253     // Enable the DMA channel interrupts.
254     // These are enabled by default by DMA_CreateHandle(), but re-enable them
255     // anyway in case they were disabled for some reason.
256     rx_dma_.EnableInterrupts();
257     tx_dma_.EnableInterrupts();
258   }
259 
260   sdk_status = SPI_SlaveTransferCreateHandleDMA(
261       base_, &handle_, callback, this, tx_dma_.handle(), rx_dma_.handle());
262   if (sdk_status != kStatus_Success) {
263     PW_LOG_ERROR("SPI_SlaveTransferCreateHandleDMA failed: %ld", sdk_status);
264     return ToPwStatus(sdk_status);
265   }
266 
267   return OkStatus();
268 }
269 
TransferComplete(Status status,size_t bytes_transferred)270 void McuxpressoResponder::TransferComplete(Status status,
271                                            size_t bytes_transferred) {
272   if (config_.handle_cs) {
273     SPI_DisableSSInterrupt(base_);
274   }
275 
276   // Abort the DMA transfer (if active).
277   SPI_SlaveTransferAbortDMA(base_, &handle_);
278 
279   // Check for TX underflow / RX overflow
280   //
281   // Ideally we want to check for FIFO under/overflow only *while* the transfer
282   // is running. But if the initiator sent more bytes than the DMA was set up
283   // to tx/rx, both of these errors will happen (after the DMA is complete).
284   //
285   // To do this without risk of false positives, we would need to find a way to
286   // capture this status immediately when the DMA is complete, or otherwise
287   // monitor it during the transfer. Perhaps with a custom DMA chain, we could
288   // capture the status registers via DMA, but that might still be too late.
289   if (status.ok() && config_.check_fifo_error != FifoErrorCheck::kNone) {
290     if (SPI_RxError(base_)) {
291       PW_LOG_ERROR("RX FIFO overflow detected!");
292       if (config_.check_fifo_error == FifoErrorCheck::kError) {
293         status = Status::DataLoss();
294       }
295     }
296     if (SPI_TxError(base_)) {
297       PW_LOG_ERROR("TX FIFO underflow detected!");
298       if (config_.check_fifo_error == FifoErrorCheck::kError) {
299         status = Status::DataLoss();
300       }
301     }
302   }
303 
304   // TODO(jrreinhart) Remove these safety checks.
305   if (rx_dma_.IsBusy()) {
306     PW_LOG_WARN("After completion, rx_dma still busy!");
307   }
308   if (rx_dma_.IsActive()) {
309     PW_LOG_WARN("After completion, rx_dma still active!");
310   }
311 
312   // Empty the FIFOs.
313   // If the initiator sent more bytes than the DMA was set up to receive, the
314   // RXFIFO will have the residue. This isn't strictly necessary since they'll
315   // be cleared on the next call to SPI_SlaveTransferDMA(), but we do it anyway
316   // for cleanliness.
317   SPI_EmptyFifos(base_);
318 
319   // Clear the FIFO DMA request signals.
320   //
321   // From IMXRT500RM 53.4.2.1.2 DMA operation:
322   // "A DMA request is provided for each SPI direction, and can be used instead
323   // of interrupts for transferring data... The DMA controller provides an
324   // acknowledgement signal that clears the related request when it (the DMA
325   // controller) completes handling that request."
326   //
327   // If the initiator sent more bytes than the DMA was set up to receive, this
328   // request signal will remain latched on, even after the FIFO is emptied.
329   // This would cause a subsequent transfer to receive one stale residual byte
330   // from this prior transfer.
331   //
332   // We force if off here by disabling the DMA request signal.
333   // It will be re-enabled on the next transfer.
334   SPI_EnableRxDMA(base_, false);
335   SPI_EnableTxDMA(base_, false);
336 
337   // Invoke the callback
338   auto received = current_transaction_.rx_data.subspan(0, bytes_transferred);
339   current_transaction_ = {};
340   completion_callback_(received, status);
341 }
342 
SdkCallback(SPI_Type * base,spi_dma_handle_t * handle,status_t sdk_status,void * userData)343 void McuxpressoResponder::SdkCallback(SPI_Type* base,
344                                       spi_dma_handle_t* handle,
345                                       status_t sdk_status,
346                                       void* userData) {
347   // WARNING: This is called in IRQ context.
348   auto* responder = static_cast<McuxpressoResponder*>(userData);
349   PW_CHECK_PTR_EQ(base, responder->base_);
350   PW_CHECK_PTR_EQ(handle, &responder->handle_);
351 
352   return responder->DmaComplete(sdk_status);
353 }
354 
DmaComplete(status_t sdk_status)355 void McuxpressoResponder::DmaComplete(status_t sdk_status) {
356   // WARNING: This is called in IRQ context.
357   PW_CHECK(!config_.handle_cs,
358            "DmaComplete should never be called when handle_cs=true!");
359 
360   // Move to idle state.
361   if (State prev; !TryChangeState(State::kBusy, State::kIdle, &prev)) {
362     // Spurious callback? Or race condition in DoWriteReadAsync()?
363     PW_LOG_WARN("DmaComplete not in busy state, but %u",
364                 static_cast<unsigned int>(prev));
365     return;
366   }
367 
368   // Transfer complete.
369   auto status = ToPwStatus(sdk_status);
370   size_t bytes_transferred =
371       status.ok() ? current_transaction_.rx_data.size() : 0;
372   TransferComplete(status, bytes_transferred);
373 }
374 
FlexcommSpiIrqHandler(void * base,void * arg)375 void McuxpressoResponder::FlexcommSpiIrqHandler(void* base, void* arg) {
376   // WARNING: This is called in IRQ context.
377 
378   SPI_Type* spi = static_cast<SPI_Type*>(base);
379   auto* responder = static_cast<McuxpressoResponder*>(arg);
380   PW_CHECK_PTR_EQ(spi, responder->base_);
381 
382   // NOTE: It's possible that CS could deassert and INTSTAT.SSD could latch
383   // shortly after the IRQ handler is entered (due to INTSTAT.SSA), re-setting
384   // the IRQ as pending in the NVIC. In this case, we could handle both SSA and
385   // SSD in the same interrupt. When that happens, the IRQ remains pended in
386   // the NVIC, and the handler will file again. We simply ignore the second
387   // interrupt.
388   //
389   // It would wrong to try and handle only one of SSA or SSD per invocation
390   // because if the interrupt was handled late enough, it might only fire once.
391   const auto active_irqs = SPI_GetActiveInterrupts(spi);
392 
393   // CS asserted?
394   if (active_irqs & kSPI_SlaveSelAssertIrq) {
395     SPI_ClearActiveInterrupts(spi, kSPI_SlaveSelAssertIrq);
396     responder->CsAsserted();
397   }
398 
399   // CS de-asserted?
400   if (active_irqs & kSPI_SlaveSelDeassertIrq) {
401     SPI_ClearActiveInterrupts(spi, kSPI_SlaveSelDeassertIrq);
402     responder->CsDeasserted();
403   }
404 }
405 
CsAsserted()406 void McuxpressoResponder::CsAsserted() {
407   // WARNING: This is called in IRQ context.
408 }
409 
WaitForQuiescenceAfterCsDeassertion()410 Status McuxpressoResponder::WaitForQuiescenceAfterCsDeassertion() {
411   // When CS is deasserted, the master is indicating that it has finished
412   // clocking out data into our FIFO. That could be more, less, or the same
413   // number of bytes requested by the user (in DoWriteReadAsync).
414   //
415   // Definitions:
416   //   S: The DMA transfer size (as requested by the user).
417   //   M: The number of bytes sent by the master.
418   //
419   // Case | Condition | DMA will complete? | FIFO will empty?
420   // -----|-----------|--------------------|-------------------
421   //    1 |   M < S   | No                 | Yes
422   //    2 |   M = S   | Yes                | Yes
423   //    3 |   M > S   | Yes                | No
424   //
425   // At this point, the RX FIFO might still have data that the DMA has not yet
426   // read.
427   //
428   // We wait for either the DMA channel to become inactive (case 2 or 3) or for
429   // the RX FIFO to become empty (case 1 or 2). When the FIFO empties, we also
430   // need to wait for the DMA channel to be non-busy, indicating that it has
431   // finished moving the data to SRAM.
432   //
433   // It is expected that by the time this function is called, the hardware will
434   // have already quiesced, and we won't actually wait at all. A warning log
435   // will indicate if that assumption does not hold true.
436   constexpr unsigned int kMaxWaitCount = 10000;  // Arbitrary
437 
438   unsigned int wait_count;
439   for (wait_count = 0; wait_count < kMaxWaitCount; ++wait_count) {
440     if (!rx_dma_.IsActive()) {
441       // The DMA has consumed as many bytes from the FIFO as it ever will.
442       break;
443     }
444 
445     if (SPI_RxFifoIsEmpty(base_) && !rx_dma_.IsBusy()) {
446       // The FIFO is empty, and the DMA channel has moved all data to SRAM.
447       break;
448     }
449 
450     // DMA is still active and FIFO is not empty. We need to wait.
451   }
452 
453   if (wait_count == kMaxWaitCount) {
454     PW_LOG_ERROR(
455         "After CS de-assertion, timed out waiting for DMA done or FIFO empty.");
456     return Status::DeadlineExceeded();
457   }
458 
459   if (wait_count != 0) {
460     PW_LOG_WARN(
461         "After CS de-assertion, waited %u times for DMA done or FIFO empty.",
462         wait_count);
463   }
464   return OkStatus();
465 }
466 
CsDeasserted()467 void McuxpressoResponder::CsDeasserted() {
468   // WARNING: This is called in IRQ context.
469   PW_CHECK(config_.handle_cs,
470            "CsDeasserted should only be called when handle_cs=true!");
471 
472   // Move to idle state.
473   if (State prev; !TryChangeState(State::kBusy, State::kIdle, &prev)) {
474     PW_LOG_WARN("CsDeasserted not in busy state, but %u",
475                 static_cast<unsigned int>(prev));
476     return;
477   }
478 
479   Status wait_status = WaitForQuiescenceAfterCsDeassertion();
480 
481   // Get the number of bytes actually transferred.
482   //
483   // NOTE: SPI_SlaveTransferGetCountDMA() fails if _handle.state != kSPI_Busy.
484   // Thus, it must be called before SPI_SlaveTransferAbortDMA() which changes
485   // the state to kSPI_Idle. Also, the DMA channel interrupts are disabled when
486   // CS is respected, because SPI_RxDMACallback() and SPI_TxDMACallback() also
487   // change the state to kSPI_Idle.
488   size_t bytes_transferred = 0;
489   status_t sdk_status =
490       SPI_SlaveTransferGetCountDMA(base_, &handle_, &bytes_transferred);
491 
492   // Transfer complete.
493   Status xfer_status = OkStatus();
494   if (!wait_status.ok()) {
495     bytes_transferred = 0;
496     xfer_status = wait_status;
497   } else if (sdk_status != kStatus_Success) {
498     PW_LOG_ERROR("SPI_SlaveTransferGetCountDMA() returned %" PRId32,
499                  sdk_status);
500     bytes_transferred = 0;
501     xfer_status = ToPwStatus(sdk_status);
502   }
503   TransferComplete(xfer_status, bytes_transferred);
504 }
505 
DoWriteReadAsync(ConstByteSpan tx_data,ByteSpan rx_data)506 Status McuxpressoResponder::DoWriteReadAsync(ConstByteSpan tx_data,
507                                              ByteSpan rx_data) {
508   if (!TryChangeState(State::kIdle, State::kBusy)) {
509     PW_LOG_ERROR("Transaction already started");
510     return Status::FailedPrecondition();
511   }
512   PW_CHECK(!current_transaction_);
513 
514   // TODO(jrreinhart): There is a race here. If DoCancel() is called, it will
515   // move to kIdle, and invoke the callback with CANCELLED. But then we will
516   // still go on to perform the transfer anyway. When the transfer completes,
517   // SdkCallback will see kIdle and skip the callback. We avoid this problem
518   // by saying that DoWriteReadAsync() and DoCancel() should not be called from
519   // different threads, thus we only have to worry about DoCancel() racing the
520   // hardware / IRQ.
521 
522   spi_transfer_t transfer = {};
523 
524   if (!tx_data.empty() && !rx_data.empty()) {
525     // spi_transfer_t has only a single dataSize member, so tx_data and
526     // rx_data must be the same size. Separate rx/tx data sizes could
527     // theoretically be handled, but the SDK doesn't support it.
528     //
529     // TODO(jrreinhart) Support separate rx/tx data sizes.
530     // For non-DMA, it's a pretty simple patch.
531     // It should be doable for DMA also, but I haven't looked into it.
532     if (tx_data.size() != rx_data.size()) {
533       return Status::InvalidArgument();
534     }
535 
536     transfer.txData = SpanDataDiscardConst(tx_data);
537     transfer.rxData = SpanData(rx_data);
538     transfer.dataSize = rx_data.size();
539   } else if (!tx_data.empty()) {
540     transfer.txData = SpanDataDiscardConst(tx_data);
541     transfer.dataSize = tx_data.size();
542   } else if (!rx_data.empty()) {
543     transfer.rxData = SpanData(rx_data);
544     transfer.dataSize = rx_data.size();
545   } else {
546     return Status::InvalidArgument();
547   }
548 
549   current_transaction_ = {
550       .rx_data = rx_data,
551   };
552 
553   if (config_.handle_cs) {
554     // Complete the transfer when CS is deasserted.
555     SPI_EnableSSInterrupt(base_);
556   }
557 
558   status_t sdk_status = SPI_SlaveTransferDMA(base_, &handle_, &transfer);
559   if (sdk_status != kStatus_Success) {
560     PW_LOG_ERROR("SPI_SlaveTransferDMA failed: %ld", sdk_status);
561     return ToPwStatus(sdk_status);
562   }
563 
564   return OkStatus();
565 }
566 
DoCancel()567 void McuxpressoResponder::DoCancel() {
568   if (!TryChangeState(State::kBusy, State::kIdle)) {
569     return;
570   }
571   TransferComplete(Status::Cancelled(), 0);
572 }
573 
574 }  // namespace pw::spi
575