☰
嵌入式DMA驱动开发:串口、SPI、ADC与TIM的实战经验
2026/10/6 11:52:58 网站建设 项目流程

这一期聊聊 DMA。我在嵌入式驱动开发这条路上,几乎每个项目都躲不开它:串口要收日志,SPI 要读 Flash,ADC 要采波形,电机要做加减速曲线,统统都需要 DMA 帮忙搬数据。很多刚入门的朋友觉得 DMA 高大上,其实它就是把“CPU 亲自搬数据”这件事外包出去。真正难的不是配置那几十个寄存器,而是怎么把异常场景、缓冲管理、并发访问这些细节处理干净。这篇文章我不打算念芯片手册,而是把我这几年调过的串口 DMA、SPI DMA、ADC DMA 和 TIM DMA 的经验整理成一个可复用的套路,包括完整的代码框架和踩坑记录,希望能帮你少熬几个夜。

1. 为什么嵌入式驱动开发绕不开 DMA

1.1 DMA 的本质:把 CPU 从“搬运工”岗位撤下来

DMA 的全称是 Direct Memory Access,直接内存访问。你可以把它理解成一个专职搬运工:CPU 告诉他“把这批货从仓库 A 搬到仓库 B”,然后搬运工就自己一趟一趟地搬,搬完了喊一声“老板,办完了”,整个过程老板不用参与。对应到嵌入式系统里,仓库 A 就是外设数据寄存器,仓库 B 就是内存缓冲区。搬运的“货”是一个个字节或字。

有人会问,CPU 自己搬不也一样吗?数据量小的时候确实一样。比如串口波特率 9600,一秒钟才 960 字节,中断开销可以忽略。但数据量一大,差异就很明显。拿 1 Mbps 的串口举例,每个字节触发一次中断,就算中断服务函数只花 20 微秒,CPU 有一半时间都耗在应答和状态判断上。这时候如果把搬运工作交给 DMA,CPU 只需要在缓冲区满或者一帧数据收完后处理一次,整体效率能提升一个量级。更典型的场景是外置高速 ADC,比如采样率 50 kHz、16 bit 双通道,光是数据流就接近 200 KB/s,如果每一路都靠 CPU 搬运,系统基本没有余力干其他事。

DMA 驱动难不难?说难也难,说不难也不难。难在细节太多:通道映射、优先级仲裁、缓存一致性、环形缓冲区水位、中断与主循环之间的竞态,随便哪一环没处理好,系统就会出现“看起来在跑但数据是错”的诡异问题。这也是驱动开发和业务开发明显不同的地方:业务代码跑错了,看堆栈基本能定位;DMA 跑错了,往往是一连串无规律的错数,排查起来特别磨人。

1.2 驱动开发的层次:别一上来就怼寄存器

很多教程喜欢贴寄存器配置,但实际工程里我更推荐做分层。底层是芯片厂商的寄存器操作,中间层是平台相关的 DMA 服务,再往上是驱动专用层,最后是业务接口。我见过不少同事直接在业务代码里操作 DMA_CNDTR,刚开始还行,一旦换芯片或者加功能,代码就成了一锅粥。

以串口 DMA 为例,推荐的分层大概是这样的:

  • 硬件层:定义 DMA 通道、外设请求、内存地址;
  • 中间层:封装初始化、启动传输、停止传输、中断处理;
  • 驱动层:负责串口帧的缓冲与解析,比如环形缓冲区、空闲帧检测;
  • 应用层:只调 uart_recv/uart_send,不关心底下是 DMA 还是轮询。

这样做的好处是,DMA 的优势可以被整个系统复用。我做过的项目里,同样一套串口抽象,既能跑在 STM32F4 上,也能跑在 GD32、AT32 甚至 HC32 上。不同 MCU 的 DMA 控制器差异不小,但驱动层接口可以保持稳定。

1.3 不同芯片的 DMA 差异:选型和移植要注意什么

