搜索
bottom↓
回复: 17

简易的基于任务的用状态机实现的modbus主机模式探讨

[复制链接]

出0入0汤圆

发表于 2019-1-5 14:24:22 | 显示全部楼层 |阅读模式
modbus协议已经十分普及了,本坛也能搜索到很多相关的帖子。
使用modbus协议的仪器设备,以及终端模块也很常见易购,大家以后做项目,可以直接用现成的,省去了做终端的麻烦。
关于modbus的主机程序,看了其他网友的,要么挺复杂的,用在资源有限的小mcu上难度较大。要么过于简陋,编写维护都成问题。

我试着写了写,发现抓住要点后,程序可以做的很简单,发布出来大家一起探讨吧。我的实验条件有限,单片机是M8,编译平台是CV。

modbus通讯协议是一问一答的,发、收,以及数据处理过程的各个状态是很明确的,用状态机就很容易搭起程序框架,
并且使得各个状态间互不干扰,如在发送状态及收到数据后进行数据处理中,不再理会收到的数据,这样收发数据缓存还可以共用一个。


任务组织,将每一个发送、接收以及数据处理过程作为一个任务,程序都写在一个任务模块中,前后照应,便于编写维护。
由于发送及收到的数据在任务中都是确定的,所以收到的数据基本上可以直接使用。
每个任务的数据量够用即可,大数据任务可以拆分成多个小任务,如一次访问多个连续地址。这样收发缓存不用设置太大,crc校验可以直接用计算法,省去了查表计算用到的表格空间。

任务放在任务表中,每个任务表由任务定时器和任务程序指针构成。
任务定时器是可写的,必须放在RAM中。
任务程序指针可放在ROM中,也可以放在RAM中,以便动态改变任务。
当前任务号用变量bh表示


任务的程序结构很简单,switch -- case 结构,按当前任务状态跳转。
    发送准备:发送数据装入收发缓存中
   
    收到数据处理:
   
    出错处理:可能的错误有超时,crc错误或数据长度不足,从机报错。

任务的其他状态都在mb_master()及中断程序中处理。

任务可以定时运行,条件终止,正常终止,出错重来。都是通过改变任务状态来实现的。
任务由主程序mb_master()调用,而mb_master()又是在系统中轮询的,轮询周期就是任务定时及超时定时的时基。

本帖子中包含更多资源

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

x

出0入0汤圆

 楼主| 发表于 2019-1-5 14:25:26 | 显示全部楼层
主程序框架:
void mb_master(void)
{
     
  各任务定时器-1
  如果当前任务完成了,任务状态是空闲的,就去找下一个可运行任务,并将任务状态置为发送准备
  
  超时倒计时,减到0算超时,置任务状态为超时

  switch (任务状态)
  {
    case 发送准备:
      任务处理,任务处理中也只是处理发送准备部分
      如果仍然是发送准备状态,则补充完发送需要的其他数据,然后启动发送,并将状态变为正在发送

    case 收到数据整理:
       检查错误,crc校验,置状态为错误标志,若没有错误,改变状态为收到数据处理

    case 超时及数据处理:
       任务处理,处理任务除发送外的其他部分
  }
}
----
任务程序框架:
void rw1(void)  // 读模拟量
{
  switch(任务状态)
  {
    case 发送准备:
      装载发送数据,如果任务不具备运行条件,可以置任务状态为空闲状态,取消任务

    case 收到数据处理:
      写变量……,完成后,置任务状态为空闲状态。

    case 出错:
      出错处理,完成后,置任务状态为发送准备,重来一次,或转为空闲状态,忽略本次错误

  }
}
可以用宏定义将switch--case结构变为更为简洁的形式。
如用begin代替switch,任务程序就成这样了:
viod rw1(void)
{
  begin:
  ……

}

出0入0汤圆

 楼主| 发表于 2019-1-5 14:28:33 | 显示全部楼层
全部程序,不含串口初始化,不涉及其他程序部分
单片机是M8,编译平台CV

void rw1(void); // 任务声明
void rw2(void);
void rw3(void);

