Modbus C# 第六天:四种寄存器

这是系列第六篇。前五天你一直在用 Holding Registers(保持寄存器),但 Modbus 协议一共定义了 四种 寄存器。你截图里 Modbux 左侧那三块(Coils、Discrete Inputs、Input Registers)一直没碰过,今天一次性把四种都讲清楚。

一、先搞懂一个核心问题:Modbus 为什么要分四种

Modbus 协议设计之初,工业设备的数据其实分两种“维度”:

维度一:这个数据是“数字”还是“开关”?

  • 数字:温度 25.6℃、压力 101.3kPa、累计产量 10000 件
  • 开关:电机开/关、阀门开/关、限位开关触发/未触发

维度二:这个数据是“你控制设备”还是“设备告诉你状态”?

  • 你控制设备:让电机启动、设定目标温度
  • 设备告诉你:电机正在运行、当前温度是 25.6℃

这两个维度交叉,就得到了四种寄存器:

维度 你控制设备(可写) 设备告诉你(只读)
开关(1位) 线圈 Coils 离散输入 Discrete Inputs
数字(16位) 保持寄存器 Holding Registers 输入寄存器 Input Registers

一句话总结:Modbus 用四种寄存器,把“数字/开关”和“可写/只读”这两个维度都覆盖到了。

二、线圈 Coils —— 控制开关的寄存器

2.1 它是什么

线圈是 1 位(1 bit) 的寄存器。在 Modbus 协议层面,它只能存 0 或 1。

为什么叫“线圈”:这个名字来自早期工业控制系统里的继电器线圈。继电器线圈通电(1)就吸合,断电(0)就释放。现代 Modbus 里,任何布尔输出都用线圈表示。

2.2 能读能写

线圈是 可读可写 的。

  • 读:看看开关现在是开还是关
  • 写:把开关打开或关闭

2.3 实际用途

线圈是 控制命令,用来让设备做某件事:

用途 例子
控制继电器 让继电器吸合/断开
控制电机 启动电机 / 停止电机
控制阀门 打开阀门 / 关闭阀门
控制指示灯 点亮指示灯 / 熄灭

2.4 什么时候用

当你需要 用程序去控制设备的某个开关 时,用线圈。

真实场景:你的上位机界面上有一个“启动电机”按钮。你点击按钮,程序就往线圈地址写 true,设备收到后启动电机。

2.5 C# 代码

// 读取线圈状态:从地址 0 开始,读 4 个线圈
// slaveID: 从站地址(Unit ID)
// startAddress: 起始地址
// numOfPoints: 读取数量
// 返回值:bool[] —— 每个元素代表一个线圈的状态(true=开,false=关)
bool[] coils = master.ReadCoils(2, 0, 4);

// 遍历打印每个线圈的状态
for (int i = 0; i < coils.Length; i++)
{
    // 输出格式:线圈 0:True 或 线圈 0:False
    Console.WriteLine($"线圈 {i}:{coils[i]}");
}

// 写入单个线圈:把地址 0 的线圈设为 true(开)
// slaveID: 从站地址
// coilAddress: 要写的线圈地址
// value: true=开,false=关
master.WriteSingleCoil(2, 0, true);

三、离散输入 Discrete Inputs —— 读开关状态的寄存器

3.1 它是什么

离散输入也是 1 位(1 bit) 的寄存器。在 Modbus 协议层面,它只能存 0 或 1。

和线圈的区别:离散输入是 只读 的。

3.2 为什么只读

因为离散输入代表的是 设备内部或外部的物理状态,不是你能控制的。

比如:

  • 门磁传感器:门是开还是关,传感器说了算,你改不了
  • 限位开关:机械臂碰到限位了没有,机械状态说了算,你改不了
  • 按钮:操作员按了没有,操作员说了算,你改不了

这些状态是“读”出来的,不是“写”出来的。

3.3 实际用途

离散输入是 状态监测,用来知道设备现在处于什么状态:

用途 例子
读取限位开关 机械臂是否碰到限位
读取按钮状态 操作员是否按下了按钮
读取门磁 柜门是打开还是关闭
读取故障报警 设备是否处于故障状态

3.4 什么时候用

当你需要 知道某个物理开关现在是什么状态 时,用离散输入。

真实场景:你的上位机界面上有一个“门状态”指示灯。程序需要不断读取离散输入地址,知道门是开还是关,然后更新指示灯颜色。

3.5 C# 代码

