STM32串口DMA实战指南:原理、CubeMX配置与高频坑解析
2026/9/17 16:13:38 网站建设 项目流程

串口调试这事儿,很多刚刚从标准库转到HAL库、或者直接被CubeMX“毒害”的朋友,都会遇到一个很尴尬的阶段:HAL_UART_Transmit用得顺手,点灯、发日志都没毛病,可一旦数据量上来、或者需要高频收发,就会明显发现整个程序被串口“绑架”了——CPU 全耗在傻等寄存器标志位上。等你开始意识到该用 DMA 来解放 CPU,接着又会掉进“DMA 配置了不工作”“发送两三次就卡死”“接收不到完整一帧”这类大坑。

这篇文章是 STM32CubeMX 系列教程的第 5 篇(也是我实战中反复踩坑后整理出来的总结),专门把 DMA 这件事从上到下拆清楚:DMA 的原理、CubeMX 里每一项配置的含义、串口收发怎么结合 DMA 写代码、以及几个网上问烂了的高频坑到底怎么解决。不管你是刚装好 CubeMX 的新手,还是被 DMA 发送偶发卡死折磨过的老玩家,这篇里的代码和思路都能直接拿过去参考。

1. 没有DMA时,串口在一秒里浪费了多少CPU时间

先说清楚一个底层事实:无论是标准库还是 HAL 库,串口发送一个字节,本质是往数据寄存器DR里写数据,然后等硬件把这一字节从移位寄存器按位发出去。发送完成的标志通常是TXE(发送数据寄存器空)或TC(发送完成)。阻塞式发送就是“读标志、写数据”无限循环。

1.1 串口搬运一帧数据的完整路径

以波特率 115200、发送 100 字节为例。串口每传一字节实际占用 10 bit(8 位数据 + 1 位起始 + 1 位停止,若带奇偶校验则更多)。算一下:

单字节耗时 = 10 / 115200 ≈ 86.8 微秒 100 字节耗时 ≈ 8.68 毫秒

这 8.68 毫秒里,CPU 如果用的是HAL_UART_Transmit阻塞发送,那就是完全空转的。主循环被暂停,按键扫描停摆,传感器数据处理全部延后。如果你的程序里还有多个串口要轮流发日志,那 CPU 空转的时间几乎是灾难级别。

有人会说,那我用中断发送不就行了?每发一个字节进一次中断,主程序照样可以干别的。这个思路方向对,但代价是:中断次数 = 数据量

100 字节就要进 100 次发送中断。虽然单次中断很短,但累积起来依然会抢占 CPU,并且每次中断还要做压栈出栈、保存恢复现场,这些时间都是实打实的开销。更关键的是,如果系统里还有定时器中断、外部中断、ADC 采集中断等实时性要求更高的任务,串口中断一多,优先级冲突和响应延迟问题就全都冒出来了。

1.2 中断发送的局限在哪里

用中断发送除了频率高,还有一个编程上容易引发 Bug 的地方:缓冲区管理。

中断发送本质上是“边发边取”,你传给串口的发送缓冲区,在最后一个字节发完之前绝对不能释放或覆盖。很多新手在这里出问题:第一次调用HAL_UART_Transmit_IT后不等待发送完成,直接把 buffer 内容改了,结果串口发出去的是篡改后的数据,或者干脆乱码。

1.3 DMA解决的核心问题

DMA(Direct Memory Access,直接存储器访问)解决的就是上述“CPU 搬砖”的问题。它的本质是一个独立于 CPU 的硬件搬运工,可以自动把数据从内存搬到外设(串口发送),或从外设搬到内存(串口接收),搬完一整个数据块之后,再通过中断告诉 CPU 一声“我搞定了”。

用 DMA 发送 100 字节,CPU 只需要做两件事:启动 DMA、等待完成中断。中间那 8.68 毫秒,CPU 全都可以拿去跑主逻辑。数据量越大,DMA 的优势越明显。这就是为什么做通信、做日志、做 OTA 升级、做上位机交互,DM A 是绕不开的一环。

2. CubeMX中串口DMA的配置项,每一项都别选错

CubeMX 的出现把配置门槛降了一大截,但很多朋友双击串口,看到DMA Settings选项卡里那一堆参数就头晕。这里先以最常见的 USART1 为例,讲清楚每个配置项的含义和选法。

