1. STM32 DMA传输异常问题深度解析
最近在调试STM32的DMA功能时,遇到了两个典型问题:一是DMA只搬运一个字节就停止工作,二是DMA关闭后重新使能失败。这两个问题在嵌入式开发中非常常见,但官方文档往往没有详细说明其中的"坑"。今天我就结合自己的踩坑经历,详细分析问题原因和解决方案。
DMA(直接内存访问)是STM32中极其重要的外设,它能不经过CPU直接在内存和外设间传输数据,大幅提升系统效率。但在实际使用中,DMA的配置和使用有许多需要注意的细节,稍有不慎就会出现各种异常情况。下面我们就具体分析这两个问题的成因和解决方法。
2. DMA只搬运一个字节的问题
2.1 问题现象与原因分析
当配置好DMA后,发现它只传输了一个字节就停止了,这通常是由于DMA初始化结构体中的某些关键参数未正确初始化导致的。在STM32的标准外设库中,DMA_InitTypeDef结构体包含多个成员变量,如果这些变量没有正确初始化,就会导致DMA工作异常。
重要提示:栈上的局部变量在未初始化时,其值是随机的。如果直接使用未初始化的DMA_InitTypeDef结构体,其中的参数可能是任何值,这会导致DMA配置异常。
2.2 解决方案与代码实现
正确的做法是先用DMA_StructInit函数初始化结构体,确保所有参数都有合理的默认值,然后再修改需要自定义的参数。以下是标准做法:
c复制DMA_InitTypeDef dma_init;
DMA_StructInit(&dma_init); // 关键步骤:初始化结构体
// 然后配置具体参数
dma_init.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
dma_init.DMA_MemoryBaseAddr = (uint32_t)buffer;
dma_init.DMA_BufferSize = BUFFER_SIZE;
// 其他必要配置...
DMA_Init(DMA1_Channel5, &dma_init);
2.3 深入原理:为什么需要StructInit
DMA_StructInit函数会将结构体初始化为以下默认值:
- DMA_DIR = DMA_DIR_PeripheralSRC
- DMA_Mode = DMA_Mode_Normal
- DMA_Priority = DMA_Priority_Low
- DMA_MemoryInc = DMA_MemoryInc_Enable
- DMA_PeripheralInc = DMA_PeripheralInc_Disable
- DMA_MemoryDataSize = DMA_MemoryDataSize_Byte
- DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte
- DMA_M2M = DMA_M2M_Disable
这些默认值确保了DMA最基本的正常工作状态。如果不调用StructInit,结构体中的这些参数可能是任何随机值,导致DMA行为异常。
3. DMA关闭后重新使能失败问题
3.1 问题场景描述
在开发中,我们经常需要动态修改DMA的缓冲区地址(BufferSize)或传输数据量(NDTR)。常见的做法是先禁用DMA,修改参数,再重新使能。但实际操作中,直接这样操作经常会失败,DMA无法重新启动。
3.2 错误做法示例
以下是典型的错误代码:
c复制DMA_Cmd(usartc.dma_rx_stream, DISABLE);
usartc.dma_rx_stream->M0AR = (uint32_t)log_buf; // 修改内存地址
usartc.dma_rx_stream->NDTR = LOG_BUF_SIZE; // 修改传输数量
DMA_Cmd(usartc.dma_rx_stream, ENABLE); // 这里经常会失败
3.3 问题根本原因
问题的关键在于:当软件将DMA控制寄存器(CR)中的EN位清零后,硬件需要一定时间才能真正停止DMA传输。如果在EN位尚未被硬件真正清零时就尝试重新使能,操作就会失败。
3.4 解决方案一:循环模式(DMA_Mode_Circular)
如果使用循环模式,解决方案相对简单:
c复制dma_init.DMA_Mode = DMA_Mode_Circular; // 设置为循环模式
DMA_Cmd(usartc.dma_rx_stream, DISABLE);
while (usartc.dma_rx_stream->CR & DMA_SxCR_EN); // 等待EN位真正清零
usartc.dma_rx_stream->M0AR = (uint32_t)log_buf;
usartc.dma_rx_stream->NDTR = LOG_BUF_SIZE;
DMA_Cmd(usartc.dma_rx_stream, ENABLE);
关键点:
- 在DISABLE后,必须等待EN位真正清零
- 循环模式下,可以简单地重置缓冲区和传输量
3.5 解决方案二:普通模式(DMA_Mode_Normal)
普通模式更为复杂,因为如果需要"接着传输"(而不是重新开始),必须正确处理当前传输状态:
c复制DMA_Cmd(usartc.dma_rx_stream, DISABLE);
while (usartc.dma_rx_stream->CR & DMA_SxCR_EN);
// 清除所有相关标志位(关键步骤!)
DMA_ClearFlag(usartc.dma_rx_stream,
DMA_FLAG_TCIF5 | // 传输完成标志
DMA_FLAG_HTIF5 | // 半传输标志
DMA_FLAG_TEIF5 | // 传输错误标志
DMA_FLAG_DMEIF5 | // 直接模式错误标志
DMA_FLAG_FEIF5); // FIFO错误标志
// 如果要接着传输,需要获取当前传输位置
uint32_t recv_len = DMA_GetCurrDataCounter(usartc.dma_rx_stream);
usartc.dma_rx_stream->M0AR = (uint32_t)(log_buf + (LOG_BUF_SIZE - recv_len));
usartc.dma_rx_stream->NDTR = recv_len;
DMA_Cmd(usartc.dma_rx_stream, ENABLE);
关键点:
- 必须清除所有相关标志位,否则使能可能失败
- 如果要继续传输,不能简单重置NDTR,而要使用剩余传输量
- 内存地址也要相应调整到当前位置
4. 深入理解DMA状态机
4.1 DMA使能/禁用的硬件过程
理解DMA的状态机对解决这类问题很有帮助。当软件修改CR寄存器的EN位时:
- 软件写EN=0:这只是请求DMA停止,硬件需要完成当前传输才会真正停止
- 硬件完成当前传输后,才会将EN位清零
- 在EN位尚未清零前,任何修改关键参数的操作都可能导致不可预知的行为
4.2 标志位的重要性
DMA有多种状态标志位(TC、HT、TE等),这些标志位在DMA停止时可能被置位。如果不清除这些标志位就直接重新使能,新的传输可能会立即触发这些标志位对应的中断或错误状态。
5. 实际项目中的最佳实践
5.1 完整的安全DMA重启流程
基于以上分析,我总结出一个安全的DMA重启流程:
c复制void safe_restart_dma(DMA_Stream_TypeDef* stream, uint32_t new_addr, uint32_t new_size) {
// 1. 请求停止DMA
DMA_Cmd(stream, DISABLE);
// 2. 等待DMA真正停止
while (stream->CR & DMA_SxCR_EN);
// 3. 清除所有标志位
uint32_t flag_pos;
if (stream == DMA1_Stream0) flag_pos = 0;
else if (stream == DMA1_Stream1) flag_pos = 6;
// ...其他stream类似处理
DMA_ClearFlag(stream,
DMA_FLAG_TCIF0 << flag_pos |
DMA_FLAG_HTIF0 << flag_pos |
DMA_FLAG_TEIF0 << flag_pos |
DMA_FLAG_DMEIF0 << flag_pos |
DMA_FLAG_FEIF0 << flag_pos);
// 4. 更新参数
stream->M0AR = new_addr;
stream->NDTR = new_size;
// 5. 重新使能
DMA_Cmd(stream, ENABLE);
}
5.2 调试技巧
当DMA行为异常时,可以检查以下寄存器:
- DMA_LISR/DMA_HISR:查看中断标志状态
- DMA_Streamx.NDTR:查看剩余传输量
- DMA_Streamx.CR:检查配置是否正确
5.3 性能考量
频繁启停DMA会影响系统性能。在可能的情况下:
- 优先使用循环模式
- 如果必须使用普通模式,考虑使用双缓冲技术
- 尽量减少DMA重启次数
6. 常见问题排查指南
6.1 DMA完全不工作
检查步骤:
- 确认DMA时钟已使能
- 检查DMA通道与外设的映射关系是否正确
- 验证DMA中断是否配置正确(如果需要)
- 确保源地址和目的地址都是可访问的
6.2 DMA传输部分数据后停止
可能原因:
- NDTR值设置不正确
- 没有正确配置DMA_Mode(误设为Normal而非Circular)
- 传输完成中断中错误地禁用了DMA
6.3 DMA传输数据错误
排查方向:
- 检查PeripheralDataSize和MemoryDataSize是否匹配
- 确认地址对齐是否符合要求
- 检查是否有其他总线主控(如另一个DMA或CPU)在修改同一内存区域
7. 高级话题:DMA与Cache的一致性
在使用带有Cache的STM32系列(如STM32H7)时,还需要注意:
- 确保DMA缓冲区是Cache对齐的
- 在DMA传输前执行SCB_CleanDCache_by_Addr
- 在DMA传输后执行SCB_InvalidateDCache_by_Addr
- 或者将DMA缓冲区配置为Non-Cacheable
这些措施可以避免Cache一致性问题导致的DMA传输异常。