这些年经手过的 MCU 里,STM32 的 DMA 是大家最熟悉的,但千万别以为 GD32、AT32 可以当作“完全平替”。GD32 的 DMA 命名和 STM32 相似,但寄存器时序和部分标志位行为有差异,比如有些型号需要额外等待外设请求同步;AT32 的 DMA 控制器在多通道请求映射上更灵活,但库函数风格又偏 TSMC 那一套,代码迁移时容易踩“看起来能用、实际上没有搬”的坑;HC32F460 的 DMA 使用 DDL 外设库,带有描述符表的概念,适合做链式传输,但学习成本会高一些。

我在给不同平台做驱动选型时,一般先看三件事:第一,目标外设有没有对应的 DMA 请求线;第二,DMA 通道是否支持内存到内存模式;第三,DMA 支持普通(一次性)还是循环模式。这三个问题确认完,基本就知道这个芯片适配串口 DMA 容不容易了。下面的表是我做过平台的一点对比:

芯片系列DMA 控制器循环模式链式传输缓存一致性处理
STM32F4/F7/H7DMA1/DMA2 + BDMA支持支持H7 需特别处理 D-Cache
GD32F4xxDMA0/DMA1支持支持视具体内核而定
AT32F435DMA 多通道支持支持大部分无 D-Cache 问题
HC32F460DMA 描述符支持支持无 D-Cache,但注意内存边界

这张表不是让你记型号,而是想强调:DMA 驱动不是“一套代码走天下”,换芯片时必须对照参考手册重新确认请求映射和状态标志逻辑。

2. 串口 DMA 驱动:从原理到工程落地

2.1 接收为什么要用循环模式,发送为什么要用普通模式

串口接收数据是“持续不断”的,你不知道一帧数据何时开始、何时结束,所以接收端最合适的做法是启用 DMA 循环模式(circular mode),让数据不断写入内存环形缓冲区。同时配合串口空闲中断(IDLE Interrupt)来判定一帧结束。发送端刚好相反,你要发的内容长度是确定的,一次搬完就完事,所以用普通模式(normal mode),并在传输完成中断里做帧尾清理。

有些朋友会把接收做成“一收一中断”,这样能逐字节处理,但代价是 CPU 被打断的次数太多,失去了 DMA 的意义。正确思路是:让 DMA 一直在后台跑,CPU 只在三种情况下介入——缓冲区半满、缓冲区全满、串口检测到空闲。这三种情况分别处理的业务逻辑不同:

  • 半满中断:表示缓冲区的下半部分可被处理,先搬走上半部分数据;
  • 全满中断:表示上半部分数据已被处理完,可以继续搬走下半部分;
  • 空闲中断:表示当前没有新数据进来,往往意味着一帧完整数据已到达。

这里最关键的是环形缓冲区水位计算。我常用的方案是缓冲区长度取 2 的幂次,比如 256、512,然后用位与运算代替取模,快又简单。每次在中断里通过当前 DMA 计数寄存器计算“尾指针”,与逻辑“头指针”做差,得到可读数据长度。具体公式后面代码部分会展开。

2.2 环形缓冲区 + 空闲中断:串口 DMA 接收的标准模型

我用过最好的串口 DMA 接收模型是这样的:DMA 循环模式,数据寄存器地址固定,内存地址指向一个环形缓冲区。当串口空闲时,硬件会置 IDLE 标志,我们在中断里读取“DMA 当前还有多少字节没搬完”的寄存器,算出新收到的数据区间,然后更新环形缓冲区的写指针。

为什么一定要“空闲中断”?因为串口数据是流式的,DMA 本身不知道‘帧’在哪里结束。空闲中断提供了天然的帧边界:当总线空闲超过一个字节时间,就认为当前数据帧结束。这样协议解析器拿到的是一个完整帧,而不是被拆散的字节流。对于 Modbus、YModem、AT 指令这类协议,这个特性尤其好用。

