上一小节分享了在串口中断中进行实时协议解析的一种处理方法,仅适用于一些需要对串口指令实时处理的特殊场合。大部分情况下,串口中断服务函数中是不建议太多的处理代码的,一般可以在串口中断中完成数据的接收,通过串口中断将数据接收到数据缓冲区中【最好的方式是采用环形队列】,本节代码即是采用超时接收的处理方式,在串口中断中进行数据的接收,在定时中断中进行断帧,实现数据的接收【前面小节介绍过此方法】,然后在主程序中对数据协议进行解析处理。本小节代码和前面不同的地方在于舍弃了帧头帧尾的判断,增加了CRC校验的功能,自定义的协议格式,参考的是MODBUS通信协议进行设计的。因此本小节的代码,也可以用于RS485通信中,只是不是标准的MODBUS协议罢了,是自定义的一种协议。关于自定义协议的分析,大家可以查找网络上的相关文档进行了解,也可以参看本人的视频课程:串口中断即时解析用户自定义通讯协议(接收数据字节固定)的编程,视频地址链接:https://www.ixigua.com/6846389376847610376中的具体介绍。另外,如果协议指令只是用于控制如一个单片机控制板中的不同的器件时,如多路继电器等,也可以省略地址信息,如果用于多从机的控制,可以加上地址,大家可以根据具体的应用场合举一反三,适当的调整协议指令格式,灵活处理。本小节代码对应的单片机视频课程为:串口超时接收用户自定义通讯协议(基于CRC16校验)的编程实现,视频课程地址链接为:https://www.ixigua.com/6847871377668571652。话不多说,下面是具体的代码,大家可以适当的参考下,有不当之处的欢迎评论区留言讨论。

/* ****************************************** * * 文件名:main.c * 描 述:主程序 * 版本号: * 备 注: ****************************************** */ /************************************************************************************ 功能:带CRC校验的 自定义通讯协议 01 01 02 03 CRC16/MODBUS 地址 功能字 数据 校验字 例如: 01 01 80 80 30 78 LED点亮 0x8080毫秒 01 02 80 80 C0 78 蜂鸣器鸣响 0x8080毫秒 crc16.c中的CRC校验算法计算得到的CRC校验字和在线工具中 计算的CRC校验字高低字节正好相反。 CRC在线计算工具网址:http://www.ip33.com/crc.html 可以根据自己的应用需求进行协议的扩展,如控制多路继电器, 同时还可以扩展到RS485多从机系统中使用 **************************************************************************************/ //头文件 #include <reg51.h> #include "delay.h" #include "uart.h" #include "CRC16.h" #include "time.h" //本机地址 #define LOCAL_ADRRESS 0x01 void main(void) { unsigned char i; unsigned int crc; unsigned char crch,crcl; Timer0Init();//定时器初始化 UartInit();//串口初始化 EA = 1;//开总中断 DelayXms(10); while(1) { if(recv_flag)//接收到数据 { recv_flag = 0;//清零接收标志位 start_timer = 0;//关软件定时器 //校验本机地址,是本机的,则接收处理,否则退出 if(recv_buf[0] != LOCAL_ADRRESS) { return; } //CRC16校验,校验正确,我们才处理,否则直接返回,并给出错误码 crc = GetCRC16(recv_buf,recv_cnt - 2); crch = crc >> 8; crcl = crc & 0xFF; if((recv_buf[recv_cnt - 2] != crch) || (recv_buf[recv_cnt - 1] != crcl)) { recv_buf[1] = recv_buf[1] | 0x80; crc = GetCRC16(recv_buf,recv_cnt - 2); recv_buf[4] = crc & 0xFF; recv_buf[5] = crc >> 8; for(i = 0;i<recv_cnt;i ) { sendByte(recv_buf[i]); } return; } switch(recv_buf[1])//命令字/功能字判断 { case 1:led_data = recv_buf[2]; led_data = led_data << 8; led_data = led_data recv_buf[3];//取数据 led_cnt = 0;//目的是使LED点亮上述接收的数据的时间 break; case 2:beep_data = recv_buf[2]; beep_data = beep_data << 8; beep_data = beep_data recv_buf[3];//取数据 beep_cnt = beep_data;//蜂鸣器鸣响时间数据 break; default:break; } //返回接收数据 测试用 for(i = 0;i<recv_cnt;i ) { sendByte(recv_buf[i]); } clr_recvbuffer(recv_buf);//清除缓冲buffer recv_cnt = 0; } } }

