DMA这东西,刚接触嵌入式的人多半会经历三个阶段:第一阶段觉得它就是个"搬运工",配置好寄存器就能用;第二阶段发现它动不动就罢工,数据对不上、中断不进、传输卡死;第三阶段才明白,DMA真正的难点从来不是"怎么配",而是"什么时候不该用、用了之后CPU和总线的账怎么算"。
这篇是嵌入式驱动开发经验系列的第6期,专门聊DMA。我会把串口DMA收发、SPI DMA、定时器触发DMA、DMA测速这几个高频场景拆开讲,重点放在那些手册上不会写、但实际调试中一定会遇到的东西。不管你是刚把STM32的DMA通道配通,还是已经在做AD7606这类高速采集的驱动,下面这些内容应该都能对上你的某个具体问题。
1. DMA到底在替谁干活:先把数据流和总线账算清楚
很多人配DMA的时候是照着例程抄的,通道号、方向、数据宽度、优先级填完就下载,跑通了就过。但一旦出现"偶尔丢一个字节""传输完成中断进两次""测速结果忽高忽低"这类问题,就完全不知道从哪查。根因往往在于:你根本没搞清楚这条数据流上到底有几个主设备在抢总线。
1.1 从"CPU搬数据"到"DMA搬数据"的本质变化
先建立一个最朴素的模型。没有DMA的时候,串口收到一个字节,触发中断,CPU进中断把数据从数据寄存器读出来存到缓冲区,退出中断,等下一个字节。这个过程里,CPU是搬运工,每搬一个字节都要付出"进中断-读寄存器-写内存-退中断"的固定开销。波特率一高,比如115200甚至更高,CPU就被这些中断切得稀碎,主循环里稍微有点耗时操作就会丢数据。
DMA的做法是:告诉DMA控制器"源地址是串口数据寄存器,目的地址是我的缓冲区,搬N个字节,搬完通知我"。之后CPU该干嘛干嘛,DMA自己在后台完成搬运。这里的关键变化是——搬运这件事从"CPU的时间片"变成了"总线的带宽占用"。CPU不再被中断打断,但DMA每次搬运都要占用总线周期,这个周期是从系统总带宽里扣的。
所以第一个要建立的认知是:DMA不是"免费"的,它把CPU的时间成本换成了总线带宽成本。在低负载场景下这笔买卖非常划算,但在多路DMA同时跑、且都往同一块SRAM写数据的时候,总线仲裁就会成为瓶颈,表现出来就是"每路单独测都正常,一起跑就出错"。
1.2 数据流三要素:源、目的、触发源
任何一条DMA传输,你都要能一句话说清楚三件事:
- 源在哪:是外设寄存器(如USART->DR、SPI->DR、ADC->DR),还是内存(如一个数组)。
- 目的在哪:是内存缓冲区,还是外设寄存器。
- 谁来触发每一次搬运:是外设的硬件请求(串口RXNE、SPI TXE、ADC EOC),还是软件触发,还是定时器事件。
这三要素里,触发源是最容易出问题的一环。举个例子,串口DMA接收,触发源是USART的RXNE事件,每收到一个字节DMA就搬一次。如果你把DMA配成了"内存到内存"模式,那它根本不会理睬串口的请求,自然收不到数据。再比如SPI DMA发送,触发源是SPI的TXE(发送寄存器空)事件,DMA每搬一个字节进DR,TXE就会再次置位请求下一次搬运,形成连续流。
我见过一个很典型的错误:有人用定时器触发ADC,然后想用DMA把ADC数据搬到内存,结果DMA配的是软件触发。现象是ADC在正常转换,但内存里的数据永远不变。原因就是DMA根本没被ADC的EOC事件触发,它只在启动时搬了一次就停了。
1.3 通道映射不是随便选的:查表这件事不能省
STM32的DMA请求和通道(或Stream)之间是固定映射的,不是你想用哪个通道就用哪个。比如STM32F103系列,USART1_RX固定映射到DMA1通道5,USART1_TX固定映射到DMA1通道4,SPI1_RX是通道2,SPI1_TX是通道3。你如果给USART1_RX配了通道2,那它永远收不到DMA请求。
这个映射关系在参考手册的"DMA请求映射"表里写得清清楚楚,但很多人不看,凭感觉选通道,然后花几个小时排查"为什么DMA不进中断"。我的习惯是:在动手写代码之前,先把这条数据流的通道号、触发源、方向在纸上写一遍,对照手册确认无误再开始配。这一步花两分钟,能省掉后面两小时的抓瞎。
对于STM32F4/F7/H7这些用Stream的系列,还要注意Stream和Channel的二级映射:一个Stream可以挂多个Channel,但同一时刻只能选一个。选错了Channel,同样是"配置全对但就是不工作"。
1.4 传输模式:Normal和Circular的取舍逻辑
DMA有两种基本模式:Normal(搬完N个就停,需要手动重启)和Circular(搬完N个自动回到起点继续搬)。选哪个不是拍脑袋决定的,取决于你的数据消费方式。
串口接收这种持续不断的场景,通常用Circular模式配合"半传输中断+传输完成中断"来做双缓冲,这样CPU可以在DMA搬前半段的时候处理后半段,实现无缝衔接。而像"采集固定1024个点然后处理"这种一次性任务,用Normal模式更合适,搬完触发一次中断,处理完再手动启动下一轮。
这里有个坑:Circular模式下,传输完成中断会周期性触发,如果你在中断里做了耗时操作,下一次传输完成可能在你还没处理完的时候就来了。表现出来就是中断里读到的数据被覆盖、或者中断嵌套导致栈溢出。解决办法要么是缩短中断处理时间(只做标记,主循环处理),要么是加大缓冲区让中断间隔变长。
2. 串口DMA收发:从"能收"到"收得稳"之间隔着什么
串口DMA是绝大多数人接触DMA的第一个场景,也是问题最多的场景。热词里"串口dma接收""串口dma"反复出现,说明这块的坑确实密集。下面按接收和发送分开说,重点讲那些"配通了但用不稳"的情况。
2.1 串口DMA接收:不定长数据的处理才是真难点
串口DMA接收最基础的配置是:DMA从USART->DR搬到内存缓冲区,Circular模式,长度设为缓冲区大小。这样数据会源源不断填进缓冲区,你只需要在需要的时候去读就行。但问题来了——你怎么知道当前收到了多少字节?缓冲区里哪些是新数据?
这就是不定长接收的核心难点。DMA本身只告诉你"搬了多少个"(通过NDTR寄存器),但它不知道你的数据帧边界在哪。常见的解决方案有三种:
第一种是空闲中断(IDLE)配合DMA。串口在总线空闲一个字节时间后会触发IDLE中断,你在这个中断里读取DMA剩余计数,算出这一帧收了多少字节,然后处理。这个方案的好处是不依赖协议里的长度字段,对任意帧格式都适用。缺点是如果两帧之间没有空闲间隔(背靠背发送),会合并成一帧。
第二种是协议里带长度字段或结束符。比如Modbus RTU用3.5个字符时间的间隔做帧边界,或者自定义协议用固定帧头帧尾。这种方式需要你在DMA缓冲区里做解析,找到边界后再处理。
第三种是定时器超时判定。用一个定时器在每次收到数据时重置,超时未收到新数据就认为一帧结束。这个方案对背靠背数据更友好,但需要额外的定时器资源。
我个人的经验是:如果对端是PC或标准协议设备,优先用IDLE中断;如果是两个MCU之间通信且数据量大,用长度字段更可靠。IDLE中断在STM32F1上有个已知问题——清除标志位的顺序不对会导致中断反复触发,正确顺序是先读SR再读DR。
2.2 串口DMA发送:什么时候该等,什么时候不该等
发送方向相对简单,但有个关键决策:发送完成之前,能不能修改发送缓冲区?
DMA发送是异步的,你启动DMA之后,CPU继续往下跑,但DMA还在从缓冲区里读数据往串口送。如果你这时候修改了缓冲区内容,DMA送出去的就是被改过的数据。所以标准做法是:启动DMA发送后,要么等传输完成中断再复用缓冲区,要么用双缓冲交替发送。
对于连续发送大量数据的场景,双缓冲是标配。准备两个缓冲区A和B,DMA在发A的时候CPU往B里填数据,A发完切换DMA去发B,CPU再往A里填。这样发送就不会断流。实现上可以用传输完成中断来切换,也可以用半传输中断做更细粒度的流水线。
有个细节容易被忽略:串口DMA发送完成中断触发时,最后一个字节可能还在移位寄存器里没发完。如果你在中断里立刻拉低RS485的DE引脚(发送使能),最后一个字节会被截断。正确做法是等TC(传输完成)标志置位,而不是等DMA的传输完成中断。STM32的USART有专门的TC中断,或者你可以在DMA完成中断里轮询TC标志。
2.3 收发共存时的通道冲突与优先级
当串口同时收发都用DMA时,RX和TX占用两个不同的通道,这本身没问题。但如果系统里还有SPI DMA、ADC DMA在跑,就要考虑优先级了。
STM32的DMA通道有四级优先级:Very High、High、Medium、Low。优先级决定的是当多个通道同时请求总线时,谁先被服务。注意,它不决定传输速度,只决定仲裁顺序。如果两个通道优先级相同,则通道号小的优先。
实际配置时的原则是:对实时性要求高的用高优先级,数据量大但可以容忍延迟的用低优先级。比如串口接收通常比SPI发送更紧急(因为串口没有流控的话,收慢了就丢数据),所以串口RX给High,SPI TX给Medium。但这不是绝对的,如果你的SPI接的是显示屏,刷新延迟肉眼可见,那SPI的优先级就得提上去。
还有一个隐藏问题:高优先级通道如果持续请求,低优先级通道可能被"饿死"。比如串口以极高波特率持续接收,DMA请求几乎不间断,那低优先级的ADC DMA可能长时间得不到总线。这种情况要么降低高优先级通道的请求频率(比如用FIFO攒一批再搬),要么重新分配优先级。
3. SPI DMA与定时器触发DMA:高速场景下的时序陷阱
串口DMA的节奏相对慢,问题暴露得也慢。到了SPI DMA和定时器触发DMA这种高速场景,时序问题会被放大,很多在串口上"看起来正常"的配置,到这里就直接崩了。
3.1 SPI DMA的全双工陷阱:TX和RX必须同时启动
SPI是全双工总线,每发送一个字节的同时也会接收一个字节。这个特性在DMA配置上有个硬性要求:TX DMA和RX DMA必须同时使能,否则会丢数据或卡死。
原因在于SPI的工作机制:主机拉低CS、开始产生时钟,每个时钟周期移出一位同时移入一位。如果你只启动了TX DMA,RX方向的数据寄存器溢出(OVR标志置位),SPI会停止工作。反过来只启动RX DMA,TX没有数据可发,时钟根本不会产生,RX也收不到东西。
所以正确的顺序是:先配置好TX和RX两个DMA通道,然后先使能RX DMA,再使能TX DMA,最后使能SPI。这个顺序保证了SPI开始产生时钟的时候,两个方向的DMA都已经就绪。我见过有人先使能SPI再使能DMA,结果第一个字节就丢了。
另外,SPI的DMA请求映射也要注意。以STM32F103为例,SPI1的RX是DMA1通道2,TX是DMA1通道3;SPI2的RX是DMA1通道4,TX是DMA1通道5。如果你用的是SPI2但按SPI1的通道配,那肯定不工作。
3.2 定时器触发DMA:TIM DMA Burst到底解决什么问题
热词里出现了"stm32的tim dma burst应用",这个功能值得单独说。TIM DMA Burst(突发)允许定时器在特定事件(如更新事件、比较匹配)时,通过DMA一次性向定时器的多个寄存器写入数据。典型应用是用DMA动态修改PWM的占空比,实现呼吸灯、电机调速等效果,而不需要CPU干预。
普通做法是每次要改占空比就进中断写CCR寄存器,占空比变化频繁时中断开销很大。用TIM DMA Burst,你可以准备一个占空比数组,DMA在每次更新事件时自动把下一个值写进CCR,CPU完全不参与。这对于需要生成复杂波形(如正弦波PWM、SVPWM)的场景非常有用。
配置要点:TIM的DMA请求要选对事件(通常是UPDATE),DMA方向是内存到外设,外设地址是CCR寄存器,内存地址递增,外设地址不递增。还要注意TIM的DMA Burst长度设置,它决定了每次触发搬几个寄存器。如果只改CCR1,长度设为1就行;如果要同时改CCR1到CCR4,长度设为4。
有个坑:TIM DMA Burst和普通的DMA请求不是一回事。普通DMA请求是外设发一个请求DMA搬一次,而Burst是在一个事件里连续搬多个。如果你把Burst长度设成了4但只准备了1个数据,DMA会读到数组越界的内容写进寄存器,现象就是PWM输出莫名其妙。
3.3 AD7606这类高速采集:DMA和BUSY信号的配合
热词里"安富莱ad7606"出现了,AD7606是8通道16位并行/串行ADC,采样率能到200kSPS。用DMA搬运它的数据时,核心问题是什么时候启动DMA、什么时候读数据。
AD7606的转换由CONVST信号触发,转换期间BUSY为高,转换完成后BUSY变低,此时可以读取数据。如果用DMA搬运,通常的做法是:用定时器产生CONVST信号,同时用这个信号触发DMA读取。但要注意,DMA读取必须在BUSY变低之后才能进行,否则读到的是无效数据。
一种可靠的方案是用BUSY的下降沿触发DMA请求。但BUSY是GPIO,不是外设,不能直接触发DMA。变通做法是用定时器在CONVST之后延迟一个转换时间再触发DMA,这个延迟要略大于AD7606的最大转换时间(约4微秒)。或者用外部中断在BUSY下降沿启动DMA,但中断延迟会引入抖动。
对于200kSPS这种速率,每个采样点间隔5微秒,中断延迟可能就占了一两微秒,抖动很明显。所以高速采集场景下,用硬件定时器精确控制时序比用中断更可靠。安富莱的例程里通常是用定时器触发CONVST,然后用同一个定时器的另一个通道在固定延迟后触发DMA读取,这样时序是硬件保证的,不受中断影响。
4. DMA测速:为什么你的测速结果不可信
热词里"dma测速""dma测速工具""dma测速失败代码"扎堆出现,说明很多人在这件事上翻过车。DMA测速看起来简单——记录传输前后的时间戳,除以数据量就是速度。但实际做下来,结果往往和理论值差很远,甚至同一段代码跑两次结果都不一样。
4.1 测速的三种方法及其误差来源
常见的DMA测速方法有三种:
方法一:用系统滴答定时器(SysTick)计时。在DMA启动前读SysTick计数,传输完成中断里再读一次,差值乘以滴答周期就是耗时。问题是SysTick分辨率通常是1毫秒,而一次DMA传输可能只有几十微秒,用1毫秒的尺子量几十微秒的东西,误差极大。
方法二:用通用定时器计时。配置一个定时器以最高频率计数(比如72MHz),DMA启动时清零并启动,完成时读取计数值。这个方法分辨率高,但要注意定时器溢出。如果传输时间超过定时器周期,计数会回绕,需要处理溢出。
方法三:用DWT的CYCCNT计数器。Cortex-M3/M4/M7内核有个DWT单元,其中的CYCCNT是一个32位的周期计数器,每个CPU时钟周期加一。用它测速分辨率就是CPU周期,非常精确。但要注意两点:一是CYCCNT在调试器连接时可能被复位,二是32位计数器在72MHz下约60秒溢出,长时间测量要处理回绕。
我推荐用DWT方案,代码简单且精度足够。初始化就三行:使能DWT、使能CYCCNT、清零CYCCNT。测量时读两次CYCCNT,差值除以CPU频率就是秒数。
4.2 影响测速结果的隐藏因素
即使计时方法正确,测速结果仍然可能偏离理论值,原因通常有这几个:
总线仲裁开销。如果测速时有其他DMA通道在活动,你的传输会被仲裁延迟。所以测速应该在"干净"的环境下做,关掉其他DMA通道。
存储器等待周期。如果DMA访问的是外部SRAM或SDRAM,等待周期会显著拉低速度。内部SRAM通常零等待,但Flash有等待周期。测速时要明确数据源和目的地的存储器类型。
Cache的影响(针对带Cache的M7等内核)。如果DMA访问的内存区域被Cache缓存,可能出现数据一致性问题,测速结果也会异常。DMA缓冲区通常要配置为Non-Cacheable或使用Cache维护操作。
编译优化等级。这个听起来无关,但如果你的测速代码里包含了CPU参与的环节(比如启动DMA前后的准备和收尾),优化等级会影响这部分耗时。测速时最好把CPU参与的部分单独计时,只测DMA搬运本身。
4.3 一个可复现的测速代码框架
下面给一个基于DWT的DMA测速框架,以内存到内存传输为例,你可以把源和目的换成外设寄存器来测串口或SPI的DMA速度。
#include "stm32f1xx.h" static void DWT_Init(void) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; } static uint32_t DWT_GetCycles(void) { return DWT->CYCCNT; } #define TEST_SIZE 1024 static uint8_t src_buf[TEST_SIZE]; static uint8_t dst_buf[TEST_SIZE]; void DMA_MemToMem_Test(void) { uint32_t start, end, cycles; float us, speed_mbps; DWT_Init(); for (int i = 0; i < TEST_SIZE; i++) { src_buf[i] = (uint8_t)i; } DMA1_Channel1->CCR &= ~DMA_CCR_EN; DMA1_Channel1->CPAR = (uint32_t)src_buf; DMA1_Channel1->CMAR = (uint32_t)dst_buf; DMA1_Channel1->CNDTR = TEST_SIZE; DMA1_Channel1->CCR = DMA_CCR_MEM2MEM | DMA_CCR_MINC | DMA_CCR_PINC | DMA_CCR_DIR | DMA_CCR_PL_0; start = DWT_GetCycles(); DMA1_Channel1->CCR |= DMA_CCR_EN; while (!(DMA1->ISR & DMA_ISR_TCIF1)); end = DWT_GetCycles(); DMA1->IFCR = DMA_IFCR_CTCIF1; cycles = end - start; us = (float)cycles / (SystemCoreClock / 1000000.0f); speed_mbps = (TEST_SIZE * 8.0f) / us; printf("cycles=%lu, time=%.2f us, speed=%.2f Mbps\r\n", cycles, us, speed_mbps); }这段代码测的是纯DMA搬运的周期数,不包含CPU准备时间。实测在STM32F103(72MHz)上,1024字节内存到内存传输大约需要几千个周期,换算下来速度在几十MB/s量级。注意内存到内存模式在F1上只有DMA1支持,且通道1是唯一支持M2M的通道。
如果你要测串口DMA的实际吞吐,把源换成USART->DR,触发源改成串口的RX请求,然后在对端以固定波特率连续发送数据,测接收完指定字节数的时间。这时候算出来的速度应该接近波特率除以10(每字节10位:起始+8数据+停止),如果差很多,说明中间有丢包或停顿。
5. 那些让DMA"莫名其妙不工作"的细节
前面讲的都是相对"正常"的问题,这一节专门收罗那些看起来毫无道理、但确实会发生的DMA故障。这些坑我基本都踩过,写出来帮你省时间。
5.1 传输完成标志没清导致中断反复进
DMA的中断标志清除有个容易搞错的地方:不同系列、不同通道的标志清除方式不一样。STM32F1的DMA1和DMA2各有自己的IFCR寄存器,清除时要写对应的位。F4的DMA用LIFCR和HIFCR两个寄存器分别清低端和高端Stream的标志。H7又不一样。
如果你清错了寄存器或清错了位,标志一直置位,中断就会反复进。现象是程序卡在中断里出不来,或者主循环完全跑不动。排查方法很简单:在中断里读一下ISR寄存器,看看是哪个标志置位,然后对照手册确认清除位是否正确。
还有一个细节:有些标志是"读清除"的,有些是"写清除"的。DMA的传输完成标志通常是写1清除,但如果你在中断里先读了ISR再写IFCR,顺序不对可能清不掉。标准做法是直接写IFCR对应的位,不要先读。
5.2 数据宽度不匹配导致的"数据错位"
DMA的数据宽度有三个可配项:外设数据宽度、内存数据宽度、以及传输时的对齐方式。如果外设是8位的(比如USART->DR),内存缓冲区你定义成了uint16_t数组,DMA配置里内存宽度选了16位,那DMA会按16位搬运,每次从串口读两个字节拼成一个16位值写进内存。结果就是数据全乱了。
正确的做法是:外设和内存的数据宽度要匹配实际的数据类型。串口是8位,内存就用uint8_t数组,宽度都选8位。ADC是12位,但ADC->DR寄存器是16位对齐的,所以外设宽度选16位,内存也用uint16_t。SPI如果配置成8位模式,两边都选8位;配置成16位模式,两边都选16位。
有个例外是当你确实需要做数据宽度转换时,比如把8位的串口数据打包成16位存储。这时候外设宽度8位、内存宽度16位,DMA会自动做拼接。但要注意字节序问题,小端模式下先收到的字节在低地址,拼出来的16位值是高字节在后。
5.3 DMA和Cache的一致性:M7用户必看
如果你用的是STM32H7、F7这类带D-Cache的芯片,DMA和Cache的一致性问题一定会遇到。现象是:DMA明明把数据写进了内存,但CPU读出来还是旧值;或者CPU写了新数据到缓冲区,DMA发出去的还是旧数据。
原因是DMA直接访问物理内存,不经过Cache。而CPU访问内存时,如果该地址被Cache缓存了,读的是Cache里的副本,写也只写Cache。两边看到的内存内容不一致。
解决办法有三种:
- 把DMA缓冲区配置为Non-Cacheable。通过MPU(内存保护单元)把这块区域设为非缓存,CPU和DMA都直接访问物理内存,自然一致。这是最简单可靠的方案,代价是CPU访问这块内存会慢一些。
- 使用Cache维护操作。在DMA读取之前,对缓冲区做Clean操作(把Cache里的新数据写回内存);在DMA写入之后,对缓冲区做Invalidate操作(丢弃Cache里的旧数据,强制从内存重新读)。这个方案性能好但容易出错,Clean和Invalidate的时机和范围必须精确。
- 使用带Cache一致性的DMA。部分高端芯片的DMA控制器支持硬件一致性,但STM32系列基本没有,所以主要靠前两种。
我的建议是:对DMA缓冲区一律用MPU配成Non-Cacheable,省心。虽然损失一点CPU访问性能,但避免了大量隐蔽的调试时间。如果缓冲区很大且CPU访问频繁,再考虑用Cache维护操作精细控制。
5.4 中断优先级配置不当引发的连锁反应
DMA中断的优先级配置有个常见误区:DMA传输完成中断的优先级如果低于其他中断,可能被长时间阻塞,导致DMA无法及时重启。在Circular模式下这会导致数据覆盖,在Normal模式下会导致传输停顿。
更隐蔽的问题是中断嵌套。如果DMA中断优先级较高,它可能打断其他中断的处理。如果其他中断里正好在操作DMA相关的数据结构,就会出现竞态。比如主循环在准备下一批DMA数据,DMA完成中断来了,中断里修改了缓冲区指针,主循环继续用旧指针操作,数据就乱了。
解决办法是:DMA中断里只做最少的操作(置标志、切换缓冲区),复杂处理放到主循环。如果必须在中断里操作共享数据,用临界区保护或者原子操作。STM32的NVIC优先级分组也要注意,确保DMA中断的抢占优先级和子优先级的组合符合预期。
6. 从能跑到跑得好:DMA使用的几条经验法则
聊了这么多具体问题,最后收拢一下,给几条我在实际项目中总结出来的DMA使用原则。这些不是手册上的规定,而是踩坑之后形成的习惯。
第一条:能用DMA的地方优先用,但不要为了用而用。DMA适合"数据搬运量大、CPU参与无意义"的场景,比如串口收发、ADC采集、SPI通信。但如果只是偶尔搬几个字节,用DMA的配置开销和中断开销可能比CPU直接搬还大。判断标准很简单:如果搬运操作在总运行时间里占比很低,就别上DMA。
第二条:缓冲区大小要留余量,但不要太大。DMA缓冲区太小会导致频繁中断,太大则浪费内存且增加延迟。串口接收缓冲区一般取最大帧长的2到4倍,ADC采集缓冲区取一次处理所需点数的整数倍。Circular模式下缓冲区大小最好是2的幂,方便用位运算做索引回绕。
第三条:中断里只做标记,不做处理。这条前面提过,但值得再强调。DMA中断触发频率可能很高,中断里每多一条指令,在高频场景下都是负担。把数据处理放到主循环,中断里只更新一个状态变量或切换缓冲区指针。
第四条:测速要在真实负载下做。空载测出来的DMA速度没有参考价值,因为实际系统中总线上还有其他主设备在活动。测速时要把所有相关的DMA通道都开起来,模拟真实工况,这样得到的数据才能用来做容量规划。
第五条:文档和注释要写清楚通道映射和触发源。DMA配置代码如果只写寄存器操作,过两个月你自己都看不懂。在代码里注释清楚"这个通道对应USART1_RX,触发源是RXNE,Circular模式,缓冲区大小256",后面维护的人会感谢你。
DMA这个主题往深了挖还有很多内容,比如多缓冲模式、FIFO阈值配置、双AHB主接口的负载均衡等,这些在高端系列上才会用到。对于大多数项目来说,把上面这些基础场景吃透,已经能解决九成以上的问题。真正拉开差距的,往往不是谁背的寄存器多,而是谁在出问题的时候能快速定位到是"配置错了"还是"时序不对"还是"总线抢不过"。这个判断力,只能靠一个个坑喂出来。