需要注意的是,很多 MCU 的“空闲中断”不是每个串口都有,或者叫法不同。STM32 和 GD32 的 UART 有 IDLE 标志,AT32 也有类似功能,但某些新系列可能叫“line idle”。移植前先查参考手册,别想当然。

2.3 发送 DMA 的几个隐藏坑:先关闭,再配置,再启动

发送 DMA 看起来简单,实际项目里出 bug 最多的反而是发送。最常见的一个是:上一次 DMA 传输还没结束,你又开始配置新的 DMA 传输,结果数据长度被 DMA 硬件改掉,发送内容就乱了。更常见的是,关闭 DMA 和清除中断标志的顺序反了,导致发送完成后进入一次多余的完成中断。

我归纳的安全发送序列是这样的:

  1. 调用 DMA_Cmd 或对应的硬件关闭函数,使能 DMA 通道停止;
  2. 等待 DMA 通道状态寄存器确实变为 Disabled,或者延迟几个周期;
  3. 清除发送完成标志 TC,防止脏标志残留;
  4. 重新设置内存地址、传输长度,明确“源地址是内存,目标地址是串口数据寄存器”;
  5. 启动 DMAC,最后使能 UART 的 DMA 发送请求。

这个序列看起来啰嗦,但可以规避 90% 以上的发送异常。另一个容易踩的坑是发送大帧时,用户直接操作发送缓冲区,导致 DMA 读取时缓冲区内容已经被业务任务覆盖。解决办法是发送时做好指针所有权管理,要么拷贝到独立发送缓冲区,要么在发送完成回调之后再释放原缓冲区。驱动层一定要和业务层约定清楚“缓冲区生命周期”。

3. SPI、ADC、TIM:三个高频 DMA 场景实战

3.1 SPI DMA:什么时候用,什么时候反而更慢

项目里经常有人问,SPI 到底要不要上 DMA?我的判断标准是看单次传输数据量。如果只是读一个寄存器,那用普通函数加中断就够了,DMA 的初始化开销反而让传输变慢。但如果数据量超过 16 字节,尤其像读 Flash、刷屏幕、采集传感器连续数据,DMA 几乎是必须的。

SPI DMA 和串口 DMA 的区别在于:SPI 是同步通信,DMA 不仅要搬收数据,还必须同时产生发送数据,否则主模式会因为没有写数据而停止时钟。因此主 SPI 模式的 DMA 驱动一定是“发送 DMA + 接收 DMA”成对出现的,即使你只关心收,也得提供一个假的发送缓冲区用来填充时钟。这是我见过新手最容易忽略的点,能读出数据但时钟波形异常,或者读一半卡死,多半是只配了 RX DMA 没配 TX DMA。

SPI DMA 还有个特性要注意:CS 片选信号和 DMA 传输的配合。在 SPI 主模式下,片选通常由软件控制,你必须在 DMA 传输启动前拉低 CS,传输完成后等 SPI 总线空闲(检查 BSY 标志)后再拉高 CS。我做过 W25Q128 Flash 的驱动,如果 CS 拉高太早,最后一个字节的末尾时钟还没发完,数据就会错位。解决办法是在关闭 DMA 和禁止 SPI 之前,轮询等待 SPI_SR 的 BSY 位清零。

3.2 ADC 多通道 DMA 采集:锯齿波错位与乒乓缓冲

ADC 连续采集场景里,DMA 几乎是标配。以 STM32 多通道扫描模式为例,ADC 每完成一次转换就会触发 DMA 请求,把结果搬运到内存数组。如果数组按通道顺序排列,业务代码可以直接按索引取数。这看起来很简单,但多通道数据错位是我被问得最多的问题。

