ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32 DMA从原理到实战:串口接收、ADC采样与常见坑解析

STM32 DMA从原理到实战:串口接收、ADC采样与常见坑解析 DMA这个外设一直是个有点“劝退”的存在。很多人一打开CubeMX看到DMA配置页面里那些Stream、Channel、外设地址、内存地址、传输方向、数据宽度、优先级的下拉框直接就懵了。明明只是想让串口收个数据怎么整出这么多名词来我在带新人的时候发现他们对DMA的恐惧其实都来自同一个误区把DMA当成一个“需要配置的外设”去学而不是把它当成一个“帮你搬数据的工具”去理解。这篇就把我实际使用STM32 DMA的经验、代码和踩坑记录完整写出来从原理到CubeMX配置再到裸代码一次讲透看完你会发现DMA这个东西配好一次之后真的就再也回不去了。1. DMA本质CPU把“搬砖”外包给一个专职搬运工1.1 没有DMA之前数据是怎么流动的先想一个最简单的场景串口接收一包数据。没有DMA的时候程序里无非三种处理方式轮询方式CPU死循环等在一个判断接收标志的语句上来一个字节读一个字节。这种方式的缺陷很明显接收期间CPU什么都干不了如果主循环里还有显示刷新、按键扫描、控制运算整体实时性会非常差稍微来点高速率数据就丢。中断方式串口每收到一个字节就触发一次中断CPU放下手里的活去读数据寄存器。这个方法看起来不错但数据量大或者波特率高的时候中断频率会高得吓人。115200波特率大约每秒11520个字节算下来不到87微秒就中断一次如果每个中断里还要做队列处理CPU资源和指令流水线基本都被打碎了。合理的做法让专门的硬件去盯这些字节攒够了或者等一段空闲再一次性通知CPU来取。第三种做法就是DMA的用武之地。它的全称是Direct Memory Access直接存储器访问。说白了它就是一个挂在总线上的专用搬运引擎可以按你设定的源地址、目的地址、传输数量自动把数据从一边搬到另一边全程不需要CPU干预。1.2 DMA到底怎么“搬”的用公司里的场景来类比CPU是老板外设寄存器是生产线上的物料箱内存是仓库DMA是物流专员。没有物流专员时老板得自己跑到物料箱旁边把每一个零件拿出来再走回仓库放下来一个零件跑一趟。有了物流专员之后老板只需要交代一句“去把这一箱零件搬到仓库西区”然后就可以继续处理别的公务了。物流专员DMA会按照指令一趟一趟把零件搬到位全部搬完之后才跟老板汇报一声“搬完了”触发传输完成中断或者连汇报都不需要只是默默干活不使能中断。对应到STM32上你要“交代”给DMA的信息就四件事从哪里搬源地址。可能是外设的数据寄存器也可能是内存数组。搬到哪去目标地址。可能是一个内存缓冲区也可能是外设的数据寄存器。搬多少传输数据量也就是数据个数。怎么搬单次还是循环、数据宽度是多少、搬完要不要通知CPU。把这几个问题在脑子里过一遍再去看CubeMX的配置界面其实每个选项都有对应关系了就不会觉得是凭空冒出来的概念。1.3 三种搬运方向的实际含义DMA传输方向在STM32的寄存器里用DIR位控制常见有三种方向CubeMX中的体现实际业务场景外设到内存PeripheralToMemory串口/SPI/I2C接收、ADC多通道采样值存入数组、定时器捕获值采集内存到外设MemoryToPeripheral串口/SPI发送数据、DAC波形数据输出、定时器产生PWM占空比序列内存到内存MemoryToMemory大数组拷贝、图像数据搬移、Flash临时数据搬运三种方向配置上差别很小本质都是填地址、填长度、开使能。但业务差别很大后面我会用串口接收和ADC采集两个例子分别展开把一个方向吃透其他方向就是“反着填”而已。还有一个很多人忽略的点DMA本身不产生数据它只负责搬迁。串口如果没收到数据DMA什么也搬不了ADC如果没启动采样DMA也没有输入源。所以DMA一定是要挂在某个具体的外设“事件”上触发的不是自己在那空转的。这点理解了你就不会出现“我DMA配置明明对了为什么数组里永远是0”这种疑问了。2. 串口DMA接收CubeMX几步配好代码接手就能跑2.1 为什么要优先拿串口当DMA的第一个实验对象我给别人讲DMA基本都是用串口接收来当第一个案例。原因很简单串口数据是外部实实在在发进来的效果立竿见影调试还方便——随便一个USB转TTL模块加串口助手就能验证。你不需要额外的信号发生器也不需要搭复杂的外围电路就一个串口收发看得明明白白。我自己早期做产品的时候遇到过这样一个问题设备要实时接收上位机发来的配置命令同时还要跑一个PID调节的算法。本来CPU算力就不宽裕串口每来一个字节就中断一次中断里还要做协议解析的状态机结果就是CPU占用率居高不下波形刷新频率上不去。后来换成DMA加空闲中断接收中断频率从每字节一次降到了每包数据一次CPU占用率一下子降了百分之三四十整个系统的实时性明显改善。这就是串口DMA最直接的价值。2.2 CubeMX配置步骤和每项设置的理由我用STM32CubeMX的配置界面来带大家过一遍以常见的USART1接收不定长数据为例打开串口外设在Categories里勾选USART1Mode选择Asynchronous波特率设置成你需要的值我习惯115200。这一步是前提DMA搬的是串口收到的数据串口本身必须先工作起来。添加DMA请求切换到DMA Settings标签页点击Add按钮。这里会弹出来一个请求选择框选择USART1_RX系统会自动选择一个合适的DMA控制器和通道/流如果是F1系列自动选DMA1的通道5之类的如果是F4系列自动选DMA2的Stream几加Channel几。设置方向Direction选PeripheralToMemory表示数据从串口的数据寄存器搬到内存的数组里。设置传输模式Mode这一步非常关键。选Normal模式时DMA搬完设定长度的数据就停下来了选Circular模式时搬完设定长度后地址和计数器会自动重置继续等待下一轮搬运相当于DMA一直在待命。我的建议是接收不定长数据时用Circular模式。因为外部数据什么时候来、来多少你根本不知道。你用Normal模式就得每次接收前重新调用一次启动函数万一漏调用数据就没了。Circular模式配合后面的空闲中断就可以实现DMA一直在后台待命、CPU只在收到完整一包数据后处理一次的优雅效果。数据宽度Data Width都选Byte。串口寄存器是8位的内存缓冲区也按字节处理两边宽度一致数据才不会错位。最后在NVIC设置里使能串口全局中断。这些设置做完之后CubeMX生成的初始化代码会帮你把DMA句柄和串口句柄关联起来。最关键的一行是这一句HAL_UART_Receive_DMA(huart1, rx_buffer, BUFFER_SIZE);这一句一旦被调用效果就是DMA开始盯着USART1的数据寄存器每收到一个字节就搬到rx_buffer里一直搬到BUFFER_SIZE个字节之后要么停下Normal模式要么重新轮回继续搬Circular模式。2.3 DMA加空闲中断不定长数据一次收完这里要解释一个核心概念DMA负责搬数据但它不知道“一包数据”什么时候结束。串口头一次发来5个字节隔了50毫秒又发来3个字节DMA只会机械地往缓冲区里放不会告诉你“第一包收完了”。所以实际工程里我几乎都是DMA加串口空闲中断配合使用。所谓空闲中断IDLE Interrupt就是串口线上检测到一段没有数据的时间一个字节传输时间的空闲后触发的中断。这个中断一来就说明“这一波数据已经发完了”这时候去DMA缓冲区里取数据取到的就是完整的一包。代码长这样// 在main函数里启动DMA接收注意这个函数只调用一次 HAL_UART_Receive_DMA(huart1, rx_buffer, RX_BUFFER_SIZE); // 串口中断服务函数在stm32f1xx_it.c或stm32f4xx_it.c里 void USART1_IRQHandler(void) { HAL_UART_IRQHandler(huart1); // 关键检查空闲标志 if (__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart1); // 清标志 // 计算本次接收到的数据长度 uint16_t received_len RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(hdma_usart1_rx); // 在这里处理数据做协议解析存到应用层缓冲区 process_uart_frame(rx_buffer, received_len); } }这里的核心技巧是__HAL_DMA_GET_COUNTER这个函数。它返回的是DMA计数寄存器里还剩余多少数据没搬。假设你设置的缓冲区大小是256DMA自动搬了8个字节之后空闲中断触发counter值就是256 - 8 248那么RX_BUFFER_SIZE - counter就是8正好是本次接收的长度。这个技巧不用你手动维护任何接收计数变量非常干净。2.4 为什么缓冲区要预分配而且不能太小有一个常见的坑DMA没有“先看看数据多大再分配空间”的能力所以你必须提前预分配一块足够大的内存区域给它。这块缓冲区本质上是DMA的“待收纳区”如果你告诉DMA“往这个数组里搬”那这个数组在程序运行期间就一直被DMA注视着你不能把它当普通数组随意释放。我见过有人图省事在某个函数里定义了一个局部数组然后启动DMA接收结果函数一退出局部数组就失效了DMA还在傻傻地往里写于是内存被踩得乱七八糟程序莫名其妙跑飞。这是个非常隐蔽的错误因为很多情况下它不会立刻导致崩溃而是产生一些间歇性的诡异现象。正确的做法是在文件作用域定义全局数组或者用静态数组#define RX_BUFFER_SIZE 256 uint8_t rx_buffer[RX_BUFFER_SIZE];缓冲区大小的选择也讲究。如果业务场景里单包数据最大是64字节缓冲区定64就够吗我的建议是留出两倍余量。因为Circular模式下DMA是循环写入的如果上包数据还没来得及处理下一包数据就把缓冲区冲掉了数据就会错乱。缓冲区越大给CPU留的处理窗口就越宽裕。2.5 发送方向的DMA配置有什么不同串口DMA发送的配置和接收几乎对称方向选MemoryToPeripheralMode选Normal。发送用Normal模式没问题因为你每次要发什么东西都是主动发起的发完一个包再发下一个包不需要循环待命。发送的启动函数也很好记对应关系如下HAL_UART_Receive_DMA(huart1, rx_buffer, len); // 接收外设 - 内存 HAL_UART_Transmit_DMA(huart1, tx_buffer, len); // 发送内存 - 外设不过要注意如果需要在发送完成后做点什么比如关闭RS485方向控制引脚、释放发送缓冲区可以重写回调函数void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { // 发送完成可以撤销RS485的发送使能或者标记发送空 RS485_DE_CTRL(0); } }要特别提醒的一点是调用HAL_UART_Transmit_DMA之后数据并没有立刻发送完发送是异步的CPU不会停在这里等。所以如果你紧接着又去修改tx_buffer的内容或者复用同一个缓冲区发下一包数据就可能把还没发出去的数据改掉。正确做法是发送完成回调里设置一个标志等回调触发了再复用缓冲区。3. 数据宽度、传输模式与内存到内存的搬运逻辑3.1 数据宽度匹配Bug的隐形来源数据宽度Data Width这个选项是DMA配置里最容易埋雷的地方。它本质上决定了DMA每次搬运一“份”数据是多少个字节。可以选Byte8位、HalfWord16位、Word32位。规则说起来很简单源和目的的数据宽度要能匹配上。但实际中很多人会踩一个典型的坑ADC采集到的数据本身是12位的存在寄存器里是右对齐的16位格式你为了节省空间把缓冲区定义成uint8_t数组结果发现DMA搬过来的数据全是乱的每隔一个字节就多出一个0或乱码。比如代码里如果把内存缓冲区定义成uint16_t数组但是外设寄存器是8位的应该全部按Byte宽度配置如果内存用的是uint16_t数组同时外设寄存器是16位的那两边都配HalfWord。一旦一边Byte一边HalfWordDMA的地址递增逻辑和搬运次数就会对不上数据整体错位。我在调试ADC数据紊乱的问题时排查步骤里第一件事就是核对DMA源和目的的数据宽度是不是一致。这个检查30秒就能确认但能省掉一晚上抓头发的时间。建议读者在配DMA的时候养成习惯外设寄存器是多少位内存变量类型就用多少位的两边宽度对齐别盲目省内存。3.2 Normal模式和Circular模式的取舍这个选择在配置时要想清楚因为涉及程序设计思路。Normal模式适合那些“一次性传输”场景比如SD卡读一个扇区到内存缓冲区读完了就结束了比如串口发送一包数据发出去了任务就完成了。这种模式用完一个传输就停在那边需要手动再次启动才能继续搬数据。Circular模式适合那些“持续不断”的场景比如ADC采样一秒钟要采几千次你肯定不希望每次采完都重新启动一次DMA比如串口接收不定长数据外部随时可能发东西进来循环模式保证DMA一直在值班。循环模式下的地址和计数寄存器会在传输完成后自动恢复到初始值所以只需要启动一次其他的交给硬件。需要注意的一个细节Circular模式下如果你在某次空闲中断里发现接收长度为0也就是RX_BUFFER_SIZE - counter 0说明其实没有新数据到达这次空闲中断可能是残留标志导致的误触发。我一般会在处理函数里先判断一下长度长度为0就直接返回避免无谓的协议解析动作。3.3 内存到内存DMA还可以当“高级memcpy”用除了外设和内存之间的搬运STM32的DMA还支持内存到内存的传输。在CubeMX里把DMA请求选为MemoryToMemory方向选MemoryToMemory然后把源和目标都填成内存地址即可。这种方式在实际项目中很有用。我在做一段音频播放功能时需要把SD卡读出来的音频数据从临时缓冲区搬到DAC的DMA缓冲区如果靠CPU用memcpy去搬一方面占用CPU时间另一方面大块数据拷贝还可能导致中断响应延迟。改成DMA内存到内存搬运整个过程不占用CPU搬完触发一次中断通知即可。不过内存到内存模式有个前提要记牢这种模式下DMA是“立刻全速搬运”的不会等任何外设事件触发。所以它一次搬完就结束了没有循环模式可用。CubeMX里MemoryToMemory模式下Mode选项只有Normal就是这个原因。还有一种常见用法是做环形缓冲区的整理。串口DMA循环接收的时候数据在缓冲区里可能是“断开的”——比如上一包数据的尾巴在缓冲区末尾下一包数据的头在缓冲区开头。如果不想处理这种“回绕”逻辑可以当数据跨过缓冲区边界时用内存到内存DMA把末尾的部分搬到另一个连续区域这样协议解析代码就可以始终用线性的连续缓冲区来处理。这种方式在处理流式数据的时候非常省心。3.4 触发方式DMA是“被喊来干活的”再强调一次触发逻辑DMA的每一次搬运除了内存到内存模式都需要一个外设事件来触发。串口收到一个字节会触发DMA搬一次ADC转换完成会触发DMA搬一次SPI收到数据也会触发DMA搬一次。这就好比物流专员不会自己没事找事去仓库搬东西一定是生产线那边按了铃他才过去。这个机制带来的好处是天然的事件驱动外设什么时候有数据DMA就什么时候搬运不需要CPU去轮询外设状态。从软件角度看整个数据处理链路变成了“数据自己流进缓冲区”——程序只需要等待“数据到齐”的通知而不是主动去“扒拉”数据。理解到这一层你再看DMA的各种配置就不会觉得是死记硬背了。所有配置选项都是围绕一个目的告诉DMA搬运工你听谁的指令从哪拿货放哪去拿多少怎么循环。4. ADC多通道采样、双缓冲与“不该用DMA”的场景4.1 ADC连续采样DMA的另一个经典舞台ADC是DMA应用最频繁的外设之一尤其是多通道连续采样场景。没有DMA的时候要么轮询等待每个通道转换完成再去读数据寄存器要么用定时器触发中断在中断里读。前者阻塞CPU后者中断频率高。配置方法和串口类似在CubeMX里把ADC的连续转换模式打开然后在DMA Settings里添加ADC的请求方向选PeripheralToMemory模式选Circular数据宽度根据ADC分辨率来选。对于最常见的12位ADC数据寄存器是16位的内存缓冲区就用uint16_t数组。这里有一个重要的对应关系ADC多通道扫描模式下转换顺序和数据缓冲区的下标一一对应。比如你配置了3个通道扫描顺序是CH0、CH1、CH2那DMA搬回来的数据顺序也永远是adc_buffer[0]对应CH0adc_buffer[1]对应CH1adc_buffer[2]对应CH2以此类推。这个顺序关系是由ADC的通道扫描序列决定的改通道顺序就要同步调整数据解析逻辑不然数据张冠李戴。我实际使用中ADC加DMA配置完成后主循环只需要定期读取缓冲区里最新的值就行——注意是一定时间间隔去读不是每次循环都读。因为DMA一直在循环往缓冲区写你在任意时刻去读读到的“当前值”都只是最近一次转换的结果控制周期本来就是离散的没必要每次循环都去刷新。4.2 双缓冲彻底解决“数据读到一半被覆盖”的问题前面提到过Circular模式的覆盖风险。在ADC高速采样这样的场景下如果你在CPU里用普通数组直接给DMA使用DMA搬完一轮立刻开始下一轮如果CPU处理数据的速度跟不上DMA搬运的速度就会发生“上一轮数据还没处理完这一轮数据已经写进来了”的冲突。这在实时性要求高的场合是不能接受的。双缓冲Double Buffer模式的思路是准备两块缓冲区DMA先往A缓冲区搬搬完后自动切换到B缓冲区继续搬同时触发中断通知CPU“A缓冲区的数据已经备好了你可以处理了”。等CPU处理完ADMA可能已经写了一轮B之后又会切回A。这样CPU和DMA交替使用两块互不冲突的内存谁都不会踩到谁正在用的数据。在支持双缓冲的STM32系列上CubeMX的DMA配置里会有DMA Request和Mode的选项代码里会用HAL_ADCEx_MultiModeStart_DMA或者类似的函数配合内存地址切换。不是所有系列的DMA都支持双缓冲用之前先查一下参考手册里的DMA特性表。如果芯片不支持硬件双缓冲也可以用软件模拟开辟两块缓冲区在DMA传输完成中断回调里切换下一次DMA的目标地址。效果类似只是多一次中断处理的开销。4.3 哪些情况不该用DMADMA不是万能的有些场景强行用DMA反而增加复杂度。这是我做了不少项目之后才慢慢体会到的。首先是SPI短数据收发。如果SPI一次只交换三个字节比如读一个传感器寄存器用DMA的话光是配置DMA句柄、启动传输、等待完成中断的代码开销就比那三个字节本身的传输时间还长。而且SPI从机在速率不高、字节很短的情况下中断方式或者简单的轮询方式完全够用。DMA适合的是“大批量、高频率、可预测”的数据流不适合“短小精悍”的命令式交互。其次是CAN通信。看过很多网上的提问问CAN总线用中断接收还是DMA接收我个人的经验是CAN建议用中断一般不用DMA。原因有两个第一CAN单帧数据量最多8字节数据量很小DMA的大批量搬运优势完全发挥不出来第二CAN报文接收过程中需要做滤波、ID判断、时间戳记录等处理这些逻辑没法用单纯的DMA搬运替代还是得靠中断服务函数里的代码来处理。DMA只能把报文搬运到缓冲区但协议层的处理还是得CPU来做。第三是低功耗场景。DMA和所有外设一样工作时本身也耗电。如果产品对功耗要求极为苛刻需要进入深度睡眠模式那DMA配置必须被认真地关闭处理否则它可能会成为阻止系统进入低功耗的“拦路虎”——因为DMA始终处于使能状态时钟无法关闭。4.4 DMA中断优先级的设置思路DMA配置里还有一项Priority即优先级。这个优先级影响的是DMA传输请求在处理中断/DMA调度时的紧迫程度。我的建议是实时性要求高的数据流给高优先级比如ADC采样、编码器计数捕获实时性要求低的给低优先级比如串口打印日志。还有一个容易忽略的点DMA的中断优先级和对应外设的中断优先级是配合使用的。串口空闲中断和DMA传输完成中断都影响数据接收的及时性如果其中一个优先级设置过低另一个高可能出现数据已到达但中断没有及时得到响应的情况。实践中我把串口全局中断和对应DMA中断设置为同一优先级档次避免互相打断造成的逻辑混乱。5. 我调过的DMA坑从现象到根因的排查链路5.1 坑一数据错乱、每隔一个字节一个0根因是数据宽度不匹配现象串口DMA接收到的数据打印出来完全不对原本发的是01 02 03 04收到的却是01 00 02 00 03 00 04 00这种带0间隔的数据。排查思路先在接收回调里把len和缓冲区原始字节打出来确认DMA确实搬了数据而且数量是对的接着看数据规律——每隔一个字节一个0这个特征太典型了几乎可以锁定是数据宽度配置问题。外设侧也就是串口数据寄存器是8位的而内存缓冲区却被配置成了16位的HalfWord模式DMA每次搬2个字节高字节永远是0低字节才有一个有效数据。解决办法把外设和内存的Data Width都改成Byte然后重新生成代码。排查完这个坑之后我给自己定了条规矩不管是串口、SPI还是ADC外设寄存器多少位DMA两侧就配多少位绝不对称着配。5.2 坑二带Cache芯片上的DMA数据全乱根因是缓存一致性现象在STM32H7这种带D-Cache的芯片上DMA收到的数据有时是旧的有时是乱的而且高速传输时特别明显。排查思路一开始以为又是宽度问题检查了没问题后来发现规律——如果数据量小、传输慢偶尔是对的一旦DMA高速持续搬数据错误率大幅度上升。此时要意识到D-Cache缓存了CPU访问内存的数据CPU从缓冲区读数据时可能读到的是Cache里的旧副本而DMA往内存里写的新数据CPU不见得能及时看见。这就是典型的Cache一致性问题。解决办法在CPU读取DMA缓冲区之前先调用SCB_InvalidateDCache_by_Addr来使对应的Cache行失效强制CPU重新从内存里取数据在CPU写完数据要发给DMA发送前调用SCB_CleanDCache_by_Addr把脏数据写回内存。这是带高级Cache内核的芯片上使用DMA时必须养成的习惯F1/F4这些不带D-Cache的芯片暂时不用管但一旦上H7这一条坑躲不过去。5.3 坑三局部变量缓冲区导致程序跑飞根因是缓冲区生命周期现象程序偶发跑飞而且DMA接收数据越多跑飞得越频繁。排查思路这个坑是我早年间踩的现象很隐秘。后来我检查代码发现DMA接收的缓冲区定义在了一个函数里是局部变量。函数执行完毕栈空间被回收但DMA还在往这个地址写数据相当于把函数返回地址、局部变量、返回栈区都给踩了一遍程序不乱飞才怪。解决办法DMA缓冲区必须定义成全局数组、静态数组或者使用堆内存但确保生命周期覆盖整个DMA使用过程。这是使用DMA的一个铁律凡是被DMA访问的内存生命周期至少要覆盖到DMA停用或者传输完成。栈上的临时数组不能给DMA用这句话我建议刻在桌面上。5.4 坑四GD32E230 ADC DMA数据紊乱案例根因是触发时机和连续请求现象有人在国产GD32E230上配置ADC多通道DMA采集发现数据紊乱每次读出来的通道对应关系都对不上。排查思路这种问题在GD32、STM32G0/G4等系列上出现过很多次核心原因是ADC的DMA请求触发模式配置不对。很多系列的ADC支持两种DMA请求方式一种是每个通道转换完成都触发一次DMA请求另一种是整个规则组转换完成后触发一次DMA请求。如果你需要每个通道都触发但配置成了组触发那么一次转换完成只搬一个数据通道对应关系自然错乱。另外还有连续请求Continuous Requests的概念和ADC的连续转换模式必须配合好不然DMA请求可能在一次转换中被重复触发多次缓冲区里多出多余数据。解决办法先查看具体芯片的参考手册里ADC和DMA的互联章节确认DMA请求的触发模式然后用调试器检查DMA CNDTR计数寄存器的变化规律看每次转换减少的值是否符合通道数最后逐一核对通道扫描顺序和缓冲区下标的对应关系。这类数据紊乱问题九成离不开触发模式、宽度、顺序这三个因素的排查链路——先确认宽度再核对触发模式最后检查扫描顺序千万别上来就改代码。5.5 排查DMA问题的一个高效技巧盯死CNDTR最后分享一个调试DMA问题特别省时间的小技巧DMA有一个计数寄存器在F1系列叫CNDTR在F4/H7系列可能叫NDTR它表示DMA还剩下多少数据没搬运。调试时在调试器的寄存器窗口把这个值打开或者在代码里用__HAL_DMA_GET_COUNTER把它读出来打印。这个寄存器能告诉你很多信息数据在不在动如果外设有数据来CNDTR应该持续减少。如果一直不变大概率DMA没有触发成功或者外设根本没产生数据。收到多少数据缓冲区长度减去CNDTR当前值就是你实际接收的数据长度。数据传输有没有溢出如果CNDTR为0了还在往缓冲区写说明循环模式下缓冲区被打满了但没有人处理这时候就该看看处理速度是不是跟不上了。比起到处打断点看变量盯CNDTR是一条更接近硬件真实状态的调试路径。我在调DMA相关问题时第一件事就是看这个寄存器基本能过滤掉一半的伪问题。写在最后的一点心得从我自己的经验看DMA学习的最大门槛其实不是代码而是思维方式的转变从“CPU一个一个把数据抠出来”变成“DMA一箱一箱把数据搬过来”。串口接收这个场景一旦跑通ADC、SPI、I2C、内存搬运这些场景都是同一套配置逻辑的变体换汤不换药。你先花一个晚上把串口DMA加空闲中断的实验调通掌握“缓冲区加计数寄存器”的组合用法后面再配其他外设的DMA基本就是几分钟的事。至于坑谁都得踩几个宽度配置、缓存一致性、缓冲区生命周期这老三样提前知道能少走不少弯路。
返回列表