Skip to content

Overview

DMA (Direct Memory Access) lets peripherals and memory, or memory and memory, move data directly without the CPU, notifying the CPU via an interrupt when done — greatly improving the efficiency of large data transfers (serial, SPI, ADC sampling, etc.). This tutorial completes one memory-to-memory transfer via DMA and verifies the result over serial.

In plain words: DMA is like a dedicated "porter". Ordinary copying has the boss (CPU) hauling boxes himself, and while moving data he can't do anything else; DMA lets a porter do the hauling, and when done the porter calls out "done!" (interrupt), and the boss comes over to check. This tutorial hires a porter to move a box of 64 bytes from warehouse A to warehouse B, then compares the goods on both sides.

This tutorial is based on the official Ai-Thinker SDK (Ai-Thinker-Open/Ai-Thinker-WB2, version release_bl_iot_sdk_1.6.40) DMA APIs. The DMA capability comes from components/platform/hosal/include/hosal_dma.h and the low-level bl602_dma.h; official DMA usage can be found in applications/peripherals/spi_ws2812 (SPI send uses DMA + LLI linked list).

🎯Page GoalMove 64 bytes from a source buffer to a destination buffer via DMA, print the result over serial, and learn channel request, LLI linked list configuration and the start flow.
🧰Prerequisites① Ai-WB2 development board (Type-C data cable) ② Environment set up per [SDK Installation](../sdk/sdk_intro) and [GPIO Output (Blink LED)](./gpio_led) completed.
🔗RelatedDMA is commonly used for large serial/SPI transfers, see [UART (Serial)](./uart) and [SPI Protocol](./spi).

Enter the Example Project

The official SDK has no standalone DMA transfer example, so this tutorial rewrites the official blink example skeleton (main/main.c + Makefile), with the DMA call sequence identical to the official spi_ws2812 example:

cd ~/Ai-Thinker-WB2/applications/get-started/blink

Note: cd is the “change directory” command; this borrows the official blink example skeleton as this tutorial’s project. All subsequent make build and make flash commands must run inside this directory first.

Write the Code

Replace main/main.c with the following. The full code for this step has been moved to the end of this page:

📜 Full Code — in the “Full Code” section below, collapsed by default — click to expand. This tutorial’s rewritten code is based on the official blink skeleton (applications/get-started/blink); the DMA call sequence matches the official example (applications/peripherals/spi_ws2812/app/main.c) exactly.

Code highlights:

Code Purpose
hosal_dma_chan_request(0) Request a DMA channel (assign the porter a number); failing means another module has taken the channels
dma_lli_list_init(src, dst, length) Configure the LLI linked list (the move list): source address, destination address, transfer length and address increment mode; a wrong list moves data to the wrong place
DMA_LLI_Init(dma_chan, &llicfg) Initialize LLI mode, dir = DMA_TRNS_M2M specifies memory-to-memory transfer; a wrong direction moves data the wrong way
DMA_LLI_Update(dma_chan, (uint32_t)dma_lli) Load the move list into the DMA channel; without loading it the channel doesn’t know what to do
hosal_dma_irq_callback_set(...) Register the transfer-complete interrupt callback (the porter calls out “done!”); without it you never know when it’s done
hosal_dma_chan_start(dma_chan) Start the transfer; the CPU is then free, and the interrupt callback runs on completion; without it nothing ever moves
memcmp(src_buf, dst_buf, ...) Compare the source/destination buffers byte by byte to verify the transfer result; a mismatch means the transfer went wrong

💡 For memory-to-memory transfers set both source/destination addresses to DMA_MINC_ENABLE (increment); when moving to a peripheral (e.g. SPI FIFO), set the destination address to DMA_MINC_DISABLE (fixed address), see the official spi_ws2812 example.

Build the Project

Build in the project directory:

make -j8

Note: make is the “build” command, turning code into firmware (a program file) the board can run; -j8 builds with 8 parallel cores, faster.

On success a firmware build_out/blink.bin is generated.

Flash the Firmware

Keep the board connected via USB, confirm the serial device, and flash:

make flash p=/dev/ttyUSB0 b=921600

Note: make flash is the “flash” command, writing the compiled firmware into the board’s chip. After p= comes the serial device (change it to your computer’s actual port — check with ls /dev/ttyUSB*); b= is the flash baud rate (transfer speed).

⏳ During flashing, press and hold the EN button on the board when prompted to enter download mode; wait for the progress bar to complete — that means the flash succeeded.