#define MBBMAX 3  /* 任务数 */
void (* flash mmbrwb[MBBMAX])(void)={rw1,rw2,rw3}; // 任务表在flash中
uchar mmbdsb[MBBMAX]; // 任务定时表,在RAM中
uchar mmbbh; // 当前任务号

#define MBZJG 1  /* 帧间隔时间(按波特率调整) */
#define MBSIZE 14  /* 缓存大小(12B数据+2Bcrc) */
uchar mmbbuf[MBSIZE]; // mod缓存
uchar mmbgs;  // mod数据个数,从1开始
uchar mmbjs;  // mod帧间计时(用于判断帧尾)/mod发送指针,从0开始
uchar mmbcs;  // 应答超时
uchar mmbzt=50; // mod状态,初始为任务空闲状态

#define DEkz PORTD.2 /* PD2为485发送控制 */
#define Dreg UDR     /* 收发数据寄存器 */
/***/
interrupt [USART_RXC] void mb_rx(void)  // 接收缓存非空中断
{
  uchar nrxd=Dreg; // 读接收缓存
  if(mmbzt<=2)
  {
    mmbjs=0;  // 复位帧间计时
    switch (mmbzt)
    {
      case 0:
        mmbgs=1; // 作为第一个数据
        if(nrxd==mmbbuf[0]) mmbzt=1; else mmbzt=2; // 第一个数据是站点地址,与发送地址一致,转后续字节,否则等待帧结束
        break;
      case 1:
        if(mmbgs<(MBSIZE-1)) mmbbuf[mmbgs++]=nrxd; else mmbzt=2; // 装入后续字节,超量则等待帧结束
        break;
    }
  }
}
/***/
interrupt [USART_TXC] void mb_tx(void)  // TXC发送完成中断 DRE→Dreg空中断
{
  if(mmbzt==6) // 发送中
  {
    if(mmbjs<mmbgs)
    {
      Dreg=mmbbuf[mmbjs++]; // 没发完,接着发下一个
    }
    else
    {
      DEkz=0;   // 发完后485芯片DE低电平
      mmbzt=0;  // 等待接收状态
    }
  }
}
/***/
void mb_zjjs(void)  // 帧间计时判断,定时(2mS)轮询
{
  if(mmbzt==1) {if(++mmbjs>MBZJG) mmbzt=8;} // 收到一帧,准备处理
  if(mmbzt==2) {if(++mmbjs>MBZJG) mmbzt=0;} // 等到帧结束,转等待接收
}
/***/
uint mb_crc16(uchar *np,uchar nlen)  // 计算crc16校验: 数据、字节数
{
  uchar nxh;
  uint ncrc =0xFFFF;
  while(nlen--)
  {
    ncrc^=*np++;
    for(nxh=0;nxh<8;nxh++)
    {
      if (ncrc & 0x0001)
      { ncrc>>=1; ncrc^=0xA001;}
      else { ncrc>>=1; }
    }
  }
  return (ncrc);
}
/******/
void mb_master(void)  // modbus主机模式主程序,定时调用,调用周期即为任务定时及超时计时的时基
{
  uchar nxh;
  uint ncrc;
     
  for(nxh=0; nxh<MBBMAX; nxh++)
  {
    if(mmbdsb[nxh]) mmbdsb[nxh]--; // 任务定时倒计时
    if(mmbzt==50)
    {
      mmbbh++; if(mmbbh>=MBBMAX) mmbbh=0; // 调度空闲时找下一个可运行的任务
      if(mmbdsb[mmbbh]==0) {mmbzt=5;}
    }
  }

  if((mmbzt<5)&&(--mmbcs==0)) mmbzt=21; // 应答超时倒计时,减到0算超时

  switch (mmbzt)
  {
    case 5: // 发送数据准备
      (*mmbrwb[mmbbh])();
      if(mmbzt==5)
      {
        ncrc=mb_crc16(mmbbuf,mmbgs); // 计算crc
        mmbbuf[mmbgs++]=ncrc;    // crc低字节
        mmbbuf[mmbgs++]=ncrc>>8; // crc高字节
        mmbjs=0; // 发送指针
        DEkz=1; Dreg=mmbbuf[mmbjs++]; // 485芯片DE高电平,启动发送
        mmbzt++; //zt=6正在发送
      }
      break;
    case 8:  // 收到数据整理
      mmbzt=22; // 字节数不够或crc错误
      if(mmbgs>=5) // 至少有5个字节
      {
        if(mb_crc16(mmbbuf,mmbgs)==0) // crc正确
        {
          mmbzt=(mmbbuf[1]&0x80)?23:10; // 功能码最高位=1,从机报错
        }
      }
    case 21:
      (*mmbrwb[mmbbh])(); // 收到数据、超时及出错处理
      break;
  }
}
/******/
/*         任务定时, 应答超时, 从机站号,   功能码,    地址,  数据 */
void mb_rwzb(uchar nds, uchar ncs, uchar nzh, uchar ngn, uint ndz, uint nsj)
{
  mmbdsb[mmbbh]=nds; // 任务定时
  mmbcs=ncs; // 应答超时
  mmbgs=0;   // 有效字节数
  mmbbuf[mmbgs++]=nzh;    // 0从机站号
  mmbbuf[mmbgs++]=ngn;    // 1功能码
  mmbbuf[mmbgs++]=ndz>>8; // 2 首地址高字节
  mmbbuf[mmbgs++]=ndz;    // 3 首地址低字节
  mmbbuf[mmbgs++]=nsj>>8; // 4 个数或其他高字节
  mmbbuf[mmbgs++]=nsj;    // 5 个数或其他低字节
}
/*** 任务模块 ***/
void rw1(void)  // 读模拟量
{
  switch(mmbzt)
  {
    case 5: // 任务准备
      mb_rwzb(100,5,  1,4,1,1);
      break;
    case 10: // 收到的数据
      mwd=((uint)mmbbuf[3]<<8)|mmbbuf[4]; // 读取到的温度值
    case 21: case 22: case 23: // 出错
      mmbzt=50; // 任务完成,忽略出错
      break;
  }
}
/***/
void rw2(void) // 写一个线圈
{
  static uchar nxhh;

  switch(mmbzt)
  {
    case 5:
      if(++nxhh>20) nxhh=0;
      if(nxhh>15)  mmbzt=50;  // 8个线圈轮流开停,中间停顿一下,通过读线圈任务可看到线圈的通断情况
      else
      { mb_rwzb(10,5,  1,5,nxhh>>1,(nxhh&0x01)?0x0000:0xFF00); }
      break;
    case 10:  
      mmbzt=50; // 任务完成
      break;
    case 21: case 22: case 23: // 出错
      mmbzt=5; // 出错重发,不可恢复错误,要防止任务阻塞
      break;
  }
}
/***/
void rw3(void)  // 读线圈
{
  switch(mmbzt)
  {
    case 5:
      mb_rwzb(5,5,  1,1,0,8);
      break;
    case 10: // 收到的数据
      mjdq=mmbbuf[3];  // 收到的线圈状态
    case 21: case 22: case 23: // 出错
      mmbzt=50; // 任务完成,忽略出错
      break;
  }
}