// 读取离散输入状态:从地址 0 开始,读 4 个离散输入
// 返回值:bool[] —— 每个元素代表一个离散输入的状态
// 注意:离散输入是只读的,没有对应的写方法
bool[] discreteInputs = master.ReadInputs(2, 0, 4);

// 遍历打印每个离散输入的状态
for (int i = 0; i < discreteInputs.Length; i++)
{
    // 输出格式:离散输入 0:True 或 离散输入 0:False
    Console.WriteLine($"离散输入 {i}:{discreteInputs[i]}");
}

补充说明:Modbus 协议层面,离散输入存的是 0 或 1。NModbus 库在 C# 里把它封装成了 bool:0 对应 false,1 对应 true。所以你在代码里拿到的是 bool[],可以直接用 if (discreteInputs[i]) 判断。这和“C 语言里非 0 即 true”是两回事——C# 的 bool 只有 true/false,不允许用数字当条件。

四、输入寄存器 Input Registers —— 读设备实时数据的寄存器

4.1 它是什么

输入寄存器是 16 位(2 字节) 的寄存器,可以存 0 到 65535 的整数。

和保持寄存器的区别:输入寄存器是 只读 的。

4.2 为什么只读

因为输入寄存器代表的是 设备采集到的实时数据,不是你能改的。

比如:

  • 温度传感器采集到的当前温度
  • 电流表读到的当前电流
  • AD 转换器采集到的模拟量

这些数据是设备“测”出来的,你改不了。

4.3 实际用途

输入寄存器是 实时数据采集,用来读取设备的当前测量值:

用途 例子
读取温度 温度传感器当前读数
读取压力 压力变送器当前读数
读取电流 电流表当前读数
读取 AD 采集值 模拟量转换后的数字值

4.4 什么时候用

当你需要 读取设备的实时测量数据 时,用输入寄存器。

真实场景:你的上位机上显示“当前温度”。程序需要不断读取输入寄存器,把温度值显示出来。注意——这个温度是设备测出来的,你只能读,不能改。

4.5 C# 代码

// 读取输入寄存器:从地址 0 开始,读 2 个寄存器(假设存一个 float)
// 返回值:ushort[] —— 每个元素是一个 16 位寄存器值
// 注意:输入寄存器是只读的,没有对应的写方法
ushort[] inputRegisters = master.ReadInputRegisters(2, 0, 2);

// 打印每个寄存器的原始值
for (int i = 0; i < inputRegisters.Length; i++)
{
    Console.WriteLine($"输入寄存器 {i}:{inputRegisters[i]}");
}

// 如果这两个寄存器存的是一个 float,可以拼接成小数(第三天学的方法)
float temperature = ConvertToFloat(inputRegisters);
Console.WriteLine($"温度:{temperature}");

五、保持寄存器 Holding Registers —— 你一直在用的寄存器

5.1 它是什么

保持寄存器是 16 位(2 字节) 的寄存器,可以存 0 到 65535 的整数。

它是四种寄存器里最通用的:可读可写,可以存输入、输出、配置参数等。

5.2 能读能写

保持寄存器是 可读可写 的。

  • 读:获取当前设定值或数据
  • 写:修改设定值或控制参数

5.3 实际用途

保持寄存器用途非常广:

用途 例子
设定值 目标温度、目标转速
配置参数 PID 参数、设备地址
测量值 有些设备把测量值也放在这里
状态数据 设备状态字

这也是为什么很多模拟器(包括 Modbux)默认把 I/O 都映射到保持寄存器。

5.4 什么时候用

当你要 读写一个 16 位的数字 时,用保持寄存器。

真实场景:你的上位机上有一个“目标温度”输入框。操作员输入 50,程序往保持寄存器写 50,设备把目标温度设为 50。设备也可以把当前温度放在保持寄存器里,程序读出来显示。

5.5 C# 代码

// 读取保持寄存器:从地址 0 开始,读 2 个寄存器
// 返回值:ushort[] —— 每个元素是一个 16 位寄存器值
ushort[] holdingRegisters = master.ReadHoldingRegisters(2, 0, 2);

// 打印每个寄存器的原始值
for (int i = 0; i < holdingRegisters.Length; i++)
{
    Console.WriteLine($"保持寄存器 {i}:{holdingRegisters[i]}");
}

// 写入单个保持寄存器:把地址 0 的值设为 500
// slaveID: 从站地址
// registerAddress: 要写的寄存器地址
// value: 要写入的值(0-65535)
master.WriteSingleRegister(2, 0, 500);

