☰
FreeRTOS下DMA接管UART:解决中断风暴的完整实战指南
2026/10/20 12:39:37 网站建设 项目流程

简介:面向嵌入式开发者的实时操作系统串口收发工程资料包,基于STM32F302CB平台演示如何通过直接存储器访问(DMA)优化通用异步收发器(UART)通信,实现高效且不丢数据的循环接收,覆盖任务调度、缓冲区切换、数据解析等核心环节。工程压缩包共包含六百五十九个文件,体积约十三点八一兆字节,以C源码、头文件、汇编文件及官方工程配置为主,辅以链接脚本、调试配置、图形化初始化配置等资源,目录结构清晰,便于移植与二次开发。目前已有三百九十六人学习下载。工程内提供了完整的HAL驱动代码、DMA中断处理示例以及FreeRTOS任务集成框架,同时附有构建脚本、调试描述文件和丰富的外设配置实例,可帮助读者深入理解DMA循环接收的底层机制,掌握中断服务函数与实时任务协同设计的思路,尤其适合需要高速连续串行通信的工业自动化和物联网设备开发者。此类设计在实际项目中能显著降低CPU占用,提升通信稳定性,是进阶嵌入式开发的实用参考。

1. FreeRTOS 下用 DMA 接管 UART:中断风暴的终结与三个隐藏代价

很多人第一次在 FreeRTOS 里碰串口,都是被「丢字节」和「高波特率必翻车」这两件事逼到查手册的。用查询方式收数据,CPU 被占死,任务调度成了摆设;用传统中断逐字节收,115200 波特率下每秒一万多次中断,系统响应被割成碎片,稍一忙就进 HardFault。我最早做某跨平台系统的串口通信模块时,就是被这种中断风暴折磨到换方案,才彻底转向 DMA。

这套「FreeRTOS + DMA + UART」的方案,核心思路是让串口硬件直接把数据搬到内存里,收满指定长度或总线空闲了再通知 CPU,从「数据搬运工」变成「数据验收员」。对整个系统而言,CPU 占用率能下降八成以上;对你来说,这套组合的代码、移植模板和踩坑清单,才是真正值钱的部分。注意,DMA 不是万能的,缓冲区管理、断帧判定、Cache 一致性这三个问题没处理好,移植回去一样翻车。

2. 为什么 FreeRTOS 的串口必须配 DMA:先看懂「中断风暴」数学模型

2.1 实时性断裂的本质:一次中断三次进出

在 FreeRTOS 里跑传统串口中断,每次收一个字节,中断服务函数里要做三件事:读数据寄存器、把数据写进缓冲区、判断是否要唤醒接收任务。每件事在 ARM Cortex-M 上都是十几次时钟周期的操作,加上中断压栈和出栈的固定开销,一次中断要消耗 60-100 个周期。

115200 波特率意味着每 86.8 微秒来一个字节,算下来一秒钟约 1.15 万次中断。这还没算 FreeRTOS 在中断里调用 portYIELD_FROM_ISR 时进行的上下文切换开销——每次切换要保存和恢复十几个寄存器。结果就是,系统里最高优先级接收任务的实时性看上去有保障,实际上 CPU 的时间片被切得稀碎,任何高于串口中断优先级的任务稍微跑久一点,串口缓冲区就溢出。

更隐蔽的问题在低优先级任务。假设有一个周期 10ms 的采集任务,被串口中断不断抢占,它的实际完成时间可能被拉到 50ms 开外。这时候你调 FreeRTOS 的任务优先级根本没意义,因为中断已经把调度的颗粒度打碎了。

2.2 DMA 改变了什么:搬运交给硬件,通知只在边界

DMA 接管串口接收后,数据流是这样:串口外设收到一个字节,直接由 DMA 控制器搬到指定的内存数组里,完全绕开 CPU。只有当 DMA 搬完设定长度的数据,或者外设触发空闲中断时,CPU 才被叫醒一次。

以 512 字节的 DMA 缓冲区为例,同样是 115200 波特率,CPU 每秒被叫醒的次数从 1.15 万次降到大约 20 次左右——也就是缓冲区满了才处理一次,加上空闲中断若干次。理论上,DMA 收满一个缓冲区和 FreeRTOS 任务处理这段数据是并行的,CPU 只在 DMA 搬运结束的那一刻参与工作。

