STM32之RS485与ModBus详解

发布于:2025-09-12 ⋅ 阅读:(22) ⋅ 点赞:(0)

一、RS485

1. RS485 概述

  • 连接与传输:基于硬件有线连接的数据传输方式,属串行(串行)通信,用于工业场景
  • 对比 RS232:RS232 电气稳定性差、易受干扰、传输距离短;RS485 稳定性好、传输距离远
  • 接线与连接:需 A、B 两根数据线,MCU 借差分线连 485 芯片保障数据稳定一致

2. 采用 RS485 的原因

  • 传输距离:低速率、满足布线要求时,可达 1200 米,还能借中继节点延长
  • 传输速度:最高 10Mbps(1.25MB/s ),但高速下传输距离会缩短
  • 多设备连接:理论单设备可连 32 个 485 设备,自定义地址(0x01 - 0xxx ),结合技术可扩至 128 台
  • 成本优势:芯片与设备成本低

3. RS485 工作(数据收发)

  • 基础逻辑:借 A、B 线收发数据,485 芯片依 MCU 时钟周期,调 A、B 电压差完成收发
  • 数据发送规则
    • 发数据 1:A 端子电压 - B 端子电压>200mV ~ 6V ,实际开发板常用>2V ~ 3.3V(受 3.3V 供电限制 )
    • 发数据 0:B 端子电压 - A 端子电压>2V ~ 3.3V
    • 注意:200mV 是判断 0/1 理论最低标准,导线压降可能致电压差不足 200mV ,引发数据丢失

4. RS485原理图分析

4.1 原理图分析连线关系和对应引脚

        本案例中,单片机上有485芯片里面的RS485_TX和RX引脚分别连接USART2_RX和USART2_TX引脚,最后通过引脚复用,通过PA2和PA3引脚连入MCU;而RS485——RE直接通过服用PD7引脚连入MCU。

4.2 开发流程分析

【引脚分析】

  • 当前使用的串口对应 USART2
    • USART2_TX ==> PA2 复用推挽模式
    • USART2_RX ==> PA3 浮空输入
  • 485 芯片数据发送模式和数据接收模式控制
    • RS485_RE --> PD7 推挽模式

代码实现过程

  • 时钟使能
    • USART2 GPIOA GPIOD
  • 引脚配置
    • PA2 复用推挽模式
    • PA3 浮空输入
    • PD7 推挽模式
  • 配置 USART2
    • 波特率,8N1,USART_TX | USART_RX
    • 中断使能 RXNE 和 IDLE,对应 USART2_IRQn
    • 中断函数 USART2_IRQHandler
  • 【重点】
    • 通过 USART2 进行数据发送操作,
      • 要求必须通过 PD7 设置为高电平输出,打开 485 发送数据模式,
      • 当前数据发送完成,将 PD7 设置为低电平输出,485 芯片进入数据接收模式。

4.3 代码示例

rs485.h:

#ifndef _RS485_H
#define _RS485_H
#include "stm32f10x.h"
#include "stdio.h"
#include "stdlib.h"
#include "string.h"
#include "usart1.h"
#include "systick.h"
#include "led.h"
#include "beep.h"
#define RS485_DATA_SIZE (256)
typedef struct rs485_data
{
    u8 data[RS485_DATA_SIZE]; // 接受数据缓冲区
    u8 flag; // 数据处理标志位
    u16 count; // 读取到的有效字节个数
} RS485_Data;
extern RS485_Data rs485_val;
void RS485_Init(u32 brr);
void RS485_SendByte(u8 byte);
void RS485_SendBuffer(u8 *buffer, u16 count);
void RS485_SendString(const char * str);
#endif

rs485.c:

#include "rs485.h"

RS485_Data rs485_val = {0};

