开源PLC学习笔记07(再从51开始 PLC指令)——2013_11_12
本帖最后由 oldbeginner 于 2013-11-12 16:43 编辑PLC的结构对我来说还是比较复杂的,前后逻辑联系非常紧密,幸亏提前有思想准备,否则早就半途而废了。
FX1NPrcosessing函数链接上位机和下位机之间的核心(因为IAP缘故,该函数像中间机)。
这节笔记将理解PLC指令,为了减小难度,只理解LD OUT SET RST END,元件只涉及x和y。然后理解这些指令是如何在单片机内部实现的。 楼主加油呀,分享一下 本帖最后由 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 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));
根据字面意思就很好理解,不是开头和并且未到末尾就待在循环体中。
***************************************************
支持楼主! 楼主会因为PLC学习笔记一炮而红。 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: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-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: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搞混了,在这个程序里如何区分我还是不太了解,在后面的复习中要专门理解一下。
mark一下 一直关注了 楼主都可以出书了. 好东西,真不错 楼主 您太强大的 那个报文你是怎么知道的呢 感谢楼主 把自己的东西分享出来
实例,指令LDX002,指令码为24 02,存储格式为02 24,转换成 ASCII 码传送为30 32 32 34。
则orderL=0x30
orderH=0x32
利用联合体概念, order=0x3230,则ppp=0x0230。
这里
orderL=0x30
orderH=0x32
有错吧,应该取的是二进制 不是ASCII啊 楼主大才呀!PLC玩的这么牛! ljt80158015 发表于 2016-6-8 14:32
实例,指令LDX002,指令码为24 02,存储格式为02 24,转换成 ASCII 码传送为30 32 32 34。
则orderL=0x30 ...
(引用自16楼)
2402-->02-24->30,32,32,34
0x0224-->0x2402
实际上漏了一步字符转hex
页:
[1]