/* ****************************************** * * 文件名:time.c * 描 述:定时/计数器0初始化及中断处理 * 版本号: * 备 注: ****************************************** */ #include "time.h" #include "uart.h" //全局变量定义 unsigned int led_data; unsigned int beep_data; unsigned int led_cnt; unsigned int beep_cnt; void Timer0Init(void) //1毫秒@11.0592MHz { TMOD &= 0xF0; //设置定时器模式 TMOD |= 0x01; //设置定时器模式 TL0 = 0x66; //设置定时初值 TH0 = 0xFC; //设置定时初值 TF0 = 0; //清除TF0标志 ET0 = 1; TR0 = 1; //定时器0开始计时 } void timer0_ISR() interrupt 1 { TR0 = 0; if(start_timer == 1) { recv_timer_cnt ;//1、累加定时时间计数器 if(recv_timer_cnt > MAX_REV_TIME)//2、判断定时时间是否超过了设定的最大的阈值, //超过则说明等待一段时间后没有新的数据到,我们 //判断一包数据接收完毕 { recv_timer_cnt = 0;//3、清除定时计数器 处理数据 清除buffer(放到数据处理之后) recv_flag = 1; } } //LED点亮时间控制 if(led_cnt < led_data) { led_cnt ; LED = 0; } else { LED = 1; } //蜂鸣器鸣响时间控制 if(beep_cnt != 0) { beep_cnt--; BEEP = ~BEEP; } TL0 = 0x66; //设置定时初值 TH0 = 0xFC; //设置定时初值 TR0 = 1; }

//time.h #ifndef __TIME_H__ #define __TIME_H__ #include <reg51.h> sbit LED = P1^0; sbit BEEP = P3^7; extern unsigned int led_data; extern unsigned int beep_data; extern unsigned int led_cnt; extern unsigned int beep_cnt; void Timer0Init(void); #endif

/* ************************************** * * 文件名:uart.c * 描 述:串口处理 * 版本号: * 备 注: ************************************** */ #include "uart.h" unsigned char recv_flag = 0; unsigned char start_timer = 0; unsigned char recv_buf[MAX_REV_NUM]; unsigned char recv_cnt; unsigned char recv_timer_cnt; void UartInit(void) //9600bps@11.0592MHz { PCON &= 0x7F; //波特率不倍速 SCON = 0x50; //8位数据,可变波特率 TMOD &= 0x0F; //清除定时器1模式位 TMOD |= 0x20; //设定定时器1为8位自动重装方式 TL1 = 0xFD; //设定定时初值 TH1 = 0xFD; //设定定时器重装值 ET1 = 0; //禁止定时器1中断 ES = 1; TR1 = 1; //启动定时器1 } void sendByte(unsigned char dat) { SBUF = dat; while(!TI); TI = 0; } void sendString(unsigned char *dat)//Hello World! { while(*dat != '\0') { sendByte(*dat ); } } char putchar(char c) { sendByte(c); return c; } void clr_recvbuffer(unsigned char *buf) { unsigned char i; for(i = 0;i<MAX_REV_NUM;i ) { buf[i] = 0; } } /************************************* 1、中断服务函数一定是一个没有返回值的函数 2、中断服务函数一定是没有参数的函数 3、中断服务函数函数名后跟关键字 interrupt 4、interrupt n 0 - 4 5个中断源 8*n 0003H 0003H INT0 000BH T0 0013H INT1 001BH T1 0023H ES 5、中断服务函数不能北主程序或其他程序所调用 6、 n 后面 跟 using m (0 - 3 )工作寄存器组 ***************************************/ void uart_ISR() interrupt 4 { if(RI) { RI = 0; start_timer = 1;//1、每接收一帧数据的时候,打开软件定时器,去计数 if(recv_cnt < MAX_REV_NUM) { recv_buf[recv_cnt] = SBUF;//2、接收数据到数据缓冲区,注意缓冲区的大小范围问题 recv_cnt ; } else { recv_cnt = MAX_REV_NUM; } recv_timer_cnt = 0;//3、每接收一帧数据,记得把定时计数清零 相当于是喂狗信号 //但是在定时中断里面会不断累加 } if(TI) { TI = 0; } }