void RS485_Init(u32 brr)
{
	// 1. 时钟使能 USART2 GPIOA GPIOD
	RCC_APB2PeriphClockCmd(RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPDEN, ENABLE);
	RCC_APB1PeriphClockCmd(RCC_APB1ENR_USART2EN, ENABLE);
	
	// 2. GPIO PA2 配置 复用推挽
	GPIO_InitTypeDef GPIO_InitStructure = {0};
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	// GPIO PA3 配置 浮空输入
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	// GPIO PD7 配置 推挽
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	
	GPIO_Init(GPIOD, &GPIO_InitStructure);
	
	/// 3. USART2 配置
	USART_InitTypeDef USART_InitStructure = {0};
	
	USART_InitStructure.USART_BaudRate = brr;
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;
	USART_InitStructure.USART_Parity = USART_Parity_No;
	USART_InitStructure.USART_StopBits = USART_StopBits_1;
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
	USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
	
	USART_Init(USART2, &USART_InitStructure);
	
	USART_Cmd(USART2, ENABLE);
	
	// 4. USART2 串口中断配置
	USART_ITConfig(USART2, USART_IT_IDLE, ENABLE);
	USART_ITConfig(USART2, USART_IT_RXNE, ENABLE);
	
	NVIC_InitTypeDef NVIC_InitStructure = {0};
	
	NVIC_InitStructure.NVIC_IRQChannel = USART2_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
	
	NVIC_Init(&NVIC_InitStructure);
}

void USART2_IRQHandler(void)
{
	u16 val = 0;
	
	/*
	检测到数据总线空闲,表示数据接收完毕,进行展示处理,同时
	处理当前中断标志位
		IDLE 清除要求
			1. 读取 USARTx->SR
			2. 读取 USARTx->DR
	不建议使用 void USART_ClearFlag(USART_TypeDef* USARTx, uint16_t USART_FLAG)
	*/
	if (USART_GetITStatus(USART2, USART_IT_IDLE) == SET)
	{
		rs485_val.flag = 1;
			
		val = USART2->SR;
		val = USART2->DR;
		
		USART1_SendBuffer(rs485_val.data, rs485_val.count);
	}
	
	/*
	1. 处理 RXNE 中断
		接收数据缓冲区非空,需要进行接收数据处理
	2. 处理 IDLE 中断
		当前接收数据总线已空闲,数据接收完毕
	*/
	
	/*
	如果 flag 为 1 表示以当前数据已进行后续处理,需要对当前占用的内存空间
	进行擦除操作,方便进入下一次数据接受
	*/
	if (rs485_val.flag)
	{
		memset(&rs485_val, 0, sizeof(RS485_Data));
	}
	
	if (USART_GetITStatus(USART2, USART_IT_RXNE) == SET)
	{
		// 从 USART3 读取数据,存储到 rs485_val 结构中,同时赋值 data 存储数据
		// count 累加有效数据个数
		rs485_val.data[rs485_val.count++] = USART_ReceiveData(USART2);

	
		// 如果数据已满,利用 USART1 发送数据到 PC 串口调试工具
		if (rs485_val.count == RS485_DATA_SIZE)
		{
			// USART1_SendBuffer(esp8266_val.data, esp8266_val.count);
			rs485_val.flag = 1;
			
			USART1_SendBuffer(rs485_val.data, rs485_val.count);
		}
	}
}

void RS485_SendByte(u8 byte)
{
	while (USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET);
	
	USART_SendData(USART2, byte);
}

void RS485_SendBuffer(u8 *buffer, u16 count)
{
	// PD7 高电平
	GPIO_SetBits(GPIOD, GPIO_Pin_7);
	
	while (count--)
	{
		RS485_SendByte(*buffer);
		buffer += 1;
	}
	
	// PD7 低电平
	while(USART_GetFlagStatus(USART2,USART_FLAG_TC) == RESET);
	GPIO_ResetBits(GPIOD,GPIO_Pin_7);
}

void RS485_SendString(const char * str)
{
	// PD7 高电平
	GPIO_SetBits(GPIOD, GPIO_Pin_7);
	
	while (*str)
	{
		RS485_SendByte(*str);
		str++;
	}
	
	while(USART_GetFlagStatus(USART2,USART_FLAG_TC) == RESET);
	// PD7 低电平
	GPIO_ResetBits(GPIOD, GPIO_Pin_7);
}

实现效果:

二、ModBus

1. ModBus 概述

  • 开放性:Modbus 协议是完全开放的,任何人都可以免费使用,不需要支付许可证费用。这使得它在工业自动化领域得到了广泛的应用,不同厂商的设备可以方便地实现互联互通。
  • 简单性:协议简单易懂,易于实现。它采用主从通信方式,通信规则明确,对于开发者来说,无论是硬件实现还是软件编程都相对容易上手。
  • 可靠性:在工业环境中,通信的可靠性至关重要。Modbus 协议具有一定的错误检测机制,例如奇偶校验、CRC(循环冗余校验)等,可以有效保证数据传输的准确性。
  • 灵活性:支持多种电气接口,如 RS - 232、RS - 485 等,还可以通过以太网进行通信(Modbus TCP)。同时,它可以应用于不同类型的设备,包括 PLC、传感器、执行器、变频器等。
