ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32双串口同时工作实战:配置、中断接收与调试技巧

STM32双串口同时工作实战:配置、中断接收与调试技巧 做嵌入式这几年我越来越觉得串口是嵌在世界里的“基础设施”可它的数量永远不够用。今年做一个小车项目时板子上既要挂蓝牙模块和手机App通信又要接GPS模块持续回传定位数据两个模块都得占独立的串口接口还必须同时跑。也就是说我得让一颗STM32F103C8T6上的两个串口同时工作一个收蓝牙指令一个收GPS数据彼此不能干扰。这就是很多人问的“STM32双串口同时工作”。这篇文章不打算从寄存器表格开始讲而是直接围绕“双串口怎么配、怎么接、怎么收、怎么发、遇到问题怎么看”把一套完整跑通的流程和踩过的坑整理出来。文中选用的芯片是STM32F103C8T6开发环境是Keil5 STM32CubeMX HAL库代码可以直接在F103系列上改改就能用。不管你是刚接触STM32的新手还是已经在做多设备通信的老手应该都能从中找到可以直接复制的东西。1. 什么时候必须用双串口那些“一个串口不够用”的真实场景1.1 最常见的三类项目需求第一类场景是设备既要和上位机或手机通信又要和传感器模块交互。最典型的就是我前面提到的小车项目STM32通过USART1接蓝牙模块接收手机下发的速度指令和转向指令同时USART2接GPS模块不断把经纬度信息回传。两个通道的数据内容、协议格式完全不同混在一个串口里就得自己设计通信协议人为拆包、分辨数据来源处理起来既繁琐又容易出错。分开之后每个串口的数据流天然独立代码逻辑清晰得多。第二类场景是业务数据通信和调试日志分离。调程序的时候最占地方的不是逻辑而是调试输出。很多人习惯用printf打印日志可如果printf和业务通信共用同一个串口日志一多就会把业务数据时序完全打乱。我现在的习惯是USART1固定给业务通信比如和上位机做Modbus协议交互USART2专门用来输出调试日志或者接另一个调试屏幕。这样两边互不干扰业务代码里可以放心写调式信息不会影响实际的数据链路。第三类场景是两个设备之间的桥接或协议转换。有些传感器模块的波特率是固定的比如9600而蓝牙模块通常是115200两者根本没法直接对接。用双串口就可以搭一座桥STM32在USART2用9600波特率收传感器数据然后在USART1用115200波特率发给蓝牙。换算、打包、协议适配这些工作在中控里顺手完成成本极低。1.2 双串口方案的本质是“数据通道分离”不是魔法很多初学者一听“双串口”就觉得是个高深技术其实它的本质就是一颗MCU内部同时跑多个独立的串口外设。以STM32F103C8T6为例内部有USART1、USART2、USART3三套串口外设它们各有各的时钟门控、寄存器组、数据寄存器、中断向量和引脚收发路径完全独立。你可以把它们想成同一幢楼里三套互不相通的管道系统CPU通过每套管道自己的门数据寄存器收发数据管道来了新水新数据会拉一下铃触发中断CPU听到哪套门的铃响就跑去哪套门口取水。双串口之所以容易出问题问题往往不在外设本身而是使用者没把“各走各的路”这个前提配置好引脚被别的功能占了、中断回调里没区分串口实例、某个串口的时钟忘了打开、重映射后AFIO时钟没使能任何一个环节出错都会导致那个串口不工作或数据错乱。所以接下来的内容本质上就是帮你把这条“各走各的路”的路径完整打通。2. 动手前的硬件功课STM32串口资源分配与引脚复用避坑2.1 STM32F103的串口资源清单与引脚映射先看一张资源对照表。F103C8T6是LQFP48封装板上最多能引出3个串口默认引脚和重映射引脚是这样分配的串口默认TX默认RX重映射TX重映射RX挂载总线USART1PA9PA10PB6PB7APB2最高72MHzUSART2PA2PA3无无APB1最高36MHzUSART3PB10PB11PC10仅大封装有PC11APB1PA9/PA10是USART1的默认引脚能直接用基本不用操心重映射。USART2的PA2/PA3在C8T6上也是固定的没有备用映射。USART3的默认引脚PB10/PB11最常用但它还有一个重映射位置在PC10/PC11注意PC10/PC11在C8T6这种48脚的封装上是否真的引出来了很多蓝板小板为了节约引脚数量并没有把PC口全部拉出来这一点要对着原理图确认。从总线频率角度看USART1挂在APB2上最高可以有72MHz的时钟USART2和USART3挂在APB1上最高36MHz。这个差异对常用的9600、115200波特率影响不大不过在计算波特率初值和评估误差时CubeMX会按各自的总线时钟自动计算不需要手动去调寄存器但理解这个区别可以帮你排查一些奇怪的乱码问题。2.2 引脚复用里的两个隐藏坑第一个坑就是PB3、PB4和PA15这几个引脚默认被JTAG占用了。F103刚上电时PB3是JTDO、PB4是NJTRST、PA15是JTDI它们默认是调试接口功能不初始化成普通GPIO或串口。如果你想把USART1重映射到PB6/PB7问题不大但如果想复用PB3/PB4做别的功能就必须先关闭JTAG。HAL库里对应的操作是__HAL_AFIO_REMAP_SWJ_NOJTAG();这一句会把JTAG相关引脚释放为普通IO同时保留SWD的PA13/PA14这样你还能继续用ST-Link调试。很多人遇到“这个引脚怎么不输出”“串口怎么接上去没反应”的怪问题排查到最后往往就是少了这一句。第二个坑是重映射时忘了打开AFIO时钟。STM32的引脚重映射功能由AFIO复用功能IO管理使用重映射功能时必须先使能AFIO时钟否则复位状态下的重映射寄存器根本写不进去。HAL库的CubeMX配置通常会自动生成__HAL_RCC_AFIO_CLK_ENABLE()但手写寄存器或移植旧工程时就很容易漏。2.3 实际接线建议理论清楚了接线原则其实也就是三句话串口1接高速设备、串口2接中低速传感器、串口3做备用扩展。我实际项目里可以把USART1的115200留给蓝牙模块或USB转TTL调试口USART2的9600接GPS模块USART3在需要时接另一个传感器。线缆接法必须是收发交叉模块的TX接STM32的RX模块的RX接STM32的TX也就是TX对RX、RX对TX。如果接反收数据会一直为空。另外共地是铁律两个设备的地必须连在一起否则电平参考点不一致轻则乱码重则不通信。像GPS或蓝牙模块普遍是3.3V电平但有些老式模块是5V电平直接接STM32的引脚可能烧IO口这种场景必须加电平转换或分压。3. 用STM32CubeMX快速生成双串口工程配置步骤与关键参数解读3.1 CubeMX配置双串口的关键步骤用CubeMX生成工程的最大好处是图形化配置不容易漏项尤其对于双串口这种牵扯时钟、GPIO、中断多个模块的场景手写代码最怕的就是“某个细节忘了导致查半天查不出来”。基本步骤如下新建工程选择芯片型号STM32F103C8T6。在Pinout Configuration中找到USART1Mode选Asynchronous异步通信默认引脚会显示PA9/PA10。找到USART2同样选Asynchronous默认引脚PA2/PA3。分别设置两个串口的参数。我习惯USART1用115200、8位数据、无校验、1位停止位USART2用9600、8位数据、无校验、1位停止位具体看外接模块的协议要求。在NVIC设置中把USART1 global interrupt和USART2 global interrupt前面的勾都打上这是双串口中断接收的前提。时钟树页面默认会自动配好72MHz系统主频确认PCLK1和PCLK2的频率符合预期。生成代码时选择MDK-ARM V5Toolchain选择Keil然后就得到一个可直接编译的工程骨架。这一步看似简单但很多人会在第5步上翻车——只配置了串口参数没勾中断结果代码里调HAL_UART_Receive_IT一点反应都没有因为中断根本没有使能。3.2 时钟树、中断优先级与生成代码的对应关系CubeMX在生成代码时会同时生成两个串口的初始化函数分别在usart.c中定义void MX_USART1_UART_Init(void) void MX_USART2_UART_Init(void)这两个函数做的事情本质上是一样的把GPIO引脚配置为复用推挽或复用开漏模式设置串口参数波特率、字长、停止位、校验位然后使能UART外设。HAL库还会自动注册好中断优先级。同时在main函数中CubeMX已经调用了这两个初始化函数不需要你自己重复写。关于两个串口的时钟差异补充说明一下。系统主频72MHz时APB2总线的PCLK2是72MHzUSART1就基于72MHz计算波特率APB1总线的PCLK1是36MHzUSART2、USART3就基于36MHz计算波特率。CubeMX会根据你选的波特率自动算好分频寄存器所以大部分情况下都不用关心具体数值。但在手动改系统主频或者手动修改时钟树时要注意主频变了之后如果只是重新编译而不重新生成初始化代码波特率可能就不准了表现就是串口能通信但收到的字符偶尔乱码。中断优先级方面HAL库默认用的是抢占优先级子优先级分组。双串口场景下我的经验是把两个串口的抢占优先级设成相同值比如都是3子优先级可以设成0这样在中断处理很短的情况下两个串口是完全并行的关系。如果某个串口的数据特别重要不允许被耽搁可以把它的抢占优先级设得更高比如设成2但一般没有必要。中断回调函数里不能做耗时操作这个后面第4章会细讲。3.3 生成代码的结构梳理生成的工程里核心部分就三个main.c定义了UART_HandleTypeDef huart1;和UART_HandleTypeDef huart2;这两个句柄分别代表串口1和串口2。usart.c包含MX_USART1_UART_Init和MX_USART2_UART_Init两个初始化函数。stm32f1xx_it.c包含USART1和USART2的中断服务函数内部调用了HAL库的中断处理函数HAL_UART_IRQHandler(huart1)和HAL_UART_IRQHandler(huart2)。双串口能不能正常工作关键就是看这三块是否都配置到位。如果编译后某个串口没反应先检查main.c中MX_USART2_UART_Init是否被调用再检查中断函数中是否有HAL_UART_IRQHandler(huart2)这两处是最容易遗漏的。4. 双串口接收中断的核心代码与缓冲区设计4.1 为什么推荐用中断接收而不是轮询串口接收有两种常见方式轮询和中断。轮询就是主循环里不断读HAL_UART_Receive但主循环里还有别的任务什么时候去读不确定数据来了没读就会被后面来的数据覆盖。中断接收的好处是数据一到CPU立刻跳进中断服务函数把数据保存下来不会丢数据主循环只负责处理已经收好的数据。双串口同时工作的情况下两个串口的中断互相独立USART2的数据到了会触发USART2的中断USART1的数据到了会触发USART1的中断。中断服务函数执行时间极短所以两个串口看起来是在“同时工作”实际是靠中断并发完成的。这就是为什么用中断接收几乎是双串口的标配。4.2 按行接收的简单实现区分两个串口的中断回调HAL库的中断回调机制有一点需要注意无论是哪个串口触发中断最终调用的都是同一个弱函数HAL_UART_RxCpltCallback。所以在这个回调内部必须通过huart-Instance判断当前是哪个串口实例否则两个串口的数据会全部混在一起处理。下面是一套最简单、适合短帧数据通信的双串口接收代码示例。思路是收到一个字节就存一个遇到换行符\n或者缓冲区满则认为一帧接收完成。/* 全局变量 */ uint8_t uart1_rx_byte 0; uint8_t uart2_rx_byte 0; uint8_t uart1_rx_buf[128]; uint8_t uart2_rx_buf[128]; volatile uint16_t uart1_rx_cnt 0; volatile uint16_t uart2_rx_cnt 0; volatile uint8_t uart1_rx_flag 0; volatile uint8_t uart2_rx_flag 0; /* main函数中启动双串口中断接收 */ HAL_UART_Receive_IT(huart1, uart1_rx_byte, 1); HAL_UART_Receive_IT(huart2, uart2_rx_byte, 1); /* 中断回调根据Instance区分串口 */ void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { if (uart1_rx_byte \n || uart1_rx_cnt sizeof(uart1_rx_buf) - 1) { uart1_rx_buf[uart1_rx_cnt] \0; uart1_rx_flag 1; uart1_rx_cnt 0; } else { uart1_rx_buf[uart1_rx_cnt] uart1_rx_byte; } HAL_UART_Receive_IT(huart1, uart1_rx_byte, 1); } else if (huart-Instance USART2) { if (uart2_rx_byte \n || uart2_rx_cnt sizeof(uart2_rx_buf) - 1) { uart2_rx_buf[uart2_rx_cnt] \0; uart2_rx_flag 1; uart2_rx_cnt 0; } else { uart2_rx_buf[uart2_rx_cnt] uart2_rx_byte; } HAL_UART_Receive_IT(huart2, uart2_rx_byte, 1); } }这段代码有几个关键点需要特别注意。第一在中断回调里处理完一个字节后必须重新调用HAL_UART_Receive_IT因为HAL库的机制是每次只接收指定的字节数接收完成后中断就停了如果这里忘了重新启动串口就只会收到第一帧之后再也没反应。第二判断Instance时用USART1、USART2这个宏即可这是串口外设的寄存器基地址比较稳定。第三接收缓冲区定义成volatile防止主循环里读取时被编译器优化掉。主循环中处理接收完成的标志while (1) { if (uart1_rx_flag) { printf(USART1: %s\r\n, uart1_rx_buf); uart1_rx_flag 0; } if (uart2_rx_flag) { printf(USART2: %s\r\n, uart2_rx_buf); uart2_rx_flag 0; } }注意这个示例有个隐藏的坑如果两个串口的接收数据都需要printf但printf重定向只指向了USART1那么USART2的数据也会从USART1打印出来看起来就像“串口2的数据跑到串口1上去了”。这个问题我会在第5章专门展开。4.3 升级为环形缓冲区应对不确定长度的数据按行接收适合一帧数据短、间隔明确的简单场景。但如果两个串口的数据流量大、帧长可变甚至可能一帧数据还没收完下一帧就到了那上面的方案就会丢数据。这种情况我更推荐用环形缓冲区。环形缓冲区的思想很简单缓冲区是一个固定大小的数组写数据时从“头指针”位置写入并后移读数据时从“尾指针”位置读取并后移头尾相等时代表缓冲区为空。数据写到缓冲区尾部时会绕回开头继续写只要读数据的速度跟得上写数据的速度理论上就能保证不丢字节。typedef struct { uint8_t buffer[256]; volatile uint16_t head; volatile uint16_t tail; } RingBuffer; RingBuffer uart1_rb; RingBuffer uart2_rb; void RB_Init(RingBuffer *rb) { rb-head 0; rb-tail 0; } uint8_t RB_IsEmpty(RingBuffer *rb) { return rb-head rb-tail; } void RB_Write(RingBuffer *rb, uint8_t data) { uint16_t next (rb-head 1) % sizeof(rb-buffer); if (next ! rb-tail) /* 有空位才写入满了丢弃 */ { rb-buffer[rb-head] data; rb-head next; } } uint8_t RB_Read(RingBuffer *rb, uint8_t *data) { if (RB_IsEmpty(rb)) return 0; *data rb-buffer[rb-tail]; rb-tail (rb-tail 1) % sizeof(rb-buffer); return 1; }对应的中断回调就可以简化成void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { RB_Write(uart1_rb, uart1_rx_byte); HAL_UART_Receive_IT(huart1, uart1_rx_byte, 1); } else if (huart-Instance USART2) { RB_Write(uart2_rb, uart2_rx_byte); HAL_UART_Receive_IT(huart2, uart2_rx_byte, 1); } }主循环里再按协议去解析缓冲区中的数据帧解析完一个字节就消费一个字节。这样接收数据不会因为主循环忙碌而丢失是工程上更健壮的方案。4.4 双串口发送的几种姿势发送比接收简单双串口发送的本质就是分别调HAL_UART_Transmit函数阻塞模式下会等数据全部发完才返回。举个例子/* 串口1发送 */ HAL_UART_Transmit(huart1, (uint8_t*)Hello USART1\r\n, 15, 1000); /* 串口2发送 */ HAL_UART_Transmit(huart2, (uint8_t*)Hello USART2\r\n, 15, 1000);HAL_UART_Transmit的最后一个参数是超时时间单位毫秒。如果串口出故障导致数据发不出去这个函数会在超时后返回而不是让程序卡死。我习惯设置成50到1000毫秒具体看数据量和波特率。还有一点很实用在实际项目中人们往往希望printf输出能灵活指定到某个串口而不只是默认的USART1。我一般会封装两个发送函数void USART1_SendString(char *str) { HAL_UART_Transmit(huart1, (uint8_t*)str, strlen(str), 100); } void USART2_SendString(char *str) { HAL_UART_Transmit(huart2, (uint8_t*)str, strlen(str), 100); }需要用哪个串口发数据就调用哪个函数代码可读性会好很多。如果要用printf默认可以通过重定向fputc实现但fputc只能绑定一个串口另一个串口的数据就不能走printf。这种时候建议干脆不要依赖printf直接封装自己的debug_log函数在函数内部根据参数决定发哪个串口。5. 双串口同时跑的实测问题与排查思路5.1 现象一第二个串口完全收不到数据这是我被问得最多的一个问题。配置完成后USART1能收发改用USART2接设备后却什么反应都没有。遇到这种情况我建议按这个顺序排查第一步确认初始化函数确实被调用了。CubeMX生成的MX_USART2_UART_Init不会自动出现在main里吗其实会但如果你手动改过main函数或者把初始化代码删了就有问题。缺少时钟初始化的串口外设完全没有供电。第二步确认GPIO引脚模式正确。串口RX引脚必须是复用输入TX引脚必须是复用推挽输出。如果用CubeMX配置它会自动设置好但手写代码时很多人会把RX设成普通的GPIO输入导致数据进不了外设。第三步确认中断使能。打开stm32f1xx_it.c看看里面有没有USART2_IRQHandler并且确认里面确实调用了HAL_UART_IRQHandler(huart2)。缺失的话即使数据到了也不会触发回调HAL_UART_Receive_IT等于白写。第四步检查NVIC里USART2全局中断是否勾选。CubeMX里没勾的话中断向量表里根本不会生成相关入口。第五步用一个USB转TTL模块在外部回环测试把USART2的TX和RX用跳线短接然后用串口助手通过USB转TTL模块单独测试这个串口能不能自收自发。如果TX短接RX后都收不到自己发出去的数据基本可以断定是硬件初始化问题如果自己发的能收到再检查外部接的模块接线是否交叉、共地是否可靠。5.2 现象二两个串口的数据互相串线这个现象非常有意思从串口1发数据串口2上竟然也能看到或者串口1收到的内容混进了串口2的报文。我先说结论这通常不是硬件上真的“串线”了而是代码里把数据输出都打到了同一个地方。最常见的坑就是我前面提到的printf重定向问题。CubeMX生成的工程默认通过重定向fputc把printf指向USART1那么不管你在主循环里怎么区分uart1和uart2的数据一旦你写printf就会从USART1发出去如果此时你正用串口1连接电脑USB转TTL模块也会把串口1收到的数据原样发回电脑看起来就像串口2的数据跑到了串口1上。解决办法很简单给每个串口单独封装打印函数不要全局依赖printf。或者如果非要使用printf可以在主循环里用uart1_rx_flag判断串口1数据用uart2_rx_flag判断串口2数据然后分别用HAL_UART_Transmit(huart1, ...)和HAL_UART_Transmit(huart2, ...)发送而不是都用printf。另外还要排除一种硬件层面的干扰如果两个串口的RX引脚靠得太近信号线之间有浮动干扰数据会出现偶发乱码。这种情况通常出现在飞线搭电路的实验环境把线束整理一下、缩短距离就能缓解。5.3 现象三偶发性数据丢失或程序卡死双串口刚调通时感觉一切正常跑几分钟后某个串口突然数据断了或者整个程序卡死。这个问题十有八九出在中断处理耗时太长上。HAL库的中断处理函数HAL_UART_RxCpltCallback是在中断上下文里执行的而中断里最忌讳的就是做耗时操作。如果你在这个回调里面放了HAL_UART_Transmit发送很长的字符串、调用了printf、或者跑了一个几百毫秒的延时函数USART1的中断处理时间过长会导致USART2的中断一直被挂起USART2接收到的数据在硬件接收寄存器里被后续数据覆盖最终表现为数据丢失。我推荐的方案是中断回调里只做数据的快速搬运和标志位置位所有的解析、处理、响应都放到主循环里做。如果确实需要在中断里做简单的响应也尽量只发几个固定字节不要发长数据。还有一个隐藏细节是中断优先级抢占。如果USART1的抢占优先级比USART2高那么当USART2正在处理中断时USART1的中断可以立刻打断它。如果USART1的回调执行时间稍长USART2那半截没处理完的数据就可能丢。所以双串口场景下我给两个串口设置相同的抢占优先级配合精简的回调函数就能有效避免这类问题。5.4 排查方法论的总结回头看双串口问题大多集中在三个点上时钟和GPIO有没有全部使能、中断有没有正确触发、回调里有没有区分串口实例。我每次碰到双串口故障基本都按照“先静态查代码再硬件回环测试最后看波形”的顺序去做。静态查代码重点看初始化函数调用、中断服务函数、回调函数三处硬件回环测试可以快速把问题范围缩小到“MCU初始化问题”还是“外部模块接线问题”如果两个串口同时工作时的时序问题特别诡异逻辑分析仪是最有效的工具直接挂在RX引脚上看波形波特率是否正确、数据是否中断、帧和帧之间有没有丢数据一目了然。6. 升级思路从双串口到多串口、从中断到DMA6.1 DMA接收与空闲中断应对大数据量场景如果双串口的数据量很大比如一个串口持续不断地接收GPS数据另一个串口还在接收传感器高频回传那么每收一个字节就触发一次中断CPU会忙得喘不过气来。这时就需要DMA介入。DMA直接内存访问可以让串口硬件收到数据后自动把数据搬运到内存缓冲区里完全不需要CPU干预。CPU只在缓冲区满或者检测到一帧结束时才过来处理。配合IDLE空闲中断可以判断一帧数据的结束当串口在总线上检测到一段时间没有数据就认为当前帧接收完成。F1系列的HAL库中较新的版本支持HAL_UARTEx_ReceiveToIdle_DMA这个接口一步到位把空闲检测和DMA接收组合起来uint8_t uart1_dma_buf[128]; uint8_t uart2_dma_buf[128]; HAL_UARTEx_ReceiveToIdle_DMA(huart1, uart1_dma_buf, sizeof(uart1_dma_buf)); HAL_UARTEx_ReceiveToIdle_DMA(huart2, uart2_dma_buf, sizeof(uart2_dma_buf));接收完成或空闲后的回调函数会告诉你本次实际收到的字节数void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart-Instance USART1) { /* uart1_dma_buf中前Size个字节是一帧完整数据 */ USART1_ProcessData(uart1_dma_buf, Size); HAL_UARTEx_ReceiveToIdle_DMA(huart1, uart1_dma_buf, sizeof(uart1_dma_buf)); } else if (huart-Instance USART2) { USART2_ProcessData(uart2_dma_buf, Size); HAL_UARTEx_ReceiveToIdle_DMA(huart2, uart2_dma_buf, sizeof(uart2_dma_buf)); } }DMA接收的缓冲区大小要大于最大帧长度否则一帧会被拆成两部分触发两次回调。如果一帧数据可能超过缓冲区需要自己在回调里做粘包检查把两次回调的数据拼起来。使用DMA模式时还要注意DMA通道的分配不能冲突。F103C8T6的DMA1有7个通道USART1_RX通常用DMA1_Channel5USART2_RX通常用DMA1_Channel6不同型号可能不一致最稳妥的办法还是翻参考手册或者直接看CubeMX生成的DMA配置。6.2 多串口同时跑的工程化要点如果项目需要的不只是双串口而是三串口甚至更多思路是一样的但有几个工程层面的问题要提前考虑。串口中断回调函数HAL_UART_RxCpltCallback是唯一的如果串口多了判断Instance的if-else会越来越长。这种时候建议用函数指针数组或者switch-case把串口处理逻辑做成可配置的表项避免回调函数膨胀。接收缓冲区也要独立每个串口有自己的缓冲区、自己的写入索引、自己的处理标志。中断优先级分配要更精细。串口多的时候可以把最关键的通信串口设成高抢占优先级次要的调试串口设成低优先级。但要记住中断优先级高不意味着它可以把所有中断都打断还要看NVIC分组设置。从工程管理的角度我强烈建议把每个串口的通信协议、缓冲区、状态机放到独立的源文件里保持代码的可维护性。双串口也许还能在一个main.c里勉强撑住到了三串口四串口主文件就会变成几千行的“屎山”排查问题极其痛苦。6.3 个人使用心得串口资源其实是“规划”出来的做过的双串口项目多了我的一个体会是串口资源少不是最要命的真正要命的是不规划就开始写代码。拿到一块板子先看看外接的设备有哪些各自是什么接口、什么波特率、什么协议再决定用哪两个串口分配什么优先级的任务。这一步做在前面后面调双串口就是半小时的事跳过这一步可能一下午都陷在排查各种初始化遗漏里。另外PCB画板阶段就要注意需要使用的串口引脚尽量直接连接到排针或接插件方便调试时插USB转TTL或者逻辑分析仪。毕竟双串口既然要同时跑调试的时候大概率两个串口都要接出来看数据预留调试节点是很有必要的。最后分享一个小技巧程序里给两个串口各定义一个调试开关宏。比如#define DEBUG_UART1_ENABLE和#define DEBUG_UART2_ENABLE上线前直接关掉打印程序逻辑不用动。这个习惯帮我省了很多在客户现场排查串口数据的时间你也可以试试。
返回列表