这里有个关键选型点:DMA 的传输模式。我一般会用 Normal 模式加手动重装载,而不是直接用 Circular 模式,原因在后续章节展开,但先记住一个结论——Circular 模式省心但丢帧边界非常难判断,半传输中断那套机制在 FreeRTOS 任务调度下反而容易产生竞态。

2.3 任务架构怎么搭:生产者-消费者模型的正确姿势

硬件层面搞定 DMA 以后,软件层要把接收链路由中断驱动改成「DMA 写缓冲、任务读缓冲」的生产者-消费者模型。典型结构分三段。

第一段是 DMA 中断回调函数里,把本次搬运的数据长度记下来,然后通过二值信号量或 direct to task notification 唤醒接收任务;中断里不能做的解析、组帧、分发,全部放到任务上下文。第二段是接收任务本体,挂起在信号量上等待唤醒,被唤醒后从缓冲区里按帧解析。第三段是业务任务和接收任务之间用 FreeRTOS 的 StreamBuffer 或者消息队列解耦。

/* FreeRTOS DMA UART 接收任务骨架 */ static void uart_rx_task(void *arg) { uint8_t frame_buf[128]; uint16_t frame_len = 0; for (;;) { /* 等待 DMA 半满/全满或空闲中断信号量 */ xSemaphoreTake(rx_sem, portMAX_DELAY); /* 从 DMA 缓冲区拷贝一帧数据到本地栈缓冲 */ frame_len = dma_uart_pack_frame(uart_dma_buf, &dma_buf_wr_idx, frame_buf, 128); if (frame_len > 0) { /* 这里做协议解析,比如帧头校验、CRC 校验 */ parse_and_dispatch(frame_buf, frame_len); } } }

这段代码的逻辑核心在 dma_uart_pack_frame:它负责计算 DMA 写指针走到哪了,把新到的数据按帧切出来。注意,解析动作放在任务里,不在中断回调里做,是为了避免在 ISR 里执行过长逻辑导致新的 DMA 中断被阻塞——这是判断这套方案是否合格的分水岭。

信号量参数上,xSemaphoreTake 的等待时间设为 portMAX_DELAY,让接收任务在数据没来的时候彻底睡眠,不占 CPU。实际项目中建议把信号量换成「任务通知(Task Notification)」,因为通知的延迟比信号量低大约 20%-30%,而且不消耗独立的信号量对象。

3. 拆分 DMA 缓冲区与帧边界判定:半传输中断和空闲中断的分工

3.1 为什么单靠「传输完成」中断会卡死自己

如果用 DMA 的 Normal 模式,搬运完设定的字节数才触发一次传输完成中断,那 CPU 必须赶在 DMA 写完缓冲区之前把数据取走,否则 DMA 继续搬运新数据就会覆盖还没处理的内容。问题是 FreeRTOS 任务的调度延迟不可控——如果接收任务优先级不够高,或者恰好有更高优先级任务在跑,DMA 缓冲区被覆盖几乎是必然的。

把缓冲区做大能缓解,但治标不治本,内存开销先吃不消。于是经典做法是引入「半传输中断」:DMA 每搬满半个缓冲区就触发一次中断,CPU 处理前半段数据;等搬满整个缓冲区,再触发传输完成中断,CPU 处理后半段。这样在一个 DMA 周期内,CPU 始终在处理「另一半」的数据,缓冲区永远有安全区。

3.2 环形缓冲区的写指针和读指针怎么跟踪

实际落地时,我们不用裸数组直接当缓冲区,而是包一层环形缓冲区。环形缓冲区的好处是读写解耦,DMA 只管写,任务只管读,互不干扰。麻烦的是要维护两个指针:写指针由 DMA 硬件在搬运时更新,读指针由任务在消费数据时更新。

/* DMA + FreeRTOS 环形缓冲区结构体 */ typedef struct { uint8_t pool[DMA_BUF_SIZE]; /* 实际存储区 */ volatile uint16_t wr_idx; /* DMA 写到的位置 */ volatile uint16_t rd_idx; /* 任务读到的位置 */ uint16_t dma_half_len; /* 半传输阈值 */ uint16_t dma_full_len; /* 整包阈值 */ } dma_ring_buf_t; /* DMA 传输完成中断回调 */ void uart_dma_rx_complete_cb(DMA_HandleTypeDef *hdma) { dma_ring_buf_t *rb = (dma_ring_buf_t *)hdma->RxBuf; BaseType_t xHigherPriorityTaskWoken = pdFALSE; /* 若是半传输中断,写指针移动到 half_len */ if (hdma->Instance->ISR & DMA_ISR_HTIF) { rb->wr_idx = rb->dma_half_len; } /* 若是全传输中断,写指针回到尾部或者循环 */ else if (hdma->Instance->ISR & DMA_ISR_TCIF) { rb->wr_idx = rb->dma_full_len; } /* 通知接收任务处理 */ vTaskNotifyGiveFromISR(rx_task_handle, &xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); }

