7.1 Modbus寄存器类型回顾

在Modbus协议中,从设备通过四种类型的寄存器来暴露数据,每种寄存器有特定的访问属性和用途:

寄存器种类 PLC/组态地址 Modbus寄存器地址范围 简称 读写状态 数据宽度 典型用途
线圈状态 00001~09999 0000H~FFFFH 0x 可读可写 1位 开关输出(继电器、LED)
离散输入状态 10001~19999 0000H~FFFFH 1x 只读 1位 开关输入(按键、传感器状态)
输入寄存器 30001~39999 0000H~FFFFH 3x 只读 16位 模拟量输入(温度、湿度、电压)
保持寄存器 40001~49999 0000H~FFFFH 4x 可读可写 16位 参数设置、控制输出

注意:Modbus协议中地址从0开始,但PLC组态地址通常从1开始编号,且前导数字表示类型。例如:40001对应保持寄存器地址0x0000。

7.2 传感器点表设计思路

点表(Point Map)是从设备将内部数据映射到Modbus寄存器地址的规划表。设计点表时,需要考虑以下因素:

  1. 数据类型:确定每个数据是位(布尔)还是16位整数(或浮点数拆分到多个寄存器)。

  2. 访问权限:哪些数据只读(传感器值),哪些可读写(控制参数)。

  3. 地址分配:合理规划地址范围,避免冲突,方便扩展。

  4. 单位与精度:明确数值的物理含义(如温度0.1°C,湿度0.01%等)。

  5. 硬件资源对应:将物理输入/输出与寄存器位一一对应。

7.3 开发板硬件资源分析

以提供的开发板图片为例,硬件资源包括:

  • 温湿度模块:模拟量传感器,输出温度和湿度值(通常通过I2C或单总线读取,但Modbus需要将其转换为寄存器值)。

  • 用户按键:3个独立按键,可作为离散输入。

  • 用户LED:4个独立LED,可作为线圈输出控制。

  • 有源蜂鸣器:可通过线圈控制鸣响。

  • RS485接口:用于Modbus RTU通信。

  • 其他:复位按键、启动模式、电源等。

7.4 点表设计示例

假设我们要为这个开发板设计Modbus从设备点表,需要定义以下数据:

7.4.1 离散输入(只读位)—— 对应物理按键
地址(PLC) Modbus地址 简称 名称 说明
10001 0x0000 1x1 按键1状态 0=未按下,1=按下
10002 0x0001 1x2 按键2状态 同上
10003 0x0002 1x3 按键3状态 同上
7.4.2 线圈(可读写位)—— 控制LED和蜂鸣器
地址(PLC) Modbus地址 简称 名称 说明
00001 0x0000 0x1 LED1控制 0=灭,1=亮
00002 0x0001 0x2 LED2控制 同上
00003 0x0002 0x3 LED3控制 同上
00004 0x0003 0x4 LED4控制 同上
00005 0x0004 0x5 蜂鸣器控制 0=关,1=开
7.4.3 输入寄存器(只读16位)—— 传感器数据
地址(PLC) Modbus地址 简称 名称 数据类型 单位 说明
30001 0x0000 3x1 温度值 uint16_t 0.1°C 实际温度 = 寄存器值 × 0.1
30002 0x0001 3x2 湿度值 uint16_t 0.01% 实际湿度 = 寄存器值 × 0.01
7.4.4 保持寄存器(可读写16位)—— 参数设置
地址(PLC) Modbus地址 简称 名称 数据类型 单位 说明
40001 0x0000 4x1 温度报警阈值 uint16_t 0.1°C 当温度超过此值时触发报警
40002 0x0001 4x2 湿度报警阈值 uint16_t 0.01% 当湿度超过此值时触发报警
40003 0x0002 4x3 设备地址 uint16_t - Modbus从站地址(1~247)
40004 0x0003 4x4 波特率 uint16_t - 可选值:0=9600,1=19200,...

7.5 点表在代码中的实现(libmodbus)

在libmodbus从设备中,我们需要使用modbus_mapping_t结构体来定义点表对应的内存区域。

// 定义存储数组
// 线圈:5个(LED1~4 + 蜂鸣器)
uint8_t coil_tab[1];  // 1字节即可存放8个线圈,但为了清晰可用数组

// 离散输入:3个按键
uint8_t discrete_input_tab[1];

// 输入寄存器:2个(温度、湿度)
uint16_t input_reg_tab[2];

// 保持寄存器:4个(阈值、地址、波特率)
uint16_t holding_reg_tab[4];

// 创建映射结构体
modbus_mapping_t *mb_mapping;

// 初始化映射
mb_mapping = modbus_mapping_new_start_address(
    0, 5,           // 线圈起始地址0,数量5
    0, 3,           // 离散输入起始地址0,数量3
    0, 2,           // 输入寄存器起始地址0,数量2
    0, 4            // 保持寄存器起始地址0,数量4
);

// 将内存指针指向实际存储数组
mb_mapping->tab_bits = coil_tab;
mb_mapping->tab_input_bits = discrete_input_tab;
mb_mapping->tab_input_registers = input_reg_tab;
mb_mapping->tab_registers = holding_reg_tab;