2.1 在CubeMX里找到DMA入口并添加TX/RX请求

工程建好之后,按如下路径操作:

  1. 左侧Categories选择Connectivity > USART1
  2. Mode中点击Asynchronous开启异步串口模式。此时下面会出现DMA Settings选项卡。
  3. 进入DMA Settings,点击Add,可以看到两个可添加的 DMA 请求:USART1_TXUSART1_RX。分别添加。

添加完成后你会看到两行配置条目,分别对应发送通道和接收通道。F103 上默认会分配给DMA1_Channel4(TX)和DMA1_Channel5(RX),CubeMX 已经根据硬件映射表帮你选好通道,这个不需要手动干预。如果你用的是 F4、H7 系列,则可能是 DMA2 的某个 Stream(流)。不同系列 Channel/Stream 的叫法不同,但配置思路完全一致。

2.2 关键参数的选法及理由

以下是我在实际项目里经过大量测试后认为最适合串口场景的配置组合:

配置项TX(发送)RX(接收)说明
DirectionMemory To PeripheralPeripheral To Memory数据是从内存搬到外设(发送),还是从外设搬到内存(接收)
PriorityLow / Medium 视情况Medium / High串口速率不高时 Low 就够;大量数据收发时建议 RX 优先级高于 TX
ModeNormalCircular 或 Normal发送用 Normal,接收不定长数据建议 Circular,详见下文
Increment AddressMemory 勾选,Peripheral 不勾选Peripheral 不勾选,Memory 勾选外设地址固定,内存缓冲区地址逐字节递增
Data WidthByteByte串口一个寄存器一次传一个字节,选 Byte

很多教程直接让你照抄这些配置,但没解释Increment Address到底为什么这样勾。这里我特意说明一下:Peripheral指的是外设寄存器地址(串口DR地址),它只有一个固定地址,永远不需要递增,所以必须保持DisableMemory指的是你定义的缓冲区地址,DMA 要把一坨数据依次写到连续的缓冲区里,所以Memory一定要Enable。如果你关掉了内存地址自增,那 DMA 就会把所有字节都搬运到缓冲区的第一个位置,后发的数据覆盖先发的数据,收到的数据永远是最后一个字节。

Data Width选 Byte,是因为串口DR寄存器本身是 8 位的。虽然有些芯片支持写 RDR 时也能按半字或字访问,但实际串口数据就是字节流,选 Byte 最简单且不容易出问题。选 Half Word 或 Word 需要严格对齐缓冲区,反而容易引入额外的坑。

2.3 生成代码后HAL库替你做了什么

配置完成后点击生成代码,CubeMX 会在mx_usart1_uart_init()中自动创建两个 DMA 初始化结构体,并通过__HAL_LINKDMA把 DMA 句柄挂到串口句柄上。核心代码类似:

static void MX_USART1_UART_Init(void) { huart1.Instance = USART1; huart1.Init.BaudRate = 115200; huart1.Init.WordLength = UART_WORDLENGTH_8B; huart1.Init.StopBits = UART_STOPBITS_1; huart1.Init.Parity = UART_PARITY_NONE; huart1.Init.Mode = UART_MODE_TX_RX; huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; huart1.Init.OverSampling = UART_OVERSAMPLING_16; HAL_UART_Init(&huart1); hdma_usart1_tx.Instance = DMA1_Channel4; hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH; hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE; hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE; hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; hdma_usart1_tx.Init.Mode = DMA_NORMAL; hdma_usart1_tx.Init.Priority = DMA_PRIORITY_LOW; HAL_DMA_Init(&hdma_usart1_tx); __HAL_LINKDMA(&huart1, hdmatx, hdma_usart1_tx); }

注意__HAL_LINKDMA这一步非常关键。它建立了外设句柄和 DMA 句柄之间的连接,之后你调用HAL_UART_Transmit_DMA时,HAL 库会自动通过这个连接找到对应的 DMA 通道并启动搬运。如果你是自己手写初始化而没有做这个 Link,后面所有 DMA 函数都会让你莫名其妙地卡死或收不到数据。

3. 串口发送DMA:把阻塞发送改成DMA发送的正确姿势