这段代码里最核心的是 wr_idx 的赋值逻辑:半传输中断到达时,DMA 写完了前半段,wr_idx 指向半阈值;全传输中断到达时,DMA 写完整个缓冲区,wr_idx 指向满阈值。任务侧每次拿到新的 wr_idx,就对比自己上次处理到的 rd_idx,把两者之间的数据全部取走。

有一个容易被忽略的参数:DMA 源地址和外设地址的方向配置。串口 DMA 接收时,外设地址是 UART 数据寄存器,内存地址是 pool 数组,方向是外设到内存,这些必须在初始化时设置正确,否则数据根本不会进到 pool 里。排查时第一件事就是打印外设地址和内存地址,八成问题出在这。

3.3 空闲中断负责「断帧」,别让粘包毁了协议

DMA 半满和全满中断解决的是「大流量持续灌入」的场景,但实际串口数据几乎都是不定长的。一帧数据可能 10 个字节,也可能 200 个字节。如果只靠 DMA 长度阈值,CPU 必须在收到一帧后等缓冲区填满才知道有数据,延迟无法接受——尤其像 Modbus 这种短帧协议,一帧 8 个字节,512 字节的缓冲区要 64 帧才填满,实时性全没了。

解决办法是启用 UART 的空闲中断。总线空闲超过一个字节时间,说明一帧发完了,立刻触发空闲中断。中断里我们不做数据拷贝,只是更新写指针并通知任务。

/* UART 空闲中断处理:表示一帧结束 */ void UART_IRQHandler(void) { BaseType_t xHigherPriorityTaskWoken = pdFALSE; if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLE_FLAG(&huart1); /* DMA 还在搬运的话,先关闭 DMA 通道获取实际写指针 */ size_t dma_remaining = __HAL_DMA_GET_COUNTER(&hdma_usart1_rx); rb.wr_idx = rb.dma_full_len - dma_remaining; /* 用任务通知唤醒接收任务 */ vTaskNotifyGiveFromISR(rx_task_handle, &xHigherPriorityTaskWoken); } portYIELD_FROM_ISR(xHigherPriorityTaskWoken); }

空闲中断和 DMA 配合的细节在于:DMA 计数器(CNDTR)时刻在变,表示还剩多少字节要搬。用「DMA 总长度减去剩余计数」得到当前已写入的位置,这样即使 DMA 还没到半满或全满阈值,我们也能精确知道数据停在哪。

有个参数值得专门说——空闲中断的判定时间由串口的波特率决定,16 倍过采样下大约是一个字节的传输时间。波特率越高,空闲判定越快,所以 CPU 的响应要跟上,建议空闲中断不要分配太低的优先级。

4. 从零配置 FreeRTOS DMA UART 工程:初始化顺序与参数对照

4.1 初始化顺序:GPIO、DMA、UART、RTOS 的先后关系

初次上手最容易踩的就是初始化顺序。我见过有人先初始化 UART 再初始化 DMA,结果串口一收到数据,DMA 还没就位,数据直接丢在硬件 FIFO 里。

正确顺序是:先配置 GPIO 的复用功能,确保引脚输出到串口的 TX/RX 通道;再配置 DMA 控制器的数据流,把它绑定到 UART 的接收请求;然后初始化 UART 外设本身,使能 DMA 接收请求和空闲中断;最后才创建 FreeRTOS 的信号量和任务。顺序反了,轻则收不到数据,重则 DMA 配置失败直接 HardFault。

