Uh oh!
There was an error while loading. Please reload this page.
SERCOM async/DMA for Wire, SPI, and UART - #385
Conversation
crabel99
commented
Feb 20, 2026
This example async IS31FL3733 DMA library illustrates how integrating DMA into I2C unlocks true non-blocking LED matrix control on SAMD boards.
|
justin-biolumic
commented
Mar 17, 2026
This branch is working for me using SPI but I'm having trouble using it with an i2c EEPROM. I have created a project to demonstrate the problem. https://bitbucket.org/biolumic/samd-i2c-broken/src/master In the platformio.ini file you can switch between the master and sercom-async-dma branches. Building and uploading the master branch works but swapping to the sercom-async-dma does not. I have created a fork of this repo with a couple of small changes to make it work with PlatformIO but nothing that should impact the functionality of this branch. The initial connection works but then the communications stop (timeout). This is what I get on the serial: This what it should look like: Let me know if I can provide any more information to help identify the cause of this issue. |
justin-biolumic
commented
Apr 1, 2026
justin-biolumic
commented
Apr 2, 2026
In file SERCOM_inline.h, function readDataWIRE() has this at line 80. This is where the stop bit gets set. If I change it to: the EEPROM reads succeeds. However, the EEPROM stops working immediately after. I'm not sure if this is related but I've run out of time this week to investigate further. I realize this is probably not a fix I'm just posting this to hopefully help someone else looking at this understand the issue I having. |
crabel99
commented
Apr 2, 2026
I tweaked the if (sercom->getTxnIndexWIRE() < sercom->getTxnLengthWIRE()) {
bool more = isRead ? sercom->readDataWIRE() : sercom->sendDataWIRE();
awaitingAddressAck = false;
if (!isRead || more) return; // made this a conditional return based on more boolean
} |
justin-biolumic
commented
Apr 7, 2026
@crabel99 commit https://github.com/adafruit/ArduinoCore-samd/pull/385/commits/ff93e56090ae50c8905d8b51f90c953f33a50202 fixes the issue I was having. Why did you revert it? |
crabel99
commented
Apr 13, 2026
@justin-biolumic I was shotgunning my own project to roll back anything that had changed from when it last worked I will go ahead and revert the reversion. |
justin-biolumic
commented
Apr 14, 2026
I have found this branch breaks some of our other SPI drivers. I don't have time to investigate this ATM. I'll report back when I do. |
crabel99
commented
Apr 16, 2026
@justin-biolumic, the issue may be if there are a number of async calls being made during startup. I made the SERCOM buffer relatively small, 8 for each SERCOM. So, for some boards, that is 56 buffer items. I have to be careful when adapting libraries to force sync behavior on |
justin-biolumic
commented
Apr 17, 2026
@crabel99, actually it's not an issue with an SPI driver. It looks like an interrupt handlers are mixed up somehow. You can see in this image that SERCOM4_Handler points to void SPI_IT_HANDLER(void), but SERCOM4 is the Wire device referred to in the other calls in the stack. For context, this is happening on a project that uses all 6 SERCOMs.
I'm unable to share the code so I don't know how helpful this is. The project works fine when I swap back to the master branch though so I don't think it's an issue with how the SERCOMs are set up in my code. I'll do more testing when I have time. |
@justin-biolumic, I committed a change that I think should, hopefully, resolve your issue. Master only supports DMA on SPI. This branch provides full DMA support for all three serial channels and for the chip ADC. The big shift was moving/unifying the DMA architecture into SERCOM.h/cpp and then writing the protocol-specific implementations. Another major shift is to use the chip's PendSV to execute deferred callbacks outside the ISR context. This is super useful even with regular ISR-type events. I am surprised that this feature was not built into the code from the beginning. This, coupled with the TaskScheduler library, enables a highly efficient code architecture. |
justin-biolumic
commented
Apr 19, 2026
justin-biolumic
commented
Apr 19, 2026
After some more digging I've found the issue only happens when using Wire1. It doesn't matter what order I bring the interfaces up, only Wire1 fails. If I swap the pins/SERCOMs between the Wire interfaces Wire1 still fails. Just to be clear, I have two Wire interfaces. Both interfaces work when they are attached to Wire but neither interface works if I attach it Wire1. So something about how Wire1 is setup causes it to end up in the dummy handler when calling endTransmission(). |
@justin-biolumic, I have a test that reproduces the issue. I'm going to work on isolating it today. |
justin-biolumic
commented
May 7, 2026
@crabel99 Have you made any progress on the Wire1 issue? |
crabel99
commented
May 7, 2026
@justin-biolumic my day job has been consuming my time, and I haven't had the bandwidth to work on this. I should be back to it next week. |
crabel99
commented
May 17, 2026
@justin-biolumic, I made the changes; please let me know if this works. There was a bug in |
justin-biolumic
commented
May 17, 2026
@crabel99, no change for me. I still end up in the dummy handler when using Wire1 regardless if this is the first or second I2C interface I bring up. |
crabel99
commented
Jun 18, 2026
@justin-biolumic Can you send me snippets of your /* * Serial interfaces*/// Serial
#definePIN_SERIAL_TX (16ul)
#definePIN_SERIAL_RX (17ul)
#definePAD_SERIAL_TX (UART_TX_PAD_0)
#definePAD_SERIAL_RX (SERCOM_RX_PAD_1)
/* * SPI Interfaces*/
#defineSPI_INTERFACES_COUNT1
#definePIN_SPI_MOSI (14u)
#definePIN_SPI_MISO (15u)
#definePIN_SPI_SCK (11u)
#definePIN_SPI_SS (7u)
#definePERIPH_SPI sercom0
#definePAD_SPI_TXSPI_PAD_2_SCK_3
#definePAD_SPI_RXSERCOM_RX_PAD_1staticconstuint8_tSS = PIN_SPI_SS;
staticconstuint8_tMOSI = PIN_SPI_MOSI;
staticconstuint8_tMISO = PIN_SPI_MISO;
staticconstuint8_tSCK = PIN_SPI_SCK;
/* * Wire Interfaces*/// dI2C Interface
#defineWIRE_INTERFACES_COUNT2
#definePIN_WIRE_SDA (9u)
#definePIN_WIRE_SCL (10u)
#definePERIPH_WIRE sercom2
#defineWIRE_ALT_SERCOM (true)
#defineWIRE_IT_HANDLER SERCOM2_Handler
staticconstuint8_tSDA = PIN_WIRE_SDA;
staticconstuint8_tSCL = PIN_WIRE_SCL;
// Component I2C Interface
#definePIN_WIRE1_SDA (18u)
#definePIN_WIRE1_SCL (19u)
#definePERIPH_WIRE1 sercom1
#defineWIRE1_ALT_SERCOM (false)
#defineWIRE1_IT_HANDLER SERCOM1_Handler
staticconstuint8_tSDA1 = PIN_WIRE1_SDA;
staticconstuint8_tSCL1 = PIN_WIRE1_SCL;
/* * USB*/
#definePIN_USB_HOST_ENABLE (30ul)
#definePIN_USB_DM (31ul)
#definePIN_USB_DP (32ul)The above snippet is from one of my projects for a device with 4 of the 6 used. |
justin-biolumic
commented
Jun 19, 2026
We're using all 6 SERCOMs. |
crabel99
commented
Jun 20, 2026
You have: #defineWIRE_IT_HANDLER1 SERCOM4_Handlerbut #defineWIRE1_IT_HANDLER SERCOM4_HandlerBecause the macro name is incorrect, the core never emits |
…in a multi-master configuration
…erving legacy behavior.
…added stopTransmissionWIRE to allow sync closeout, handle errors and continue processing the transaction queue
…or pin assignment validation
- Fix misleading indentation in retry logic (lines 847, 857) - Remove ambiguous overload for Wire.begin() with integer literals (uint16_t version now requires explicit enableGeneralCall parameter) - Remove unused variable in SPI.cpp - Remove redundant unsigned < 0 check in setPending()
- Add __attribute__((weak)) to all SPI interrupt handlers (SERCOM4, SPI1, etc) This allows variants to override them when SERCOM is used for other peripherals (e.g., MKR variants use SERCOM4 for Serial2/UART) - Explicitly cast slave addresses to uint8_t in Wire examples to avoid any potential overload resolution issues on different compiler versions
…final read." This reverts commit ff93e56.
… final read." This reverts commit 7f91df1.
justin-biolumic
commented
Jun 21, 2026
@crabel99 I can confirm that fixing the macro resolved the issue I was having. Thanks. |
crabel99
commented
Jun 21, 2026
@justin-biolumic That is excellent news. I am sorry it took so long to isolate that issue! |
justin-biolumic
commented
Jun 21, 2026
@crabel99 Nothing to be sorry for, it was my mistake. I'm sorry for sending you on a wild goose chase! |
Signed-off-by: Cal Abel <crabel@mac.com>
…m-async-dma-pendsv-merge
crabel99
commented
Aug 22, 2026
Superseded by #395, which carries the reviewed SERCOM async/DMA work on the current SAME5x integration branch and passes the full 12-job matrix. |



