3、音频DMA引擎:DMA控制器配置、音频DMA传输模式、环形缓冲区管理
音频数据流处理,说白了就是一场与时间的赛跑。CPU 如果亲自去搬运每一个音频采样点,那基本上就别干别的了。所以,DMA 引擎就是那个帮你干苦力活的「搬运工」。我刚开始接触 Zephyr 音频子系统时,最头疼的就是 DMA 配置。配置不对,声音要么断断续续,要么直接没声。
这一节,我们就来聊聊 DMA 控制器怎么配、音频传输有哪些模式、以及环形缓冲区这个经典数据结构怎么玩。嗯,这些都是实战中绕不开的硬骨头。
3.1 DMA 控制器配置
在 Zephyr 里,DMA 控制器是通过设备树(Device Tree)来描述的。我个人习惯是先看芯片手册,再对着设备树改。你想想看,如果连硬件支持几个 DMA 通道都不清楚,代码写得再漂亮也是白搭。
先看一个典型的 DMA 配置示例:
// 在设备树中定义 DMA 控制器
dma0: dma@40020000 {
compatible = "st,stm32-dma";
reg = <0x40020000 0x400>;
interrupts = <11 0>, <12 0>, <13 0>, <14 0>,
<15 0>, <16 0>, <17 0>, <18 0>;
dma-channels = <8>;
#dma-cells = <2>;
status = "okay";
};
// 音频外设引用 DMA
&i2s1 {
dmas = <&dma0 3 0x2040>, // 发送通道
<&dma0 4 0x2040>; // 接收通道
dma-names = "tx", "rx";
status = "okay";
};
这里有个坑,我踩过好几次。dma-cells 里的第二个参数,不同芯片厂商定义完全不同。ST 的芯片用 16 位编码表示优先级、方向、数据宽度等。NXP 的 i.MX 系列又是另一套规则。所以,一定要看对应 SoC 的 binding 文档。
在代码层面,初始化 DMA 的流程大致如下:
#include <zephyr/drivers/dma.h>
const struct device *dma_dev;
struct dma_config dma_cfg = {0};
void audio_dma_init(void)
{
dma_dev = DEVICE_DT_GET(DT_NODELABEL(dma0));
if (!device_is_ready(dma_dev)) {
printk("DMA device not ready\n");
return;
}
// 配置 DMA 传输参数
dma_cfg.channel_direction = MEMORY_TO_PERIPHERAL;
dma_cfg.source_data_size = 2; // 16-bit 音频数据
dma_cfg.dest_data_size = 2;
dma_cfg.source_burst_length = 4; // 每次突发传输 4 个采样点
dma_cfg.dest_burst_length = 4;
dma_cfg.block_count = 1;
dma_cfg.user_data = NULL;
// 配置完成后,启动 DMA
dma_config(dma_dev, 3, &dma_cfg); // 通道 3 用于 I2S TX
}
我个人习惯把 burst_length 设成 4 或 8。设得太小,DMA 频繁中断,CPU 负担重;设得太大,延迟会变高。对于 48kHz 的音频,4 个采样点也就 83 微秒的延迟,完全可以接受。
3.2 音频 DMA 传输模式
音频 DMA 传输模式,说白了就两种:单次传输和循环传输。但在实际项目中,我们几乎只用循环传输。为什么?因为音频流是连续的,你不可能每播完一段就停下来重新配置一次。
Zephyr 的 DMA API 支持两种模式:
| 模式 | 描述 | 适用场景 |
|---|---|---|
| 单次传输 | 传输完指定数据量后停止 | 短音频提示音、按键音 |
| 循环传输 | 传输完成后自动重新开始 | 音乐播放、实时语音 |
配置循环传输时,关键是要设置好回调函数。DMA 每完成一个 block 的传输,就会触发一次中断,回调函数里你就可以填充新的数据或者处理已接收的数据。
static void dma_callback(const struct device *dma_dev,
void *user_data,
uint32_t channel,
int status)
{
if (status < 0) {
printk("DMA transfer error: %d\n", status);
return;
}
// 这里处理音频数据
// 比如:从环形缓冲区读取新数据填充到 DMA 源地址
audio_buffer_refill();
}
void audio_dma_start_cyclic(void)
{
struct dma_block_config block_cfg = {0};
// 配置循环传输
dma_cfg.complete_callback_en = 1;
dma_cfg.error_callback_en = 1;
dma_cfg.user_data = NULL;
// 设置 block 参数
block_cfg.source_address = (uint32_t)audio_tx_buffer;
block_cfg.dest_address = (uint32_t)&I2S->DR; // 外设地址
block_cfg.block_size = AUDIO_BLOCK_SIZE; // 每次传输的字节数
dma_cfg.head_block = &block_cfg;
// 启动循环传输
dma_config(dma_dev, 3, &dma_cfg);
dma_start(dma_dev, 3);
}
3.3 环形缓冲区管理
环形缓冲区,也叫循环缓冲区,是音频处理中最基础的数据结构。它的核心思想是:用一块固定大小的内存,通过两个指针(读指针和写指针)来管理数据,写满了就从头开始覆盖。
我刚开始做音频项目时,自己手写了一个环形缓冲区,结果各种边界条件没处理好,不是读到了脏数据,就是写指针追上了读指针。后来我直接用 Zephyr 提供的 ring_buffer 模块,省心多了。
Zephyr 的环形缓冲区 API 使用起来很简单:
#include <zephyr/sys/ring_buffer.h>
#define AUDIO_RING_BUF_SIZE 4096 // 4KB 缓冲区
uint8_t ring_buf_data[AUDIO_RING_BUF_SIZE];
struct ring_buf audio_ring;
void audio_ring_init(void)
{
ring_buf_init(&audio_ring, sizeof(ring_buf_data), ring_buf_data);
}
// 生产者(比如 DMA 回调)往缓冲区写数据
int audio_ring_put(uint8_t *data, size_t len)
{
return ring_buf_put(&audio_ring, data, len);
}
// 消费者(比如音频处理线程)从缓冲区读数据
int audio_ring_get(uint8_t *data, size_t len)
{
return ring_buf_get(&audio_ring, data, len);
}
这里有个关键点:环形缓冲区的大小必须是 2 的幂次方。Zephyr 的 ring_buffer 实现内部用了位运算来取模,如果不是 2 的幂次方,性能会下降,而且可能出现 bug。我建议设成 4096 或 8192,对于 48kHz 16-bit 双声道音频,4096 字节可以存约 64 毫秒的数据,足够应付大部分场景。
实际项目中,我通常会用双缓冲区的思路来配合环形缓冲区。DMA 在传输一块数据时,CPU 在处理另一块数据。这样流水线作业,效率最高。
// 双缓冲区 + 环形缓冲区的配合使用
#define NUM_DMA_BUFFERS 2
uint8_t dma_buffers[NUM_DMA_BUFFERS][AUDIO_BLOCK_SIZE];
int current_buffer = 0;
void dma_callback(...)
{
// DMA 传输完成,将当前缓冲区数据放入环形缓冲区
ring_buf_put(&audio_ring, dma_buffers[current_buffer], AUDIO_BLOCK_SIZE);
// 切换到下一个缓冲区
current_buffer = (current_buffer + 1) % NUM_DMA_BUFFERS;
// 更新 DMA 源地址
dma_block_cfg.source_address = (uint32_t)dma_buffers[current_buffer];
dma_config(dma_dev, 3, &dma_cfg);
}
嗯,这里要注意:DMA 正在传输的缓冲区,绝对不能去修改里面的数据。否则你会听到各种奇怪的噪音。我当初调试时,就是因为没处理好这个同步问题,折腾了两天才找到原因。
最后总结一下:DMA 配置要仔细看芯片手册,循环传输是音频的主流模式,环形缓冲区是数据流的「蓄水池」。这三者配合好了,音频系统就稳了一大半。下一节我们会聊音频编解码器的驱动开发,到时候这些知识都会用上。