jianhong_wu
发表于 2014-4-5 23:06
cjseng 发表于 2014-4-5 21:53 static/image/common/back.gif
鸿哥,你知道你单片机不能做精确定时的原因了吗?
你每次用到定时中断,都是先把定时器停掉,然后执行一端 ...
其实你说的这些问题之前也有很多人跟我质疑过。
(1)如果不是做实时时钟,我这样的时间精度可以满足绝大多数的项目要求的。如果要求再高一点的,就不要把按键扫描放在定时中断里,也不要选用动态数码管扫描的方案。
(2)如果是做实时时钟等精度非常高的,我还是觉得不适合用单片机来做。应该直接用时钟芯片或者cpld/FPGA来做。
(3)至于我每次进入定时中断之后都关闭中断,退出的时候才打开中断,这个是我多年的使用习惯,我觉得不会影响我对时间的要求。当然,在这一点上也许不关闭定时中断才是最佳的方法,但是我个人还是会**先关闭中断,退出的时候再打开,因为我以前印象中吃过这样的亏,但是也不敢完全确定是不是由于这个原因引起的。反正关了定时中断再处理也不会对我系统造成什么影响。
jianhong_wu
发表于 2014-4-5 23:10
mwxpk 发表于 2014-4-5 15:29 static/image/common/back.gif
哄不会的啊,全是没用教程
是的,正如你所说“哄不会的”,我分享的经验主要是针对初学者,对于像您这样的比较有经验的工程师来说,确实是没用的教程。你批评的有道理,我接受。
cjseng
发表于 2014-4-6 00:47
本帖最后由 cjseng 于 2014-4-6 00:50 编辑
jianhong_wu 发表于 2014-4-5 23:06 https://bbs.21ic.com/static/image/common/back.gif
其实你说的这些问题之前也有很多人跟我质疑过。
(1)如果不是做实时时钟,我这样的时间精度可以满足绝大 ...
你这样做对系统的影响就是定时周期不确定。你的周期=定时器周期+中断程序运行时间,而中断程序运行时间是不确定的,会因为各种条件变化,忽长忽短。
你要关闭中断,到退出前再打开,否则会吃亏,这是因为你的定时周期经常小于程序运行时间。
比如你那个给下棋计时的程序,定时是很不精确的,累积起来,误差不是一般的大,可能会达到秒级。这是你的程序造成的,实际上完全可以做得更好,在几十秒内,做到毫秒级的误差一点儿问题也没有。
至于时钟芯片,那个误差并不小,一年的误差在几分钟范围内的,用单片机的定时器做,可以做到比时钟芯片更小的误差(可以用程序修正,比如隔上几天,调快或调慢一秒)。
mrguoch
发表于 2014-4-6 11:40
这个还不错,学习一下
jianhong_wu
发表于 2014-4-6 12:00
cjseng 发表于 2014-4-6 00:47 static/image/common/back.gif
你这样做对系统的影响就是定时周期不确定。你的周期=定时器周期+中断程序运行时间,而中断程序运行时间是 ...
没错。我的周期=定时器周期+中断程序运行时间,而中断程序运行的时间虽然会因为各种条件变化,忽长忽短,但实际上“忽长”的时候它不会长很多,“忽短”的时候也不会短很多,所以我默认它是固定不会变化的,对于大多数的项目是够用的。我的象棋计数器通过修正比例系数的方法矫正时间后,2个小时内精度可以控制在1秒钟以内,所以我认为这样的精度是绰绰有余了。当然,我相信你还能把精度做得更高。
jianhong_wu
发表于 2014-4-6 12:02
本帖最后由 jianhong_wu 于 2014-4-7 08:49 编辑
第三十九节:判断数据头来接收一串数据的串口通用程序框架。
开场白:
上一节讲了判断数据尾的程序框架,但是在大部分的项目中,都是通过判断数据头来接收数据的,这一节要教会大家两个知识点:
第一个:如何在已经接收到的一串数据中解析数据头协议并且提取有效数据。
第二个:无论是判断数据头还是判断数据尾,无论是单片机还是上位机,最好在固定协议前多发送一个填充的无效字节0x00,因为硬件原因,第一个字节往往容易丢失。
具体内容,请看源代码讲解。
(1)硬件平台:
基于朱兆祺51单片机学习板。
(2)实现功能:
波特率是:9600 。
通讯协议:EB 00 55XX YY
加无效填充字节后,上位机实际上应该发送:00EB 00 55XX YY
其中第1位00是无效填充字节,防止由于硬件原因丢失第一个字节。
其中第2,3,4位EB 00 55就是数据头
后2位XX YY就是有效数据
任意时刻,单片机从电脑“串口调试助手”上位机收到的一串数据中,只要此数据中包含关键字EB 00 55 ,并且此关键字后面两个字节的数据XX YY 分别为01 02,那么蜂鸣器鸣叫一声表示接收的数据头和有效数据都是正确的。
(3)源代码讲解如下:
#include "REG52.H"
#define const_voice_short40 //蜂鸣器短叫的持续时间
#define const_rc_size10//接收串口中断数据的缓冲区数组大小
#define const_receive_time5//如果超过这个时间没有串口数据过来,就认为一串数据已经全部接收完,这个时间根据实际情况来调整大小
void initial_myself(void);
void initial_peripheral(void);
void delay_long(unsigned int uiDelaylong);
void T0_time(void);//定时中断函数
void usart_receive(void); //串口接收中断函数
void usart_service(void);//串口服务程序,在main函数里
sbit beep_dr=P2^7; //蜂鸣器的驱动IO口
unsigned intuiSendCnt=0; //用来识别串口是否接收完一串数据的计时器
unsigned char ucSendLock=1; //串口服务程序的自锁变量,每次接收完一串数据只处理一次
unsigned intuiRcregTotal=0;//代表当前缓冲区已经接收了多少个数据
unsigned char ucRcregBuf; //接收串口中断数据的缓冲区数组
unsigned intuiRcMoveIndex=0;//用来解析数据协议的中间变量
unsigned intuiVoiceCnt=0;//蜂鸣器鸣叫的持续时间计数器
void main()
{
initial_myself();
delay_long(100);
initial_peripheral();
while(1)
{
usart_service();//串口服务程序
}
}
void usart_service(void)//串口服务程序,在main函数里
{
/* 注释一:
* 识别一串数据是否已经全部接收完了的原理:
* 在规定的时间里,如果没有接收到任何一个字节数据,那么就认为一串数据被接收完了,然后就进入数据协议
* 解析和处理的阶段。这个功能的实现要配合定时中断,串口中断的程序一起阅读,要理解他们之间的关系。
*/
if(uiSendCnt>=const_receive_time&&ucSendLock==1) //说明超过了一定的时间内,再也没有新数据从串口来
{
ucSendLock=0; //处理一次就锁起来,不用每次都进来,除非有新接收的数据
//下面的代码进入数据协议解析和数据处理的阶段
uiRcMoveIndex=0; //由于是判断数据头,所以下标移动变量从数组的0开始向最尾端移动
/* 注释二:
* 判断数据头,进入循环解析数据协议必须满足两个条件:
* 第一:最大接收缓冲数据必须大于一串数据的长度(这里是5。包括2个有效数据,3个数据头)
* 第二:游标uiRcMoveIndex必须小于等于最大接收缓冲数据减去一串数据的长度(这里是5。包括2个有效数据,3个数据头)
*/
while(uiRcregTotal>=5&&uiRcMoveIndex<=(uiRcregTotal-5))
{
if(ucRcregBuf==0xeb&&ucRcregBuf==0x00&&ucRcregBuf==0x55)//数据头eb 00 55的判断
{
if(ucRcregBuf==0x01&&ucRcregBuf==0x02)//有效数据01 02的判断
{
uiVoiceCnt=const_voice_short; //蜂鸣器发出声音,说明数据头和有效数据都接收正确
}
break; //退出循环
}
uiRcMoveIndex++; //因为是判断数据头,游标向着数组最尾端的方向移动
}
uiRcregTotal=0;//清空缓冲的下标,方便下次重新从0下标开始接受新数据
}
}
void T0_time(void) interrupt 1 //定时中断
{
TF0=0;//清除中断标志
TR0=0; //关中断
if(uiSendCnt<const_receive_time) //如果超过这个时间没有串口数据过来,就认为一串数据已经全部接收完
{
uiSendCnt++; //表面上这个数据不断累加,但是在串口中断里,每接收一个字节它都会被清零,除非这个中间没有串口数据过来
ucSendLock=1; //开自锁标志
}
if(uiVoiceCnt!=0)
{
uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫
beep_dr=0;//蜂鸣器是PNP三极管控制,低电平就开始鸣叫。
}
else
{
; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。
beep_dr=1;//蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
}
TH0=0xfe; //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;
TR0=1;//开中断
}
void usart_receive(void) interrupt 4 //串口接收数据中断
{
if(RI==1)
{
RI = 0;
++uiRcregTotal;
if(uiRcregTotal>const_rc_size)//超过缓冲区
{
uiRcregTotal=const_rc_size;
}
ucRcregBuf=SBUF; //将串口接收到的数据缓存到接收缓冲区里
uiSendCnt=0;//及时喂狗,虽然main函数那边不断在累加,但是只要串口的数据还没发送完毕,那么它永远也长不大,因为每个中断都被清零。
}
else//我在其它单片机上都不用else这段代码的,可能在51单片机上多增加" TI = 0;"稳定性会更好吧。
{
TI = 0;
}
}
void delay_long(unsigned int uiDelayLong)
{
unsigned int i;
unsigned int j;
for(i=0;i<uiDelayLong;i++)
{
for(j=0;j<500;j++)//内嵌循环的空指令数量
{
; //一个分号相当于执行一条空语句
}
}
}
void initial_myself(void)//第一区 初始化单片机
{
beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。
//配置定时器
TMOD=0x01;//设置定时器0为工作方式1
TH0=0xfe; //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;
//配置串口
SCON=0x50;
TMOD=0X21;
TH1=TL1=-(11059200L/12/32/9600);//这段配置代码具体是什么意思,我也不太清楚,反正是跟串口波特率有关。
TR1=1;
}
void initial_peripheral(void) //第二区 初始化外围
{
EA=1; //开总中断
ES=1; //允许串口中断
ET0=1; //允许定时中断
TR0=1; //启动定时中断
}
总结陈词:
这一节讲了常用的判断数据头来接收一串数据的程序框架,但是在很多项目中,仅仅靠判断数据头还是不够的,必须要有更加详细的通讯协议,比如可以包含数据类型,有效数据长度,有效数据,数据校验的通讯协议。这样的程序该怎么写?欲知详情,请听下回分解-----常用的自定义串口通讯协议。
(未完待续,下节更精彩,不要走开哦)
wuzx-61
发表于 2014-4-6 15:27
顶楼主对初学者的贡献!......
gyh974
发表于 2014-4-6 16:49
好文
风雪天殇
发表于 2014-4-6 17:42
曾经在某个论坛上看过吴老师对单片机和嵌入式系统的理解,很强
zxcscm
发表于 2014-4-6 22:22
收益匪浅啊,希望楼主前辈能继续更新下去!
狼牙0354
发表于 2014-4-7 08:25
其实还是蛮不错的
ahczqmz
发表于 2014-4-7 08:30
:lol
sdwflzg
发表于 2014-4-7 10:25
确实讲得不错!开卷有益!
jianhong_wu
发表于 2014-4-7 13:06
本帖最后由 jianhong_wu 于 2014-7-21 00:05 编辑
第四十节:常用的自定义串口通讯协议。
开场白:上一节讲了判断数据头的程序框架,但是在很多项目中,仅仅靠判断数据头还是不够的,必须要有更加详细的通讯协议,比如可以包含数据类型,数据地址,有效数据长度,有效数据,数据校验的通讯协议。这一节要教会大家三个知识点:第一个:常用自定义串口通讯协议的程序框架。第二个:累加校验和的校验方法。累加和的意思是前面所有字节的数据相加,超过一个字节的溢出部分会按照固定的规则自动丢弃,不用我们管。比如以下数据:
eb 00 55 01 00 02 0028 6b
其中eb 00 55为数据头,01为数据类型,00 02为有效数据长度,00 28 分别为具体的有效数据,6b为前面所有字节的累加和。累加和可以用电脑系统自带的计算器来验证。打开电脑上的计算器,点击“查看”下拉的菜单,选“科学型”,然后选左边的“十六进制”,最后选右边的“字节”,然后把前面所有的字节相加,它们的和就是6b,没错吧。第三个:原子锁的使用方法,实际上是借鉴了"红金龙吸味"关于原子锁的建议,专门用来保护中断与主函数的共享数据。
具体内容,请看源代码讲解。
(1)硬件平台:基于朱兆祺51单片机学习板。
(2)实现功能:波特率是:9600.通讯协议:EB 00 55GG HH HH XX XX …YYYY CY其中第1,2,3位EB 00 55就是数据头其中第4位GG就是数据类型。01代表驱动奉命,02代表驱动Led灯。其中第5,6位HH就是有效数据长度。高位在左,低位在右。其中第5,6位HH就是有效数据长度。高位在左,低位在右。其中从第7位开始,到最后一个字节Cy之前,XX..YY都是具体的有效数据。在本程序中,当数据类型是01时,有效数据代表蜂鸣器鸣叫的时间长度。当数据类型是02时,有效数据代表Led灯点亮的时间长度。最后一个字节CY是累加和,前面所有字节的累加。发送以下测试数据,将会分别控制蜂鸣器和Led灯的驱动时间长度。蜂鸣器短叫发送:eb 00 55 01 00 02 00 28 6b蜂鸣器长叫发送:eb 00 55 01 00 02 00 fa 3dLed灯短亮发送:eb 00 55 02 00 02 00 28 6cLed灯长亮发送:eb 00 55 02 00 02 00 fa3e
(3)源代码讲解如下:
#include "REG52.H"
/* 注释一:
* 请评估实际项目中一串数据的最大长度是多少,并且留点余量,然后调整const_rc_size的大小。
* 本节程序把上一节的缓冲区数组大小10改成了20
*/
#define const_rc_size20//接收串口中断数据的缓冲区数组大小
#define const_receive_time5//如果超过这个时间没有串口数据过来,就认为一串数据已经全部接收完,这个时间根据实际情况来调整大小
void initial_myself(void);
void initial_peripheral(void);
void delay_long(unsigned int uiDelaylong);
void T0_time(void);//定时中断函数
void usart_receive(void); //串口接收中断函数
void usart_service(void);//串口服务程序,在main函数里
void led_service(void);//Led灯的服务程序。
sbit led_dr=P3^5;//Led的驱动IO口
sbit beep_dr=P2^7; //蜂鸣器的驱动IO口
unsigned intuiSendCnt=0; //用来识别串口是否接收完一串数据的计时器
unsigned char ucSendLock=1; //串口服务程序的自锁变量,每次接收完一串数据只处理一次
unsigned intuiRcregTotal=0;//代表当前缓冲区已经接收了多少个数据
unsigned char ucRcregBuf; //接收串口中断数据的缓冲区数组
unsigned intuiRcMoveIndex=0;//用来解析数据协议的中间变量
/* 注释二:
* 为串口计时器多增加一个原子锁,作为中断与主函数共享数据的保护,实际上是借鉴了"红金龙吸味"关于原子锁的建议.
*/
unsigned charucSendCntLock=0; //串口计时器的原子锁
unsigned intuiVoiceCnt=0;//蜂鸣器鸣叫的持续时间计数器
unsigned charucVoiceLock=0;//蜂鸣器鸣叫的原子锁
unsigned char ucRcType=0;//数据类型
unsigned intuiRcSize=0;//数据长度
unsigned char ucRcCy=0;//校验累加和
unsigned intuiRcVoiceTime=0;//蜂鸣器发出声音的持续时间
unsigned intuiRcLedTime=0; //在串口服务程序中,Led灯点亮时间长度的中间变量
unsigned intuiLedTime=0;//Led灯点亮时间的长度
unsigned intuiLedCnt=0; //Led灯点亮的计时器
unsigned char ucLedLock=0;//Led灯点亮时间的原子锁
void main()
{
initial_myself();
delay_long(100);
initial_peripheral();
while(1)
{
usart_service();//串口服务程序
led_service(); //Led灯的服务程序
}
}
void led_service(void)
{
if(uiLedCnt<uiLedTime)
{
led_dr=1; //开Led灯
}
else
{
led_dr=0; //关Led灯
}
}
void usart_service(void)//串口服务程序,在main函数里
{
/* 注释三:
* 我借鉴了朱兆祺的变量命名习惯,单个字母的变量比如i,j,k,h,这些变量只用作局部变量,直接在函数内部定义。
*/
unsigned int i;
if(uiSendCnt>=const_receive_time&&ucSendLock==1) //说明超过了一定的时间内,再也没有新数据从串口来
{
ucSendLock=0; //处理一次就锁起来,不用每次都进来,除非有新接收的数据
//下面的代码进入数据协议解析和数据处理的阶段
uiRcMoveIndex=0; //由于是判断数据头,所以下标移动变量从数组的0开始向最尾端移动
while(uiRcregTotal>=5&&uiRcMoveIndex<=(uiRcregTotal-5))
{
if(ucRcregBuf==0xeb&&ucRcregBuf==0x00&&ucRcregBuf==0x55)//数据头eb 00 55的判断
{
ucRcType=ucRcregBuf; //数据类型一个字节
uiRcSize=ucRcregBuf; //数据长度两个字节
uiRcSize=uiRcSize<<8;
uiRcSize=uiRcSize+ucRcregBuf;
ucRcCy=ucRcregBuf; //记录最后一个字节的校验
ucRcregBuf=0;//清零最后一个字节的累加和变量
/* 注释四:
* 计算校验累加和的方法:除了最后一个字节,其它前面所有的字节累加起来,
* 溢出的不用我们管,C语言编译器会按照固定的规则自动处理。
* 以下for循环里的(3+1+2+uiRcSize),其中3代表3个字节数据头,1代表1个字节数据类型,
* 2代表2个字节的数据长度变量,uiRcSize代表实际上一串数据中的有效数据个数。
*/
for(i=0;i<(3+1+2+uiRcSize);i++) //计算校验累加和
{
ucRcregBuf=ucRcregBuf+ucRcregBuf;
}
if(ucRcCy==ucRcregBuf)//如果校验正确,则进入以下数据处理
{
switch(ucRcType) //根据不同的数据类型来做不同的数据处理
{
case 0x01: //驱动蜂鸣器发出声音,并且可以控制蜂鸣器持续发出声音的时间长度
uiRcVoiceTime=ucRcregBuf;//把两个字节合并成一个int类型的数据
uiRcVoiceTime=uiRcVoiceTime<<8;
uiRcVoiceTime=uiRcVoiceTime+ucRcregBuf;
ucVoiceLock=1;//共享数据的原子锁加锁
uiVoiceCnt=uiRcVoiceTime; //蜂鸣器发出声音
ucVoiceLock=0;//共享数据的原子锁解锁
break;
case 0x02: //点亮一个LED灯,并且可以控制LED灯持续亮的时间长度
uiRcLedTime=ucRcregBuf;//把两个字节合并成一个int类型的数据
uiRcLedTime=uiRcLedTime<<8;
uiRcLedTime=uiRcLedTime+ucRcregBuf;
ucLedLock=1;//共享数据的原子锁加锁
uiLedTime=uiRcLedTime; //更改点亮Led灯的时间长度
uiLedCnt=0;//在本程序中,清零计数器就等于自动点亮Led灯
ucLedLock=0;//共享数据的原子锁解锁
break;
}
}
break; //退出循环
}
uiRcMoveIndex++; //因为是判断数据头,游标向着数组最尾端的方向移动
}
uiRcregTotal=0;//清空缓冲的下标,方便下次重新从0下标开始接受新数据
}
}
void T0_time(void) interrupt 1 //定时中断
{
TF0=0;//清除中断标志
TR0=0; //关中断
/* 注释五:
* 此处多增加一个原子锁,作为中断与主函数共享数据的保护,实际上是借鉴了"红金龙吸味"关于原子锁的建议.
*/
if(ucSendCntLock==0)//原子锁判断
{
ucSendCntLock=1; //加锁
if(uiSendCnt<const_receive_time) //如果超过这个时间没有串口数据过来,就认为一串数据已经全部接收完
{
uiSendCnt++; //表面上这个数据不断累加,但是在串口中断里,每接收一个字节它都会被清零,除非这个中间没有串口数据过来
ucSendLock=1; //开自锁标志
}
ucSendCntLock=0; //解锁
}
if(ucVoiceLock==0) //原子锁判断
{
if(uiVoiceCnt!=0)
{
uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫
beep_dr=0;//蜂鸣器是PNP三极管控制,低电平就开始鸣叫。
}
else
{
; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。
beep_dr=1;//蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
}
}
if(ucLedLock==0)//原子锁判断
{
if(uiLedCnt<uiLedTime)
{
uiLedCnt++;//Led灯点亮的时间计时器
}
}
TH0=0xfe; //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;
TR0=1;//开中断
}
void usart_receive(void) interrupt 4 //串口接收数据中断
{
if(RI==1)
{
RI = 0;
++uiRcregTotal;
if(uiRcregTotal>const_rc_size)//超过缓冲区
{
uiRcregTotal=const_rc_size;
}
ucRcregBuf=SBUF; //将串口接收到的数据缓存到接收缓冲区里
if(ucSendCntLock==0)//原子锁判断
{
ucSendCntLock=1; //加锁
uiSendCnt=0;//及时喂狗,虽然在定时中断那边此变量会不断累加,但是只要串口的数据还没发送完毕,那么它永远也长不大,因为每个串口接收中断它都被清零。
ucSendCntLock=0; //解锁
}
}
else//我在其它单片机上都不用else这段代码的,可能在51单片机上多增加" TI = 0;"稳定性会更好吧。
{
TI = 0;
}
}
void delay_long(unsigned int uiDelayLong)
{
unsigned int i;
unsigned int j;
for(i=0;i<uiDelayLong;i++)
{
for(j=0;j<500;j++)//内嵌循环的空指令数量
{
; //一个分号相当于执行一条空语句
}
}
}
void initial_myself(void)//第一区 初始化单片机
{
led_dr=0; //关Led灯
beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。
//配置定时器
TMOD=0x01;//设置定时器0为工作方式1
TH0=0xfe; //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;
//配置串口
SCON=0x50;
TMOD=0X21;
TH1=TL1=-(11059200L/12/32/9600);//这段配置代码具体是什么意思,我也不太清楚,反正是跟串口波特率有关。
TR1=1;
}
void initial_peripheral(void) //第二区 初始化外围
{
EA=1; //开总中断
ES=1; //允许串口中断
ET0=1; //允许定时中断
TR0=1; //启动定时中断
}
总结陈词:
这一节讲了常用的自定义串口通讯协议的程序框架,这种框架在判断一串数据是否接收完毕的时候,都是靠“超过规定的时间内,没有发现串口数据”来判定的,这是我做绝大多数项目的串口程序框架,但是在少数要求实时反应非常快的项目中,我会用另外一种响应速度更快的串口程序框架,这种程序框架是什么样的?欲知详情,请听下回分解-----在串口接收中断里即时解析数据头的特殊程序框架。
(未完待续,下节更精彩,不要走开哦)
sdwflzg
发表于 2014-4-14 08:26
每天学一课,受益匪浅
290813633
发表于 2014-4-15 18:58
zhitao2072 发表于 2014-3-15 11:23 static/image/common/back.gif
顶起来。。。。。。。
SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS厉害
jianhong_wu
发表于 2014-4-19 11:43
第四十一节:在串口接收中断里即时解析数据头的特殊程序框架。
开场白:
上一节讲了常用的自定义串口通讯协议的程序框架,这种框架在判断一串数据是否接收完毕的时候,都是靠“超过规定的时间内,没有发现串口数据”来判定的,这是我做绝大多数项目的串口程序框架,但是在少数要求实时反应非常快的项目中,这样的程序框架可能会满足不了系统对速度的要求,这一节就是要介绍另外一种响应速度更加快的串口程序框架,要教会大家一个知识点:在串口接收中断里即时解析数据头的特殊程序框架。我在这种程序框架里,会尽量简化数据头和数据尾,同时也简化校验,目的都是为了提高响应速度。
具体内容,请看源代码讲解。
(1)硬件平台:
基于朱兆祺51单片机学习板。
(2)实现功能:
波特率是:9600.
通讯协议:EBGG XX XX XX XX ED
其中第1位EB就是数据头.
其中第2位GG就是数据类型。01代表驱动蜂鸣器,02代表驱动Led灯。
其中第3,4,5,6位XX就是有效数据长度。高位在左,低位在右。
其中第7位ED就是数据尾,在这里也起一部分校验的作用,虽然不是累加和的方式。
在本程序中,
当数据类型是01时,4个有效数据代表一个long类型数据,如果这个数据等于十进制的123456789,那么蜂鸣器就鸣叫一声表示正确。
当数据类型是02时,4个有效数据代表一个long类型数据,如果这个数据等于十进制的123456789,那么LED灯就会闪烁一下表示正确。
十进制的123456789等于十六进制的75bcd15 。
发送以下测试数据,将会分别控制蜂鸣器Led灯。
控制蜂鸣器发送:eb 01 07 5b cd 15 ed
控制LED灯发送:eb 02 07 5b cd 15 ed
(3)源代码讲解如下:#include "REG52.H"
#define const_rc_size20//接收串口中断数据的缓冲区数组大小
#define const_receive_time5//如果超过这个时间没有串口数据过来,就认为一串数据已经全部接收完,这个时间根据实际情况来调整大小
#define const_voice_short80 //蜂鸣器短叫的持续时间
#define const_led_short80 //LED灯亮的持续时间
void initial_myself(void);
void initial_peripheral(void);
void delay_long(unsigned int uiDelaylong);
void T0_time(void);//定时中断函数
void usart_receive(void); //串口接收中断函数
void led_service(void);//Led灯的服务程序。
sbit led_dr=P3^5;//Led的驱动IO口
sbit beep_dr=P2^7; //蜂鸣器的驱动IO口
unsigned intuiRcregTotal=0;//代表当前缓冲区已经接收了多少个数据
unsigned char ucRcregBuf; //接收串口中断数据的缓冲区数组
unsigned intuiVoiceCnt=0;//蜂鸣器鸣叫的持续时间计数器
unsigned charucVoiceLock=0;//蜂鸣器鸣叫的原子锁
unsigned intuiRcVoiceTime=0;//蜂鸣器发出声音的持续时间
unsigned intuiLedCnt=0; //Led灯点亮的计时器
unsigned char ucLedLock=0;//Led灯点亮时间的原子锁
unsigned long ulBeepData=0; //蜂鸣器的数据
unsigned long ulLedData=0; //LED的数据
unsigned char ucUsartStep=0; //串口接收字节的步骤变量
void main()
{
initial_myself();
delay_long(100);
initial_peripheral();
while(1)
{
led_service(); //Led灯的服务程序
}
}
void led_service(void)
{
if(uiLedCnt<const_led_short)
{
led_dr=1; //开Led灯
}
else
{
led_dr=0; //关Led灯
}
}
void T0_time(void) interrupt 1 //定时中断
{
TF0=0;//清除中断标志
TR0=0; //关中断
/* 注释一:
* 此处多增加一个原子锁,作为中断与主函数共享数据的保护,实际上是借鉴了"红金龙吸味"关于原子锁的建议.
*/
if(ucVoiceLock==0) //原子锁判断
{
if(uiVoiceCnt!=0)
{
uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫
beep_dr=0;//蜂鸣器是PNP三极管控制,低电平就开始鸣叫。
}
else
{
; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。
beep_dr=1;//蜂鸣器是PNP三极管控制,高电平就停止鸣叫。
}
}
if(ucLedLock==0)//原子锁判断
{
if(uiLedCnt<const_led_short)
{
uiLedCnt++;//Led灯点亮的时间计时器
}
}
TH0=0xfe; //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;
TR0=1;//开中断
}
void usart_receive(void) interrupt 4 //串口接收数据中断
{
/* 注释二:
* 以下就是吴坚鸿在串口接收中断里即时解析数据头的特殊程序框架,
* 它的特点是靠数据头来启动接受有效数据,靠数据尾来识别一串数据接受完毕,
* 这里的数据尾也起到一部分的校验作用,让数据更加可靠。这种程序结构适合应用
* 在传输的数据长度不是很长,而且要求响应速度非常高的实时场合。在这种实时要求
* 非常高的场合中,我就不像之前一样做数据累加和的复杂运算校验,只用数据尾来做简单的
* 校验确认,目的是尽可能提高处理速度。
*/
if(RI==1)
{
RI = 0;
switch(ucUsartStep) //串口接收字节的步骤变量
{
case 0:
ucRcregBuf=SBUF;
if(ucRcregBuf==0xeb)//数据头判断
{
ucRcregBuf=0;//数据头及时清零,为下一串数据的接受判断做准备
uiRcregTotal=1;//缓存数组的下标初始化
ucUsartStep=1;//如果数据头正确,则切换到下一步,依次把上位机来的数据存入数组缓冲区
}
break;
case 1:
ucRcregBuf=SBUF;//依次把上位机来的数据存入数组缓冲区
uiRcregTotal++; //下标移动
if(uiRcregTotal>=7)//已经接收了7个字节
{
if(ucRcregBuf==0xed)//数据尾判断,也起到一部分校验的作用,让数据更加可靠,虽然没有用到累加和的检验方法
{
ucRcregBuf=0;//数据尾及时清零,为下一串数据的接受判断做准备
switch(ucRcregBuf) //根据不同的数据类型来做不同的数据处理
{
case 0x01://与蜂鸣器相关
ulBeepData=ucRcregBuf; //把四个字节的数据合并成一个long型的数据
ulBeepData=ulBeepData<<8;
ulBeepData=ulBeepData+ucRcregBuf;
ulBeepData=ulBeepData<<8;
ulBeepData=ulBeepData+ucRcregBuf;
ulBeepData=ulBeepData<<8;
ulBeepData=ulBeepData+ucRcregBuf;
if(ulBeepData==123456789)//如果此数据等于十进制的123456789,表示数据正确
{
ucVoiceLock=1;//共享数据的原子锁加锁
uiVoiceCnt=const_voice_short; //蜂鸣器发出声音
ucVoiceLock=0;//共享数据的原子锁解锁
}
break;
case 0x02://与Led灯相关
ulLedData=ucRcregBuf; //把四个字节的数据合并成一个long型的数据
ulLedData=ulLedData<<8;
ulLedData=ulLedData+ucRcregBuf;
ulLedData=ulLedData<<8;
ulLedData=ulLedData+ucRcregBuf;
ulLedData=ulLedData<<8;
ulLedData=ulLedData+ucRcregBuf;
if(ulLedData==123456789)//如果此数据等于十进制的123456789,表示数据正确
{
ucLedLock=1;//共享数据的原子锁加锁
uiLedCnt=0;//在本程序中,清零计数器就等于自动点亮Led灯
ucLedLock=0;//共享数据的原子锁解锁
}
break;
}
}
ucUsartStep=0; //返回上一步数据头判断,为下一次的新数据接收做准备
}
break;
}
}
else//我在其它单片机上都不用else这段代码的,可能在51单片机上多增加" TI = 0;"稳定性会更好吧。
{
TI = 0;
}
}
void delay_long(unsigned int uiDelayLong)
{
unsigned int i;
unsigned int j;
for(i=0;i<uiDelayLong;i++)
{
for(j=0;j<500;j++)//内嵌循环的空指令数量
{
; //一个分号相当于执行一条空语句
}
}
}
void initial_myself(void)//第一区 初始化单片机
{
led_dr=0; //关Led灯
beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。
//配置定时器
TMOD=0x01;//设置定时器0为工作方式1
TH0=0xfe; //重装初始值(65535-500)=65035=0xfe0b
TL0=0x0b;
//配置串口
SCON=0x50;
TMOD=0X21;
TH1=TL1=-(11059200L/12/32/9600);//这段配置代码具体是什么意思,我也不太清楚,反正是跟串口波特率有关。
TR1=1;
}
void initial_peripheral(void) //第二区 初始化外围
{
EA=1; //开总中断
ES=1; //允许串口中断
ET0=1; //允许定时中断
TR0=1; //启动定时中断
}总结陈词:
前面花了4节内容仔细讲了各种串口接收数据的常用框架,从下一节开始,我开始讲串口发送数据的程序框架,这种程序框架是什么样的?欲知详情,请听下回分解-----通过串口用delay延时方式发送一串数据。
(未完待续,下节更精彩,不要走开哦)
cjseng
发表于 2014-4-19 14:58
唉!把数据处理放在串口中断里,数据量不大还好,要是一次接收上百个字节怎么办呢?这边还没处理完,那边又有新的数据进来了,要丢数据的啊。
还有,为什么 TI=0; 不这样做的话,岂不是要一直进入串口中断?
jianhong_wu
发表于 2014-4-19 16:14
cjseng 发表于 2014-4-19 14:58 static/image/common/back.gif
唉!把数据处理放在串口中断里,数据量不大还好,要是一次接收上百个字节怎么办呢?这边还没处理完,那边又 ...
(1)你说的没错,把数据处理放在串口中断里,只适合数据量小的情况下。如果数据量大的话,请参考我第40节的内容。
(2)感谢你的提醒,让我终于想起来为什么要加TI=0了,就是为了及时清除发送数据产生的中断。
cjseng
发表于 2014-4-19 20:51
jianhong_wu 发表于 2014-4-19 16:14 static/image/common/back.gif
(1)你说的没错,把数据处理放在串口中断里,只适合数据量小的情况下。如果数据量大的话,请参考我第40节 ...
你前边用的那种方法也不一定能处理大数据量的串口通讯,除非你事先知道每两帧数据之间的间隔时间的大小。
要确保通讯可靠,除了硬件,完善的协议很重要。你还不如参考一下MODBUS通讯协议呢。