news 2026/9/1 6:28:34

DMA固件开发指南:从原理到串口实作

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DMA固件开发指南:从原理到串口实作

简介:DMA固件开发指南[源码]定位于嵌入式系统开发者,针对直接内存访问(DMA)控制器在固件层面的设计、配置与调试,帮助读者掌握块传输、请求传输及级联传输等典型模式,并理解如何通过DMA降低CPU负担、提升系统吞吐。压缩包共4个文件,包含一个C源码示例(dma_demo.c)、工程配置(dma_demo)、版本管理忽略文件(.gitignore)及在线运行环境描述(.inscode),整体仅8KB,轻量且便于对照学习。内容覆盖从需求分析、硬件接口配置到软件设计的关键步骤,并给出DMA控制器初始化、启动传输及中断处理的示例代码;同时总结了数据不完整、性能瓶颈等常见问题的排查思路与调试技巧。已有397人学习下载,适合具备一定嵌入式基础、希望快速上手DMA固件开发的工程师。 做嵌入式固件这行十多年,DMA 是我用过就回不去的硬件外设之一。早年在 STM32F103 上做串口数据采集,CPU 动不动就被中断打满,后来把串口、ADC、SPI 全部切换到 DMA 搬运,CPU 占用率从 60% 直接降到 5% 以内。这次整理这份 DMA 固件开发指南,不打算把参考手册给你翻译一遍,而是把我实际做 DMA 固件时踩过的坑、验证过的思路和可以直接抄的源码结构整理出来。内容涵盖 DMA 的核心原理、传输模式选型、句柄关联、串口 DMA 收发完整实现,以及从 MCU 到 Zynq/DSP 的差异。适合正在做 STM32、GD32、Zynq、DSP 这类平台固件的开发者,尤其是刚把 DMA 纳入自己项目、对中断和内存管理还不太熟的朋友。看完这篇,你应该能少走很多弯路。

1. 为什么固件开发离不开 DMA

1.1 DMA 到底解决了什么问题

CPU 本质上是个执行指令的机器,而外设收发数据时,CPU 要一条一条地把数据从寄存器搬到内存、再从内存搬到外设。这个搬运过程本身不产生任何业务价值,却占用了 CPU 最宝贵的执行周期。DMA 做的事情很简单:把"搬运"这个动作接过去,让 CPU 去跑协议栈、跑业务逻辑、跑算法。

我用一个生活化的类比。你点了一份外卖,骑手送到楼下,如果每一份外卖都要你亲自下楼跑一趟去拿,你一天什么事都别干了。DMA 就相当于物业的快递柜,外卖到了直接放进柜子,你攒到合适的时候一次性去取。CPU 该写代码写代码,该算数据算数据,只有快递柜满了(缓冲区满/传输完成)才需要你去处理一下。

1.2 什么样的固件场景需要 DMA

不是所有场景都需要 DMA。判断标准很简单:数据量大不大、频率高不高、CPU 还忙不忙。以下是我实际项目里用 DMA 最多的几类场景:

  • 串口数据流:比如 4G 模块、GPS、LoRa 这类持续往外吐数据的设备。一个 115200 波特率的串口,每秒大概产生 11.5KB 数据,用中断接收的话每字节触发一次中断,CPU 几乎被钉死。换成 DMA 接收 + 空闲中断,CPU 只在收到一帧完整数据时才参与处理。
  • ADC 连续采样:电力监测、音频采集这类需要以几十 kHz 以上频率连续采样的场景,定时器触发 ADC,ADC 用 DMA 把结果持续搬运到内存环形缓冲区,CPU 只需要定期处理已经采集到的数据块。
  • 存储类应用:SSD 固件开发里,主机数据通过 PCIe/NVMe 到达控制器后,绝大部分数据搬运都是 DMA 完成的。固件只需要维护描述符和状态位,真正搬数据的是 DMA 引擎。
  • 显示与波形生成:STM32F103 用 PWM+DMA 驱动 WS2812 灯带,或者用 DMA 从查找表往 DAC 搬运波形数据。热词里提到的"dma句柄与dac句柄关联"就是这个方向,后面我会专门讲。

在这些场景里,DMA 不只是"省 CPU",它直接决定了系统能不能支撑更高的吞吐率。判断一个功能要不要上 DMA,我一般会问三个问题:数据是不是持续产生?单次数据量是不是超过几十字节?CPU 是不是同时还在跑协议栈或 UI?如果答案大部分是"是",那就应该用 DMA。

2. DMA 固件设计的核心细节

2.1 传输模式选型:单次、循环还是双缓冲

