news 2026/10/5 12:56:38

STM32 DMA从原理到实战:串口、ADC、内存搬运一次讲透

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32 DMA从原理到实战:串口、ADC、内存搬运一次讲透

做了这么多年嵌入式开发,我可以说DMA是个“用之前觉得没必要,用之后再也回不去”的外设。尤其是做串口大批量收发、ADC连续采样、或者要在内存里搬运大块数据的时候,没有DMA,CPU就一直在那里搬数据,主循环被拖得一卡一卡;有了DMA,数据搬运这个脏活累活直接交给硬件做,CPU腾出手来处理业务逻辑。这篇文章我不准备念手册,而是按我实际项目里配置STM32 DMA时的思路来写:先从原理上搞清楚DMA到底干了什么,再把通道映射、初始化参数这些最容易懵的地方逐个拆开,然后分享串口、ADC、内存搬运这三个最高频场景的具体配置,最后附上CubeMX完整配置流程和这几年我踩过的坑。内容覆盖F1和F4系列,HAL库和标准库都会提到,新手照着一步步做就能用起来。

1. DMA到底是什么,为什么你迟早要用它

1.1 没有DMA的时候,CPU在干什么

先从一个最简单的场景说起。你用STM32的串口接收一帧数据,传统做法是开一个接收中断,来一个字节进一次中断服务函数,把数据从USART->DR寄存器里读出来,放到内存数组里。一次两次没什么感觉,但如果波特率是921600,一帧数据几百个字节,CPU几乎每几十微秒就要被打断一次,所有时间都耗在“从外设寄存器搬到内存”这种机械操作上。

这种“把数据从一个地址搬到另一个地址”的动作,本质上是纯粹的重复劳动,根本不需要CPU去执行指令。DMA(Direct Memory Access,直接存储器访问)就是专门干这个的硬件模块。它能在不需要CPU干预的情况下,自己完成外设寄存器和内存之间的数据搬移。

我经常跟同事打一个比方:CPU好比一个项目经理,中断就是各种紧急电话;每来一个电话,项目经理就得放下手里的事去接。而DMA就像你请了一个专职快递员,外地寄来的货直接由快递员签收、入库,全部搞定之后才跟你打声招呼说“货到了”。项目经理该写方案写方案,该开会开会,效率一下子就不一样了。

1.2 DMA的几个核心概念:方向、搬运单位、指针自增

配置DMA之前,要先搞懂它内部是怎么工作的。一个典型的DMA传输过程,要明确这几个东西:

  • 传输方向(Direction):数据从哪到哪。常见的有三种,外设到内存(比如串口接收、ADC采样)、内存到外设(比如串口发送、DAC输出)、内存到内存(比如把一个大数组拷到另一个数组)。

  • 数据宽度(Data Alignment):每次搬一个字节(Byte)、半个字(Half Word,16位)还是一个整字(Word,32位)。这个必须和外设寄存器以及内存缓冲区对齐,串口DR寄存器一般是8位,ADC转换结果是12位或16位,所以ADC一般配半字,串口配字节。

  • 地址自增(Inc):外设地址通常是固定的,比如USART->DR寄存器永远是那一个地址,所以Peripheral Increment关闭;但内存缓冲区的地址要依序递增,数据到了之后一个一个往后放,所以Memory Increment要打开。

  • 传输模式(Mode):普通模式(Normal)搬完指定长度就停,适合一次性任务;循环模式(Circular)搬完自动从头开始,适合持续接收串口数据或者连续ADC采样,缓冲区会被反复覆盖。

举个例子,串口接收时配置成“外设到内存”,外设地址固定为USART->DR,内存地址递增指向rx_buffer[0]、rx_buffer[1]……每次传输一个字节,触发源是“USART接收到一个字节”这个事件。DMA模块自己在背后完成一切,等你设定好的长度搬完了,它再抛一个“传输完成”事件出来,你也可以在这个事件里做后续处理。

搞懂这四个概念,后面配置代码里的参数就基本能看懂了。DMA不是魔法,它就是一套可配置的硬件搬运逻辑,你把起点、终点、搬多少、每次搬多宽这四个信息告诉它,它就能自己跑。

2. 配置DMA之前先搞懂通道、优先级和初始化参数

2.1 DMA1和DMA2:同一套规矩,两个不同班底

