1.MODBUS特性
modbus协议大部分都用于工控领域,结合RS485通信。大部分工控领域是用作从机模式,也就是配套上位机主机。modbus协议规定了,其数据通信协议,也就是规定其通信帧,同时形式以及其响应内容。这些可查阅相关手册获取。
这里我们介绍的是modbus rtu协议,也就是工业领域用的最多的协议。
那么如何将串口通过485传递,再进行modbus解析呢,这中间又有哪些技术点需要学习和掌握呢。接下来,我一一分享。
2.物理层
3.技术要点分析
利用modbus帧间隔,3.5个字符
假设波特率为115200 一个字符是10个位 1起始+数据8+校验位+停止位
1/115200 *30 =2.6ms
也就是帧间隔为2.6ms
那么如何利用2.6ms进行断帧,
字节处理代码
/* ********************************************************************************************************* * 函 数 名: MODS_ReciveNew * 功能说明: 串口接收中断服务程序会调用本函数。当收到一个字节时,执行一次本函数。 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ void MODS_ReciveNew(uint8_t _byte) { /* 3.5个字符的时间间隔,只是用在RTU模式下面,因为RTU模式没有开始符和结束符, 两个数据包之间只能靠时间间隔来区分,Modbus定义在不同的波特率下,间隔时间是不一样的, 所以就是3.5个字符的时间,波特率高,这个时间间隔就小,波特率低,这个时间间隔相应就大 4800 = 7.297ms 9600 = 3.646ms 19200 = 1.771ms 38400 = 0.885ms */ uint32_t timeout; g_mods_timeout = 0; timeout = 35000000 / SBAUD485; /* 计算超时时间,单位us 35000000*/ /* 硬件定时中断,定时精度us 硬件定时器1用于ADC, 定时器2用于Modbus */ bsp_StartHardTimer(1, timeout, (void *)MODS_RxTimeOut); if (g_tModS.RxCount < S_RX_BUF_SIZE) { g_tModS.RxBuf[g_tModS.RxCount++] = _byte; } }
cnt_now = TIM_GetCounter(TIM_HARD); /* 读取当前的计数器值 */ cnt_tar = cnt_now + _uiTimeOut; /* 计算捕获的计数器值 */ if (_CC == 1) { s_TIM_CallBack1 = (void (*)(void))_pCallBack; TIM_SetCompare1(TIM_HARD, cnt_tar); /* 设置捕获比较计数器CC1 */
第一次字节到达:
cnt_now = 1000
(假设当前计数值)。_uiTimeOut = 500
(假设超时时间)。cnt_tar = 1000 + 500 = 1500
。- 定时器比较值设置为1500,并启动计数。
第二次字节到达(假设此时计数值为1200,肯定会小于超时时长,因为串口接收一个字节假设是133ms,但是超时时间是500ms,也就是第一次的
cnt_now,到第二次的cnt_now
小于500ms):
cnt_now = 1200
。cnt_tar = 1200 + 500 = 1700
。- 定时器比较值更新为1700。也就是 TIM_SetCompare1(TIM_HARD, cnt_tar); 为新的值,就不会触发中断。
中断触发条件
- 定时器将会在计数值达到1700时触发中断,而不是1500。
- 因此,第一个比较值1500将被忽略,不会触发中断。
- 总结,不到超时时间不会触发中断,一个字的传输时间,肯定小于3.5个字符,这里要保证中断及时性。
数据解析阶段
static void MODS_04H(void)
{
/*
主机发送:
11 从机地址
04 功能码
00 寄存器起始地址高字节
08 寄存器起始地址低字节
00 寄存器个数高字节
02 寄存器个数低字节
F2 CRC高字节
99 CRC低字节
从机应答: 输入寄存器长度为2个字节。对于单个输入寄存器而言,寄存器高字节数据先被传输,
低字节数据后被传输。输入寄存器之间,低地址寄存器先被传输,高地址寄存器后被传输。
11 从机地址
04 功能码
04 字节数
00 数据1高字节(0008H)
0A 数据1低字节(0008H)
00 数据2高字节(0009H)
0B 数据2低字节(0009H)
8B CRC高字节
80 CRC低字节
例子:
01 04 2201 0006 2BB0 --- 读 2201H A01通道模拟量 开始的6个数据
01 04 2201 0001 6A72 --- 读 2201H
*/
uint16_t reg;
uint16_t num;
uint16_t i;
uint16_t status[10];
memset(status, 0, 10);
g_tModS.RspCode = RSP_OK;
if (g_tModS.RxCount != 8)
{
g_tModS.RspCode = RSP_ERR_VALUE; /* 数据值域错误 */
goto err_ret;
}
reg = BEBufToUint16(&g_tModS.RxBuf[2]); /* 寄存器号 */
num = BEBufToUint16(&g_tModS.RxBuf[4]); /* 寄存器个数 */
if ((reg >= REG_A01) && (num > 0) && (reg + num <= REG_AXX + 1))
{
for (i = 0; i < num; i++)
{
switch (reg)
{
/* 测试参数 */
case REG_A01:
status[i] = g_tVar.A01;
break;
default:
status[i] = 0;
break;
}
reg++;
}
}
else
{
g_tModS.RspCode = RSP_ERR_REG_ADDR; /* 寄存器地址错误 */
}
err_ret:
if (g_tModS.RspCode == RSP_OK) /* 正确应答 */
{
g_tModS.TxCount = 0;
g_tModS.TxBuf[g_tModS.TxCount++] = g_tModS.RxBuf[0];
g_tModS.TxBuf[g_tModS.TxCount++] = g_tModS.RxBuf[1];
g_tModS.TxBuf[g_tModS.TxCount++] = num * 2; /* 返回字节数 */
for (i = 0; i < num; i++)
{
g_tModS.TxBuf[g_tModS.TxCount++] = status[i] >> 8;
g_tModS.TxBuf[g_tModS.TxCount++] = status[i] & 0xFF;
}
MODS_SendWithCRC(g_tModS.TxBuf, g_tModS.TxCount);
}
else
{
MODS_SendAckErr(g_tModS.RspCode); /* 告诉主机命令错误 */
}
}
第一步,初始化准备
uint16_t reg; uint16_t num; uint16_t i; uint16_t status[10]; memset(status, 0, 20);
- 定义了一些变量:
reg
(寄存器地址)、num
(寄存器数量)、i
(循环计数器)和status
(存储读取的寄存器值的数组)。- 使用
memset
函数将status
数组初始化为 0。
第二步:判断接收到的数据个数是否正确
g_tModS.RspCode = RSP_OK; if (g_tModS.RxCount != 8) { g_tModS.RspCode = RSP_ERR_VALUE; /* 数据值域错误 */ goto err_ret; }
- 首先将响应代码设置为
RSP_OK
。- 检查接收的数据长度是否为 8 字节,如果不是,设置响应代码为
RSP_ERR_VALUE
并跳转到错误处理部分。
第三步:数据解析
reg = BEBufToUint16(&g_tModS.RxBuf[2]); /* 寄存器号 */ num = BEBufToUint16(&g_tModS.RxBuf[4]); /* 寄存器个数 */
大端转小端函数实现 c 复制代码 uint16_t BEBufToUint16(uint8_t *_pBuf) { return (((uint16_t)_pBuf[0] << 8) | _pBuf[1]); } _pBuf:指向两个字节的缓冲区指针。 _pBuf[0]:高字节。 _pBuf[1]:低字节。 ((uint16_t)_pBuf[0] << 8):将高字节左移 8 位,移到 16 位整数的高字节位置。 _pBuf[1]:保持低字节的位置。 |:按位或运算,将高字节和低字节组合成一个 16 位整数。 例子 假设我们有两个字节的数据存储在缓冲区中,并且它们的值为: _pBuf[0] = 0x22(高字节) _pBuf[1] = 0x01(低字节) 我们希望将这两个字节转换为一个 16 位的无符号整数。 调用函数: c 复制代码 uint8_t buffer[2] = {0x22, 0x01}; uint16_t result = BEBufToUint16(buffer); 分析函数执行步骤: (uint16_t)_pBuf[0]:将高字节 0x22 转换为 16 位无符号整数,结果为 0x0022。 ((uint16_t)_pBuf[0] << 8):将 0x0022 左移 8 位,结果为 0x2200。 _pBuf[1]:低字节为 0x01。 ((uint16_t)_pBuf[0] << 8) | _pBuf[1]:将 0x2200 和 0x01 按位或运算,结果为 0x2201。 最终,result = 0x2201。 总结 BEBufToUint16 函数用于将大端格式的两个字节转换为一个 16 位的无符号整数,确保数据在处理时的正确性。这个转换在处理诸如 Modbus RTU 协议的数据时非常重要,因为该协议使用大端格式来传输数据。
第四步:读取寄存器数据
if ((reg >= REG_A01) && (num > 0) && (reg + num <= REG_AXX + 1)) { for (i = 0; i < num; i++) { switch (reg) { /* 测试参数 */ case REG_A01: status[i] = g_tVar.A01; break; default: status[i] = 0; break; } reg++; } } else { g_tModS.RspCode = RSP_ERR_REG_ADDR; /* 寄存器地址错误 */ }
- 检查寄存器地址和数量是否在有效范围内。如果有效,则读取相应的寄存器数据。
- 使用
switch
语句根据寄存器地址获取相应的数据,并将其存储在status
数组中。- 如果寄存器地址不在有效范围内,设置响应代码为
RSP_ERR_REG_ADDR
。
第五步:应答回复
err_ret: if (g_tModS.RspCode == RSP_OK) /* 正确应答 */ { g_tModS.TxCount = 0; g_tModS.TxBuf[g_tModS.TxCount++] = g_tModS.RxBuf[0]; /* 返回从机地址 */ g_tModS.TxBuf[g_tModS.TxCount++] = g_tModS.RxBuf[1]; /* 返回从机指令 */ g_tModS.TxBuf[g_tModS.TxCount++] = num * 2; /* 返回字节数 */ for (i = 0; i < num; i++) /* 返回数据 */ { g_tModS.TxBuf[g_tModS.TxCount++] = status[i] >> 8; g_tModS.TxBuf[g_tModS.TxCount++] = status[i] & 0xFF; } MODS_SendWithCRC(g_tModS.TxBuf, g_tModS.TxCount); /* 发送正确应答 */ } else { MODS_SendAckErr(g_tModS.RspCode); /* 告诉主机命令错误 */ }
- 检查响应代码,如果为
RSP_OK
,则构建应答消息。
- 将接收的从机地址和指令码复制到发送缓冲区。
- 将字节数(寄存器数量 * 2)添加到发送缓冲区。
- 将读取的寄存器数据转换为大端格式并添加到发送缓冲区。
- 使用
MODS_SendWithCRC
函数发送应答消息。- 如果响应代码不为
RSP_OK
,使用MODS_SendAckErr
函数发送错误应答消息。