# Dma Migration

> Skill for discovering applications that use the legacy DMADRV driver and migrating them to the DMA 2.0 architecture (DMA Manager + DMA Channel Driver).

- Skill: `ducanh-silabs/dma-migration` (Agent Skill)
- Install (CLI): `npx skillmds@latest add ducanh-silabs/dma-migration`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ducanh-silabs/dma-migration/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: DucAnh-silabs (https://skillmd.com/u/ducanh-silabs)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/ducanh-silabs/dma-migration

---


# DMA Migration: DMADRV to DMA 2.0

## Overview

The legacy DMADRV driver is being deprecated in favor of a new two-component DMA 2.0 architecture:

- **DMA Manager** (`sl_dma_manager`) — a service that owns everything global to the DMA peripheral: enabling/disabling the peripheral, channel allocation, sync-bit allocation, round-robin configuration, and IRQ dispatching.
- **DMA Channel Driver** (`sl_dma_channel`) — a per-channel driver that submits and controls data transfers. Requires a channel number previously allocated by the DMA Manager.

### Deprecation Timeline

| Milestone | Release |
|-----------|---------|
| DMA Manager + DMA Channel Driver available, DMADRV deprecated (no new features) | **2026.6.0** |
| DMADRV removed from SDK | **2027.6.0** |

> All SiSDK components have migrated to DMA 2.0 starting with 2026.6.0. Keeping `dmadrv` in your application increases code size because DMADRV is pulled in solely for your application, in addition to the new components already used by the rest of the SDK.

---

## When to Use

Use this skill when:

- A source file includes `"dmadrv.h"`.
- A `.slcp` project file lists `id: dmadrv` in its `component` section.
- Source code contains calls to any `DMADRV_*` API.
- A user requests migration of an application to "DMA 2.0" or the "new DMA driver".

---

## Instructions

### Step 1 — Discover Affected Files

Search the application directory for all DMADRV usage before making any changes.

1. Find source files that include the legacy header:
   ```
   grep -r "#include \"dmadrv.h\"" app/
   ```

2. Find `.slcp` project files that declare the DMADRV component:
   ```
   grep -r "id: dmadrv" app/ --include="*.slcp"
   ```

3. Collect every `DMADRV_*` call site so nothing is missed:
   ```
   grep -rn "DMADRV_" app/ --include="*.c" --include="*.h"
   ```

Record the full list of files and call sites before proceeding.

---

### Step 2 — Update SLCP Component Declarations

In each affected `.slcp` file:

**Remove:**
```yaml
  - id: dmadrv
```

**Add:**
```yaml
  - id: dma_manager
  - id: dma_channel
```

If the application only needs channel allocation (e.g., it drives the channel using `sl_hal_ldma` or direct register access), you may omit `dma_channel` and add only `dma_manager`.

> The `dma_manager_init` and `dma_descriptor_allocator` components are automatically included as dependencies — do not add them explicitly.

#### LDMAXBAR clock when using `dma_manager` without `dma_channel`

The DMA Manager only enables the LDMA peripheral clock (`SL_BUS_CLOCK_LDMA0`). The LDMAXBAR crossbar — which routes peripheral trigger signals to DMA channels via `LDMAXBAR->CH[n].REQSEL` — has a **separate** clock (`SL_BUS_CLOCK_LDMAXBAR0`) that the DMA Manager does not enable on its own.

Without LDMAXBAR clocked, `sl_hal_ldma_init_transfer` will bus-fault on the first write to `LDMAXBAR->CH[n].REQSEL`, leaving all channel registers at zero and the transfer never starting.

When `dma_channel` is present, it handles LDMAXBAR internally. When using `sl_hal_ldma_*` directly (hybrid pattern), explicitly enable the LDMAXBAR0 clock in the application before calling `sl_hal_ldma_init_transfer`:

```c
// DMA Manager is auto-initialized via SL Main (LDMA clock, LDMA init, NVIC setup).
// LDMAXBAR has a separate bus clock that the DMA Manager does not enable. Without it,
// sl_hal_ldma_init_transfer faults when writing to LDMAXBAR->CH[n].REQSEL. Enable it
// manually whenever sl_hal_ldma_* is used directly instead of the dma_channel driver.
sl_clock_manager_enable_bus_clock(SL_BUS_CLOCK_LDMAXBAR0);

sl_dma_manager_reserve_channel(NULL, LDMA_CHANNEL);
```

**Example** — `segment_lcd_ldma/segment_lcd_app.c`:

```c
void segment_lcd_app_init(void)
{
  // ... LCD and buffer init ...

  // DMA Manager is auto-initialized via SL Main (LDMA clock, LDMA init, NVIC setup).
  // LDMAXBAR has a separate bus clock that the DMA Manager does not enable. Without it,
  // sl_hal_ldma_init_transfer faults when writing to LDMAXBAR->CH[n].REQSEL. Enable it
  // manually whenever sl_hal_ldma_* is used directly instead of the dma_channel driver.
  sl_clock_manager_enable_bus_clock(SL_BUS_CLOCK_LDMAXBAR0);

  // Reserve the fixed channel so the DMA Manager does not allocate it to others.
  sl_dma_manager_reserve_channel(NULL, LDMA_CHANNEL);

  // ... descriptor setup and sl_hal_ldma_init_transfer / sl_hal_ldma_start_transfer ...
}
```

`SL_BUS_CLOCK_LDMAXBAR0` is declared in `sl_device_clock.h`, which is included transitively via `sl_dma_manager.h`. The `clock_manager` component must be present in the `.slcp` (it already is when migrating from `hal_ldma`).

---

### Step 3 — Update Headers

In every `.c` / `.h` file that includes `"dmadrv.h"`:

**Remove:**
```c
#include "dmadrv.h"
```

**Add:**
```c
#include "sl_dma_manager.h"
#include "sl_dma_channel.h"
```

---

### Step 4 — Update Local Variable Types

DMADRV uses `unsigned int` for channel identifiers. DMA 2.0 uses `uint8_t` for channel numbers and `sl_dma_channel_handle_t` for the per-channel driver context.

**Before:**
```c
static unsigned int tx_channel, rx_channel;
```

**After:**
```c
static uint8_t tx_channel, rx_channel;
static sl_dma_channel_handle_t tx_handle, rx_handle;
```

---

### Step 5 — Update Initialization

The DMA Manager **auto-initializes via SL Main** (`sl_platform_init()` → `sl_dma_manager_instances_init()`). Remove all `DMADRV_Init()` calls — no manual init is needed. Similarly, remove `DMADRV_DeInit()` calls — there is no global deinit equivalent.

**Before:**
```c
void app_init(void)
{
  DMADRV_Init();

  Ecode_t status = DMADRV_AllocateChannel(&tx_channel, NULL);
  EFM_ASSERT(status == ECODE_EMDRV_DMADRV_OK);

  status = DMADRV_AllocateChannel(&rx_channel, NULL);
  EFM_ASSERT(status == ECODE_EMDRV_DMADRV_OK);
}
```

**After:**
```c
void app_init(void)
{
  // No init call needed — DMA Manager auto-initializes via SL Main.

  sl_status_t status = sl_dma_manager_allocate_channel(NULL, &tx_channel);
  EFM_ASSERT(status == SL_STATUS_OK);

  status = sl_dma_manager_allocate_channel(NULL, &rx_channel);
  EFM_ASSERT(status == SL_STATUS_OK);

  // Signature: sl_dma_channel_init(handle*, peripheral, channel_number, callback, user_data)
  // TX channel: no callback — transfer completion is polled via sl_dma_channel_get_status.
  status = sl_dma_channel_init(&tx_handle, SL_PERIPHERAL_LDMA0, tx_channel, NULL, NULL);
  EFM_ASSERT(status == SL_STATUS_OK);

  status = sl_dma_channel_init(&rx_handle, SL_PERIPHERAL_LDMA0, rx_channel, rx_callback, NULL);
  EFM_ASSERT(status == SL_STATUS_OK);

  // Set peripheral signals once; not repeated per transfer.
  // Verify exact signal names in sl_dma_signals.h for the target device.
  sl_dma_channel_set_peripheral_signal(&tx_handle, SL_DMA_SIGNAL_EUSART0_TXFL);
  sl_dma_channel_set_peripheral_signal(&rx_handle, SL_DMA_SIGNAL_EUSART0_RXFL);
}
```

Key differences:
- `DMADRV_Init()` removed entirely — DMA Manager is auto-initialized by SL Main.
- `sl_dma_channel_init` signature: `(handle*, peripheral, channel_number, callback, user_data)`.
- `sl_dma_channel_init` returns `sl_status_t` — check its return value.
- The peripheral signal is set once on the handle, not passed on every transfer call.
- Returns `sl_status_t` instead of `Ecode_t`.

---

### Step 6 — Update Transfer Calls

> **Important — Transfer size unit change:** DMADRV used **item count** for the `len` parameter. The new DMA Channel Driver uses **byte count** for `size`. Convert as follows:
> - `dmadrvDataSize1` (byte): `size = len × 1` (same value)
> - `dmadrvDataSize2` (halfword): `size = len × 2`
> - `dmadrvDataSize4` (word): `size = len × 4`

#### Memory → Peripheral

**Before:**
```c
DMADRV_MemoryPeripheral(tx_channel,
                        dmadrvPeripheralSignal_EUSART0_TXBL,
                        (void *)&(EUSART0->TXDATA),
                        tx_buffer,
                        true,               // srcInc
                        strlen(tx_buffer),  // item count
                        dmadrvDataSize1,
                        NULL,               // callback
                        NULL);              // user param
```

**After:**
```c
sl_dma_channel_submit_transfer_m2p(&tx_handle,
                                   tx_buffer,
                                   (void *)&(EUSART0->TXDATA),
                                   strlen(tx_buffer),      // byte count
                                   SL_DMA_CTRL_SIZE_BYTE,
                                   NULL);  // NULL = auto-allocate descriptor
```

#### Peripheral → Memory

**Before:**
```c
DMADRV_PeripheralMemory(rx_channel,
                        dmadrvPeripheralSignal_EUSART0_RXDATAV,
                        rx_buffer,
                        (void *)&(EUSART0->RXDATA),
                        true,
                        RX_BUFFER_SIZE,
                        dmadrvDataSize1,
                        rx_callback,
                        NULL);
```

**After:**
```c
sl_dma_channel_submit_transfer_p2m(&rx_handle,
                                   (void *)&(EUSART0->RXDATA),
                                   rx_buffer,
                                   RX_BUFFER_SIZE,
                                   SL_DMA_CTRL_SIZE_BYTE,
                                   NULL);
```

#### Memory → Memory (new in DMA 2.0)

DMADRV had no dedicated M2M API. DMA 2.0 adds one with automatic unit-size selection and large-transfer segmentation:

```c
sl_dma_channel_submit_transfer_m2m(&handle,
                                   src_buffer,
                                   dst_buffer,
                                   size,   // byte count
                                   NULL);
```

#### Ping-Pong Transfers

Dedicated `sl_dma_channel_submit_ping_pong_transfer_m2p` / `_p2m` APIs are available. The callback is still invoked on each buffer completion, but its return type is `void` — call `sl_dma_channel_abort()` to stop instead of returning `false`.

**Before:**
```c
DMADRV_PeripheralMemoryPingPong(dma_channel,
                                dmadrvPeripheralSignal_EUSART0_RXDATAV,
                                rx_buf0,
                                rx_buf1,
                                (void *)&(EUSART0->RXDATA),
                                true,
                                BUFFER_SIZE,
                                dmadrvDataSize1,
                                pingpong_callback,
                                NULL);
```

**After:**
```c
// Signal already set at init: sl_dma_channel_set_peripheral_signal(&dma_handle, SL_DMA_SIGNAL_EUSART0_RXFL)
sl_dma_channel_submit_ping_pong_transfer_p2m(
    &dma_handle,
    (void *)&(EUSART0->RXDATA),
    rx_buf0,
    rx_buf1,
    BUFFER_SIZE,           // byte count
    SL_DMA_CTRL_SIZE_BYTE,
    NULL);                 // auto-allocate descriptors
```

To stop ping-pong, call `sl_dma_channel_abort(&handle)` from within the callback (or any other context) instead of returning `false`.

#### API Mapping Summary

| DMADRV API | DMA 2.0 API |
|---|---|
| `DMADRV_Init()` | *(remove — DMA Manager auto-inits via SL Main)* |
| `DMADRV_AllocateChannel(&ch, NULL)` | `sl_dma_manager_allocate_channel(NULL, &ch)` |
| `DMADRV_AllocateChannelById(ch, NULL)` | `sl_dma_manager_reserve_channel(NULL, ch)` |
| `DMADRV_FreeChannel(ch)` | `sl_dma_channel_deinit(&handle)` + `sl_dma_manager_free_channel(NULL, ch)` |
| `DMADRV_DeInit()` | *(remove — no global deinit)* |
| `DMADRV_MemoryPeripheral(...)` | `sl_dma_channel_submit_transfer_m2p(...)` |
| `DMADRV_PeripheralMemory(...)` | `sl_dma_channel_submit_transfer_p2m(...)` |
| `DMADRV_MemoryPeripheralPingPong(...)` | `sl_dma_channel_submit_ping_pong_transfer_m2p(...)` |
| `DMADRV_PeripheralMemoryPingPong(...)` | `sl_dma_channel_submit_ping_pong_transfer_p2m(...)` |
| *(no equivalent)* | `sl_dma_channel_submit_transfer_m2m(...)` |
| *(no equivalent)* | `sl_dma_channel_submit_transfer_list(...)` *(also covers triple-buffer)* |
| `DMADRV_TransferActive(ch, &active)` | `sl_dma_channel_get_status(&handle, &status)` → `status.enabled` |
| `DMADRV_TransferDone(ch, &done)` | `sl_dma_channel_get_status(&handle, &status)` → `!status.active` |
| `DMADRV_TransferRemainingCount(ch, &remaining)` | `sl_dma_channel_get_status(&handle, &status)` → compute `total - status.bytes_completed` |
| `DMADRV_StopTransfer(ch)` | `sl_dma_channel_abort(&handle)` *(invokes callbacks with `aborted=true`)* |
| `DMADRV_PauseTransfer(ch)` | `sl_dma_channel_suspend(&handle)` *(suspends peripheral handshake only)* |
| `DMADRV_ResumeTransfer(ch)` | `sl_dma_channel_resume(&handle)` *(resumes peripheral handshake)* |

---

### Step 7 — Update Callbacks

Callbacks in DMADRV are passed per-transfer and return `bool`. In DMA 2.0, the callback is registered once at `sl_dma_channel_init` and returns `void`.

**Before:**
```c
static bool rx_callback(unsigned int channel,
                        unsigned int sequence_no,
                        void *user_param)
{
  (void)channel;
  (void)sequence_no;
  (void)user_param;
  rx_transfer_complete = true;
  return true;  // unused for simple transfers; controls ping-pong continuation
}
```

**After:**
```c
static void rx_callback(sl_dma_channel_handle_t *handle,
                        void *user_data,
                        bool error,
                        bool aborted)
{
  (void)handle;
  (void)user_data;

  if (error) {
    // Channel is now disabled after a hardware error.
    return;
  }
  if (aborted) {
    // Transfer was stopped via sl_dma_channel_abort().
    return;
  }
  rx_transfer_complete = true;
}
```

For ping-pong transfers, the callback fires on each buffer completion (matching DMADRV behavior). There is no return value to control continuation; call `sl_dma_channel_abort(handle)` to stop.

For RTOS applications using semaphores from ISR context, the FreeRTOS pattern becomes:

**Before:**
```c
static bool rx_callback(unsigned int channel, unsigned int sequence_no,
                        void *user_param)
{
  BaseType_t task_woken = pdFALSE;
  xSemaphoreGiveFromISR(dma_rx_complete, &task_woken);
  return task_woken == pdTRUE;
}
```

**After:**
```c
static void rx_callback(sl_dma_channel_handle_t *handle, void *user_data,
                        bool error, bool aborted)
{
  (void)handle;
  (void)user_data;
  if (error || aborted) {
    return;
  }
  BaseType_t task_woken = pdFALSE;
  xSemaphoreGiveFromISR(dma_rx_complete, &task_woken);
  portYIELD_FROM_ISR(task_woken);
}
```

---

### Step 8 — Update Transfer Status Checks

**Before:**
```c
bool active;
DMADRV_TransferActive(tx_channel, &active);
while (active) {
  sl_sleeptimer_delay_millisecond(1);
  DMADRV_TransferActive(tx_channel, &active);
}
```

**After:**
```c
sl_dma_channel_status_t status;
sl_dma_channel_get_status(&tx_handle, &status);
while (status.active) {
  sl_sleeptimer_delay_millisecond(1);
  sl_dma_channel_get_status(&tx_handle, &status);
}
```

`sl_dma_channel_status_t` exposes three fields:
- `enabled` — channel is enabled (equivalent to DMADRV "transfer active" — channel is enabled, may be waiting for peripheral signal)
- `active` — data is actively moving (no peripheral signal wait); `!active` is equivalent to DMADRV "transfer done"
- `bytes_completed` — bytes completed in the current active descriptor (DMADRV returned items remaining; calculate remaining as `total_bytes - bytes_completed`)

---

### Step 9 — Update Cleanup

**Before:**
```c
void app_cleanup(void)
{
  DMADRV_StopTransfer(tx_channel);
  DMADRV_StopTransfer(rx_channel);
  DMADRV_FreeChannel(tx_channel);
  DMADRV_FreeChannel(rx_channel);
  DMADRV_DeInit();
}
```

**After:**
```c
void app_cleanup(void)
{
  // Abort any active transfers — triggers callbacks with aborted=true.
  sl_dma_channel_abort(&tx_handle);
  sl_dma_channel_abort(&rx_handle);

  // Deinit the channel driver handle (returns SL_STATUS_BUSY if still enabled).
  sl_dma_channel_deinit(&tx_handle);
  sl_dma_channel_deinit(&rx_handle);

  // Return channels to the DMA Manager.
  sl_dma_manager_free_channel(NULL, tx_channel);
  sl_dma_manager_free_channel(NULL, rx_channel);

  // There is no global deinit for the DMA Manager.
}
```

Key differences:
- `sl_dma_channel_abort` stops any active transfer and invokes callbacks with `aborted=true` for each pending descriptor.
- `sl_dma_channel_deinit` returns `SL_STATUS_BUSY` if the channel is still enabled. Always abort before deiniting.
- There is no `DMADRV_DeInit()` equivalent. The DMA Manager remains initialized for the lifetime of the application.

---

## Common Pitfalls

### 1. Channel ID type

`unsigned int` → `uint8_t`. Update all declarations, struct fields, and function parameters that store a channel number.

### 2. Error code type and value

`Ecode_t` / `ECODE_EMDRV_DMADRV_OK` → `sl_status_t` / `SL_STATUS_OK`.

### 3. Data size enum rename and unit change

| DMADRV | DMA 2.0 constant | Transfer size |
|---|---|---|
| `dmadrvDataSize1` (1 byte) | `SL_DMA_CTRL_SIZE_BYTE` | `size = len × 1` |
| `dmadrvDataSize2` (2 bytes) | `SL_DMA_CTRL_SIZE_HALF` | `size = len × 2` |
| `dmadrvDataSize4` (4 bytes) | `SL_DMA_CTRL_SIZE_WORD` | `size = len × 4` |

DMADRV took an **item count**; DMA 2.0 takes a **byte count**. If you used `dmadrvDataSize1`, the numeric value is unchanged. For halfword or word transfers, multiply the item count accordingly.

### 4. Peripheral signal rename and type change

DMADRV signal names follow `dmadrvPeripheralSignal_<PERIPH>_<SIGNAL>`. DMA 2.0 uses `SL_DMA_SIGNAL_<PERIPH>_<SIGNAL>`. Note specific renames for EUSART:

| Legacy Signal | New Signal |
|---|---|
| `dmadrvPeripheralSignal_USART0_TXBL` | `SL_DMA_SIGNAL_USART0_TXBL` |
| `dmadrvPeripheralSignal_EUSART0_TXBL` | `SL_DMA_SIGNAL_EUSART0_TXFL` (`TXBL` → `TXFL`) |
| `dmadrvPeripheralSignal_EUSART0_RXDATAV` | `SL_DMA_SIGNAL_EUSART0_RXFL` (`RXDATAV` → `RXFL`) |

The type also changes: `DMADRV_PeripheralSignal_t` (`uint32_t`, by value) → `sl_dma_signal_t` (`const uint32_t*`, by pointer). Signal constants are defined in `sl_device_dma.h` (auto-included via `sl_dma_manager.h`).

Verify the exact signal name for the target peripheral in `sl_dma_signals.h`.

### 5. Peripheral signal is set per-handle, not per-transfer

Call `sl_dma_channel_set_peripheral_signal()` once during initialization. Do not call it repeatedly before each transfer unless the peripheral is actually changing. The function returns `SL_STATUS_BUSY` if there are pending transfers on the channel.

### 6. Callback is registered at init, not per-transfer

Remove the callback and user-parameter arguments from every transfer submit call. Pass them to `sl_dma_channel_init` instead.

### 7. Callback return value and ping-pong stop

DMADRV callbacks return `bool` (returning `false` stopped ping-pong). DMA 2.0 callbacks return `void`. To stop a ping-pong transfer, call `sl_dma_channel_abort(handle)` from within the callback or from any other context.

### 8. `DMADRV_StopTransfer` now triggers callbacks

`DMADRV_StopTransfer()` silently stopped the channel. The new `sl_dma_channel_abort()` stops the channel **and** invokes callbacks for each pending descriptor with `aborted=true`. Ensure your callbacks handle the `aborted=true` case before calling abort.

### 9. Channel teardown sequence

To tear down a channel completely:
1. Call `sl_dma_channel_abort(&handle)` to stop any active transfer.
2. Call `sl_dma_channel_deinit(&handle)` — returns `SL_STATUS_BUSY` if channel is still enabled.
3. Call `sl_dma_manager_free_channel(NULL, channel_nbr)` to return the channel to the pool.

### 10. No global DMA Manager deinit

`DMADRV_DeInit()` has no DMA 2.0 equivalent. Remove it. The DMA Manager remains initialized for the lifetime of the application.

---

## Advanced: Transfer Status Polling Without a Callback

For TX-only channels where the application polls completion, skip the callback entirely:

```c
sl_dma_channel_init(&tx_handle, SL_PERIPHERAL_LDMA0, tx_channel, NULL, NULL);

// ... submit transfer ...

sl_dma_channel_status_t status;
sl_dma_channel_get_status(&tx_handle, &status);
while (status.active) {
  sl_sleeptimer_delay_millisecond(1);
  sl_dma_channel_get_status(&tx_handle, &status);
}
```

---

## Advanced: Channel Allocation with Properties

If an application requires specific channel capabilities, use the extended allocator instead of the basic one:

```c
// Request a high-priority channel that supports interleaving.
sl_status_t status = sl_dma_manager_allocate_channel_with_properties(
    NULL,
    SL_DMA_CHANNEL_HIGH_PRIORITY | SL_DMA_CHANNEL_SUPPORTS_INTERLEAVING,
    &channel_nbr
);
```

Available property flags:

| Flag | Effect |
|---|---|
| `SL_DMA_CHANNEL_HIGH_PRIORITY` | Allocates from low-numbered (high-priority) fixed-priority channels first |
| `SL_DMA_CHANNEL_USES_ROUND_ROBIN` | Allocates from the round-robin range |
| `SL_DMA_CHANNEL_SUPPORTS_INTERLEAVING` | Allocates only channels that support interleaving (MMLDMA/XDMA); fails if none available |
| `SL_DMA_CHANNEL_SUPPORTS_DUAL_DESTINATION` | Allocates only channels that support dual-destination (XDMA only); fails if none available |

`SL_DMA_CHANNEL_HIGH_PRIORITY` and `SL_DMA_CHANNEL_USES_ROUND_ROBIN` are mutually exclusive. Combining them triggers an assert.

---

## Advanced: Transfer Lists

DMA 2.0 introduces a high-level API for arbitrary linked transfer chains — not supported by DMADRV. Transfer lists are also usable for triple-buffer patterns.

Key constraints:
- The `next` pointer of the **last** element must be `NULL`. Transfer lists must not form loops.
- To simulate continuous ping-pong, re-submit the list from the callback.

```c
sl_dma_channel_transfer_t transfers[3];

transfers[0].source                = src1;
transfers[0].destination           = dst1;
transfers[0].size                  = size1;
transfers[0].unit_size             = SL_DMA_CTRL_SIZE_BYTE;
transfers[0].increment_source      = true;
transfers[0].increment_destination = true;
transfers[0].block_handshake_mode  = false;
transfers[0].callback_on_complete  = false;
transfers[0].descriptor            = NULL;
transfers[0].next                  = &transfers[1];

transfers[1].source                = src2;
transfers[1].destination           = dst2;
transfers[1].size                  = size2;
transfers[1].unit_size             = SL_DMA_CTRL_SIZE_BYTE;
transfers[1].increment_source      = true;
transfers[1].increment_destination = true;
transfers[1].block_handshake_mode  = false;
transfers[1].callback_on_complete  = true;   // fire callback after this transfer
transfers[1].descriptor            = NULL;
transfers[1].next                  = &transfers[2];

transfers[2].source                = src3;
transfers[2].destination           = dst3;
transfers[2].size                  = size3;
transfers[2].unit_size             = SL_DMA_CTRL_SIZE_BYTE;
transfers[2].increment_source      = true;
transfers[2].increment_destination = true;
transfers[2].block_handshake_mode  = false;
transfers[2].callback_on_complete  = true;
transfers[2].descriptor            = NULL;
transfers[2].next                  = NULL;   // end of list

sl_dma_channel_submit_transfer_list(&handle, &transfers[0]);
```


