简介:面向嵌入式开发者与学习STM32串口通信的学生,这份STM32CUBE双串口DMA互透传工程聚焦UART1与UART2之间基于DMA的高效数据转发,可用于连续、不定长串口数据的透明传输实验与项目原型验证。压缩包约3.62MB,共143个文件,包含大量.c与.h源码、Keil工程文件及.o、.crf、.d等编译中间文件,并带有.ioc配置、.uvprojx工程和HAL库串口、DMA、定时器相关驱动,便于直接打开对照CubeMX初始化、DMA通道设置、收发启动与中断回调。已有1872人学习下载。工程呈现了双串口DMA互透传的完整组织方式,读者可据此理解HAL_UART_Receive_DMA与发送接口的配合、串口错误回调处理以及数据块接收后的转发思路,适合用来拆解外设配置、排查DMA收发问题并快速搭建类似串口透传应用。
1. 两块板子对插才跑通:双串口 DMA 互透传装不对时的典型现场
有个开发者拿着一块最小系统板找我,说双串口 DMA 互透传的工程"能编译能下载,就是一个字节都过不去"。他把 USART1 和 USART2 各自初始化得好好的,DMA 也加了,仿真器里看寄存器全是对勾,可 USB 转串口发出去的数据像进了黑匣子。后来发现问题出在发送端:HAL_UART_Transmit_DMA在上一帧还没发完时返回HAL_BUSY,他直接忽略了返回值,于是第二帧开始全部丢。这种问题在 CubeMX 里点几下就能生成框架,但真正让两路串口稳定互透传,卡人的从来不是"能不能初始化",而是 DMA 通道映射、空闲中断重挂、发送队列和错误恢复这几件事。下面按我实际装这套环境的顺序写,先选版本、再配 CubeMX、再落代码,最后给出验证和踩坑记录。适合刚上手 HAL 库、准备做协议网关或串口桥的开发者照着走一遍。
2. 环境准备与版本组合:CubeMX、固件包和工具链先对齐
2.1 硬件与软件的最小清单
这套环境不需要多贵的板子,但对"线"和"电平"很挑。我把常用配置列成一张表,照着备齐基本不会卡在硬件上。
| 类别 | 推荐配置 | 说明 |
|---|---|---|
| 主控板 | STM32F4 系列最小系统板(如 F407)或 F103 系列 | F4 的 DMA 通道多、支持双缓冲,调试透传更宽裕;F103 资源紧但够用 |
| 调试器 | ST-Link V2 或 DAP-Link | 必须支持 SWD,别用只有 JTAG 的老仿真器 |
| 串口模块 | 两路 USB 转 TTL,3.3V 电平 | 5V 电平直连会打坏引脚,先量一下模块的 VCC 输出 |
| 连线 | TX-RX 交叉、RX-TX 交叉、GND 共地 | 两路串口各接一个模块,GND 必须和板子共地 |
| 图形化工具 | STM32CubeMX 6.x | 6.x 自带 JRE,不用单独装 Java |
| 固件包 | STM32Cube FW_F4 / FW_F1 对应系列包 | 版本决定有没有HAL_UARTEx_ReceiveToIdle_DMA这个接口 |
| IDE | Keil MDK 或 STM32CubeIDE | 两者生成的代码结构一致,函数名不变 |
表格里最容易被忽视的是"固件包版本"。我见过有人照着网上教程写HAL_UARTEx_ReceiveToIdle_DMA,编译报未定义,翻回去看自己装的是很老的固件包。判断方法很简单:用编辑器打开Drivers/STM32F4xx_HAL_Driver/Inc/stm32f4xx_hal_uart.h,搜索ReceiveToIdle,搜不到就是版本偏旧,要么升级固件包,要么退回自己写 IDLE 中断的老路子。我一般会先确认这一点,再决定后面代码怎么写,省得写到一半推倒重来。
2.2 安装 CubeMX 并先跑通一个空工程
CubeMX 的安装本身没什么坑,真正坑人的是路径。安装目录和工程目录都别带中文和空格,这是血泪经验——某些版本的代码生成器在拼接路径时对空格处理不好,生成到一半报错,工程目录里只剩半个文件树。装完先别急着配串口,按下面的顺序做一次最小验证。
# 1. 检查 CubeMX 能否正常启动(部分精简系统缺图形库会闪退) # 启动后 Help -> About 看版本号,确认是 6.x # 2. 新建工程,只做三件事: # System Core -> SYS -> Debug 选 Serial Wire # System Core -> RCC -> HSE 选 Crystal/Ceramic Resonator # 直接 Generate Code,工具链选 MDK-ARM # 3. 打开生成的工程,编译、下载、复位 # 如果没有报错且板子能正常停在 main 的 while(1),工具链就是通的这三步的意义在于把"下载通道"和"代码生成链路"先固定下来。很多透传调不通的案例,根子在 SYS 的 Debug 没选 Serial Wire:程序第一次下载进去后把 SWD 引脚复用成了普通 GPIO,第二次就连不上了,只能靠复位时机抢连接或者用 BOOT0 救。先选好调试口,等于给自己留了后悔药。
提示:生成代码前在 Project Manager 里勾上 "Generate peripheral initialization as a pair of .c/.h files",这样 USART 和 DMA 的初始化会单独放在
usart.c里,改配置时不会和main.c里的用户代码混在一起。
2.3 芯片型号与 DMA 请求映射的对应关系
同一个 USART 在不同系列上挂的 DMA 通道完全不一样,这一点必须在动手前查清楚,否则 CubeMX 里点出来的通道是错的,代码跑起来外设根本不触发传输。以最常用的两路串口为例:
| 外设 | STM32F407 上的 DMA 流/通道 | STM32F103 上的 DMA 通道 |
|---|---|---|
| USART1_RX | DMA2 Stream2 Channel4 | DMA1 Channel5 |
| USART1_TX | DMA2 Stream7 Channel4 | DMA1 Channel4 |
| USART2_RX | DMA2 Stream5 Channel4 | DMA1 Channel6 |
| USART2_TX | DMA2 Stream6 Channel4 | DMA1 Channel7 |
CubeMX 默认会按芯片型号自动选,但如果同一路 DMA 流被别的外设占了(比如你同时还开了 ADC 或 SPI),它会弹冲突提示,让你手动换。遇到冲突优先换串口的 DMA 流,别去动已经调好的其他外设——串口的 DMA 组合一般有备选,其他外设未必有。
3. CubeMX 图形化配置:从时钟树到两路 DMA 请求
3.1 新建工程与调试口设置
打开 CubeMX,选择 "Access to MCU Selector",输入具体型号(比如 STM32F407VGT6),双击进入引脚图。第一件事不是点串口,而是进 System Core 把两件事做掉:
- SYS → Debug 选Serial Wire。这一步决定后面能不能反复下载,务必在生成代码前完成。
- RCC → HSE 选Crystal/Ceramic Resonator,LSE 按板子实际情况选。
做完这两步,右侧引脚图上 PA13/PA14 会变成 SWDIO/SWCLK 的绿色标记,说明调试口被保留了。很多人习惯先配外设最后配时钟,我不推荐这个顺序——时钟树一旦配错,后面所有外设的波特率都是错的,排查时会被误导到串口配置上去。
3.2 时钟树:HSE 频率必须和板上晶振一致
进 Clock Configuration 页,先看左上角 "Input frequency" 是不是你板子上实际焊的晶振值。F4 最小系统板常见 8MHz,也有 25MHz 的;F103 常见 8MHz。这个值填错,整条时钟链都会偏,串口表现出来的现象是"波特率明明设的 115200,收出来全是乱码"。
我一般按这个顺序配:
- 确认 HSE 数值与实物一致;
- PLL Source 选 HSE;
- 直接在 "HCLK" 框里输入目标主频(F407 填 168,F103 填 72),回车让工具自动算分频;
- 看最终 HCLK 是不是刚好等于目标值,出现红色就是超频或分频除不尽。
配完时钟,右侧会显示各总线的实际频率,比如 APB1 是 42MHz、APB2 是 84MHz。这两路串口如果挂在不同的 APB 上,波特率寄存器算出来的分频值不一样,但 HAL 库已经帮你处理了,只要你填的波特率一致,实际输出就是一致的。
3.3 两路 USART 的参数配置
在 Connectivity 里分别点开 USART1 和 USART2,Mode 都选Asynchronous。参数页只需要改几项:
- Baud Rate:先统一填 115200,验证通了再往上调;
- Word Length:8 Bits;
- Parity:None;
- Stop Bits:1;
- Oversampling:16(默认,高速率时可试 8,但对时钟精度要求更高)。
两个串口参数必须完全一致,否则一端发出去的数据在另一端被解析成乱码。这一点在"互透传"场景里尤其致命——数据从 A 口进、B 口出,如果两口的字长或校验位不一样,转发出去的数据对端设备根本不认。
引脚分配上,USART1 默认落在 PA9/PA10,USART2 默认落在 PA2/PA3。如果板子上这几个脚被占用了(比如接了 LED 或按键),可以在引脚图上点住引脚拖到备选脚位,CubeMX 会同步更新外设配置。
3.4 DMA 请求的添加与循环模式选择
这是整套配置的核心。在 USART1 的配置页里切到DMA Settings标签,点 "Add",添加两条请求:
| 请求 | Direction | Mode | Data Width (P/M) | Priority |
|---|---|---|---|---|
| USART1_RX | Peripheral To Memory | Normal(入门)/ Circular(高速) | Byte / Byte | Medium |
| USART1_TX | Memory To Peripheral | Normal | Byte / Byte | Medium |
USART2 同样加两条。四张表填完后,"Increment Address" 这一列的规则是:内存侧勾上(地址递增),外设侧不勾(外设数据寄存器地址固定)。如果两边都勾或者都不勾,DMA 搬运的地址就错了,表现出来是"只收到第一个字节,后面全是 0"或者数据错位。
接收方向的 Mode 选择是分水岭:
- Normal:DMA 搬满指定长度就停,需要软件重新启动。配合空闲中断使用时,每收完一帧都要重挂一次,中间有几个字节的空窗。
- Circular:DMA 搬满一圈自动回到起点继续,永不停止。适合高速连续流,但软件必须靠缓冲区指针差值和半传输/传输完成事件来划分帧边界。
我一般入门先用 Normal 模式把链路跑通,确认数据能过去,再根据吞吐需求决定要不要切 Circular。上来就用 Circular 加环形缓冲,出问题时很难判断是模式选错还是索引算错。
3.5 NVIC 优先级与代码生成设置
切到 NVIC Settings 标签,把四路 DMA 中断和两路 USART 全局中断都勾上 Enable。优先级这么分:
- USART1/USART2 全局中断:抢占优先级 1;
- 四路 DMA 流中断:抢占优先级 2。
这样空闲中断(IDLE)能第一时间被响应,DMA 传输完成的处理排在其后。注意 HAL 默认的优先级分组是 4 位抢占优先级、0 位子优先级,也就是说你填的数字越小优先级越高,别填反了。
最后在 Project Manager 里设置工程名、路径、工具链,把 "Generate peripheral initialization as a pair of .c/.h files" 勾上,点 GENERATE CODE。生成完后先编译一次,确认没有报错再往下写业务代码。
4. 生成代码后的透传实现:空闲中断接收加发送队列
4.1 接收端:ReceiveToIdle_DMA的启动与重挂
生成代码里MX_DMA_Init()一定在MX_USART1_UART_Init()之前被调用,这个顺序是 CubeMX 自动排的,别手动调换——DMA 时钟没开就去初始化串口的 DMA 句柄,HAL_DMA_Init会直接返回错误。
接收部分我在main.c的用户代码区加一个启动函数和一个回调。核心思路是:让 DMA 一直挂在接收上,靠空闲中断判定"这一帧结束了",然后在回调里把长度和数据交给发送队列。
/* main.c 用户代码区 */ #define RX_BUF_SIZE 512 static uint8_t u1_rx_buf[RX_BUF_SIZE]; static uint8_t u2_rx_buf[RX_BUF_SIZE]; /* 启动两路接收,DMA 模式为 Normal,每帧结束后需要重挂 */ void BSP_Uart_Start(void) { HAL_UARTEx_ReceiveToIdle_DMA(&huart1, u1_rx_buf, RX_BUF_SIZE); HAL_UARTEx_ReceiveToIdle_DMA(&huart2, u2_rx_buf, RX_BUF_SIZE); /* 关掉半传输中断:Normal 模式下用不到,开着反而会多一次回调 */ __HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT); __HAL_DMA_DISABLE_IT(&hdma_usart2_rx, DMA_IT_HT); }HAL_UARTEx_ReceiveToIdle_DMA这个接口做了两件事:启动 DMA 接收,同时打开串口的 IDLE 中断。这样只要总线上出现一个字节时间以上的静默,就会触发回调,参数Size就是这一帧的实际字节数——不需要你自己去算缓冲区长度 - __HAL_DMA_GET_COUNTER()。
回调里的处理要小心,Size是这一帧的长度,缓冲区指针是固定的。如果直接把u1_rx_buf的地址塞给发送函数,下一次接收会覆盖它,所以必须拷贝一份或者排队。
/* 接收事件回调:IDLE 或 DMA 传输完成时进入 */ void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart->Instance == USART1) { tx_ring_push(&ring_to2, &huart2, u1_rx_buf, Size); /* 1 收 -> 2 发 */ HAL_UARTEx_ReceiveToIdle_DMA(&huart1, u1_rx_buf, RX_BUF_SIZE); /* 重挂 */ } else if (huart->Instance == USART2) { tx_ring_push(&ring_to1, &huart1, u2_rx_buf, Size); /* 2 收 -> 1 发 */ HAL_UARTEx_ReceiveToIdle_DMA(&huart2, u2_rx_buf, RX_BUF_SIZE); } }重挂这一步是 Normal 模式下最容易漏的。漏了的现象是"上电后能透传一帧,之后彻底没反应",因为 DMA 已经搬完一次进入停止状态,串口那边还在等数据,但搬运工已经下班了。
4.2 发送端:为什么必须自己写一个队列
直接调HAL_UART_Transmit_DMA的问题是:这个函数在gState不是 READY 时返回HAL_BUSY,而 DMA 发送一帧需要的时间远大于 CPU 处理一帧的时间。115200 波特率下发 100 字节大约要 8.7ms,如果这期间对向又来了一帧,第二次调用必然失败。忽略返回值就是丢数据,检查返回值就得在回调里阻塞等待,两头都不讨好。
我一般用一个小的发送环形队列,队列里每格自带存储,push 时把数据拷进去,发送完成中断里推进指针。
/* 发送队列:每格自带缓冲,避免接收缓冲区被覆写 */ #define TX_QUEUE_LEN 6 #define TX_SLOT_SIZE 256 typedef struct { uint8_t data[TX_SLOT_SIZE]; uint16_t len; } tx_slot_t; typedef struct { tx_slot_t slot[TX_QUEUE_LEN]; volatile uint8_t head; /* 写入位置 */ volatile uint8_t tail; /* 当前发送位置 */ volatile uint8_t sending; } tx_ring_t; static tx_ring_t ring_to1, ring_to2; /* 分别对应发往 USART1 / USART2 */ /* 启动队列里下一帧的 DMA 发送 */ static void tx_ring_kick(tx_ring_t *r, UART_HandleTypeDef *huart) { if (r->tail == r->head) { /* 队列空,标记空闲 */ r->sending = 0; return; } r->sending = 1; if (HAL_UART_Transmit_DMA(huart, r->slot[r->tail].data, r->slot[r->tail].len) != HAL_OK) { r->sending = 0; /* 启动失败,等下一次 push 再试 */ } } /* 入队:队列满时丢掉最旧一帧,保证新数据能进 */ static void tx_ring_push(tx_ring_t *r, UART_HandleTypeDef *huart, const uint8_t *data, uint16_t len) { uint8_t next = (r->head + 1) % TX_QUEUE_LEN; if (next == r->tail) { r->tail = (r->tail + 1) % TX_QUEUE_LEN; /* 覆盖最旧帧 */ } if (len > TX_SLOT_SIZE) len = TX_SLOT_SIZE; memcpy(r->slot[r->head].data, data, len); r->slot[r->head].len = len; r->head = next; if (!r->sending) tx_ring_kick(r, huart); } /* 发送完成中断:推进 tail,继续发下一帧 */ void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { ring_to1.tail = (ring_to1.tail + 1) % TX_QUEUE_LEN; tx_ring_kick(&ring_to1, &huart1); } else if (huart->Instance == USART2) { ring_to2.tail = (ring_to2.tail + 1) % TX_QUEUE_LEN; tx_ring_kick(&ring_to2, &huart2); } }几个参数要解释清楚。TX_QUEUE_LEN定的是积压帧数,6 格在 115200 下大约能吸收 50ms 的突发流量,速率升到 921600 就要加大到 12 格以上。TX_SLOT_SIZE是单帧最大长度,超过就截断——如果对端会发长包,要么把这个值调到 1024,要么在 push 里做分片,别指望 HAL 帮你处理。
队列满时的策略我选择丢旧帧而不是丢新帧,因为透传场景里新数据通常比旧数据更有价值。如果业务上不能接受丢数据,就得加流控信号或者把队列做在堆上动态分配,那是另一个复杂度了。
4.3 错误回调:把串口从溢出状态里拉回来
DMA 加空闲中断的组合在长时间运行时,最常遇到的是溢出错误(ORE)。一旦 ORE 置位且不清理,串口的接收状态机会卡在错误态,之后的ReceiveToIdle_DMA全部返回 BUSY,表现为"跑几个小时后突然不通了,重启就好"。
/* 串口错误回调:清标志并重新挂接收 */ void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) { /* 先读 SR 再读 DR,这是清 ORE 的标准动作 */ volatile uint32_t tmp; tmp = huart->Instance->SR; tmp = huart->Instance->DR; (void)tmp; huart->ErrorCode = HAL_UART_ERROR_NONE; if (huart->Instance == USART1) { huart->RxState = HAL_UART_STATE_READY; /* 解除卡死状态 */ HAL_UARTEx_ReceiveToIdle_DMA(&huart1, u1_rx_buf, RX_BUF_SIZE); } else if (huart->Instance == USART2) { huart->RxState = HAL_UART_STATE_READY; HAL_UARTEx_ReceiveToIdle_DMA(&huart2, u2_rx_buf, RX_BUF_SIZE); } }手动把RxState掰回 READY 这一步看起来粗暴,但比等 HAL 自己恢复可靠得多。HAL 库在错误处理上的状态机逻辑比较保守,某些版本在 ORE 之后不会自动回到就绪态,不手动复位就会一直卡着。代价是如果错误发生时 DMA 里还有半个帧的数据,那半个帧会被丢掉——透传场景下可以接受。
5. 避坑与排查:六个让透传断流或丢字节的现场
5.1 编译链接阶段的三个报错
现象一:DMA_HandleTypeDef has no member named hdma_usart1_rx。原因是 CubeMX 里只配了 USART 没加 DMA 请求,生成的usart.c里根本没有这个句柄。解决方法是回 CubeMX 的 DMA Settings 标签把 RX/TX 两条请求加上,重新生成代码。别手动去usart.c里补定义,下次生成会被覆盖。
现象二:undefined reference to HAL_UARTEx_ReceiveToIdle_DMA。这是固件包版本偏旧,老版本 HAL 库里没有这个接口。两个选择:在 CubeMX 的 "Manage embedded software packages" 里升级到当前系列的最新固件包;或者退回老写法,自己开 IDLE 中断,在USARTx_IRQHandler里判断__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE)然后手动算长度。我一般选前者,少写一堆寄存器操作。
现象三:编译通过但下载后不运行,或者跑一次就死。八成是 SYS 的 Debug 没选 Serial Wire,SWD 引脚被复用后调试器连不上。补救办法是把 BOOT0 拉高进系统存储器启动模式,重新下载一个 Debug 口正确的工程;或者在复位释放的瞬间抢连接。这个坑踩一次就够记一辈子,我现在新建工程的第一件事永远是配 Debug 口。
5.2 运行阶段的丢字节、断流与乱码
现象一:上电后能透传第一帧,之后彻底没反应。原因:接收 DMA 用的是 Normal 模式,搬完指定长度(或空闲触发一次)后 DMA 就停了,回调里没有重新调用ReceiveToIdle_DMA。解决:在HAL_UARTEx_RxEventCallback里处理完数据后立即重挂,两路都要挂,漏一路就是一路不通。
现象二:低速测试正常,一提高波特率就大量丢包。原因:发送端HAL_UART_Transmit_DMA返回HAL_BUSY被忽略,或者发送队列深度不够。还有一个隐蔽原因是 DMA 流中断的优先级低于串口中断,接收事件频繁抢占,发送完成中断迟迟进不来。解决:确认发送走了队列而不是裸调 HAL 函数;把 DMA 流中断优先级调到和串口同级或更高;队列深度按波特率 × 最大帧时间 / 单帧时间估算后留一倍余量。
现象三:数据能过去,但每隔一段就错几个字节。原因通常是时钟不准。HSE 频率填的和板上晶振不一致,或者 PLL 分频后 HCLK 有偏差,波特率误差累积到几个百分点就会采样错位。解决:用示波器量一下 USART 的 TX 波形,算实际波特率和设定值的偏差,超过 2% 就要回头查时钟树。另一个可能是两路串口的字长、校验位配置不一致,转发出去的数据对端解析不了。
现象四:跑几十分钟到几小时后突然不通,断电重启恢复。原因:ORE 溢出错误没有被清理,串口接收状态机卡死。高速率下 CPU 忙于其他中断、没及时响应 DMA 请求时特别容易触发。解决:实现HAL_UART_ErrorCallback,按 4.3 节的方式清标志并把RxState复位到 READY,然后重挂接收。同时在主循环里加一个看门狗式的计数监控,收帧数长时间不增长就主动重挂一次。
现象五:两路串口互相干扰,A 口的回声串到 B 口。原因是共地没做好,或者两个 USB 转串口模块的地线通过电脑主板形成了回路,地电位差把信号耦合过去。解决:确认板子和两个模块的 GND 都可靠连接;测试时把两个模块插在同一台电脑的相邻 USB 口上有时会更糟,换成板子独立供电、模块只接 TX/RX/GND 三根线往往能消掉。
现象六:仿真器单步时正常,全速跑就丢数据。原因:断点停住时 DMA 还在搬,恢复运行时缓冲区的状态和软件记录的对不上。解决:调试透传这类 DMA 相关逻辑时,尽量用串口打印计数代替断点,或者只在初始化阶段打断点。真要观察数据流,用逻辑分析仪抓 TX 引脚波形比单步靠谱得多。
6. 装完怎么验证:三组计数加一次对拷就能定性
6.1 先看计数,再谈波形
透传工程最怕"看起来在跑"。我的习惯是在主循环里每 500ms 打印一次三组计数:USART1 收帧数、USART2 收帧数、发送队列溢出次数。前两个数应该同步增长,第三个必须长期为 0。只要溢出计数在涨,就说明发送侧跟不上接收侧,先加队列深度或者降波特率,别急着怀疑硬件。
/* 主循环里的统计打印,重定向到调试串口或复用一路 USART */ static uint32_t last_rx1 = 0, last_rx2 = 0; if (HAL_GetTick() - last_tick > 500) { uint32_t r1 = stat_rx1, r2 = stat_rx2; printf("U1=%lu(+%lu) U2=%lu(+%lu) DROP=%lu\r\n", r1, r1 - last_rx1, r2, r2 - last_rx2, stat_drop); last_rx1 = r1; last_rx2 = r2; last_tick = HAL_GetTick(); }stat_rx1和stat_rx2在接收回调里自增,stat_drop在队列满覆盖旧帧时自增。这三个数一摆出来,链路是否稳定一目了然:两个增量长期为 0 是接收没挂上;一个涨一个不涨是单向转发断了;两个都涨但 DROP 也涨是发送侧拥塞。
计数正常之后再做一次对拷。用两个 USB 转串口模块分别接 USART1 和 USART2,PC 侧脚本同时打开两个口,一端发随机数据、另一端收,比对哈希值。
import serial, os, hashlib # 两个串口参数必须和固件里配置的一致 p1 = serial.Serial('COM5', 115200, timeout=3) p2 = serial.Serial('COM6', 115200, timeout=3) payload = os.urandom(2048) p1.write(payload) got = p2.read(len(payload)) print('len:', len(got)) print('match:', hashlib.md5(payload).digest() == hashlib.md5(got).digest())timeout要给足,2048 字节在 115200 下需要接近 180ms,再算上转发延迟,设 3 秒比较稳。match为 True 只说明单向通了,把两个串口对调再跑一次,双向都通过才算这套环境真正装成。想测吞吐就逐步加大payload到 8KB、32KB,观察 DROP 计数和 match 结果,找到这套配置下的稳定上限。
6.2 边界情况再补两个动作
对拷通过后别急着收工,补两个动作能省掉后面很多麻烦。一是拔掉其中一路的 TX 线,模拟对端掉线,观察另一路是否还在正常收发、有没有因为发送失败卡住队列——这验证的是错误恢复逻辑。二是把波特率往上调一档(比如 115200 升到 460800),重跑对拷脚本,如果这时开始丢包,说明瓶颈在发送队列或者中断优先级,而不是串口本身。
我现在的习惯是每次改完串口或 DMA 配置,都强制走一遍"三组计数 + 双向对拷 + 拔线测试",三个都过才算数。这套流程看着繁琐,但比起在现场靠"重启一下试试"来定位问题,前面多花十分钟太值了。希望这套配置和验证顺序,能帮你把双串口 DMA 互透传一次装稳。
本文还有配套的精品资源,点击获取