/* FreeRTOS DMA UART 初始化顺序示例 */ void uart_dma_init(void) { /* 第一步:GPIO 复用配置 */ __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Alternate = GPIO_AF7_USART1; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); /* 第二步:DMA 流配置,必须设成循环模式配合环形缓冲 */ __HAL_RCC_DMA1_CLK_ENABLE(); hdma_rx.Instance = DMA1_Stream3; hdma_rx.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma_rx.Init.PeriphInc = DMA_PINC_DISABLE; hdma_rx.Init.MemInc = DMA_MINC_ENABLE; hdma_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; hdma_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; hdma_rx.Init.Mode = DMA_CIRCULAR; /* 循环模式保证连续接收 */ hdma_rx.Init.Priority = DMA_PRIORITY_HIGH; HAL_DMA_Init(&hdma_rx); /* 第三步:UART 使能 DMA 接收 */ 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; HAL_UART_Init(&huart1); /* 第四步:关联 DMA 并启动接收 */ __HAL_LINKDMA(&huart1, hdmarx, hdma_rx); HAL_UART_Receive_DMA(&huart1, rb.pool, DMA_BUF_SIZE); /* 第五步:创建信号量和接收任务(FreeRTOS 启动后) */ rx_sem = xSemaphoreCreateBinary(); xTaskCreate(uart_rx_task, "uart_rx", 256, NULL, 5, &rx_task_handle); }

初始化参数里,DMA_CIRCULAR 循环模式是这里最大的决策点。前面说 Normal 模式手动重装载适合某些场景,但环形缓冲区的场景下 Circular 是首选——它让 DMA 在搬满整个缓冲区后自动回卷到起点继续搬运,不用 CPU 干预,配合半满/全满中断正好实现「双缓冲」效果。

另一个参数是 MemDataAlignment。如果缓冲区声明为 uint8_t 数组,就用 BYTE;如果改成 uint16_t 数组加速传输,就要同步改这一项,否则 DMA 搬完数据在内存里是错位的,解析时全是乱码。

4.2 关键宏定义和缓冲区大小怎么定

缓冲区大小不是拍脑袋定的,它取决于你的协议帧最大长度、波特率、任务处理速度三者之间的平衡。经验值是:缓冲区至少是最大协议帧的两倍,同时保证在最高波特率下,任务取出数据的速度大于 DMA 灌入的速度。

/* 工程配置示例:典型的参数宏定义 */ #define UART_DMA_BUF_SIZE 512 /* 环形缓冲区总大小 */ #define UART_RX_FRAME_MAX 128 /* 协议单帧最大长度 */ #define UART_BAUDRATE 115200 /* 波特率 */ #define UART_RX_TASK_STACK 512 /* 接收任务栈大小(字) */ #define UART_RX_TASK_PRIO 5 /* 优先级高于业务任务 */

参数说明:512 字节的缓冲区在 115200 波特率下,DMA 灌满需要约 44ms;只要接收任务在 44ms 内处理完 256 字节,系统就稳。任务栈 512 个字(2048 字节)给协议解析预留了足够空间,如果解析里有大数组或格式化打印,建议加一倍。

我一般会把任务优先级设为 5,比业务任务高一级。太低的话 DMA 缓冲容易溢出;太高的话会抢占关键业务逻辑的 CPU。这个值不是死的,调试时先用高优先级跑通,再逐步降。

4.3 数据解析任务里如何与协议层解耦

数据进到任务上下文后,最怕的是在接收任务里同步调协议解析、写 Flash、发网络。任何一个慢操作都会阻塞下一次空闲中断的处理,造成数据堆积。

正确做法是把解析和分发拆成两个阶段。接收任务只做快速判断——检查帧头、帧尾、CRC,然后把完整帧拷贝到 StreamBuffer 里;真正的业务处理任务从 StreamBuffer 读数据,做业务逻辑。StreamBuffer 的好处是天然带阻塞读,业务任务没数据时自动挂起,不占 CPU。

/* 接收任务:拷贝到 StreamBuffer 后快速返回 */ void uart_rx_task(void *arg) { uint8_t tmp[DMA_BUF_SIZE]; uint16_t len = 0; for (;;) { xTaskNotifyWait(0, 0, NULL, portMAX_DELAY); while (rb.rd_idx != rb.wr_idx) { len = get_valid_frame(&rb, tmp, sizeof(tmp)); if (len > 0) { /* 写入流缓冲,业务任务会取走 */ xStreamBufferSend(rx_stream, tmp, len, 0); } } } }

StreamBuffer 的发送超时传 0,表示缓冲区满就立刻返回,不等待。实际使用中要注意 StreamBuffer 的尺寸必须大于可能写入的最大帧,否则大帧会写不进去。触发阈值设 1,意思是任何数据到达都唤醒对端任务,不要设成半包再唤醒,那样会额外引入一帧的延迟。