// 写入多个保持寄存器:从地址 0 开始,连续写入 10、20、30、40
master.WriteMultipleRegisters(2, 0, new ushort[] { 10, 20, 30, 40 });

六、四种寄存器对比速查表

寄存器类型 存什么 位数 能读吗 能写吗 典型用途
线圈 Coils 开关 1 位 ✅ ✅ 控制继电器、电机、阀门
离散输入 Discrete Inputs 开关 1 位 ✅ ❌ 读限位开关、按钮、门磁
输入寄存器 Input Registers 数字 16 位 ✅ ❌ 读温度、压力、电流
保持寄存器 Holding Registers 数字 16 位 ✅ ✅ 设目标值、读测量值

记忆口诀:

  • 能写的:线圈(开关)、保持寄存器(数字)
  • 只读的:离散输入(开关)、输入寄存器(数字)

七、完整例子:同时读四种寄存器

7.1 在 Modbux 里准备数据

打开 Modbux,SERVER 模式,Unit ID = 2。分别在四块区域添加数据:

区域 地址 0 地址 1
Coils 1(开) 0(关)
Discrete Inputs 1 0
Input Registers 100 200
Holding Registers 52684 52289

7.2 C# 代码

using System;
using System.Net.Sockets;
using NModbus;

class Program
{
    static void Main()
    {
        // 连接 Modbux
        var client = new TcpClient("127.0.0.1", 502);
        var factory = new ModbusFactory();
        IModbusMaster master = factory.CreateMaster(client);

        // ===== 1. 读线圈 =====
        bool[] coils = master.ReadCoils(2, 0, 2);
        Console.WriteLine("=== 线圈 ===");
        for (int i = 0; i < coils.Length; i++)
        {
            Console.WriteLine($"  线圈 {i}:{coils[i]}");
        }

        // ===== 2. 读离散输入 =====
        bool[] inputs = master.ReadInputs(2, 0, 2);
        Console.WriteLine("=== 离散输入 ===");
        for (int i = 0; i < inputs.Length; i++)
        {
            Console.WriteLine($"  离散输入 {i}:{inputs[i]}");
        }

        // ===== 3. 读输入寄存器 =====
        ushort[] inputRegs = master.ReadInputRegisters(2, 0, 2);
        Console.WriteLine("=== 输入寄存器 ===");
        for (int i = 0; i < inputRegs.Length; i++)
        {
            Console.WriteLine($"  输入寄存器 {i}:{inputRegs[i]}");
        }

        // ===== 4. 读保持寄存器 =====
        ushort[] holdingRegs = master.ReadHoldingRegisters(2, 0, 2);
        Console.WriteLine("=== 保持寄存器 ===");
        for (int i = 0; i < holdingRegs.Length; i++)
        {
            Console.WriteLine($"  保持寄存器 {i}:{holdingRegs[i]}");
        }

        // ===== 5. 写一个线圈试试 =====
        master.WriteSingleCoil(2, 0, false);  // 把线圈 0 关掉
        Console.WriteLine("=== 已写入:线圈 0 = false ===");

        // ===== 6. 写一个保持寄存器试试 =====
        master.WriteSingleRegister(2, 0, 999);  // 把保持寄存器 0 改成 999
        Console.WriteLine("=== 已写入:保持寄存器 0 = 999 ===");

        Console.ReadLine();
    }

    // 把两个寄存器拼成 float(第三天学的方法)
    static float ConvertToFloat(ushort[] registers)
    {
        byte[] bytes = new byte[4];
        bytes[0] = (byte)(registers[0] >> 8);
        bytes[1] = (byte)(registers[0] & 0xFF);
        bytes[2] = (byte)(registers[1] >> 8);
        bytes[3] = (byte)(registers[1] & 0xFF);
        return BitConverter.ToSingle(bytes, 0);
    }
}

7.3 预期输出

=== 线圈 ===
  线圈 0:True
  线圈 1:False
=== 离散输入 ===
  离散输入 0:True
  离散输入 1:False
=== 输入寄存器 ===
  输入寄存器 0:100
  输入寄存器 1:200
=== 保持寄存器 ===
  保持寄存器 0:52684
  保持寄存器 1:52289
=== 已写入:线圈 0 = false ===
=== 已写入:保持寄存器 0 = 999 ===

八、常见问题