STM32不同系列DMA模块的规模差别很大。F1系列的DMA1有7个通道,DMA2有5个通道;F4系列的DMA1和DMA2各有8个数据流(Stream),每个数据流还能对应多个通道(Channel)。

我最早用ST标准库配置F103的时候,被DMA1_Channel4、DMA1_Channel5这种东西绕得晕头转向。后来用了F4的HAL库,又冒出来DMA2_Stream2_Channel4这种组合,本质上表达能力变强了:同一个外设可以在不同Stream上工作,选定Stream之后还要指定Channel编号,用来标记这个Stream和外设事件请求之间的对应逻辑关系。

F1和F4的DMA差异,我给一个简单对照:

项目STM32F1系列STM32F4系列
模块规模DMA1:7通道,DMA2:5通道DMA1/DMA2各有8个Stream,每个Stream对应8个Channel
配置粒度通道即一个DMA请求源Stream + Channel组合方式,一个Stream可以响应不同外设的请求
典型串口映射USART1_TX对应DMA1 Channel4,RX对应DMA1 Channel5USART1_TX在DMA2 Stream7 Channel4,RX在DMA2 Stream2 Channel4(具体看手册)
使用体验通道少,选择简单,但部分外设映射固定灵活度高,但配置前必须先查映射表

选DMA1还是DMA2,有个使用习惯:尽量把DMA2留给带宽要求高的外设,因为DMA1在处理某些外设(比如ADC)时可能会和别的总线访问产生竞争;但这只是一个参考,最终还是要看你用的外设请求映射到了哪一路DMA。

注意:不同子系列之间,同一个外设的DMA映射可能会变。比如同样是USART1_RX,在某些F4型号上可能默认对应DMA2 Stream5 Channel4,而另一些型号对应DMA2 Stream2 Channel4。最可靠的方法就是打开你所用型号的参考手册(Reference Manual),翻到“DMA request mapping overview”那张表。不要凭记忆死记硬背,CubeMX生成错误配置的案例我见过太多了。

2.2 外设请求与通道映射,查表比死记硬背靠谱

DMA之所以能“自动”搬数据,靠的是外设发来的请求信号。USART接收到一个字节之后,会向DMA发送一个请求;ADC完成一次转换,也会向DMA发送一个请求;DMA收到请求之后,按照你初始化好的配置,搬一个数据,然后等待下一个请求。

关键点在于:一个外设事件能触发哪个DMA通道,是芯片设计时就定好的,不能随便改。以F103为例,USART1_RX只能使用DMA1的Channel5,ADC1只能使用DMA1的Channel1。你想把USART1_RX配到DMA1 Channel3上,硬件上根本行不通。

到了F4系列,组合方式更多一些。比如USART1_RX可以挂在DMA2的Stream2或Stream5上,但需要配合指定的Channel编号。这个映射矩阵是固定的,配置错了DMA不会工作。我的建议是:不要试图背下所有映射关系,而是学会查两张表——参考手册里的“DMA request mapping”表,以及CubeMX里DMA Settings下拉框的候选列表。CubeMX会自动列出当前外设可选的所有DMA请求通道,手动配置的时候照着选就行。

2.3 初始化参数逐个拆解:HAL库和标准库对照

HAL库配置DMA时,核心是一个InitTypeDef结构体。以F4系列串口接收为例,CubeMX生成的初始化代码大概是这个样子:

hdma_usart1_rx.Instance = DMA2_Stream2; hdma_usart1_rx.Init.Channel = DMA_CHANNEL_4; hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE; hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE; hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; hdma_usart1_rx.Init.Mode = DMA_CIRCULAR; hdma_usart1_rx.Init.Priority = DMA_PRIORITY_HIGH; HAL_DMA_Init(&hdma_usart1_rx); __HAL_LINKDMA(&huart1, hdmarx, hdma_usart1_rx);

对照前面说的四个概念,这段代码就很好理解了:外设到内存方向;外设地址不自增(始终是USART的DR寄存器);内存地址自增(rx_buffer一个接一个排);数据宽度都是8位;工作模式是循环模式;优先级高。

如果用标准库,F103上同样的配置是这个画风:

DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel5); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)rx_buffer; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize = RX_BUFFER_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_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel5, &DMA_InitStructure);