Run and Verify

After flashing, the board automatically restarts. Open a serial assistant (baud rate 921600) and check the logs:

DMA memcpy OK, length = 64

DMA memcpy OK printed means the 64 bytes were moved from the source buffer to the destination buffer by DMA with identical contents.

Seeing DMA memcpy OK, length = 64 printed means success; if DMA memcpy FAIL prints or there’s no log, check the “FAQ & Troubleshooting” section at the end.

💡 Try increasing TransferSize (e.g. 4096) and commenting out the memcmp check, printing a timestamp before the transfer instead — you’ll feel how moving large blocks of data via DMA barely takes CPU time.


API Summary for This Tutorial

hosal_dma_init

Enables the DMA controller; call once before requesting channels.

Return: 0 on success; negative error code on failure

hosal_dma_chan_request(flag)

Requests a DMA channel from the free channel pool, exclusively owned by this task (this tutorial requests normal channel 0).

Parameters:

  • flag: request flag, values: HOSAL_DMA_TYPE_NORMAL (normal channel) / HOSAL_DMA_TYPE_LLI (LLI linked list channel, pairs with DMA_LLI_Init)

Return: channel number (0~7) on success; negative error code on failure

hosal_dma_chan_start(chan)

Starts transferring data per the configured source/destination addresses.

Parameters:

  • chan: channel number (from the hosal_dma_chan_request return value)

Return: 0 on success; negative error code on failure

hosal_dma_chan_stop(chan)

Aborts the transfer task currently in progress.

Parameters:

  • chan: channel number

Return: 0 on success; negative error code on failure

hosal_dma_irq_callback_set(chan, pfn, p_arg)

Calls the callback from the interrupt on transfer completion or error (this tutorial sets dma_txing to 0 in the callback to mark transfer done).

Parameters:

  • chan: channel number
  • pfn: callback function pointer, shaped void cb(void *arg, uint32_t flag), required
  • p_arg: callback argument pointer; pass NULL if none

Return: 0 on success; negative error code on failure

hosal_dma_chan_release(chan)

