搜索
bottom↓
回复: 12

开源PLC学习笔记19(51复习进阶 沿着简易PLC之路)——2013_12_15

[复制链接]

出0入0汤圆

发表于 2013-12-15 21:16:41 | 显示全部楼层 |阅读模式
本帖最后由 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



第一阶段是以叶工的成果作为基石,然后重点在硬件设计上,并且有很多人都参与了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一样,无论是成果还是过程都是拿的出手的。




本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

出0入0汤圆

发表于 2013-12-15 22:05:19 | 显示全部楼层
                                         

出0入9汤圆

发表于 2013-12-15 22:10:28 | 显示全部楼层
楼主能坚持这么久,很厉害!

出0入0汤圆

发表于 2013-12-15 22:42:11 | 显示全部楼层
哈哈哈。。。lz还没看过 yy888 的一段卧底恩怨传奇故事吧  

出0入0汤圆

 楼主| 发表于 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= ... UEHc2c3JUDiOX-a-nJa

这篇文章看来需要理解一下。




本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

出0入0汤圆

 楼主| 发表于 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

仿真如下,

感觉时间不是很准,代码有些绕,现在也没理解。

但是,还是值得再去理解一下。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

出0入0汤圆

 楼主| 发表于 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[orderH >> 4])();

    } 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[2]) ( )

再复习一下函数散转,
code void (*key_list[16])(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还不能理解,先继续。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

出0入0汤圆

 楼主| 发表于 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[16])(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);       // OUT  T,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;
        }      
    }
}

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

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

出0入0汤圆

发表于 2013-12-25 11:26:23 | 显示全部楼层
佩服楼主这份锲而不舍的精神

出0入0汤圆

 楼主| 发表于 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函数,
分别如下(已简洁后),
*************************
  1. void _T100mS(void)
  2. {
  3.         unsigned char i;
  4.           if (Timer_100ms != 0)
  5.     {
  6.                 Timer_100ms--;

  7.           for (i=0; i<_T_num; i++ )
  8.         { if ((rTF[i/8].BYTE & (1 << (i%8))) != 0)
  9.             {
  10.                                 if (_T[i][0] < _T[i][1])  
  11.                                         { _T[i][0]++; }
  12.             }
  13.         }
  14.         }
  15. }
复制代码



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

  1. void mov_to_old(void)
  2. {
  3.   unsigned char i;

  4.   for (i=0; i<_T_num; i++ )
  5.     { if (_T[i][0] >= _T[i][1])
  6.                   {
  7.                         rT[((&_T[i][0]-&_T[0][0])/2)/8].BYTE |= 1<<(((&_T[i][0]-&_T[0][0])/2)%8);
  8.                 }
  9.           else
  10.                   {
  11.                         rT[((&_T[i][0]-&_T[0][0])/2)/8].BYTE &= ~(1 << (((&_T[i][0]-&_T[0][0])/2)%8));
  12.                  }  
  13.     }
  14. }
复制代码



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

已经看花眼了,非常不直观。

这是因为定时器的参数也比较多


现在最大的难题是,怎样把已知的这些整合在一起理解?

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

出0入0汤圆

 楼主| 发表于 2013-12-29 10:29:39 | 显示全部楼层
oldbeginner 发表于 2013-12-28 08:04
直接看代码是不够的,测试了一下,发现定时中断是影响定时时间最关键的因素,开源定时每5毫秒中断一次,则 ...

快要放弃理解时,突然想到如果只有一个定时器T0,代码是不是会变得简单。首先,

volatile signed   int   xdata  T[2];                  
//  位元件 T 内存分配

二维数组变成了一维数组,而且只有两个元素,T[0]和T[1],为何一个定时器要分配两个内存呢?先继续。

同样,
volatile TYPE_BIT_BYTE   data  rTF;         
//  T 得电失电标志位

volatile TYPE_BIT_BYTE   data  rT;      
//  位元件 T输出位

数组消失了,太好了。

变量简单多了,那么函数呢?
******************************

首先,定时器只有T0,变换变量后,

void T100mS(void)
{
        if (Timer_100ms != 0)
    {
                Timer_100ms--;

          if ((rTF.BYTE & 1) != 0)
                {
                if (T[0] < T[1])  
                        { T[0]++; }
                 }
            
    }
}

当T[0]<T[1]时,T[0]++;因为Timer_100ms每100毫秒会加1(定时中断函数),所以在5秒的时间内,只要满足T[0]<T[1],T[0]就会加到50;现在基本能猜出T[1]是用来放置定时时间的,当T[0]<T[1]时,是定时时间还未到。已经有进展了,继续。

void mov_to_old(void)
{

     if (T[0] >= T[1])
          {
                rT[0].BYTE |= 1<<0;
        }
     else
          {
                rT[0].BYTE &= ~(1 << 0);
         }  
}

从这个函数身上,可以确认,T[0]是记录时间变化的,而T[1]是定时值,当T[0]>=T[1]时,表示时间到。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]=0;
T[1]=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[1]=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[0]>=T[1]时,通过mov_to_old函数置1,即导通。

至此,只有1个定时器的实现就可以理解了。

有多个定时器,则按照同样方式理解,因为涉及到二维数组及指针,还有很多位运算技巧,形式变得比较复杂,对我这样的新手难上加难。

出0入0汤圆

 楼主| 发表于 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[0].BIT.BIT2
#define    _Y5_       rY[0].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);
                }
        }
。。。。
}

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

出0入0汤圆

发表于 2013-12-31 15:38:07 | 显示全部楼层
这个帖子很有趣,因为涉及到人气和利益的平衡问题,不同观点的讨论很实在。开源能吸引人气和小白支持,但是会伤害牛人的商业利益,能看出来都是在摸着石头过河。

这个想法很符合实际,不过开源一些已经停产的产品是没有问题的嘛,要不要来一点开放性大一点的,就像周星驰“功夫”里面的女孩说的裙子要开高一点。
回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-5-3 00:21

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

快速回复 返回顶部 返回列表