开源PLC学习笔记05(再从51开始 通讯 UART)——2013_11_10
本帖最后由 oldbeginner 于 2013-11-10 19:12 编辑在笔记03和04中,通过研究Read code代码的功能,完成了对FX1NProcessing函数的研究,这个函数是和三菱上位机软件通讯的核心。FX1NProcessing实现了对协议的翻译,但是传送数据等是通过uart来实现的。现在就可以来理解uart是如何实现数据传送的。
******************************************************************************
uart相关的函数都定义在uart.c中,但是最关键的UartInit函数(初始化)是定义在PLC51x.c文件中,为什么?
// 串口初始化 函数UartInit(); //
//-------------------------------------------------------------------------------------//
void UartInit(void)
{
SCON= 0x50;
//PCON |= 0x80;
//AUXR2|= 0x40;
//Timer1Init();
TR1=0; //停止定时器
// TCON=0x00; //定时器控制寄存器 注意:TCON只需操作一次
TMOD |= 0x20; //定时器1
TL1 = -(SYSCLK/12/32/baud);//0xfa; // -(SYSCLK/12/32/baud); //注意波特率加倍位
TH1 = TL1;
TR1=1; //启动定时器1
// ET1=1; //打开定时器1中断
UartReceiveCounter=0;
UartRxTimerStartFlag=0;
}
*****************************************
看得很难受,先把注释的行都去掉
void UartInit(void)
{
SCON= 0x50;
//停止定时器
TR1=0;
//定时器1
TMOD |= 0x20;
//0xfa; // -(SYSCLK/12/32/baud); //注意波特率加倍位
TL1 = -(SYSCLK/12/32/baud);
TH1 = TL1;
//启动定时器1
TR1=1;
UartReceiveCounter=0;
UartRxTimerStartFlag=0;
}
***********************************************
在uart.h中定义
#define SYSCLK 11059200
#define baud 9600 // 注意修改Timer1 TH1
看着还是不舒服,重新写一下,添加了注释(基于外部接11.592Mhz晶振,并把倍频开通)
void UartInit(void)
{
//停止定时器
TR1=0;
/******电源管理PCON寄存器************
位 7 6 5 4 3 2 1 0
寄存器 SMOD X X X X X X X
————————————————————
取值 1 0 0 0 0 0 0 0
说明 波特率加倍
************************************/
PCON=0x80;
/*****定时器TMOD寄存器**********
位 7 6 5 4 3 2 1 0
寄存器 GATE C/T M1 M0 GATE C/T M1 M0
——————————————---- |———————————
定时器1的设置 定时器0的设置
取值 0 0 1 0 0 0 0 0
说明 使用定时器1,工作方式2 | 没用使用
**********************************/
TMOD=TMOD | 0x20;
//定时器1工作方式2
//在下面的PCON设置上增加了倍频,所以晶振位22.1184Mhz
/**********波特率设置表(22.1184Mhz)************
波特率 2400 4800 9600 19200
TH1 E8 F4 FA FD
TL1 E8 F4 FA FD
说明,因为选取波特率9600,所以TH1和TL1都取FA
*************************************************/
TH1=0xFA;
TL1=0xFA;
//串口工作方式1
/*******串口SCON寄存器***************
位 7 6 5 4 3 2 1 0
寄存器 SM0 SM1 SM2 REN TB8 RB8 TI RI
——————————————————————————————————————
工作方式控制| 多机通信| 接受标志位|发第9位| 收第9位| 发送中断标志位接受中断标志位
取值 0 1 0 1 0 0 0 0
说明 工作方式1 开始接受
**************************************/
SCON=0x50;
//启动定时器1
TR1=1;
UartReceiveCounter=0;
UartRxTimerStartFlag=0;
}
源程序中没有开启中断,还有为什么注释掉几行?目前无法判断原因,继续下去找答案。
附录:波特率与定时器
附录2:串口相关函数 顶楼主,我是个外行,这个是什么协议,与modbusb比如何
初始化后,继续。
再返回到uart.c中,先看一下变量
volatile unsigned char UartSendBuffer; //发送缓冲
volatile unsigned char UartReceiveBuffer; //接收数据缓冲
volatile unsigned char *outlast=UartSendBuffer; //最后由中断传输出去的字节位置
volatile unsigned char *putlast=UartSendBuffer; //最后放入发送缓冲区的字节位置
volatile unsigned char UartSendBufferemptyFlag=1; //缓冲区数据发完标志 发完=1
volatile unsigned char UartSendBufferHaveDataFlag=0; //发送缓冲区非空标志 有=1
volatile unsigned char UartReceiveCounter=0; //接收计数器
volatile unsigned char UartRxTimerStartFlag=0; //接收超时计数器启动标志
volatile unsigned char UartWaitForCounter=0; //接收超时计数器
volatile unsigned char UartDataReadyFlag=0; //接收完成标志
********************************************************************
上面UartReceiveBuffer在FX1NProcessing函数多次使用,为了简洁,当时记为Buffer。变量名很多又太长,没有必要记,暂时只看一下变量名的末尾(红色),了解大概功能即可,等在具体代码中遇到再返回来确认。
// 函数名称: UartSendchar
//uart.c
// 功能描述:放入一个字节到发送缓冲区
//
// 输 入: unsigned char ucdata
//
// 输 出: void
void UartSendchar(unsigned char ucdata)
{
ES=0; // 暂停串行中断,以免数据比较时出错
while((((outlast-putlast)==2)&&(outlast > putlast ))||((outlast < putlast)&&(OutLEN-(putlast-outlast)==2)))
{
ES=1;
ucdata++;
ucdata--;
ES=0;
}
*putlast=ucdata; // 放字节进入缓冲区
putlast++; // 发送缓冲区指针加1
if (putlast==UartSendBuffer+OutLEN) putlast=UartSendBuffer; // 指针到了顶部换到底部
UartSendBufferHaveDataFlag=1;
if (UartSendBufferemptyFlag) // 缓冲区无数据
{
UartSendBufferemptyFlag =0;
SBUF=*outlast; // 未发送完继续发送
outlast++; // 最后传出去的字节位置加1
if (outlast==UartSendBuffer+OutLEN)outlast=UartSendBuffer; // 地址到顶部回到底部
if (putlast==outlast)UartSendBufferHaveDataFlag=0; // 数据发送完置发送缓冲区空标志
} // 缓冲区开始为空,置为有,启动发送
ES=1;
}
看来 UartSendchar是uart相关最复杂的函数,但要实现的功能从字面上就很好理解。
********************************************************************
按顺序理解,
ES=0; // 暂停串行中断,以免数据比较时出错
什么是ES?
// (bits in IE,定义在REG_MPC82G516.h中)
sbit EA = IE^7;
sbit ET2 = IE^5;
sbit ES = IE^4;
sbit ET1 = IE^3;
sbit EX1 = IE^2;
sbit ET0 = IE^1;
sbit EX0 = IE^0;
从定义上,和REG52.h中的定义一致,就是断开串口中断。下图左下角的开关。
while((((outlast-putlast)==2)&&(outlast > putlast ))||((outlast < putlast)&&(OutLEN-(putlast-outlast)==2)))
看来也许要再定义一个宏了,不过先理解一下,
最后由中断传输出去的字节位置outlast
最后放入发送缓冲区的字节位置 putlast
要理解还需要看中断函数,因为outlast和putlast都是由中断函数赋值的。所以要理解一下中断函数后再返回。
oldbeginner 发表于 2013-11-10 16:33 static/image/common/back.gif
初始化后,继续。
再返回到uart.c中,先看一下变量
入门时很少见到串口中断,搜一下,只要给单片机发送数据,单片机就会自动接收数据,并把它放在数据缓冲器SBUF中,如果你之前有允许串行口中断,RI就会置1,向单片机CPU申请中断,并进入中断服务程序,做完中断函数后就会自动返回断点。RI和TI中有一个为1就会马上进入中断服务子程序。
http://bbs.elecfans.com/forum.php?mod=viewthread&tid=206923
void Uart(void) interrupt 4 using 2
{
中断处理;
}
interrupt 4 using 2是什么?再搜一下,
interrupt 4指明是串行口中断;
using 2时设置 RS1=1,RS0 =0,用第2组寄存器,R0--R7的在数据存储区里的实际地址是08H-0FH。http://zhidao.baidu.com/link?url=7Ojgm_fYNoyUW0hPgWXrIOWDY5N-g37zzPKetKsbcKyTc-SWI7gI_53IorfrDeIWGQlnY1Q5SLezt7k2Rpt8Lq
为什么制定用第2组?先保留这个问题,以后解决。
void Uart(void) interrupt 4 using 2
{
if(TI)
发送中断处理;
if(RI)
接收中断处理;
}
******************************
先看发送中断处理,
TI=0;
if (UartSendBufferHaveDataFlag)
{
SBUF=*outlast; // 未发送完继续发送
outlast++; // 最后传出去的字节位置加1
if (outlast==UartSendBuffer+OutLEN)
outlast=UartSendBuffer; // 地址到顶部回到底部
if (putlast==outlast)
UartSendBufferHaveDataFlag=0; // 数据发送完置发送缓冲区空标志
}
else UartSendBufferemptyFlag =1;
1、TI=0,好理解,先复位。
2、要判断一个标志位,UartSendBufferHaveDataFlag,这个标志位就是一个句子,根据句子字面意思就是,串口发送缓冲区有了数据。
确认一下定义volatile unsigned char UartSendBufferHaveDataFlag=0; //发送缓冲区非空标志 有=1
3、如果DataFlag(暂时简洁一下)为0,则UartSendBufferemptyFlag =1,根据字面意思,缓冲区是空的。
确认一下定义volatile unsigned char UartSendBufferemptyFlag=1; //缓冲区数据发完标志 发完=1
4、如果DataFlag为1,SBUF=*outlast;( // 未发送完继续发送 ),然后outlast++; ( // 最后传出去的字节位置加1 )。基本能猜出outlast用来定位字符数组要传的字节,确认一下定义volatile unsigned char *outlast=UartSendBuffer; //最后由中断传输出去的字节位置
5、首先是一个判断,if (outlast==UartSendBuffer+OutLEN),如果成立然后outlast=UartSendBuffer; (// 地址到顶部回到底部),基本能看出来,当outlast定位到数组末尾后,即所有数组成员都被遍历后,重新回到开头。OutLen也能猜出什么意思来,确认一下,#define OutLEN 30 // FIFO 发送缓冲区长度。数组长度是一个固定值。
6、首先是一个判断, if (putlast==outlast),查看一下定义volatile unsigned char *putlast=UartSendBuffer; //最后放入发送缓冲区的字节位置 。如果成立,然后UartSendBufferHaveDataFlag=0; putlast就是指向数组的首地址,等outlast遍历数组成员后,再回到首地址,这时两者就相等,然后声明发送缓冲区是空的。这里有个疑问,putlast是指向首地址的,在程序运行期间不应该发生变化,为什么起名叫put last,字面上无法理解。
这样,发送中断处理有了一个了解。
********************************************
semonpic 发表于 2013-11-10 16:00 static/image/common/back.gif
顶楼主,我是个外行,这个是什么协议,与modbusb比如何
我也是外行,正在学习,你说的哪个协议我见到过,但是不会。帖子里讲的是入门的串口通讯。 oldbeginner 发表于 2013-11-10 18:52 static/image/common/back.gif
我也是外行,正在学习,你说的哪个协议我见到过,但是不会。帖子里讲的是入门的串口通讯。 ...
modbus 是工控设备 基本支持的一种基于485硬件的协议,
三菱那年头的老家伙,似乎是不支持,,{:titter:} MARK马克 好。。。
串口:环形队列+中断。
以前C51BBS上有一个所子写的,我觉得不错。 本帖最后由 oldbeginner 于 2013-11-11 13:16 编辑
oldbeginner 发表于 2013-11-10 18:50 static/image/common/back.gif
入门时很少见到串口中断,搜一下,只要给单片机发送数据,单片机就会自动接收数据,并把它放在数据缓冲器 ...
再继续接收中断处理,
RI = 0;
//==========================================================
// 若有必要 有待于加入 偶校验算法 .数据位的bit7位为校验位
//==========================================================
Buffer=SBUF&0x7f;
UartRxTimerStartFlag=1; // 启动超时计数器
UartWaitForCounter=0; // 清超时计数器 // 10ms
if (UartReceiveCounter>=InLEN)
{
UartDataReadyFlag=1;
//UartReceiveCounter=0;
REN=0;
}
******************************************************************
1、RI=0;好理解,复位。
2、SBUF是接收到的数据,然后&0111 1111。为什么要&0x7F呢?搜了一下,同一论坛上用STM32也不知道为什么,
有谁做过STM32串口通信使用7位数据位的?
不过网上找到一个比较可行的理解是,51单片机的确只能做到8个数据位。所问的“7位数据位串行通信”估计是“1个起始位+7个数据位+1个偶校验位+1个停止位”这种方式。http://zhidao.baidu.com/link?url ... eC9Tcjw2O5HCzdIhXeq(评论中)
按照下图来理解,把高位屏蔽(本例)或作为校验位。
3、启动超时计数器, UartRxTimerStartFlag=1;查看一下定义volatile unsigned char UartRxTimerStartFlag=0; //接收超时计数器启动标志,可以理解,继续
4、 清超时计数器,UartWaitForCounter=0;查看一下定义volatile unsigned char UartWaitForCounter=0; //接收超时计数器,应该在后面的uart相关函数会遇到,到时再返回来确认。
5、首先是一个判断,UartReceiveCounter>=InLEN,InLen曾经遇到过,再复习一下,#define InLEN 142 // 超时收 接收缓冲区长度。这是一个判断是否超时的语句,当UartReceiveCounter>=142时,就认为超时。
6、如果超时,则 UartDataReadyFlag=1;并让REN复位。查看volatile unsigned char UartDataReadyFlag=0; //接收完成标志
把REN和RI搞混了,搜一下:
REN:允许接收位。 REN用于控制数据接收的允许和禁止,REN=1时,允许接收,REN=0时,禁止接收。
中断处理函数,
void Uart(void) interrupt 4 using 2
{
if(TI)
发送中断处理;
if(RI)
接收中断处理;
}
这样串口中断函数第一遍理解就完成了,在它的帮助下,再次去理解UartSendChar函数。
本帖最后由 oldbeginner 于 2013-11-11 15:07 编辑
oldbeginner 发表于 2013-11-11 12:58 static/image/common/back.gif
再继续接收中断处理,
RI = 0;
//=================================================== ...
返回上次被卡住的地方
***************************
while((((outlast-putlast)==2)&&(outlast > putlast ))||((outlast < putlast)&&(OutLEN-(putlast-outlast)==2)))
看来也许要再定义一个宏了,不过先理解一下,
最后由中断传输出去的字节位置outlast
最后放入发送缓冲区的字节位置 putlast
****************************
在中断函数理解中,我认为outlast的作用是遍历数组每个成员,而putlast是指向首地址,用来判断outlast是否回到了首地址。
不过看到(outlast < putlast),我认为自己的理解有问题。
要理解串口中断函数,还必须了解一下环形队列。
找到了一个还算详细的资料,http://wenku.baidu.com/view/3117421303d8ce2f006623aa.html。
自己在理解发送中断时有个疑问,现在可以浮出了,
6、首先是一个判断, if (putlast==outlast),查看一下定义volatile unsigned char *putlast=UartSendBuffer; //最后放入发送缓冲区的字节位置 。如果成立,然后UartSendBufferHaveDataFlag=0; putlast就是指向数组的首地址,等outlast遍历数组成员后,再回到首地址,这时两者就相等,然后声明发送缓冲区是空的。这里有个疑问,putlast是指向首地址的,在程序运行期间不应该发生变化,为什么起名叫put last,字面上无法理解。
当然,理解错了。还需要重新返回中断处理函数,首先看发送中断。
putlast和outlast都是移动的,当发送数据时,outlast移动,这也让我误认为putlast是不动的。
环形队列的特点是,不需要进行动态的内存释放和分配,使用固定大小的内存空间(本例中OutLEN就是一个常数)反复使用。在实际的队列插入和弹出操作中,是不断交叉进行的。 http://blog.csdn.net/billow_zhang/article/details/4420789
SBUF=*outlast; // 未发送完继续发送
outlast++; // 最后传出去的字节位置加1
然后,直到
所以在第2遍理解时加入了环形队列,把outlast当作head,putlast当作tail。
*******************************
小结,
串口中断处理函数,中断发送函数利用了一个环形队列UartSendBuffer。
接收函数则没这样复制,只是利用另外一个队列,Buffer(UartReceiveBuffer)。 核心是Buffer=SBUF&0x7f。
本帖最后由 oldbeginner 于 2013-11-11 16:26 编辑
oldbeginner 发表于 2013-11-11 14:49 static/image/common/back.gif
返回上次被卡住的地方
***************************
while((((outlast-putlast)==2)&&(outlast > putlast...
第3次去理解UartSendChar函数,
while((((outlast-putlast)==2)&&(outlast > putlast ))||((outlast < putlast)&&(OutLEN-(putlast-outlast)==2)))
{
等待处理;
}
感觉条件判断内部的函数是为了等待某个条件,先看一下什么条件(左右两条件或)
基本上就是队列马上就要满了,只差一个。
出现上面两种情况后,就
ES=1;
ucdata++;
ucdata--;
ES=0;
不是很理解。
继续向下,走出while语句后,
*putlast=ucdata; // 放字节进入缓冲区
putlast++; // 发送缓冲区指针加1
putlast开始移动了。
然后一个判断语句,
if (putlast==UartSendBuffer+OutLEN)
putlast=UartSendBuffer; // 指针到了顶部换到底部
从上面的环形图中也可以看出,如果putlast又移动到位置0,这时需要人工重新赋一下地址。
然后,数据标志位置1,
UartSendBufferHaveDataFlag=1;同样可以从上图中可以看出,ucdata已经读入了队列中,所以标志有数据了。
然后又是一个判断,
if (UartSendBufferemptyFlag) // 缓冲区无数据
{
缓冲区无数据处理;
}
查看定义,volatile unsigned char UartSendBufferemptyFlag=1; //缓冲区数据发完标志 发完=1
emptyFlag初值是1,那么什么时候会置1呢?复习一下串口发送中断里的一部分,
************(串口中断函数部分)***************
if (UartSendBufferHaveDataFlag)
{
SBUF=*outlast; // 未发送完继续发送
outlast++; // 最后传出去的字节位置加1
。。。。。。
}
else
UartSendBufferemptyFlag =1;
**************************************
理解了emptyFlag,如果为1,则缓冲区无数据处理,
UartSendBufferemptyFlag =0;
SBUF=*outlast; // 未发送完继续发送
outlast++; // 最后传出去的字节位置加1
if (outlast==UartSendBuffer+OutLEN)
outlast=UartSendBuffer; // 地址到顶部回到底部
if (putlast==outlast)
UartSendBufferHaveDataFlag=0; // 数据发送完置发送缓冲区空标志
1、复位emptyFlag,好理解。
2、利用outlast发送,环形队列中解释比较清楚了。
3、第一个判断,outlast是否到位置0了,如果是,人工重新赋地址;
第二个判断,outlast是否追上putlast,如果是则队列空,复位DataFlag。
第1遍理解差不多完成,
********************************************************
void UartSendchar(unsigned char ucdata)
{
ES=0; // 暂停串行中断,以免数据比较时出错
while(队列马上就要满了)
{
不是很理解的代码;
}
*putlast=ucdata; // 放字节进入缓冲区
putlast++; // 发送缓冲区指针加1
if (putlast==UartSendBuffer+OutLEN)
putlast=UartSendBuffer; // 指针到了顶部换到底部
UartSendBufferHaveDataFlag=1;
if (UartSendBufferemptyFlag) // 缓冲区无数据
{
缓冲区无数据处理;
}
ES=1;
}
***************************************************
因为UartSendChar和串口中断函数互相调用环形队列,所以必须熟悉环形队列的模型(有点像太极图)。
有了上面的模型,在利用UartSendBufferHaveDataFlag和UartSendBufferemptyFlag这两个标志位来判断状态。
另外,这里没有使用队列已经满了的标志位,我猜测和那段我不太理解的代码有关。
oldbeginner 发表于 2013-11-11 16:00 static/image/common/back.gif
第3次去理解UartSendChar函数,
while((((outlast-putlast)==2)&&(outlast > putlast ))||((outlast < put ...
还剩下两个比较简单的函数
// 函数名称: UartSendString
//
// 功能描述:发送字符串
//
// 输 入: unsigned char *str
//
// 输 出: void
void UartSendString(unsigned char *str)
{
while(*str) // 遇到停止符0结束
{
UartSendchar(*str++);
//while(UartSendBufferHaveDataFlag);
}
}
不停调用UartSendChar,
为什么写while(UartSendBufferHaveDataFlag),然后又注释掉。当UartSendBufferHaveDataFlag=1时,表示队列写入了字符,
然后在中断处理函数中,
if (putlast==outlast)
UartSendBufferHaveDataFlag=0; // 数据发送完置发送缓冲区空标志
这里可能是为了确认写入后并被发送后,再发送下一条字符,为什么又注释掉了呢?
感觉应该是while(队列满标志位);但是这个程序中没有设置队列满标志位。
*********************************
// 函数名称: UartSendByte
//
// 功能描述:发送一串数据
//
// 输 入: unsigned char *Startaddr,unsigned char SendByte
//
// 输 出: void
void UartSendByte(unsigned char *Startaddr,unsigned char SendByte)
{
while(SendByte--)
{
UartSendchar(*Startaddr++);
}
}
发送长度确定。
oldbeginner 发表于 2013-11-11 17:09 static/image/common/back.gif
还剩下两个比较简单的函数
// 函数名称: UartSendString
//
理解了uart是如何工作后,就要进入我还没接触过IAP了。
http://www.amobbs.com/thread-5558672-1-1.html oldbeginner 发表于 2013-11-11 18:21 static/image/common/back.gif
理解了uart是如何工作后,就要进入我还没接触过IAP了。
http://www.amobbs.com/thread-5558672-1-1.html ...
while((((outlast-putlast)==2)&&(outlast > putlast ))||((outlast < putlast)&&(OutLEN-(putlast-outlast)==2)))
{
ES=1;
ucdata++;
ucdata--;
ES=0;
}
这个ucdata++;然后ucdata--是什么意思? 秋原之风 发表于 2013-11-11 22:49 static/image/common/back.gif
while((((outlast-putlast)==2)&&(outlast > putlast ))||((outlast < putlast)&&(OutLEN-(putlast-outl ...
感觉这段代码没有意义,此时tail就要追上head了,只差1格队列就满了,只是为了让tail停一下。 克鲁克各国 {:smile:} 太感谢楼主了帮了我大忙 好东西啊
页:
[1]