对比一下就能发现,HAL库和标准库描述的其实是同一组配置信息,只是组织方式变了。标准库里DMA_BufferSize就是一次要搬的数据个数,HAL库是在启动传输时传入长度。理解了DMA的工作原理,换库根本不叫事。

优先级这里多说一句。DMA通道/数据流之间是有仲裁的,如果两个DMA同时请求,优先级高的先执行。我们在配置串口接收的时候,通常会把DMA优先级设成High或Very High,因为串口数据如果不及时取走,硬件FIFO溢出就会丢字节。但也不是所有外设都给最高优先级就好,如果某个DMA在跑内存到内存的大块搬运,优先级又很高,其他DMA就可能被饿死,要按实际场景权衡。

3. 实战中最高频的三个DMA场景

3.1 串口空闲中断+DMA接收:降低CPU负载的标准配置

串口接收是我用得最多的DMA场景。裸机环境下,用“DMA循环接收+串口空闲中断”这个组合,就能达到类似“收完一整帧再通知CPU”的效果,中间一个字节都不需要CPU操心。

具体思路是这样:先用DMA循环模式把串口数据持续搬到一个环形缓冲区里,同时开启串口的空闲中断(IDLE interrupt)。当一帧数据接收完毕,总线上出现一个空闲状态时,串口硬件会产生一个空闲中断。在这个中断里,我只需要读一下DMA当前还剩下多少没搬(也就是计数器的值),就能算出这一帧数据的长度。

HAL库下的启动代码很简洁:

// 启动DMA循环接收 HAL_UART_Receive_DMA(&huart1, rx1_buffer, RX1_BUFFER_SIZE); // 使能串口空闲中断 __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);

然后在串口中断服务函数里追加空闲判断:

void USART1_IRQHandler(void) { HAL_UART_IRQHandler(&huart1); if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) != RESET) { __HAL_UART_CLEAR_IDLEFLAG(&huart1); // 计算这一帧数据的长度 uint16_t len = RX1_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx); // 把rx1_buffer里的len个字节拷贝出来,交给协议层处理 } }

这里有三个细节需要特别注意。

第一,__HAL_DMA_GET_COUNTER拿到的是DMA剩余未传输的数量。循环模式下,如果DMA已经把整个缓冲区填满并绕回开头,这个计数器会反映当前写指针的位置。用缓冲区总长度减去计数器剩余值,就是“这次空闲到来之前新收到的数据长度”。

第二,数据拷贝要及时,否则下一帧数据到了之后会覆盖掉当前缓冲区内容。我一般在空闲中断里只做“搬运到应用缓冲区”和“置标志位”两件事,真正协议解析放到主循环里做。

第三,空闲中断标志的清除方式和普通中断不同,不要用__HAL_UART_CLEAR_FLAG(&huart1, UART_FLAG_IDLE),直接调用__HAL_UART_CLEAR_IDLEFLAG(&huart1),因为空闲标志的清除序列比较特殊,处理不当会导致中断反复触发。

用这套方案之后,我做过一个实际对比:921600波特率下接收512字节,传统逐字节中断方式,CPU在接收期间的占用率超过八成;换成DMA+空闲中断后,CPU占用率几乎可以忽略,主循环的实时性一下子提上来了。

3.2 ADC多通道连续采样配合DMA:采集不丢数据

ADC多通道扫描模式下,转换完一个通道会产生一个数据,转换完下一个通道又产生一个数据。如果在中断里一个值一个值地读,不仅频繁打断CPU,遇到中断响应不及时还容易漏读。用DMA就很优雅:ADC转换完成信号直接触发DMA搬运,把结果自动存进数组里。

F103上的配置,ADC1对应的DMA通道是DMA1 Channel1。标准库示例大致是这样的:

DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel1); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)adc_value; // uint16_t adc_value[通道数] DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize = CHANNEL_COUNT; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel1, &DMA_InitStructure); ADC_DMACmd(ADC1, ENABLE); ADC_SoftwareStartConvCmd(ADC1, ENABLE);

一次循环之后,adc_value[0]就是第一个通道的结果,adc_value[1]就是第二个通道的结果。这样CPU只需要定期读一次数组,采样率高的场合也不会丢数据。

用HAL库的话,启动方式变成一句:

HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_value, CHANNEL_COUNT);

