基于CH582M实现CRC-16校验的串口/RS485协议

发布时间:2026/5/24 1:07:31

基于CH582M实现CRC-16校验的串口/RS485协议 文章目录一、 核心逻辑1.核心算法CRC-16 的 C 语言实现与逻辑拆解2. 发送端主机逻辑“算余数贴封条”3. 接收端从机逻辑“掐头去尾重新计算”4. 极简自定义协议帧结构二、 【发送端】核心逻辑与完整代码主控机1. 发送端逻辑“算余数贴封条”2. 发送端完整 Main.c三、 【接收端】核心逻辑与完整代码从机/充电板1. 接收端逻辑“掐头去尾重新计算”2. 接收端完整 Main.c一、 核心逻辑1.核心算法CRC-16 的 C 语言实现与逻辑拆解无论是发送端还是接收端生成和校验防伪标签的核心都是下面这个函数。很多人看到按位异或^和移位就头疼其实它的逻辑非常规律// // 【基础算法】标准 Modbus CRC-16 计算// uint16_tCalc_CRC16(uint8_t*pBuf,uint8_tlen){uint16_tcrc0xFFFF;// 1. 预置 16 位寄存器为全 1for(uint8_ti0;ilen;i){crc^pBuf[i];// 2. 把新拉进来的字节与 CRC 寄存器进行异或for(uint8_tj0;j8;j)// 3. 循环 8 次处理这一个字节的 8 个位{if(crc0x0001){// 判断最低位是否为 1 (是否够除)crc1;// 先将整体向右移 1 位crc^0xA001;// 再与工业多项式 0xA001 进行异或}else{crc1;// 最低位为 0不够除直接右移跳过}}}returncrc;}代码通俗解析为什么初始值是 0xFFFF工业界为了防止全零数据段导致校验失效强制在开始计算前给寄存器“垫”一个全 1 的底类似电子秤的预先去皮。为什么要判断 crc 0x0001 并全部向右移 1正常的数学除法是从左往右算的。但在真实的单片机串口UART / RS485通信中硬件规则是低位先行LSB First。最右边的位会最先发到线缆上。所以代码必须顺应硬件脾气盯紧最右边最低位并不断把数据往右边推。0xA001 是什么它是国际标准 CRC-16 的生成多项式在十六进制下反转过来的“暗号”。当最低位为 1 时说明遇到了硬骨头就拿这个暗号去做一次异或运算相当于除法里的做减法。2. 发送端主机逻辑“算余数贴封条”主机首先准备好要发送的核心数据如包头 地址 功能码。将这串核心数据丢进 CRC-16 算法中计算出一个 16位2字节的校验码余数。把这 2 个字节的校验码通常是低位在前高位在后拼接在核心数据的尾部。加上包尾一并发送出串口。3. 接收端从机逻辑“掐头去尾重新计算”从机通过串口接收到一整帧完整的数据。检查长度、包头、包尾是否合法。关键点从机提取出核心数据部分不含接收到的 CRC 和包尾用一模一样的 CRC 算法自己重新算一遍。将自己算出的 CRC 与数据帧里带过来的 CRC 进行比对。相等则执行动作不等则直接丢弃说明数据在传输路上被干扰了绝不瞎回复。4. 极简自定义协议帧结构本例不使用冗长复杂的标准 Modbus 寄存器读写规则而是针对具体动作如控制继电器启停设计了以下 6 字节极简自定义协议名称字节数示例说明包头10xAA固定起始符地址10x01目标设备地址功能码10x010x01:开启充电0x02:结束充电CRC低位10xXXCRC校验码的低 8 位CRC高位10xXXCRC校验码的高 8 位包尾10x55固定结束符二、 【发送端】核心逻辑与完整代码主控机1. 发送端逻辑“算余数贴封条”主机的任务非常单纯不需要处理复杂的中断只需像打包员一样按规矩发货准备好要发送的核心数据包头AA 目标地址01 功能码01。将这串核心数据丢进 CRC-16 算法中计算出一个 16位2字节的校验码。把这 2 个字节的校验码低位在前高位在后拼接在核心数据的尾部。加上包尾55一并轰出串口。2. 发送端完整Main.c将以下代码放入你的主机工程中直接调用Send_Charge_Command()函数即可指哪打哪。#includeCH58x_common.h// // 【基础算法】标准 Modbus CRC-16 计算// uint16_tCalc_CRC16(uint8_t*pBuf,uint8_tlen){uint16_tcrc0xFFFF;for(uint8_ti0;ilen;i){crc^pBuf[i];for(uint8_tj0;j8;j){// 注意串口是低位先行所以统一右移if(crc0x0001){crc1;crc^0xA001;}else{crc1;}}}returncrc;}// // 【发送动作】打包并发送指令// voidSend_Charge_Command(uint8_ttarget_addr,uint8_tcmd){uint8_tsend_buf[10];uint8_tlen0;// 1. 组装数据前半截send_buf[0]0xAA;// 包头send_buf[1]target_addr;// 目标地址send_buf[2]cmd;// 功能码len3;// 2. 召唤 CRC 算法生成防伪标签uint16_tmy_crcCalc_CRC16(send_buf,len);// 3. 将 CRC 附加在数据后 (小端模式低位在前高位在后)send_buf[len]my_crc0xFF;send_buf[len1](my_crc8)0xFF;// 4. 封上包尾send_buf[len2]0x55;// 5. 串口发送 (总长6字节)UART2_SendString(send_buf,6);}// // 【主机主函数】// intmain(){SetSysClock(CLK_SOURCE_PLL_60MHz);/* --- 串口2硬件初始化 --- */GPIOPinRemap(ENABLE,RB_PIN_UART2);GPIOB_SetBits(GPIO_Pin_23);GPIOB_ModeCfg(GPIO_Pin_22,GPIO_ModeIN_PU);GPIOB_ModeCfg(GPIO_Pin_23,GPIO_ModeOut_PP_5mA);UART2_DefInit();UART2_BaudRateCfg(9600);while(1){// 测试每隔5秒向 0x01 地址发送开启(0x01)指令mDelaymS(5000);Send_Charge_Command(0x01,0x01);}}三、 【接收端】核心逻辑与完整代码从机/充电板1. 接收端逻辑“掐头去尾重新计算”接收端处于被动状态它的核心任务是“防伪对账”绝不能瞎回复通过串口中断结合超时机制接收一整帧数据。检查数据总长度至少6字节并核对包头和包尾。关键点提取出核心数据部分不含接收到的 CRC 和包尾用一模一样的 CRC 算法自己重新算一遍。将单片机自己算的 CRC与发过来的 CRC 进行比对。相等则执行动作并回复不等则直接丢弃数据说明在路上被干扰了。2. 接收端完整 Main.c将以下代码放入你的从机充电板工程中。#includeCH58x_common.huint8_tRxBuff[100];volatileuint8_trecv_len0;volatileuint8_trecv_flag0;uint8_ttrigB;// // 【基础算法】标准 Modbus CRC-16 计算// uint16_tCalc_CRC16(uint8_t*pBuf,uint8_tlen){uint16_tcrc0xFFFF;for(uint8_ti0;ilen;i){crc^pBuf[i];for(uint8_tj0;j8;j){if(crc0x0001){crc1;crc^0xA001;}else{crc1;}}}returncrc;}// // 【从机主函数业务解析大脑】// intmain(){SetSysClock(CLK_SOURCE_PLL_60MHz);/* --- 串口2硬件初始化 --- */GPIOPinRemap(ENABLE,RB_PIN_UART2);GPIOB_SetBits(GPIO_Pin_23);GPIOB_ModeCfg(GPIO_Pin_22,GPIO_ModeIN_PU);GPIOB_ModeCfg(GPIO_Pin_23,GPIO_ModeOut_PP_5mA);UART2_DefInit();UART2_BaudRateCfg(9600);/* --- 开启接收超时中断 --- */UART2_ByteTrigCfg(UART_7BYTE_TRIG);trigB7;UART2_INTCfg(ENABLE,RB_IER_RECV_RDY|RB_IER_LINE_STAT);PFIC_EnableIRQ(UART2_IRQn);while(1){if(recv_flag1){// 1. 长度校验 (带2字节CRC协议最短6字节)if(recv_len6){// 2. 包头包尾特征字校验if(RxBuff[0]0xAARxBuff[recv_len-1]0x55){// 3. 计算本地 CRC (recv_len - 3 代表仅取核心数据)uint16_tmy_crcCalc_CRC16(RxBuff,recv_len-3);// 提取主机发来的 CRCuint16_trecv_crcRxBuff[recv_len-3]|(RxBuff[recv_len-2]8);// 4. 核心对账if(my_crcrecv_crc){// --- 校验通过提取控制指令 ---uint8_tdev_addrRxBuff[1];uint8_tfunc_cmdRxBuff[2];uint8_treply_buf[20];uint8_treply_len0;reply_buf[0]0xBB;reply_buf[1]dev_addr;reply_buf[2]func_cmd;switch(func_cmd){case0x01:// 收到开启指令// TODO: 加入 GPIO 驱动继电器闭合的代码reply_buf[3]0x01;reply_len7;break;case0x02:// 收到结束指令// TODO: 加入 GPIO 驱动继电器断开的代码reply_buf[3]0x01;reply_len7;break;default:reply_len0;break;}// --- 给主机的应答数据也贴上 CRC 封条 ---if(reply_len0){uint16_tsend_crcCalc_CRC16(reply_buf,reply_len-3);reply_buf[reply_len-3]send_crc0xFF;reply_buf[reply_len-2](send_crc8)0xFF;reply_buf[reply_len-1]0x55;UART2_SendString(reply_buf,reply_len);}}}}// 无论对错强制清空接收状态避免死锁recv_len0;recv_flag0;}}}// // 【底层苦力串口中断服务函数】// __INTERRUPT __HIGH_CODEvoidUART2_IRQHandler(void){volatileuint8_ti;switch(UART2_GetITFlag()){caseUART_II_RECV_RDY:// 接收达到触发点for(i0;i!trigB;i){RxBuff[recv_len]UART2_RecvByte();}break;caseUART_II_RECV_TOUT:// 接收超时断包一帧接收完毕iUART2_RecvString(RxBuff[recv_len]);recv_leni;recv_flag1;// 通知主循环开始解析break;}}

相关新闻