Modbus 支持三种数据传输模式
  • Modbus RTU(Remote Terminal Unit):这是一种紧凑的、高效的传输模式,使用二进制编码表示数据。在 RTU 模式下,每个字节包含 8 位数据,通信效率较高,常用于串行通信(如 RS - 485)。
  • Modbus ASCII:采用 ASCII 字符编码表示数据,每个字节由两个 ASCII 字符组成。这种模式相对 RTU 模式数据量较大,但可读性强,适用于对数据可读性要求较高的场合。
  • Modbus TCP:基于 TCP/IP 协议的 Modbus 版本,通过以太网进行通信。它使用标准的 TCP 端口 502,通信速度快,适用于远程监控和大规模的工业自动化系统。

一、8421BCD 码的基础逻辑(先理解编码规则)

8421BCD 码用 4 位二进制对应 1 位十进制数,4 位的位权从高位到低位依次为 8、4、2、1,仅表示 0-9(若出现 1010-1111,属于无效码)。例如:

 
  • 十进制数 “25” → 拆分为 “2” 和 “5” → 分别编码:2(0010)、5(0101) → 8421BCD 码为 “0010 0101”(即十六进制0x25,二进制00100101)。
  • 十进制数 “100” → 拆分为 “1”“0”“0” → 编码为 “0001 0000 0000”(需 3 个 4 位组,对应 2 个字节 + 1 个 4 位组,实际传输时会补成完整字节,如0x01 0x00)。

二、结合三种 Modbus 模式的案例(均以 “传输十进制温度 25℃” 为例)

假设场景:温度传感器采集到温度为十进制 25℃,约定用 8421BCD 码传输,传感器作为 Modbus 从机(地址 0x01),PLC 作为主设备,读取传感器的输入寄存器(地址 0x0000)。

1. Modbus RTU 模式(二进制编码,高效紧凑)

Modbus RTU 以二进制字节为单位传输,数据帧格式为:从机地址 + 功能码 + 寄存器地址 + 寄存器数量 + 校验码(CRC16)(请求帧);从机地址 + 功能码 + 数据长度 + 数据 + 校验码(CRC16)(应答帧)。

 
  • 编码逻辑:十进制 25℃ → 8421BCD 码为0010 0101(1 个字节,十六进制0x25)。
  • 主设备请求帧(读从机 0x01 的输入寄存器 0x0000,读 1 个寄存器):
    从机地址 功能码(读输入寄存器) 寄存器起始地址(高字节) 寄存器起始地址(低字节) 寄存器数量(高字节) 寄存器数量(低字节) CRC16 校验码(高字节) CRC16 校验码(低字节)
    0x01 0x04 0x00 0x00 0x00 0x01 0x60 0x00
    (注:功能码 0x04 用于读输入寄存器,CRC16 校验码通过工具计算得出)
  • 从机应答帧(返回温度数据):
    从机地址 功能码 数据长度(字节数) 温度数据(8421BCD 码) CRC16 校验码(高字节) CRC16 校验码(低字节)
    0x01 0x04 0x02 0x00 0x25 0x98 0x39
    (注:输入寄存器为 16 位(2 字节),因此数据长度为 0x02,8421BCD 码0x25补高位 0x00,组成0x0025,主设备解析时忽略高位 0,得到0x25→十进制 25℃)
2. Modbus ASCII 模式(ASCII 编码,可读性强)

Modbus ASCII 将每个二进制字节转换为 2 个 ASCII 字符(0-9、A-F)传输,数据帧以:开头,以\r\n结尾,校验码为 LRC(纵向冗余校验)。

 
  • 编码逻辑:十进制 25℃→8421BCD 码0x25→转换为 ASCII 字符为 “25”;寄存器地址、功能码等也需转为 ASCII。
  • 主设备请求帧(读从机 0x01 的输入寄存器 0x0000,读 1 个寄存器):
    :010400000001F9\r\n
    • 解析::(帧起始)→01(从机地址 ASCII)→04(功能码 ASCII)→0000(寄存器地址 0x0000 的 ASCII)→0001(寄存器数量 1 的 ASCII)→F9(LRC 校验码 ASCII)→\r\n(帧结束)
  • 从机应答帧(返回温度数据):
    :01040200253C\r\n
    • 解析:01(从机地址)→04(功能码)→02(数据长度 2 字节的 ASCII)→0025(8421BCD 码0x0025的 ASCII)→3C(LRC 校验码)→\r\n;主设备将 “0025” 转为十六进制0x0025,再按 8421BCD 码解析为十进制 25℃。
