
目录
之前我们详细介绍了Modbus-RTU协议,从机代码部分,从机代码可以参考:
下面我们对其主机如何编写做一个简单的说明,本章只是对03功能的码的使用,更多功能码,可以参考上方链接的功能码部分,根据本章思路进行拓展。
注:本章只是对之前从机篇章的一个延伸,只是对用到的部分进行简单描述,详细描述可以参考上方链接,搞懂从机的收发原理,主机其实就是加了一个数据发送。
1. 概述
进行一个简单的描述,Modbus 串行链路协议是一个主-从协议。在同一时刻,只有一个主节点连接于总线,一个或多个子节点 (最大编号为 247 ) 连接于同一个串行总线。
主节点以两种模式对子节点发出 Modbus 请求:单播模式和广播模式。
我们想要实现对某一从机设备温湿度数据的采集,需要采用单播模式, 主节点以特定地址访问某个子节点,子节点接到并处理完请求后,子节点向主节点返回一个报文(一个'应答')。在这种模式,一个 Modbus 事务处理包含 2 个报文:一个来自主节点的请求,一个来自子节点的应答。
这里我找了一个就有Modbus协议的温湿度传感器,其应答方式为,主机需要发送的问询帧:
| 地址码 | 功能码 | 起始地址 | 数据长度 | 校验码低位 | 校验码高位 |
| 0x01 | 0x03 | 0x00 0x00 | 0x00 0x02 | 0xC4 | 0x0B |
从机也就是温湿度传感器做出应答,假如此时温度值为-10.1℃,湿度为65.8%RH,那么此时从机的应道数据为:
| 地址码 | 功能码 | 返回有效字节数 | 湿度值 | 温度值 | 校验码低位 | 校验码高位 |
| 0x01 | 0x03 | 0x04 | 0x02 0x92 | 0xFF 0x9B | 0x5A | 0x3D |
温度计算:
当温度低于 0 ℃ 时温度数据以补码的形式上传。
温度:FF9B H(十六进制)= -101 => 温度 = -10.1℃
湿度计算:
湿度:292 H (十六进制)= 658 => 湿度 = 65.8%RH
2. 代码编写
大概了解一下主机需要发送的数据,我们直接开始编写代码,这里可以直接使用我们之前编写的从机代码,在其上直接修改。
2.1 数据发送
我们知道,在Modbus当中,从机设备不能主动向主机设备发送数据,因此需要主机设备向从机设备发送问询帧,告诉从机设备需要向我发送什么数据,这里我们可以直接使用之前写RS485的时候写的数组发送函数:
//串口发送一个数组
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
uint16_t i;
USART_REDE_TX_MODE_H;
for (i = 0; i < Length; i ++) //遍历数组
{
Serial_SendByte(Array[i]); //依次调用Serial_SendByte发送每个字节数据
}
USART_REDE_RX_MODE_L;
}
通过创建一个数组进行数据发送,如:
arr[8] = {0x01,0x03,0x00,0x00,0x00,0x02,0xC4,0x0B};
Serial_SendArray(arr,8);
不过这样会有一个不方便的一点,不利于后续调整,因为发送的数据后两位是CRC校验后的数据,如果我们改动前方的数据,还要去自己校验CRC的值进行填写,不方便后续调整数据,因此我对发送的数据进行整合一下,首先创建如下几个参数:
uint8_t Host_Txbuf[8]; //modbus发送数组
uint8_t slave_add; //要匹配的从机设备地址(做主机实验时使用)
创建函数:
//参数1从机,参数2起始地址,参数3寄存器个数
void Host_Read03_slave(uint8_t slave,uint16_t StartAddr,uint16_t num)
{
}
对所要发送的数据,根据上方表格:
| 地址码 | 功能码 | 起始地址 | 数据长度 | 校验码低位 | 校验码高位 |
| 0x01 | 0x03 | 0x00 0x00 | 0x00 0x02 | 0xC4 | 0x0B |
modbus.slave_add = slave;
modbus.Host_Txbuf[Send_03_j++] = slave;//这是要匹配的从机地址
modbus.Host_Txbuf[Send_03_j++] = 0x03;//功能码
modbus.Host_Txbuf[Send_03_j++] = StartAddr/256;//起始地址高位
modbus.Host_Txbuf[Send_03_j++] = StartAddr%256;//起始地址低位
modbus.Host_Txbuf[Send_03_j++] = num/256;//寄存器个数高位
modbus.Host_Txbuf[Send_03_j++] = num%256;//寄存器个数低位
crc = crc16(modbus.Host_Txbuf, Send_03_j); //获取CRC校验位
modbus.Host_Txbuf[Send_03_j++] = crc/256;//寄存器个数高位
modbus.Host_Txbuf[Send_03_j++] = crc%256;//寄存器个数低位
将封装好的数据发送出去:
//发送数据包装完毕
USART_REDE_TX_MODE_H;//RS485转为发送状态
for(Send_03_i = 0; Send_03_i < Send_03_j + 1; Send_03_i++)
{
Serial_SendByte(modbus.Host_Txbuf[Send_03_i]);
}
USART_REDE_RX_MODE_L;//RS485恢复接收状态
此时的发送函数:
//主机选择从机
//参数1从机,参数2起始地址,参数3寄存器个数
void Host_Read03_slave(uint8_t slave,uint16_t StartAddr,uint16_t num)
{
uint16_t Send_03_j = 0;
uint16_t Send_03_i = 0;
uint16_t crc;//计算的CRC校验位
modbus.slave_add = slave;
modbus.Host_Txbuf[Send_03_j++] = slave;//这是要匹配的从机地址
modbus.Host_Txbuf[Send_03_j++] = 0x03;//功能码
modbus.Host_Txbuf[Send_03_j++] = StartAddr/256;//起始地址高位
modbus.Host_Txbuf[Send_03_j++] = StartAddr%256;//起始地址低位
modbus.Host_Txbuf[Send_03_j++] = num/256;//寄存器个数高位
modbus.Host_Txbuf[Send_03_j++] = num%256;//寄存器个数低位
crc = crc16(modbus.Host_Txbuf, Send_03_j); //获取CRC校验位
modbus.Host_Txbuf[Send_03_j++] = crc/256;//寄存器个数高位
modbus.Host_Txbuf[Send_03_j++] = crc%256;//寄存器个数低位
//发送数据包装完毕
USART_REDE_TX_MODE_H;//RS485转为发送状态
for(Send_03_i = 0; Send_03_i < Send_03_j + 1; Send_03_i++)
{
Serial_SendByte(modbus.Host_Txbuf[Send_03_i]);
}
USART_REDE_RX_MODE_L;//RS485恢复接收状态
}
这样我们只要在主函数调用该函数进行数据发送即可:
Host_Read03_slave(0x01,0x0000,0x0002);
2.2 定时器修改
上面数据我们会发现主机发送数据过于频繁,不能有效处理数据,并且对于温湿度这些数据,也不需要过于实时发送,因此我们可以加一个定时器,让主机1s发送一次数据,方便数据处理,声明变量:
uint8_t Host_send_flag; //主机设备发送数据完毕标志位
int Host_Sendtime; //发送完一帧数据后时间计数
uint8_t Host_time_flag; //发送时间到标志位,=1表示到发送数据时间了
uint8_t Host_End; //接收数据后处理完毕
来到定时器服务函数,添加代码:
modbus.Host_Sendtime++;//发送完上一帧后的时间计数
if(modbus.Host_Sendtime > 1000)//距离发送上一帧数据1s
{
//1s时间到
modbus.Host_time_flag = 1;//发送数据标志位位置1
modbus.Host_Sendtime = 0;//清空计数器
}
对于发送函数也要进行修改,添加一个计数器的标志位:
void Host_Read03_slave(uint8_t slave,uint16_t StartAddr,uint16_t num)
{
uint16_t Send_03_j = 0;
uint16_t Send_03_i = 0;
uint16_t crc;//计算的CRC校验位
if(modbus.Host_time_flag == 1)
{
modbus.slave_add = slave;
modbus.Host_Txbuf[Send_03_j++] = slave;//这是要匹配的从机地址
modbus.Host_Txbuf[Send_03_j++] = 0x03;//功能码
modbus.Host_Txbuf[Send_03_j++] = StartAddr/256;//起始地址高位
modbus.Host_Txbuf[Send_03_j++] = StartAddr%256;//起始地址低位
modbus.Host_Txbuf[Send_03_j++] = num/256;//寄存器个数高位
modbus.Host_Txbuf[Send_03_j++] = num%256;//寄存器个数低位
crc = crc16(modbus.Host_Txbuf, Send_03_j); //获取CRC校验位
modbus.Host_Txbuf[Send_03_j++] = crc/256;//寄存器个数高位
modbus.Host_Txbuf[Send_03_j++] = crc%256;//寄存器个数低位
//发送数据包装完毕
USART_REDE_TX_MODE_H;//RS485转为发送状态
for(Send_03_i = 0; Send_03_i < Send_03_j + 1; Send_03_i++)
{
Serial_SendByte(modbus.Host_Txbuf[Send_03_i]);
}
USART_REDE_RX_MODE_L;//RS485恢复接收状态
modbus.Host_time_flag = 0;//时间标志位清零
modbus.Host_send_flag=1;//表示发送数据完毕
}
}
2.3 数据接收
主机发送完问询帧以后,就需要去应答,也就是接收从机发送的信号,对于应道可以直接套从机的接收部分:
//做一些数据处理
void Send_03_Data_Resive(void)
{
}
//主机接收从机的消息进行处理
void HOST_ModbusRX(void)
{
u16 crc,rccrc;//计算crc和接收到的crc
if(modbus.Modbus_RX_Flag == 0) //如果接收未完成则返回空
{
return;
}
//接收数据结束
//(数组中除了最后两位CRC校验位其余全算)
crc = crc16(&modbus.Modbus_RX_BUFF[0],modbus.Modbus_RX_recount-2); //获取CRC校验位
rccrc = modbus.Modbus_RX_BUFF[modbus.Modbus_RX_recount-2]*256+modbus.Modbus_RX_BUFF[modbus.Modbus_RX_recount-1];//计算读取的CRC校验位
if(crc == rccrc) //CRC检验成功 开始分析包
{
if(modbus.Modbus_RX_BUFF[0] == modbus.slave_add) // 检查地址是是对应从机发过来的
{
switch(modbus.Modbus_RX_BUFF[1])//分析功能码
{
case 0: break;
case 1: break;
case 2: break;
case 3: Send_03_Data_Resive();break;//3号功能码处理
case 4: break;
case 5: break;
case 6: break;
case 7: break;
default:break;
}
}
}
modbus.Modbus_RX_recount = 0;//接收计数清零
modbus.Modbus_RX_Flag = 0; //接收标志清零
}
2.4 验证
让我们来主函数,看看数据能不能正常处理,对于应道数据:
| 地址码 | 功能码 | 返回有效字节数 | 湿度值 | 温度值 | 校验码低位 | 校验码高位 |
| 0x01 | 0x03 | 0x04 | 0x02 0x92 | 0xFF 0x9B | 0x5A | 0x3D |
可以看出数组的3,4,5,6是温湿度数据,我们将数据合并,并且强转为十进制表示的是具体数据:
RX_shi_str = (modbus.Modbus_RX_BUFF[3]<< 8) | modbus.Modbus_RX_BUFF[4];
RX_wen_str= (modbus.Modbus_RX_BUFF[5]<< 8) | modbus.Modbus_RX_BUFF[6];
RX_shi=(int)RX_shi_str;
RX_wen=(int)RX_wen_str;
完整主函数代码:
#include "stm32f10x.h"
#include "RS485_USART1.h"
#include "OLED.h"
#include "OLED_Data.h"
#include "Modbus.h"
#include "Modbus_CRC.h"
#include "Delay.h"
#include "Timer.h"
unsigned int RX_wen_str,RX_shi_str;
unsigned int RX_wen,RX_shi;
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//定时器分组
My_RS485_Init();//RS485初始化
Modbus_Init();//Modbus地址初始化
OLED_Init();//OLED初始化
OLED_Clear();//清屏
Timer_Init();//定时器初始化
while (1)
{
// Modbus_Data_Treat();
Host_Read03_slave(0x01,0x0000,0x0002);
HOST_ModbusRX();
RX_shi_str = (modbus.Modbus_RX_BUFF[3]<< 8) | modbus.Modbus_RX_BUFF[4];
RX_wen_str= (modbus.Modbus_RX_BUFF[5]<< 8) | modbus.Modbus_RX_BUFF[6];
RX_shi=(int)RX_shi_str;
RX_wen=(int)RX_wen_str;
// OLED_ShowHexNum(0, 0, modbus.Modbus_RX_BUFF[0], 2, OLED_8X16);
// OLED_ShowHexNum(24, 0, modbus.Modbus_RX_BUFF[1], 2, OLED_8X16);
// OLED_ShowHexNum(48, 0, modbus.Modbus_RX_BUFF[2], 2, OLED_8X16);
// OLED_ShowHexNum(72, 0, modbus.Modbus_RX_BUFF[3], 2, OLED_8X16);
// OLED_ShowHexNum(0, 24, modbus.Modbus_RX_BUFF[4], 2, OLED_8X16);
// OLED_ShowHexNum(24, 24, modbus.Modbus_RX_BUFF[5], 2, OLED_8X16);
// OLED_ShowHexNum(48, 24, modbus.Modbus_RX_BUFF[6], 2, OLED_8X16);
// OLED_ShowHexNum(72, 24, modbus.Modbus_RX_BUFF[7], 2, OLED_8X16);
OLED_ShowNum(0, 0, RX_shi, 4, OLED_8X16);
OLED_ShowNum(72, 0, RX_wen, 4, OLED_8X16);
OLED_Update();
}
}
看一下效果,OLED屏幕数据除以十就是正常数据:

我们吹口气,数据正常改变:

能够正常通信,同样的道理可以看看其他功能码,这里就不在一一验证了。
主机代码:



5274

被折叠的 条评论
为什么被折叠?



