2. DMA配置实战:CubeMX配置、MDK工程建立、外设到内存传输、内存到外设传输
DMA这东西,说白了就是帮CPU干苦力的。CPU本来要亲自把数据从外设搬到内存,或者从内存搬到外设,现在DMA代劳了,CPU就能腾出手来处理更重要的逻辑。我刚开始接触DMA时,总觉得配置起来很麻烦,后来发现只要理清三个要素——源地址、目的地址、传输长度——剩下的就是工具链的事了。
这一章我们直接动手。从CubeMX生成代码开始,到MDK里建立工程,再到两种经典传输模式:外设到内存、内存到外设。你跟着走一遍,以后DMA配置就是肌肉记忆。
2.1 用CubeMX配置DMA
我个人习惯,所有外设初始化都先在CubeMX里搞定。图形化界面点一点,比手写寄存器快得多,也不容易漏配置。
2.1.1 选择DMA通道
打开CubeMX,选好你的芯片型号(我以STM32F407为例)。在Pinout & Configuration标签页里,找到DMA设置。你会看到DMA1和DMA2两个控制器,每个控制器下面有多个Stream(数据流)。
选哪个Stream?记住一个原则:外设的DMA请求是固定的。比如USART1的发送请求只能连到DMA2的Stream7,接收请求只能连到Stream5。你可以在参考手册的DMA请求映射表里查到。
| 外设 | 请求方向 | DMA控制器 | Stream | 通道 |
|---|---|---|---|---|
| USART1_TX | 内存→外设 | DMA2 | Stream7 | 通道4 |
| USART1_RX | 外设→内存 | DMA2 | Stream5 | 通道4 |
| SPI1_TX | 内存→外设 | DMA2 | Stream3 | 通道3 |
| ADC1 | 外设→内存 | DMA2 | Stream0 | 通道0 |
嗯,这里要注意:同一个Stream不能同时服务两个外设。我在项目里就踩过这个坑,ADC和定时器抢同一个Stream,结果数据全乱了。
2.1.2 配置参数
选好Stream后,点Add按钮,配置以下参数:
- Direction:Peripheral to Memory 或 Memory to Peripheral
- Priority:一般选Medium,实时性要求高的选High
- Mode:Normal(单次传输)或 Circular(循环传输)
- Data Width:Byte、Half Word、Word,必须和外设寄存器宽度一致
- Increment Address:外设地址一般不递增,内存地址递增
2.1.3 生成代码
配置完点Project → Generate Code。CubeMX会自动生成MX_DMA_Init()函数,以及外设的DMA初始化代码。你不需要手动写寄存器,但建议打开生成的代码看一眼,理解HAL库是怎么配置的。
2.2 MDK工程建立与代码整合
CubeMX生成的代码是HAL库风格的。如果你用MDK(Keil),直接打开生成的.uvprojx文件就行。我个人习惯把用户代码写在/* USER CODE BEGIN */和/* USER CODE END */之间,这样重新生成代码时不会被覆盖。
2.2.1 工程结构
打开工程后,你会看到这些关键文件:
- main.c:主函数,外设初始化调用
- stm32f4xx_hal_msp.c:外设的GPIO、DMA、NVIC配置
- stm32f4xx_it.c:中断服务函数
- dma.c:DMA初始化函数
别急着写代码。先编译一次,确保工程能通过。我见过太多人上来就写逻辑,结果编译报错一堆,最后发现是CubeMX生成的代码版本和HAL库不匹配。
2.2.2 添加用户代码
在main.c的USER CODE区域,添加DMA传输的启动函数。比如:
/* USER CODE BEGIN 2 */
HAL_UART_Transmit_DMA(&huart1, tx_buffer, BUFFER_SIZE);
HAL_UART_Receive_DMA(&huart1, rx_buffer, BUFFER_SIZE);
/* USER CODE END 2 */
启动后,DMA会自动工作。你只需要在传输完成中断里处理数据。
2.3 外设到内存传输实战
这个场景最常见:ADC采集数据、UART接收数据、SPI读取传感器数据。数据从外设寄存器自动搬到内存数组里,CPU不用管。
2.3.1 配置要点
- 外设地址:固定不变,比如ADC的DR寄存器地址
- 内存地址:递增,指向一个数组
- 传输长度:等于你要采集的数据个数
- 数据宽度:和外设寄存器位宽一致,ADC一般是16位
核心逻辑:外设每产生一个数据,DMA就自动搬运一次。搬运完指定次数后,触发传输完成中断。
2.3.2 代码示例
以ADC多通道采集为例:
#define ADC_BUFFER_SIZE 100
uint16_t adc_buffer[ADC_BUFFER_SIZE];
// 启动DMA传输
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, ADC_BUFFER_SIZE);
// 传输完成回调
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
// 数据已经在adc_buffer里了,直接处理
ProcessADCData(adc_buffer, ADC_BUFFER_SIZE);
// 重新启动下一次采集
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, ADC_BUFFER_SIZE);
}
你看,主循环里根本不用管数据搬运的事。CPU可以专心做数据处理、显示、通信。
2.4 内存到外设传输实战
反过来,把内存里的数据发送到外设。比如UART发送数据、SPI发送波形数据、DAC输出波形。
2.4.1 配置要点
- 内存地址:递增,指向发送数据数组
- 外设地址:固定不变,比如UART的DR寄存器
- 传输长度:要发送的数据字节数
- 循环模式:如果你要持续输出波形,用Circular模式
2.4.2 代码示例
以UART发送为例:
#define TX_BUFFER_SIZE 256
uint8_t tx_buffer[TX_BUFFER_SIZE];
// 填充数据
sprintf((char*)tx_buffer, "Hello DMA!\r\n");
// 启动DMA发送
HAL_UART_Transmit_DMA(&huart1, tx_buffer, strlen((char*)tx_buffer));
// 发送完成回调
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
// 发送完成,可以准备下一包数据
PrepareNextPacket();
}
用DMA发送UART数据,CPU占用率几乎为零。我做过一个项目,需要同时用4个UART口以115200波特率发送数据,用DMA后CPU占用率不到5%。
2.5 两种传输模式对比
| 对比项 | 外设→内存 | 内存→外设 |
|---|---|---|
| 典型应用 | ADC采集、UART接收 | UART发送、DAC输出 |
| 地址递增 | 外设固定,内存递增 | 内存递增,外设固定 |
| 触发源 | 外设数据就绪 | 外设发送请求 |
| 循环模式 | 连续采集数据 | 持续输出波形 |
| 中断触发 | 传输完成或半传输 | 传输完成或半传输 |
2.6 知识结构图
下面这张图帮你理清DMA配置的整体流程:
2.7 避坑指南
最后分享几个我实际项目中踩过的坑:
- DMA时钟没使能:CubeMX一般会自动处理,但如果你手动改过时钟树,记得检查RCC里DMA的时钟是否打开。
- 缓冲区对齐:DMA传输要求缓冲区地址按数据宽度对齐。比如Word传输,缓冲区地址必须是4的倍数。我遇到过数组定义在非对齐地址上,结果DMA传输数据错位。
- 中断优先级:DMA传输完成中断的优先级不要设得太低,否则在高负载下可能丢数据。我一般设成比主循环高、比实时中断低。
- 半传输中断:Circular模式下,用半传输中断可以实现双缓冲。数据填满前半段时,CPU处理前半段,DMA继续填后半段,效率翻倍。
好了,这一章的内容就是这些。你跟着配置一遍,跑通两个例程,DMA的基本功就算打牢了。以后遇到大数据量传输的场景,你第一个想到的就会是DMA。