5.13 DMA设备
5.13 DMA设备
📚 本节导读
学习时长: 约 35 分钟
难度级别: ⭐⭐⭐⭐☆
前置知识: C 语言基础、MCU DMA 基础知识、5.1 设备驱动框架、5.4 Serial 设备
🎯 学习目标
- 理解 OneOS Soft DMA 抽象层的设计理念
- 掌握
dma_ring_t环形缓冲区(head/tail/ring_count/copy_*)的使用 - 理解半满/全满/超时三种中断回调机制及
irq_mask & status判定 - 掌握
hard_dma_info_t/hard_dma_ops_t/soft_dma_t结构与字段 - 了解 Soft DMA 的
hard_dma_ops_t接口与"如何把外设接入 Soft DMA"的适配方法
一、概述
DMA(Direct Memory Access)是 MCU 中用于外设与内存之间高速数据传输的硬件模块,无需 CPU 干预。OneOS 提供了 Soft DMA 软件抽象层,位于 drivers/dma/ 目录下,核心头文件为 soft_dma.h 和 dma.h。
DMA 框架的特点:
- Soft DMA 抽象:在硬件 DMA 之上封装一层软件抽象,提供环形缓冲区管理
- 环形缓冲区:内置
dma_ring_t环形缓冲区,支持高效的数据读写 - 三种中断回调:半满中断、全满中断、超时中断
- 硬件适配:通过
hard_dma_ops_t接口适配不同 MCU 的 DMA 控制器
1.1 一句话核心
Soft DMA 适用于外设(串口/SPI/ADC)高速、持续接收数据且不想让 CPU 逐字节中断处理的场景。它让你"把数据接收交给 DMA 硬件 + 一个环形缓冲区,半满/全满/超时三种时机回调通知应用处理"。
1.2 与"逐字节中断接收"的对比(为什么用 DMA)
| 维度 | 逐字节中断接收 | Soft DMA 接收 |
|---|---|---|
| CPU 开销 | 每个字节一次中断,高频时 CPU 几乎被占满 | DMA 硬件搬运,只有半满/全满才中断一次 |
| 丢数据风险 | 高(CPU 忙时中断来不及) | 低(DMA 持续搬运) |
| 不定长帧 | 靠空闲中断/字节计数拼帧 | 靠超时中断(data_timeout)检测帧结束 |
| 实现复杂度 | 简单 | 较高(要理解环形缓冲 + 三种中断) |
| 适用场景 | 低速、字节量小 | 高速、数据量大、持续接收 |
结论:高速、数据量大、持续接收(如串口高速收发、ADC 连续采样、SPI 大块传输)用 Soft DMA;低速、偶发、字节少的场景逐字节中断足够且更简单。
1.3 典型应用场景
场景一:串口不定长数据接收——用循环 DMA + 超时中断,配合 data_timeout(us/Byte)检测帧结束:
g_uart_dma.hard_info.mode = HARD_DMA_MODE_CIRCULAR;
g_uart_dma.hard_info.flag = HARD_DMA_FLAG_FULL_IRQ | HARD_DMA_FLAG_TIMEOUT_IRQ;
g_uart_dma.hard_info.data_timeout = 90; /* 90us/Byte:超时即认为一帧结束 */场景二:ADC 连续采样——DMA 把 ADC 结果持续搬到缓冲区,半满/全满中断分批处理(乒乓缓冲)。
场景三:SPI 大块传输——DMA 在内存和外设间连续搬运,CPU 只负责"开始传输"和"传输完成"两个时间点。
数值演算 —— 超时中断的 data_timeout 如何决定帧结束:data_timeout 单位是 us/Byte。超时定时器周期 = data_timeout × count(count 见 4.x soft_dma_timer_init)。以 data_timeout = 90、缓冲 size = 1024(count = 1024/6 ≈ 170)为例:
| 计算项 | 公式 | 代入 | 结果 |
|---|---|---|---|
| 超时周期 | data_timeout × count | 90 × 170 | 15300 us = 15.3 ms |
| 触发条件 | 超时周期内 head 未变化 | 15.3ms 无新数据 | 触发 timeout_irq,判定一帧结束 |
关键理解:接收过程中只要持续有数据,
head就一直前进,soft_dma_timer_callback里last_head != last_trig_head的判断(见 4.4.2 双层缓冲数据流 与 4.5 中断处理链)会阻止超时触发;一旦数据流停顿超过data_timeout对应的周期,就认为一帧接收完毕,触发超时回调处理整帧。
1.4 选型边界表(何时不该用它)
| 场景 | 该用什么 | 为什么 |
|---|---|---|
| 低速、偶发、字节少的接收 | 逐字节中断(Serial 设备) | DMA 配置开销大,低速场景收益低 |
| 数据量小且要求实时响应 | 中断 + 信号量 | 中断响应更及时,无需环形缓冲 |
| 需要通用设备接口抽象 | 5.4 Serial / 5.5 SPI / 5.11 ADC 设备 | 这些设备内部已集成 DMA 或中断,应用不必直接用 Soft DMA |
| 单次大块传输后停止 | DMA 单次模式(HARD_DMA_MODE_NORMAL) | 不需要环形缓冲和超时检测 |
二、核心数据结构
2.1 环形缓冲区 dma_ring_t
来源:drivers/dma/soft_dma.h
typedef struct dma_ring
{
uint8_t *buff;
int size;
int head;
int tail;
} dma_ring_t;字段详解(源码本身无注释,下表为文档补充说明,不属于源码):
| 字段 | 类型 | 含义 |
|---|---|---|
buff | uint8_t * | 缓冲区指针(实际存储数据的起始地址) |
size | int | 缓冲区总大小(字节),head/tail 都小于 size |
head | int | 写指针(生产者),DMA/copy_line_to_ring 写入后前进 |
tail | int | 读指针(消费者),copy_ring_to_line 读取后前进 |
关键理解:
head/tail在[0, size)内循环前进,越过末尾自动绕回 0。数据量 =ring_count(head - tail,绕回时+ size);剩余空间 =size - count - 1(ring_space,留一个空位区分满/空,见 4.1 及 4.2 源码)。
环形缓冲区操作函数:
int ring_count(dma_ring_t *ring); /* 获取缓冲区中数据量 */
int ring_space(dma_ring_t *ring); /* 获取缓冲区剩余空间 */
void copy_ring_to_line(dma_ring_t *ring, uint8_t *line, int count); /* 环形缓冲区 → 线性缓冲区 */
void copy_line_to_ring(dma_ring_t *ring, uint8_t *line, int count); /* 线性缓冲区 → 环形缓冲区 */
void copy_ring_to_ring(dma_ring_t *dst, dma_ring_t *src, int count); /* 环形缓冲区 → 环形缓冲区 */2.2 硬件 DMA 信息 hard_dma_info_t
来源:drivers/dma/soft_dma.h
typedef struct hard_info
{
uint32_t mode; /* HARD_DMA_MODE_CIRCULAR */
uint32_t max_size; /* 64 * 1024 */
uint32_t flag; /* HARD_DMA_FLAG_HALF_IRQ */
uint32_t data_timeout; /* 90 us/Byte */
} hard_dma_info_t;字段详解(mode/max_size/flag/data_timeout 字段含义的补充说明,不属于源码):
| 字段 | 含义 |
|---|---|
mode | 传输模式:HARD_DMA_MODE_NORMAL(0) 普通单次 或 HARD_DMA_MODE_CIRCULAR(1) 循环 |
max_size | 单次最大传输大小(如 64*1024 = 64KB),限制硬件缓冲分配 |
flag | 硬件支持的中断标志位:HARD_DMA_FLAG_HALF_IRQ(1<<1) / HARD_DMA_FLAG_FULL_IRQ(1<<2) / HARD_DMA_FLAG_TIMEOUT_IRQ(1<<3) |
data_timeout | 数据超时时间(us/Byte),用于超时中断的帧结束检测 |
2.3 硬件 DMA 操作接口 hard_dma_ops_t
来源:drivers/dma/soft_dma.h
typedef struct hard_dma_ops
{
uint32_t (*get_index)(soft_dma_t *dma);
os_err_t (*dma_init)(soft_dma_t *dma);
os_err_t (*dma_start)(soft_dma_t *dma, void *buff, uint32_t size);
uint32_t (*dma_stop)(soft_dma_t *dma);
} hard_dma_ops_t;函数指针详解(源码本身无注释,下表为文档补充说明):
| 函数指针 | 说明 |
|---|---|
get_index | 获取当前 DMA 传输位置(已传输字节数),HAL 实现如 ST 的 RxXferSize - RxXferCount |
dma_init | 初始化 DMA 硬件(可选,可填 OS_NULL,soft_dma_init 会判空) |
dma_start | 启动 DMA 传输(把 buff/size 配给硬件 DMA) |
dma_stop | 停止 DMA 传输,返回已传输字节数(注意:Soft DMA 框架的 soft_dma_stop 返回 os_err_t,而 ops.dma_stop 返回字节数,两者不同) |
2.4 Soft DMA 回调 soft_dma_callbacks_t
来源:drivers/dma/soft_dma.h
typedef struct soft_dma_callbacks
{
void (*dma_half_callback)(soft_dma_t *dma);
void (*dma_full_callback)(soft_dma_t *dma);
void (*dma_timeout_callback)(soft_dma_t *dma);
} soft_dma_callbacks_t;函数指针详解(源码本身无注释,下表为文档补充说明):
| 函数指针 | 说明 |
|---|---|
dma_half_callback | 半满中断回调(DMA 完成缓冲一半,用于乒乓处理前半段) |
dma_full_callback | 全满中断回调(DMA 完成整个缓冲,处理完整数据块) |
dma_timeout_callback | 超时中断回调(超时时间内未收新数据,用于不定长帧结束检测) |
2.5 Soft DMA 主体 soft_dma_t
来源:drivers/dma/soft_dma.h
struct soft_dma
{
hard_dma_info_t hard_info;
hard_dma_ops_t ops;
soft_dma_callbacks_t cbs;
#ifdef OS_SOFT_DMA_SUPPORT_SIMUL_TIMEOUT
os_bool_t timer_inited;
#ifdef OS_USING_HRTIMER
os_hrtimer_t timer;
#else
os_timer_id timer;
#endif
#endif
soft_dma_rx_buffer_t buffer;
uint32_t status;
uint32_t irq_mask;
os_spinlock_t lock; /* Spin lock. */
};字段详解(源码本身无注释,下表为文档补充说明,不属于源码):
| 字段 | 含义 |
|---|---|
hard_info | 硬件 DMA 信息(模式/最大尺寸/中断标志/超时) |
ops | 硬件 DMA 操作接口(get_index/dma_init/dma_start/dma_stop) |
cbs | 三个回调(half/full/timeout) |
timer_inited/timer | 超时检测定时器(仅在 OS_SOFT_DMA_SUPPORT_SIMUL_TIMEOUT 配置下存在;若开启 HRTIMER 用 os_hrtimer_t,否则用 os_timer_id) |
buffer | 接收缓冲区管理(软缓冲 buff + 硬缓冲 hard_buffs[2]/hard_buff + 超时跟踪字段),详见 4.4 双层缓冲机制 |
status | 状态标志位:SOFT_DMA_EN(1<<0)/HALF(1<<1)/FULL(1<<2)/TIMEOUT(1<<3) |
irq_mask | 中断使能掩码(soft_dma_irq_enable 设置),控制哪些中断会触发回调 |
lock | 自旋锁,保护环形缓冲区多上下文访问 |
关键理解 ——
timer_inited/timer是有条件的成员:它们只存在于OS_SOFT_DMA_SUPPORT_SIMUL_TIMEOUT(软件模拟超时)配置下,且timer类型随OS_USING_HRTIMER变化(高精度定时器或软件定时器)。这解释了为什么soft_dma_t结构体有#ifdef——不编译该配置时,这两个字段根本不占内存。
三、API 详解
3.1 初始化和反初始化
os_err_t soft_dma_init(soft_dma_t *dma);
os_err_t soft_dma_deinit(soft_dma_t *dma);3.2 启动和停止
os_err_t soft_dma_start(soft_dma_t *dma, dma_ring_t *ring);
os_err_t soft_dma_stop(soft_dma_t *dma);3.3 中断控制
void soft_dma_irq_enable(soft_dma_t *dma, os_bool_t enable);3.4 中断处理(HAL 层调用)
void soft_dma_half_irq(soft_dma_t *dma); /* 半满中断处理 */
void soft_dma_full_irq(soft_dma_t *dma); /* 全满中断处理 */
void soft_dma_timeout_irq(soft_dma_t *dma); /* 超时中断处理 */HAL 层在 DMA 硬件中断中调用这些函数,由 Soft DMA 层管理环形缓冲区并触发回调。
四、核心源码详解
说明:以下源码均取自
drivers/dma/soft_dma.c,与项目源码逐字一致(含原始英文注释)。
4.1 ring_count —— 环形缓冲区数据量
一句话定位:计算环形缓冲区中当前有多少字节数据(生产者在 head,消费者在 tail)。
完整源码(drivers/dma/soft_dma.c,与项目一致):
int ring_count(dma_ring_t *ring)
{
int head = ring->head;
if (head >= ring->tail)
{
return head - ring->tail;
}
else
{
return head - ring->tail + ring->size;
}
}分步详解:
第 1 步:按 head/tail 相对位置计算(条件 head >= tail)
if (head >= ring->tail)
{
return head - ring->tail;
}
else
{
return head - ring->tail + ring->size;
}
}详细解释:head 是写指针、tail 是读指针,数据量 = 写指针 - 读指针。关键点:环形缓冲区可能"绕回",当 head < tail(写指针绕过了尾部)时,数据量要加上缓冲区总大小 size。
数值演算(假设 size = 8):
| 场景 | head | tail | 计算 | 结果 |
|---|---|---|---|---|
| 未绕回 | 5 | 2 | 5 - 2 | 3 |
| 已绕回 | 2 | 6 | 2 - 6 + 8 | 4 |
4.2 copy_ring_to_line —— 环形缓冲区→线性缓冲区
一句话定位:把环形缓冲区中 count 字节拷到线性缓冲区,自动处理"绕回分两段"。
完整源码(drivers/dma/soft_dma.c,与项目一致):
void copy_ring_to_line(dma_ring_t *ring, uint8_t *line, int count)
{
int copy_count;
OS_ASSERT(ring->buff != OS_NULL);
OS_ASSERT(ring->head < ring->size);
OS_ASSERT(ring->tail < ring->size);
OS_ASSERT(line != OS_NULL);
OS_ASSERT(count < ring->size);
if (count == 0)
return;
if (ring->tail + count <= ring->size)
{
memcpy(line, ring->buff + ring->tail, count);
ring->tail += count;
if (ring->tail >= ring->size)
ring->tail = 0;
}
else
{
copy_count = ring->size - ring->tail;
count -= copy_count;
memcpy(line, ring->buff + ring->tail, copy_count);
ring->tail = 0;
memcpy(line + copy_count, ring->buff, count);
ring->tail = count;
}
}分步详解:
第 1 步:无需绕回则一次拷完(条件 tail + count <= size)
if (ring->tail + count <= ring->size)
{
memcpy(line, ring->buff + ring->tail, count);
ring->tail += count;
if (ring->tail >= ring->size)
ring->tail = 0;
}详细解释:若 tail + count 没超过缓冲末尾,说明数据连续,一次 memcpy 拷完,tail 前进 count(若正好到末尾则绕回 0)。
第 2 步:绕回则分两段拷(条件 需要绕回)
else
{
copy_count = ring->size - ring->tail;
count -= copy_count;
memcpy(line, ring->buff + ring->tail, copy_count);
ring->tail = 0;
memcpy(line + copy_count, ring->buff, count);
ring->tail = count;
}详细解释:若数据跨过了缓冲末尾,先拷 copy_count = size - tail 字节(从 tail 到末尾),tail 绕回 0,再拷剩余 count 字节(从开头)。这是环形缓冲区跨边界处理的核心逻辑。
4.3 soft_dma_start —— 启动 Soft DMA
一句话定位:初始化缓冲、启动硬件 DMA、启动超时定时器、置使能标志。
完整源码(drivers/dma/soft_dma.c,与项目一致):
os_err_t soft_dma_start(soft_dma_t *dma, dma_ring_t *ring)
{
OS_ASSERT((dma->status & SOFT_DMA_EN) == 0);
soft_dma_buffer_init(dma, ring);
OS_ASSERT(dma->buffer.hard_buff->buff != OS_NULL);
dma->buffer.last_head = 0;
dma->buffer.last_trig_head = -1;
dma->ops.dma_start(dma, dma->buffer.hard_buff->buff, dma->buffer.hard_buff->size);
soft_dma_timer_init(dma);
dma->status |= SOFT_DMA_EN;
return OS_SUCCESS;
}分步详解:
第 1 步:断言未重复启动(条件 status & SOFT_DMA_EN)
OS_ASSERT((dma->status & SOFT_DMA_EN) == 0);详细解释:若已处于使能状态(SOFT_DMA_EN),断言失败,防止重复启动。
第 2 步:初始化缓冲并启动硬件(无条件执行)
soft_dma_buffer_init(dma, ring);
OS_ASSERT(dma->buffer.hard_buff->buff != OS_NULL);
dma->buffer.last_head = 0;
dma->buffer.last_trig_head = -1;
dma->ops.dma_start(dma, dma->buffer.hard_buff->buff, dma->buffer.hard_buff->size);详细解释:soft_dma_buffer_init 按循环/普通模式初始化软/硬缓冲。重置 last_head/last_trig_head(超时检测用)。dma->ops.dma_start(buff, size) 用硬件缓冲启动 DMA 传输。
第 3 步:启动定时器并置使能(无条件执行)
soft_dma_timer_init(dma);
dma->status |= SOFT_DMA_EN;
return OS_SUCCESS;
}详细解释:soft_dma_timer_init 启动超时检测定时器(若硬件没有半满/全满中断则用定时器软件模拟,见 OS_SOFT_DMA_SUPPORT_SIMUL_TIMEOUT)。置 SOFT_DMA_EN 标志表示已启动。
4.4 双层缓冲机制 —— 为什么既有"硬缓冲(线性)"又有"软缓冲(环形)"
这是理解 Soft DMA 数据传输的核心机制。前面你可能会疑惑:为什么既有环形缓冲区、又有线性缓冲区?答案是——DMA 硬件需要一块连续线性内存来搬运,而应用层希望用环形缓冲来方便地读写数据流,这两层之间需要一座"桥",这座桥就是 Soft DMA 的双层缓冲结构。
4.4.1 双层缓冲的载体:soft_dma_rx_buffer_t
来源:drivers/dma/soft_dma.h
typedef struct soft_dma_rx_buffer
{
uint32_t flag;
dma_ring_t *buff;
dma_ring_t *hard_buff;
dma_ring_t hard_buffs[2];
int32_t last_head;
int32_t last_trig_head;
} soft_dma_rx_buffer_t;字段详解(源码本身无注释,下表为文档补充说明,不属于源码):
| 字段 | 含义 |
|---|---|
flag | 缓冲管理标志位(框架内部保留) |
buff | 软缓冲指针:指向用户传入的环形缓冲区(soft_dma_start(dma, ring) 的 ring),是"逻辑数据流" |
hard_buff | 硬缓冲指针:指向当前正在被硬件 DMA 写入的那块线性缓冲 |
hard_buffs[2] | 两个硬缓冲实例:正常模式下乒乓切换,一个被 DMA 写满时切换另一个继续写 |
last_head / last_trig_head | 超时检测用:记录上一次的写位置,用于判断"数据流是否停顿" |
关键理解:
buff(软缓冲)与hard_buff(硬缓冲)都是dma_ring_t结构,但含义截然不同——buff是"环形、可绕回、逻辑连续"的;hard_buff是"线性、物理连续、直接交给 DMA 寄存器寻址"的。后面你会看到,DMA 永远只写hard_buff,而应用层永远只读buff,两者之间用copy_ring_to_ring搬运。
4.4.2 一图看懂双层缓冲的数据流向
┌────────────────────────────────────────────────┐
│ 应用层(CPU) │
│ copy_ring_to_line(...) 从 buff 读数据 │
└───────────────────────▲────────────────────────┘
│ 读软缓冲(逻辑、环形、可绕回)
┌───────────────────────┴────────────────────────┐
软缓冲(环形) │ buff(dma_ring_t,环形,size=1024) │
──────────────▶ │ ┌─┬─┬─┬─┬─┬─┬─┬─┐ head/tail 循环前进 │
│ │ │D│D│D│ │ │ │ │ copy_ring_to_ring 写入 │
│ └─┴─┴─┴─┴─┴─┴─┴─┘ │
└───────────────────────▲────────────────────────┘
│ 中断时拷入(copy_ring_to_ring)
┌───────────────────────┴────────────────────────┐
硬缓冲(线性) │ hard_buff(线性,连续,DMA 直接写) │
──────────────▶ │ ┌────┬────┬────┬────┬────┬────┐ │
│ │ B0 │ B1 │ B2 │ B3 │ B4 │ B5 │ ← DMA 递增写 │
│ └────┴────┴────┴────┴────┴────┘ │
└───────────────────────▲────────────────────────┘
│ DMA 硬件(MCU DMA 控制器)
│ 把外设数据搬到 hard_buff->buff
└──────────────────────────上图用三个箭头画出了数据的完整流向。下面从上到下、按箭头方向逐步拆解每一步在做什么、为什么这么做,并用一个具体例子走一遍。
流程详解
数据从外设最终到达应用,需要经过三个环节,每个环节对应一个箭头、一次内存搬运:
| 环节 | 箭头 | 谁在写 | 写入哪里 | 何时发生 |
|---|---|---|---|---|
| ① | DMA 硬件 → 硬缓冲 | DMA 硬件(不占 CPU) | 硬缓冲 hard_buff->buff(线性) | 持续进行,硬件自动搬 |
| ② | 硬缓冲 → 软缓冲 | CPU(中断里) | 软缓冲 buff(环形) | 半满/全满/超时中断时 |
| ③ | 软缓冲 → 应用层 | CPU(应用代码) | 应用自己的 line 数组 | 应用主动 copy_ring_to_line |
第 ① 步:DMA 硬件把外设数据写进硬缓冲(线性)
- 做了什么:MCU 的 DMA 控制器拿到外设(串口/SPI/ADC)的数据后,按递增地址逐个字节写进
hard_buff->buff。这一步完全由硬件完成,CPU 不参与。 - 为什么硬缓冲必须是线性:DMA 的搬运逻辑是"把地址从
起始地址加到起始地址+长度",它只认识连续的内存,不理解"绕回"这种环形概念。所以交给 DMA 的buff/size必须是一块连续线性内存(dma_start(dma, hard_buff->buff, hard_buff->size))。 - 代价:DMA 写满
size后(正常模式)就停了,或者(循环模式)硬件自动把写地址绕回开头——但无论哪种,DMA 眼里始终只有一块线性内存。
第 ② 步:CPU 在中断里把硬缓冲数据搬进软缓冲(环形)
- 做了什么:当 DMA 写满、写到一半(半满)或停顿超时时,会触发中断;CPU 进入中断处理(
soft_dma_full_irq/half_irq/timeout_irq→soft_dma_irq),用copy_ring_to_ring(dma->buffer.buff, hard_buff, head)把硬缓冲里的新数据拷进软缓冲。 - 为什么这一步必须做:应用层想要"环形、可绕回、读写指针解耦"的逻辑数据流,而硬缓冲是线性的、且很快会被 DMA 覆盖——必须趁 DMA 还没覆盖前,把已收到的数据搬到安全的软缓冲里。这一步是两层之间的"桥"。
- 关键点:
copy_ring_to_ring读的是硬缓冲(线性、src),写的是软缓冲(环形、dst),它会自动处理软缓冲的绕回——如果软缓冲剩余空间不够放整块硬缓冲数据,就分两段写入(跨边界)。
第 ③ 步:应用用 copy_ring_to_line 从软缓冲读数据
- 做了什么:应用在任意方便的时刻(不一定要在中断里)用
copy_ring_to_line(&ring, line, count)从软缓冲读走count字节,tail指针前进、越界绕回。 - 为什么应用只读软缓冲、不读硬缓冲:因为硬缓冲是"正在被 DMA 写的临时场所",数据随时会被覆盖,且是线性不连续的逻辑流;软缓冲才是"已经搬好的、安全的、逻辑连续的数据流",
head/tail记录了"已写多少、已读多少"。应用从软缓冲读,既安全又方便。
具体数值演算:跟着一帧数据走一遍
假设:软缓冲 size = 8,硬缓冲 size = 4,外设连续发来 6 个字节 D0 D1 D2 D3 D4 D5。我们跟踪 head/tail(这里以循环模式、硬缓冲独立于软缓冲为例,便于看到两步搬运)。
| 时刻 | 事件 | 硬缓冲(线性) | 软缓冲 head/tail | 软缓冲内容 |
|---|---|---|---|---|
| t0 | 启动 | 空 | 0/0 | 空 |
| t1 | DMA 写入 D0 D1 D2 D3 | D0 D1 D2 D3 | 0/0 | 空(还没搬) |
| t2 | 中断:copy_ring_to_ring 搬 4 字节 | (待覆盖) | 4/0 | D0 D1 D2 D3 |
| t3 | DMA 继续写 D4 D5(写满 4 字节) | D4 D5 | 4/0 | D0 D1 D2 D3 |
| t4 | 中断:搬 2 字节 | (待覆盖) | 6/0 | D0 D1 D2 D3 D4 D5 |
| t5 | 应用 copy_ring_to_line 读走 6 字节 | —— | 6/6 | 空(已读走) |
要点解读:
- t1→t2:DMA 先写硬缓冲,CPU 后搬运——两者不同步,所以硬缓冲里的数据"暂存"着,等中断来搬。
- t2→t3→t4:硬缓冲被 DMA 覆盖(t3 时
D0~D3已被D4 D5覆盖掉一部分),但好在我们 t2 已经把D0~D3搬进软缓冲了——这就是必须"趁未被覆盖前搬走"的原因。 - t5:应用从软缓冲一次性读走 6 字节,软缓冲回到空(
head==tail)。
为什么不能只用一个缓冲?(再次强调)
因为DMA 硬件和应用层对内存形状的要求互相矛盾:
- DMA 硬件:要求连续线性内存、地址自增,它不理解"环形绕回";
- 应用层:希望是环形缓冲(写满绕回、读写指针解耦、不要求物理连续);
- 因此必须用一块线性硬缓冲对接 DMA,用一块环形软缓冲对接应用,中间由框架拷贝。硬缓冲是"硬件能写的地方",软缓冲是"应用能读的地方",二者通过
copy_ring_to_ring桥接。
一句话记忆:硬件往硬缓冲里"倒"数据,CPU 趁还没被覆盖前"抢"到软缓冲,应用再从软缓冲"取"走——三个环节(倒→抢→取)缺一不可,对应着 DMA、中断搬运、应用读取三类动作。
边界情况:应用没有及时读取,会不会造成数据覆盖?
会。 如果应用读取速度跟不上 DMA 写入速度,软缓冲里的旧数据会被新数据覆盖,造成数据丢失/错乱。下面说明为什么、以及框架的"保护"有多弱。
为什么一定会覆盖? 看 copy_line_to_ring(drivers/dma/soft_dma.c),它不做空间检查,直接 memcpy 进目标缓冲:
void copy_line_to_ring(dma_ring_t *ring, uint8_t *line, int count)
{
...
if (ring->head + count <= ring->size)
{
memcpy(ring->buff + ring->head, line, count);
ring->head += count;
if (ring->head == ring->size)
ring->head = 0;
}
...
}而 copy_ring_to_ring(硬缓冲→软缓冲的桥)内部就是调用 copy_line_to_ring,所以框架搬多少、软缓冲就无条件写多少。当软缓冲剩余空间(ring_space)小于搬运量时,写入就会压过 tail 还没读走的区域 → 旧数据被覆盖。
两种模式各自的覆盖场景:
| 模式 | 覆盖发生的位置 | 具体表现 |
|---|---|---|
| 正常模式(双硬缓冲乒乓) | copy_ring_to_ring 把硬缓冲数据搬进软缓冲时 | 软缓冲满了仍无脑写入 → 覆盖旧数据;同时 DMA 硬缓冲若写满又没搬走,soft_dma_normal_updata_buffer 打印 drop some data 直接丢 |
| 循环模式(硬=软 或 独立硬缓冲) | DMA 持续绕回写 / 中断搬入时 | 应用不读,head 绕回后追平甚至超过 tail,直接覆盖未读数据 |
数值演算(软缓冲 size=8,应用一直不读):
| 时刻 | head | tail | ring_space | 事件 |
|---|---|---|---|---|
| t0 | 0 | 0 | 7(留 1 区分满/空) | 空 |
| t1 | 4 | 0 | 3 | 写入 4 字节 |
| t2 | 8 | 0 | -1(已满) | 又写 4 字节,缓冲满 |
| t3 | 2 | 0 | 5 | 再写 4 字节:memcpy 写 buff[0..3],覆盖 tail=0 处还没读走的 D0 |
t3 时 head 从 8 绕回 2,写入地址正好压过 tail 未读的数据 → 旧数据被覆盖。
框架提供了哪些"保护"(都很弱):
- 正常模式
drop some data:仅当 DMA 硬缓冲写满时打印警告,是事后发现,且copy_ring_to_ring本身照样可能覆盖软缓冲; ring_space可主动检测:应用可在读前用ring_space(ring)判断剩余空间,但框架不会自动阻塞或丢弃。
正确做法:
- 应用要足够快读走数据(常用:DMA 中断里
os_sem_post,应用线程os_sem_take后立即读); - 缓冲区开够大,让"读走速度"长期大于"DMA 写入速度";
- 对丢数据不敏感的流式数据(音频/传感器连续采样)偶尔覆盖可接受;对丢一字节就错的协议数据务必保证读取及时性,或靠上层协议重传。
一句话:Soft DMA 是"无背压"设计——宁可覆盖旧数据,也不拖慢 DMA。防止覆盖的责任在应用侧:及时读 + 缓冲区够大 + 必要时用
ring_space主动判断。
4.4.3 正常模式(NORMAL)—— 两个硬缓冲乒乓切换
源码(drivers/dma/soft_dma.c 中 soft_dma_normal_buffer_init,与项目逐字一致):
static uint8_t *soft_dma_normal_buffer_init(soft_dma_t *dma, dma_ring_t *ring)
{
/* buffer */
memset(&dma->buffer, 0, sizeof(dma->buffer));
/* soft buffer */
dma->buffer.buff = ring;
/* hard buffer */
int size = 128;
if (!(dma->hard_info.flag & HARD_DMA_FLAG_HALF_IRQ))
{
size = max(size, (uint64_t)600 / dma->hard_info.data_timeout + 1);
}
size = min(size, ring->size);
size = min(size, dma->hard_info.max_size);
dma->buffer.hard_buffs[0].size = size;
dma->buffer.hard_buffs[1].size = size;
#ifdef OS_USING_DMA_RAM
dma->buffer.hard_buffs[0].buff = os_dma_malloc_align(size * 2, BSP_CACHE_LINE_SIZE);
#else
dma->buffer.hard_buffs[0].buff = os_calloc(1, size * 2);
#endif
dma->buffer.hard_buffs[1].buff = dma->buffer.hard_buffs[0].buff + size;
dma->buffer.hard_buff = &dma->buffer.hard_buffs[0];
OS_ASSERT(dma->buffer.hard_buffs[0].buff != OS_NULL);
OS_ASSERT(dma->buffer.hard_buffs[1].buff != OS_NULL);
return dma->buffer.hard_buff->buff;
}分步详解:
第 1 步:软缓冲直接指向用户环形缓冲(无条件执行)
dma->buffer.buff = ring;详细解释:soft_dma_start(dma, ring) 传入的 ring 被记为软缓冲 buff。应用层最后就是从这个环形缓冲里读数据的。所以你在 soft_dma_start 传的那个环形缓冲,就是"软缓冲"。
第 2 步:计算单个硬缓冲大小(按条件动态调整)
int size = 128;
if (!(dma->hard_info.flag & HARD_DMA_FLAG_HALF_IRQ))
{
size = max(size, (uint64_t)600 / dma->hard_info.data_timeout + 1);
}
size = min(size, ring->size);
size = min(size, dma->hard_info.max_size);详细解释:单个硬缓冲默认 128 字节。若硬件没有半满中断(flag 里没有 HARD_DMA_FLAG_HALF_IRQ),就加大到 600 / data_timeout + 1,目的是让"两次通知之间"能攒够足够数据(否则超时模拟半满会很频繁)。再限制:不能超过软缓冲 ring->size,也不能超过硬件 max_size。两个硬缓冲 hard_buffs[0]/hard_buffs[1] 大小相同。
第 3 步:分配一整块内存,切成两个硬缓冲(无条件执行)
dma->buffer.hard_buffs[0].buff = os_calloc(1, size * 2);
dma->buffer.hard_buffs[1].buff = dma->buffer.hard_buffs[0].buff + size;
dma->buffer.hard_buff = &dma->buffer.hard_buffs[0];详细解释:一次申请 size*2 字节,前半段给 hard_buffs[0]、后半段给 hard_buffs[1](两块线性连续)。当前活动硬缓冲 hard_buff 先指向 hard_buffs[0]。注意:这里 hard_buffs[0].buff + size 是纯指针运算,两块硬缓冲是"紧挨着的两块线性内存"。
核心机制:乒乓切换(soft_dma_normal_updata_buffer,drivers/dma/soft_dma.c,与项目逐字一致):
static os_err_t soft_dma_normal_updata_buffer(soft_dma_t *dma, uint32_t head)
{
os_ubase_t level;
os_spin_lock_irq(&dma->lock, &level);
if (head > 0)
{
dma_ring_t *hard_buff = dma->buffer.hard_buff;
if (dma->buffer.hard_buff == &dma->buffer.hard_buffs[0])
{
dma->buffer.hard_buff = &dma->buffer.hard_buffs[1];
}
else if (dma->buffer.hard_buff == &dma->buffer.hard_buffs[1])
{
dma->buffer.hard_buff = &dma->buffer.hard_buffs[0];
}
else
{
OS_ASSERT(OS_FALSE);
}
head = dma->ops.dma_stop(dma);
dma->ops.dma_start(dma, dma->buffer.hard_buff->buff, dma->buffer.hard_buff->size);
dma->buffer.hard_buff->head = 0;
dma->buffer.hard_buff->tail = 0;
if (head < hard_buff->size)
{
hard_buff->head = head;
copy_ring_to_ring(dma->buffer.buff, hard_buff, head);
OS_ASSERT(dma->buffer.buff->head < dma->buffer.buff->size);
}
else if (head == hard_buff->size)
{
os_kprintf("drop some data\r\n");
}
else
{
OS_ASSERT(OS_FALSE);
}
}
os_spin_unlock_irq(&dma->lock, level);
return OS_SUCCESS;
}分步详解:
第 1 步:只在有新数据时处理(条件 head > 0)
if (head > 0)
{详细解释:head 是硬件 DMA 当前已写字节数。只有写入过数据才需要搬运。
第 2 步:硬缓冲乒乓切换(条件 当前是哪个缓冲)
dma_ring_t *hard_buff = dma->buffer.hard_buff;
if (dma->buffer.hard_buff == &dma->buffer.hard_buffs[0])
{
dma->buffer.hard_buff = &dma->buffer.hard_buffs[1];
}
else if (dma->buffer.hard_buff == &dma->buffer.hard_buffs[1])
{
dma->buffer.hard_buff = &dma->buffer.hard_buffs[0];
}详细解释:把"活动硬缓冲"从 [0] 切到 [1]、或从 [1] 切到 [0]。这样 DMA 立刻在新缓冲上继续接收,同时旧缓冲的数据可以被安全拷走——这就是乒乓缓冲的本质:两块缓冲轮流用,一块被 DMA 写、另一块被 CPU 搬走,互不冲突。
第 3 步:重启 DMA 到新硬缓冲(无条件执行)
head = dma->ops.dma_stop(dma);
dma->ops.dma_start(dma, dma->buffer.hard_buff->buff, dma->buffer.hard_buff->size);
dma->buffer.hard_buff->head = 0;
dma->buffer.hard_buff->tail = 0;详细解释:先停掉旧 DMA(dma_stop 返回已写字节数 head),再用新硬缓冲 buff/size 重启 DMA,并把新硬缓冲的 head/tail 清零(新缓冲从 0 开始写)。
第 4 步:把旧硬缓冲数据拷入软环形缓冲(条件 字节数未超量)
if (head < hard_buff->size)
{
hard_buff->head = head;
copy_ring_to_ring(dma->buffer.buff, hard_buff, head);
OS_ASSERT(dma->buffer.buff->head < dma->buffer.buff->size);
}
else if (head == hard_buff->size)
{
os_kprintf("drop some data\r\n");
}详细解释:把旧硬缓冲(hard_buff 保存的指针)里 head 字节拷进软缓冲 dma->buffer.buff(copy_ring_to_ring 会把硬缓冲的线性数据按环形规则写入软缓冲)。若 head == size(写满了才停,说明应用没及时读、软缓冲放不下)就丢弃并打印警告。
数值演算 —— 正常模式乒乓流程(设 data_timeout=90、无半满中断、软缓冲 ring->size=1024):
| 步骤 | 事件 | 活动硬缓冲 | 数据流向 |
|---|---|---|---|
| 0 | soft_dma_start | hard_buffs[0](大小 = 600/90+1=7,取 min(7,1024,64K)=7) | DMA 开始写 hard_buffs[0] |
| 1 | head 到 7(hard_buffs[0] 写满 7 字节) | 切到 hard_buffs[1] | 把 hard_buffs[0] 的 7 字节 copy_ring_to_ring 进软缓冲 |
| 2 | DMA 写 hard_buffs[1] 到 7 字节 | 切回 hard_buffs[0] | 把 hard_buffs[1] 的 7 字节搬进软缓冲 |
| 3 | …… 持续乒乓 …… | 轮流 | 软缓冲 head 持续前进,攒够后应用 copy_ring_to_line 读走 |
关键理解:
soft_dma_normal_buffer_init里算出单个硬缓冲只有 7 字节(因为没半满中断、data_timeout=90),意味着每收 7 字节就要中断一次并乒乓。这个值虽小,但保证了无半满中断时也能高频搬数据、不丢帧。
4.4.4 循环模式(CIRCULAR)—— 单个硬缓冲 + DMA 自动绕回
源码(drivers/dma/soft_dma.c 中 soft_dma_circular_buffer_init,与项目逐字一致):
static uint8_t *soft_dma_circular_buffer_init(soft_dma_t *dma, dma_ring_t *ring)
{
/* buffer */
memset(&dma->buffer, 0, sizeof(dma->buffer));
/* soft buffer */
dma->buffer.buff = ring;
/* hard buffer */
dma->buffer.hard_buff = &dma->buffer.hard_buffs[0];
if (ring->size <= dma->hard_info.max_size)
{
dma->buffer.hard_buff->size = ring->size;
dma->buffer.hard_buff->buff = ring->buff;
}
else
{
dma->buffer.hard_buff->size = dma->hard_info.max_size;
#ifdef OS_USING_DMA_RAM
dma->buffer.hard_buff->buff = os_dma_malloc_align(dma->hard_info.max_size, BSP_CACHE_LINE_SIZE);
#else
dma->buffer.hard_buff->buff = os_calloc(1, dma->hard_info.max_size);
#endif
}
OS_ASSERT(dma->buffer.hard_buff->buff != OS_NULL);
return dma->buffer.hard_buff->buff;
}分步详解:
第 1 步:软缓冲指向用户环形缓冲(无条件执行)
dma->buffer.buff = ring;详细解释:与正常模式相同,软缓冲 = 用户传入的环形缓冲。
第 2 步:硬缓冲直接复用软缓冲(条件 ring->size <= max_size)
dma->buffer.hard_buff = &dma->buffer.hard_buffs[0];
if (ring->size <= dma->hard_info.max_size)
{
dma->buffer.hard_buff->size = ring->size;
dma->buffer.hard_buff->buff = ring->buff;
}详细解释:循环模式的关键优化——若软缓冲大小不超过硬件上限 max_size,就让硬缓冲直接指向软缓冲的同一块内存(hard_buff->buff = ring->buff)。此时硬缓冲和软缓冲是同一块环形内存,DMA 循环写入它、应用也从它读,无需中间拷贝。
第 3 步:软缓冲太大则另分配硬缓冲(条件 ring->size > max_size)
else
{
dma->buffer.hard_buff->size = dma->hard_info.max_size;
dma->buffer.hard_buff->buff = os_calloc(1, dma->hard_info.max_size);
}详细解释:若软缓冲太大(超过 max_size),硬件 DMA 写不了那么大,就单独分配一块 max_size 的线性硬缓冲给 DMA 循环写入;此时每次中断要用 copy_ring_to_ring 把硬缓冲新数据搬到软缓冲(见 soft_dma_circular_updata_buffer)。
数值演算 —— 循环模式两种情形(设 ring->size=1024):
| 情形 | 条件 | 硬缓冲行为 | 是否需要拷贝 |
|---|---|---|---|
| 情形 A | 1024 <= 64K(软缓冲不大) | 硬缓冲 = 软缓冲同一块内存,DMA 循环写满绕回 | 否(硬=软,直接读) |
| 情形 B | 1024 > 64K(软缓冲超大) | 另分配 64K 线性硬缓冲,DMA 只写这 64K | 是(中断时 copy_ring_to_ring 搬入软缓冲) |
关键理解:循环模式(
HARD_DMA_MODE_CIRCULAR)下硬件 DMA 配置为循环模式,写满hard_buff->size后自动回到开头继续写(硬件自动绕回)。因此无需乒乓切换,一个硬缓冲就够。正常模式(HARD_DMA_MODE_NORMAL)DMA 写满即停,所以必须两块硬缓冲乒乓才能持续接收。
4.4.5 一句话总结双层缓冲
"硬件只认线性内存,应用只想用环形缓冲" —— Soft DMA 用一块(或两块)线性硬缓冲对接 DMA 硬件,用一块环形软缓冲对接应用,两者之间用
copy_ring_to_ring桥接。正常模式用两块硬缓冲乒乓(因为 DMA 单次写满即停),循环模式用一块硬缓冲(因为 DMA 硬件自动绕回);软缓冲够大时循环模式甚至让硬缓冲直接复用软缓冲内存、零拷贝。
4.5 中断处理链 soft_dma_irq / soft_dma_irq_callback
一句话定位:HAL 中断进来后,更新缓冲并触发对应回调。
完整源码(drivers/dma/soft_dma.c,与项目一致):
static void soft_dma_irq(soft_dma_t *dma)
{
uint32_t head = dma->ops.get_index(dma);
soft_dma_updata_buffer(dma, head);
soft_dma_irq_callback(dma);
}
static void soft_dma_irq_callback(soft_dma_t *dma)
{
int count = ring_count(dma->buffer.buff);
if (count <= 0)
{
dma->status &= ~(SOFT_DMA_HALF | SOFT_DMA_FULL | SOFT_DMA_TIMEOUT);
return;
}
if (dma->irq_mask & dma->status & SOFT_DMA_HALF)
{
dma->cbs.dma_half_callback(dma);
dma->status &= ~SOFT_DMA_HALF;
}
if (dma->irq_mask & dma->status & SOFT_DMA_FULL)
{
dma->cbs.dma_full_callback(dma);
dma->status &= ~SOFT_DMA_FULL;
}
if (dma->irq_mask & dma->status & SOFT_DMA_TIMEOUT)
{
dma->cbs.dma_timeout_callback(dma);
dma->status &= ~SOFT_DMA_TIMEOUT;
}
}分步详解:
第 1 步:读当前 DMA 位置并更新缓冲(无条件执行)
uint32_t head = dma->ops.get_index(dma);
soft_dma_updata_buffer(dma, head);详细解释:get_index 获取硬件 DMA 当前写到哪个位置,soft_dma_updata_buffer 把它同步进环形缓冲区,并更新 SOFT_DMA_HALF/SOFT_DMA_FULL 状态(见源码第 371-384 行:越过半满置 HALF,绕回置 FULL)。
第 2 步:按状态触发回调(条件 对应状态置位且中断使能)
int count = ring_count(dma->buffer.buff);
if (count <= 0)
{
dma->status &= ~(SOFT_DMA_HALF | SOFT_DMA_FULL | SOFT_DMA_TIMEOUT);
return;
}
if (dma->irq_mask & dma->status & SOFT_DMA_HALF)
{
dma->cbs.dma_half_callback(dma);
dma->status &= ~SOFT_DMA_HALF;
}
if (dma->irq_mask & dma->status & SOFT_DMA_FULL)
{
dma->cbs.dma_full_callback(dma);
dma->status &= ~SOFT_DMA_FULL;
}
if (dma->irq_mask & dma->status & SOFT_DMA_TIMEOUT)
{
dma->cbs.dma_timeout_callback(dma);
dma->status &= ~SOFT_DMA_TIMEOUT;
}
}详细解释:若无数据(count <= 0)清空所有状态返回。否则按位与判断:dma->irq_mask(中断使能掩码)与 dma->status(状态)与对应标志。命中则调对应回调(half/full/timeout)并清除该状态位。为什么用 irq_mask & status:status 记录"发生了哪些中断",irq_mask 记录"应用开启了哪些中断",两者按位与后才真正决定"这次要不要调某个回调"——既不会漏调(状态已置位)、也不会误调(掩码关闭时不调)。
soft_dma_irq_callback() 条件走向总结:
soft_dma_irq_callback(dma) 条件走向总结:
├── count = ring_count(buffer.buff)
├── count <= 0(无数据)
│ └── 清空 SOFT_DMA_HALF|FULL|TIMEOUT 状态;return
├── (irq_mask & status & SOFT_DMA_HALF) 非 0(半满且使能)
│ ├── cbs.dma_half_callback(dma)
│ └── status &= ~SOFT_DMA_HALF
├── (irq_mask & status & SOFT_DMA_FULL) 非 0(全满且使能)
│ ├── cbs.dma_full_callback(dma)
│ └── status &= ~SOFT_DMA_FULL
├── (irq_mask & status & SOFT_DMA_TIMEOUT) 非 0(超时且使能)
│ ├── cbs.dma_timeout_callback(dma)
│ └── status &= ~SOFT_DMA_TIMEOUT
└── 三个 if 独立判断,互不影响,可能同时触发多个回调注意:
soft_dma_half_irq/soft_dma_full_irq/soft_dma_timeout_irq三个 HAL 入口都先走soft_dma_irq(读 head → 更新缓冲 → 调本回调);其中soft_dma_timeout_irq会先置SOFT_DMA_TIMEOUT状态再走soft_dma_irq。
soft_dma_half_irq/soft_dma_full_irq/soft_dma_timeout_irq(HAL 调用的入口):
void soft_dma_half_irq(soft_dma_t *dma)
{
soft_dma_irq(dma);
}
void soft_dma_full_irq(soft_dma_t *dma)
{
soft_dma_irq(dma);
}
void soft_dma_timeout_irq(soft_dma_t *dma)
{
dma->status |= SOFT_DMA_TIMEOUT;
soft_dma_irq(dma);
}详细解释:三个 HAL 入口都走 soft_dma_irq;timeout_irq 先置 SOFT_DMA_TIMEOUT 状态再处理。
4.6 soft_dma_stop —— 停止 Soft DMA
完整源码(drivers/dma/soft_dma.c,与项目一致):
os_err_t soft_dma_stop(soft_dma_t *dma)
{
if ((dma->status & SOFT_DMA_EN) == 0)
return OS_SUCCESS;
soft_dma_timer_deinit(dma);
soft_dma_buffer_deinit(dma);
dma->ops.dma_stop(dma);
dma->buffer.last_head = 0;
dma->buffer.last_trig_head = -1;
dma->status &= ~SOFT_DMA_EN;
return OS_SUCCESS;
}分步详解:
第 1 步:未启动则直接返回(条件 status & SOFT_DMA_EN 为 0)
if ((dma->status & SOFT_DMA_EN) == 0)
return OS_SUCCESS;详细解释:若未在使能状态,直接返回(无需停止)。
第 2 步:停止定时器、释放缓冲、停硬件(无条件执行)
soft_dma_timer_deinit(dma);
soft_dma_buffer_deinit(dma);
dma->ops.dma_stop(dma);
dma->buffer.last_head = 0;
dma->buffer.last_trig_head = -1;
dma->status &= ~SOFT_DMA_EN;
return OS_SUCCESS;
}详细解释:停止超时定时器、释放分配的缓冲(soft_dma_buffer_deinit)、调 ops.dma_stop 停硬件,重置超时跟踪字段,清除 SOFT_DMA_EN 标志。
五、中断回调机制
Soft DMA 支持三种中断回调,对应不同的传输状态:
| 中断类型 | 触发条件 | 典型用途 |
|---|---|---|
| 半满中断 | DMA 传输完成缓冲区的一半 | 处理前半部分数据,实现乒乓缓冲 |
| 全满中断 | DMA 传输完成整个缓冲区 | 处理完整数据块 |
| 超时中断 | 在指定时间内未收到新数据 | 处理不完整的数据帧(如 UART 空闲中断) |
DMA 传输过程(循环模式):
┌──────────────────────────────────────────────────┐
│ 环形缓冲区 (dma_ring_t) │
│ ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐│
│ │ D0 │ D1 │ D2 │ D3 │ D4 │ D5 │ D6 │ D7 ││
│ └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘│
│ ↑ 半满中断 ↑ 全满中断 │
│ └─ 缓冲区前一半完成 ─┘ │
└──────────────────────────────────────────────────┘六、代码示例
6.1 基本使用:初始化与启动
#include <oneos_config.h>
#include <dma.h>
/* 定义 DMA 环形缓冲区 */
static uint8_t g_dma_buffer[1024];
static dma_ring_t g_dma_ring;
static soft_dma_t g_uart_dma;
/* 全满中断回调 */
void dma_full_handler(soft_dma_t *dma)
{
int count = ring_count(&g_dma_ring);
if (count > 0)
{
uint8_t data[128];
int read_len = (count > 128) ? 128 : count;
copy_ring_to_line(&g_dma_ring, data, read_len);
os_kprintf("DMA full: received %d bytes\r\n", read_len);
}
}
/* 超时中断回调 */
void dma_timeout_handler(soft_dma_t *dma)
{
int count = ring_count(&g_dma_ring);
if (count > 0)
{
uint8_t data[64];
int read_len = (count > 64) ? 64 : count;
copy_ring_to_line(&g_dma_ring, data, read_len);
os_kprintf("DMA timeout: %d bytes (frame end)\r\n", read_len);
}
}
void dma_basic_example(void)
{
/* 初始化环形缓冲区 */
g_dma_ring.buff = g_dma_buffer;
g_dma_ring.size = sizeof(g_dma_buffer);
g_dma_ring.head = 0;
g_dma_ring.tail = 0;
/* 配置硬件 DMA 信息 */
g_uart_dma.hard_info.mode = HARD_DMA_MODE_CIRCULAR;
g_uart_dma.hard_info.max_size = 65536;
g_uart_dma.hard_info.flag = HARD_DMA_FLAG_FULL_IRQ | HARD_DMA_FLAG_TIMEOUT_IRQ;
g_uart_dma.hard_info.data_timeout = 90; /* 90us/Byte */
/* 注册回调 */
g_uart_dma.cbs.dma_full_callback = dma_full_handler;
g_uart_dma.cbs.dma_timeout_callback = dma_timeout_handler;
/* 初始化 Soft DMA */
soft_dma_init(&g_uart_dma);
/* 启动 DMA 传输 */
soft_dma_start(&g_uart_dma, &g_dma_ring);
os_kprintf("DMA started with circular mode\r\n");
}6.2 环形缓冲区基本操作
void ring_buffer_operations(void)
{
uint8_t buffer[256];
dma_ring_t ring;
uint8_t data[] = "Hello DMA!";
uint8_t read_buf[32];
/* 初始化环形缓冲区 */
ring.buff = buffer;
ring.size = sizeof(buffer);
ring.head = 0;
ring.tail = 0;
/* 写入数据 */
copy_line_to_ring(&ring, data, strlen((char *)data));
os_kprintf("After write: count=%d, space=%d\r\n",
ring_count(&ring), ring_space(&ring));
/* 读取数据 */
int len = ring_count(&ring);
copy_ring_to_line(&ring, read_buf, len);
read_buf[len] = '\0';
os_kprintf("Read: %s\r\n", read_buf);
/* 验证空状态 */
os_kprintf("After read: count=%d\r\n", ring_count(&ring));
}6.3 乒乓缓冲模式
static uint8_t g_ping_buffer[512];
static uint8_t g_pong_buffer[512];
/* dma/ring 必须是 static/全局,soft_dma_start 会保存 ring 指针供 DMA 异步使用 */
static soft_dma_t g_pingpong_dma;
static dma_ring_t g_pingpong_ring;
void dma_pingpong_half_handler(soft_dma_t *dma)
{
os_kprintf("Processing PING buffer (first half)\r\n");
}
void dma_pingpong_full_handler(soft_dma_t *dma)
{
os_kprintf("Processing PONG buffer (second half)\r\n");
}
void dma_pingpong_example(void)
{
os_err_t ret;
g_pingpong_dma.hard_info.mode = HARD_DMA_MODE_CIRCULAR;
g_pingpong_dma.hard_info.max_size = 65536;
g_pingpong_dma.hard_info.flag = HARD_DMA_FLAG_HALF_IRQ | HARD_DMA_FLAG_FULL_IRQ;
g_pingpong_dma.cbs.dma_half_callback = dma_pingpong_half_handler;
g_pingpong_dma.cbs.dma_full_callback = dma_pingpong_full_handler;
ret = soft_dma_init(&g_pingpong_dma);
if (ret != OS_SUCCESS)
{
os_kprintf("soft_dma_init failed, ret=%d\r\n", ret);
return;
}
g_pingpong_ring.buff = g_ping_buffer;
g_pingpong_ring.size = 1024; /* ping + pong = 512 + 512 */
g_pingpong_ring.head = 0;
g_pingpong_ring.tail = 0;
ret = soft_dma_start(&g_pingpong_dma, &g_pingpong_ring);
if (ret != OS_SUCCESS)
{
os_kprintf("soft_dma_start failed, ret=%d\r\n", ret);
return;
}
os_kprintf("Ping-pong DMA started\r\n");
}注意:
soft_dma_t和dma_ring_t必须定义成static/全局,不能是函数栈上的局部变量——因为soft_dma_start会把ring指针存进dma->buffer.buff,DMA 中断回调会持续异步使用它;若ring是局部变量,函数返回后内存失效,回调会访问已失效内存。
6.4 停止和反初始化
void dma_stop_example(void)
{
os_err_t ret;
/* 停止 DMA 传输(soft_dma_stop 返回 os_err_t,不是剩余字节数) */
ret = soft_dma_stop(&g_uart_dma);
if (ret != OS_SUCCESS)
{
os_kprintf("stop DMA failed, ret=%d\r\n", ret);
return;
}
os_kprintf("DMA stopped\r\n");
/* 反初始化 */
ret = soft_dma_deinit(&g_uart_dma);
if (ret != OS_SUCCESS)
{
os_kprintf("DMA deinit failed, ret=%d\r\n", ret);
return;
}
os_kprintf("DMA deinitialized\r\n");
}注意:框架层
soft_dma_stop返回的是错误码os_err_t(OS_SUCCESS/OS_FAILURE),不是"未传输字节数"。想获取已传输字节数应看 HAL 层ops.dma_stop的返回值(它才返回字节数)——但 Soft DMA 框架内部soft_dma_stop调用dma->ops.dma_stop(dma)时丢弃了其返回值,所以框架层不向外暴露字节数。
七、Soft DMA 的适配(接入一个外设)
7.1 先认识适配层:Soft DMA 是怎么"长"到外设驱动里的
与 5.8/5.11/5.12 的"HAL 厂商目录"不同,Soft DMA 不是一个独立设备,而是一个嵌入到具体外设驱动(如串口)里的软件抽象。它的适配工作 = 为某个外设实现 hard_dma_ops_t 四个函数,并把 DMA 中断接到 soft_dma_*_irq。
关键认知:drivers/dma/soft_dma.c 是跨平台统一的框架层,它只管理环形缓冲区、状态机、回调,不碰 DMA 寄存器。真正操作 DMA 的是各外设驱动里的适配代码——例如 ST 的 drivers/hal/st/drivers/drv_usart.c 里就内嵌了串口 DMA 的 Soft DMA 适配(stm32_sdma_* 系列函数)。
| 平台/外设 | 文件 | 适配内容 |
|---|---|---|
| ST 串口 | drivers/hal/st/drivers/drv_usart.c | stm32_sdma_int_*/stm32_sdma_dma_* 实现 ops |
| ST 虚拟串口 | drivers/hal/st/drivers/drv_vuart.c | Soft DMA 接收 |
| nRF 串口 | drivers/hal/nrf/drivers/nrf52/drv_uart.c | Soft DMA 接收 |
| Samsung 串口 | drivers/hal/samsung/drivers/drv_uart.c | Soft DMA 接收 |
7.2 适配的全景图:把外设接入 Soft DMA
┌──────────────────────────────────────────────────────────┐
│ 应用层(Application) │
│ soft_dma_init/start/stop + 三个回调处理数据 │
└──────────────────────────────┬───────────────────────────┘
▼
┌──────────────────────────────────────────────────────────┐
│ Soft DMA 框架层(drivers/dma/soft_dma.c) │
│ soft_dma_* / ring_* / 状态机 / 回调(不改) │
└──────────────────────────────┬───────────────────────────┘
│ hard_dma_ops_t(4 个函数)
▼
┌──────────────────────────────────────────────────────────┐
│ 外设驱动适配层(drv_usart.c 等) ★需实现★ │
│ ①实现 hard_dma_ops_t(get_index/dma_start/dma_stop) │
│ ②DMA/外设中断里调 soft_dma_full_irq/half_irq/timeout_irq│
└──────────────────────────────┬───────────────────────────┘
│ HAL 库
▼
┌──────────────────────────────────────────────────────────┐
│ 硬件(MCU 的 DMA 控制器 + 外设(串口/SPI/ADC)) │
└──────────────────────────────────────────────────────────┘适配的关键洞察:Soft DMA 框架层不用改,只需给外设驱动补上"四个 ops 函数 + 中断接线",外设就获得 DMA 接收能力。
7.3 需要实现的"接口契约":hard_dma_ops_t
以 ST 串口为例(drivers/hal/st/drivers/drv_usart.c,与项目逐字一致):
① get_index —— 报告当前 DMA 已传输位置(两种实现)
/* interrupt rx mode */
static uint32_t stm32_sdma_int_get_index(soft_dma_t *dma)
{
struct stm32_uart *uart = os_container_of(dma, struct stm32_uart, sdma);
UART_HandleTypeDef *huart = uart->huart;
return huart->RxXferSize - huart->RxXferCount;
}
/* dma rx mode */
static uint32_t stm32_sdma_dma_get_index(soft_dma_t *dma)
{
struct stm32_uart *uart = os_container_of(dma, struct stm32_uart, sdma);
UART_HandleTypeDef *huart = uart->huart;
return huart->RxXferSize - __HAL_DMA_GET_COUNTER(huart->hdmarx);
}② dma_start / dma_stop —— 启动/停止 DMA 传输(stm32_sdma_dma_start 用 HAL_UART_Receive_DMA,stm32_sdma_dma_stop 用 HAL_UART_DMAPause/HAL_UART_DMAStop)。
③ 在适配函数里绑定 ops 并选模式(stm32_usart_sdma_init,drivers/hal/st/drivers/drv_usart.c,与项目逐字一致):
static void stm32_usart_sdma_init(struct stm32_uart *uart, dma_ring_t *ring)
{
soft_dma_t *dma = &uart->sdma;
soft_dma_stop(dma);
memset(&dma->hard_info, 0, sizeof(dma->hard_info));
dma->hard_info.max_size = 64 * 1024;
dma->hard_info.flag = HARD_DMA_FLAG_HALF_IRQ | HARD_DMA_FLAG_FULL_IRQ | HARD_DMA_FLAG_TIMEOUT_IRQ;
dma->hard_info.data_timeout = uart_calc_byte_timeout_us(uart->serial.config.baud_rate);
if (uart->huart->hdmarx == OS_NULL)
{
dma->hard_info.mode = HARD_DMA_MODE_NORMAL;
dma->ops.get_index = stm32_sdma_int_get_index;
dma->ops.dma_init = OS_NULL;
dma->ops.dma_start = stm32_sdma_int_start;
dma->ops.dma_stop = stm32_sdma_int_stop;
}
else
{
if (uart->huart->hdmarx->Init.Mode == HARD_DMA_MODE_NORMAL)
{
dma->hard_info.mode = HARD_DMA_MODE_NORMAL;
}
else
{
dma->hard_info.mode = HARD_DMA_MODE_CIRCULAR;
}
dma->ops.get_index = stm32_sdma_dma_get_index;
dma->ops.dma_init = OS_NULL;
dma->ops.dma_start = stm32_sdma_dma_start;
dma->ops.dma_stop = stm32_sdma_dma_stop;
}
}关键理解 —— 怎么决定用哪种模式:先清空
hard_info,再填max_size=64KB、flag(三种中断全开)、data_timeout(由波特率算出)。然后看是否配了 DMA 通道(hdmarx):无通道 → 用中断接收模式(stm32_sdma_int_*);有通道 → 再看 DMA 的Init.Mode,NORMAL就NORMAL、否则CIRCULAR,并绑定stm32_sdma_dma_*。这样同一套 Soft DMA 框架,既支持纯中断接收,也支持真正 DMA 接收,应用层无感知。
接口契约要点:
hard_dma_ops_t 函数 | 职责 | 关键点 |
|---|---|---|
get_index | 报告已传输字节数 | 中断模式用外设计数,DMA 模式用 DMA 计数器 |
dma_init | 初始化(可选) | 可填 OS_NULL,soft_dma_init 判空 |
dma_start | 用 buff/size 启动传输 | HAL_UART_Receive_DMA 等 |
dma_stop | 停止并返回已传输字节数 | HAL_UART_DMAPause/DMAStop |
7.4 适配的标准流程(5 步)
第 1 步:确认外设是否配了 DMA 通道——查外设句柄(如 huart->hdmarx)是否为 OS_NULL,决定用中断模式还是 DMA 模式。
第 2 步:定义 ops 四个函数——按 7.3 实现 get_index/dma_start/dma_stop(dma_init 可省)。
第 3 步:绑定 ops 与 hard_info——在适配函数里根据是否 DMA 通道选 HARD_DMA_MODE_CIRCULAR/NORMAL 并绑定对应 ops(见 stm32_usart_sdma_init)。
第 4 步:中断接线——在 DMA 完成中断、UART 空闲中断里调 soft_dma_*_irq,让框架更新缓冲并触发回调:
/* DMA 接收完成中断(半满/全满) → 调 soft_dma_half_irq / soft_dma_full_irq */
void HAL_UARTEx_RxHalfCpltCallback(UART_HandleTypeDef *huart) { soft_dma_half_irq(&uart->sdma); }
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t size) { soft_dma_full_irq(&uart->sdma); }
/* UART 空闲/超时中断 → 调 soft_dma_timeout_irq(实现不定长帧检测) */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
/* ... 取 uart 后 ... */
os_invalid_dcache(uart->rx_buff, uart->rx_size);
soft_dma_timeout_irq(&uart->sdma);
}第 5 步:填充 soft_dma_t 字段并启动——在 soft_dma_init 前填 hard_info(mode/flag/data_timeout)和 cbs(三个回调),然后 soft_dma_init + soft_dma_start。
7.5 适配检查清单(快速自查)
| 检查项 | 说明 |
|---|---|
get_index 返回递增的已传输字节数? | 否则 soft_dma_updata_buffer 算错 head,数据错乱 |
| DMA 中断接线完整? | 半满→half_irq、全满→full_irq、空闲/超时→timeout_irq,漏接则对应回调永不触发 |
选择了正确的 mode? | 有 DMA 通道用 CIRCULAR,无通道中断模拟用 NORMAL |
环形缓冲大小 ≤ max_size? | 超出时 Soft DMA 会内部 os_calloc 分配硬缓冲(见 soft_dma_circular_buffer_init) |
data_timeout 合理? | 太小则正常数据停顿也会误判帧结束,太大则帧间延迟大 |
| DMA 缓冲 cache 一致性? | DMA 写内存后 CPU 读,需 os_invalid_dcache(如 ST 回调里先 invalid dcache) |
八、注意事项
- 缓冲区大小:环形缓冲区大小需是 2 的幂次,且不能超过
hard_info.max_size - 中断上下文:回调函数在中断上下文中执行,不能进行阻塞操作
- 循环模式:
HARD_DMA_MODE_CIRCULAR模式下 DMA 会持续传输,无需手动重启 - 数据超时:
data_timeout用于检测帧结束,配合超时中断实现不定长数据接收 - 自旋锁保护:
soft_dma_t.lock用于多核/中断保护,HAL 层实现需正确使用 - 及时读取软缓冲:Soft DMA 是"无背压"设计,应用读取太慢会造成软缓冲数据被覆盖(
copy_line_to_ring不做空间检查)。需保证读取速度 > DMA 写入速度,必要时用ring_space主动检测剩余空间
📝 本节小结
核心要点回顾
本节围绕 DMA 设备展开,核心要点如下:
- Soft DMA 抽象层:OneOS 在硬件 DMA 之上的软件抽象层,提供环形缓冲区管理。
- 环形缓冲区:
dma_ring_t(buff/size/head/tail)支持生产者-消费者的无锁数据读写,ring_count/ring_space查询状态,copy_ring_to_line/copy_line_to_ring与线性缓冲互拷。 - 双层缓冲机制:Soft DMA 用线性硬缓冲(
hard_buffs[2],直接交给 DMA 硬件写)对接硬件、用环形软缓冲(buff,用户传入)对接应用,copy_ring_to_ring桥接两者。正常模式 DMA 单次写满即停 → 用两块硬缓冲乒乓切换;循环模式 DMA 自动绕回 → 用一块硬缓冲即可(软缓冲够大时甚至让硬缓冲直接复用软缓冲内存、零拷贝)。 - 三种中断回调:半满(乒乓缓冲)、全满(整块数据处理)、超时(不定长帧结束检测),用
irq_mask & status决定是否触发。 - 硬件适配:HAL 层通过
hard_dma_ops_t(get_index/dma_init/dma_start/dma_stop)适配不同 MCU 的 DMA 控制器。 - 典型应用:串口 DMA 接收、SPI DMA 传输、ADC DMA 采样。
- 关键结构体:
soft_dma_t的timer成员仅在OS_SOFT_DMA_SUPPORT_SIMUL_TIMEOUT下存在且随OS_USING_HRTIMER变化类型;status存"发生了什么中断"、irq_mask存"应用开了哪些中断"。 - 外设接入 Soft DMA 的适配要点:Soft DMA 嵌入外设驱动(非独立设备),需实现
hard_dma_ops_t四函数 + 把 DMA/外设中断接到soft_dma_*_irq。
关键易错点速查
| 易错点 | 正确做法 |
|---|---|
| 环形缓冲区大小非 2 的幂 | dma_ring_t.size 需是 2 的幂次,且不能超过 hard_info.max_size |
| 在中断回调里做阻塞操作 | 半满/全满/超时回调在中断上下文执行,不能阻塞(不能调用睡眠/打印等) |
| 循环模式下手动重启 DMA | HARD_DMA_MODE_CIRCULAR 下 DMA 持续传输,无需手动重启 |
| 忽略超时中断的作用 | data_timeout 用于检测帧结束,配合超时中断实现 UART 不定长接收 |
| 忘用自旋锁保护共享数据 | soft_dma_t.lock 用于多核/中断保护,HAL 层实现需正确加锁/解锁 |
把 soft_dma_stop 返回值当字节数 | soft_dma_stop 返回 os_err_t,字节数在 ops.dma_stop(框架内部丢弃),别混用 |
把 soft_dma_t/dma_ring_t 放栈上 | soft_dma_start 会保存 ring 指针供异步使用,必须 static/全局 |
| 混淆硬缓冲与软缓冲 | hard_buff(线性,DMA 直接写,正常模式两块乒乓)是硬件侧;buff(环形,用户传入)是应用侧;别直接把 DMA 配到 buff 上 |
循环模式硬缓冲大小 > max_size | 循环模式若 ring->size > max_size 框架会另分配硬缓冲并在中断里 copy_ring_to_ring;软缓冲不大时可零拷贝直接读 |
| 软缓冲未及时读取 | copy_line_to_ring 不做空间检查,应用读太慢会覆盖软缓冲旧数据(无背压设计);应保证读取速度 > DMA 写入速度,必要时用 ring_space 检测 |
| DMA 中断漏接线 | 半满→half_irq、全满→full_irq、空闲/超时→timeout_irq,漏接则回调永不触发 |
get_index 返回非递增值 | get_index 须返回已传输字节数,否则 head 算错、数据错乱 |
| 忽略 DMA 缓冲 cache 一致性 | DMA 写内存后 CPU 读需 os_invalid_dcache,否则读到脏数据 |
下一步
掌握了 DMA 设备后,接下来学习 HWCrypto 硬件加密设备:
- 5.14 HWCrypto设备:了解 OneOS 利用 MCU 硬件加密引擎加速哈希与对称加密运算。