5. FreeRTOS DMA UART 常见问题与排查:五个必踩的坑

5.1 数据错位或乱码:DMA 搬运方向反了

现象:串口收到数据是乱码,每个字节都不对,偶尔能收到几个正确的字符,但整体不可用。

原因:最常见的两个——GPIO 复用配置错误,引脚实际连到了别的串口;DMA 方向初始化成了 MEMORY_TO_PERIPH,数据从内存发到外设而不是从外设收进内存。第二种更隐蔽,因为系统不报错,DMA 也正常搬,只是方向反了,寄存器里根本没数据。

解决:第一步检查 GPIO 复用功能编号是否正确;第二步打印 DMA 控制器的 CR 寄存器里的 DIR 位,确认是 00(外设到内存)。我排查时会先用轮询方式接收一个字节做比对,能收到说明 UART 本身没问题,收不到就是 DMA 配置的问题,直接看 DIR 位。

5.2 任务收不到数据:信号量与 DMA 中断优先级打架

现象:DMA 中断被打断,数据正常搬运了,但接收任务永远不被唤醒;偶尔按一下复位键,积压的数据全部吐出来。

原因:DMA 中断的优先级被配置成了比 FreeRTOS 内核的 SysTick 或 PendSV 还低。Cortex-M 的中断优先级数值越小优先级越高,如果 DMA 中断优先级数值大于等于 PendSV 的阈值,FreeRTOS 上下文切换机制根本不会触发 ISR 里的通知操作——具体表现就是「中断进了,任务不跑」。

解决:把 DMA 中断优先级调到高于 PendSV。在 CubeMX 里将 DMA 中断优先级设为 Highest 或 High 级别,同时确认 FreeRTOS 的 configMAX_SYSCALL_INTERRUPT_PRIORITY 允许该优先级调用 fromISR 结尾的 API。我通常设 DMA 中断优先级为 5,SysTick 为 15,保证 DMA 响应最快。

5.3 DMA 缓冲区溢出:任务处理慢导致数据被覆盖

现象:串口长时间跑高流量数据后,一帧数据中间插入了几字节的旧数据,或者帧的头尾错乱。

原因:缓冲区是循环的,DMA 写到头自动回卷。如果任务处理速度跟不上 DMA 写入速度,wr_idx 追上 rd_idx,就会把还没消费的数据覆盖掉。FreeRTOS 任务调度的不确定性加剧了这个问题——任何一次高优先级任务抢占,都会让接收任务多等几十毫秒。

解决:最直接的办法是加大缓冲区,给任务更多处理时间;更根本的办法是用双缓冲区交替使用——A 缓冲被 DMA 写满后,自动切到 B 缓冲,CPU 只处理 A;反过来同理。双缓冲把「DMA 写入」和「CPU 读取」的冲突概率降到最低。

5.4 低功耗模式下 DMA 收不到数据:外设时钟被关了

现象:设备进入睡眠模式后,串口外部发数据无法唤醒,或者唤醒后 DMA 数据全丢。

原因:低功耗模式下,DMA 控制器的时钟被关闭,串口数据到了但没有搬运通道,数据在 UART 的 FIFO 里直接溢出。即使 UART 能用外部中断唤醒芯片,DMA 来不及启动,数据已经丢了。

解决:在每个需要串口唤醒的低功耗入口前,把 DMA 重新初始化,或者配置 DMA 控制器为低功耗模式的唤醒源。我一般用「睡眠模式 + 保留 DMA 时钟」的折中方案:睡眠能省的功耗有限,但数据完整性有保障。深度睡眠模式下串口唤醒基本不可用,除非外接硬件缓冲。

5.5 FreeRTOS 内存不足:任务栈分配导致启动失败

现象:系统启动后任务不运行,调试器看任务状态是 Ready 但永远不被调度;或者 vTaskStartScheduler 直接断言失败。

原因:FreeRTOS 的堆大小配置不足。接收任务的栈、StreamBuffer 的存储区、其它业务任务的栈,总和超过了 configTOTAL_HEAP_SIZE。DMA 缓冲区没占 FreeRTOS 堆(它是静态数组),但 StreamBuffer 会动态分配。

解决:把接收任务栈从 512 降到 256,或者把 configTOTAL_HEAP_SIZE 从 4KB 提到 16KB。我用的是 heap_4 方案,碎片少一些,但堆仍然要留至少 20% 的余量。建议在启动时调用 xPortGetFreeHeapSize 打印一次堆剩余量,心里有数。

