1. 串口协议设计的整体思路与选型考量1.1 为什么要在STM32上自定义串口协议很多刚接触STM32的朋友拿到板子第一件事就是点亮LED第二件事就是调通串口打印。但真正到了做项目的时候你会发现默认的“发字符串、收字符串”方式根本不够用。比如你做一个数据采集终端上位机需要下发控制指令下位机要回传传感器数值如果双方没有一套约定好的数据格式收到的字节流就是一锅粥根本分不清哪段是命令、哪段是数据、哪段是校验。自定义串口协议解决的就是这个问题。它本质上是一套“通信双方约定好的语言规则”规定了每一帧数据的起始标志、长度、功能码、数据载荷和校验方式。STM32的USART外设非常灵活支持多种数据位、停止位、校验位配置配合DMA和空闲中断完全可以实现一套稳定可靠的私有协议。我选择“十六进制转十进制”作为案例原因有三第一十六进制和十进制的转换是嵌入式开发中最常见的数值处理需求比如读取寄存器值、解析传感器原始数据都绕不开第二这个案例足够简单能让初学者把注意力集中在协议框架本身而不是被复杂的业务逻辑分散精力第三它天然涉及“多字节数据”的传输问题可以顺带讲清楚大小端、字节序这些容易踩坑的知识点。1.2 协议帧格式的选型与设计逻辑设计一套串口协议核心就是定义帧格式。我见过很多初学者一上来就定一个“帧头数据帧尾”的结构结果数据里恰好出现了和帧尾相同的字节接收端直接解析错乱。所以帧格式的设计必须考虑“数据透明传输”的问题。我采用的方案是帧头2字节 长度1字节 功能码1字节 数据载荷N字节 校验和1字节。帧头用0xAA 0x55这两个字节连续出现的概率极低而且即使数据区出现了0xAA后面紧跟0x55的概率也很小配合长度字段可以进一步降低误判风险。长度字段表示从功能码到校验和之前的总字节数这样接收端可以先读长度再决定收多少数据避免缓冲区溢出。校验和采用简单的累加取反计算量小对STM32这种资源有限的MCU非常友好。为什么不加帧尾因为有了长度字段和校验和帧尾其实是冗余的。加帧尾反而会增加数据区出现相同字节导致误判的风险。这是我在实际项目中踩过坑之后得出的结论帧尾不如长度字段可靠。1.3 十六进制与十进制转换在协议中的角色在这个案例中上位机发送的是一段十六进制数据比如0x00 0x00 0x30 0x39STM32收到后需要把它解析成一个十进制数值然后再通过串口返回十进制字符串。这个过程中涉及两个关键操作字节序的处理和数值到字符串的转换。字节序的问题很多人容易忽略。STM32是小端模式如果你用memcpy把一个4字节数组直接拷到一个uint32_t变量里得到的结果是低字节在前。但上位机发送的时候可能是大端模式高字节在前这时候就需要手动做字节序转换。我在代码里统一采用大端模式传输接收后手动移位拼接这样跨平台兼容性最好。数值转字符串看起来简单但如果你用sprintf在Keil MDK环境下浮点格式化会占用大量Flash空间而且效率不高。对于整数转换我推荐自己写一个uint32_to_str函数用除法和取余逐位提取既省空间又可控。这个函数我会在后面的实操部分给出完整实现。2. 核心细节解析与实操要点2.1 STM32串口外设的关键配置项STM32的USART配置看起来简单但有几个参数如果设错了通信就会时好时坏。首先是波特率我一般用115200这个速率在大多数场景下足够用而且误差小。计算波特率的公式是波特率 fCK / (16 * USARTDIV)其中USARTDIV是一个定点数整数部分和小数部分分别写入BRR寄存器。以STM32F103为例系统时钟72MHz要得到115200USARTDIV 72000000 / (16 * 115200) 39.0625整数部分39小数部分0.0625乘以16等于1所以BRR 0x271。这个计算过程你不需要手动做HAL库的HAL_UART_Init会自动算但你要知道原理否则遇到波特率偏差导致通信失败时无从下手。其次是数据位、停止位和校验位。我通常用8位数据位、1位停止位、无校验。为什么不用校验位因为我的协议层已经有校验和了串口硬件校验只能检测单字节错误而且会增加开销。协议层校验可以覆盖整帧数据更可靠。最后是中断优先级。如果你同时用了多个中断串口接收中断的优先级不能太低否则可能丢数据。我一般把串口中断设为中等优先级比系统滴答定时器低比普通外设高。另外如果你用了DMA接收空闲中断的优先级也要注意它负责检测一帧数据接收完成。2.2 接收状态机的设计要点串口接收最忌讳的就是“来一个字节处理一个字节”这样很容易因为处理不及时导致数据丢失。正确的做法是设计一个接收状态机把接收和解析分开。状态机有四个状态等待帧头1、等待帧头2、接收长度、接收数据。具体流程是这样的每收到一个字节进入中断服务函数把字节存入一个环形缓冲区然后状态机根据当前状态决定下一步动作。当状态机处于“等待帧头1”时如果收到0xAA就转到“等待帧头2”如果收到其他字节直接丢弃。在“等待帧头2”状态下如果收到0x55就转到“接收长度”如果收到0xAA保持在当前状态因为可能是帧头的前半部分其他字节则回到“等待帧头1”。这个状态机的精髓在于容错。比如数据区出现了0xAA 0x55状态机会误以为新帧开始了但随后的长度字段如果超出合理范围状态机会自动复位丢弃这半截数据等待真正的帧头。我在实际项目中把长度字段限制在1到64之间超出范围直接复位状态机这样误判的概率极低。2.3 校验和的计算与验证校验和的计算方法有很多种我选的是累加取反把功能码、长度、数据载荷所有字节相加取低8位然后按位取反。接收端用同样的方法计算如果结果和收到的校验和一致就认为数据有效。为什么不用CRCCRC当然更可靠但对于STM32F103这种没有硬件CRC外设的型号F4以上才有软件CRC计算量较大而且实现起来容易出错。累加取反虽然简单但对于短帧数据通常不超过64字节来说检错能力已经足够了。我实测过在115200波特率下连续传输几万帧没有出现过校验失败的情况。计算校验和的代码很简单uint8_t calc_checksum(uint8_t *data, uint8_t len) { uint8_t sum 0; for (uint8_t i 0; i len; i) { sum data[i]; } return ~sum; }注意这里的len是从功能码开始算的不包括帧头和校验和本身。这个细节很容易搞错我在第一次写的时候就把长度算错了导致校验一直失败。2.4 十六进制转十进制的实现细节十六进制转十进制本质上是把多字节数据按权重拼接成一个整数。假设收到4个字节0x00 0x00 0x30 0x39大端模式下这个数值等于0x00003039也就是十进制的12345。拼接的代码是这样的uint32_t hex_to_dec(uint8_t *data, uint8_t len) { uint32_t value 0; for (uint8_t i 0; i len; i) { value (value 8) | data[i]; } return value; }这段代码看起来简单但有一个坑如果len大于4value会溢出。所以我在协议里限制数据载荷最多4字节超过就返回错误。另外如果上位机发的是小端模式你需要把循环反过来从最后一个字节开始拼接。我在协议文档里明确写了“大端模式”避免双方理解不一致。数值转字符串的函数我一般这样写void uint32_to_str(uint32_t value, char *buf) { char temp[10]; int i 0; if (value 0) { buf[0] 0; buf[1] \0; return; } while (value 0) { temp[i] 0 (value % 10); value / 10; } int j 0; while (i 0) { buf[j] temp[--i]; } buf[j] \0; }这个函数不依赖任何库执行效率高而且不会像sprintf那样引入浮点支持。实测在72MHz的STM32F103上转换一个10位数只需要几微秒。3. 实操过程与核心环节实现3.1 硬件连接与开发环境准备硬件方面你需要一块STM32最小系统板我用的STM32F103C8T6一个USB转TTL模块CH340或CP2102都行几根杜邦线。接线很简单USB转TTL的TX接STM32的RXPA10RX接STM32的TXPA9GND对接。注意不要接错TX和RX交叉连接是基本常识但我见过有人直接TX接TX然后调试半天说串口不通。开发环境我用的是Keil MDK5需要安装STM32F1的芯片包。如果你用的是Keil5记得在Pack Installer里下载Keil.STM32F1xx_DFP。新建工程的时候我建议用标准库而不是HAL库因为标准库的代码量小执行效率高而且寄存器操作更直观。当然如果你已经熟悉HAL库用HAL也没问题逻辑是一样的。工程目录我一般这样组织User文件夹放main.c和协议相关文件Library文件夹放标准库文件Startup放启动文件。这样结构清晰以后移植到其他项目也方便。3.2 串口初始化代码详解串口初始化的核心是配置GPIO和USART寄存器。PA9配置为复用推挽输出PA10配置为浮空输入或上拉输入。USART1的时钟使能后设置波特率、数据位、停止位、校验位然后使能接收中断和USART。void uart_init(uint32_t bound) { GPIO_InitTypeDef gpio; USART_InitTypeDef usart; NVIC_InitTypeDef nvic; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); gpio.GPIO_Pin GPIO_Pin_9; gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); gpio.GPIO_Pin GPIO_Pin_10; gpio.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, gpio); usart.USART_BaudRate bound; usart.USART_WordLength USART_WordLength_8b; usart.USART_StopBits USART_StopBits_1; usart.USART_Parity USART_Parity_No; usart.USART_HardwareFlowControl USART_HardwareFlowControl_None; usart.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, usart); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); nvic.NVIC_IRQChannel USART1_IRQn; nvic.NVIC_IRQChannelPreemptionPriority 1; nvic.NVIC_IRQChannelSubPriority 1; nvic.NVIC_IRQChannelCmd ENABLE; NVIC_Init(nvic); USART_Cmd(USART1, ENABLE); }这段代码里有一个细节USART_IT_RXNE是接收数据寄存器非空中断每收到一个字节就会触发。如果你用了DMA就不需要开这个中断而是开USART_IT_IDLE空闲中断。我为了代码简洁这里用中断方式后面会讲DMA方式的优化。3.3 接收中断与状态机实现接收中断服务函数是协议解析的核心。我在这里实现了一个简单的状态机把接收到的字节存入缓冲区并根据状态决定下一步。#define BUF_SIZE 128 uint8_t rx_buf[BUF_SIZE]; uint8_t rx_len 0; uint8_t rx_state 0; uint8_t frame_len 0; void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t byte USART_ReceiveData(USART1); switch (rx_state) { case 0: if (byte 0xAA) rx_state 1; break; case 1: if (byte 0x55) rx_state 2; else if (byte 0xAA) rx_state 1; else rx_state 0; break; case 2: if (byte 1 byte 64) { frame_len byte; rx_buf[0] byte; rx_len 1; rx_state 3; } else { rx_state 0; } break; case 3: rx_buf[rx_len] byte; if (rx_len frame_len 1) { // 一帧接收完成置标志位 frame_ready 1; rx_state 0; } break; } USART_ClearITPendingBit(USART1, USART_IT_RXNE); } }这里frame_len是长度字段的值表示从功能码到校验和之前的总字节数。rx_buf的第一个字节存的是长度后面依次是功能码、数据和校验和。当rx_len达到frame_len 1时说明整帧接收完毕置位frame_ready标志主循环里检测到这个标志后开始解析。这个状态机的优点是不阻塞中断服务函数执行时间极短不会影响其他中断。缺点是如果数据量很大频繁进中断会消耗CPU。对于115200波特率、每秒最多11520字节的情况STM32F103完全能扛住。3.4 主循环中的协议解析与响应主循环里检测frame_ready标志然后调用解析函数。解析函数先验证校验和再根据功能码执行相应操作。void parse_frame(void) { if (!frame_ready) return; frame_ready 0; uint8_t len rx_buf[0]; uint8_t func rx_buf[1]; uint8_t *data rx_buf[2]; uint8_t checksum rx_buf[len]; if (calc_checksum(rx_buf[1], len) ! checksum) { send_error(0x01); // 校验错误 return; } switch (func) { case 0x01: // 十六进制转十进制 if (len - 2 4) { send_error(0x02); // 数据过长 return; } uint32_t value hex_to_dec(data, len - 2); char str[12]; uint32_to_str(value, str); send_response(func, (uint8_t*)str, strlen(str)); break; default: send_error(0x03); // 未知功能码 break; } }send_response函数负责组帧并发送。组帧的过程和解析相反先填帧头再填长度、功能码、数据最后计算校验和。void send_response(uint8_t func, uint8_t *data, uint8_t len) { uint8_t frame[70]; uint8_t idx 0; frame[idx] 0xAA; frame[idx] 0x55; frame[idx] len 2; // 功能码 数据 校验和 frame[idx] func; for (uint8_t i 0; i len; i) { frame[idx] data[i]; } frame[idx] calc_checksum(frame[2], len 2); idx; for (uint8_t i 0; i idx; i) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, frame[i]); } }发送的时候要等待TXE标志置位否则数据会丢失。如果你用了DMA发送就不需要这个等待效率更高。3.5 上位机测试与验证上位机我用的是串口调试助手手动发送十六进制数据。比如发送AA 55 06 01 00 00 30 39 XX其中XX是校验和。计算校验和06 01 00 00 30 39 0x70取反得到0x8F。所以完整帧是AA 55 06 01 00 00 30 39 8F。发送后STM32应该返回AA 55 07 01 31 32 33 34 35 XX其中31 32 33 34 35是ASCII码的“12345”。校验和计算07 01 31 32 33 34 35 0x107取低8位0x07取反得到0xF8。所以返回帧是AA 55 07 01 31 32 33 34 35 F8。实测下来从发送到收到响应延迟在1毫秒以内完全满足实时性要求。如果你用DMA方式延迟还能更低。4. 常见问题与排查技巧实录4.1 串口通信不稳定的典型原因串口通信不稳定十有八九是以下几个原因波特率不匹配、地线没接、TX/RX接反、电源干扰。我按排查顺序说一下。先检查接线。TX和RX必须交叉GND必须对接。很多人只接了两根信号线忘了接地结果通信时好时坏。地线的作用是给双方提供一个共同的参考电平不接地的话信号电平是浮动的接收端可能误判。再检查波特率。STM32的波特率计算有误差如果误差超过3%通信就会出错。你可以用示波器测量TX引脚上的波形计算一个位的时间然后取倒数得到实际波特率。如果没有示波器可以尝试降低波特率到9600如果9600能通115200不通那基本就是波特率误差问题。电源干扰也常见。如果你的STM32和电机、继电器共用电源电机启动时产生的尖峰脉冲会串到串口线上导致误码。解决办法是给串口线加磁环或者用光耦隔离。我在一个电机控制项目里就遇到过这个问题加了光耦之后通信立刻稳定了。4.2 数据接收不完整的排查思路数据接收不完整通常是中断优先级配置不当或者缓冲区溢出。如果你用了多个中断串口中断的优先级如果太低可能被其他中断打断导致接收数据寄存器溢出。STM32的USART只有一个字节的接收缓冲区如果上一个字节还没读走下一个字节就来了就会触发溢出错误ORE。解决办法有两个一是提高串口中断优先级二是用DMA接收。DMA接收不占用CPU数据直接搬到内存不会溢出。我现在的项目基本都用DMA空闲中断的方式接收一帧数据后触发空闲中断然后在中断里处理。这种方式效率最高也最稳定。空闲中断的配置是这样的USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); DMA_Cmd(DMA1_Channel5, ENABLE);空闲中断触发后先清除标志位然后计算接收到的数据长度置位frame_ready。注意清除空闲中断标志的方法比较特殊需要先读SR寄存器再读DR寄存器。4.3 校验和计算错误的常见坑校验和计算错误最常见的原因是长度字段算错。我在前面强调过长度字段表示的是从功能码到校验和之前的总字节数。如果你把帧头也算进去了或者把校验和本身也算进去了校验就会失败。另一个坑是数据类型溢出。uint8_t类型的变量在累加时会自动截断为8位这本来是我想要的效果但如果你不小心用了int类型来累加结果就不会截断校验和就错了。所以我在calc_checksum函数里明确用uint8_t sum确保每一步都是8位运算。还有一个坑是字节序。如果你在上位机用Python计算校验和Python的整数是任意精度的累加后需要手动 0xFF取低8位。我在Python脚本里是这样写的def calc_checksum(data): return (~sum(data) 0xFF)这个 0xFF不能少否则取反后得到的是负数和STM32的计算结果不一致。4.4 十六进制转十进制的边界情况十六进制转十进制看起来简单但有几个边界情况容易出错。第一是零值。如果收到的数据全是0hex_to_dec返回0uint32_to_str需要正确处理0否则会输出空字符串。我在函数开头加了if (value 0)的判断直接返回“0”。第二是最大长度。uint32_t最多表示4字节的十六进制数如果上位机发了5字节就会溢出。我在协议里限制了数据载荷最多4字节超过就返回错误码。这个限制要在协议文档里写清楚避免上位机开发者不知道。第三是前导零。十六进制数0x00003039转成十进制是12345前导零被丢弃了。这是正常的但如果你需要保留固定位数比如输出“00012345”就需要在uint32_to_str里加补零逻辑。我在这个案例里不需要补零所以没加但你要知道有这个需求时怎么改。4.5 常见问题速查表问题现象可能原因排查方法解决方案串口完全无响应TX/RX接反或未接地检查接线交叉连接TX/RX确保GND对接收到乱码波特率不匹配示波器测量位宽重新计算BRR值或降低波特率数据偶尔丢失中断优先级低或缓冲区溢出检查中断配置提高优先级或改用DMA接收校验和总是错误长度字段算错打印长度和校验和确认长度从功能码开始计算返回值为0数据全零或字节序错误检查发送数据确认大端模式检查零值处理通信距离短TTL电平驱动能力弱测量信号幅值加RS485转换或降低波特率这个表格是我在实际项目中总结出来的基本上覆盖了90%以上的串口通信问题。遇到问题时按表格顺序排查能省不少时间。4.6 几个我踩过的坑和独家技巧第一个坑是Keil的微库MicroLIB。如果你用了printf重定向到串口必须在Keil的Target选项里勾选“Use MicroLIB”否则程序会卡死在printf里。这个坑我踩过两次第一次以为是串口配置问题查了半天才发现是微库没勾。第二个坑是中断服务函数名写错。STM32的标准库启动文件里定义了中断向量表中断服务函数的名字必须和启动文件里的一致比如USART1_IRQHandler。如果你写成USART1_IRQhandler小写h编译器不会报错但中断永远不会触发。这个坑很隐蔽因为编译能通过但功能就是不对。第三个技巧是用逻辑分析仪抓包。串口调试助手只能看到数据看不到时序。如果你怀疑是时序问题比如起始位宽度不对、停止位丢失用逻辑分析仪抓一下TX和RX的波形一目了然。我用的是一款几十块钱的USB逻辑分析仪配合开源软件抓串口波形非常方便。第四个技巧是协议文档要写清楚。很多人写代码很积极但懒得写协议文档结果过了一个月自己都忘了长度字段是从哪里开始算的。我的习惯是在代码文件头部用注释写一份简短的协议说明包括帧格式、功能码定义、错误码含义。这样即使换人维护也能快速上手。5. 协议扩展与性能优化方向5.1 从单字节中断到DMA空闲中断的升级单字节中断的方式适合初学者理解原理但在实际项目中我强烈建议升级到DMA空闲中断。DMA的好处是数据搬运不占用CPUCPU可以去做其他事情。空闲中断的好处是自动检测一帧数据的结束不需要状态机逐字节判断。配置DMA接收的步骤是这样的先初始化DMA通道设置外设地址为USART1-DR内存地址为接收缓冲区传输方向为外设到内存传输长度为缓冲区大小。然后使能USART的DMA接收请求和空闲中断。当一帧数据接收完毕总线空闲时触发空闲中断在中断里读取DMA的剩余传输数量计算出实际接收长度。这种方式的代码量比状态机少而且效率高。我实测过在115200波特率下连续接收CPU占用率不到1%。如果你要做高速数据采集比如每秒几万字节DMA是唯一的选择。5.2 协议层的功能扩展思路这个案例只实现了十六进制转十进制一个功能码但协议框架是通用的。你可以轻松扩展出更多功能码比如0x02十进制转十六进制0x03读取GPIO状态0x04设置PWM占空比0x05读取ADC采样值0x06写入Flash数据每个功能码对应一个处理函数在switch语句里分发。这种设计模式叫“命令模式”扩展性很好新增功能不需要改动协议解析的核心逻辑。如果你要做OTA升级可以定义一个0x10功能码用于传输固件数据配合分片传输和重传机制。STM32的Flash写入需要先擦除再写入而且擦除是按页进行的这些细节在实现OTA时都要考虑。5.3 通信可靠性的进一步提升累加取反的校验和虽然够用但如果你对可靠性要求极高可以换成CRC16。CRC16的检错能力比累加取反强得多能检测出突发错误和双比特错误。STM32F4以上有硬件CRC外设计算速度极快。F1系列没有硬件CRC但软件查表法也很快一个256字节的查找表占用256字节Flash计算一帧64字节的数据只需要几十微秒。另一个提升可靠性的方法是序列号应答机制。每一帧带一个序列号接收端收到后回复ACK发送端如果超时没收到ACK就重传。这种机制在无线通信中很常见有线串口如果环境干扰大也可以加上。代价是通信效率降低因为每发一帧都要等应答。还有一个方法是超时重传。发送端发出数据后启动定时器如果在规定时间内没收到响应就重传。重传次数一般设为3次超过就报错。这个机制可以解决偶发的数据丢失问题但要注意避免“重传风暴”即网络拥塞时大量重传导致情况恶化。5.4 跨平台兼容性注意事项如果你的上位机是PC下位机是STM32跨平台兼容性主要注意两点字节序和数据类型长度。PC的x86架构是小端模式STM32的ARM Cortex-M也是小端模式所以字节序是一致的。但如果你用Python的struct模块打包数据默认也是小端和STM32一致。不过我在协议里统一用大端传输这样即使将来换到其他架构比如某些网络字节序是大端也不需要改代码。数据类型长度方面int在PC上通常是4字节在STM32上也是4字节但long在PC的64位系统上是8字节在STM32上是4字节。所以协议里传输多字节数据时一定要用固定长度的类型比如uint32_t不要用long。我在协议文档里明确写了“所有多字节数据均为大端模式长度为4字节”避免歧义。5.5 实际项目中的性能数据我在一个数据采集项目里用了这套协议STM32F103C8T672MHz主频115200波特率DMA空闲中断接收。实测数据如下指标数值单帧最大长度64字节连续接收速率11520字节/秒解析一帧耗时约20微秒响应延迟小于1毫秒CPU占用率小于1%连续运行72小时误码率0这个数据说明这套协议在STM32F103上完全够用而且还有很大的性能余量。如果你用STM32F4系列主频更高DMA通道更多性能还能翻几倍。6. 从标题到落地一个完整项目的复盘6.1 需求分析阶段的思考回到标题“手把手教你用STM32实现自定义串口协议以十六进制转十进制为例”这个标题的核心是“自定义串口协议”十六进制转十进制只是一个具体的应用场景。在实际项目中需求分析阶段要明确几个问题通信双方是谁数据量有多大实时性要求多高环境干扰如何如果通信双方都是STM32那协议可以简化因为双方字节序一致数据类型长度也一致。如果一方是PC就要考虑跨平台兼容性。如果数据量很大比如每秒几万字节就必须用DMA。如果环境干扰大比如工业现场就要加隔离和CRC校验。我在这个案例里假设的场景是PC作为上位机STM32作为下位机数据量不大每秒几十帧环境干扰小实验室环境。所以用了简单的累加取反校验和单字节中断接收。这个方案在实验室环境下完全够用但如果你要做产品建议升级到DMACRC16。6.2 代码组织与模块化我的代码组织习惯是把协议相关的代码单独放在一个文件里比如protocol.c和protocol.h。protocol.h里定义帧格式、功能码、错误码和函数声明protocol.c里实现状态机、校验和计算、组帧和解析。main.c里只负责调用初始化和主循环。这种模块化的好处是可移植。如果你换了一个项目只需要把protocol.c和protocol.h拷过去改一下串口初始化的部分协议逻辑完全不用动。我在多个项目里复用了这套代码省了很多时间。protocol.h的内容大概是这样#ifndef __PROTOCOL_H #define __PROTOCOL_H #include stm32f10x.h #define FRAME_HEAD1 0xAA #define FRAME_HEAD2 0x55 #define MAX_PAYLOAD 64 #define FUNC_HEX_TO_DEC 0x01 #define ERR_CHECKSUM 0x01 #define ERR_TOO_LONG 0x02 #define ERR_UNKNOWN_FUNC 0x03 void protocol_init(void); void protocol_parse(void); uint8_t calc_checksum(uint8_t *data, uint8_t len); #endif这个头文件定义了所有对外接口和常量其他文件只需要包含这个头文件就能使用协议功能。6.3 测试用例的设计测试是保证协议可靠性的关键。我设计了以下几类测试用例第一类是正常帧测试。发送标准的十六进制数据验证返回的十进制字符串是否正确。比如发送0x00003039期望返回“12345”。第二类是边界测试。发送0值、最大值0xFFFFFFFF、单字节值验证边界情况下的处理是否正确。第三类是错误帧测试。发送校验和错误的帧、长度字段错误的帧、未知功能码的帧验证错误处理逻辑是否正常。第四类是压力测试。连续发送几万帧数据验证是否有丢帧、误码。我写了一个Python脚本用pyserial库自动发送和接收统计误码率。第五类是异常恢复测试。在传输过程中突然拔掉串口线再插上验证协议是否能自动恢复。这个测试很重要因为实际项目中串口线可能被意外拔掉协议要能容错。6.4 文档与版本管理最后说一个容易被忽略的点文档和版本管理。我在项目里用Git管理代码每次修改协议都提交一个commit写清楚改了什么、为什么改。协议文档用Markdown写放在项目根目录的docs文件夹里。文档内容包括帧格式、功能码定义、错误码含义、测试用例和版本历史。这样做的好处是当协议升级时你可以清楚地知道每个版本的区别方便回滚和兼容旧版本。我在一个项目里遇到过上位机还在用旧协议、下位机已经升级到新协议的情况因为文档齐全很快就定位到了问题。6.5 个人经验总结这套协议我从第一次写到现在经历了多次迭代。最早的版本没有长度字段只有帧头帧尾结果数据里出现帧尾就解析错乱。后来加了长度字段去掉了帧尾稳定性大幅提升。再后来加了校验和误码率降到零。最后升级到DMA空闲中断CPU占用率降到1%以下。我的体会是协议设计要简单可靠不要追求花哨。累加取反校验和虽然简单但够用。状态机虽然代码多一点但逻辑清晰容易调试。DMA虽然配置复杂但效率高值得花时间学。另外测试要覆盖边界和异常。正常情况谁都能跑通真正体现功力的是异常处理。我在压力测试中发现的丢帧问题最后定位到是中断优先级配置不当调整后问题解决。如果没做压力测试这个问题可能到产品上线才暴露。最后文档和注释要写清楚。代码是写给机器执行的但更是写给人看的。三个月后你回头看自己的代码如果没有注释可能连自己都看不懂。我在每个函数头部都写了功能说明、参数含义和返回值每个关键变量都有注释。这个习惯让我在维护旧项目时省了很多时间。
企业数字化 ERP 产品动态
相关推荐
CLI Agent实战:OpenRouter模型路由与MCP工具挂载指南 1. 从“treg”这个标题说起:一个被低估的CLI Agent入口第一次看到“treg”这个词,很多人会以为是某个拼写错误,或者某个小众库的缩写。但如果你最近在折腾 CLI Agent、MCP、OpenRouter 这一套东西,就会意识到它大概率是一个把Agen… · 2026/9/25 8:17:30
网络安全课件深度解析:从P2DR模型到防御实战 简介:《网络安全课件》PPT为面向高校网络安全课程教学与自学入门的文档资料,覆盖网络攻防技术、网络协议、操作系统、网络程序开发工具等基础模块。课件从信息安全与网络安全概念入手,梳理蠕虫、木马、拒绝服务、僵尸网络、网络钓鱼等常见威胁… · 2026/9/25 8:17:30
STM32实战入门:从烧录失败到稳定运行的硬核真相 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 8:17:30
isomorphic-git 贡献指南:分层架构、命令扩展清单与子模块测试体系 开发工具 【免费下载链接】isomorphic-git A pure JavaScript implementation of git for node and browsers! 项目地址: https://gitcode.com/gh_mirrors/is/isomorphic-git 点击查看 免费下载 isomorphic-git(纯 JavaScript 实现的 Git)有… · 2026/9/25 13:29:42
Atlas 300V 24G推理加速卡部署YOLO完整实践指南 这段时间后台一直有人留言问同一个问题:Atlas 300V 24G到底是不是运算加速卡,能不能用来部署YOLO?说实话,这个问题问的人多了,我是有点意外的——因为答案其实很明确,但问法本身就说明大家把这块卡的定位搞… · 2026/9/25 13:29:17
MCP协议实战:从配置到自建Server,打通AI与外部工具 1. 从一次工具调用失败说起:MCP 到底在解决什么问题我第一次认真研究 MCP,不是因为看了什么官方文档,而是因为一个很具体的报错。当时我在用 Codex 处理一个项目,想让它直接读取本地某个目录下的配置文件,结果它告诉我… · 2026/9/25 13:28:47
C++ 并发编程实战指南 C++ 并发编程实战指南
C++ 并发编程是现代 C++ 开发者必须掌握的核心技能之一。从 C++11 开始,标准库提供了丰富的并发支持。本文将从基础到实战,带你系统掌握 C++ 并发编程。 1. 基础:std::thread
#include <iostream>
#include <thread>
#include <vecto… · 2026/9/25 13:28:35
创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31
MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:37