oldbeginner 发表于 2013-11-12 16:37:30

开源PLC学习笔记07(再从51开始 PLC指令)——2013_11_12

本帖最后由 oldbeginner 于 2013-11-12 16:43 编辑

PLC的结构对我来说还是比较复杂的,前后逻辑联系非常紧密,幸亏提前有思想准备,否则早就半途而废了。


FX1NPrcosessing函数链接上位机和下位机之间的核心(因为IAP缘故,该函数像中间机)。

这节笔记将理解PLC指令,为了减小难度,只理解LD OUT SET RST END,元件只涉及x和y。然后理解这些指令是如何在单片机内部实现的。

shenarlon 发表于 2013-11-12 21:53:53

楼主加油呀,分享一下

oldbeginner 发表于 2013-11-13 13:08:18

本帖最后由 oldbeginner 于 2013-11-13 14:54 编辑


首先是理解指令,参考资料是http://www.amobbs.com/thread-3303497-1-1.html

这个参考资料内容还是比较多,因为我暂时只想理解LD OUT SET RST END X Y,所以我只摘录相关的内容。
参考资料分了5部分来介绍,我只用前4部分。

一、标注说明:
注1:下列未特别标注者,均为十六进制数格式。
注2:p 为寄存器名称编号,pp 为32位寄存器名称编号,表示为十进制数,但在存储格式中以十六进制数 ppp 表示。
注7:X,Y为寄存器位(点)元件名称(包含地址)。

二、寄存器位(点)元件地址 ppp = 0-FFF,其分布如下:
X0-377(八进制) =(0-FF)   + 400=400-4FF
Y0-377(八进制) =(0-FF)   + 500=500-5FF

三、存储格式及报文发送格式说明:

存储格式为字型,低在先,高在后(如指令END,指令码为00 0F,存储为0F 00。指令LDX002,指令码为24 02,存储为02 24)。
在报文发送时,以字为单位传送,低字节在先。在字节传送过程中,高位在先,低位在后,转换成 ASCII 码 后传送(即以存储格式传送)。
指令LDX002,指令码为24 02,存储格式为02 24,转换成 ASCII 码传送为30 32 32 34。
注:报文发送方式未经上机验证!

四、三菱指令格式说明:
2、三菱指令命名: LD= 2               
                OUT = C (仅对Y,M有效)
                SET = D (仅对Y,M有效)
                RST = E (仅对Y,M有效)
3、三菱基本指令分类:
纯单字指令
                  END    000F
单字指令
                  LD   2000+ppp         ;(扩展 Mp除外)
                  OUT    C000+ppp         ;(仅对Y,M有效)
                  SET    D000+ppp         ;(仅对Y,M有效)
                  RST    E000+ppp         ;(仅对Y,M有效)
5、三菱应用指令中的数据/地址格式:

16数据/地址格式   = 8m00+xx ,8n00+yy

***************************************
确实有些抽象,这时候结合代码一起了解。

当FX1NProcessing函数把三菱上位机程序写到PLC程序空间后,运行正常后,就进入了
//   PLC 入口 函数main_PLC();                                                      //
//------------------------------PLC51x.c----------------------------------------------//

void main_PLC (void)
{
CODE_p = (unsigned char code *)CODE_START;
Pi = 0x01;
do{
      orderL = *CODE_p;
      CODE_p++;
      orderH = *CODE_p;
      CODE_p++;
      ppp = order & 0xfff;
      (*key_list)();
    } while((CODE_p < CODE_END) && (CODE_p != CODE_START));
}

就是上面这个函数把FLASH中PLC程序空间的数据翻译成相应的函数并执行。

这个函数虽然代码不长,但其实还是比较复杂的。


oldbeginner 发表于 2013-11-13 14:39:18

本帖最后由 oldbeginner 于 2013-11-13 15:07 编辑

oldbeginner 发表于 2013-11-13 13:08 static/image/common/back.gif
首先是理解指令,参考资料是http://www.amobbs.com/thread-3303497-1-1.html

这个参考资料内容还是比较 ...

映射表有4种,
code (*key_list)();
code (*key_list_1)();
code (*key_list_2)();
code (*key_list_3)();

