ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

FreeRTOS下DMA接管UART:解决中断风暴的完整实战指南

FreeRTOS下DMA接管UART:解决中断风暴的完整实战指南 简介面向嵌入式开发者的实时操作系统串口收发工程资料包基于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 中断优先级为 5SysTick 为 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 读数据会先查 CacheCache 没有才访问内存。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 的组合在逻辑上已经足够可靠剩下的瓶颈全在这类硬件细节上希望这些排查路径能帮你少走几趟弯路。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表