STM32F1 DMA 学习复盘笔记
实验场景:存储器到存储器 DMA 搬运(内存→内存),MYDMA 封装库实验 核心痛点:DMA 传输计数器不是字节数,是搬运次数;数据宽度决定搬运字节与地址步长,数组类型必须和 DMA 数据宽度匹配
一、DMA 关键寄存器参数(F1)
- 传输计数器
DMA_SetCurrDataCounter()
- 含义:DMA 搬运多少次,不是多少字节!
- 总搬运字节 = 搬运次数 × 单次搬运字节宽度
PeripheralDataSize外设数据宽度MemoryDataSize存储器数据宽度
两个宽度同时决定两件事:①单次搬运多少字节;②搬运完成后源 / 目标地址自增步长
表格
| 配置宏 | 单次搬运大小 | 地址自增步长 | 适配数组类型 |
|---|---|---|---|
| DMA_MemoryDataSize_Byte | 8bit (1 字节) | +1 | uint8_t |
| DMA_MemoryDataSize_HalfWord | 16bit (2 字节) | +2 | uint16_t |
| DMA_MemoryDataSize_Word | 32bit (4 字节) | +4 | uint32_t |
⚠️重点:只有数组类型和 DMA 数据宽度匹配时,传入的计数器数值才等于数组元素个数。不匹配时不能直接填数组元素数量。
二、实验案例分析
实验:把源数组 4 个元素 DMA 拷贝到目标数组,函数调用:MyDMA_Init(源地址,目标地址,4);
案例 1:uint32_t DataA 4、uint32_t DataB 4
情况 A:DMA 配置 Word(32bit)✅正确
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
MyDMA_Init((uint32_t)DataA,(uint32_t)DataB,4);
- 每次搬运 4 字节,地址 + 4;搬运 4 次
- 效果:
DataA[0]→DataB[0]、DataA[1]→DataB[1]、DataA[2]→DataB[2]、DataA[3]→DataB[3],4 个元素完整拷贝。
情况 B:DMA 配置 Byte(8bit)❌错误,无法实现预期
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
MyDMA_Init((uint32_t)DataA,(uint32_t)DataB,4);
- 每次搬运 1 字节,地址 + 1;搬运 4 次,总共搬运 4 字节。
- STM32 小端模式:只会读取DataA 0 这个 32 位变量内部的 4 个字节,依次写入 DataB 0~DataB 3。
- 现象:DataB 数组存的是 DataA 0 拆分的字节;DataA 1、DataA 2、DataA 3 完全没有搬运。
- 修改方案:需要拷贝 4 个 uint32_t 一共 16 字节,计数器改为
16。
案例 2:uint8_t DataA 4、uint8_t DataB 4
情况 A:DMA 配置 Byte(8bit)✅正确
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
MyDMA_Init((uint32_t)DataA,(uint32_t)DataB,4);
每次搬运 1 字节,地址 + 1,搬运 4 次;4 个 uint8_t 元素一一拷贝。
情况 B:DMA 配置 Word(32bit)❌错误
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
MyDMA_Init((uint32_t)DataA,(uint32_t)DataB,4);
每次搬运 4 字节,地址 + 4,搬运 4 次,总计搬运 16 字节。
- 第一次读取 DataA 0~DataA 3 拼成 32 位;后续三次地址直接越出数组边界,读取内存垃圾。
- 现象:数据错乱,内存越界,偶发程序跑飞、OLED 乱码。
三、DMA 等待传输完成代码坑(分号陷阱)
错误版本
while(DMA_GetFlagStatus(DMA1_FLAG_TC1)==RESET)
DMA_ClearFlag(DMA1_FLAG_TC1);
等价于while(条件){DMA_ClearFlag();}
问题:DMA 传输完成跳出循环后,清除标志的代码属于循环体,不会执行。TC1 完成标志一直置 1;第二次调用 DMA 时直接跳过等待,数据出错。
正确版本
while(DMA_GetFlagStatus(DMA1_FLAG_TC1)==RESET); //空循环阻塞等待传输完成
DMA_ClearFlag(DMA1_FLAG_TC1);
规范写法,防止漏写分号,建议用大括号空语句:
while(DMA_GetFlagStatus(DMA1_FLAG_TC1)==RESET)
{
;
}
DMA_ClearFlag(DMA1_FLAG_TC1);
补充知识点:
DMA_Cmd(DISABLE)不会自动清除 TC 传输完成标志,必须软件手动清除。
四、避坑总结
- DMA 计数器 = 搬运次数,不是字节数。总字节 = 次数 × 单次搬运字节。
- MemoryDataSize 同时控制:单次搬运字节 + 内存地址自增步长。
- 数组类型必须和 DMA 数据宽度匹配,计数器才等于数组元素个数。不匹配要手动计算总字节数填写计数器。
- 等待 DMA 传输完成,警惕 while 后分号;TC 标志硬件不会自动清零,软件手动 ClearFlag。
- 内存到内存 DMA 越界访问会破坏其他全局变量,现象:数据乱码、程序偶发死机。
五、做题 / 调试快速判断流程
- 看数组类型:uint8 /uint16 /uint32_t
- 看 DMA 配置的 MemoryDataSize
- 看传入的计数器值
- 计算总字节 = 计数器 × 单次搬运字节,对比数组实际总字节,判断是否匹配。
口诀:看类型,对宽度,算总字节,盯 while 分号,记得清 TC 标志。