// 初始化默认值
coil_tab[0] = 0;  // 所有LED关闭
discrete_input_tab[0] = 0;
input_reg_tab[0] = 250;  // 25.0°C
input_reg_tab[1] = 5000; // 50.00%
holding_reg_tab[0] = 300; // 30.0°C报警
holding_reg_tab[1] = 8000; // 80.00%报警
holding_reg_tab[2] = 1;    // 地址1
holding_reg_tab[3] = 0;    // 波特率9600

7.6 物理量与寄存器值的转换

7.6.1 温度转换

假设温湿度传感器返回真实温度 = 25.3°C,采用0.1°C单位:

// 写入输入寄存器
input_reg_tab[0] = (uint16_t)(temperature * 10); // 253

当主设备读取30001时,得到值253,实际温度 = 253 × 0.1 = 25.3°C。

7.6.2 湿度转换

湿度真实值 = 45.67%,采用0.01%单位:

input_reg_tab[1] = (uint16_t)(humidity * 100); // 4567

7.7 从设备主循环处理

在从设备主循环中,需要不断更新只读寄存器(输入寄存器、离散输入)的值,并根据线圈状态控制硬件。

int main(void)
{
    modbus_t *ctx;
    modbus_mapping_t *mb_mapping;
    uint8_t req[MODBUS_RTU_MAX_ADU_LENGTH];
    int rc;

    // 初始化串口等硬件
    // ...

    // 创建Modbus RTU上下文
    ctx = modbus_new_rtu("/dev/ttyS0", 9600, 'N', 8, 1);
    modbus_set_slave(ctx, 1);  // 从站地址1

    // 创建数据映射(假设已经分配好存储数组)
    mb_mapping = modbus_mapping_new_start_address(0,5, 0,3, 0,2, 0,4);
    mb_mapping->tab_bits = coil_tab;
    mb_mapping->tab_input_bits = discrete_input_tab;
    mb_mapping->tab_input_registers = input_reg_tab;
    mb_mapping->tab_registers = holding_reg_tab;

    // 连接
    modbus_connect(ctx);

    while (1) {
        // 1. 更新物理输入(按键状态)
        discrete_input_tab[0] = read_button1();
        discrete_input_tab[1] = read_button2();
        discrete_input_tab[2] = read_button3();

        // 2. 更新传感器数据(温度、湿度)
        float temp = read_temperature();
        float hum = read_humidity();
        input_reg_tab[0] = (uint16_t)(temp * 10);
        input_reg_tab[1] = (uint16_t)(hum * 100);

        // 3. 根据线圈状态控制输出
        if (coil_tab[0] & 0x01) set_led1(1); else set_led1(0);
        if (coil_tab[0] & 0x02) set_led2(1); else set_led2(0);
        if (coil_tab[0] & 0x04) set_led3(1); else set_led3(0);
        if (coil_tab[0] & 0x08) set_led4(1); else set_led4(0);
        if (coil_tab[0] & 0x10) set_buzzer(1); else set_buzzer(0);

        // 4. 接收Modbus请求并回复
        rc = modbus_receive(ctx, req);
        if (rc > 0) {
            modbus_reply(ctx, req, rc, mb_mapping);
        } else if (rc == -1) {
            // 错误处理
            break;
        }
    }

    // 清理
    modbus_mapping_free(mb_mapping);
    modbus_close(ctx);
    modbus_free(ctx);
    return 0;
}

7.8 点表设计的进阶考虑

7.8.1 多字节数据(如浮点数)

如果传感器输出浮点数,可以用两个连续的保持寄存器组合成32位数据(例如IEEE754格式)。主设备需按相同格式解析。

7.8.2 地址对齐与扩展

建议在点表中预留一些空地址,方便后续功能添加。例如,线圈从0x00开始,离散输入从0x20开始,输入寄存器从0x40开始,保持寄存器从0x60开始,避免重叠。

7.8.3 异常处理

设计点表时要考虑非法地址访问时的异常响应,从设备应返回异常码0x02(非法数据地址)。

7.8.4 广播地址支持

如果需要支持广播(地址0),则从设备应处理写操作但不回复响应。

7.9 调试与测试点表

使用Modbus主站软件(如Modbus Poll)测试点表:

  1. 读离散输入:检查按键状态是否正确。

  2. 读输入寄存器:检查温湿度值是否合理。

  3. 写线圈:控制LED亮灭,验证输出。

  4. 写保持寄存器:修改报警阈值,再读取确认。

7.10 本章核心知识点总结

  1. 四种寄存器类型:线圈(0x)、离散输入(1x)、输入寄存器(3x)、保持寄存器(4x)。

  2. 点表设计步骤:列出所有数据→确定类型→分配地址→定义单位精度→编码实现。

  3. 物理量与寄存器转换:使用缩放因子(如×10、×100)将浮点转为整数。

  4. 从设备程序结构:更新输入数据→接收请求→处理请求→回复。

  5. 硬件对应:线圈→输出(LED、蜂鸣器);离散输入→输入(按键);输入寄存器→传感器值;保持寄存器→参数设置。

  6. 注意事项:地址范围、字节序、异常处理、广播地址。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