现象 原因 解决
SlaveException Unit ID 或地址不对 检查 Modbux 里的 Unit ID 和地址
读 Input Registers 报错 方法名写错了 用 ReadInputRegisters,不是 ReadHoldingRegisters
写 Coil 报错 用了数字,不是 bool 改成 true 或 false
读出来全是 0 或 false 地址没设值 在 Modbux 里对应区域加数据
ReadInputs 报错 方法名写错了 用 ReadInputs,不是 ReadDiscreteInputs
读 Input Registers 报错“地址不存在” Modbux 里 Input Registers 区域是空的 先加数据,再加地址

九、你学到了什么

  • 四种寄存器的本质区别:开关/数字 × 可写/只读
  • 线圈和保持寄存器可以写,离散输入和输入寄存器只能读
  • 方法名不同,参数顺序一样:从站、起始地址、数量
  • 数值类读出来是 ushort[],开关类读出来是 bool[]
  • 用哪种寄存器,看设备手册——设备手册会告诉你数据放在哪里

Modbus 四种寄存器总结

一、四种寄存器总览

寄存器 中文名 存什么 能读 能写 C# 里读出来
Coils 线圈 开关 ✅ ✅ bool[]
Discrete Inputs 离散输入 开关 ✅ ❌ bool[]
Input Registers 输入寄存器 数字 ✅ ❌ ushort[]
Holding Registers 保持寄存器 数字 ✅ ✅ ushort[]

注意:表中“存什么”描述的是 Modbus 协议层面的数据性质。在 C# 代码里,NModbus 库会把开关类的 0/1 自动转换成 bool(0→false,1→true),把数字类的值返回为 ushort。协议层存的是 0/1,语言层拿到的是 bool/ushort,两者不矛盾。

二、按“存什么”分两类

类别 寄存器 存什么 C# 里读出来
开关类 Coils、Discrete Inputs 0 或 1 bool[]
数字类 Input Registers、Holding Registers 0 ~ 65535 ushort[]

三、按“能不能写”分两类

类别 寄存器 能写吗
可写 Coils、Holding Registers ✅ 能读能写
只读 Discrete Inputs、Input Registers ❌ 只能读

记忆口诀:

  • 带“输入”的(Discrete Inputs、Input Registers)→ 只能读
  • 不带“输入”的(Coils、Holding Registers)→ 能读能写

四、四种寄存器的 C# 方法

寄存器 读方法 写方法
Coils ReadCoils WriteSingleCoil / WriteMultipleCoils
Discrete Inputs ReadInputs 无
Input Registers ReadInputRegisters 无
Holding Registers ReadHoldingRegisters WriteSingleRegister / WriteMultipleRegisters

参数顺序都一样:

master.ReadCoils(从站, 起始地址, 数量);
master.ReadInputs(从站, 起始地址, 数量);
master.ReadInputRegisters(从站, 起始地址, 数量);
master.ReadHoldingRegisters(从站, 起始地址, 数量);

写方法:

master.WriteSingleCoil(从站, 地址, true/false);
master.WriteSingleRegister(从站, 地址, 值);

五、四种寄存器典型用途

寄存器 典型用途 例子
Coils 控制开关 启动电机、打开阀门、点亮指示灯
Discrete Inputs 读开关状态 限位开关、按钮、门磁
Input Registers 读实时数据 温度、压力、电流
Holding Registers 读/写参数 目标温度、PID 参数、累计产量

六、四种寄存器最容易混的点

容易混的点 正确理解
以为 Discrete Inputs 读出来是 0/1 ❌ 是 bool(true/false)
以为 Input Registers 能写 ❌ 只能读
以为 Coils 读出来是数字 ❌ 是 bool
以为四种寄存器的读方法参数不一样 ❌ 参数顺序完全一样

补充说明:上表“读出来是 bool”是指 C# 代码里拿到 bool 类型的值。协议层存的是 0/1,NModbus 库负责转换。不要和“C 语言非 0 即 true”混淆——C# 的 bool 只有 true/false,不允许用数字做条件判断。

七、四种寄存器最核心的两句话

  • 开关类的读出来是 bool,数字类的读出来是 ushort
  • 带“输入”的只能读,不带“输入”的能读能写

八、实际工作中怎么用

你不能自己决定用哪种寄存器,只能按设备手册来。

手册说 你用什么
温度在输入寄存器 ReadInputRegisters
电机控制在线圈 WriteSingleCoil
累计产量在保持寄存器 ReadHoldingRegisters
限位开关在离散输入 ReadInputs

手册怎么写,你就怎么读。

最后编辑于 :
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容