尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

STM32 DMA原理与实战:从CPU解放到高效数据传输

STM32 DMA原理与实战:从CPU解放到高效数据传输 1. 从“搬运工”到“CPU解放者”DMA到底是什么如果你玩过STM32肯定遇到过这种情况用串口接收一长串数据或者用ADC连续采集几百个点CPU就得像个苦力一样不断地从外设寄存器里把数据“搬”到内存里。这个过程叫“轮询”CPU啥也干不了就光顾着搬砖了。更糟的是如果数据量一大或者传输速度一快CPU就彻底被“堵”在数据搬运的路上主程序直接卡死。这时候DMA就该登场了。DMA全称Direct Memory Access直接存储器访问。你可以把它想象成你雇的一个“专职搬运工”。以前你需要亲自CPU去门口外设拿快递数据再跑回屋里内存放下。现在你只需要告诉这个搬运工“去把门口那一堆箱子搬到客厅那个角落。” 然后你就可以去干别的更重要的事了比如执行复杂的算法、刷新屏幕。这个“搬运工”DMA会独立于CPU在内存和外设之间建立起一条直接的数据通道高效地完成大批量数据的搬运。在STM32的世界里DMA控制器是一个独立的硬件模块。它就像一个交通调度中心连接着各种外设如UART、ADC、SPI、I2C和内存SRAM。当你配置好DMA的“搬运任务单”——也就是源地址、目标地址、数据量、传输模式后DMA控制器就会在外设发出“我有数据了”或“我需要数据”的信号时自动启动传输完全不需要CPU干预。等所有数据搬完它还会给CPU发个“任务完成”的通知中断让CPU知道可以来处理这些数据了。所以学习DMA的核心价值就两点第一把CPU从简单重复的IO操作中解放出来提升系统整体效率和实时性第二实现高速、大数据量的稳定传输这是很多实际应用如音频处理、图像采集、高速通信的基石。无论你是做物联网传感器数据采集还是驱动液晶屏或是实现一个USB高速设备DMA都是你必须掌握的核心技能。2. 庖丁解牛STM32 DMA的硬件架构与核心概念在动手写代码之前我们必须像了解自家厨房一样搞清楚STM32 DMA这个“厨房”里有哪些“灶台”、“水管”和“开关”。不同系列的STM32如F1、F4、H7其DMA控制器的数量和功能有差异但核心思想相通。我们以最常见的STM32F1系列拥有1个DMA7个通道和STM32F4/H7系列拥有2个DMA多个流/通道为例来拆解。2.1 DMA通道、流与请求映射谁为谁服务这是最容易让人混淆的地方。你可以把DMA控制器看作一个“运输公司”。DMA控制器运输公司STM32F1有1个DMAF4有2个DMADMA1, DMA2。每个控制器管理多个“运输线路”。通道F1概念 / 流F4/H7概念运输线路这是具体的运输路线。每个通道/流一次只能执行一个运输任务。F1的DMA有7个通道Channel1-7F4的每个DMA有8个流Stream0-7。关键点来了一个外设的传输请求必须通过特定的通道/流来执行。请求映射派单系统这个“派单系统”是硬件固定好的。比如在STM32F103上串口1的发送请求USART1_TX只能由DMA1的通道4来服务ADC1的转换完成请求只能由DMA1的通道1来服务。这个映射关系在芯片的参考手册Reference Manual里有详细的表格编程时必须严格查阅不能随意指定。F4/H7的“流”选择更灵活一些一个外设请求可以映射到多个流上但一个流在同一时刻也只能服务一个外设。简单来说外设客户发出运输请求 → 根据固定映射表分配给某个DMA的某个通道/流运输线路 → 该通道/流启动传输。2.2 传输的“三要素”源、目的与数据宽度配置DMA传输本质上就是回答三个问题从哪里搬搬到哪里去一次搬多大源地址Source Address数据的老家。可以是外设的数据寄存器地址如USART1-DR也可以是内存中的某个数组地址如adc_buffer。目的地址Destination Address数据的新家。同理可以是内存地址也可以是外设寄存器地址。内存到外设M2P比如把一段要发送的数据从内存搬到串口的发送数据寄存器。常用于UART、SPI发送。外设到内存P2M比如把ADC转换完成的数据从ADC数据寄存器搬到内存数组。常用于ADC、UART接收。内存到内存M2M这是DMA一个非常实用的功能可以在CPU不干预的情况下高速地在两个内存区域间拷贝数据。F1系列只有部分型号的DMA支持而F4/H7系列都支持。数据宽度Data Width一次操作搬运的数据位数。通常有字节8位、半字16位、字32位。这里有一个至关重要的对齐原则源地址、目的地址、传输的数据宽度三者需要合理匹配。例如如果你配置为半字16位传输那么源地址和目的地址最好都是2字节对齐的地址是2的倍数。不对齐虽然可能也能工作但会引发硬件总线错误Hard Fault或者性能下降。对于外设寄存器其数据宽度通常是固定的如UART DR是8/9位实际按8位或16位访问需要根据外设来设置。2.3 传输模式单次、循环与自动重装DMA怎么知道要搬多少趟呢这就涉及到传输计数器NDTR和传输模式。传输计数器NDTR / Number of Data这是一个寄存器你初始化时告诉DMA总共要搬运多少个“数据单元”。这个“数据单元”的大小就是你上面设置的数据宽度。每成功搬运一次这个值就自动减1。单次模式Normal Mode当NDTR减到0传输就彻底停止。DMA通道会关闭需要你重新配置使能才能进行下一次传输。适用于已知长度的单次数据块传输。循环模式Circular Mode当NDTR减到0时硬件会自动将其重载为初始值传输从头开始周而复始。这是ADC连续采集、UART持续接收等场景的“神器”。你只需要开辟一个足够大的缓冲区比如adc_buffer[1024]配置为循环模式DMA就会自动不停地用新数据覆盖旧数据形成一个数据“圆环”。CPU只需要定期去缓冲区里读取最新数据即可实现了“乒乓缓冲”或“环形缓冲”的效果。2.4 中断与标志位如何知道“搬完了”DMA是个沉默的搬运工活干完了得通知你。它通过状态标志位和中断来通信。传输完成中断TC, Transfer Complete当NDTR从1减到0即所有数据都搬运完毕时会触发此中断。在单次模式下这意味着任务结束在循环模式下这意味着完成了一轮循环。半传输完成中断HT, Half Transfer当NDTR减到初始值的一半时触发。这个功能非常有用特别是在循环模式下。你可以开启HT和TC中断实现“双缓冲”机制。当HT中断发生时意味着DMA已经填满了缓冲区的上半部分CPU可以安全地处理这上半部分数据与此同时DMA正在填充下半部分。当TC中断发生时CPU处理下半部分数据。这样数据生产和消费几乎可以并行极大地提高了效率避免了处理数据时覆盖新数据的风险。传输错误中断TE, Transfer Error传输过程中发生错误如总线错误时触发。直接模式错误中断DME, Direct Mode ErrorF4/H7系列中在直接模式下配置错误时触发。在程序中我们通常在初始化时使能我们关心的中断如TC和HT然后在对应的中断服务函数ISR里第一件事就是清除对应的中断标志位然后进行数据处理或启动下一次任务。3. 实战配置以STM32F1的UART DMA收发为例理论说再多不如一行代码。我们以STM32F103蓝桥杯、正点原子开发板常用芯片为例使用标准外设库Standard Peripheral Library来配置一个经典的场景用DMA接收不定长串口数据并用DMA发送数据。这里会用到IDLE空闲中断来检测一帧数据的结束是实际项目中非常实用的技巧。3.1 硬件与工程准备假设我们使用USART1波特率115200引脚PA9(TX) PA10(RX)。DMA映射关系查表可知USART1_RX对应DMA1_Channel5USART1_TX对应DMA1_Channel4。首先在工程中初始化USART1为异步模式使能USART1本身的中断为了IDLE中断。我们重点看DMA的配置。3.2 DMA接收配置外设到内存循环模式IDLE中断接收的目标是让DMA自动把串口收到的每一个字节都存到我们定义的缓冲区里并且我们能在“一帧数据接收完成”比如接收到一个换行符或者一段时间没收到新数据时得到通知。#define USART_RX_BUF_SIZE 256 uint8_t USART_RX_Buffer[USART_RX_BUF_SIZE]; // DMA接收缓冲区 void USART1_DMA_RX_Config(void) { DMA_InitTypeDef DMA_InitStructure; // 1. 开启DMA1时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 2. 复位并初始化DMA1 Channel5 (USART1_RX) DMA_DeInit(DMA1_Channel5); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)USART1-DR; // 源外设地址 DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)USART_RX_Buffer; // 目的内存地址 DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; // 传输方向外设-内存 DMA_InitStructure.DMA_BufferSize USART_RX_BUF_SIZE; // 传输数量 DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; // 外设地址不递增 DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; // 内存地址递增 DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; // 外设数据宽度字节 DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; // 内存数据宽度字节 DMA_InitStructure.DMA_Mode DMA_Mode_Circular; // **循环模式**关键 DMA_InitStructure.DMA_Priority DMA_Priority_VeryHigh; // 优先级 DMA_InitStructure.DMA_M2M DMA_M2M_Disable; // 非内存到内存模式 DMA_Init(DMA1_Channel5, DMA_InitStructure); // 3. 使能USART1的DMA接收请求 USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE); // 4. 使能DMA通道 DMA_Cmd(DMA1_Channel5, ENABLE); // 5. 使能USART1的IDLE空闲线路中断 USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); }关键点解析循环模式Circular这是实现持续接收的核心。缓冲区会被循环覆盖我们永远能读到最新的数据。IDLE中断当串口检测到总线空闲一个字符帧的时间没有新数据时会产生IDLE中断。这是我们判断“一帧数据可能接收完毕”的重要信号。地址递增外设地址USART-DR固定内存地址递增这样数据才能依次存到数组里。3.3 在USART中断中处理IDLE事件配置好DMA后数据会自动搬运。我们需要在USART1的全局中断服务函数里捕获IDLE事件。void USART1_IRQHandler(void) { uint16_t temp; if(USART_GetITStatus(USART1, USART_IT_IDLE) ! RESET) { // 1. 清除IDLE中断标志重要 // 读USART_SR再读USART_DR即可清除IDLE标志位 temp USART1-SR; temp USART1-DR; // 这两行是清除IDLE标志的标准操作 // 2. 计算本次接收到的数据长度 // DMA_CNDTRx寄存器保存着剩余未传输的数据量 uint16_t remain_cnt DMA_GetCurrDataCounter(DMA1_Channel5); // 剩余数量 uint16_t recv_len USART_RX_BUF_SIZE - remain_cnt; // 已接收长度 总大小 - 剩余 if(recv_len 0) { // 3. 处理数据例如将USART_RX_Buffer中的数据复制出来 // 注意因为DMA是循环的我们需要根据读写指针或本次长度来定位有效数据 // 这里简化处理假设从缓冲区开头开始就是新数据 process_usart_data(USART_RX_Buffer, recv_len); // 4. 重置DMA缓冲区可选但推荐 // 为了安全在处理完数据后可以重置DMA的传输计数器让DMA从缓冲区头重新开始填 // 但要注意在重置前DMA可能已经又写入了新数据所以更安全的做法是使用双缓冲或记录读写位置 // DMA_Cmd(DMA1_Channel5, DISABLE); // DMA_SetCurrDataCounter(DMA1_Channel5, USART_RX_BUF_SIZE); // DMA_Cmd(DMA1_Channel5, ENABLE); } } // ... 其他中断处理如RXNE, 如果使能了的话 }注意这是一个简化示例。在真实的循环缓冲区应用中你需要维护一个“写指针”由DMA的CNDTR推算和一个“读指针”由你的程序控制来计算未处理的数据长度避免覆盖和重复处理。这是DMA应用中的一个核心技巧。3.4 DMA发送配置内存到外设单次模式发送通常是CPU准备好数据后启动一次DMA传输发完即止。#define USART_TX_BUF_SIZE 128 uint8_t USART_TX_Buffer[USART_TX_BUF_SIZE]; void USART1_DMA_TX_Config(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel4); // USART1_TX DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)USART1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)USART_TX_Buffer; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralDST; // 方向内存-外设 DMA_InitStructure.DMA_BufferSize 0; // 初始化为0发送时再设置 DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode DMA_Mode_Normal; // **单次模式** DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel4, DMA_InitStructure); // 使能USART1的DMA发送请求 USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE); // 先不使能DMA通道等有数据要发时再开启 // DMA_Cmd(DMA1_Channel4, ENABLE); } // 启动一次DMA发送 void USART1_DMA_Send(uint8_t *data, uint16_t len) { // 1. 等待上一次DMA发送完成如果DMA正在忙 while(DMA_GetCmdStatus(DMA1_Channel4) ENABLE); // 2. 关闭DMA通道修改配置前先关闭是良好习惯 DMA_Cmd(DMA1_Channel4, DISABLE); // 3. 设置内存地址、数据长度 DMA_SetCurrDataCounter(DMA1_Channel4, len); DMA_SetMemoryAddress(DMA1_Channel4, (uint32_t)data); // 注意标准库可能需要用DMA_Init重新配置地址 // 4. 清除TC传输完成标志可选但推荐 DMA_ClearFlag(DMA1_Channel4, DMA_IT_TC); // 5. 使能DMA通道开始传输 DMA_Cmd(DMA1_Channel4, ENABLE); }关键点解析单次模式Normal发送完指定长度数据后自动停止。发送流程每次发送前需要检查DMA通道是否空闲DMA_GetCmdStatus然后重新配置数据地址和长度再重新使能。这是因为单次模式传输完成后通道会被硬件禁用。标准库的地址设置上面示例中DMA_SetMemoryAddress在某些库版本可能不直接存在更通用的做法是修改DMA_InitStructure中的DMA_MemoryBaseAddr后再次调用DMA_Init。或者使用DMA_CMARx寄存器直接操作。HAL库则提供了更统一的HAL_DMA_Start函数。4. 进阶技巧与避坑指南从“能用”到“用好”当你成功点亮了第一个DMA传输真正的挑战才刚刚开始。下面这些从实际项目中踩坑总结的经验可能比数据手册更有用。4.1 内存对齐与数据宽度那些诡异的HardFault这是DMA新手最常见的“崩溃”点。症状是代码一运行到DMA使能或者传输过程中直接进入HardFault中断。根因DMA作为总线主设备访问内存时有对齐要求。例如你配置了数据宽度为DMA_PeripheralDataSize_HalfWord16位但源或目的地址是一个奇数如0x2000 0001这就产生了非对齐访问。对于Cortex-M3/M4内核非对齐访问在某些情况下是支持的但可能影响性能而在DMA通过AHB总线访问时可能会直接触发总线错误。解决方案确保地址对齐对于字节传输任何地址都可。对于半字传输地址必须是2的倍数。对于字传输地址必须是4的倍数。在定义用于DMA传输的数组时可以使用编译器指令进行强制对齐。// GCC/ARMCC 编译器 uint8_t buffer[1024] __attribute__((aligned(4))); // 强制4字节对齐 // IAR 编译器 #pragma data_alignment4 uint8_t buffer[1024];匹配外设数据宽度仔细查阅数据手册。比如STM32的ADC数据寄存器是12位的但通常按16位半字访问。那么DMA的数据宽度就应该设置为半字并且内存缓冲区也应按半字uint16_t数组定义并确保半字对齐。4.2 缓存一致性Cache Coherency问题F7/H7系列的“幽灵数据”当你升级到使用Cortex-M7内核的STM32F7/H7系列时会迎来一个性能利器缓存Cache。但DMA和缓存一起工作会带来一个经典问题缓存一致性问题。现象CPU在DMA传输完成后去读内存中的数据发现数据是旧的脏数据或者DMA去写一块内存但CPU之前写入的数据还在缓存里没刷到内存导致DMA读到旧数据。原理CPU读写数据时操作的是缓存Cache。DMA读写数据时直接操作的是内存RAM。两者不同步。解决方案以HAL库为例对于DMA要写入的内存CPU读在CPU读取DMA数据之前需要无效化Invalidate对应的数据缓存。这告诉CPU“缓存里的数据可能过期了下次读的时候直接从内存拿。”// 假设接收缓冲区是 rx_buf SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, expected_data_size);对于CPU要写入、DMA要读取的内存CPU写在启动DMA传输之前需要清理Clean对应的数据缓存。这强制将CPU缓存中已修改的数据写回到内存中确保DMA拿到的是最新数据。// 假设发送缓冲区是 tx_buf数据已由CPU准备好 SCB_CleanDCache_by_Addr((uint32_t*)tx_buf, data_size_to_send); HAL_DMA_Start(...); // 然后启动DMA使用非缓存内存可以将DMA缓冲区定义在非缓存区域。在链接脚本中指定一块内存区域并通过MPU内存保护单元将其配置为Device或Normal Non-cacheable类型。这是一劳永逸但需要更深入系统配置的方法。4.3 外设与DMA的启动顺序谁先谁后这是一个时序逻辑问题顺序错了可能导致第一次传输失败。正确顺序以ADC DMA循环采集为例配置并使能DMA通道。配置ADC并将ADC与DMA关联ADC_DMACmd或HAL中的HAL_ADC_Start_DMA。最后使能ADC开始转换。为什么如果先使能ADC开始转换转换完成事件会立即触发DMA请求。但如果此时DMA通道还未就绪这个请求就会被忽略导致第一个或前几个数据丢失。所以要确保“服务员”DMA先就位再让“客户”外设开始“点单”产生数据。4.4 双缓冲与环形缓冲区高效数据处理的基石简单的DMA循环模式只能保证数据不丢失但CPU在读取数据时DMA可能正在覆盖它。这就需要更精细的缓冲区管理策略。双缓冲Double Buffer利用DMA的半传输完成HT和传输完成TC中断。将DMA缓冲区大小设为实际需要的两倍N*2。当HT中断到来DMA填满了前半部分Buf0CPU可以安全处理Buf0同时DMA继续填充后半部分Buf1。当TC中断到来DMA填满了Buf1并从头开始CPU处理Buf1。如此交替实现了无锁的、高效的生产者-消费者模型。HAL库的HAL_ADC_Start_DMA函数就支持直接传入两个缓冲区地址实现双缓冲。软件环形缓冲区Software Ring Buffer当DMA工作在循环模式时硬件上它就在循环写入一个线性数组。我们可以在软件层面将这个数组视为一个“环”。维护一个write_ptr通过DMA_CNDTR计算得出DMA当前写入位置和一个read_ptrCPU读取位置。可读数据长度 (write_ptr - read_ptr buffer_size) % buffer_size。这种方法更灵活缓冲区大小可以任意但需要自己计算指针。4.5 DMA与中断的优先级谁更重要当DMA传输和CPU代码、其他中断同时竞争总线资源时谁先谁后这由NVIC嵌套向量中断控制器中的优先级决定。DMA通道中断优先级每个DMA通道的传输完成、半传输完成等中断其优先级在NVIC中配置。如果DMA传输完成中断里需要进行复杂运算应该设置一个合适的优先级避免阻塞更紧急的中断如电机控制的PWM中断。DMA总线仲裁DMA控制器本身作为总线主设备其访问总线的优先级也可以配置在DMA初始化结构体的DMA_Priority字段有低、中、高、非常高四级。这个优先级决定了当多个DMA通道或DMA与CPU同时请求总线时谁先被服务。对于高速、实时性要求高的数据流如摄像头接口DCMI应设置为DMA_Priority_VeryHigh。5. 从HAL库与CubeMX视角看DMA效率与便利的权衡如果你在使用STM32CubeMX和HAL库配置DMA会变得图形化、简单化但理解背后的细节同样重要。5.1 CubeMX图形化配置在CubeMX中你可以在引脚分配界面找到相应外设如USART1在“DMA Settings”标签页点击“Add”然后选择传输方向Peripheral To Memory, Memory To Peripheral等并配置优先级、模式Normal/Circular、数据宽度、地址递增等。CubeMX会自动生成初始化代码并帮你处理好时钟使能和外设-DMA的链接。一个容易遗漏的步骤在NVIC Settings中记得使能DMA通道的中断如果要用到的话并设置其优先级。CubeMX不会自动为你使能DMA传输完成中断。5.2 HAL库的DMA驱动模型HAL库将DMA封装成了一组统一的API其核心思想是“句柄Handle”和“回调函数Callback”。启动传输HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)。调用这个函数HAL库会帮你配置DMA、启动传输然后立即返回非阻塞。中断处理传输完成、半传输完成、错误等事件都在HAL库的DMA全局中断函数DMAx_Streamy_IRQHandler中处理。它会清除标志位并调用你预先注册的回调函数。回调函数机制这是HAL库的精髓。你不需要直接写中断服务函数而是实现对应的回调函数。// 在main.c或某个文件中 void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if(huart-Instance USART1) { // USART1 DMA发送完成可以准备下一包数据了 tx_complete_flag 1; } } void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart) { // 半传输完成可以处理前半部分数据了 process_rx_data(huart-pRxBuffPtr, RX_BUFFER_SIZE/2); } void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { // 全传输完成可以处理后半部分数据了双缓冲模式 process_rx_data(huart-pRxBuffPtr RX_BUFFER_SIZE/2, RX_BUFFER_SIZE/2); // 注意在循环模式下这个回调在每一轮完成时都会调用 }HAL库DMA的优缺点优点标准化移植方便中断处理逻辑清晰回调函数与CubeMX无缝集成。缺点有一定的代码开销和运行时开销对极致性能和资源紧张的项目可能不友好。有些底层细节被隐藏出了问题调试起来可能更复杂。我个人在项目中的习惯是对于快速原型开发、复杂度不高的应用优先使用CubeMXHAL效率极高。对于性能要求苛刻、资源紧张的最终产品或者需要精细控制时序的场景我会回归到标准外设库LL库甚至直接寄存器操作虽然开发慢但心里有底代码量也小。6. 调试DMA当传输静默失败时你该如何排查DMA不工作是最让人头疼的因为它静默无声。下面是一个系统性的排查清单时钟检查DMA的时钟AHB开了吗RCC_AHBPeriphClockCmd(DMAx, ENABLE);这是第一步也是最容易忘记的一步。外设DMA请求使能你开启外设的DMA请求了吗例如对于UART除了配置DMA本身还需要USART_DMACmd(USART1, USART_DMAReq_Rx | USART_DMAReq_Tx, ENABLE);。在HAL库中HAL_UART_Receive_DMA函数内部会处理这个。地址配置源地址和目的地址设置对了吗特别是外设寄存器地址一定要用取地址符如(uint32_t)USART1-DR。内存地址也要确保是有效的数组首地址。传输数量NDTR这个值大于0吗如果你错误地将其初始化为0DMA不会启动任何传输。DMA通道使能最后一步DMA_Cmd调用了吗或者HAL库的HAL_DMA_Start调用成功了吗中断与标志位在调试器中观察DMA通道的ISR中断状态寄存器或xISR寄存器。查看TCIF传输完成标志、TEIF传输错误标志是否被置起。传输错误标志是定位问题如对齐错误的关键。外设状态外设本身工作正常吗例如UART的发送寄存器空标志TXE、接收寄存器非空标志RXNE是否正常先确保不用DMA时外设中断或轮询模式是正常的。内存内容在调试器中实时查看你定义的目标内存数组。在传输启动后数据是否有变化这是最直接的证据。优先级与阻塞是否有更高优先级的中断长时间关闭了总中断导致DMA请求无法被响应或者CPU正在访问一个被DMA占用的总线如Flash导致DMA等待超时缓存问题仅限F7/H7如果你启用了缓存是否做了正确的Clean/Invalidate操作可以尝试先将MPU配置为禁用某块内存区域的缓存看问题是否消失来快速定位是否是缓存一致性问题。调试DMA耐心和系统性的思维是关键。从一个最简单的M2M内存拷贝实验开始逐步增加外设是学习DMA最稳妥的路径。当你第一次看到CPU利用率几乎为0而数据却在高速、稳定地流动时你会觉得前面所有的折腾都是值得的。DMA不是STM32的选修课而是嵌入式高手必经的桥梁掌握了它你才真正拥有了驾驭这颗芯片高速数据流的能力。
返回列表