有关MODBUS技术栈分解

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 */

 

  1. 第一次字节到达

    • cnt_now = 1000(假设当前计数值)。
    • _uiTimeOut = 500(假设超时时间)。
    • cnt_tar = 1000 + 500 = 1500
    • 定时器比较值设置为1500,并启动计数。
  2. 第二次字节到达(假设此时计数值为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 函数发送错误应答消息。
  • 23
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值