DMA 的传输模式看起来就几个选项,但选错的人非常多。最常见的三种:

单次传输(Normal):传输次数到了就停,需要重新使能才能再触发。适合一次性的数据块搬运,比如把一帧协议数据从内存搬到 SPI 发送寄存器。

循环传输(Circular):搬完自动重新开始,配合环形缓冲区特别好用。ADC 连续采样、串口接收不定长数据,都用循环模式。

双缓冲(Double Buffer):两个缓冲区轮流用,一个在搬数据,另一个在处理数据。这个模式在高速场景几乎标配,比如音频流、高速 ADC、以太网 DMA 描述符。

选型的核心原则是:看你的数据是"有界的任务"还是"无界的流"。有界任务用单次,无界流用循环,既要无界又要处理低延迟就用双缓冲。我在做串口 DMA 接收时默认选循环模式,理由是串口数据永远不知道什么时候结束,循环模式可以保证 DMA 永远在等数据,不会因为传输次数耗尽而停止接收。

2.2 中断与回调:什么时候该开中断

DMA 传输完成可以产生中断,但开中断不是越多越好。我见过不少项目,DMA 确实用了,但中断里做的事情太多,导致实时性反而变差了。

我的做法是分三层处理:传输完成中断里只置标志位,或者用信号量通知任务层去处理,绝不在中断里做耗时的数据处理。传输一半中断(Half Transfer)很多人忽略,但在音频或 ADC 双缓冲场景里非常好用——DMA 写完前半段缓冲区时触发,CPU 可以处理前半段,DMA 正在写后半段,相当于软双缓冲。错误中断一定要开,DMA 传输错误(溢出、总线错误)如果不处理,系统会安静地丢数据,排查起来极其痛苦。

一个实际经验:在裸机环境里,我习惯把 DMA 完成中断和空闲中断合并到一个 IRQHandler 里处理,先读状态寄存器判断是哪种事件,再分别处理。这样能避免两个中断源抢优先级导致事件丢失。

2.3 DMA 句柄与设备句柄的关联

热词里有一条"dma句柄与dac句柄关联",这其实是个非常关键的固件设计概念。DMA 不是一个孤立的模块,它是给其他外设服务的。在 HAL/LL 库(比如 STM32Cube)里,每个使用 DMA 的外设都有独立的句柄,同时又有 DMA 通道的句柄。

正确的关联方式是这样:外设句柄(如 UART_HandleTypeDef、DAC_HandleTypeDef)负责描述外设本身的工作参数,比如波特率、分辨率、数据格式。DMA 句柄(DMA_HandleTypeDef)负责描述搬运参数,比如源地址、目的地址、数据宽度、传输方向。两者通过__HAL_LINKDMA这样的宏绑定在一起。

绑定的本质是把 DMA 通道信息挂到外设句柄的成员指针上,这样外设启动时(比如HAL_UART_Transmit_DMA),内部会调用HAL_DMA_Start_IT去配置并启动 DMA。

这里踩过一个坑:句柄关联写在某个条件初始化分支里,启动函数在另一个分支里调用,初始化顺序一乱,huart->hdmatx还是空指针,调用HAL_UART_Transmit_DMA直接进 HardFault。所以我养成了一个习惯:在调试器里把外设句柄展开看一遍,确认hdmatxhdmarx都已经指向有效地址,再跑传输。

3. 实操:STM32 串口 DMA 收发的完整实现

3.1 工程配置与内存规划

示例以 STM32G031 为例(手上正好有这个片子,用 LL 库做串口 DMA 特别清爽),思路完全适用于 F1/F4/H7、GD32 等同系列。

先说内存规划。DMA 搬运要保证源地址和目的地址都对齐到数据宽度。串口数据宽度一般是 8bit,对齐要求不高,但如果你要搬 32bit 数据,缓冲区起始地址最好 4 字节对齐。我一般这样声明接收缓冲区:

#define UART_RX_BUF_SIZE 512 static uint8_t uart_rx_buf[UART_RX_BUF_SIZE] __attribute__((aligned(4))); static volatile uint16_t uart_rx_index = 0; /* DMA 当前写到的位置 */ static volatile uint8_t uart_frame_ready = 0;

volatile关键字不能省,因为 DMA 会异步写这块缓冲区,编译器优化时如果不加volatile,CPU 侧很可能读到缓存里的旧值。

LL 库初始化串口 DMA 比 HAL 库直观得多。核心步骤:配置串口参数、使能 UART 的 DMA 请求、配置 DMA 通道、使能 DMA。