数组里数据的排列顺序,和你在ADC通道配置里的rank顺序一致。配置多通道ADC时,要保证adc_value数组的大小不小于通道数,否则DMA会越界写内存,这是很多初学者容易忽略的。

3.3 内存到内存搬运:处理大量数据的快车道

DMA不只是能在外设和内存之间搬数据,还能做内存到内存的搬运。做图像处理、协议组包、大量数据预处理的时候,用memcpy虽然也行,但如果对速度敏感,DMA搬运可以解放CPU。

HAL库启动一个内存到内存的DMA传输,示例代码如下:

DMA_HandleTypeDef hdma_mem2mem; hdma_mem2mem.Instance = DMA2_Stream0; hdma_mem2mem.Init.Channel = DMA_CHANNEL_0; hdma_mem2mem.Init.Direction = DMA_MEMORY_TO_MEMORY; hdma_mem2mem.Init.PeriphInc = DMA_PINC_ENABLE; hdma_mem2mem.Init.MemInc = DMA_MINC_ENABLE; hdma_mem2mem.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD; hdma_mem2mem.Init.MemDataAlignment = DMA_MDATAALIGN_WORD; hdma_mem2mem.Init.Mode = DMA_NORMAL; hdma_mem2mem.Init.Priority = DMA_PRIORITY_HIGH; HAL_DMA_Init(&hdma_mem2mem); // 启动一次性搬运 HAL_DMA_Start(&hdma_mem2mem, (uint32_t)src_buffer, (uint32_t)dst_buffer, BUFFER_SIZE); // 或者轮询等待完成 HAL_DMA_PollForTransfer(&hdma_mem2mem, HAL_DMA_FULL_TRANSFER, 1000);

这里有个很容易出错的地方:内存到内存模式,外设地址其实也是内存地址,所以PeriphInc要打开,让源地址也递增。如果你沿用外设到内存的习惯(PeriphInc_Disable),源数据就会一直读同一个地址,得到的结果全是第一个元素重复。

另外,内存到内存的DMA传输没有外设硬件请求来触发,DMA一旦启动就会连续不断地搬,直到搬完设定长度。这种模式只配置成Normal模式就够用,循环模式在这类场景里意义不大。

4. 从CubeMX到代码落地,完整走一遍DMA配置

4.1 CubeMX图形化配置的关键选项

很多新手喜欢用CubeMX生成初始化代码,这没问题,但要注意CubeMX只是帮你把结构体填充好,不等于你能跳过理解。我建议用CubeMX的人一定要看着DMA Settings面板里的选项,逐项对应到原理上。

以STM32F407串口接收为例,在CubeMX里选中USART1,然后在DMA Settings选项卡里点击Add,添加一个DMA Request,方向选“RX”。此时CubeMX会自动给你推荐一个DMA映射,比如DMA2 Stream2 Channel4,同时让你设置以下参数:

  • Priority:我一般选High
  • Mode:选Circular循环模式
  • Data Width:Peripheral和Memory都选Byte
  • Increment Address:Memory勾选,外设不勾

ADC这边,进入ADC1配置,开启Scan Conversion Mode和Continuous Conversion Mode,然后在DMA Settings里添加DMA Request。Cache这些高级设置先不管,CubeMX默认值在大多数场景够用。

生成代码之前还有一个地方要检查:NVIC(嵌套向量中断控制器)设置。串口接收场景,至少要开启USART1全局中断,因为空闲中断是串口事件,必须通过USART1_IRQHandler处理。DMA自身的中断是否开启,取决于你想不想用DMA传输完成中断。循环模式下我通常不开DMA传输完成中断,而用串口空闲中断来通知“一帧数据到了”。普通传输模式下选择DMA传输完成中断才合理。

4.2 真正写代码时要注意的细节

CubeMX生成的代码框架比较完整,但有几个点必须在应用层手工补齐,否则DMA跑不起来。

第一,启动DMA接收。CubeMX默认不会自动开启DMA传输,你需要在初始化完成后手动调用HAL_UART_Receive_DMA。这个函数会同时配置好DMA并启动接收。

第二,空闲中断的使能。CubeMX默认不会使能空闲中断,需要手动添加__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);。

第三,中断服务函数里的处理逻辑。CubeMX生成的stm32f4xx_it.c里面USART1_IRQHandler已经调用了HAL_UART_IRQHandler,但空闲标志的处理需要你在它后面追加代码。注意追加的位置要在HAL_UART_IRQHandler之后,否则可能会被它的中断处理逻辑干扰。