出100入85汤圆

发表于 2019-1-5 14:51:31 | 显示全部楼层
不明觉厉

出0入0汤圆

发表于 2019-1-6 22:50:05 | 显示全部楼层
zhanan 发表于 2019-1-5 14:28
全部程序,不含串口初始化,不涉及其他程序部分
单片机是M8,编译平台CV

请教一下:三个任务里的状态10分支似乎没有执行到?

出0入0汤圆

发表于 2019-1-6 23:29:24 | 显示全部楼层
MARK学习,很好的思路!!!

出0入0汤圆

 楼主| 发表于 2019-1-7 19:00:16 | 显示全部楼层
887799 发表于 2019-1-6 22:50
请教一下:三个任务里的状态10分支似乎没有执行到?

收到回传数据后状态变为8,在mb_master()中,如果数据基本没有错误则状态变为10。

状态10,是收到数据后进行处理的用的,任务中必须用的,即便是不理会收到的数据,也要将状态变成空闲。

出0入0汤圆

 楼主| 发表于 2019-1-7 19:09:07 | 显示全部楼层
    mmbzt=(mmbbuf[1]&0x80)?23:10; // 功能码最高位=1,从机报错

   功能码和发送的一致,就认为数据基本正确,数据怎么处理就交给任务了。

出0入0汤圆