6. 进阶:DMA 与 Cache 一致性问题——Cortex-M7 上的隐形杀手

6.1 为什么 CM7 核心会出现「DMA 写了数据,CPU 读到旧值」

Cortex-M7 或更高端核心带 D-Cache 时,CPU 读数据会先查 Cache,Cache 没有才访问内存。DMA 是直接访问内存的外设,它写完的数据在内存里,但 CPU 的 Cache 里可能还留着旧值,于是任务读到的还是老数据——这就是 DMA 和 CPU 之间的 Cache 一致性问题。

症状极其隐蔽:数据明明是新的,调试器单步看数组确实是新值,但跑起来任务就是拿不到最新内容;有时候要等 Cache 被其他访问挤掉,才能读到一次新数据,表现出「大部分时间正常,偶尔丢一帧」。

解决手段有两种:一是用 MPU 把 DMA 缓冲区所在的 SRAM 区域配置成非缓存属性,让 CPU 每次都直接访问内存;二是在 DMA 搬运完成中断里调用 SCB_CleanInvalidateDCache,主动刷新 Cache。

/* 方案一:MPU 配置 DMA 缓冲区区域为非缓存 */ void mpu_dma_buffer_config(void) { MPU_Region_InitTypeDef MPU_InitStruct; MPU_InitStruct.Enable = MPU_REGION_ENABLE; MPU_InitStruct.BaseAddress = (uint32_t)uart_dma_buf; MPU_InitStruct.Size = MPU_REGION_SIZE_4KB; MPU_InitStruct.SubRegionDisable = 0; MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL1; MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS; MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE; MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; /* 关键 */ MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE; HAL_MPU_ConfigRegion(&MPU_InitStruct); __HAL_MPU_ENABLE(); }

这段配置里 IsCacheable 设为 NOT_CACHEABLE,是核心中的核心。这样 CPU 访问这块缓冲区时不经过 Cache,直接读内存,DMA 写进去的数据立刻对任务可见。注意,不是所有内存区域都适合关闭 Cache,像协议栈的收发缓冲区也建议这样做,全局变量、任务栈别碰,关掉 Cache 会让这些区域的访问性能大打折扣。

方案二适合不想动 MPU 的场景:

/* 方案二:DMA 中断里主动刷 Cache */ void uart_dma_rx_complete_cb(DMA_HandleTypeDef *hdma) { /* 先清掉 D-Cache 中的旧数据,再让 CPU 读内存 */ SCB_InvalidateDCache_by_Addr((uint32_t *)rb.pool, DMA_BUF_SIZE); /* 通知接收任务处理 */ vTaskNotifyGiveFromISR(rx_task_handle, NULL); }

注意这里用的是 Invalidate(失效)而不是 Clean。因为 DMA 是从外设往内存写,内存里的数据是最新的,CPU 的 Cache 是旧的,所以要让 Cache 失效,迫使 CPU 重新从内存读。反过来,如果是 CPU 往内存写、DMA 从内存读的场景(比如 DMA 发送),就要用 Clean 把 Cache 里的数据先写回内存,再启动 DMA。

6.2 实测脏数据场景与判定方法

我最初在某个图像处理 Demo 里遇到这个坑,症状是任务偶尔解析出完全不符合协议的数据,但帧头校验明明通过了——后来发现是因为 Cache 里残留了上一帧的数据,帧头字段恰好没变,校验码又是对旧数据算的,整个链路的错误就这样被隐藏了。

判定方法很简单:在 DMA 完成中断里主动 Invalidate 一次 Cache,看问题是否消失——消失大概率就是 Cache 一致性问题。也可以用调试器对比 DMA 计数器和 buffer 里的实际数据差异:DMA 明明搬了 256 字节,任务读的时候前面 64 字节还是旧值,不用再猜,直接上 MPU 方案。

从那以后我每次在带 Cache 的核心上移植 DMA 外设,第一件事就是检查映射缓冲区地址的 MPU 配置,不把这个前提确定清楚,后面任何调试都是白费时间。这套 FreeRTOS DMA UART 的组合在逻辑上已经足够可靠,剩下的瓶颈全在这类硬件细节上,希望这些排查路径能帮你少走几趟弯路。

本文还有配套的精品资源,点击获取

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

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

立即咨询