第四,如果使用DMA发送,还需要处理DMA发送完成回调。HAL库中一般重写HAL_UART_TxCpltCallback,在这个回调里释放信号量或者置标志位。不要在这个回调里做耗时操作,因为它本身运行在中断上下文。

4.3 调试验证:怎么确认DMA真的在工作

DMA配置完,怎么确认它真的在搬数据?我最常用的方法有三种。

第一种,直接在调试器里看内存。用STM32CubeIDE或者Keil的调试模式,全速运行程序,让串口工具发一帧数据,然后暂停程序,查看rx1_buffer数组里的内容。如果数据正确出现在数组里,说明DMA接收链路是通的。

第二种,看DMA计数器的值。定时读取__HAL_DMA_GET_COUNTER(&hdma_usart1_rx),在串口不发送数据时它应该是缓冲区总长度;发送数据之后,它应该变小,等数据被处理完再恢复。这个计数器的变化能直观反映DMA搬运的进度。

第三种,逻辑分析仪或示波器观察串口引脚。如果数据确实从设备端发出,初始化配置又没有问题,那问题多半出在DMA方向或者地址映射上。这个时候直接检查外设初始化结构体里的每项配置,对照参考手册找差异,比反复试错快得多。

调试时还有一个很常见的现象:程序一跑,rx1_buffer里面全是0xFF或者乱码。这通常不是DMA的问题,而是串口波特率、数据位、停止位配置不一致导致的接收错误。DMA只是把收到的东西原样搬进内存,它没有能力修正物理层的通信错误。先把串口通信本身调通,再打开DMA,排查思路会清晰很多。

5. DMA使用中的常见问题与排查技巧实录

5.1 数据错位、乱码、卡死:别急着改代码,先查这几个地方

我在论坛里看到过不少人问DMA相关的问题,归结起来无非三类:数据错位、乱码、程序卡死。排查顺序我给出一个自己的习惯:

  • 先确认外设自身通信是否正常。关掉DMA,用阻塞方式或普通中断方式接收,看数据是不是对的。如果阻塞方式下数据本身就有问题,那就是串口参数或者电路问题,DMA不背这个锅。

  • 再确认DMA地址配置。外设地址必须指向外设的数据寄存器,比如USART1->DR、ADC1->DR;内存地址指向一个不小于缓冲区大小的数组。地址写错,轻则数据不对,重则直接HardFault。

  • 然后确认数据宽度一致。串口8位数据就用Byte,ADC 12位/16位结果就用HalfWord。Peripheral和Memory两边宽度必须匹配。如果你用的是Word宽度去接收8位串口数据,内存里就会出现大量空白字节。

  • 最后查缓冲区大小和模式匹配。普通模式下DMA搬完设定的长度就停了,如果串口之后又来了数据,就会丢;循环模式则要注意缓冲区覆盖问题,数据处理速度跟不上接收速度,就会出现“上一帧还没处理完就被下一帧覆盖”的丢包。

程序卡死这个现象,多半不是DMA本身导致,而是DMA搬到了不该搬的地方,破坏了内存数据。比如缓冲区数组定义得太小,DMA写越界,恰好把某个中断向量表或者关键的全局变量覆盖了,程序跑飞就很正常了。解决这类问题,用调试器观察PC指针停在哪个中断或异常里,回头查DMA的地址配置和缓冲区大小,通常能找到线索。

5.2 缓存一致性问题:F4/F7/H7系列的特殊陷阱

用带Cache的STM32(比如F7、H7)跑DMA时,有个特别隐蔽的坑——缓存一致性问题。简单说,CPU写数据时会先写到Cache里,再异步刷到内存;DMA访问内存时不经过Cache。如果CPU刚往一块内存写了数据,DMA就把它搬走,DMA可能读到的是旧数据;反过来,DMA往内存写了数据,CPU去读时可能读的是Cache里的旧值。

F4系列没有独立的D-Cache,F4也有相应的处理要求,但一般默认没问题;F7和H7上,如果启用了D-Cache,就必须在DMA传输前做Cache Clean,在DMA接收后做Cache Invalidate。HAL库提供了对应接口:

SCB_CleanDCache(); // DMA发送前,把CPU写入的数据刷到内存 SCB_InvalidateDCache(); // DMA接收后,让CPU重新从内存读取数据