CubeMX 帮你配置好之后,HAL 库并不会自动使用 DMA 发送。它只是完成了底层初始化,真正发送还是靠你调用相应函数。

3.1 发送DMA的核心函数与调用逻辑

发送相关的核心函数是:

HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size);

这个函数的逻辑很简单:传入串口句柄、缓冲区地址和数据长度,HAL 库会设置 DMA 通道的源地址、目标地址、传输长度,然后启动 DMA。DMA 搬运完成后,会触发发送完成中断,HAL 库自动跳到回调函数HAL_UART_TxCpltCallback里,让你可以在里面处理后续逻辑。

写一个最简单的发送函数:

uint8_t tx_buf[256]; void uart_send_dma(uint8_t *data, uint16_t len) { memcpy(tx_buf, data, len); HAL_UART_Transmit_DMA(&huart1, tx_buf, len); }

这里我特意先做了一次memcpy到专用发送缓冲区tx_buf,原因是:DMA 发送是异步的,函数返回后数据才刚开始传输。如果你直接把外部传入的data指针交给 DMA,而data指向的缓冲区后续又被别的代码修改,那发出去的内容就会变成修改后的内容,产生隐性 Bug。使用发送专用缓冲区,至少能保证 DMA 在搬运期间看到的是一个稳定副本。

3.2 防止连续发送冲突的状态标志

HAL_UART_Transmit_DMA有一个让人又爱又恨的机制:HAL 库内部有个gState状态机,发送期间状态是HAL_UART_STATE_BUSY_TX,如果此时再次调用发送函数,HAL 会直接返回HAL_BUSY,但不报任何错误。很多初学者在这里会卡壳:连续调用两次发送,第二次没反应。

解决办法是在自己的代码里用标志位管理发送“忙”状态:

volatile uint8_t uart1_tx_busy = 0; void uart_send_dma(uint8_t *data, uint16_t len) { while (uart1_tx_busy); // 等待上一次发送完成 memcpy(tx_buf, data, len); uart1_tx_busy = 1; HAL_UART_Transmit_DMA(&huart1, tx_buf, len); } void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { uart1_tx_busy = 0; } }

回调函数里把忙标志位清零,发送函数开头等待标志位为 0。这样即使主循环里频繁调用发送函数,也不会出现因为 HAL 的gState还没复位而吞掉数据的情况。

3.3 一个可以直接抄的发送框架

实际项目中我通常把发送函数封装成支持可变长参数的形式,方便直接当 printf 用:

#include <stdarg.h> #include <stdio.h> uint8_t uart1_tx_buf[512]; volatile uint8_t uart1_tx_busy = 0; void uart1_send_dma_printf(const char *fmt, ...) { va_list args; va_start(args, fmt); uint16_t len = vsnprintf((char *)uart1_tx_buf, sizeof(uart1_tx_buf), fmt, args); va_end(args); if (len >= sizeof(uart1_tx_buf)) len = sizeof(uart1_tx_buf) - 1; while (uart1_tx_busy); uart1_tx_busy = 1; HAL_UART_Transmit_DMA(&huart1, uart1_tx_buf, len); }

这个封装的好处是从此主代码里可以直接uart1_send_dma_printf("adc:%d\r\n", adc_val);发日志,而完全不用关心底层是 DMA 还是阻塞。配合uart1_tx_busy使用,即使中断里想发送数据,只要注意别在中断里长时间while等待即可。

有一点需要提醒:不建议在中断上下文里调用带while (uart1_tx_busy)的版本。如果 DMA 发送卡死,这个 while 会直接死循环,中断永远出不来。更好的做法是中断里只置一个“想发数据”标志位,真正发送放在主循环处理。

4. 串口接收DMA:不定长数据就得靠空闲中断来收尾

发送 DMA 相对简单,接收 DMA 才最容易让人崩溃。很多人第一次用HAL_UART_Receive_DMA都会发现一个诡异现象:接收缓冲区的数据确实在更新,但你不知道“这一帧数据什么时候结束”,不知道数据长度,也不知道该什么时候处理。

4.1 接收DMA的配置选择:Normal和Circular怎么选

CubeMX 中接收 DMA 模式有两种:Normal 和 Circular(循环模式)。

Normal 模式下,DMA 一旦把Size设定个数的字节搬完,就停止工作,必须重新调用HAL_UART_Receive_DMA才能继续接收。这意味着如果你定义了 256 字节的缓冲区,DMA 必须收满 256 字节才触发接收完成中断,在这之前即使对方发来 10 个字节,DMA 也只会默默把数据放进缓冲区,不给你任何通知。

Circular 模式下,DMA 搬运完Size个字节后计数器自动回绕,继续从缓冲区头部重新搬运,无限循环运行。配合串口空闲中断,我们就可以做到“不确定数据长度,也能知道一帧数据什么时候结束”。

结论:做不定长数据接收,RX 建议选 Circular。做定长数据块接收(如接收一条固定格式指令、OTA 分包),Normal 模式更直观,收满一组就触发一次回调。

4.2 空闲中断的触发时机与处理思路

串口空闲中断(IDLE Interrupt)的触发时机是:当串口接收完一帧数据后,接收线上出现一个字节时间的空闲电平。也就是说,对方发完最后一个字节后,总线上持续一个字节周期没有新数据,硬件就认为这一帧收完了,触发 IDLE 事件。

这个机制天然适合处理不定长协议帧:数据来的时候我就在 DMA 循环模式里继续收,一旦对方停顿一整个字节时间,IDLE 就告诉我们“可以统计这次收了多长了”。

使用 H A L 库时,CubeMX 默认不会使能 IDLE 中断,需要手动开启:

__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);