因为我打算实现的功能简单,所以只接触第一种最简单的映射,而且16个元素只会用到4个。
code (*key_list)()={
          CMDFNC ,   // 0 (FNC应用指令)   
          CMDP   ,   // 1 (P 应用指令)
          LD   ,   // 2 (LD指令, 5000+ppp, 扩展 Mp除外)
          LDI    ,   // 3 (LDI指令, 5000+ppp, 扩展 Mp除外)   
          AND    ,   // 4 (AND指令, 5000+ppp, 扩展 Mp除外)   
          ANI    ,   // 5 (ANI指令, 5000+ppp, 扩展 Mp除外)   
          OR   ,   // 6 (OR指令, 5000+ppp, 扩展 Mp除外)   
          ORI    ,   // 7 (ORI指令, 5000+ppp, 扩展 Mp除外)   
          CMDER,   // 8 (多字指令,第二字及以后有效)
          CMDER,   // 9
          CMDER,   // A (多字指令,第二字及以后有效, 仅对M1536-M3071有效,需加偏移量200)
          CMDER,   // B (Pn指令, 仅对CJ,CALL有效)
          OUTYM,   // C (OUT指令, 仅对Y,M有效)
          SETYM,   // D (SET指令, 仅对Y,M有效)
          RSTYM,   // E (RST指令, 仅对Y,M有效)
          CMDCH };   // F (纯单字指令)

为什么用code,
1,把段码放在code里,是为了节省RAM。如果放在前256字节内,查表时只要八位地址即可,所以会快些。ROM读取不会慢。单片机执行的每一条指令都是从ROM区读取的。
ROM区的内容是只读的,所以你不能将改变(程序运行中改变)的数组放进去。http://zhidao.baidu.com/question/121263820.html?qbl=relate_question_1

不理解为什么没有unsigned int或unsigned char搭配code。先继续。
*****************************************************************
先看一下代码中的定义
#define   CODE_START   0x8000         // PLC执行代码缓冲区首地址

但是另外还有一个
unsigned char code CODE_START = {0x0,0x24,0x8,0x0,0x0,0x88,0x0,0x28,0x0,0xd5,0x1,0x24,0x9,
0x0,0x1,0x88,0x1,0x28,0x0,0xe5,0xca,0x1,0x2,0x84,0x2,0xc8,0x2,0x28,0x1,0xd5,0xcb,0x1,0x2,0x84,0x3,
0xc8,0x3,0x28,0x1,0xe5,0x0f,0x0
                          };       
两者是什么关系?怎样联系?先继续

volatile unsigned char code *CODE_p;
1、然后理解第1句代码,
CODE_p = (unsigned char code *)CODE_START;
就是让CODE_p指向PLC执行代码缓冲区

2、第2句,Pi = 0x01;
volatile unsigned intdataPi;   
因为Pi是16位的数,所以Pi=0x0001,作用应该在下面了解。

3、最后是do while循环。先看循环体,最后看循环条件。
循环体
      orderL = *CODE_p;
          CODE_p++;
      orderH = *CODE_p;
          CODE_p++;
      ppp = order & 0xfff;
      (*key_list)();

4、看定义
#define   orderL    order0.BYTES.BYTEL    // 命令位地址缓冲区低位
#define   orderH    order0.BYTES.BYTEH    // 命令位地址缓冲区高位

还要再看order0的定义
volatile TYPE_BYTES_WORDxdata order0;       

还要再看TYPE_BYTES_WORD的定义
typedef union {   //重点注意C编辑器的多字节变量类型的高低字节前后排列次序
struct { unsigned char BYTEH;   //可以按字节寻址
         unsigned char BYTEL;   //可以按字节寻址
         }BYTES;
unsigned intWORD;       //可以按字寻址
}TYPE_BYTES_WORD;//定义一个既能按字节寻址也可按字寻址的新变量类型

类似笔记02中的联合体,幸亏当时理解了,现在难度就减小了。

用同样的方法来理解,
unsigned char   orderL
unsigned char   orderH
将orderL和orderH都理解位8位无符号数,他俩又可以组成一个字。

5、   orderL = *CODE_p;
          CODE_p++;
      orderH = *CODE_p;
          CODE_p++;