//uart.h #ifndef __UART_H__ #define __UART_H__ #include <reg51.h> #include <stdio.h> #define MAX_REV_NUM 20 #define MAX_REV_TIME 5 extern unsigned char start_timer; extern unsigned char recv_buf[MAX_REV_NUM]; extern unsigned char recv_cnt; extern unsigned char recv_timer_cnt; extern unsigned char recv_flag; void UartInit(void); void sendByte(unsigned char dat); void sendString(unsigned char *dat); void clr_recvbuffer(unsigned char *buf); #endif

/* ********************* * * 文件名:CRC16.c * 描 述:通用的CRC16校验算法文件 * 版本号:v1.0 * 备 注:此文件代码源自网络 ******************************************************************************* */ #include "CRC16.h" /* CRC16计算函数,ptr-数据指针,len-数据长度,返回值-计算出的CRC16数值 */ unsigned int GetCRC16(unsigned char *ptr, unsigned char len) { unsigned int index; unsigned char crch = 0xFF; //高CRC字节 unsigned char crcl = 0xFF; //低CRC字节 unsigned char code TabH[] = { //CRC高位字节值表 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40 } ; unsigned char code TabL[] = { //CRC低位字节值表 0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7, 0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09, 0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC, 0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3, 0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32, 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4, 0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A, 0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 0x2D, 0xED, 0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26, 0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60, 0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F, 0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68, 0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA, 0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5, 0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0, 0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C, 0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89, 0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C, 0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83, 0x41, 0x81, 0x80, 0x40 } ; while (len--) //计算指定长度的CRC { index = crch ^ *ptr ; crch = crcl ^ TabH[index]; crcl = TabL[index]; } return ((crch<<8) | crcl); }

//CRC16.h #ifndef __CRC16_H__ #define __CRC16_H__ unsigned int GetCRC16(unsigned char *ptr, unsigned char len); #endif

delay.c 和delay.h前期文章中已经分享过,本节不再单独贴出。通过Proteus仿真和虚拟串口,可以对上述的代码功能进行简单的测试,当然也可以通过开发板或自制通信板进行相关的测试,具体的测试结果如下所示:

串口超时接收用户自定义通讯协议(串口超时接收用户自定义通讯协议)(1)

LED灯点亮命令测试

串口超时接收用户自定义通讯协议(串口超时接收用户自定义通讯协议)(2)

蜂鸣器鸣响命令测试

串口超时接收用户自定义通讯协议(串口超时接收用户自定义通讯协议)(3)

CRC校验错误测试

串口通信的相关代码分享告一段落,其他串口相关的介绍,可以关注后,查看主页单片机视频集合中的单片机应用实践篇中的相关视频内容,其他的几个视频的代码就不再单独贴出了,因为根据最近几篇文章分享的模块化代码,很容易的就可以根据视频的功能要求,调试出来相应的程序来,没有必要每个视频都贴出源码的。另外视频代码仅供大家参考,Bug在所难免,主要是示例程序代码,很多的功能并不完善,希望大家可以根据这种编程思路,进行相应的拓展。主要是感觉串口通信自定义协议是一个重点,也是一个难点,很多初学的同学,对此会感觉比较难,实际上,如果理解透彻了,也就不觉得难了的。同时,串口如果掌握好了,后面很多的器件或模块接口都是采用串口通信,学习起来也就轻松了许多,比如蓝牙模块,WIFI模块,4G模组,RFID等。所以,有必要把串口好好的学一下,欢迎大家关注,留言讨论。

,