开启后,USART1_IRQHandler里就会收到该中断事件。

4.3 接收完整例程与长度计算

下面是一个完整的不定长接收示例,基于 F103 + HAL 库,缓冲区大小设 256,DMA 使用 Circular 模式,串口使能 IDLE 中断。

#define RX_BUF_SIZE 256 uint8_t rx_buf[RX_BUF_SIZE]; volatile uint16_t rx_len = 0; void uart1_rx_start(void) { HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_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)) { __HAL_UART_CLEAR_IDLEFLAG(&huart1); // 计算本次接收到的数据长度 uint16_t cur_cnt = __HAL_DMA_GET_COUNTER(&hdma_usart1_rx); rx_len = RX_BUF_SIZE - cur_cnt; // 数据位于 rx_buf[0] ~ rx_buf[rx_len - 1] } }

这里有个重要细节:__HAL_DMA_GET_COUNTER拿到的是 DMA 还剩下多少字节没搬,用RX_BUF_SIZE减去它,就是 DMA 已经搬进来的字节数。比如 DMA 计数是 246,说明已经搬了 10 个字节进来,rx_len就是 10。

在 Circular 模式下,数据是循环写入缓冲区的,如果帧很长跨越了缓冲区尾部并回绕到头部,处理逻辑就会变得复杂。最稳妥的做法是:接收缓冲区开得足够大,协议帧最大长度远小于缓冲区,这样即使回绕也很容易处理。另一种方案是使用 DMA 的半传输中断 + 完整传输中断打配合,读两个半缓冲区,实现双缓冲处理。这些偏进阶,我后面再详细写。

产生 IDLE 中断后,如果你希望新的一帧数据从头开始存放(而不是覆盖上一帧的残留位置),可以在HAL_UART_RxCpltCallback(DMA 到达缓冲区末尾时触发)中重新启动接收:

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { // 清零缓冲区或做必要处理 memset(rx_buf, 0, RX_BUF_SIZE); } }

不过要注意,HAL_UART_RxCpltCallback在 Circular 模式下本身就会被频繁调用(每到一次缓冲区边界就触发),不宜在里面做耗时操作,置个标志位或清空缓冲区就足够了。

5. 实战中高频出现的几个坑,以及我是怎么排查的

这篇文章如果只讲配置和调用,其实网上已经很多了。真正有价值的,是那些让你调试两三天才找到答案的坑。下面的内容全部来自我实际项目里的排查经历,每一条都有血有泪。

5.1 第二次DMA发送失败:TC标志被卡住

现象:第一次调用HAL_UART_Transmit_DMA发送正常,紧接着第二次调用同一函数,数据发不出去,或者发了几秒后又卡死。用调试器看,HAL_UART_Transmit_DMA返回值是HAL_BUSY

根因:HAL 库判断串口是否“忙”的标准之一是上次发送的 TC(发送完成)标志是否被处理。第一次发送完成后,如果没有清除 TC 标志或等待其置位,HAL 的状态机可能没有回到就绪状态。

解决:发送完成回调里清除 TC 标志:

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { __HAL_UART_CLEAR_FLAG(&huart1, UART_FLAG_TC); uart1_tx_busy = 0; } }

