oldbeginner 发表于 2013-11-10 15:28:52

开源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:串口相关函数

semonpic 发表于 2013-11-10 16:00:01

顶楼主,我是个外行,这个是什么协议,与modbusb比如何

oldbeginner 发表于 2013-11-10 16:33:15


初始化后,继续。

再返回到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 18:50:55

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,字面上无法理解。

这样,发送中断处理有了一个了解。
********************************************

oldbeginner 发表于 2013-11-10 18:52:09

semonpic 发表于 2013-11-10 16:00 static/image/common/back.gif
顶楼主,我是个外行,这个是什么协议,与modbusb比如何

我也是外行,正在学习,你说的哪个协议我见到过,但是不会。帖子里讲的是入门的串口通讯。

jetli 发表于 2013-11-10 19:24:19

oldbeginner 发表于 2013-11-10 18:52 static/image/common/back.gif
我也是外行,正在学习,你说的哪个协议我见到过,但是不会。帖子里讲的是入门的串口通讯。 ...

modbus 是工控设备 基本支持的一种基于485硬件的协议,

三菱那年头的老家伙,似乎是不支持,,{:titter:}

LJM4U 发表于 2013-11-10 20:14:44

MARK马克

tiger5 发表于 2013-11-10 20:44:07

好。。。
串口:环形队列+中断。
以前C51BBS上有一个所子写的,我觉得不错。

oldbeginner 发表于 2013-11-11 12:58:04

本帖最后由 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 14:49:24

本帖最后由 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:00:59

本帖最后由 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 17:09:02

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 18:21:34

oldbeginner 发表于 2013-11-11 17:09 static/image/common/back.gif
还剩下两个比较简单的函数
// 函数名称: UartSendString
//


理解了uart是如何工作后,就要进入我还没接触过IAP了。

http://www.amobbs.com/thread-5558672-1-1.html

秋原之风 发表于 2013-11-11 22:49:05

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--是什么意思?

oldbeginner 发表于 2013-11-12 13:03:06

秋原之风 发表于 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停一下。

XYY136 发表于 2015-7-3 22:08:54

克鲁克各国

ykwsxq 发表于 2015-10-21 15:07:34

{:smile:} 太感谢楼主了帮了我大忙

wuwei520 发表于 2015-11-25 15:07:21

好东西啊
页: [1]
查看完整版本: 开源PLC学习笔记05(再从51开始 通讯 UART)——2013_11_10