3. Modbus TCP 模式(基于 TCP/IP,远程高速传输)

Modbus TCP 去掉了 RTU 的 CRC 校验和 ASCII 的 LRC 校验,增加 “MBAP 报文头”(用于 TCP 通信标识),数据部分与 RTU 类似(二进制编码),默认端口 502。

 
  • 编码逻辑:与 RTU 一致,十进制 25℃→8421BCD 码0x0025(16 位寄存器数据)。
  • 主设备请求帧(MBAP 头 + Modbus 数据):
    MBAP 头(6 字节) Modbus 数据(6 字节)
    事务处理标识(0x0001) 从机地址(0x01)
    协议标识(0x0000,Modbus 协议) 功能码(0x04)
    数据长度(0x0006,后续 Modbus 数据字节数) 寄存器地址(0x0000)
    - 寄存器数量(0x0001)
    (完整帧:00 01 00 00 00 06 01 04 00 00 00 01
  • 从机应答帧
    MBAP 头(6 字节) Modbus 数据(5 字节)
    事务处理标识(0x0001,与请求一致) 从机地址(0x01)
    协议标识(0x0000) 功能码(0x04)
    数据长度(0x0005,后续 Modbus 数据字节数) 数据长度(0x02)
    - 温度数据(0x00 0x25)
    (完整帧:00 01 00 00 00 05 01 04 02 00 25);主设备解析数据部分0x0025为 8421BCD 码,得到十进制 25℃。

2. ModBus 通信栈和数据帧

目前,使用下列情况实现 MODBUS:
以太网上的 TCP/IP。各种媒介(有线:EIA/TIA - 232 - E、EIA - 422、EIA/TIA - 485 - A;光纤、无线等等)上的异步串行传输。

  • ModBus 数据传递的标准格式
  • ADU: Application Data Unit 应用程序数据单元,是整个 ModBus 协议要求的数据传递完整数据包
    • 地址域:当前数据发送 / 接受目标接收设备的地址。
    • PDU : 协议数据单元 / 功能码数据单元,组成是功能码 + 数据
    • 差错校验 (CRC) : 针对于整个 ADU 数据的校验机制。
  • PDU: Protocol Data Unit 协议数据单元 / 功能码数据单元
    • 功能码:绝对当前 ModBus 协议内容具体功能模式,例如读,写操作
    • 数据:可以认为是 ModBus 有效载荷。有效数据。

3. RS485 一主多从结构和 ModBus 地址域

4. ModBus 数据类型

  • 离散量输入
    • 一般用于设备状态,例如设备开光状态,设备运行状态,状态仅有 0 和 1,程序无法控制【离散量输入】数据内容, 完全由硬件本身状态控制。
  • 线圈
    • LED 灯控制,Beep 控制,声光警告器控制,继电器,固态继电器。仅需要一位二进制既可以控制工作状态,例如 0 表示不工作,1 表示正常工作。同样可以读取设备工作状态。
  • 输入寄存器
    • 只读寄存器,一般对应传感器采样数据在当前设备中的存储位置,数据仅可以通过传感器采样分析方式修改,用户只能读取传感器反馈的数据内容,对应 2 个字节 (16 bit)。如果传感器采样数据较为复杂,可能会利用多组【输入寄存器】来描述数据内容。例如 数据高位 2 字节,数据低位 2 字节,精度 2 字节,指数范围 2 字节...
  • 保持寄存器
    • 可以进行写入数据控制,读取数据内容,例如车辆行驶模式设置 (纯电,混动,增程,运动,越野,雪地,自定义),设备工作状态。

一、离散量输入(Discrete Inputs)

  • 是什么:专门用来反映设备的 “开关状态类信息”,数据只有 0 或 1 两种,且只能从硬件读取,程序无法主动修改它的值 。
  • 空间与权限:占用 1bit 空间,权限是只读 ,就像设备状态的 “只读快照”。
  • 举例:工厂里的 “设备运行状态检测”,比如生产线上的传送带,电机的 “运行 / 停止” 是硬件状态(电机自己决定转或停 ),PLC 通过离散量输入读取:电机运行时,离散量输入值为 1;电机停止,值为 0 。再比如 “门禁系统里门的开关状态”,门打开,对应离散量输入是 1;门关闭,值为 0 ,PLC 只能读这个状态,没法直接改门是开还是关。

二、线圈(Coils)

  • 是什么:用来控制设备 “开关动作”,或者读取设备 “开关动作状态”,数据是 0 或 1 ,支持读写操作 。
  • 空间与权限:占用 1bit 空间,权限是读写 ,相当于设备的 “控制开关 + 状态反馈” 。
  • 举例:控制工厂里的 “LED 指示灯”,PLC 写 1 到对应线圈,灯亮;写 0 ,灯灭 。同时,也能读这个线圈的值,判断灯当前是亮(1 )还是灭(0 )。再比如 “控制继电器”,线圈值为 1 时,继电器吸合,接通电路;值为 0 时,继电器断开,通过读线圈也能知道继电器当前状态。

三、输入寄存器(Input Registers)

  • 是什么:存的是传感器等设备采集的 “模拟量数据(经转换后的数字量 )”,只能从硬件读,程序不能直接写 。一般是 16bit(2 字节 )大小,用来存更复杂的数值,像温度、压力等。
  • 空间与权限:占用 16bit 空间,权限是只读 ,是传感器数据的 “只读容器” 。
  • 举例:工厂里 “温度传感器测水温”,传感器把水温(比如 25℃ )转换成 16bit 的数字量(假设对应数值是 0x00FF ),存在输入寄存器里,PLC 只能读这个寄存器的值,再转换成实际温度显示或参与控制逻辑,没法直接往这个寄存器写数据改水温(水温由传感器硬件检测决定 )。如果传感器要传 “温度、湿度、气压” 一组复杂数据,可能会用多个输入寄存器,比如温度存在寄存器 1,湿度存在寄存器 2 等。

四、保持寄存器(Holding Registers)

  • 是什么:可读写,能存设备的 “状态参数、控制指令” 等,用途灵活,常用来设置设备工作模式、读写设备运行数据 。也是 16bit 大小。
  • 空间与权限:占用 16bit 空间,权限是读写 ,相当于设备的 “可配置参数库 + 状态存储区” 。
  • 举例:控制 “新能源汽车的行驶模式”,车辆的 PLC(或控制器 )里,保持寄存器存着模式值:写 1 设为纯电模式,写 2 设为混动模式 。同时,也能读这个寄存器,知道当前车处于什么模式。再比如 “工厂里变频器的频率设置”,往保持寄存器写 50Hz 对应的数值,变频器就按 50Hz 运行,读这个寄存器能知道当前设置的频率。

5. ModBus 功能码

  • ModBus 功能码绝对当前内容具体作用
    • 公共功能码【重点】
      • 要求所有支持 ModBus 协议通信的设备必须执行的功能码内容。例如 离散量输入读取,线圈读写操作
    • 用户定义功能码
      • 企业 / 个人,可以根据自身需求,自定义功能码,要求 ModBus 协议支持的两端
    • 保留功能码
      • 一般是用于较早期设备功能保持使用,目前逐步淘汰,或者不再使用。

        针对于离散量输入,线圈,输入寄存器和保持寄存器操作【功能码】

6. ModBus 数据模式

三种数据模式

  • ModBus RTU : 8421 BCD 码
  • ModBus ASCII
  • ModBus TCP

发送数据 15,利用 ModBus 方式发送

  • ModBus RTU 方式: 0001 0101
  • ModBus ASCII 方式: 0011 0001 0001 0101 按照字符 '1' 和字符 '5' 处理
  • RTU 8421BCD 码

案例:

对比项 Modbus RTU Modbus ASCII Modbus TCP
编码规则 二进制编码(可兼容 8421 BCD 码) ASCII 字符编码(每个字节拆成 2 个 ASCII 字符) 二进制编码(与 RTU 编码逻辑一致)
数据效率 高(1 字节 = 8 位数据) 低(1 字节数据→2 个 ASCII 字符,占 2 字节) 高(同 RTU 编码,无额外字符转换)
传输载体 串行总线(如 RS-485、RS-232) 串行总线(如 RS-485、RS-232) 以太网(TCP/IP 网络)
校验方式 CRC16 校验(二进制校验,效率高) LRC 校验(ASCII 字符校验,可读性强) 依赖 TCP 校验(无需额外 Modbus 校验)
典型场景 工业现场短距离、多设备串行组网(如 PLC 与传感器) 需人工调试 / 监控的简单串行场景(如现场临时诊断) 远程监控、跨网络大规模系统(如云端 - 工厂)
数据表示示例(发送 15) 二进制编码:0001 0101(若用 8421 BCD 码,150001 0101 直接对应) ASCII 编码:'1'(0011 0001) + '5'(0011 0101)→0011 0001 0011 0101 二进制编码:0001 0101(与 RTU 编码一致,通过 TCP 报文传输)

8.  ModBus 实际数据帧分析

9. RS485 + ModBus 案例

9.1 开发分析

9.2 代码实现

modbus.h:

#ifndef _MODBUS_H
#define _MODBUS_H

#include "stm32f10x.h"

#include "rs485.h"

#include "stdio.h"
#include "stdlib.h"
#include "string.h"

typedef struct{
	float tem;  //温度
	float hum;   //湿度
	float bp;   //压强
 	u32 lux;     //照度
}Sensor_Data;

extern Sensor_Data sensor_data;

/**
 * @brief ModBus 发送 03 功能码函数,对应功能是读取多个【输入寄存器】
 *        或者【保持寄存器】数据
 * 
 * @param id       设备地址
 * @param addr     读取目标数据寄存器地址
 * @param data_len 读取的数据个数
 */
void ModBus_Send03Cmd(u8 id,u16 addr,u16 data_len);

void Analysis_ModBus_Tem_Hum(Sensor_Data *sensor_data);

void Analysis_ModBus_BP_Lux(Sensor_Data *sensor_data);

/**
 * @brief ModBus CRC 校验函数
 */
uint16_t ModBus_CRC16(const uint8_t *data, uint16_t length);


#endif

modbus.c:

#include "modbus.h"

Sensor_Data sensor_data = {0};
extern RS485_Data rs485_val;

void ModBus_Send03Cmd(u8 id,u16 addr,u16 data_len){
	//用于存储03功能码对应发送ModBus协议问询帧
	u8 modbus_03_buffer[8];
	
	//ModBus 协议组包,设备地址 + 03功能码
	modbus_03_buffer[0] = id;
	modbus_03_buffer[1] = 0x03;
	
	// 寄存器起始地址
	modbus_03_buffer[2] = addr / 256; // 寄存器地址高位
	modbus_03_buffer[3] = addr % 256; // 寄存器地址低位
	
	// 请求数据长度
	modbus_03_buffer[4] = data_len / 256; // 请求寄存器个数数据高位
	modbus_03_buffer[5] = data_len % 256; // 请求寄存器个数数据高位
	
	uint16_t crc = ModBus_CRC16(modbus_03_buffer, 6);
	
	// CRC 校验位
	modbus_03_buffer[6] =  crc % 256;// CRC 数据校验结果低位
	modbus_03_buffer[7] =  crc / 256;// CRC 数据校验结果高位
	
	for (int i = 0; i < 8; i++)
	{
		printf("%02X ", modbus_03_buffer[i]);
	}
	
	printf("\r\n");

// 调用 RS485_SendBuffer 发送组好的报文
    RS485_SendBuffer(modbus_03_buffer, 8);
}

void Analysis_ModBus_Tem_Hum(Sensor_Data *sensor_data){
	// 1. 校验数据完整性与合法性
  // 至少需要 9 字节(地址1 + 功能码1 + 数据长度1 + 数据4 + CRC2 )
	if(rs485_val.count < 9){
		return;
	}
	//取出接收数据指针
	u8 *modbus_data = rs485_val.data;
	
	// 2. CRC 校验(排除最后 2 字节 CRC )
  uint16_t crc_calc = ModBus_CRC16(modbus_data, rs485_val.count - 2);
	//将高 8 位左移 8 位(腾出低 8 位空间),再与低 8 位进行 “或运算”,组合成一个 16 位的完整 CRC 值
	uint16_t crc_recv = (modbus_data[rs485_val.count - 1] << 8) | modbus_data[rs485_val.count - 2];
  if (crc_calc != crc_recv) {
      // CRC 校验失败,清空缓存
      memset(&rs485_val, 0, sizeof(RS485_Data));
      return;
  }
	
	// 3. 校验功能码(确保是 0x03 应答)
  if (modbus_data[1] != 0x03) {
      return;
  }
	
	// 4. 解析数据
  // 湿度:第 3 字节是数据长度(通常 0x04 ,对应 2 个寄存器、4 字节数据 )
  if (modbus_data[2] == 0x04) {
      // 湿度值 = (高 8 位 << 8) | 低 8 位 ,再转换为实际值
      short hum_raw = (modbus_data[3] << 8) | modbus_data[4]; 
      sensor_data->hum = hum_raw * 0.1f;

      // 温度值 = (高 8 位 << 8) | 低 8 位 ,再转换为实际值
      short tem_raw = (modbus_data[5] << 8) | modbus_data[6]; 
      sensor_data->tem = tem_raw * 0.1f;
  }

  // 5. 解析完成后,清空 rs485_val ,准备下一次接收
  memset(&rs485_val, 0, sizeof(RS485_Data));
}

// 解析大气压值 + 光照强度(Lux)数据,根据文档格式修改
void Analysis_ModBus_BP_Lux(Sensor_Data *sensor_data) 
{
    // 1. 校验数据长度和基本合法性
    // 文档中大气压+光照问询帧应答,有效字节数等需结合实际,这里至少 9 字节(地址1+功能码1+数据长度1+数据n+CRC2 )
    if (rs485_val.count < 9) {
        return; 
    }
    u8 *modbus_data = rs485_val.data;

    // 2. CRC 校验(排除最后 2 字节 CRC )
    uint16_t crc_calc = ModBus_CRC16(modbus_data, rs485_val.count - 2);
    uint16_t crc_recv = (modbus_data[rs485_val.count - 1] << 8) | modbus_data[rs485_val.count - 2];
    if (crc_calc != crc_recv) {
        memset(&rs485_val, 0, sizeof(RS485_Data));
        return;
    }

    // 3. 校验功能码(确保是 0x03 应答)
    if (modbus_data[1] != 0x03) {
        return;
    }

    // 4. 解析数据,根据文档定义:
    //    - 大气压值:寄存器 505,实际值是寄存器值的 0.1 倍(文档说“实际值 10 倍” ,所以要除以 10 )
    //    - Lux 值:寄存器 506(高 16 位)、507(低 16 位),组合成 32 位实际值
    //    数据长度需根据返回判断,文档示例中“有效字节数 0x06”,即数据段 6 字节(对应 3 个寄存器,每个 2 字节 )
    if (modbus_data[2] == 0x06) { 
        // 解析大气压值
        // 问询帧起始地址对应寄存器 505(地址 40506 ),数据段前 2 字节是大气压值(寄存器值)
        uint16_t bp_reg_val = (modbus_data[3] << 8) | modbus_data[4]; 
        // 文档说“实际值 10 倍”,所以实际大气压 = 寄存器值 / 10.0 
        sensor_data->bp = bp_reg_val / 10.0f; 

        // 解析 Lux 值:高 16 位(寄存器 506,modbus_data[5]、modbus_data[6] ) + 低 16 位(寄存器 507,modbus_data[7]、modbus_data[8] )
        // 组合成 32 位实际值(文档说“实际值”,假设无需额外倍数转换,直接组合 )
        uint32_t lux_high = (modbus_data[5] << 8) | modbus_data[6];
        uint32_t lux_low = (modbus_data[7] << 8) | modbus_data[8];
        uint32_t lux_reg_val = (lux_high << 16) | lux_low;
        sensor_data->lux = lux_reg_val; 
    }

    // 5. 解析完成后,清空 rs485_val ,准备下一次接收
    memset(&rs485_val, 0, sizeof(RS485_Data));
}

uint16_t ModBus_CRC16(const uint8_t *data, uint16_t length) 
{
    uint16_t crc = 0xFFFF;
    uint16_t i, j;

    for (i = 0; i < length; i++) {
        crc ^= (uint16_t)data[i];
        for (j = 0; j < 8; j++) {
            if (crc & 0x0001) {
                crc >>= 1;
                crc ^= 0xA001;
            } else {
                crc >>= 1;
            }
        }
    }
    return crc;
}

0voice · GitHub


网站公告

今日签到

点亮在社区的每一天
去签到