如果你的F7/H7工程开了D-Cache,DMA数据莫名其妙错乱,先不要怀疑DMA配置,把Cache的Clean和Invalidate加上试试。这算是我在项目里印象最深的一次“冤案”:H7跑以太网,DMA收包总是一会儿正常一会儿乱码,排查了三天,最后发现是忘了做Cache Invalidate。后来只要涉及DMA和Cache交互,我第一反应就是缓存一致性。

5.3 实际项目中的其他经验:中断优先级、主循环负载和DMA联动

串口DMA接收的项目,中断优先级一定要设计好。串口空闲中断优先级如果设得过高,频繁打断其他中断,会导致系统实时性下降;设置得过低,又可能在主循环忙的时候漏掉空闲中断,导致一帧数据迟迟不被处理。我一般把串口中断优先级设在中间偏上,具体数值取决于你的系统中哪些中断更重要。

中断回调里的逻辑也很有讲究。HAL_UART_TxCpltCallback这类回调函数运行在中断上下文,里面绝对不能做阻塞操作,比如加延时、等待信号量、打印日志。我见过有人在DMA接收完成回调里做字符串拼接和格式化,结果回调执行时间过长,后续的数据直接丢失。正确的做法是回调里只做小任务和置标志位,或者用一个简短的消息队列把数据地址和长度传出去,真正的解析放到主循环。

DMA配合外设时,还要注意“传输未完成就被其他外设打断”的情况。比如你用DMA发送串口数据,发送过程中如果又调了一次HAL_UART_Transmit_DMA,底层配置会被重置,上一次传输可能直接夭折。正确的做法是在发送前检查DMA状态,确认上一次传输已经完成,再启动下一次。类似的问题在ADC定时触发采样时也会出现,定时器频率太高,DMA还没搬完上一批数据,新一轮采样又来了,处理不及时就会覆盖掉数据。

说到底,DMA使用中的大部分问题,根源不是你不会写DMA初始化,而是你对整体数据流缺乏预判。每次配置之前,把“谁产生数据、产生多快、数据放到哪儿、谁去消费数据、消费多快”这五个问题想清楚,DMA的坑基本能避开八成。

我个人在实际项目里的体会是:DMA配置这件事,资料虽多,但真正帮到我的还是“慢下来做映射表确认”这个动作。每个新项目拿到芯片后,我都会花十几分钟翻参考手册里的DMA请求映射表,再把用到的外设、方向、通道抄在一张纸上,对应到CubeMX配置界面里逐项核对。这个习惯帮我免掉了好几次“查半天发现是通道选错”的尴尬。如果你也被DMA配置折腾得头疼,不妨试试这个方法,比反复试错稳妥得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 12:52:09

STM32嵌入式C++调试实战:GDB与Renode工程化收尾指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 12:48:02

高频交易场景下TensorFlow模型推理的毫秒级优化实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 12:46:33

Qt 5.15.2 Android环境搭建:JDK/NDK版本匹配全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 12:45:03

Rust链接Oracle库报错:file format not recognized的完整排查与修复

说实话,这个报错我第一次看到的时候整整折腾了一个下午。项目本身不复杂,就是 Rust 服务要连 Oracle 数据库,按常规思路加了 Oracle Instant Client,配好ORACLE_HOME,然后在build.rs里告诉 cargo 去链接clntsh&#xf…

作者头像 李华
网站建设 2026/10/5 12:43:45

Paperclip:Node.js+React构建本地AI智能体的实践范式

1. 项目概述:Paperclip 不是回形针,而是一个正在成型的 AI 智能体开发范式“Paperclip”这个词在当前技术圈里,已经悄悄脱离了办公文具的原始语义,变成一个高频出现、自带隐喻张力的技术代号。它不是某个开源仓库的官方名称&#…

作者头像 李华
网站建设 2026/10/5 12:42:01

openrig:统一装配Claude Code与Codex的YAML配置与npm分发方案

1. 从 openrig 这个标题说起:它到底想解决什么问题 第一次看到 openrig 这个词,我脑子里蹦出来的第一反应是“open”加“rig”的组合。rig 在英文里有“装配、搭建、装置”的意思,在工程语境里常指把一堆零散部件组合成一套能跑起来的系统。所…

作者头像 李华