void uart_dma_init(void) { LL_USART_InitTypeDef usart_init = {0}; usart_init.BaudRate = 115200; usart_init.DataWidth = LL_USART_DATAWIDTH_8B; usart_init.StopBits = LL_USART_STOPBITS_1; usart_init.Parity = LL_USART_PARITY_NONE; usart_init.TransferDirection = LL_USART_DIRECTION_TX_RX; usart_init.HardwareFlowControl = LL_USART_HWCONTROL_NONE; LL_USART_Init(USART1, &usart_init); LL_USART_EnableDMAReq_RX(USART1); LL_USART_EnableDMAReq_TX(USART1); LL_DMA_SetPeriphAddr(DMA1, LL_DMA_CHANNEL_1, LL_USART_DMA_GetRegAddr(USART1, LL_USART_DMA_REG_DATA_RECEIVE)); LL_DMA_SetMemoryAddr(DMA1, LL_DMA_CHANNEL_1, (uint32_t)uart_rx_buf); LL_DMA_SetDataLength(DMA1, LL_DMA_CHANNEL_1, UART_RX_BUF_SIZE); LL_DMA_SetPeriphSize(DMA1, LL_DMA_CHANNEL_1, LL_DMA_PDATAALIGN_BYTE); LL_DMA_SetMemorySize(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MDATAALIGN_BYTE); LL_DMA_SetMode(DMA1, LL_DMA_CHANNEL_1, LL_DMA_MODE_CIRCULAR); LL_DMA_SetTransferDirection(DMA1, LL_DMA_CHANNEL_1, LL_DMA_DIRECTION_PERIPH_TO_MEMORY); LL_DMA_EnableIT_TC(DMA1, LL_DMA_CHANNEL_1); LL_DMA_EnableIT_HT(DMA1, LL_DMA_CHANNEL_1); LL_DMA_EnableChannel(DMA1, LL_DMA_CHANNEL_1); }

几个细节值得展开:LL_USART_DMA_GetRegAddr获取的是 USART 数据寄存器的地址,DMA 的源地址填

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/1 6:27:33

Overlay叠加层实战:从《我的世界》终末之诗到直播滚动字幕

很多玩《我的世界》的玩家,第一次通关末地,盯着屏幕上那两段缓缓滚动的“终末之诗”时,都会停下手里的鼠标。那感觉不像是在结束一个游戏,更像是读完了一部长篇小说的结尾。如果你恰好是一个喜欢“折腾”的技术玩家,大…

作者头像 李华
网站建设 2026/9/1 6:27:30

YOLOv5斗地主牌面识别与安卓端NCNN部署实战

简介:面向斗地主牌面识别与安卓端AI部署需求的完整解决方案,围绕YOLOv5训练的高精度检测模型best.pt,支持从截图中定位并识别扑克牌花色与数字。资源包共39个文件,约14.38MB,主要包含Python训练推理脚本(in…

作者头像 李华
网站建设 2026/9/1 6:27:20

安卓PS5模拟器SharpEmu深度解析:原理、性能与实测

从去年开始,主机模拟器在安卓平台上的热度就一直没降过。PS2、Switch、Wii 等平台的模拟器已经能在不少手机上流畅运行,不少玩家甚至把安卓掌机当成怀旧游戏机来用。也正因为这个趋势,当网上传出“安卓平台出现首款 PS5 模拟器 SharpEmu”的时…

作者头像 李华
网站建设 2026/9/1 6:26:34

从原理到实战:构建与精调动态压枪系统的完整指南

在游戏辅助工具开发领域,动态压枪数据的精调一直是提升实战效果的核心环节。无论是FPS游戏的新手想要快速上手,还是资深玩家追求极致的操控体验,一套稳定、精准且可维护的压枪方案都至关重要。本文将以实战为导向,深入探讨如何从零…

作者头像 李华
网站建设 2026/9/1 6:26:24

智慧物流调度架构设计:基于GPIO适配异构电梯的机器人梯控实现

摘要: 在智慧楼宇第三方物流集成打通跨层通道的业务中,如果底层硬件采用破线方式去截取不同品牌电梯的通信协议,不仅面临庞大的非标定制成本,还会遭到物业方对设备安全的强烈抗拒。面对异构的机房要求,技术选型必须转向…

作者头像 李华
网站建设 2026/9/1 6:26:21

Linux 之大文件拆分、合并与校验

有时候进行程序开发的时候,某些公司会限定 git 单个提交的文件大小,如果我要提交的某个 xxx.jar 太大导致超过限制的话,可能就没办法将 xxx.jar 文件作为一个整体提交上去。不过,我么可以曲线救国绕过这种限制,使用 sp…

作者头像 李华