Related Issue: #382
SERCOM Async/DMA API Comparison: sercom-async-dma vs Master
Executive Summary
Motivation
The SAMD21/SAMD51 SERCOM peripherals support hardware-accelerated DMA transfers, but the Arduino core's synchronous blocking APIs don't expose this capability. This creates performance bottlenecks in applications that need to communicate with multiple peripherals efficiently. The master branch Wire library had internal async/DMA support, but the API remained entirely synchronous, and the patterns weren't extended to SPI or UART.
This branch extends transparent async/DMA operation across all three major SERCOM interfaces (Wire/I2C, SPI, UART) while maintaining 100% backward compatibility with existing synchronous code.
Design Intent
Primary Goals:
Key Design Decision:
Non-Goals:
writeAsync()methods)Philosophy
"Seamless by default, async by choice"
The API design follows a simple principle: when a callback is provided (
!= nullptr), the operation is asynchronous and returns immediately; when no callback is provided (== nullptr), the operation is synchronous and blocks until complete. This allows:USE_ZERODMAis definedThe transaction pool architecture (8 transactions matching SERCOM queue depth) enables efficient pipelining of operations without exposing queue management to applications.
Hardware Testing Status
✅ Tested Configurations
Testing Notes:
USE_ZERODMAenabled)USE_ZERODMAdisabled)USE_ZERODMA) has been tested alongside fallback paths (without DMA library)Recommended Pre-Merge Validation
Before merging to master, reviewers should consider:
Known Limitations & Future Development
Current Limitations (SAMD21/SAMD51 Silicon Errata):
Future Development Roadmap:
Hardware CRC Integration (Requires DMA):
Additional Testing:
Performance Optimization:
SAMD51 Clock Selection Enhancement:
Strict I2C Pad Validation:
Optional Companion Libraries (Future):
API Change Summary
Quick reference of what changed across the three interfaces:
endTransmission()+requestFrom()now accept callbacksread(buffer, size, callback)andwrite(buffer, size, callback)transfer()now accepts callbacksDetailed API Comparison
Wire API Changes
Master Branch (Original)
Note: Master branch Wire already had some async operation support through internal transaction mechanisms, but the API was entirely synchronous (blocking).
sercom-async-dma Branch (Enhanced)
Wire API Summary
beginTransmission(addr)endTransmission(stop)endTransmission(stop=true)endTransmission(stop, callback, user)requestFrom(addr, qty, stop)requestFrom(addr, qty, stop=true)requestFrom(addr, qty, stop, rxBuf, cb, user)requestFrom(rxBuffer=ptr)setRxBuffer/setTxBuffer/...begin(addr, ..., enable10Bit)begin(..., speed)write(data, qty, setExternal=true)Wire Design Pattern Notes
Wire uses a multi-stage transaction builder pattern:
beginTransmission(address)- Start buildingwrite(...)- Add data to staging buffer (loader transaction)endTransmission(callback)- Execute the built transaction (sync or async)Or for reads:
requestFrom(address, quantity, callback)- Execute read transaction directlyThis pattern influenced the unified API approach for UART and SPI, but those interfaces use single-call operations rather than multi-stage building.
Key Enhancement: The sercom-async-dma branch adds async callback support to the Wire API while maintaining full backward compatibility with the synchronous blocking behavior. When callbacks are nullptr (default), behavior is identical to master branch.
UART API Changes
Master Branch (Original)
sercom-async-dma Branch (New)
UART API Summary
int read()size_t write(uint8_t)read(buf, size)read(buf, size, callback)write(buf, size)write(buf, size, callback)API Strategy: Single unified method with optional callback parameter (like Wire)
SPI API Changes
Master Branch (Original)
sercom-async-dma Branch (New)
SPI API Summary
void transfer(void*)transfer(tx, rx, count, true)transfer(tx, rx, count, true, callback)nullptr)API Strategy: Extended existing method signature with optional callback parameters
Wire API (For Reference)
Wire already had async/DMA in master, but documentation for pattern:
Cross-Interface API Patterns
Design Consistency
Async Pattern (All Three Interfaces)
Note: For async calls, any user-provided buffers must remain valid until the completion callback fires.
Coverage Analysis
UART Coverage
Before (Master):
After (sercom-async-dma):
SPI Coverage
Before (Master):
After (sercom-async-dma):
waitForTransfer()check for non-blocking code pathsFingerprint (Method Signature) Differences
UART: New Overloads Added
SPI: Existing Signature Extended
Default Parameter Behavior
UART New Methods (Defaults)
SPI Extended Method (Defaults)
Implementation Transparency
Test Coverage
UART Functional Tests (NEW)
SPI Hardware Tests (NEW)
Master Branch
Summary of Changes
What's New
UART:
read(buffer, size, callback, user)write(buffer, size, callback, user)SPI:
transfer()signaturewaitForTransfer()andisBusy()for non-blocking patternsBoth:
What Changed in Existing API
transfer()signature extended with optional parametersWhat Stayed the Same
Migration Guide: Master → sercom-async-dma
UART: No changes required
Opt-in to new async features
SPI: No changes required
Opt-in to new async features
Verdict
✅ API design is clean and consistent:
Update 17 Feb 2026: added links to repositories for SerialRTT and DebugUtils