很好理解,利用CODE_p把PLC缓冲区的内容赋值给orderL和orderH。

6、      ppp = order & 0xfff;
先看定义
volatile unsigned int ppp;
#define   order   order0.WORD         // 命令位地址缓冲区

order就是由orderL和orderH合并而成的,根据一、标注说明:注释2pp 为32位寄存器名称编号,表示为十进制数,但在存储格式中以十六进制数 ppp 表示。
所以把order和0xfff与一下,然后赋值给ppp。

实例,指令LDX002,指令码为24 02,存储格式为02 24,转换成 ASCII 码传送为30 32 32 34。
则orderL=0x30
orderH=0x32
利用联合体概念, order=0x3230,则ppp=0x0230。

7、 (*key_list)();
利用实例来理解,orderH向右移4位,则orderH=0x03,这里不是很理解。如果向左移,才有意义。这样orderH=0x02
然后*key_list=*key_list=LD。
此时调用了LD函数,下一步再理解LD函数,目前只需了解通过映射表,LD函数被调用了。
void LD   (void)    // 2(LD指令, 2000+ppp, 扩展 Mp除外)
{ ACC_BIT <<= 1;
ACC_BIT |= RD_ppp(ppp);
}

8、最后是循环条件while((CODE_p < CODE_END) && (CODE_p != CODE_START));
根据字面意思就很好理解,不是开头和并且未到末尾就待在循环体中。

***************************************************


bsz84 发表于 2013-11-13 15:00:01

支持楼主!

kinsno 发表于 2013-11-13 15:07:42

楼主会因为PLC学习笔记一炮而红。

oldbeginner 发表于 2013-11-13 16:08:47

oldbeginner 发表于 2013-11-13 14:39 static/image/common/back.gif
映射表有4种,
code (*key_list)();
code (*key_list_1)();


再回到指令函数中
void LD   (void)    // 2(LD指令, 2000+ppp, 扩展 Mp除外)
{
ACC_BIT <<= 1;
ACC_BIT |= RD_ppp(ppp);
}
和笔记01中的不同了。

多了一个RD_ppp(ppp);

unsigned char RD_ppp(unsigned int a)    // (读入点内容)
{
unsigned char n;
unsigned char *p;
p = ADDR_int_ppp(a);
n = *p >> (a % 8);
return(n & 0x01);
}

又调用了一个很长的函数
//    函数名称:   ADDR_int_ppp                                                             //
//    函数类型:   char* ADDR_int_ppp(unsigned int a)                                       //
//                                                                                                                           //
//    功能描述:读入并列的n位软元件点的起始地址,返回地址绝对指针                        //
//                                                                                                                           //
//    入口参数:unsigned inta ...... 读入软元件点的起始地址                            //
//                                                                                                                           //
//    出口参数:返回地址绝对指针

char* ADDR_int_ppp(unsigned int a)    // (读入int点内容,返回地址绝对指针)
{
unsigned char *p;
a &= 0xfff;
if (a<0x400)
    {
      if (a < _S_num)
      {
          p= (unsigned char*)rS + (a / 8);
      }
    }
else if (a<0x500)
    {
       a -= 0x400;
      if (a < _X_num)
      {
         p = (unsigned char*)rX + (a / 8);
      }
    }
else if (a<0x600)
    {
       a -= 0x500;
      if (a < _Y_num)
      {
      p = (unsigned char*)rY + (a / 8);
      }
    }
。。。。。。。
return(p);
}

查看定义
#define   _X_num      48    // 48个输入端口,    编号:X0-X57
#define   _Y_num      48    // 48个输出端口,    编号:Y0-Y57

volatile TYPE_BIT_BYTE   datarX
#define   _X_BYTE   (_X_num + 7) / 8    // 48个输入端口,所占内存字节数

rX就是笔记01中rX,在笔记01中把rX理解为结构体
现在同样 结构体 rX

取一段来理解,a就是ppp,寄存器名称,16进制表示
。。。。。。
else if (a<0x500)
    {
       a -= 0x400;
      if (a < 48)
      {
         p = (unsigned char*)rX + (a / 8);
      }
    }
