开源PLC学习笔记19(51复习进阶 沿着简易PLC之路)——2013_12_15
本帖最后由 oldbeginner 于 2013-12-15 22:07 编辑在学习开源PLC时,感觉到逻辑的相关性,要理解一句代码,可能需要重新复习上个月刚理解过的代码。比如现在要把所有代码移植到PROTEUS用51仿真上,就要做两件事,
1,把PLC指令补全;2、增加FX1N中的通讯功能。同时还要把内部flash读取改为EEPROM读取函数。
有两种复习方法,一种是按需复习,需要理解什么就复习什么;另一种是沿着简易PLC之路。这里我选择第二种,虽然效率可能会低一些,但是或许会有学习之外的收获。
***********************************************************************
1、简易PLC是在2009年3月底启动的,
http://www.amobbs.com/forum.php?mod=viewthread&tid=3261673
这个帖子很有趣,因为涉及到人气和利益的平衡问题,不同观点的讨论很实在。开源能吸引人气和小白支持,但是会伤害牛人的商业利益,能看出来都是在摸着石头过河。
2、2009年6月中旬,简易PLC第一阶段结束,硬件设计和DIY完成;重新做了PWM转C程序
http://www.amobbs.com/thread-3427108-1-2.html
http://www.amobbs.com/thread-3418305-1-1.html
http://cache.amobbs.com/bbs_upload782111/files_16/ourdev_457197.jpg
http://cache.amobbs.com/bbs_upload782111/files_17/ourdev_465104.JPG
第一阶段是以叶工的成果作为基石,然后重点在硬件设计上,并且有很多人都参与了DIY,为第二阶段打下了坚实基础。
3、2009年9月中旬,简易PLC第二阶段取得重大突破,并在月底完美结束
第二阶段分成了三个任务:
3.1 仿三菱 FX1N PLC 下载通讯协议 C Code示范程序
http://www.amobbs.com/thread-3579264-1-2.html
3.2 PLC 功能代码(无下载)
http://www.amobbs.com/thread-3589911-1-2.html
第三个任务就是综合下载通讯协议和功能代码
3.3 【DIY 简易型的 PLC】★ 实施方案-----完美结束
http://www.amobbs.com/thread-3257782-1-1.html
我的感觉,这是一个非常优秀的项目管理过程,很多公司项目管理团队本身都难以企及的。吸引我写开源PLC笔记除了PLC本身知识外,还有就是这一它的实现过程也非常吸引人。目前在论坛上还没有发现另一个开源项目能像简易PLC一样,无论是成果还是过程都是拿的出手的。
{:smile:} 楼主能坚持这么久,很厉害! 哈哈哈。。。lz还没看过 yy888 的一段卧底恩怨传奇故事吧{:titter:}{:lol:} 本帖最后由 oldbeginner 于 2013-12-17 21:13 编辑
学习思路计划这样,
1、这节笔记重点还是下位机,先单独通讯模拟;然后函数实现模拟(重点定时器);最后合并。
2、PWM转C也是需要理解的,因为它和下位机的关系密切,会放在另一节笔记当中。
3、硬件电路设计,硬件电路设计思想没有,但是在另一帖子对简易PLC下一代电路有过设计思路,可以作为借鉴;之前还要考证一下是否适合硬件设计入门,因为我还没有这方面知识。
********************************************************
首先是下位机通讯模拟,因为采用的上位机是FXGP,事实证明软件本身有些麻烦。
http://www.amobbs.com/thread-5563153-1-1.html
在问题1的帖子里,知道了要短接,为什么?我还需要了解一下,因为目前使用串口助手和使用FXGP效果不一致。
先理解一下,9600,n,8,1和 9600,e,7,1 的区别
9600,n,8,1
9600,e,7,1
下位机相应的函数为,
UartSendByte((unsigned char *)OrderSend1,8);
其中
unsigned char code OrderSend1[]={0x02,0x36,0x32,0x36,0x36,0x03,0x44,0x37};
为什么少了几个字符?需要找资料。
9600,e,7,1是什么?这个问题曾经理解过,但是不够,
搜一下,
http://bbs.21ic.com/icview-346408-1-1.html
计算、修改D7位的数据后再按9600.N.8.1的格式发送即可。D7位是偶校验位,需要计算,不能屏蔽。按9600.N.8.1的格式发送。
对下位机的影响,下位机接收后,需要把D7屏蔽得到正确ASCII码(SBUF&0x7f);发送时,D7需要置1吗?
还要参考
台达变频器和C51单片机之间的通讯应用
http://wenku.baidu.com/link?url=DTK-MqKowcK3mcfxkg394zJelt0pGtQm3jY7dZlW2tSVGTkVn3jgVtsOCa5YhaWZLgOjN325Pl4lfCeiu1Z6tmq8UEHc2c3JUDiOX-a-nJa
这篇文章看来需要理解一下。
oldbeginner 发表于 2013-12-17 21:10
学习思路计划这样,
1、这节笔记重点还是下位机,先单独通讯模拟;然后函数实现模拟(重点定时器);最后 ...
通讯难点在于通讯协议需要破解,我感觉可以单独作为一个项目。因为目前主要目的是学习,所以只利用现有的协议,不再花精力研究如何破解协议。
PLC功能如何实现上,其实也是有难度的,比如定时器,是如何实现的?感觉就非常难,很好奇开发者是如何想出的(或者借助什么帮助)?
要理解以下的指令,是如何实现的?
LD X2
OUT T1K50
LD T1
OUT Y5
翻译成三菱指令,
02 24 01 06 32 80 00 80
01 26 05 C5
仿真如下,
感觉时间不是很准,代码有些绕,现在也没理解。
但是,还是值得再去理解一下。 oldbeginner 发表于 2013-12-23 10:07
通讯难点在于通讯协议需要破解,我感觉可以单独作为一个项目。因为目前主要目的是学习,所以只利用现有的 ...
再复习一下PLC执行命令(仿真利用EEPROM,不能利用指针直接访问EEPROM地址)
void main_PLC(void)
{
CODE_p = CODE_START;
do{
orderL = ReadEEPROM(CODE_p);
CODE_p++;
orderH = ReadEEPROM(CODE_p);
CODE_p++;
ppp = order & 0xfff;
(*key_list)();
} while((CODE_p < (CODE_START+step)-1) && (CODE_p != CODE_START));
}
***************************************************
依次执行的指令是:
0x02
0x24
0x01
0x06
0x32
0x80
0x00
0x80
0x01
0x26
0x05
0xC5
****************************************************
此时,ppp=0x402;
然后调用函数 (*key_list) ( )
再复习一下函数散转,
code void (*key_list)(void)={
CMDFNC , // 0 (FNC应用指令)
CMDP , // 1 (P 应用指令)
LD , // 2 (LD指令, 2000+ppp, 扩展 Mp除外)
。。。。。
}
即执行 LD()函数,
void LD (void) // 2(LD指令, 2000+ppp, 扩展 Mp除外)
{
ACC_BIT <<= 1;
ACC_BIT |= RD_ppp(ppp);
}
调用了 RD_ppp ( 0x402 ) 函数
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 ( 0x402 ) 函数
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);
}
}
。。。。。
return(p);
}
a= 0x402 - 0x400 = 0x002;
p= rX + 0;
return rX;
复习一下rX,
位元件 X 存储位
再理解一下,LD函数过程
感觉有难度,*rX>>2还不能理解,先继续。
oldbeginner 发表于 2013-12-23 10:59
再复习一下PLC执行命令(仿真利用EEPROM,不能利用指针直接访问EEPROM地址)
void main_PLC(void)
{
main_PLC函数中的
do while循环进入下一轮
再复习一下函数散转,
code void (*key_list)(void)={
CMDFNC , // 0 (FNC应用指令)
CMDP , // 1 (P 应用指令)
LD , // 2 (LD指令, 2000+ppp, 扩展 Mp除外)
。。。。。
}
这时执行 CMDFNC函数,
void CMDFNC(void) // 0 (应用指令)
{
。。。。。
else if ((ppp >= 0x600) && (ppp < 0x800)) // 三字指令
{
WR_TK(ppp - 0x600); // OUTT,K 0000+(T)8000+xx 8000+yy
}
。。。。。
}
此时,ppp = 0x601,
调用
WR_TK ( 0x001);
************************************
void WR_TK(unsigned int a) // ,(K值写入T)
{
unsigned char i;
unsigned char *p;
unsigned int*Ip;
addr0L = ReadEEPROM(CODE_p);
CODE_p++;
addr0T = (ReadEEPROM(CODE_p) << 4) & 0xe0;
CODE_p++;
addr0H = ReadEEPROM(CODE_p);
CODE_p++;
addr0T |= (ReadEEPROM(CODE_p) & 0x1f);
CODE_p++;
指针移动了,看看结构
然后,
if (a < _T_num)
{
i = 1 <<(a % 8);
p = (unsigned char*)rTF + (a / 8);
Ip = _T + a;
if ((ACC_BIT & 0x01) != 0) // _Tx, _Tx_F, _TKx, K
{
if ((*p & i) == 0)
{
*p |= i; // rTF相应位置1
*Ip++ = 0;
*Ip = RD_ADDR(addr0, addr0T);
}
}
else
{ *p &= ~i; // rTF相应位清0
*Ip = 0;
}
}
}
这里就需要一步一步理解了。
佩服楼主这份锲而不舍的精神 本帖最后由 oldbeginner 于 2013-12-28 08:52 编辑
oldbeginner 发表于 2013-12-23 11:35
main_PLC函数中的
do while循环进入下一轮
直接看代码是不够的,测试了一下,发现定时中断是影响定时时间最关键的因素,开源定时每5毫秒中断一次,则时间差不多准确的。
**********************
void Timer0(void) interrupt 1 using 3
{
// //定时5000微秒
TH0=0xED; //重新给TH0赋初值
TL0=0xFF;
if (++Timer_5ms == 20)
{
Timer_100ms++;
Timer_5ms = 0;
}
}
定时中断函数,每5毫秒中断一次。
**********************
再回到主函数主循环,
while (1)
{
//更新IO端口,必须
RefreshIO();
//PLC执行去找二当家的,当老大就是好
main_PLC();
//100ms 定时子函数
_T100mS();
mov_to_old();
}
和定时器有关的有_T100mS和mov_to_old函数,
分别如下(已简洁后),
*************************
void _T100mS(void)
{
unsigned char i;
if (Timer_100ms != 0)
{
Timer_100ms--;
for (i=0; i<_T_num; i++ )
{ if ((rTF.BYTE & (1 << (i%8))) != 0)
{
if (_T < _T)
{ _T++; }
}
}
}
}
*****************************
void mov_to_old(void)
{
unsigned char i;
for (i=0; i<_T_num; i++ )
{ if (_T >= _T)
{
rT[((&_T-&_T)/2)/8].BYTE |= 1<<(((&_T-&_T)/2)%8);
}
else
{
rT[((&_T-&_T)/2)/8].BYTE &= ~(1 << (((&_T-&_T)/2)%8));
}
}
}
*******************************
已经看花眼了,非常不直观。
这是因为定时器的参数也比较多
现在最大的难题是,怎样把已知的这些整合在一起理解? oldbeginner 发表于 2013-12-28 08:04
直接看代码是不够的,测试了一下,发现定时中断是影响定时时间最关键的因素,开源定时每5毫秒中断一次,则 ...
快要放弃理解时,突然想到如果只有一个定时器T0,代码是不是会变得简单。首先,
volatile signed int xdataT;
//位元件 T 内存分配
二维数组变成了一维数组,而且只有两个元素,T和T,为何一个定时器要分配两个内存呢?先继续。
同样,
volatile TYPE_BIT_BYTE datarTF;
//T 得电失电标志位
volatile TYPE_BIT_BYTE datarT;
//位元件 T输出位
数组消失了,太好了。
变量简单多了,那么函数呢?
******************************
首先,定时器只有T0,变换变量后,
void T100mS(void)
{
if (Timer_100ms != 0)
{
Timer_100ms--;
if ((rTF.BYTE & 1) != 0)
{
if (T < T)
{ T++; }
}
}
}
当T<T时,T++;因为Timer_100ms每100毫秒会加1(定时中断函数),所以在5秒的时间内,只要满足T<T,T就会加到50;现在基本能猜出T是用来放置定时时间的,当T<T时,是定时时间还未到。已经有进展了,继续。
void mov_to_old(void)
{
if (T >= T)
{
rT.BYTE |= 1<<0;
}
else
{
rT.BYTE &= ~(1 << 0);
}
}
从这个函数身上,可以确认,T是记录时间变化的,而T是定时值,当T>=T时,表示时间到。rT是输出。
这样就完成了一半的理解,下面再结合指令一起理解了(定时器只能用T0)。
******************************************
LD X2
OUT T0K50
LD T0
OUT Y5
先考虑设置定时器部分,
0x02
0x24
0x00
0x06
0x32
0x80
0x00
0x80
结合之前的理解,直接进入到WR_TK函数,因为只有一个定时器,参数a就是0,这里省略,同时省略了CODE_p
void WR_TK( )
{
unsigned char *p;
unsigned int*Ip;
p = &rTF;
Ip = T;
if ((ACC_BIT & 0x01) != 0)
{
if ((*p & 1) == 0)
{
*p |= 1; // rTF相应位置1
*Ip++ = 0;
*Ip = RD_ADDR(addr0, addr0T);
}
}
else
{ *p &= ~1; // rTF相应位清0
*Ip = 0;
}
}
ACC_BIT就是X2的值(只看最后一位二进制),如果X2接通,那么执行
*p |= 1; // rTF相应位置1
*Ip++ = 0;
*Ip = RD_ADDR(addr0, addr0T);
即
T=0;
T=RD_ADDR(0x0032, 0);
再来看
int RD_ADDR(unsigned int a, unsigned char c)
{
signed int Ia;
signed int *Ip;
if ((c & 0xe0) == 0x00) //m=0, K 以十六进制数表示读入
{ Ia = a;
}
else ;
return(Ia);
}
即
T=0x0032;(十进制就是50)
******************************************
然后执行
LD T0
OUT Y5
0x00
0x26
0x05
0xC5
void LD (void) // 2(LD指令, 2000+ppp, 扩展 Mp除外)
{
ACC_BIT <<= 1;
ACC_BIT |= RD_ppp(ppp);
}
ACC_BIT非常重要(可以参考笔记1),因为只有定时器0,所以ppp=0x600固定
unsigned char RD_ppp(unsigned int a) // (读入点内容)
{
unsigned char n;
unsigned char *p;
p = ADDR_int_ppp(a);
n = *p;
return(n & 0x01);
}
p = ADDR_int_ppp(0x600);
char* ADDR_int_ppp(unsigned int a)
{
。。。。
else if(a<0x800)
{ a -= 0x600;
if (a < _T_num)
{ p = (unsigned char*)rT + (a / 8);
}
}
。。。
return(p);
}
p = (unsigned char*)rT + (a / 8);
即
p=&rT;
return &rT;
所以
ACC_BIT = rT & 0x01;
这样确认rT确实是定时器0的输出,当T>=T时,通过mov_to_old函数置1,即导通。
至此,只有1个定时器的实现就可以理解了。
有多个定时器,则按照同样方式理解,因为涉及到二维数组及指针,还有很多位运算技巧,形式变得比较复杂,对我这样的新手难上加难。
oldbeginner 发表于 2013-12-29 10:29
快要放弃理解时,突然想到如果只有一个定时器T0,代码是不是会变得简单。首先,
volatile signed int...
上面的代码确实挺复杂,如果一上来我就学这些,也许到现在可能还没入门。这里有必要再复习一下笔记一里的指令函数,和开源PLC的指令函数区别。
还是老规矩,选择一个简单的功能来理解,
比如
LD X2
OUT Y5
开源PLC执行的函数依次为:
void LD (void) // 2(LD指令, 2000+ppp, 扩展 Mp除外)
{ ACC_BIT <<= 1;
ACC_BIT |= RD_ppp(ppp);
}
void OUTYM(void) // C (OUT指令, C000+ppp, 仅对Y,M有效)
{ WR_YM(ppp,ACC_BIT);
}
********************************
对比一下笔记一中的相应函数,
void _LD(unsigned char x2)
{
ACC_BIT <<= 1;
ACC_BIT |= x2;
}
_OUT(_Y5_);
其中,
#define_OUT(a) a = OUTx();
char OUTx(void)
{
return(ACC_BIT & 0x01);
}
#define _X2_ rX.BIT.BIT2
#define _Y5_ rY.BIT.BIT5
*********************************
从直观和简洁上,笔记一中代码更适合入门理解,然后再来理解开源PLC中的指令。
开源PLC的指令借助了RD_ppp
char* ADDR_int_ppp(unsigned int a) // (读入int点内容,返回地址绝对指针)
{
unsigned char *p;
a &= 0xfff;
。。。。
else if(a<0x500)
{ a -= 0x400;
if (a < _X_num)
{ p = (unsigned char*)rX + (a / 8);
}
}
。。。。
}
这个帖子很有趣,因为涉及到人气和利益的平衡问题,不同观点的讨论很实在。开源能吸引人气和小白支持,但是会伤害牛人的商业利益,能看出来都是在摸着石头过河。
这个想法很符合实际,不过开源一些已经停产的产品是没有问题的嘛,要不要来一点开放性大一点的,就像周星驰“功夫”里面的女孩说的裙子要开高一点。
页:
[1]