这是系列第六篇。前五天你一直在用 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 |
手册怎么写,你就怎么读。