如果你的回调里没有清除,可以考虑在启动下一次发送前主动调用__HAL_UART_CLEAR_FLAG(&huart1, UART_FLAG_TC)。有些版本 HAL 库已经在内部处理了,但为了兼容性,我还是建议大家显式清一次。

5.2 DMA接收计数停摆,收满一次就“死”了

现象:使用 Normal 模式接收 DMA,第一次收满R X_BUF_SIZE个字节后触发回调,但之后再串口发数据,程序却毫无反应。__HAL_DMA_GET_COUNTER读取到的值始终等于RX_BUF_SIZE,不动了。

根因:Normal 模式下 DMA 计数器减到 0 后,通道自动关闭。HAL 库的HAL_UART_Receive_DMA是一次性启动,DMA 停止后自然不会再搬运任何数据。

解决:在HAL_UART_RxCpltCallback中重新启动接收:

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE); } }

这也是为什么接收不定长数据我强烈建议用 Circular 模式的原因——少一个重新启动的环节,代码更不容易出问题。

5.3 H7系列Cache导致的数据不一致

现象:在 STM32H750/H743 上使用 DMA 接收串口数据,接收缓冲区里的值看起来是“旧”的,明明物理上已经收到了新数据,CPU 读出来的却是老的缓存值。发送场景则相反,CPU 向缓冲区写入有效数据后启动 DMA,DMA 读出来的却是旧数据。

根因:H7 内核带有 D-Cache,而 DMA 访问内存时不经过 Cache。CPU 读到的数据优先命中 Cache 中的旧内容,DMA 写入的新数据没有同步到 Cache;CPU 写入的数据如果还在 Cache 里没有回写内存,DMA 也看不到。

解决:在读取 DMA 接收数据之前,先失效对应地址范围的 Cache:

SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, rx_len);

在启动 DMA 发送之前,将待发送缓冲区写回内存:

SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, len);

更彻底的做法是:在 CubeMX 中将 DMA 缓冲区所在的 MPU 区域配置为不可缓存(DeviceStrongly Ordered)。我实际项目中用SCB_InvalidateDCache_by_Addr就已经足够解决问题,优先推荐。

这里特别提醒:如果缓冲区定义在 DTCM RAM 中,那更麻烦,因为 DTCM 是内核私有内存,DMA 根本访问不到,CubeMX 配置时最好把DMA buffer定向到 SRAM(如 AXI SRAM 或 SRAM1/2/3)。如果你无处下手,把缓冲区数组用__attribute__((section(".ARM.__at_0x24000000")))指定到 AXI SRAM 是比较省事的方式。

5.4 波特率被时钟配置“偷走”,串口乱码的潜在原因

现象:DMA 收发都配好了,但串口数据偶发乱码,或者完全对不上。用逻辑分析仪抓波形发现实际波特率和预期差距很大。

根因:CubeMX 里填入的外部晶振频率和板子上实际的晶振型号不一致。例如开发板实际是 25MHz 晶振,但 CubeMX 的HSE Value还是默认的 8MHz,或者反过来。这样系统时钟和 APB 外设时钟都会按错误的输入频率计算,最终导致串口波特率和配置值偏差过大。

解决:在RCC > High Speed Clock (HSE)里填入板子实际晶振频率,再检查Clock Configuration图,确认APB1/APB2实际频率符合预期。很多串口乱码问题的根子不在代码,而在时钟树配置。我通常会在用 DDR 或 USB 之前,先确认所有外设时钟是否符合预期,省得后面排查成本翻倍。

5.5 常见问题速查表

