上一节分享了通过判读数据帧头来实现接收一串数据并解析的程序,不过上次的串口程序框架是基于定时器中断+串口中断,通过超时接收来实现一包数据的接收,也就是先实现串口数据接收到一自定义数据缓冲区数组中,然后在主程序中进行数据的判断解析和处理。这一程序的框架是我们在日常串口程序的编程中经常使用的,但是对于一些特殊的场合,如需要对串口发送过来的协议数据进行实时处理时,就有可能会因为主程序中其他的任务而影响其实时性。因为我们举例时,只是在介绍相关的处理方法,主程序中并没有其他的额外的任务需要处理,因此体现不出来。但是我们在实际的项目开发中,不可能只处理串口数据,一般情况下还有数据采集任务,显示任务,按键任务,控制任务,数据处理任务等等。因此,如果在任务比较多的情况下,又需要对串口发送过来的指令进行实时的处理,超时接收的串口处理机制,就不如本节介绍的在串口中断中即时解析数据的通信程序框架来的实用。当然,大家在平时应用时,需要根据具体的情况进行选择,各有利弊,没有最好的程序,只有适用的程序,只要能够很好的完成项目的需求,并且可以连续稳定可靠运行的代码,个人认为就是最好的代码,不是吗?本节代码对应的视频课程为:单片机应用实践篇之串口中断中即时解析数据帧头的通信程序,视频链接为:
https://www.ixigua.com/6845268053077262862。好了,话不多说,代码如下,此次代码分为main.c,time.c,time.h,uart.c,uart.h,delay.c,delay.h【delay.c和delay.h前面的小节已经贴过代码,不再贴出】。这点和视频中有所区别,但仅仅是重新划分了模块,做了相应的模块化处理,实现的功能是一致的,是直接对课程视频的代码进行的再加工处理,方便大家理解和阅读。
//main.c/*************************************************************************程序功能:在串口中断中即时解析数据协议的通信程序 数据协议:帧头【2字节】 命令【1字节】 数据【2字节】 帧尾【1字节】 AA 55 01 xx xx 0D 例如: 上位机发送:AA 55 01 08 80 0D LED点亮 0x0880 ms 上位机发送:AA 55 02 08 80 0D 蜂鸣器鸣响 0x0880 ms 下位机返回:AA 55 01 08 80 0D/AA 55 02 08 80 0D 可以根据自己的需求,适当更改或扩充协议,完成特定的功能****************************************************************************/#include <reg51.h>#include "delay.h"#include "uart.h"#include "time.h"void main(){ Timer0Init();//定时器初始化 UartInit();//串口初始化 EA = 1;//开总中断 //提示信息 printf("Wait for Serial Communication Test Start.rn"); printf("Please Send a Command:rn"); DelayXms(10); while(1) { if(recv_flag == 1)//接收数据包完成 { recv_flag = 0;//清除标志位,防止重复触发处理 sendString(recv_buf);//发送回去,测试用 clr_recvbuffer(recv_buf);//清空缓冲区 } }}//uart.c#include "uart.h"#include "time.h"//全局变量定义unsigned char recv_buf[MAX_REV_NUM];//接收缓冲区unsigned char recv_cnt;//接收计数器unsigned char recv_flag;//接收数据标志位unsigned char machine_step = 0;//接收状态步骤变量unsigned int beep_data;//蜂鸣器鸣响时间中间变量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){ while(*dat != ' ') { sendByte(*dat++); }}//printf重定向处理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 ES5、中断服务函数不能北主程序或其他程序所调用6、 n 后面 跟 using m (0 - 3 )工作寄存器组***************************************/void uart_ISR() interrupt 4{ if(RI) { RI = 0; switch(machine_step)//状态机处理 { case 0:recv_buf[0] = SBUF; if(recv_buf[0] == 0xAA)//帧头 { machine_step = 1; }else { machine_step = 0; } break; case 1:recv_buf[1] = SBUF; if(recv_buf[1] == 0x55)//帧头 { machine_step = 2; recv_cnt = 2; }else { machine_step = 0; } break; case 2:recv_buf[recv_cnt] = SBUF;//接收剩余数据 recv_cnt++; if(recv_cnt > 4) { machine_step = 3; } else { machine_step = 2; } break; case 3:recv_buf[recv_cnt] = SBUF; if(recv_buf[recv_cnt] == 0x0D)//判断帧尾 { switch(recv_buf[2])//判断是LED数据还是蜂鸣器数据 { case 1:led_data = recv_buf[3];//取LED点亮时间 led_data = led_data << 8; led_data = led_data + recv_buf[4]; led_cnt = 0;//目的是使LED点亮上述接收的数据的时间 break; case 2:beep_data = recv_buf[3];//取蜂鸣器鸣响时间 beep_data = beep_data << 8; beep_data = beep_data + recv_buf[4]; beep_cnt = beep_data; break; default:break; } machine_step = 0;//状态变量清零 recv_cnt = 0;//接收计数器清零 recv_flag = 1;//接收完一串数据,标志位置1 } break; default:break; } } if(TI)//不使用发送中断时,可注释掉 { TI = 0; }}//uart.h#ifndef __UART_H__#define __UART_H__#include <reg51.h>#include <stdio.h>#define MAX_REV_NUM 20//最大接收字节数extern unsigned char recv_buf[MAX_REV_NUM];extern unsigned char recv_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//time.c#include "time.h"#include "uart.h"//全局变量定义unsigned int led_data;//LED点亮时间unsigned int led_cnt;//LED累计时间计数变量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; //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>//IO 声明sbit LED = P1^0;sbit BEEP = P3^7;extern unsigned int led_data;extern unsigned int led_cnt;extern unsigned int beep_cnt;void Timer0Init(void);#endif
LED点亮控制协议命令
蜂鸣器鸣响控制协议命令
串口仿真硬件原理图
借助proteus仿真软件和虚拟串口以及串口调试助手,可以完成上述程序功能的功能测试,当然也可以在实物开发板上进行实验验证。帧头和帧尾的设置,以及具体的协议指令和数据等大家可以参考着自己设置,从而理解下常用的个人自定义通信协议。实际上,所谓的通信协议,无论是个人自定义的协议还是标准的通信协议,无外乎都是收发双方约定好的一个数据包,数据包还是由一帧一帧的数据组成的,所谓的一帧数据,也就是串口发送时的一个字节数据,加上起始位和停止位等,大家可以这样子来理解。从事单片机教学这些年,感觉学生对通信协议是学习起来最困难的,不理解通信协议,实际上就是我刚才描述的那段话,话糙理不糙。上述的通信协议只是借助帧头和帧尾来进行数据的区分,并未增加校验字节,后续会继续分享个人自定义协议的实现及相关的代码,感兴趣的可以加关注,一起学习交流。
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!