为什么错位?ADC 的通道切换顺序和 DMA 搬运顺序是同步的,但如果中途发生过一次转换异常,或者 DMA 启动前 ADC 已经开始转换,数组里的通道顺序就可能整体偏移一个位置。解决办法有两个:一是每次 DMA 搬运完成后重置 ADC 的 DMA 指针到 buffer 起始位置;二是使用“双缓冲”或“乒乓缓冲”方案,一组数据在采集时,另一组数据给业务处理,处理完成后再切换。乒乓缓冲虽然增加了一点内存开销,但彻底避免了对同一数组边写边读导致的脏数据问题。

外置高速 ADC 场景更依赖 DMA。比如安富莱 AD7606 这种 8 通道 16 位同步采样芯片,如果通过并行总线接 MCU,一次要读 16 个字节;通过 SPI 模式接 MCU,更是需要连续读多个帧。此时不用 DMA,CPU 会被中断吞没。ADS127L11 这类高分辨率 ADC 在连续读数模式下,对 SPI 时钟和 DMA 带宽都有要求,驱动配置时要特别关注高速模式下的 SPI FIFO——有些 MCU 的 SPI FIFO 深度不足,DMA 一停,后续数据就会溢出。

3.3 TIM + DMA Burst:让硬件自己生成复杂波形

这个功能知道的人不多,但好用得惊人。所谓 TIM+DMA Burst,就是定时器事件触发 DMA,把一组数据搬运到定时器的多个寄存器,比如 ARR、CCR1、CCR2。每更新一次定时器,DMA 就搬运一次数据,从而生成连续可变的 PWM 波形,实现梯形加减速、呼吸灯、SPWM 逆变器这类需求。CPU 只需要维护一张波形表,剩下的重复搬运全部交给 DMA。

以步进电机加减速为例,传统做法是每个定时器中断里修改一次 ARR,中断频率高到一定程度 CPU 就顶不住了。用 TIM+DMA Burst 后,你可以预先算好从启动频率到目标频率的 ARR 序列,DMA 按节奏逐次写入。定时器更新事件本身就是 DMA 请求源,完全不需要中断。使用时要确认所选 MCU 的 DMA 支持“burst request”,STM32 的 TIM1/TIM8 支持,GD32 也有类似功能;配完以后建议用示波器看 PWM 频率变化是否平滑,曲线异常多半是数据序列长度或 DMA 触发时机没算对。

4. 手把手实现一个可用的串口 DMA 驱动

4.1 平台准备:我用 STM32F407 做基准

这一节我用 STM32F407 作为基准平台,用 USART1 + DMA1 实现串口 DMA 收发。为什么选这个组合?F407 资源多、资料全,网上能找到大量对照例程,适合用来讲原理。你在 GD32、AT32 上移植时,只要替换 DMA 通道请求号和库函数名就行。

先初始化 DMA。接收用循环模式,发送用普通模式,代码如下:

