简介:面向STM32F407嵌入式开发者的六串口通信工程,适合需要同时使用UART1~UART6、处理不定长数据接收的初学者与工程人员参考。工程基于CubeMx生成,六个串口均启用DMA与空闲中断,能够接收不定长度数据;若项目只需部分串口,可在CubeMx中裁剪后,仅删去少量与硬件资源相关的代码即可移植,默认采用片外8MHz晶振,也可在CubeMx中调整。压缩包共67个文件、约581KB,以41个.h头文件和19个.c源文件为主体,另有.ioc工程配置、MDK-ARM工程文件及启动文件等,便于直接编译与二次修改。已有7803人学习下载,说明其在STM32串口通信配置场景中具备较好的参考价值,读者可从中获得六串口并发的初始化框架、DMA与空闲中断接收思路、工程裁剪与移植方法,以及CubeMx配置与HAL库代码组织方式,适合用作课程设计、项目原型或串口通信调试的基础模板。
1. 六个串口一起开:STM32F407 上这件事比想象中琐碎
单看手册,STM32F407 挂六个串口(USART1、USART2、USART3、UART4、UART5、USART6),资源是够的。真到板子上把六路同时跑起来,问题往往不在「会不会配一个 UART」,而在时钟树、AF 复用号、DMA 流、中断优先级这几处互相牵扯——配错一个,六路里就有一两路静默,剩下几路照常工作,很难第一时间定位。某开发者做多路传感器采集板,六路设备波特率各不相同,其中 UART4、UART5 的引脚又和 SDIO、I2C2 撞车,头一版板子改了两轮才跑通。这篇按「资源分配 → 初始化 → 收发链路 → 避坑 → 压测」拆开讲,适合手里已经有 F407 板子、要把多路串口同时挂在一条总线上收数据的场景;新手能照着抄,熟手可以重点看 AF 号和 DMA 请求映射那几段。
2. 先把时钟和引脚钉死:USART1~UART6 的资源分配表
六串口并发的第一道坎不在代码,在选型阶段。F407 的六个串口不是均匀挂在两条总线上的:USART1 和 USART6 挂 APB2,USART2、USART3、UART4、UART5 挂 APB1。主频 168MHz 时,APB1 是 42MHz,APB2 是 84MHz,两者正好差一倍。这意味着同一个 115200 的波特率,在 USART1 和 USART2 上算出来的 BRR 值完全不同,HAL 库会替你算,但你得知道自己拿的是哪条总线的时钟,否则排查乱码时会漏掉这一层。
2.1 APB1 与 APB2 的分频差,决定了波特率误差余量
F407 标准配置是 HSE 8MHz 经 PLL 倍频到 168MHz,AHB 不分频,APB1 四分频得 42MHz,APB2 二分频得 84MHz。串口波特率的计算公式是 USARTDIV = fCK / (16 × baud)(过采样 16 倍时),fCK 就是所在总线的时钟。以 115200 为例,APB2 上的 USART1 得到 45.57,APB1 上的 USART2 得到 22.79,小数部分四舍五入后引入的误差都在 0.1% 量级,正常通信没问题。
真正容易翻车的是非标波特率。比如某路设备跑 921600,APB1 的 42MHz 除以 16×921600 得 2.848,尾数只剩 0.848,实际误差会明显放大;而挂 APB2 的 USART1 得到 5.696,误差小得多。所以六路里如果有高速率设备,优先把它排到 USART1 或 USART6 上,这是选型阶段就该定的事。
| 串口 | 挂载总线 | 总线时钟(168MHz 主频下) | 常见引脚 | 备注 |
|---|---|---|---|---|
| USART1 | APB2 | 84MHz | PA9 / PA10 | 高速率优先放这里 |
| USART2 | APB1 | 42MHz | PA2 / PA3 | PA2 与以太网 MDIO 冲突 |
| USART3 | APB1 | 42MHz | PB10 / PB11 | 与 I2C2 复用 |
| UART4 | APB1 | 42MHz | PA0 / PA1 或 PC10 / PC11 | PA0 兼 WKUP,PC10/11 与 SDIO 冲突 |
| UART5 | APB1 | 42MHz | PC12 / PD2 | PD2 与 SDIO_CMD 冲突,RX 无备选脚 |
| USART6 | APB2 | 84MHz | PC6 / PC7 或 PA11 / PA12 | PC6/PC7 与 SDIO_D6/D7、TIM8 冲突 |
这张表建议直接写进项目 README,后面改板、换芯片型号时第一眼就能看出哪两个外设抢了同一个脚。
2.2 引脚复用号:AF7 和 AF8 是两拨,不是一拨
F407 的复用功能编号里,USART1、USART2、USART3 归在 AF7,而 UART4、UART5、USART6 归在 AF8。这一点和很多人「串口都配 AF7」的直觉相反,也是六串口项目里最高频的静默故障源——引脚配成 AF7,GPIO 时钟、串口时钟、NVIC 全都开了,示波器上 TX 就是没有波形,代码怎么看都没错。
我一般的做法是把 AF 号也塞进静态描述表,和串口实例、IRQ 号放在一起,初始化时从表里取,不手写第二遍。这样即使后面把 UART4 从 PA0/PA1 改到 PC10/PC11,也只需要改表里两行。
引脚冲突要提前算清楚。典型冲突有三组:UART5 的 RX 只有 PD2 一个选择,而 PD2 是 SDIO_CMD,如果板子上挂了 SD 卡,UART5 就没法用;UART4 用 PA0/PA1 时,PA0 是 WKUP 引脚,PA1 在以太网 RMII 方案里是 REF_CLK;USART6 用 PC6/PC7 时,这两脚同时是 SDIO_D6/D7 和 TIM8_CH1/CH2。是否冲突,取决于你的板子还用了什么外设,选型阶段拿原理图逐个对一遍,比调试阶段拿示波器找三天划算。
2.3 中断向量与 DMA 请求是硬映射,先抄下来
六个串口各自有独立的中断向量,不存在共享入口:USART1_IRQHandler、USART2_IRQHandler、USART3_IRQHandler、UART4_IRQHandler、UART5_IRQHandler、USART6_IRQHandler。NVIC 里对应 USART1_IRQn 到 USART6_IRQn,使能时逐个开,别指望一个函数兜住六个。
DMA 请求同样是固定的,选错了流不会报错,只是数据永远搬不动:
| 串口 | RX 的 DMA | TX 的 DMA |
|---|---|---|
| USART1 | DMA2 Stream2 Channel4 | DMA2 Stream7 Channel4 |
| USART2 | DMA1 Stream5 Channel4 | DMA1 Stream6 Channel4 |
| USART3 | DMA1 Stream1 Channel4 | DMA1 Stream3 Channel4 |
| UART4 | DMA1 Stream2 Channel4 | DMA1 Stream4 Channel4 |
| UART5 | DMA1 Stream0 Channel4 | DMA1 Stream7 Channel4 |
| USART6 | DMA2 Stream1 Channel5 | DMA2 Stream2 Channel5 |
看到 USART6 的 Channel 是 5、其余是 4 的时候,很多人会愣一下——这是芯片固定映射,不是笔误。DMA1 一共 8 个流,六个串口如果全开 RX+TX,DMA1 上要占 10 个请求位,实际装不下,所以要么部分串口不用 DMA,要么把 USART1、USART6 放到 DMA2 上。我在六串口项目里的常规分配是:USART1、USART6 走 DMA,其余四路走中断接收,这样 DMA1 只被四路占用,还能留出余量给别的外设。
3. 初始化落地:一次把六个串口拉起来的代码组织
六个串口配置流程完全一样,唯一的差别是参数。如果按 CubeMX 生成的风格复制六份 MX_USARTx_UART_Init,代码会膨胀到七八百行,改一个波特率要改六处,后面加第七个口就是灾难。合理的组织方式是一张静态描述表 + 一个通用初始化函数,把差异全部数据化。
3.1 用一张静态表管六个串口
先把每个串口的关键属性抽出来:实例指针、中断号、TX/RX 引脚和端口、AF 号、波特率。这样表里一行就是一路串口,一眼能看全六路配置。
/* uart_port.h —— 六串口的静态描述,新增一路只加一行 */ typedef struct { USART_TypeDef *inst; /* 串口实例,如 USART1 */ IRQn_Type irqn; /* 中断号,如 USART1_IRQn */ GPIO_TypeDef *tx_port; /* TX 所在端口 */ uint16_t tx_pin; /* TX 引脚号 */ GPIO_TypeDef *rx_port; /* RX 所在端口 */ uint16_t rx_pin; /* RX 引脚号 */ uint8_t af; /* 复用号:USART1~3 为 7,UART4/5、USART6 为 8 */ uint32_t baud; /* 该路实际波特率,六路可以各不相同 */ } uart_port_t; extern const uart_port_t g_uart_ports[6]; extern UART_HandleTypeDef g_huart[6];/* uart_port.c —— 表内容,顺序与 g_huart 下标一一对应 */ const uart_port_t g_uart_ports[6] = { { USART1, USART1_IRQn, GPIOA, GPIO_PIN_9, GPIOA, GPIO_PIN_10, 7, 921600 }, { USART2, USART2_IRQn, GPIOA, GPIO_PIN_2, GPIOA, GPIO_PIN_3, 7, 115200 }, { USART3, USART3_IRQn, GPIOB, GPIO_PIN_10, GPIOB, GPIO_PIN_11, 7, 115200 }, { UART4, UART4_IRQn, GPIOA, GPIO_PIN_0, GPIOA, GPIO_PIN_1, 8, 57600 }, { UART5, UART5_IRQn, GPIOC, GPIO_PIN_12, GPIOD, GPIO_PIN_2, 8, 9600 }, { USART6, USART6_IRQn, GPIOC, GPIO_PIN_6, GPIOC, GPIO_PIN_7, 8, 115200 }, };表里最值得看的是 af 那一列:前三行是 7,后三行是 8。这份表就是整个六串口模块的唯一配置源,NVIC 优先级、DMA 分配、日志前缀都应该从这里派生,不要另起一份数组。
3.2 从 GPIO 到 NVIC 的初始化顺序
初始化顺序有个不容易察觉的约束:GPIO 和串口时钟必须先开,才能写寄存器;HAL 的 UART_Init 要在 GPIO 配好之后调用;NVIC 使能放在最后。顺序颠倒不会报错,但会出现「第一帧发不出去」这种间歇性现象。
/* 打开某一路串口所需的全部外设时钟,六个实例逐个判断 */ static void uart_clock_enable(const uart_port_t *p) { if (p->inst == USART1 || p->inst == USART6) { __HAL_RCC_USART1_CLK_ENABLE(); /* 占位,实际按实例分支 */ __HAL_RCC_USART6_CLK_ENABLE(); } else { __HAL_RCC_USART2_CLK_ENABLE(); __HAL_RCC_USART3_CLK_ENABLE(); __HAL_RCC_UART4_CLK_ENABLE(); __HAL_RCC_UART5_CLK_ENABLE(); } } /* 通用初始化:GPIO -> 时钟 -> 参数 -> NVIC */ void uart_init_one(uint8_t idx) { const uart_port_t *p = &g_uart_ports[idx]; GPIO_InitTypeDef gpio = {0}; /* 1. 引脚:TX 推挽复用,RX 上拉输入,避免悬空收到乱码 */ gpio.Mode = GPIO_MODE_AF_PP; gpio.Pull = GPIO_PULLUP; gpio.Speed = GPIO_SPEED_FREQ_VERY_HIGH; gpio.Alternate = p->af; gpio.Pin = p->tx_pin; HAL_GPIO_Init(p->tx_port, &gpio); gpio.Pin = p->rx_pin; HAL_GPIO_Init(p->rx_port, &gpio); /* 2. 句柄参数:六路共用一套模板,只改实例和波特率 */ g_huart[idx].Instance = p->inst; g_huart[idx].Init.BaudRate = p->baud; g_huart[idx].Init.WordLength = UART_WORDLENGTH_8B; g_huart[idx].Init.StopBits = UART_STOPBITS_1; g_huart[idx].Init.Parity = UART_PARITY_NONE; g_huart[idx].Init.Mode = UART_MODE_TX_RX; g_huart[idx].Init.HwFlowCtl = UART_HWCONTROL_NONE; g_huart[idx].Init.OverSampling = UART_OVERSAMPLING_16; HAL_UART_Init(&g_huart[idx]); /* 3. NVIC:抢占优先级按实时性排,不要六个都用 0 */ HAL_NVIC_SetPriority(p->irqn, 3, 0); HAL_NVIC_EnableIRQ(p->irqn); }g_huart 用全局数组而不是结构体指针,是为了让中断服务函数能靠下标直接拿到句柄。g_huart[0] 对应 USART1、g_huart[5] 对应 USART6,这个约定要在工程里写死,别在别处重新排序。
HAL_UART_Init 内部会自动判断实例挂在哪条总线上,从 RCC 取对应时钟做 BRR 计算,所以六路波特率各不相同也完全没问题,不需要手动填 BRR。
3.3 初始化之后先自检,别等上层业务报错
六个口全初始化完,建议先跑一段自检再进业务。最省事的做法是让六路依次发送一句带编号的字符串,用 USB-TTL 逐个接上确认。如果板子上六路都接了设备,就看设备的接收指示灯,或者直接把六路 TX 短接到六路 RX 做回环测试——短接回环能一次性确认 TX、RX、波特率、NVIC 四件事是否都对。
/* 上电自检:六路依次发一行标识,回环测试时直接从对应 RX 读回 */ void uart_selftest(void) { for (uint8_t i = 0; i < 6; i++) { char msg[16]; int n = snprintf(msg, sizeof(msg), "UART%d OK\r\n", i + 1); HAL_UART_Transmit(&g_huart[i], (uint8_t *)msg, n, 100); } }自检通过后再打开接收中断和 DMA,顺序反了的话,自检期间收到的数据会触发中断,和后面的业务逻辑搅在一起,现象是「第一包数据莫名其妙丢了几个字节」。
4. 收发链路:中断、环形缓冲和 DMA 空闲中断怎么搭配
六路并发收数据,最怕的不是收不到,是收到了但来不及处理。115200 波特率下一个字节约 87 微秒,如果中断里做协议解析、拼包、打印日志,很容易在解析一组数据时错过另一路的字节,表现出来就是某一路随机丢包。分层是唯一解:中断里只负责把字节从寄存器搬进缓冲区,协议解析放在主循环按包处理。
4.1 环形缓冲:中断只搬字节,解析放主循环
环形缓冲的关键是 head 和 tail 都用 volitate 修饰的 uint16_t,写入只在中断做、读取只在主循环做,单生产者单消费者场景下不需要关中断。缓冲区大小取 2 的幂,取模用位与代替除法,六路各开一个实例,每路 256 或 512 字节。
#define RB_SIZE 512 /* 必须是 2 的幂,取模用 & 代替 % */ typedef struct { uint8_t buf[RB_SIZE]; volatile uint16_t head; /* 只在中断里写 */ volatile uint16_t tail; /* 只在主循环里写 */ volatile uint32_t drop; /* 溢出计数,排查丢包时看这个 */ } ring_t; static inline void rb_put(ring_t *r, uint8_t b) { uint16_t next = (r->head + 1) & (RB_SIZE - 1); if (next == r->tail) { /* 满了:丢新字节并计数,不阻塞中断 */ r->drop++; return; } r->buf[r->head] = b; r->head = next; } static inline int rb_get(ring_t *r, uint8_t *b) { if (r->tail == r->head) return 0; /* 空 */ *b = r->buf[r->tail]; r->tail = (r->tail + 1) & (RB_SIZE - 1); return 1; }drop 字段平时没人看,出问题时它是第一个该看的数字。六路联调时如果某一路 drop 持续增长,说明主循环处理不过来,要么加大缓冲区,要么优化解析逻辑,不要第一反应去怀疑硬件。
HAL 的回调里逐个字节塞进环形缓冲:
/* 中断回调:只做入队,不做任何解析 */ void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { uint8_t idx = uart_index_of(huart); /* 由实例反查下标 */ rb_put(&g_ring[idx], g_rx_byte[idx]); HAL_UART_Receive_IT(huart, &g_rx_byte[idx], 1); /* 重新挂载接收 */ }uart_index_of 遍历 g_huart 数组比对 Instance 指针即可,六路规模下开销可以忽略。这个重新挂载的动作不能漏,漏一次该路就永久静默,而且不会报错。
4.2 DMA + IDLE:一次收完整帧的不定长数据
逐字节中断在小数据量下够用,但六路都在 921600 时中断频率太高。UART4、UART5 这类跑的是不定长帧协议的,用 DMA + IDLE 空闲中断更合适:DMA 把数据搬到缓冲区,总线空闲一个字符时间后触发 IDLE,中断里读剩余计数就知道这一帧有多长。
新版 HAL 里有 HAL_UARTEx_ReceiveToIdle_DMA,直接支持;老一点的库没有这个接口,得自己开 IDLE 中断并在服务函数里读 SR/DR 清标志。
/* 启动 DMA + 空闲中断接收,buf 为每路独立的大缓冲 */ #define FRAME_MAX 256 static uint8_t g_dma_buf[6][FRAME_MAX]; void uart_start_dma_rx(uint8_t idx) { /* 只开 IDLE 中断,DMA 半满/全满中断不需要 */ __HAL_UART_ENABLE_IT(&g_huart[idx], UART_IT_IDLE); HAL_UARTEx_ReceiveToIdle_DMA(&g_huart[idx], g_dma_buf[idx], FRAME_MAX); } /* 空闲事件回调:Size 就是本次收到的字节数 */ void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { uint8_t idx = uart_index_of(huart); uart_frame_push(idx, g_dma_buf[idx], Size); /* 整帧入队,交给主循环 */ HAL_UARTEx_ReceiveToIdle_DMA(huart, g_dma_buf[idx], FRAME_MAX); /* 重新挂载 */ }Size 参数是 HAL 帮你算好的已接收字节数,不用自己去读 NDTR。这里同样有「重新挂载」的坑:不重新调用,DMA 收完一次就停了,现象是上电收到一帧之后就再也没数据。
4.3 发送端别用阻塞式 HAL_UART_Transmit
六路并发时,阻塞式发送是最隐蔽的性能杀手。HAL_UART_Transmit 在发送一整帧期间会把 CPU 挂在轮询里,115200 下发 100 字节大约 8.7 毫秒,这 8.7 毫秒里其他五路的中断全被延迟。六路同时发的时候,延迟叠加起来非常可观。
常见做法是发送也走 DMA,配合一个简单队列:主循环把待发数据压入队列,发送完成中断里取出下一帧。如果数据量很小(比如十几字节的状态上报),也可以退一步用 HAL_UART_Transmit_IT,但要注意 huart 的 gState 在忙时会返回 HAL_BUSY,队列能兜住这种情况。
/* 非阻塞发送:忙则返回 HAL_BUSY,由上层队列稍后重试 */ HAL_StatusTypeDef uart_send_async(uint8_t idx, const uint8_t *data, uint16_t len) { if (g_huart[idx].gState != HAL_UART_STATE_READY) { return HAL_BUSY; /* 上一帧还没发完,交给队列重试 */ } return HAL_UART_Transmit_DMA(&g_huart[idx], (uint8_t *)data, len); }检查 gState 这一步不能省,直接调 Transmit_DMA 在忙时返回 BUSY,数据就悄悄丢了,而且不会有任何日志。
5. 避坑与排查:六串口联调时最容易翻车的五个点
5.1 现象:引脚有电平变化,但波形是一段乱码,换到 USART1 就正常
原因:UART4、UART5、USART6 的复用号是 AF8,配成 AF7 时 GPIO 虽然被切到复用模式,但选中的是另一套外设功能,TX 上会有信号出来,只是内容完全对不上。这个错误在 USART1~USART3 上不会犯,所以调通前三个口之后特别容易顺手复制。
解决:把 AF 号从静态表里取,不再手写。改完对照第 2 章那张表复查一遍,尤其确认 USART6 用的是 8 而不是 7。
5.2 现象:六路里只有 UART4、UART5 乱码,其余正常,波特率看起来也没设错
原因:这两路挂在 APB1 上,如果工程里把系统时钟改过(比如为了降功耗把 APB1 分频改了),而代码里某处硬编码了 BRR 或者手动算了 USARTDIV,就会和实际总线时钟不符。
解决:一律交给 HAL_UART_Init 从 RCC 取时钟计算,不要手写 BRR。改完时钟树之后重新跑一遍六路回环,别只测 USART1。乱码排查的顺序建议是:先确认波特率误差(高速率优先怀疑 APB1),再确认数据位/停止位/校验,最后才怀疑线材和电平。
5.3 现象:六个口全开之后,主循环里某个任务周期性卡顿,串口数据也偶尔丢
原因:中断服务函数里做了耗时操作,比如在 HAL_UART_RxCpltCallback 里直接拼包、判断帧头、甚至调用 printf。六路中断叠加,主循环几乎拿不到执行时间。
解决:中断里只做入队(第 4 章的 rb_put),所有解析搬到主循环。判断标准很简单:中断函数体应该在十几行以内,且不含任何循环和字符串操作。
5.4 现象:上电后某一路收不到数据,示波器上看 RX 引脚有波形,但 MCU 没反应
原因:HAL_UART_Receive_IT 或 ReceiveToIdle_DMA 没有挂载,或者挂载之后被 HAL_UART_ErrorCallback 里的溢出错误打断后再没恢复。F407 的串口有 ORE 溢出标志,高波特率下如果中断响应不及时,ORE 置位,HAL 会进错误回调,此后接收就停了。
解决:在错误回调里清标志并重新挂载接收。溢出计数单独记一个变量,联调时打印出来,能直接看出是哪一路在丢。
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) { uint8_t idx = uart_index_of(huart); g_ore_cnt[idx]++; /* 溢出计数,联调时观察 */ __HAL_UART_CLEAR_OREFLAG(huart); /* 清 ORE,否则接收永久停摆 */ HAL_UART_Receive_IT(huart, &g_rx_byte[idx], 1); /* 重新挂载 */ }5.5 现象:DMA 方式下,某两路数据互相串台,或者只有一路能收到
原因:两个串口被分到了同一个 DMA 流的同一个通道。比如 UART4_RX 固定是 DMA1 Stream2 Channel4,如果另一路也误配到 Stream2,后配置的会覆盖前一个,现象就是先配的那个口静默。
解决:按第 2 章的 DMA 映射表逐行核对,配置完后在初始化末尾读一遍 DMA_SxCR 的 CHSEL 位确认。DMA1 只有 8 个流,六路全开 RX+TX 装不下,必须做取舍——这也是我在项目里让 USART1、USART6 走 DMA2 的原因。
6. 进阶:把六路串口合成一条可切换的调试总线
六路都通了之后,下一个问题是日志怎么输出。六路各自接设备,调试信息散在六个串口上,抓一次现场要插六根线,效率很低。我常用的做法是留一路(通常是 USART1,APB2 时钟、速率余量大)作为调试总线,其余五路的数据加前缀后转发过来,通过一个命令字切换只看哪一路。
实现上分两层:一层是统一日志接口,带串口编号和时间戳;一层是过滤开关,用一个位掩码控制哪几路允许输出。
/* 调试总线:位掩码控制哪几路参与输出,bit0 对应 USART1 */ static uint8_t g_log_mask = 0x3F; /* 默认六路全开 */ void log_uart(uint8_t idx, const char *fmt, ...) { if ((g_log_mask & (1u << idx)) == 0) return; /* 被过滤掉 */ char line[128]; int n = snprintf(line, sizeof(line), "[U%d] ", idx + 1); va_list ap; va_start(ap, fmt); n += vsnprintf(line + n, sizeof(line) - n, fmt, ap); va_end(ap); n += snprintf(line + n, sizeof(line) - n, "\r\n"); /* 调试总线固定走 g_huart[0],用 DMA 发,避免阻塞其它五路 */ if (g_huart[0].gState == HAL_UART_STATE_READY) { HAL_UART_Transmit_DMA(&g_huart[0], (uint8_t *)line, n); } }配一个极简的命令解析挂在 USART1 的接收路径上,收到m3就把掩码切成只看第 3 路,收到m3f切回全开。这样现场只要一根线,就能在六路之间来回切。
这套东西有一个前提:调试总线的带宽要够。六路都在 115200、每路每秒几十条日志时,USART1 跑 921600 才勉强够用,所以我在第 2 章的表里把 USART1 放到了 APB2 上并给了最高波特率。如果日志量再大,就得做限流——按串口编号做令牌桶,每路每秒最多 N 条,超出的直接丢弃并计数。限流比加大缓冲区有效,缓冲再大也会被日志洪水冲垮。
还有一个验证习惯值得保留:每次改完六串口的配置,我会先跑一轮「六路自环 + 一路对一路交叉」的短接测试,确认没有串台,再接真实设备。自环测试能覆盖 AF 号、时钟、波特率、DMA 流、中断挂载这五个环节,两分钟就能跑完,比接上设备之后逐路猜要快得多。从那以后我每次开新板子,都先让六个口轮流发同一句话、再用短接线逐对回读,一次把底层问题清干净,再去碰协议层。希望帮到你。
本文还有配套的精品资源,点击获取