现象可能原因排查方向
第二次 DMA 发送失败TC 标志未清除 / HAL 状态机忙碌检查是否为 HAL_BUSY,回调中清除 TC 标志
接收不到数据DMA 未启动 / Normal 模式计数到 0 停止确认HAL_UART_Receive_DMA已调用,dmarx挂在串口句柄上
收到的数据全部相同/覆盖内存地址自增未打开检查MemInc是否为DMA_MINC_ENABLE
偶发乱码晶振频率错误/APB 时钟不准核对 HSE、PLL 配置,用逻辑分析仪看实际波特率
DMA 数据全 0 / 旧数据D-Cache 缓存未同步使用SCB_CleanDCache_by_Addr/SCB_InvalidateDCache_by_Addr
一收满就卡死Normal 模式被停止重启动HAL_UART_Receive_DMA

6. DMA不止用于串口:ADC、PWM、SPI的顺带一提

DMA 在串口上的价值已经很明显,其他外设其实更依赖 DMA,比如 ADC 多通道采集、PWM 灯带驱动、SPI 高速数据搬运。这些场景的原理和串口大同小异,但在 CubeMX 里的配置细节各有不同。

6.1 ADC多通道采集与DMA的配合

多通道 ADC 如果不开 DMA,规则组转换完成后你就得自己在中断里读ADCx->DR,多个通道的数据还要手动拼接。开了 DMA 后,ADC 每转换完一个通道,DMA 会把数据自动搬运到内存数组对应位置,转换完成中断里直接拿数组就能用。

CubeMX 里配置 ADC 开启扫描模式(Scan Mode),DMA Settings 里添加 ADC 的 DMA 请求,Mode 选择 Circular。生成代码后注意定义缓冲区的类型要和ADC_Resolution匹配,比如 12 位分辨率时数据寄存器是 16 位的,缓冲区就该用uint16_t数组。

uint16_t adc_values[8]; HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_values, 8);

之后就可以不断从adc_values里读更新的数据。这里面有个细节:ADC 的 DMA 模式和串口不同,几乎总是用 Circular,因为 ADC 一直在转换,DMA 需要持续搬运。回调函数HAL_ADC_ConvCpltCallback在每次 DMA 搬完一轮时触发,适合做数据处理或置标志位。

6.2 PWM+DMA驱动灯带

驱动 WS2812 这类单总线灯带时,PWM + DMA 是一个非常经典的组合。原理是:

  • 用定时器输出一路 PWM,频率约 800kHz。
  • 通过 DMA 修改定时器的比较寄存器(CCR),实现不同占空比的数据位编码。
  • DMA 每传一个数据,CCR 就切换一次占空比,从而在 PWM 引脚上生成一串符合协议的数字信号。

CubeMX 里只需要配置定时器 PWM Generation,在 DMA Settings 里添加TIMx_CHx对应的 DMA 请求,然后准备一个显示缓冲数组,启动 DMA 传输即可。

这种玩法的优点是刷新灯带不占 CPU,缺点是缓冲区大小要和灯珠数量精确对齐,而且 DMA 一次性传完整个数组,灯带数量大了之后内存占用比较可观。我测试过 60 颗灯珠、30fps 刷新率的情况下,CPU 占用率几乎为零,效果比 GPIO 翻转方案舒服太多。

6.3 SPI、CAN选择DMA还是中断

SPI 通信和串口类似,数据量小的时候用HAL_SPI_Transmit_IT即可,数据量大、频率高,建议开 DMA。SPI 的 DMA 和串口有个差异:SPI 是全双工,往往需要 TX、RX 两个 DMA 同时工作,CubeMX 里直接添加两个 DMA 请求就行。

CAN 总线则不一样。CAN 协议本身就有 FIFO 硬件缓存,通常收一帧 8 字节数据用中断接收完全足够,不需要 DMA。只有在使用 CAN FD、数据量非常大的情况下才值得考虑 DMA。所以不要盲目什么外设都用 DMA,中断资源够用且数据量小的情况下,中断反而更简单可靠。

DMA 用到现在,我感觉它最大的价值不是“跑得快”,而是“把 CPU 从机械重复的数据搬家中解放出来”。当你开始把串口收发、ADC 采集这些重活交给 DMA 之后,再去想产品还有哪些功能可以加、哪些逻辑还能优化,思路会完全不一样。建议你把上面的 Circular 接收例程先跑通,再自己改成数据帧解析、命令分发,体会一下“串口数据自己就准备好了,我只管处理”的感觉,基本就出师了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询