发表于 2019-1-7 22:51:08 | 显示全部楼层
本帖最后由 887799 于 2019-1-7 22:56 编辑
zhanan 发表于 2019-1-7 19:09
    mmbzt=(mmbbuf[1]&0x80)?23:10; // 功能码最高位=1,从机报错

   功能码和发送的一致,就认为数据基 ...


你写的三个任务里是包含了5,10,21的状态处理,可是这三个任务的执行触发条件是在状态5,和21的时候执行的,状态10执行不到。
   
case 5: // 发送数据准备
      (*mmbrwb[mmbbh])();

    case 21:
      (*mmbrwb[mmbbh])(); // 收到数据、超时及出错处理

没有case 10:

出0入0汤圆

发表于 2019-1-8 08:05:02 来自手机 | 显示全部楼层
谢谢分享。。。。。。

出0入0汤圆

发表于 2019-1-8 09:01:30 | 显示全部楼层
mark 状态机,modbus

出0入8汤圆

发表于 2019-1-8 09:04:22 | 显示全部楼层
写PLC程序也经常用状态机,但是基本不会用0,1,2,3,。。。这样定义状态,大多数会是0,100,200,300,。。。之类的,这样的话以后一不小心需要中间插入状态比较方便。

出0入0汤圆

发表于 2019-1-8 11:00:16 | 显示全部楼层
其实本坛armink已经在rtt上移植了modbus主机模式,是从freemodbus移植的
也有人是自己写的主机模式,看看是太简陋,这也是很多人说主机模式不稳定的原因

出0入0汤圆

 楼主| 发表于 2019-1-8 13:44:14 | 显示全部楼层
887799 发表于 2019-1-7 22:51
你写的三个任务里是包含了5,10,21的状态处理,可是这三个任务的执行触发条件是在状态5,和21的时候执 ...

收到的数据经过检查没有错误,才认为可以进入数据处理状态,即状态10。
因为是简易系统,所以不想把数据检查搞复杂,只是简单地进行了:数据长度够不够,crc校验对不对,从机有没有返回出错信息。
想着尽量通用一些,其实再简化一些,或者再扩充一些都是可以的。
程序经过实物验证过,不是模拟的。

你是否觉得跳不到10上去?  这里我用的是排除法,并且用到了C里面的case的一个特点:
case 8:  // 收到数据整理
      mmbzt=22; // 字节数不够或crc错误                                                   先假设有错误
      if(mmbgs>=5) // 至少有5个字节
      {
        if(mb_crc16(mmbbuf,mmbgs)==0) // crc正确
        {
          mmbzt=(mmbbuf[1]&0x80)?23:10; // 功能码最高位=1,从机报错      错误排除,在这里变成10
        }
      }                                                                                                   这里没有break,接着下去进入任务程序了
    case 21:
      (*mmbrwb[mmbbh])(); // 收到数据、超时及出错处理                            这时可能的状态只有10、21、22、23

出0入0汤圆

发表于 2019-1-8 16:42:35 | 显示全部楼层
zhanan 发表于 2019-1-8 13:44
收到的数据经过检查没有错误,才认为可以进入数据处理状态,即状态10。
因为是简易系统,所以不想把数据 ...

多谢指点,是我看的不够仔细,没注意到case 8 这里少了一个break语句。

出0入0汤圆

 楼主| 发表于 2019-1-9 13:09:57 | 显示全部楼层
简单的从机模式: https://www.amobbs.com/thread-5705320-1-1.html?_dsign=a42585a8

出0入8汤圆

发表于 2019-1-9 14:00:30 来自手机 | 显示全部楼层
还是过于简单了吧

出0入0汤圆

发表于 2020-9-15 15:34:23 | 显示全部楼层
学习了。谢谢!
回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

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

GMT+8, 2024-4-20 03:01

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

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