void uart_dma_init(void) { // USART1_RX -> DMA1_Stream5 通道4 // USART1_TX -> DMA1_Stream4 通道4 DMA_InitTypeDef dma; __HAL_RCC_DMA1_CLK_ENABLE(); dma.PeriphInc = DMA_PINC_DISABLE; dma.MemInc = DMA_MINC_ENABLE; dma.PeriphDataWidth = DMA_PDATAALIGN_BYTE; dma.MemDataWidth = DMA_MDATAALIGN_BYTE; dma.Priority = DMA_PRIORITY_HIGH; // 接收 DMA:循环模式 dma.Direction = DMA_PERIPH_TO_MEMORY; dma.Init.Mode = DMA_CIRCULAR; dma.Init.PeriphBaseAddr = (uint32_t)&USART1->DR; dma.Init.MemBaseAddr = (uint32_t)rx_buf; dma.Init.BufferSize = RX_BUF_SIZE; HAL_DMA_Init(&rx_dma_handle); HAL_NVIC_EnableIRQ(DMA1_Stream5_IRQn); // 发送 DMA:普通模式 dma.Direction = DMA_MEMORY_TO_PERIPH; dma.Init.Mode = DMA_NORMAL; HAL_DMA_Init(&tx_dma_handle); }

这里故意省略了 HAL_DMA_DeInit 等细节,实际项目里建议在初始化前先调用一次。另外,如果你用的芯片没有 HAL 库,比如裸机搞寄存器,对应“设置方向、设置循环模式、设置地址、设置长度、使能通道”这套顺序是不变的。

4.2 中断处理:如何识别接收长度并更新环形缓冲区

当 USART1 产生 IDLE 中断和 DMA 产生半满/全满中断时,都要汇入同一个处理函数。这个函数做的事情可以概括为“取出 DMA 已经搬入缓冲区的数据长度,交给环形缓冲区记账”。

void uart_dma_irq_handler(void) { uint32_t pos = RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(&rx_dma_handle); while (pos != rtail) { rb_write(&rx_ring, rx_buf[pos]); pos = (pos + 1) % RX_BUF_SIZE; } rtail = pos; }

这里 rtail 是逻辑尾部,DMA 的计数器寄存器告诉硬件还差多少字节填满缓冲区,用缓冲区总长度减去它,就是硬件已经搬完的位置。有的库函数叫 DMA_GetCurrDataCounter,有的叫 DMA_CNDTR,本质都一样。要特别注意:读 DMA 计数寄存器时,必须和外设中断标志之间建立正确的前后关系。先读计数器、再清除标志,否则可能把一个空帧识别成一帧数据。

半满中断和全满中断属于 DMA 中断,处理思路类似,但要注意和 IDLE 中断之间的优先级关系。我的习惯是 IDLE 优先级高于 DMA 半满/全满,因为帧结束的完整性判断优先,数据搬运处理可以稍微晚一点。

4.3 发送接口:一次简单但安全的 DMA 发送

发送接口封装成下面这种形式,已经足够应付大多数协议:

int uart_dma_send(const uint8_t *data, uint16_t len) { if (tx_busy) return -1; tx_busy = 1; memcpy(tx_buf, data, len); // 拷贝到专用发送缓冲区 __HAL_DMA_DISABLE(&tx_dma_handle); __HAL_DMA_CLEAR_FLAG(&tx_dma_handle, DMA_FLAG_TC); tx_dma_handle.Init.BufferSize = len; HAL_DMA_Start_IT(&tx_dma_handle, (uint32_t)tx_buf, (uint32_t)&USART1->DR, len); __HAL_UART_ENABLE_DMA(&uart_handle, UART_DMA_TX); return 0; }

这段代码看起来平平无奇,但每个操作都有讲究。先判断 tx_busy 是为了防止上一次还没发完,业务层又来发,导致数据重叠;先关闭 DMA 是为了保证配置生效时不带脏状态;使用拷贝后的专用发送缓冲区是为了防止业务层在发送中改写数据;最后开启 UART DMA 发送请求,可以确保 DMA 输出数据被真正移入串口发出去。

发送完成中断里,我一般做两件事:清掉 DMA 的 TC 标志和 UART 的 TC 标志,再把 tx_busy 置 0。如果业务需要“发送完成”事件,可以在这里回调。注意不要把长耗时的业务处理放进中断,发送下一帧之前需要先让当前帧走完,否则又会撞车。

4.4 测速与带宽验证:怎么确认 DMA 真的发挥作用

调完驱动后,不能只看“能收到数据”,还要验证性能。我常用的办法是:在 DMA 搬运完成中断里翻转一个 GPIO,接逻辑分析仪或者示波器看翻转周期的下限。如果 GPIO 翻转频率远高于预期,说明 DMA 中断处理得很轻盈,CPU 负荷低;如果翻转频率上不去,说明中断里做了太多事,需要优化。

如果你想串行测速,比如每秒处理多少字节,可以在应用层做一个计数器,每处理 1000 帧打印一次帧长和耗时。找“DMA 测速失败”问题的时候,重点要看是不是任务被打断、帧数据被拆,而不是 DMA 本身慢。很多所谓 DMA 测速失败代码,其实是没关全局中断或是在中断里调用打印函数,导致测量结果失真。

5. 常见坑与调试经验:DMA 测速失败和缓存一致性

5.1 一张速查表:症状、原因、解决

先把这几年遇到的典型 DMA 问题整理成了表,碰到类似现象可以直接对照:

现象常见原因解决思路
接收到的数据错位没有等 DMA 传输完成就覆盖缓冲区引入双缓冲,或等 TC 标志后再处理
丢第一帧字节DMA 使能和串口请求使能顺序反了先使能 DMA 通道,再使能外设 DMA 请求
发一串数据只发出去一半发送缓冲区被业务层提前修改拷贝到专用发送缓冲区后再启动 DMA
高速串口偶发丢字节UART 溢出标志未处理,DMA 停卡中断里读取 SR,清除 ORE 标志
数据看起来是旧数据D-Cache 造成缓存一致性问题对 DMA 缓冲区做 invalidate,或配置 MPU 为 non-cacheable
DMA 搬运到一半停止内存访问仲裁或链表配置错误检查 DMA 优先级、系统总线负载
定时器 DMA 波形异常DMA Burst 传输长度与寄存器地址不对按参考手册配置突发计数和地址增量

这张表中,最容易被忽视的是缓存一致性问题。STM32F4 没有 D-Cache,问题不大;但 H7 系列如果不开 MPU,DMA 读取的缓冲区内容可能和 CPU 看到的不一致。解决办法有两种:一种是用SCB_CleanDCache_by_Addr在 DMA 启动前把 CPU 写过的数据刷到内存;另一种是直接把 DMA 缓冲区放在 MPU 配置的 non-cacheable 区域。第二种更省心,缺点是访问效率略降,一般 DMA 缓冲区完全能接受。

5.2 我踩过的几个关键坑

第一个坑是 DMA 的 Continuous Requests。一开始我误解成“只要设置了循环模式就能不断传输”,后来发现有些外设必须由硬件请求信号来触发 DMA,比如 SPI 每收到一个字节才产生一次请求。循环模式不会自动产生请求信号,所以必须把 DMA 的 Continuous Request 位打开,才能在没有外设请求时主动连续搬运。这个位在寄存器里通常叫 CIRC 或者类似字段,配置错误会导致 DMA 从不启动。遇到“DMA 卡住不动”的现场,先去看外设请求使能位和 DMA 循环模式位,别急着怀疑硬件。

第二个坑是中断里处理缓冲区的竞态。我曾在一个项目里用 UART 接收 GPS 数据,主循环里解析缓冲区,接收中断里往缓冲区写。由于写和读都在同一个缓冲区,如果不保证临界区,解析线程会读到半个写入的帧。后来我改成“中断只搬数据,不解析;主循环关闭中断后取数据”,问题就消失了。记住一句话:在中断里更新跨任务共享的数据时,维护一个不可重入的临界区,比任何花哨设计都可靠。

第三个坑是 DMA 中断在低功耗模式下的表现。部分芯片进入低功耗睡眠后,DMA 时钟会被关闭,如果业务需要在睡眠中采集数据,需要在唤醒时重新配置 DMA。我在一个用电池供电的环境监测项目里,就因为漏了重新初始化 DMA,导致休眠唤醒后串口接收整个失灵。后来我把 DMA 的重新初始化挂在系统 resume 回调里,才算彻底解决。

5.3 面试常问的 DMA “八股”:实际工程中怎么答

每次聊到招人,都会有人问嵌入式面试里常考的 DMA 题。这种题不是背概念,而是看你对本质有没有理解通。

第一个问题:DMA 和 CPU 谁快?从峰值带宽看,现代 MCU 的 DMA 和 CPU 都能在几个时钟周期内搬一个 32 位字,差异不大。但 DMA 不占用 CPU 流水线,不影响指令执行,所以在系统层面 DMA 更划算。回答时最好补充一句:DMA 减少了上下文切换和中断延迟,所以系统吞吐更高。

第二个问题:循环 DMA 和普通 DMA 区别?普通 DMA 搬完指定长度后停止;循环 DMA 搬到末尾后自动回卷继续搬,不需要 CPU 介入,适合连续流数据。项目里接收持续数据用循环模式,发送一次性数据用普通模式。

第三个问题:DMA 中断里为什么不能处理复杂逻辑?因为中断里如果执行耗时的函数,会阻塞整个系统的实时性。DMA 中断处理的核心是快速记账和缓冲切换,真正解析、计算、写 Flash 的操作都应放到主循环或者低优先级线程。回答时能加上这个“为什么”,面试官基本就认可你是有实战经验的。

6. 从 DMA 驱动到嵌入式软件架构:一点个人体会

6.1 驱动接口的抽象:换芯片不伤筋动骨

写 DMA 驱动这几年,我最大的体会是:寄存器配置只是开始,真正值钱的是驱动接口抽象。如果你把串口 DMA 封装成三个核心操作——init、send、receive,上层业务根本不需要知道底层是 DMA 还是中断。这样即使中间换芯片,只要保证接口行为一致,上层代码一行都不用改。

我在多个项目里就是这么做的。底层从 STM32 切到 GD32 时,我把所有寄存器操作集中放在一个文件里,外部接口完全不动;切到 AT32 时,只需要改 DMA 通道号和库函数名。当然,这种抽象有代价:每个芯片的 DMA 中断标志位不同,封装层代码量会多一些。但和后面项目迭代节省的时间相比,这点成本根本不算什么。

6.2 多用工具,别凭感觉调试

调试 DMA 问题,逻辑分析仪和示波器是我的老朋友。用逻辑分析仪抓串口波形,可以直观看到 DMA 发送的数据和中断时序是否匹配;用示波器抓 GPIO 翻转,可以测量中断响应时间。遇到过 DMA 看似正常但数据总错一位的诡异问题,最后就是用逻辑分析仪对比 SPI 时序才发现,CS 拉高太早导致最后一个时钟周期不完整。

软件工具方面,我习惯把 DMA 缓冲区的内容打印成十六进制数组,逐一核对帧头、帧尾和长度字段。不同芯片固件库的寄存器视图各有差异,但只要把“当前 DMA 计数寄存器”和“最后一次处理的位置”这两个值打印出来,就能立刻判断是否丢数据。

6.3 下一期打算聊聊驱动中的敏捷开发方法

最后说一个最近在尝试的方向。团队里做驱动的同学越来越多,感觉驱动开发也需要节奏感。现在圈子里在讨论一种叫 bmad-method 的 AI 驱动敏捷开发框架,核心思路是把一个驱动任务拆成若干个可验证的小迭代,每个迭代完成后立刻用自动化测试做回归。我最近在研究怎么把它应用在 DMA 驱动的单元测试上:先把 DMA 搬运、环形缓冲区、协议解析拆成独立的模块,然后用模拟的 DMA 中断源做持续集成,这样每次改代码都能快速知道有没有破坏旧功能。

我个人的习惯是:DMA 写完之后,先跑一遍连续 12 小时的高压收发测试,再开始写业务逻辑。测试期间只要有任何一帧数据错位、丢失、乱序,都要回到驱动层查原因,而不是用业务逻辑去打补丁。如果你正在为 DMA 的“偶发丢数据”头疼,我的建议是先怀疑缓冲区管理和中断竞态,再怀疑硬件,最后才怀疑芯片型号有问题。这样排查方向基本不会错,也能让你在嵌入式驱动开发这条路上越走越顺。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询