。。。。。。
return(p);

1、a=a-0x400,暂时不理解,可能和下列注释有关
                //      编写读写代码须知
                //    1.8000 以上地址与 PLC程序相关;
                //    2.4000以上地址与 PLC寄存器[数据寄存器0x4000~0x7e40,31.125K (HEX).特殊寄存器0x0e00~0x0ec0,384字节 (HEX)]相关

2、如果a<48,因为设定的x端口只有48个。

3、假设a=40,则 p为 rX的地址,a/8是用来分组的。

4、返回该地址。

******************************
再进入
unsigned char RD_ppp(unsigned int a)    // (读入点内容)
{
unsigned char n;
unsigned char *p;
p = ADDR_int_ppp(a);
n = *p >> (a % 8);
return(n & 0x01);
}

此时 p指向rX
然后,
n=rX>>0,即n=rX
然后返回 rX & 0000 0001。

*********************************
再进入
void LD   (void)    // 2(LD指令, 2000+ppp, 扩展 Mp除外)
{
ACC_BIT <<= 1;
ACC_BIT |= RD_ppp(ppp);
}

ACC_BIT<<=1确保最低位为0,
然后
ACC_BIT=rX & 0000 0001

***********************************

继续采用笔记01中的方法,联合LD和OUT一起理解


void OUTYM(void)    // C (OUT指令, C000+ppp, 仅对Y,M有效)
{
    WR_YM(ppp,ACC_BIT);
}
相当于
WR_YM(ppp, rX & 0000 0001);

还需要了解
//    函数名称:   WR_YM                                                                  //
//    函数类型:   void WR_YM(unsigned int a,unsigned char i)                               //
//                                                                                           //
//    功能描述:将1点软元件的内容写入Y,M存储器                                           //
//                                                                                           //
//    入口参数:unsigned inta ...... 读入软元件点的起始地址   
void WR_YM(unsigned int a,unsigned char i)    // (写入Y,M点内容)
{
unsigned char *p;
a &= 0xfff;
i &= 0x01;
if ((a>=0x500) && (a<0x600))
    {
      a -= 0x500;
      if (a < _Y_num)
         { p = (unsigned char*)rY + (a / 8);
             if (i == 0)   *p &= ~(1 << (a % 8));
               else*p |= 1 << (a % 8);
         }
    }
else if ((a>=0x800) && (a<0xe00))
    {
      a -= 0x800;
      if (a < _M_num)
         { p = (unsigned char*)rM + (a / 8);
             if (i == 0)   *p &= ~(1 << (a % 8));
               else*p |= 1 << (a % 8);
         }
    }
else ;
}

判断条件暂时都忽略,只看
             if (i == 0)   
                  *p &= ~(1 << (a % 8));
               else
                     *p |= 1 << (a % 8);
在这里,i就是ACC_BIT,而a是输出Y的端口号,例如0。

如果ACC_BIT=1,即x40闭合,此时 *p的最低位为1,即端口y0是1。

还是有些绕,暂时对这个复杂过程有个初步理解。


oldbeginner 发表于 2013-11-13 20:36:03

本帖最后由 oldbeginner 于 2013-11-13 20:54 编辑