Returns the channel to the free pool for other modules to reuse (the channel can't be used after release).

Parameters:

  • chan: channel number

Return: 0 on success; negative error code on failure

DMA_LLI_Init(ch, lliCfg)

Configures linked-list transfer: direction, width, source/destination addresses, etc. (LLI can chain multiple segments for continuous transfer; this tutorial uses dir = DMA_TRNS_M2M memory-to-memory).

Parameters:

  • ch: DMA channel number (DMA_CH0~DMA_CH7)
  • lliCfg: DMA_LLI_Cfg_Type struct pointer. dir (transfer direction, values: DMA_TRNS_M2M memory-to-memory / DMA_TRNS_M2P memory-to-peripheral / DMA_TRNS_P2M peripheral-to-memory), width (width, values: DMA_TRNS_WIDTH_8BITS/16BITS/32BITS)

Return: none

DMA_LLI_Update(ch, LLI)

Loads the prepared linked list head address into the channel; starting the channel then executes per the list.

Parameters:

  • ch: DMA channel number
  • LLI: linked list struct pointer (DMA_LLI_Ctrl_Type array; with multiple segments, next points to the next segment)

Return: none


Full Code

Below is the complete rewritten main/main.c source. The official SDK has no standalone DMA transfer example; this code is based on the official blink skeleton (applications/get-started/blink), and the DMA call sequence matches the official example (applications/peripherals/spi_ws2812/app/main.c) exactly:

📜 Click to expand the full main/main.c code
c
#include <stdio.h>
#include <string.h>
#include <FreeRTOS.h>
#include <task.h>
#include <blog.h>
#include <hosal_dma.h>
#include <bl602_dma.h>

#define DMA_LLI_CNT 1

static hosal_dma_chan_t dma_chan;
static DMA_LLI_Ctrl_Type dma_lli[DMA_LLI_CNT];
static volatile int dma_txing;

static uint8_t src_buf[64];
static uint8_t dst_buf[64];

/* DMA 搬运完成中断回调 */
static void dma_int_handler(void *arg, uint32_t flag)
{
    dma_txing = 0;
}

/* 配置 LLI 链表:源地址、目的地址、搬运长度 */
static void dma_lli_list_init(uint8_t *src, uint8_t *dst, uint32_t length)
{
    struct DMA_Control_Reg dmactrl;

    dmactrl.SBSize = DMA_BURST_SIZE_1;
    dmactrl.DBSize = DMA_BURST_SIZE_1;
    dmactrl.SWidth = DMA_TRNS_WIDTH_8BITS;
    dmactrl.DWidth = DMA_TRNS_WIDTH_8BITS;
    dmactrl.Prot = 0;
    dmactrl.SLargerD = 0;

    dmactrl.TransferSize = length;
    dmactrl.I = 0;

    dmactrl.SI = DMA_MINC_ENABLE;   /* 源地址自增 */
    dmactrl.DI = DMA_MINC_ENABLE;   /* 目的地址自增 */

    dma_lli[0].srcDmaAddr = (uint32_t)(src);
    dma_lli[0].destDmaAddr = (uint32_t)(dst);
    dma_lli[0].dmaCtrl = dmactrl;
    dma_lli[0].nextLLI = 0;
}

/* 内存到内存搬运 */
static void dma_mem_to_mem(uint8_t *src, uint8_t *dst, uint32_t length)
{
    DMA_LLI_Cfg_Type llicfg;

    llicfg.dir = DMA_TRNS_M2M;      /* 内存到内存 */
    llicfg.srcPeriph = DMA_REQ_NONE;
    llicfg.dstPeriph = DMA_REQ_NONE;

    dma_lli_list_init(src, dst, length);

    DMA_LLI_Init(dma_chan, &llicfg);
    DMA_LLI_Update(dma_chan, (uint32_t)dma_lli);
    hosal_dma_irq_callback_set(dma_chan, dma_int_handler, NULL);

    dma_txing = 1;
    hosal_dma_chan_start(dma_chan);
}

void main(void)
{
    int i;

    /* 源缓冲区填充 0~63,目的缓冲区清零 */
    for (i = 0; i < sizeof(src_buf); i++) {
        src_buf[i] = i;
    }
    memset(dst_buf, 0, sizeof(dst_buf));

    /* 申请 DMA 通道 */
    dma_chan = hosal_dma_chan_request(0);

    /* 启动内存搬运 */
    dma_mem_to_mem(src_buf, dst_buf, sizeof(src_buf));

    /* 等待搬运完成 */
    while (dma_txing) {
        ;
    }

    /* 逐字节比较搬运结果 */
    if (memcmp(src_buf, dst_buf, sizeof(src_buf)) == 0) {
        blog_info("DMA memcpy OK, length = %d", (int)sizeof(src_buf));
    } else {
        blog_info("DMA memcpy FAIL");
    }

    for (;;) {
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

FAQ & Troubleshooting

⚠️ Transfer result mismatch (some data lost)
Cause: width setting doesn't match the actual data (8-bit data moved with 32-bit width), or source/destination addresses not aligned
Fix: use DMA_TRNS_WIDTH_8BITS for 8-bit data; make the destination buffer long enough and 4-byte aligned (add __attribute__((aligned(4))))

⚠️ Channel request fails
Cause: DMA channels are occupied by other modules (e.g. SPI, UART interrupt send/receive mode)
Fix: confirm no other module in the project has requested a DMA channel; release unused channels with hosal_dma_chan_release

⚠️ Wrong address increment setting when moving to a peripheral
Cause: a peripheral FIFO is a fixed address; DMA_MINC_ENABLE would write out of bounds
Fix: set the destination address to DMA_MINC_DISABLE when moving to a peripheral (e.g. SPI FIFO); for memory-to-memory transfers set both sides to DMA_MINC_ENABLE

⚠️ Flashing keeps waiting, the progress bar doesn't move
Cause: download mode wasn't entered, or the cable only charges and can't transfer data
Fix: press and hold EN to enter download mode when prompted; try a Type-C cable that can transfer data and retry

⚠️ Serial device not found or permission denied
Cause: /dev/ttyUSB0 doesn't exist or permissions are insufficient on Linux; USB-to-serial driver not installed on Windows
Fix: on Linux confirm the device with ls /dev/ttyUSB*, for permissions run sudo usermod -aG dialout $USER then log in again; on Windows install the driver in Device Manager and confirm the COM number

⚠️ Running make reports no Makefile found
Cause: the build command ran in the wrong directory (it must run inside the example project)
Fix: first cd ~/Ai-Thinker-WB2/applications/get-started/blink into the project directory, then run make -j8

Self-Check

The serial prints DMA memcpy OK, length = 64 — the DMA transfer is verified.

Released under the MIT License. Build Time 2026-09-11 14:52:23