单片机UART串口模块化编程(亲手封装代码,亲测可直接移植运行)

单片机UART串口模块化编程(亲手封装代码,亲测可直接移植运行)
一、写在前面几年前我做的一个工业采集项目在批量出货后出现了偶发性死机。排查到最后发现是串口接收时发生了内存溢出——接收数组越界篡改了相邻的关键变量导致程序跑飞。那次之后我重新设计了串口模块把接收状态、缓存管理、超时判断全部抽象成结构体经过4年量产验证再没出现过类似问题。本文就是把这套设计完整拆解出来。全文分三部分结构体设计 → 中断查询混合收发 → 调度逻辑。※ 本文中的串口引脚、波特率等参数均来自量产项目已做通用化处理可直接替换为您的硬件配置。二、模块设计目标在设计之前我先明确了这个模块要解决什么问题问题本设计的解决方案接收数据长度未知超时判断 动态缓存多串口复用结构体数组化统一接口数据量过大导致溢出数组边界判断 从头覆盖中断风暴导致死机ORE溢出错误检测与清除485发送最后一字节丢失使用TXC标志而非TXBE三、结构体变量类型设计3.1 抽象类成员作为从机我们无法预知何时会有数据进来也无法知道这包数据的具体长度。针对串口接收这个对象我们可以抽象出以下成员接收的数据当前接收的数据长度正在接收的标志位接收完成的标志位超时时间的计数器根据抽象出的成员设计结构体如下。typedefstruct{uint8_treceiving_flag:1;//串口正在接收标志位uint8_treceive_done_flag:1;//串口接收完成标志位uint8_ttime_counter:6;//接收超时计数器}USART_STATE;//串口接收状态结构体类型定义typedefstruct{uint8_tbuffer[200];//收发数据缓存数组uint8_tlen;//收发数据的实际长度}BUFFER;//收发数据缓存结构体数据缓存结构体中的缓存数组大小可以根据实际的需求进行调整需注意的是当数组大小超过 255 时应该将结构体中的收发数据的实际长度的类型定义为 uint16_t否则会有接收的数据错位的情况。3.2 定义串口相关的结构体变量定义的结构体变量数组的长度是 2 是因为我们使用了两个串口可以根据实际需要更改串口的个数。USART_STATE UsartState[2]{0,0,0,0,0,0};//串口接收状态变量BUFFER Receive[2]{{0,0,0,0}};//接收数组缓存变量BUFFER sendbuffer[2]{{0,0,0,0}};//发送数组缓存变量将结构体变量数组声明成外部变量方便在其他文件中调用externUSART_STATE UsartState[2];//串口接收状态数组externBUFFER Receive[2];//接收数据缓存结构体数组externBUFFER sendbuffer[2];//发送数据缓存结构体数组3.3 串口外设的宏定义和初始化串口初始化中配置通讯波特率 9600、无校验、8 位数字位、1 位停止位使能收发使能串口接收中断初始化收发数组为 0。#defineBLE_TX_PINGPIO_PIN_10#defineBLE_TX_GPIO_PORTGPIOB#defineBLE_RX_PINGPIO_PIN_11#defineBLE_RX_GPIO_PORTGPIOB#defineBLE_USARTUSART3#defineBLE_CLKRCC_APB1_PERIPH_USART3#defineBLE_IRQnUSART3_IRQnvoidOutUartInit(void){GPIO_Config_T gpioConfig;USART_Config_T usartConfigStruct;RCM_EnableAPB1PeriphClock(BLE_CLK);usartConfigStruct.baudRate9600;usartConfigStruct.modeUSART_MODE_TX_RX;usartConfigStruct.hardwareFlowUSART_HARDWARE_FLOW_NONE;usartConfigStruct.parityUSART_PARITY_NONE;usartConfigStruct.stopBitsUSART_STOP_BIT_1;usartConfigStruct.wordLengthUSART_WORD_LEN_8B;RCM_EnableAPB2PeriphClock(RCM_APB2_PERIPH_GPIOB);/* Configure USART Tx as alternate function push-pull */gpioConfig.pinBLE_TX_PIN;gpioConfig.modeGPIO_MODE_AF_PP;gpioConfig.speedGPIO_SPEED_50MHz;GPIO_Config(BLE_TX_GPIO_PORT,gpioConfig);/* Configure USART Rx as input floating */gpioConfig.modeGPIO_MODE_IN_FLOATING;//浮空输入gpioConfig.pinBLE_RX_PIN;GPIO_Config(BLE_RX_GPIO_PORT,gpioConfig);/* USART_Config */USART_Config(BLE_USART,usartConfigStruct);USART_Enable(BLE_USART);/* Enable USART Interrupt-RXNEIE */USART_EnableInterrupt(BLE_USART,USART_INT_RXNE);//接收中断NVIC_EnableIRQRequest(BLE_IRQn,2,0);memset(Receive[0],0,sizeof(Receive[0]));memset(sendbuffer[0],0,sizeof(sendbuffer[0]));}.....3.4 串口接收数据的底层代码设计externvoiddriverUsartClear(uint8_tnum);//清空串口收发相关变量externvoiddriverReceivingJudge(uint8_tnum);//串口接收完成(空闲)判断externvoidusartIrqHandle(uint8_tnum);//串口接收中断数据处理四、串口接收中断处理先声明三个底层函数、后面逐一实现externvoiddriverUsartClear(uint8_tnum);//清空串口收发相关变量externvoiddriverReceivingJudge(uint8_tnum);//串口接收完成(空闲)判断externvoidusartIrqHandle(uint8_tnum);//串口接收中断数据处理4.1 串口接收中断处理函数设计在接收中断中完成接收数组缓存数据和长度的更新有新的数据到来应该清空串口超时的计数器将接收数据进行标志位置 1方便后续的数据帧接收完成的判断。voidusartIrqHandle(uint8_tnum)//串口接收中断中调用{USART_T*uart;uartBLE_USART;if(uart0){if(USART_ReadStatusFlag(uart,USART_FLAG_RXNE)SET){UsartState[num].time_counter0;//接收超时的计数器清零Receive[num].buffer[Receive[num].len]USART_RxData(BLE_USART);//将接收的字节存入接收数组中并更新接收数组的长度Receive[num].len;//接收数组长度加1if(Receive[num].lenMAX_RECIEVE_LEN)//超过最大接收数组的长度从零开始Receive[num].len0;UsartState[num].receiving_flag1;//正在接收数据标志位置1}if(USART_ReadStatusFlag(uart,USART_FLAG_ORE)SET)//检测是否有溢出错误USART_ResetStatusFlag(uart,USART_FLAG_ORE);//清空溢出错误}}注意​①数组边界保护在接收中断中需要判断接收数据的长度是否超过最大的数组长度若超过数组从 0 开始缓存。若不做数组超限判断当数据量大时会引起内存溢出导致其他的相邻变量被篡改。②溢出错误处理需要对溢出错误ORE进行检查若有溢出错误要清空溢出错误标志位否则会一直进入串口中断导致通讯死机4.2 接收数据超时/空闲判断超时时间按3个字节的通讯时间来计算。计算过程· 一个字节 1个起始位 1个停止位 8个数据位 10个位· 波特率9600时发送一个字节的时间 t 10 × (1/9600) 1.04ms· 3个字节 ≈ 3.12ms留余量取3.5ms· 定时器周期为0.5ms3.5ms对应的计数器值为 7当持续3.5ms没有接收到新数据就判断为一帧接收完成。注意①超时时间的设计根据波特率来计算 3 个接收字节的时间②接收完成后要关闭接收中断等接收数据处理完成后再打开接收中断。防止在处理接收数据时又有新的数据进来影响接收数据的处理。voiddriverReceivingJuge(uint8_tnum)//0.5ms定时器中要做超时判断{if(UsartState[num].receiving_flag1)//判断接收中是否有数据进来{if(UsartState[num].time_counter7)UsartState[num].time_counter;//接收时计数器累加else{UsartState[num].time_counter0;//持续1.5ms以上没有接收到新的数据判断一帧结束清空超时计数器UsartState[num].receiving_flag0;//清空正在接收的标志位USART_DisableInterrupt(BLE_USART,USART_INT_RXNE);//关闭接收中断准备处理接收的数据帧防止处理接收数据时有新的数据进来UsartState[num].receive_done_flag1;//数据帧接收完成标志位置1}}}4.3 接收数据处理完成后清空相关变量在接收数据处理完成后需要清空相关变量和标志位方便下一次的串口数据的接收voiddriverUsartClear(uint8_tnum){memset(UsartState[num],0,sizeof(UsartState[num]));//清空串口接收状态标志位和超时计数器memset(Receive[0],0,sizeof(Receive[0]));memset(sendbuffer[0],0,sizeof(sendbuffer[0]));//清空收发数据缓存区USART_EnableInterrupt(BLE_USART,USART_INT_RXNE);//使能接收中断为下一帧数据接收做准备}五、串口发送函数设计查询方式串口发送采用查询方式减少中断开销。5.1 串口查询方式发送数据函数设计voidsendBuffer(uint8_t*buffer,uint8_tlength,uint8_tnum){uint8_ti;USART_T*uartDate;if(num0)uartDateBLE_USART;for(i0;ilength;i){while(USART_ReadStatusFlag(uartDate,USART_FLAG_TXBE)RESET);USART_TxData(uartDate,buffer[i]);}while(USART_ReadStatusFlag(uartDate,USART_FLAG_TXC)RESET);}关键点说明重要RS485用户特别注意使用RS485进行收发时需要在发送开始前使能发送控制引脚发送结束后失能发送控制引脚。判断当前字节是否发送完成必须使用USART_FLAG_TXC发送完成标志而不是USART_FLAG_TXBE发送缓冲区空标志——否则在RS485发送数据时最后一个字节会发不出去。如果硬件使用的是RS485发送函数建议增加方向控制voidsendBuffer(uint8_t*buffer,uint8_tlength,uint8_tnum){uint8_ti;USART_T*uartDate;if(num0)uartDateBLE_USART;RS485_TX_ENABLE();// 使能发送for(i0;ilength;i){while(USART_ReadStatusFlag(uartDate,USART_FLAG_TXBE)RESET);USART_TxData(uartDate,buffer[i]);}while(USART_ReadStatusFlag(uartDate,USART_FLAG_TXC)RESET);// 必须等TXCRS485_TX_DISABLE();// 失能发送}六、函数调度逻辑6.1 函数中断入口voidUSART3_IRQHandler(void){usartIrqHandle(0);}6.2 定时器中断0.5ms时间片voidTMR1_UP_IRQHandler(void){staticuint8_tnum0;if(TMR_ReadIntFlag(TMR1,TMR_INT_UPDATE)SET){driverReceivingJuge(0);}}6.3 主循环中的调度在主循环 while()中查询串口数据接收是否完成并进行数据处理以及清空其中串口数据的发送会在接收数据处理函数中调用接收数据处理函数主要是进行接收数据分析以及相关协议的处理。if(UsartState[0].receive_done_flag1){recieveBufferHandle(Receive[0],0);//接收数据处理函数driverUsartClear(0);}七、这个设计的优缺点优点·结构清晰通过结构体数组支持多串口扩展·内存安全边界判断 ORE清除避免死机和溢出·实时性好中断接收 查询发送兼顾效率与可靠性·经过量产验证本设计已在批量产品中运行4年未出现因串口模块导致的死机或丢包问题局限性· 超时阈值固定波特率变化时需要重新计算· 接收缓存采用“从头覆盖”策略在极端大数据量下可能丢帧· 未实现环形缓冲区后续版本可升级八、留给读者的思考题你们在实际调试串口调试过程遇到过哪些BUG最后是怎么解决的当缓冲区满时我选择了“从头覆盖”这个决策在什么场景下是合理的在什么场景下是危险的什么情况下会考虑用中断的方式发送。欢迎在评论区讨论。版权声明本文首发于CSDN未经授权禁止转载。如果这篇文章对你有帮助欢迎点赞、收藏、留言我会继续分享更多量产项目中验证过的代码设计。