oldbeginner 发表于 2013-11-13 16:08 static/image/common/back.gif
再回到指令函数中
void LD   (void)    // 2(LD指令, 2000+ppp, 扩展 Mp除外)
{

在ADDR_int_ppp函数和WR_YM 都和寄存器位置相关,

现在需要理解一下再继续,

在参考资料,http://www.amobbs.com/thread-3303497-1-1.html
二、寄存器位(点)元件地址 ppp = 0-FFF,其分布如下:

    寄存器名称   =位地址 + 偏移量 = 实际地址ppp
S0-1023      =(0-3FF)      =   0-3FF
X0-377(八进制) =(0-FF)   + 400=400-4FF
Y0-377(八进制) =(0-FF)   + 500=500-5FF

x输入端最多有256个,用x0~x255表示,用16进制的地址映射就是, 400-4FF。
所以在计算具体端口时,要先减去0x400。下面的计数中,a是形参,实际的x寄存器名代入,
a -= 0x400;    现在可以理解,减去偏移量,得到位地址。
然后,    if (a < 48)
      {
         p = (unsigned char*)rX + (a / 8);
      }
也可以理解了,因为48是定义时已经制定了的最大端口数,所以a必须小于48。
把rX理解为8位bit组成的结构数组,利用a/8+rX找到该位的具体地址。

y输出端也是同理理解,所以在WR_YM中,会有
a -= 0x500;

四、三菱指令格式说明:
单字指令
                  LD   2000+ppp         ;(扩展 Mp除外)
例如计算 LD X02的命令格式
首先,X02的地址=0x02+0x400=0x402

然后 LD X02就是    0x2000+0x402=0x2402

最后把16进制转换成ascii码,
16进制的命令格式      0x    2    4    0   2
转换为ascii命令格式         ‘2’   ‘4’   ‘0’   ‘2’

上述的计算和转换都是在PC上位机中进行的,然后上位机通过UART发送到单片机,
存储格式为字型(搜了一下,16位),低在先,高在后 ‘0’   ‘2’   ‘2’   ‘4’
                                           即存储为   0x0224
                                             
在字型传送过程中,字型高位在先,低位在后‘0’   ‘2’   ‘2’   ‘4’
                                                         
                                                   
****************************************************************

再复习一下报文格式,下载格式,
PC机发送   字节数: 0025, 数据: STX,"E11","805C","06",'0','2','2','4','0','3','C','5','0','F','0','0',ETX,"69"
                                                            // PC机发出写PLC 805CH地址处连续06H字节(3步程序)数据指令

单片机接收到。。。'0','2','2','4'。。。。后,在指定的地址805C处
开始写入
这一功能是由FX1NProcessing函数中的Write Code函数中的这一句来实现的,WriteFlash(WriteAddr,(unsigned char *)(Buffer+10),(unsigned char)WriteLen);

上位机程序下载结束后,单片机运营交由main_PLC函数掌控,其再通过key_list把 '0','2','2','4'。。,转换成 LD()函数,并把寄存器地址代入。相当于执行了 LD X02。



oldbeginner 发表于 2013-11-14 15:33:27

oldbeginner 发表于 2013-11-13 20:36 static/image/common/back.gif
在ADDR_int_ppp函数和WR_YM 都和寄存器位置相关,

现在需要理解一下再继续,


在上位机中,OUT SET RST适用所有寄存器,但是在单片机中需要分类对待,因为刚开始只处理Y输出,所以只会用到以下三个函数,
void OUTYM(void)    // C (OUT指令, C000+ppp, 仅对Y,M有效)
{
          WR_YM(ppp,ACC_BIT);
}

void SETYM(void)    // D (SET指令, D000+ppp, 仅对Y,M有效)
{
   if ((ACC_BIT & 0x01) != 0)
               WR_YM(ppp,1);
}

void RSTYM(void)    // E (RST指令, E000+ppp, 仅对Y,M有效)
{
   if ((ACC_BIT & 0x01) != 0)
             WR_YM(ppp,0);
}
*******************************************************
三个函数中都含有WR_YM函数,根据字面意思就是write y或m寄存器。功能好理解,是怎样实现的,

WR_YM函数需要两个输入变量:
1、ppp,寄存器的偏移地址(16进制),该地址是如何计算的必须了解,例如我们写的是y寄存器,则
寄存器名称   =位地址 + 偏移量 = 实际地址ppp
Y0-377(八进制) =(0-FF)   + 500=500-5FF

2、另外一个输入变量是ACC_BIT,在SETYM函数中只有ppp和1两个输入,实际上是把ACC_BIT=1代入了,相当于一个特例。同理RSTYM中是把ACC_BIT=0代入了。

ACC_BIT是怎样确定的?因为目前只学习LD函数,ACC_BIT是由LD函数决定的,而且物理意义非常明确,LD函数是用来检测输入点(触点)是导通还是断开,导通,则把ACC_BIT置1,反之复位。

知道了这两个输入,就可以开始逐步了解WR_YM函数了,
1、void WR_YM(unsigned int a,unsigned char i)    // (写入Y,M点内容)
该函数用了两个形参,a表示ppp,i表示ACC_BIT

2、 unsigned char *p;
定义了一个指针,是什么作用?继续看

3、a &= 0xfff;
    a就是命令和ppp的组合体,它的值是在main_PLC函数中赋予的,赋值过程有点复杂,会在第2遍理解main_PLC函数时详细展开。
    这里就是对a就是过滤作用,去掉命令,只要所需的ppp,
    例如
   OUT Y05就是    0xC000+0x505=0xC505
   a= 0xC505 & 0x0fff = 0x 0505,即得到所需的ppp,好理解。

4、i &= 0x01;
    i就是ACC_BIT,它的值是由前面的LD函数来确认的,因为只需要最后1位,所以过滤前7位,也好理解。

5、 if ((a>=0x500) && (a<0x600));
   根据实际地址大小确定是y寄存器还是m寄存器,谁让这个函数要同时处理y寄存器和m寄存器呢?Y0-377(八进制) =(0-FF)   + 500=500-5FF

6、a -= 0x500;
    寄存器名称   =位地址 + 偏移量 = 实际地址ppp,得到偏移量

7、if (a < _Y_num) 即if (a < 48)
    因为本程序只定义最多48个输出端口,所以偏移量不能超出。

8、p = (unsigned char*)rY + (a / 8);
   rY结构和rX一致,在笔记01中详细理解过了。因为rY[ ]有8个位元素(端口),所以利用a/8来选择具体的rY[ ](8个位元素)。

9、if (i == 0)   *p &= ~(1 << (a % 8));
   如果对应的输入x是断开的,则相应的输出端口是~1=0,
   1 << (a % 8),是把1赋值到相应的位置,利用a%8求出具体的端口(8个位元素中的某一个)。
   然后再取反,即0。

10、else*p |= 1 << (a % 8);
   反之,则相应端口输出1。

因为目前只考虑输出y,所以这个函数理解到此就可以了。


oldbeginner 发表于 2013-11-14 16:15:39

本帖最后由 oldbeginner 于 2013-11-14 16:20 编辑

oldbeginner 发表于 2013-11-14 15:33 static/image/common/back.gif
在上位机中,OUT SET RST适用所有寄存器,但是在单片机中需要分类对待,因为刚开始只处理Y输出,所以只会 ...
再回到main_PLC函数中,

假设在FX1XProcessing中写入了
PC机发送   字节数: 0025, 数据: STX,"E11","805C","06",'0','2','2','4','0','5','C','5','0','F','0','0',ETX,"69"
                                                            // PC机发出写PLC 805CH地址处连续06H字节(3步程序)数据指令


则在main_PLC执行时,是如下情况
1、main_PLC是从0x8000开始执行的;
2、执行到0x805C后如下图,





此时,通过(*key_list[])()调用了LD函数,具体输入端口x由ppp确定。
等价于 LD X02

同理分析 '0' '5' 'C' '5'
即 OUT Y05

功能上就是,X2决定Y5。

第1次理解时,把16进制数和ASCII搞混了,在这个程序里如何区分我还是不太了解,在后面的复习中要专门理解一下。

mvpgpz 发表于 2013-11-14 17:14:33

mark一下 一直关注了

cjc83com 发表于 2013-11-25 16:14:04

楼主都可以出书了.

hys668 发表于 2014-10-2 14:25:07

好东西,真不错

willnie 发表于 2014-10-5 08:49:28

楼主 您太强大的   那个报文你是怎么知道的呢

ykwsxq 发表于 2015-10-16 13:33:20

感谢楼主 把自己的东西分享出来

ljt80158015 发表于 2016-6-8 14:32:53

实例,指令LDX002,指令码为24 02,存储格式为02 24,转换成 ASCII 码传送为30 32 32 34。
则orderL=0x30
orderH=0x32
利用联合体概念, order=0x3230,则ppp=0x0230。


这里
orderL=0x30
orderH=0x32
有错吧,应该取的是二进制 不是ASCII啊

ruzi 发表于 2017-4-20 11:02:59

楼主大才呀!PLC玩的这么牛!
页: [1]
查看完整版本: 开源PLC学习笔记07(再从51开始 PLC指令)——2013_11_12