zhanan 发表于 2019-1-5 14:24:22

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

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

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

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


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

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


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

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

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

zhanan 发表于 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:
……

}

zhanan 发表于 2019-1-5 14:28:33

全部程序,不含串口初始化,不涉及其他程序部分
单片机是M8,编译平台CV

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

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

#define MBZJG 1/* 帧间隔时间(按波特率调整) */
#define MBSIZE 14/* 缓存大小(12B数据+2Bcrc) */
uchar mmbbuf; // 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 void mb_rx(void)// 接收缓存非空中断
{
uchar nrxd=Dreg; // 读接收缓存
if(mmbzt<=2)
{
    mmbjs=0;// 复位帧间计时
    switch (mmbzt)
    {
      case 0:
      mmbgs=1; // 作为第一个数据
      if(nrxd==mmbbuf) mmbzt=1; else mmbzt=2; // 第一个数据是站点地址,与发送地址一致,转后续字节,否则等待帧结束
      break;
      case 1:
      if(mmbgs<(MBSIZE-1)) mmbbuf=nrxd; else mmbzt=2; // 装入后续字节,超量则等待帧结束
      break;
    }
}
}
/***/
interrupt void mb_tx(void)// TXC发送完成中断 DRE→Dreg空中断
{
if(mmbzt==6) // 发送中
{
    if(mmbjs<mmbgs)
    {
      Dreg=mmbbuf; // 没发完,接着发下一个
    }
    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) mmbdsb--; // 任务定时倒计时
    if(mmbzt==50)
    {
      mmbbh++; if(mmbbh>=MBBMAX) mmbbh=0; // 调度空闲时找下一个可运行的任务
      if(mmbdsb==0) {mmbzt=5;}
    }
}

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

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

whatcanitbe 发表于 2019-1-5 14:51:31

不明觉厉

887799 发表于 2019-1-6 22:50:05

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



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

ITOP 发表于 2019-1-6 23:29:24

MARK学习,很好的思路!!!

zhanan 发表于 2019-1-7 19:00:16

887799 发表于 2019-1-6 22:50
请教一下:三个任务里的状态10分支似乎没有执行到?

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

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

zhanan 发表于 2019-1-7 19:09:07

    mmbzt=(mmbbuf&0x80)?23:10; // 功能码最高位=1,从机报错

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

887799 发表于 2019-1-7 22:51:08

本帖最后由 887799 于 2019-1-7 22:56 编辑

zhanan 发表于 2019-1-7 19:09
    mmbzt=(mmbbuf&0x80)?23:10; // 功能码最高位=1,从机报错

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

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

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

没有case 10:

Excellence 发表于 2019-1-8 08:05:02

谢谢分享。。。。。。

L7科创 发表于 2019-1-8 09:01:30

mark 状态机,modbus

ljx289 发表于 2019-1-8 09:04:22

写PLC程序也经常用状态机,但是基本不会用0,1,2,3,。。。这样定义状态,大多数会是0,100,200,300,。。。之类的,这样的话以后一不小心需要中间插入状态比较方便。

myxiaonia 发表于 2019-1-8 11:00:16

其实本坛armink已经在rtt上移植了modbus主机模式,是从freemodbus移植的
也有人是自己写的主机模式,看看是太简陋,这也是很多人说主机模式不稳定的原因

zhanan 发表于 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&0x80)?23:10; // 功能码最高位=1,从机报错      错误排除,在这里变成10
      }
      }                                                                                                   这里没有break,接着下去进入任务程序了
    case 21:
      (*mmbrwb)(); // 收到数据、超时及出错处理                            这时可能的状态只有10、21、22、23

887799 发表于 2019-1-8 16:42:35

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

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

zhanan 发表于 2019-1-9 13:09:57

简单的从机模式: https://www.amobbs.com/thread-5705320-1-1.html?_dsign=a42585a8

icoyool 发表于 2019-1-9 14:00:30

还是过于简单了吧

jlljh 发表于 2020-9-15 15:34:23

学习了。谢谢!
页: [1]
查看完整版本: 简易的基于任务的用状态机实现的modbus主机模式探讨