oldbeginner 发表于 2013-12-15 21:16:41

开源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一样,无论是成果还是过程都是拿的出手的。




zl_123 发表于 2013-12-15 22:05:19

{:smile:}                                       

liangyurongde 发表于 2013-12-15 22:10:28

楼主能坚持这么久,很厉害!

jetli 发表于 2013-12-15 22:42:11

哈哈哈。。。lz还没看过 yy888 的一段卧底恩怨传奇故事吧{:titter:}{:lol:}

oldbeginner 发表于 2013-12-17 21:10:16

本帖最后由 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-23 10:07:05

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:59:12

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 11:35:23

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;
      }      
    }
}

这里就需要一步一步理解了。

waking 发表于 2013-12-25 11:26:23

佩服楼主这份锲而不舍的精神

oldbeginner 发表于 2013-12-28 08:04:29

本帖最后由 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-29 10:29:39

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-30 09:53:35

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);
                }
        }
。。。。
}

kyliao 发表于 2013-12-31 15:38:07

这个帖子很有趣,因为涉及到人气和利益的平衡问题,不同观点的讨论很实在。开源能吸引人气和小白支持,但是会伤害牛人的商业利益,能看出来都是在摸着石头过河。

这个想法很符合实际,不过开源一些已经停产的产品是没有问题的嘛,要不要来一点开放性大一点的,就像周星驰“功夫”里面的女孩说的裙子要开高一点。
页: [1]
查看完整版本: 开源PLC学习笔记19(51复习进阶 沿着简易PLC之路)——2013_12_15