前言
前面已经介绍了Modbus的通信流程以及主机的数据帧,此篇主要结合STM32的代码来进一步介绍从机端的帧格式以及整个通信过程。
从机帧格式
从机格式与上一篇的主机格式类似,从机会根据主机的命令和功能码返回对应信息,这里从机返回的地址、功能码是和主机发送的数据一致,数据段则是根据主机访问的范围而定。
RTU格式从机返回的帧为地址、功能码、总字节数、数据段、CRC校验码。
ASCLL的格式类似,在此不做赘述。
举个栗子:
这里以读取数据也就是功能码是03为例,其余功能码也是大同小异,想了解的可以私聊笔者获取代码。
如上图所示:主机发送的数据包为“04 03 00 00 00 09 85 99”
04是从设备地址;03是功能码,对应的功能是读取16位寄存器的值;
00 00 是起始寄存器位置,00 09是访问的个数。
85 99 是CRC校验码。
当从机4接收到这个指令后,就会解析出命令,并返回数据包:
“04 03 12 00 00 00 01 00 02 00 03 00 04 00 05 00 06 00 07 00 08 31 16 ”
04:从机地址;03功能码;0x12:字节数;00 00-----00 08是数据段,分别对应九个16位寄存器的高低八位。
31 16是CRC校验码。
查看代码中寄存器值得定义,可以发现与返回值一致。
STM32 Modbus_RTU与维控屏通信
了解完整个代码的主从帧和通信模式,接下来来试试应用;本示例以STM32和PLC常用的维控屏进行通信,主要实现
1.屏幕控制控件LED;
2.屏幕监控STM32LED灯状态;
3.屏幕监控按键的按下,并记录日志;
4.屏幕实时显示数字变量。
STM32代码
1.定时器
由于使用的是RTU的通信帧,在上一篇的介绍中可以知道RTU格式没有固定的包头和包尾,是通过3.5个字符的时间来作为包尾的,所以在为了实现正常通信,需要设置一个能够计时毫秒的定时器,根据然后根据自己的波特率进行计算,算出所需要的延时时间。例如使用9600时,就需要定时大于4ms时间,为了保证通信正常往往会留有余量,如下图就是使用的8ms来作为标志。
2串口收发
在完成定时器的初始化后,还需要能够保证串口的正常收发,笔者这里用的是STM32最小系统,自带CH340,维控屏也是使用的的电脑进行仿真,所以不需要针对类似485这样的硬件电路来对代码进行调整,只需要正常初始化串口就行。整体的代码框架如下:
// An highlighted block
/******************************************************************************
* 函数名 :main
* 描述 :主函数
* 参数 :无
* 返回 :无
* 编写者 :
* 编写日期 :
*******************************************************************************/
int main(void)
{
SystemInit();
delay_init(72);
CLI(); //关总中断
NVIC_Configuration();
General_Gpio_Init(); //通用GPIO口初始化
GeneralIOStructInit(); //初始化IO口结构体变量
Usart1_Init(115200); //串口1初始化
SEI(); //开总中断
IWDG_Init(4,6250);
user_printf(USART1,"初始化完毕\r\n");
delay_ms(1000);
delay_ms(1000);
while(1)
{
com1_rtu_slave();//modbus处理函数
AllModuleProDeal(); //所有运行代码
General_Gpio_Update(); //通用GPIO口状态更新
heartbeat(); //心跳检测
IWDG_Feed();//喂狗
}
}
3.数据包处理
modbus从机的处理函数如下(注意此代码是灵育教育的modbus处理并非笔者上述工程所示的处理函数,但是总体思路一致,这里给大家作参考):
// An highlighted block
void Mosbus_Event()
{
u16 crc;
u16 rccrc;
if(modbus.reflag==0) //没有收到MODbus的数据包
{
return ;
}
crc= crc16(&modbus.rcbuf[0], modbus.recount-2); //计算校验码
rccrc=modbus.rcbuf[modbus.recount-2]*256 + modbus.rcbuf[modbus.recount-1]; //收到的校验码
if(crc == rccrc) //数据包符号CRC校验规则
{
if(modbus.rcbuf[0] == modbus.myadd) //确认数据包是否是发给本设备的
{
switch(modbus.rcbuf[1]) //分析功能码
{
case 0: break;
case 1: break;
case 2: break;
case 3: Modbud_fun3(); break; //3号功能码处理
case 4: break;
case 5: break;
case 6: Modbud_fun6(); break; //6号功能码处理
case 7: break;
//....
}
}
else if(modbus.rcbuf[0] == 0) //广播地址
{
}
}
modbus.recount=0; //
modbus.reflag=0;
}
void Modbud_fun3() //3号功能码处理 ---主机要读取本从机的寄存器
{
u16 Regadd;
u16 Reglen;
u16 byte;
u16 i,j;
u16 crc;
Regadd=modbus.rcbuf[2]*256+modbus.rcbuf[3]; //得到要读取的寄存器的首地址
Reglen=modbus.rcbuf[4]*256+modbus.rcbuf[5]; //得到要读取的寄存器的数量
i=0;
modbus.Sendbuf[i++]=modbus.myadd;//本设备地址
modbus.Sendbuf[i++]=0x03; //功能码
byte=Reglen*2; //要返回的数据字节数
//modbus.Sendbuf[i++]=byte/256; //
modbus.Sendbuf[i++]=byte%256;
for(j=0;j<Reglen;j++)
{
modbus.Sendbuf[i++]=Reg[Regadd+j]/256;
modbus.Sendbuf[i++]=Reg[Regadd+j]%256;
}
crc=crc16(modbus.Sendbuf,i);
modbus.Sendbuf[i++]=crc/256; //
modbus.Sendbuf[i++]=crc%256;
RS485_RT_1; //
for(j=0;j<i;j++)
{
RS485_byte(modbus.Sendbuf[j]);
}
RS485_RT_0; //
}
void Modbud_fun6() //6号功能码处理
{
u16 Regadd;
u16 val;
u16 i,crc,j;
i=0;
Regadd=modbus.rcbuf[2]*256+modbus.rcbuf[3]; //得到要修改的地址
val=modbus.rcbuf[4]*256+modbus.rcbuf[5]; //修改后的值
Reg[Regadd]=val; //修改本设备相应的寄存器
//以下为回应主机
modbus.Sendbuf[i++]=modbus.myadd;//本设备地址
modbus.Sendbuf[i++]=0x06; //功能码
modbus.Sendbuf[i++]=Regadd/256;
modbus.Sendbuf[i++]=Regadd%256;
modbus.Sendbuf[i++]=val/256;
modbus.Sendbuf[i++]=val%256;
crc=crc16(modbus.Sendbuf,i);
modbus.Sendbuf[i++]=crc/256; //
modbus.Sendbuf[i++]=crc%256;
RS485_RT_1; //
for(j=0;j<i;j++)
{
RS485_byte(modbus.Sendbuf[j]);
}
RS485_RT_0; //
}
维控屏代码
维控屏这边就是拖动控件进行布局,然后绑定寄存器地址,根据自己的需求做一些界面即可。
总结
有关modbus通信的介绍就到此为止,如有不足之处欢迎指出,相关工程也会放到笔者的下载资源,有需要的自行下载。
合集
STM32 Modbus通信学习笔记——理论基础
STM32 Modbus通信学习笔记——通信流程
STM32 Modbus通信学习笔记—— 代码及示例
STM32 使用MODBUS与维控屏通信(modbus系列代码)