简介:面向STM32F091嵌入式开发者的多串口程序资源,提供基于中断接收的UART驱动与一转多转发实现,适合嵌入式开发、物联网及工业控制从业者参考。压缩包共881个文件,约7.58MB,以C源码、H头文件、uvprojx工程配置及编译产物为主,附带HTML、PDF等说明文档,便于对照学习。已有767人学习下载。程序覆盖系统时钟、GPIO复用、串口参数配置到接收中断的完整流程,并实现一个主串口同时向多个从串口转发数据,最高支持一转七,对理解中断优先级管理、DMA通道分配及多串口数据分发策略具有较高价值;通过阅读工程源码,可快速掌握STM32F091串口驱动初始化与中断接收转发的实际写法,适用于通信桥接、数据采集等场景。
1. 一个48MHz芯片挂8路UART:STM32F091多串口方案的边界在哪里
遇到“一块板子要同时接4个以上TTL串口设备”的需求,大多数人的第一反应是换一颗管脚多的芯片,或者外挂UART扩展芯片。但STM32F091这个48MHz主频的Cortex-M0内核芯片,硬是给了最多8个UART通道,而且UART引脚分布在好几组GPIO上,不像某些芯片那样复用冲突严重到只能同时用两路。实际项目里,用F091做多串口网关、串口协议转换、多传感器采集这类活儿非常合适,只是大部分人没把串口外设理解透就开始写代码,最后被波特率误差、AF选错、中断丢数据这几个问题反复折磨。这篇文章就把F091多串口UART这条路重新走一遍,从时钟和引脚配置到FIFO调度再到现场排障,全按可复现的方式来。
2. 先捋引脚和时钟:UART复用表、APB分频和波特率误差预算
2.1 F091的8个UART在哪:复用表比直觉可靠
F091的串口资源在参考手册里被命名为USART1、USART2、USART3、USART4,外加UART5到UART8,实质上是4个带同步功能的USART和4个纯异步UART。多数LQFP封装会把其中6到8路引出到芯片引脚上,剩下个别通道可能只在特定封装里出现,所以拿到具体型号后第一步应该查数据手册里的Alternate Function映射表,而不是凭经验猜引脚。
以最常见的几路为例:USART1大多走PA9/PA10,USART2走PA2/PA3,USART3可以走PB10/PB11或PC10/PC11,UART4经常复用PA0/PA1,UART5则可能落在PC12/PD2这类离核心引脚较远的位置。重点是复用功能编号不是随便填的,USART1的默认AF可能是1,但UART4对应引脚要填AF4,UART5甚至可能是AF0或AF4以外的值。我一般会先打开对照表,把要用的每路UART和引脚、AF号填进一个Excel里,再开始写代码。
这里有个容易踩的细节:F091的GPIO复用是每个引脚从AF0到AF7里选一个,STM32CubeMX能在图形界面里帮你选,但如果你用寄存器或LL库直接写,GPIOD 或 GPIOE 这类端口还得单独使能AHB时钟,否则引脚复用配置写进去也不会生效。
| UART通道 | 常用引脚示例 | 常见AF值 | 备注 |
|---|---|---|---|
| USART1 | PA9 TX / PA10 RX | AF1 | 多数参考设计的首选 |
| USART2 | PA2 TX / PA3 RX | AF1 | 可与USART1同时使用 |
| USART3 | PB10 TX / PB11 RX | AF1 | 管脚在另一侧,布线方便 |
| UART4 | PA0 TX / PA1 RX | AF4 | 注意与定时器通道复用 |
| UART5 | PC12 TX / PD2 RX | 查表确认 | 封装不同差异较大 |
写配置代码时,我习惯把GPIO时钟使能、AF配置、UART时钟使能分成三个独立步骤,顺序固定为端口时钟、引脚复用、外设时钟。因为F091的某些端口GPIO复用配置会被后续的同端口初始化覆盖,比如你先把PA0配成UART4_TX,后面某个模块初始化又把整个GPIOA配置了一遍,UART4就无声无息地失效了。
2.2 时钟树先行:用满48MHz,还是用对APB分频
F091的主频上限是48MHz,这个频率对115200波特率来说非常友好,但前提是UART外设的时钟确实拿到了48MHz。芯片内部有三种可用时钟源:外部晶振HSE、内部高速HSI48、以及PLL倍频后的系统时钟。最常见的做法是用8MHz HSE晶振,PLL倍频6倍得到48MHz系统时钟,再把APB1和APB2分频都设为1,这样USART1挂在APB2上,USART2到UART7挂在APB1上,得到的外设时钟都是干净的48MHz。
麻烦多出在仓库里原来有工程把APB分频设成了2,PCLK变成24MHz,写代码的人没注意,照抄了基于48MHz计算的波特率寄存器值,结果串口像半聋一样,9600能通,115200乱码。F091的UART时钟来自PCLK,不是系统时钟,这一点务必确认。如果在CubeMX里打开 Clock Configuration 页,看到APB1和APB2显示的是24MHz,那就是分频没拉回来。
波特率计算的底层逻辑也不复杂。16倍过采样时,USARTDIV = PCLK / (16 × 波特率),最终写入波特率寄存器的BRR值就是向上取整的USARTDIV × 16。拿48MHz和115200举例:
USARTDIV = 48000000 / (16 × 115200) = 26.0417
BRR取整后是417,也就是分频系数算成26.0625,实际波特率约115108,误差0.08%,完全在UART异步通信的±2%容忍范围内。真正危险的是PCLK用了某个非标准时钟,比如24MHz下配置2M波特率,量化误差会迅速放大。F091也支持8倍过采样,OVER8置位后波特率上限翻倍,但分频步进变成0.5,低速场景反而更容易产生误差,所以默认保持16倍过采样就够了。
2.3 波特率误差快查:三种时钟源下的安全速率表
用48MHz PCLK做16倍过采样时,9600、19200、38400、57600、115200这几个常用波特率误差都在0.2%以内。但如果用HSI48内部时钟,就要注意HSI48的初始精度和温漂,常温下误差一般不超过1%,在高温环境或长时间运行后可能逼近1.5%到2%。多串口项目里如果一端是PC的USB转串口芯片,另一端是F091,两边误差叠加后可能超出容忍范围,表现为偶发乱码。
| 波特率 | 理论BRR | 实际波特率 | 误差 |
|---|---|---|---|
| 9600 | 312.5→313 | 9599 | 0.01% |
| 19200 | 156.25→156 | 19231 | 0.16% |
| 38400 | 78.125→78 | 38462 | 0.16% |
| 115200 | 26.04→26? | 115385 | 0.16% |
| 921600 | 3.26→3 | 923077 | 0.17% |
上表中115200取BRR=417时误差0.08%,如果手滑按整数26去配置,丢掉小数部分,误差就会从0.08%跳到0.16%,虽然还在容忍范围,但在一端设备本身误差较大的现场,这种多余误差就是压垮通讯的最后一根稻草。所以规范做法是使用库函数LL_USART_SetBaudRate,由它内部完成USARTDIV计算和取整,不要手动往寄存器里塞一个自以为算好的数。
3. 多串口基础代码:初始化、中断接收与最小FIFO
3.1 先用LL库打通一路UART:初始化顺序不能乱
以USART3为例,假设是用PB10/PB11作为收发引脚。初始化代码分四步:使能GPIOB时钟和USART3时钟,配置PB10为复用推挽输出、PB11为复用输入,设置AF1,最后配置USART参数并使能中断。
void UART3_Init(void) { // 1. 打开GPIOB 和 USART3 时钟 LL_AHB1_Group_EnableClock(LL_AHB1_GROUP_PERIPH_GPIOB); LL_APB1_Group_EnableClock(LL_APB1_GROUP_PERIPH_USART3); // 2. 配置PB10为复用推挽,PB11为复用输入 LL_GPIO_SetPinMode(GPIOB, LL_GPIO_PIN_10, LL_GPIO_MODE_ALTERNATE); LL_GPIO_SetPinMode(GPIOB, LL_GPIO_PIN_11, LL_GPIO_MODE_ALTERNATE); LL_GPIO_SetPinSpeed(GPIOB, LL_GPIO_PIN_10, LL_GPIO_SPEED_HIGH); // 3. 设置复用AF值,USART3在PB10/PB11上对应AF1 LL_GPIO_SetAFPin_0_7(GPIOB, LL_GPIO_PIN_10, LL_GPIO_AF_1); LL_GPIO_SetAFPin_0_7(GPIOB, LL_GPIO_PIN_11, LL_GPIO_AF_1); // 4. 配置UART参数并启用 LL_USART_InitTypeDef uart_init; uart_init.BaudRate = 115200; uart_init.DataWidth = LL_USART_DATAWIDTH_8B; uart_init.StopBits = LL_USART_STOPBITS_1; uart_init.Parity = LL_USART_PARITY_NONE; uart_init.TransferDirection = LL_USART_DIRECTION_TX_RX; uart_init.HardwareFlowControl = LL_USART_HWCONTROL_NONE; uart_init.OverSampling = LL_USART_OVERSAMPLING_16; LL_USART_Init(USART3, &uart_init); // 5. 使能接收中断 LL_USART_EnableIT_RXNE(USART3); NVIC_SetPriority(USART3_IRQn, 2); NVIC_EnableIRQ(USART3_IRQn); // 6. 最后使能串口 LL_USART_Enable(USART3); }这段代码里有两个顺序细节值得强调:一是NVIC优先级设置要在使能串口之前完成,防止串口一打开就立刻进中断,而中断服务里还没准备好FIFO;二是LL_USART_Enable放在最后,确保所有配置都生效后外设才进入工作状态。很多初学者会把使能外设写在第一步,后面配置寄存器时外设已经按默认参数跑起来,出现一帧乱码后再恢复正常,这个乱码源头就是配置顺序不对。
3.2 接收FIFO与中断服务函数:ISR越短越好
多串口场景下,中断服务函数绝对不能做协议解析,只负责把接收到的字节塞进环形FIFO,然后立刻返回。FIFO用环形结构实现,容量256字节,head表示写入位置,tail表示读取位置,二者相等表示空。
#define UART_FIFO_SIZE 256 typedef struct { uint8_t buf[UART_FIFO_SIZE]; volatile uint16_t head; volatile uint16_t tail; volatile uint16_t overrun_cnt; } uart_fifo_t; uart_fifo_t g_uart3_fifo; void UART3_IRQHandler(void) { uint8_t data; uint16_t next_head; if (LL_USART_IsActiveFlag_RXNE(USART3)) { data = LL_USART_ReceiveData8(USART3); next_head = (g_uart3_fifo.head + 1) % UART_FIFO_SIZE; if (next_head == g_uart3_fifo.tail) { g_uart3_fifo.overrun_cnt++; // 溢出计数,用来排查丢包 } else { g_uart3_fifo.buf[g_uart3_fifo.head] = data; g_uart3_fifo.head = next_head; } } }FIFO设计里有个关键决策:读出缓冲数据时,主循环要判断 new_head != tail 才允许写入,否则丢弃并累计overrun。这个统计量极其有用,现场出现偶发丢数据时,第一个看的不是协议层,而是这个overrun_cnt有没有增长。如果FIFO一直溢出,说明主循环处理速度跟不上接收速率,或者中断里浪费了太多时间。
对于需要接收不定长协议帧的场景,很多人会在ISR里加状态机去匹配帧头帧尾,这是一个妥协方案。我的建议是中断里只做FIFO写入,在主循环中再做帧同步,因为帧同步逻辑复杂、分支多,放在ISR里会显著拉长中断响应时间。万一一个帧头匹配函数里有几十个if分支,每个字节进来都要扫一遍,三路串口同时满负荷跑起来,CPU时间就被吃掉了。
3.3 发送路径:阻塞发送只用于调试,上线前换成队列
最简单的发送函数是轮询TXE标志,一个字节一个字节等发送寄存器空,然后写数据。这条路在单串口、低速场景下没问题,但多串口并发时,一旦某个外设模块响应慢,阻塞发送会把整个主循环卡住几百毫秒。
void UART3_Send_Blocking(uint8_t *data, uint16_t len) { for (uint16_t i = 0; i < len; i++) { while (!LL_USART_IsActiveFlag_TXE(USART3)) { // 等待发送数据寄存器为空 } LL_USART_TransmitData8(USART3, data[i]); } }这个轮询发送函数不是不能用,但只该出现在调试阶段。工程上正规做法是维护一个发送队列,主循环把要发的数据写入发送FIFO,然后使能TXE中断或启动DMA。TXE中断模式下,ISR检查发送队列非空就取一个字节发出,队列空就关掉TXE中断。这样发送动作被拆散到每个字节的发送间隙里执行,不会阻塞主循环。
void UART3_IRQHandler(void) { // 接收部分代码与上文相同 // 发送部分:TXE标志触发时,从发送队列取字节 if (LL_USART_IsActiveFlag_TXE(USART3)) { uint16_t next_head; if (g_uart3_tx_head != g_uart3_tx_tail) { next_head = (g_uart3_tx_head + 1) % UART_FIFO_SIZE; LL_USART_TransmitData8(USART3, g_uart3_txbuf[g_uart3_tx_head]); g_uart3_tx_head = next_head; } else { LL_USART_DisableIT_TXE(USART3); } } }这里把发送缓冲也做成环形FIFO,主循环只需要调用一个UART3_SendToQueue函数,把数据拷贝进发送缓冲,然后开启TXE中断即可。发送逻辑本身也变成了非阻塞,配合不同波特率的多路串口,主循环不会因为某一路上的慢速设备而停摆。这套发送队列配合DMA可以更进一步减少中断次数,但F091的DMA通道分配要逐个核对映射表,项目初期用TXE中断已经能覆盖绝大多数场景,DMA那一步可以等稳定性验证过再优化。
4. 多串口并发调度:让8路UART不互相拖累的队列与事件策略
4.1 每路串口一份数据表:配置文件替代散落的全局变量
多串口工程最容易翻车的地方就是全局变量满天飞,uart1_rx_buf、uart2_buf、uart3_count 一路写到uart8,主循环里一大堆if,改一个波特率要改七八处。正确做法是把每路串口的FIFO指针、波特率、优先级、状态机上下文打包成一个配置结构体,再用数组索引。
typedef struct { USART_TypeDef *usart; uart_fifo_t *rx_fifo; uart_fifo_t *tx_fifo; uint8_t port_index; uint8_t irq_priority; void (*frame_callback)(uint8_t port, uint8_t *data, uint16_t len); } uart_channel_t; static uart_fifo_t g_fifo1_rx, g_fifo1_tx; static uart_fifo_t g_fifo2_rx, g_fifo2_tx; const uart_channel_t g_uart_channels[] = { { USART1, &g_fifo1_rx, &g_fifo1_tx, 0, 1, OnModuleA_Frame }, { USART2, &g_fifo2_rx, &g_fifo2_tx, 1, 2, OnSensorB_Frame }, // 按实际通道数扩展 };每个串口有自己独立的接收FIFO和发送FIFO,以及一个帧完成回调函数。主循环不再为每个串口写单独的轮询代码,而是遍历通道数组,统一调用同一套FIFO读取逻辑。这套结构对熟手来说价值在于降低新增串口的成本,新接一个模块只需要往数组里追加一行配置,再写一个对应的帧处理回调,不用改主循环和中断服务函数。对新手来说则能避免“uart2响应正常但uart3莫名超时”这种索引错位问题。
4.2 主循环的事件驱动模型:轮询FIFO不等于轮询串口
多串口程序的主循环不能做成for扫描每个串口然后傻等,等某一个串口有数据进入时其它通道已经被冷落了几毫秒。更合理的模型是主循环反复查看每个通道的FIFO是否有数据,有数据就抽取一字节喂给该通道的协议状态机,状态机拼出完整一帧则触发回调。由于FIFO读取是纯内存操作,耗时极短,每一轮遍历8路FIFO的开销大约只有几十微秒,不会成为瓶颈。
状态机这一步做帧同步需要注意半包问题。比如某个模块的数据帧不定长,以0xAA开头0x55结尾,中间长度可能3字节也可能40字节,如果状态机只按帧头帧尾切包,一个帧头之后的40字节里可能夹着另一个帧的帧头,解码必然错乱。常见做法是在状态机里同时做超时保护,利用F091的定时器外设产生1ms时基,主循环每次经过时检查当前正在拼接的帧距上次收到数据的时间,超过一帧间隔就判定为帧超时,把已收到的部分丢弃。
void MainLoop_Tick(void) { static uint16_t s_timeout_counter[8]; uint8_t byte; uint16_t i; // 每1ms调用一次 for (i = 0; i < CHANNEL_NUM; i++) { if (g_uart_channels[i].rx_fifo->head != g_uart_channels[i].rx_fifo->tail) { // FIFO里有新字节,进入状态机 byte = g_uart_channels[i].rx_fifo->buf[g_uart_channels[i].rx_fifo->tail]; g_uart_channels[i].rx_fifo->tail = (g_uart_channels[i].rx_fifo->tail + 1) % UART_FIFO_SIZE; FeedFrameParser(i, byte); s_timeout_counter[i] = 0; } else { // 无新字节,超时计数累加 if (s_timeout_counter[i] < 0xFFFF) { s_timeout_counter[i]++; } } } }这段逻辑里有一个重要的边界认知:超时计数清零时机和累加时机必须严格对应。只有读到新字节才清零,主循环每轮都累加,这样才不会出现高频触发误超时。超时阈值按最慢模块的帧间隔设定,我一般取该串口一帧最长间隔再乘1.5到2倍,比如模块每20ms发一帧连续数据,超时阈值设为40ms。
4.3 中断优先级与响应时间预算
Cortex-M0内核采用线性优先级模型,没有M3/M4那种抢占优先级和子优先级的组合分组,F091的NVIC最多提供16个可编程优先级,数值越小优先级越高。多串口场景里,我把实时性要求最高的串口,比如命令控制链路,优先级设为0或1,普通数据采集串口设为2到4,日志打印串口优先级最低。
这里要算一笔时间账。115200波特率下,一个字节的接收时间大约是86.8微秒,如果中断服务里处理时间接近这个数值,下一个字节到达时RXNE中断还在排队,硬件接收移位寄存器就可能被覆盖。FIFO的作用是把ISR压缩到两三条语句,理想情况下中断响应时间小于1微秒。当你发现某一串口在大量数据涌入时偶发丢字节,先别怀疑硬件,用GPIO翻转法测一下ISR实际执行时间是不是超标,这个技巧放到最后一章展开。
5. 多串口联调避坑记录:五条从玄学到结论的排障实录
5.1 现象:上电立刻收到0xFF,主控板还没发数据,模块就报错
原因:模块的RX引脚默认复用配置被初始化覆盖。这个问题多发生在GPIO复用设置和外设初始化顺序不一致时,具体来说,先执行了UART初始化,后执行了某个端口整体初始化,把RX引脚的复用模式重新配置成普通GPIO,此时模块端的TX引脚处于未确定状态,可能采样到高电平,解析出0xFF garbage frame。
解决:写一个UART_GPIO_Reconfig函数,在所有可能影响GPIO的初始化代码之后调用,重新执行对应引脚的AF配置。排查时可以在程序里对每个串口的GPIO初始化次数打断点,确认是否有多个函数操作同一个引脚。更稳妥的做法是把所有串口GPIO配置集中到一个独立C文件,避免分散。
5.2 现象:9600波特率通讯一切正常,改成115200后偶发乱码
原因:这属于典型的波特率误差叠加。9600波特率下48MHz PCLK分频得到的BRR是313,误差只有0.01%,即使两端的时钟精度都有偏差也能容忍。115200波特率下BRR取整后误差升到0.08%,如果对方设备的晶振是11.0592MHz系统或某个廉价USB转串口芯片,叠加误差可能突破2%。
解决:先修改代码里的波特率配置为38400试一下,如果问题消失,确认是波特率相关。然后把F091的时钟切换到HSI48或者改回外部晶振,重新打印实际波特率,用示波器测TX引脚的位宽,和理论一个位宽86.8微秒比对。位宽偏差超过标准值2%时,建议换一个串口中断唤醒和DMA方案,或者把对方设备接到另一个空闲UART通道验证是不是个别通道的PCB布线问题。
5.3 现象:UART4和UART5配置完,引脚上完全没有波形,示波器量TX是恒定高电平
原因:多半是AF值填错了,或者该引脚的复用功能被另一个外设配置覆盖。F091上UART4在PA0/PA1的AF值是4,UART5的引脚复用映射经常和TIM、SPI的外设交织在一起,如果复用表核对不仔细,AF填成1或7,引脚就不会连接到UART外设。
解决:先查参考手册的AF映射表,确认目标引脚的AF号。然后检查代码里GPIO_SetAFPin使用的是AF4还是AF1。如果AF值确认无误,再检查同端口其它引脚的初始化,用调试器在初始化完成后读GPIOx->AFRL/AFRH寄存器,看目标引脚对应的4位段是不是配置成目标AF值。这一步能立刻看出是不是被其他模块覆盖。
5.4 现象:三路串口同时开,其中某一路在数据量大时丢帧严重,FIFO溢出计数猛涨
原因:中断服务函数被高优先级外设中断打断,单字节处理时间被拉长。F091的USART3优先级如果设为2,而定时器中断优先级是0,定时器中断频繁触发时,USART3的RXNE中断可能延迟几十微秒才被响应,下一个字节到达时硬件接收寄存器溢出。
解决:把对丢帧敏感的串口中断优先级调到比高频定时器中断更高。同时检查该串口的ISR里有没有调用耗时函数,比如printf重定向或延时函数。另一个技巧是把丢帧串口切换到DMA接收,DMA搬数据不占CPU,只有半满或全满中断才响应一次,对抗中断延迟的能力比RXNE模式强得多。
5.5 现象:串口A收到的数据其实是串口B发送的,内容完整但通道错了
原因:这类串扰多半是DMA通道映射配错或RX DMA请求连线错误,F091的DMA通道和外设请求有固定映射,不同UART的RX请求可能对应到不同DMA通道,如果初始化代码把一个UART的RX请求关联到了另一个UART的DMA缓冲区,接收到的数据自然串线。
解决:逐个核对DMA请求映射表,确认UART4的RX请求对应的是DMA通道X,UART5的RX请求对应的是DMA通道Y,不要凭惯性照抄chip家族的工程代码。验证方法写一个测试函数,让串口A发送连续递增字节,串口B发送同样的递增字节但起点不同,比如A发0x01、0x02、0x03,B发0x80、0x81、0x82,两路同时收,看各自的缓冲区是不是出现对方的序列,一测一个准。
6. 闭环验证多串口:回环测试、逻辑分析仪解码与ISR耗时测量
多串口程序上线前,我会强制走一遍闭环验证流程,这套流程一共三步:物理回环、逻辑分析仪抓帧、ISR耗时测量。物理回环最简单也最容易被忽视,拿两根杜邦线把每路串口的TX接到自身的RX,然后程序里发一串已知的递增数据,比如0x55、0xAA、0x01、0x02、0x03这样带特征的数据,收回来逐字节比对。这一步能验证硬件链路里最基础的问题:布线是否短路、接口是否虚焊、GPIO复用是否配置成功、波特率是否匹配。
回环通过后,逻辑分析仪登场。我的习惯是把逻辑分析仪的D0到D3接到需要联调的串口TX引脚,采样率设为24MHz,配置4路异步串口解码,波特率按目标串口值设定。此时让两个串口互相发数据,观察解码窗口里的帧和软件端收到的帧是否一一对应。逻辑分析仪的价值在于能看到总线上的真实信号时序,有一次我排查一个“偶发多收一个字节”的问题,软件层面怎么查都查不出原因,逻辑分析仪一抓,发现是对方设备在发送START位时电平建立时间过长,导致采样点边缘化,这种问题纯粹用逻辑分析仪对时钟边沿分析才能定位。
ISR耗时测量是整个验证里最容易被忽略的一环。方法是在ISR入口把某个GPIO拉高,ISR出口拉低,然后用示波器量这个GPIO的高电平脉宽。以115200波特率为例,一个字节的时间窗口是86.8微秒,ISR脉冲宽度应该远小于这个值。如果测出来ISR宽达30微秒以上,说明中断函数里存在阻塞操作。通常这个测量用PE2这种空闲引脚做,不影响正常功能。
// 在USART3_IRQHandler里加上GPIO标记 void USART3_IRQHandler(void) { GPIOE->BSRR = GPIO_PIN_2; // 置高,标记ISR开始 // 原有FIFO接收逻辑 GPIOE->BSRR = GPIO_PIN_2 << 16; // 清零,标记ISR结束 }高电平脉宽如果稳定在1微秒以内,说明ISR是健康的,丢数据的问题大概率在主循环侧,需要检查帧解析状态机是否有阻塞调用。
三路串口并行压力测试也要做,让每一路都接上真实设备或对端模拟器,以各自的最大数据速率持续跑一小时,中间观察FIFO溢出计数和主循环是否周期超过10毫秒。我的习惯是把溢出计数做成周期性通过调试串口打印出来,现场跑一晚上,第二天早上看日志,只要overrun_cnt保持0,这套多串口程序才算真正过关。
从那以后,只要碰多串口项目,我都强制先走回环、再上逻辑分析仪、最后量ISR脉宽,少一步都不动业务代码。希望帮到你。
本文还有配套的精品资源,点击获取