简易的基于任务的用状态机实现的modbus主机模式探讨
modbus协议已经十分普及了,本坛也能搜索到很多相关的帖子。使用modbus协议的仪器设备,以及终端模块也很常见易购,大家以后做项目,可以直接用现成的,省去了做终端的麻烦。
关于modbus的主机程序,看了其他网友的,要么挺复杂的,用在资源有限的小mcu上难度较大。要么过于简陋,编写维护都成问题。
我试着写了写,发现抓住要点后,程序可以做的很简单,发布出来大家一起探讨吧。我的实验条件有限,单片机是M8,编译平台是CV。
modbus通讯协议是一问一答的,发、收,以及数据处理过程的各个状态是很明确的,用状态机就很容易搭起程序框架,
并且使得各个状态间互不干扰,如在发送状态及收到数据后进行数据处理中,不再理会收到的数据,这样收发数据缓存还可以共用一个。
任务组织,将每一个发送、接收以及数据处理过程作为一个任务,程序都写在一个任务模块中,前后照应,便于编写维护。
由于发送及收到的数据在任务中都是确定的,所以收到的数据基本上可以直接使用。
每个任务的数据量够用即可,大数据任务可以拆分成多个小任务,如一次访问多个连续地址。这样收发缓存不用设置太大,crc校验可以直接用计算法,省去了查表计算用到的表格空间。
任务放在任务表中,每个任务表由任务定时器和任务程序指针构成。
任务定时器是可写的,必须放在RAM中。
任务程序指针可放在ROM中,也可以放在RAM中,以便动态改变任务。
当前任务号用变量bh表示
任务的程序结构很简单,switch -- case 结构,按当前任务状态跳转。
发送准备:发送数据装入收发缓存中
收到数据处理:
出错处理:可能的错误有超时,crc错误或数据长度不足,从机报错。
任务的其他状态都在mb_master()及中断程序中处理。
任务可以定时运行,条件终止,正常终止,出错重来。都是通过改变任务状态来实现的。
任务由主程序mb_master()调用,而mb_master()又是在系统中轮询的,轮询周期就是任务定时及超时定时的时基。
主程序框架:
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:
……
} 全部程序,不含串口初始化,不涉及其他程序部分
单片机是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;
}
}
不明觉厉 zhanan 发表于 2019-1-5 14:28
全部程序,不含串口初始化,不涉及其他程序部分
单片机是M8,编译平台CV
请教一下:三个任务里的状态10分支似乎没有执行到? MARK学习,很好的思路!!! 887799 发表于 2019-1-6 22:50
请教一下:三个任务里的状态10分支似乎没有执行到?
收到回传数据后状态变为8,在mb_master()中,如果数据基本没有错误则状态变为10。
状态10,是收到数据后进行处理的用的,任务中必须用的,即便是不理会收到的数据,也要将状态变成空闲。 mmbzt=(mmbbuf&0x80)?23:10; // 功能码最高位=1,从机报错
功能码和发送的一致,就认为数据基本正确,数据怎么处理就交给任务了。 本帖最后由 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: 谢谢分享。。。。。。 mark 状态机,modbus 写PLC程序也经常用状态机,但是基本不会用0,1,2,3,。。。这样定义状态,大多数会是0,100,200,300,。。。之类的,这样的话以后一不小心需要中间插入状态比较方便。 其实本坛armink已经在rtt上移植了modbus主机模式,是从freemodbus移植的
也有人是自己写的主机模式,看看是太简陋,这也是很多人说主机模式不稳定的原因 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
zhanan 发表于 2019-1-8 13:44
收到的数据经过检查没有错误,才认为可以进入数据处理状态,即状态10。
因为是简易系统,所以不想把数据 ...
多谢指点,是我看的不够仔细,没注意到case 8 这里少了一个break语句。 简单的从机模式: https://www.amobbs.com/thread-5705320-1-1.html?_dsign=a42585a8 还是过于简单了吧 学习了。谢谢!
页:
[1]