Modbus多机通信例程

给从机下发不同的指令,从机去执行不同的操作,这个就是判断一下功能码即可,和我们前边学的实用串口例程是类似的。多机通信,无非就是添加了一个设备地址判断而已,难度也不是很大。我们找了一个modbus调试精灵,通过设置设备地址,读写寄存器的地址以及数值数量等参数,可以直接替代串口调试助手,比较方便的下发多个字节的数据,如图1所示。我们先来就图中的设置和数据来对modbus做进一步的分析,图中的数据来自于调试精灵与我们接下来要讲的例程之间的交互。
图1modbus调试精灵
如图:我们的usb转485模块虚拟出的是com5,波特率9600,无校验位,数据位是8位,1位停止位,设备地址假设为1。
写寄存器的时候,如果我们要把01写到一个地址是0000的寄存器地址里,点一下“写入”,就会出现发送指令:010600000001480a。我们来分析一下这帧数据,其中01是设备地址,06是功能码,代表写寄存器这个功能,后边跟0000表示的是要写入的寄存器的地址,0001就是要写入的数据,480a就是crc校验码,这是软件自动算出来了。而根据modbus协议,当写寄存器的时候,从机成功完成该指令的操作后,会把主机发送的指令直接返回,我们的调试精灵会接收到这样一帧数据:010600000001480a。
假如我们现在要从寄存器地址0002开始读取寄存器,并且读取的数量是2个。点一下“读出”,就会出现发送指令:01030002000265cb。其中01是设备地址,03是功能码,代表写寄存器这个功能,0002就是读寄存器的起始地址,后一个0002就是要读取2个寄存器的数值,65cb就是crc校验。而接收到的数据是:01030400000000fa33。其中01是设备地址,03是功能码,04代表的是后边读到的数据字节数是4个,00000000分别是地址为0002和0003的寄存器内部的数据,而fa33就是crc校验了。
似乎越来越明朗了,所谓的modbus这种通信协议,无非就是主机下发了不同的指令,从机根据指令的判断来执行不同的操作而已。由于我们的开发板没有modbus功能码那么多相应的功能,我们在程序中定义了一个数组reggroup[5],相当于5个寄存器,此外又定义了第6个寄存器,控制蜂鸣器,通过下发不同的指令我们改变寄存器组的数据或者改变蜂鸣器的开关状态。在modbus协议里寄存器的地址和数值都是16位的,即2个字节,我们默认高字节是0x00,低字节就是数组reggroup对应的值。其中地址0x0000到0x0004对应的就是reggroup数组中的元素,我们写入的同时把数字又显示到我们的lcd1602液晶上,而0x0005这个地址,写入0x00,蜂鸣器就不响,写入任何其他数字,蜂鸣器就报警。我们单片机的主要工作也就是解析串口接收的数据执行不同操作,也就是主要在rs485.c这个文件中了
/***********************rs485.c文件程序源代码*************************/
#include<reg52.h>
#include<intrins.h>
sbitrs485_dir=p1^7;//rs485方向选择引脚
bitflagoncetxd=0;//单次发送完成标志,即发送完一个字节
bitcmdarrived=0;//命令到达标志,即接收到上位机下发的命令
unsignedcharcntrxd=0;
unsignedcharpdatabufrxd[40];//串口接收缓冲区
unsignedcharreggroup[5];//modbus寄存器组,地址为0x00~0x04
externbitflagbuzzon;
externvoidlcdshowstr(unsignedcharx,unsignedchary,constunsignedchar*str);
externunsignedintgetcrc16(unsignedchar*ptr,unsignedcharlen);
voidconfiguart(unsignedintbaud)//串口配置函数,baud为波特率
{
rs485_dir=0;//rs485设置为接收方向
scon=0x50;//配置串口为模式1
tmod&=0x0f;//清零t1的控制位
tmod|=0x20;//配置t1为模式2
th1=256-(11059200/12/32)/baud;//计算t1重载值
tl1=th1;//初值等于重载值
et1=0;//禁止t1中断
es=1;//使能串口中断
tr1=1;//启动t1
}
unsignedcharuartread(unsignedchar*buf,unsignedcharlen)//串口数据读取函数,数据接收指针buf,读取数据长度len,返回值为实际读取到的数据长度
{
unsignedchari;
if(len>cntrxd)//读取长度大于接收到的数据长度时,
{
len=cntrxd;//读取长度设置为实际接收到的数据长度
}
for(i=0;i<len;i++)//拷贝接收到的数据
{
*buf=bufrxd[i];
buf++;
}
cntrxd=0;//清零接收计数器
returnlen;//返回实际读取长度
}
voiddelayx10us(unsignedchart)//软件延时函数,延时时间(t*10)us
{
do{
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
}while(--t);
}
voiduartwrite(unsignedchar*buf,unsignedcharlen)//串口数据写入函数,即串口发送函数,待发送数据指针buf,数据长度len
{
rs485_dir=1;//rs485设置为发送
while(len--)//发送数据
{
flagoncetxd=0;
sbuf=*buf;
buf++;
while(!flagoncetxd);
}
delayx10us(5);//等待最后的停止位完成,延时时间由波特率决定
rs485_dir=0;//rs485设置为接收
}
voiduartdriver()//串口驱动函数,检测接收到的命令并执行相应动作
{
unsignedchari;
unsignedcharcnt;
unsignedcharlen;
unsignedcharbuf[30];
unsignedcharstr[4];
unsignedintcrc;
unsignedcharcrch,crcl;
if(cmdarrived)//有命令到达时,读取处理该命令
{
cmdarrived=0;
len=uartread(buf,sizeof(buf));//将接收到的命令读取到缓冲区中
if(buf[0]==0x01)//核对地址以决定是否响应命令,本例本机地址为0x01
{
crc=getcrc16(buf,len-2);//计算crc校验值
crch=crc>>8;
crcl=crc&0xff;
if((buf[len-2]==crch)&&(buf[len-1]==crcl))//判断crc校验是否正确
{
switch(buf[1])//按功能码执行操作
{
case0x03://读取一个或连续的寄存器
if((buf[2]==0x00)&&(buf[3]<=0x05))//寄存器地址支持0x0000~0x0005
{
if(buf[3]<=0x04)
{
i=buf[3];//提取寄存器地址
cnt=buf[5];//提取待读取的寄存器数量
buf[2]=cnt*2;//读取数据的字节数,为寄存器数*2,因modbus定义的寄存器为16位
len=3;
while(cnt--)
{
buf[len++]=0x00;//寄存器高字节补0
buf[len++]=reggroup[i++];//低字节
}
}
else//地址0x05为蜂鸣器状态
{
buf[2]=2;//读取数据的字节数
buf[3]=0x00;
buf[4]=flagbuzzon;
len=5;
}
break;
}
else//寄存器地址不被支持时,返回错误码
{
buf[1]=0x83;//功能码最高位置1
buf[2]=0x02;//设置异常码为02-无效地址
len=3;
break;
}
case0x06://写入单个寄存器
if((buf[2]==0x00)&&(buf[3]<=0x05))//寄存器地址支持0x0000~0x0005
{
if(buf[3]<=0x04)
{
i=buf[3];//提取寄存器地址
reggroup[i]=buf[5];//保存寄存器数据
cnt=reggroup[i]>>4;//显示到液晶上
if(cnt>=0xa)
str[0]=cnt-0xa+'a';
else
str[0]=cnt+'0';
cnt=reggroup[i]&0x0f;
if(cnt>=0xa)
str[1]=cnt-0xa+'a';
else
str[1]=cnt+'0';
str[2]='\0';
lcdshowstr(i*3,0,str);
}
else//地址0x05为蜂鸣器状态
{
flagbuzzon=(bit)buf[5];//寄存器值转换为蜂鸣器的开关
}
len-=2;//长度-2以重新计算crc并返回原帧
break;
}
else//寄存器地址不被支持时,返回错误码
{
buf[1]=0x86;//功能码最高位置1
buf[2]=0x02;//设置异常码为02-无效地址
len=3;
break;
}
default://其它不支持的功能码
buf[1]|=0x80;//功能码最高位置1
buf[2]=0x01;//设置异常码为01-无效功能
len=3;
break;
}
crc=getcrc16(buf,len);//计算crc校验值
buf[len++]=crc>>8;//crc高字节
buf[len++]=crc&0xff;//crc低字节
uartwrite(buf,len);//发送响应帧
}
}
}
}
voiduartrxmonitor(unsignedcharms)//串口接收监控函数
{
staticunsignedcharcntbkp=0;
staticunsignedcharidletmr=0;
if(cntrxd>0)//接收计数器大于零时,监控总线空闲时间
{
if(cntbkp!=cntrxd)//接收计数器改变,即刚接收到数据时,清零空闲计时
{
cntbkp=cntrxd;
idletmr=0;
}
else
{
if(idletmr<5)//接收计数器未改变,即总线空闲时,累积空闲时间
{
idletmr+=ms;
if(idletmr>=5)//空闲时间超过4个字节传输时间即认为一帧命令接收完毕
{
cmdarrived=1;//设置命令到达标志
}
}
}
}
else
{
cntbkp=0;
}
}
voidinterruptuart()interrupt4//uart中断服务函数
{
if(ri)//接收到字节
{
ri=0;//手动清零接收中断标志位
if(cntrxd<sizeof(bufrxd))//接收缓冲区尚未用完时,
{
bufrxd[cntrxd++]=sbuf;//保存接收字节,并递增计数器
}
}
if(ti)//字节发送完毕
{
ti=0;//手动清零发送中断标志位
flagoncetxd=1;//设置单次发送完成标志
}
}
/***********************lcd1602.c文件程序源代码*************************/
#include<reg52.h>
#definelcd1602_dbp0
sbitlcd1602_rs=p1^0;
sbitlcd1602_rw=p1^1;
sbitlcd1602_e=p1^5;
voidlcdwaitready()//等待液晶准备好
{
unsignedcharsta;
lcd1602_db=0xff;
lcd1602_rs=0;
lcd1602_rw=1;
do
{
lcd1602_e=1;
sta=lcd1602_db;//读取状态字
lcd1602_e=0;
}while(sta&0x80);//bit7等于1表示液晶正忙,重复检测直到其等于0为止
}
voidlcdwritecmd(unsignedcharcmd)//写入命令函数
{
lcdwaitready();
lcd1602_rs=0;
lcd1602_rw=0;
lcd1602_db=cmd;
lcd1602_e=1;
lcd1602_e=0;
}
voidlcdwritedat(unsignedchardat)//写入数据函数
{
lcdwaitready();
lcd1602_rs=1;
lcd1602_rw=0;
lcd1602_db=dat;
lcd1602_e=1;
lcd1602_e=0;
}
voidlcdshowstr(unsignedcharx,unsignedchary,constunsignedchar*str)//显示字符串,屏幕起始坐标(x,y),字符串指针str
{
unsignedcharaddr;
//由输入的显示坐标计算显示ram的地址
if(y==0)
addr=0x00+x;//第一行字符地址从0x00起始
else
addr=0x40+x;//第二行字符地址从0x40起始
//由起始显示ram地址连续写入字符串
lcdwritecmd(addr|0x80);//写入起始地址
while(*str!='\0')//连续写入字符串数据,直到检测到结束符
{
lcdwritedat(*str);
str++;
}
}
voidlcdinit()//液晶初始化函数
{
lcdwritecmd(0x38);//16*2显示,5*7点阵,8位数据接口
lcdwritecmd(0x0c);//显示器开,光标关闭
lcdwritecmd(0x06);//文字不动,地址自动+1
lcdwritecmd(0x01);//清屏
}
关于crc校验的算法,如果不是专门学习校验算法本身,大家可以不去研究这个程序的细节,文档直接给我们提供了函数,我们直接调用即可。
/***********************crc16.c文件程序源代码*************************/
unsignedintgetcrc16(unsignedchar*ptr,unsignedcharlen)
{
unsignedintindex;
unsignedcharcrch=0xff;//高crc字节
unsignedcharcrcl=0xff;//低crc字节
unsignedcharcodetabh[]={//crc高位字节值表
0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41,0x01,0xc0,
0x80,0x41,0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41,
0x00,0xc1,0x81,0x40,0x00,0xc1,0x81,0x40,0x01,0xc0,
0x80,0x41,0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40,
0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41,0x00,0xc1,
0x81,0x40,0x01,0xc0,0x80,0x41,0x01,0xc0,0x80,0x41,
0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41,0x00,0xc1,
0x81,0x40,0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41,
0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41,0x01,0xc0,
0x80,0x41,0x00,0xc1,0x81,0x40,0x00,0xc1,0x81,0x40,
0x01,0xc0,0x80,0x41,0x01,0xc0,0x80,0x41,0x00,0xc1,
0x81,0x40,0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40,
0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41,0x01,0xc0,
0x80,0x41,0x00,0xc1,0x81,0x40,0x00,0xc1,0x81,0x40,
0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40,0x01,0xc0,
0x80,0x41,0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40,
0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41,0x01,0xc0,
0x80,0x41,0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41,
0x00,0xc1,0x81,0x40,0x00,0xc1,0x81,0x40,0x01,0xc0,
0x80,0x41,0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41,
0x01,0xc0,0x80,0x41,0x00,0xc1,0x81,0x40,0x01,0xc0,
0x80,0x41,0x00,0xc1,0x81,0x40,0x00,0xc1,0x81,0x40,
0x01,0xc0,0x80,0x41,0x01,0xc0,0x80,0x41,0x00,0xc1,
0x81,0x40,0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41,
0x00,0xc1,0x81,0x40,0x01,0xc0,0x80,0x41,0x01,0xc0,
0x80,0x41,0x00,0xc1,0x81,0x40
};
unsignedcharcodetabl[]={//crc低位字节值表
0x00,0xc0,0xc1,0x01,0xc3,0x03,0x02,0xc2,0xc6,0x06,
0x07,0xc7,0x05,0xc5,0xc4,0x04,0xcc,0x0c,0x0d,0xcd,
0x0f,0xcf,0xce,0x0e,0x0a,0xca,0xcb,0x0b,0xc9,0x09,
0x08,0xc8,0xd8,0x18,0x19,0xd9,0x1b,0xdb,0xda,0x1a,
0x1e,0xde,0xdf,0x1f,0xdd,0x1d,0x1c,0xdc,0x14,0xd4,
0xd5,0x15,0xd7,0x17,0x16,0xd6,0xd2,0x12,0x13,0xd3,
0x11,0xd1,0xd0,0x10,0xf0,0x30,0x31,0xf1,0x33,0xf3,
0xf2,0x32,0x36,0xf6,0xf7,0x37,0xf5,0x35,0x34,0xf4,
0x3c,0xfc,0xfd,0x3d,0xff,0x3f,0x3e,0xfe,0xfa,0x3a,
0x3b,0xfb,0x39,0xf9,0xf8,0x38,0x28,0xe8,0xe9,0x29,
0xeb,0x2b,0x2a,0xea,0xee,0x2e,0x2f,0xef,0x2d,0xed,
0xec,0x2c,0xe4,0x24,0x25,0xe5,0x27,0xe7,0xe6,0x26,
0x22,0xe2,0xe3,0x23,0xe1,0x21,0x20,0xe0,0xa0,0x60,
0x61,0xa1,0x63,0xa3,0xa2,0x62,0x66,0xa6,0xa7,0x67,
0xa5,0x65,0x64,0xa4,0x6c,0xac,0xad,0x6d,0xaf,0x6f,
0x6e,0xae,0xaa,0x6a,0x6b,0xab,0x69,0xa9,0xa8,0x68,
0x78,0xb8,0xb9,0x79,0xbb,0x7b,0x7a,0xba,0xbe,0x7e,
0x7f,0xbf,0x7d,0xbd,0xbc,0x7c,0xb4,0x74,0x75,0xb5,
0x77,0xb7,0xb6,0x76,0x72,0xb2,0xb3,0x73,0xb1,0x71,
0x70,0xb0,0x50,0x90,0x91,0x51,0x93,0x53,0x52,0x92,
0x96,0x56,0x57,0x97,0x55,0x95,0x94,0x54,0x9c,0x5c,
0x5d,0x9d,0x5f,0x9f,0x9e,0x5e,0x5a,0x9a,0x9b,0x5b,
0x99,0x59,0x58,0x98,0x88,0x48,0x49,0x89,0x4b,0x8b,
0x8a,0x4a,0x4e,0x8e,0x8f,0x4f,0x8d,0x4d,0x4c,0x8c,
0x44,0x84,0x85,0x45,0x87,0x47,0x46,0x86,0x82,0x42,
0x43,0x83,0x41,0x81,0x80,0x40
};
while(len--)//计算指定长度的crc
{
index=crch^*ptr++;
crch=crcl^tabh[index];
crcl=tabl[index];
}
return((crch<<8)|crcl);
}
/***********************main.c文件程序源代码*************************/
voidconfigtimer0(unsignedintms);
externvoidlcdinit();
externvoidconfiguart(unsignedintbaud);
externvoiduartrxmonitor(unsignedcharms);
externvoiduartdriver();
voidmain()
{
ea=1;//开总中断
configtimer0(1);//配置t0定时1ms
configuart(9600);//配置波特率为9600
lcdinit();//初始化液晶
while(1)
{
uartdriver();
}
}
voidconfigtimer0(unsignedintms)//t0配置函数
{
unsignedlongtmp;
tmp=11059200/12;//定时器计数频率
tmp=(tmp*ms)/1000;//计算所需的计数值
tmp=65536-tmp;//计算定时器重载值
tmp=tmp+34;//修正中断响应延时造成的误差
t0rh=(unsignedchar)(tmp>>8);//定时器重载值拆分为高低字节
t0rl=(unsignedchar)tmp;
tmod&=0xf0;//清零t0的控制位
tmod|=0x01;//配置t0为模式1
th0=t0rh;//加载t0重载值
tl0=t0rl;
et0=1;//使能t0中断
tr0=1;//启动t0
}
voidinterrupttimer0()interrupt1//t0中断服务函数
{
th0=t0rh;//定时器重新加载重载值
tl0=t0rl;
if(flagbuzzon)//蜂鸣器鸣叫或关闭
buzz=~buzz;
else
buzz=1;
uartrxmonitor(1);//串口接收监控
}