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 fromcomponents/platform/hosal/include/hosal_dma.hand the low-levelbl602_dma.h; official DMA usage can be found inapplications/peripherals/spi_ws2812(SPI send uses DMA + LLI linked list).
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:
cdis the “change directory” command; this borrows the official blink example skeleton as this tutorial’s project. All subsequentmakebuild andmake flashcommands must run inside this directory first.
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 toDMA_MINC_DISABLE(fixed address), see the official spi_ws2812 example.
Build in the project directory:
make -j8
Note:
makeis the “build” command, turning code into firmware (a program file) the board can run;-j8builds with 8 parallel cores, faster.
On success a firmware build_out/blink.bin is generated.
Keep the board connected via USB, confirm the serial device, and flash:
make flash p=/dev/ttyUSB0 b=921600
Note:
make flashis the “flash” command, writing the compiled firmware into the board’s chip. Afterp=comes the serial device (change it to your computer’s actual port — check withls /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.
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 thememcmpcheck, 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 withDMA_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 thehosal_dma_chan_requestreturn 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 numberpfn: callback function pointer, shapedvoid cb(void *arg, uint32_t flag), requiredp_arg: callback argument pointer; passNULLif 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_Typestruct pointer.dir(transfer direction, values:DMA_TRNS_M2Mmemory-to-memory /DMA_TRNS_M2Pmemory-to-peripheral /DMA_TRNS_P2Mperipheral-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 numberLLI: linked list struct pointer (DMA_LLI_Ctrl_Typearray; with multiple segments,nextpoints 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
#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.

