MODBUS通信中V区数据读写实战:地址映射、字节序处理与故障排查
在工业自动化项目中,MODBUS协议因其简单、开放和广泛应用而成为设备间通信的首选。然而,许多开发者在处理PLC(如西门子S7-200 SMART、三菱FX系列)或组态软件(如MCGS Pro、KingView)的V区数据时,常常会遇到地址映射混乱、数据读写失败、高低字节错位等问题。本文将从实战角度出发,系统性地拆解MODBUS通信中V区的核心概念、地址计算规则、通信代码实现以及高频故障排查,提供一个从理论到落地的完整解决方案。无论你是正在学习工控通信的学生,还是需要快速解决现场通信问题的工程师,都能从中获得可直接复用的代码和清晰的排错思路。
1. 背景与核心概念:为什么是V区?
在深入MODBUS操作之前,我们必须先理解“V区”是什么,以及它在工业控制系统中的角色。
1.1 V区的定义与作用
V区,通常指变量存储区(Variable Memory Area),是PLC内部一块用于存储用户程序中间数据、状态标志、计数器/计时器当前值等信息的存储区域。它不同于输入(I)、输出(Q)、内部标志位(M)等有固定功能的区域,V区更为灵活,常用于存储工艺参数、计算中间结果、设备状态等用户自定义数据。
在不同的PLC品牌中,V区的名称可能不同:
- 西门子S7-200/200 SMART: 称为V存储区(V Memory)。
- 三菱FX系列: 通常对应数据寄存器(D Register)。
- 欧姆龙CP/CJ系列: 对应数据存储器(DM Area)。
- 组态软件(如MCGS): 软件内部定义的变量,在与PLC通信时,会被映射到PLC的特定存储区(如V区)。
核心价值: V区是PLC与上位机(SCADA、HMI、MES)进行数据交换的核心桥梁。上位机通过MODBUS协议读写V区的数据,实现对生产过程的状态监控、参数设定和逻辑控制。
1.2 MODBUS协议与V区的关联
MODBUS协议本身并不认识“V区”。它定义了一套通用的数据模型,主要包含四种数据类型:
- 线圈(Coils): 1位,可读可写,常对应PLC的Q点或M点。
- 离散输入(Discrete Inputs): 1位,只读,常对应PLC的I点。
- 保持寄存器(Holding Registers): 16位,可读可写,这是与PLC V区通信最主要的数据类型。
- 输入寄存器(Input Registers): 16位,只读。
PLC的V区数据(通常是16位整数、32位浮点数等)正是通过映射到MODBUS的“保持寄存器”地址空间,才能被外部设备访问。因此,MODBUS通信操作V区的本质,就是操作保持寄存器。
关键点: MODBUS地址是从0开始的纯数字地址,而PLC编程软件中使用的地址(如VW100)是带有区号和偏移的符号地址。两者之间的转换是通信成功的第一步,也是最容易出错的地方。
2. 环境准备与版本说明
为了进行可复现的实战演示,我们需要搭建一个仿真环境。这里不依赖任何真实的物理PLC,而是使用软件模拟,这非常适合学习和前期调试。
推荐环境配置:
- 操作系统: Windows 10/11 64位
- MODBUS从站模拟软件: Modbus Slave (v7.4.2或更高版本)。用于模拟一台PLC,提供V区数据。
- MODBUS主站调试工具/编程环境:
- 调试工具: Modbus Poll (v7.4.2或更高版本)。用于快速测试通信。
- 编程环境: Python 3.8+ 或 C# (.NET 6+)。本文将提供Python和C#两种语言的示例代码。
- 网络抓包工具(可选但强烈推荐): Wireshark。用于深度分析通信报文,是排查复杂问题的利器。
软件获取与安装:
- Modbus Slave/Poll: 可从其官方网站或可靠的软件下载站获取试用版。安装过程简单,一直点击“下一步”即可。
- Python: 访问 python.org 下载安装包,安装时务必勾选“Add Python to PATH”。
- .NET SDK: 访问微软官网下载并安装.NET 6.0或更高版本的SDK。
- Wireshark: 从 wireshark.org 下载安装。
版本兼容性说明: MODBUS RTU/TCP协议是标准协议,不同版本的软件在基础功能上兼容。本文示例代码基于通用的pymodbus(Python) 和NModbus4(C#) 库编写,这些库的API相对稳定,但读者在实际项目中仍需注意查阅所用库的最新文档。
3. 核心原理:MODBUS地址与V区地址的映射规则
这是整个MODBUS通信的基石,理解错误将导致所有读写操作失败。
3.1 MODBUS地址编码规则
MODBUS协议有两种地址表示法:
- 协议数据单元(PDU)地址: 从0开始的偏移量。例如,要访问PLC中地址为VW100(一个字,即两个字节)的数据,在PDU中可能表示为地址
99(因为100-1=99)。 - MODBUS应用层(MBAP)地址: 在协议帧中,有时会使用“从1开始”的地址,或在工具中直接输入带功能码的地址,如
4x100。
最常用的约定(也是Modbus Poll/Slave的默认设置)是: 保持寄存器的访问,使用“从0开始”的十进制偏移量作为MODBUS地址。
3.2 西门子S7-200 SMART V区映射示例
假设PLC中定义了以下变量:
VD100: 32位实数(浮点数),占用VW100和VW102两个寄存器。VW104: 16位整数,占用一个寄存器。VB106: 8位字节,但它会占用一个完整的寄存器(VW106),高字节(VB106)有效,低字节(VB107)可能为0或未定义。
映射关系如下:
| PLC地址 | 数据类型 | 占用字节 | 占用寄存器 | MODBUS保持寄存器地址(十进制,从0开始) |
|---|---|---|---|---|
| VW100 | 16位整数 | 2 | 1 | 100 |
| VW102 | 16位整数 | 2 | 1 | 102 |
| VD100 | 32位浮点 | 4 | 2 | 100 (高字), 102 (低字) |
| VW104 | 16位整数 | 2 | 1 | 104 |
| VB106 | 8位字节 | 1 (占2) | 1 | 106 |
关键理解:
- MODBUS以寄存器(16位)为单位进行读写。一个寄存器对应PLC的一个“字”(如VW100)。
- 对于32位数据(如浮点数、长整型),需要连续读写两个寄存器。字节顺序(高低位)至关重要,这直接导致了“高低字节交换”这一常见问题。
3.3 字节顺序(Endianness)与字顺序(Word Order)
这是MODBUS通信中最经典的“坑”。不同的设备厂商对多寄存器数据的解释方式不同。
- 字节顺序(Byte Order): 指在一个16位寄存器内,高字节和低字节的排放顺序。
- 大端序(Big-Endian): 高字节在前(低地址),低字节在后。
0x1234在内存中为12 34。 - 小端序(Little-Endian): 低字节在前,高字节在后。
0x1234在内存中为34 12。x86架构的计算机和许多PLC(如西门子)默认使用小端序。
- 大端序(Big-Endian): 高字节在前(低地址),低字节在后。
- 字顺序(Word Order): 对于32位数据占用两个寄存器,哪个寄存器存放高16位,哪个存放低16位。
- 高字在前: MODBUS地址小的寄存器存放高16位。
- 低字在前: MODBUS地址小的寄存器存放低16位。
常见组合:
- CDAB (Modbus默认): 字顺序为高字在前,字节顺序为大端序。较少见。
- ABCD: 字顺序为高字在前,字节顺序为小端序。
- BADC: 字顺序为低字在前,字节顺序为大端序。
- DCBA (西门子常见): 字顺序为低字在前,字节顺序为小端序。这是与西门子PLC通信时最常需要处理的顺序。
在Modbus Slave软件中,可以通过右键点击寄存器值,选择“Float”或“Double”视图,并在“Format”中切换字节顺序来模拟不同设备。
4. 完整实战案例:Python/C#读写MODBUS V区数据
我们将创建一个完整的仿真测试流程:用Modbus Slave模拟一个PLC的V区,然后用Python和C#编写主站程序对其进行读写。
4.1 从站仿真设置(Modbus Slave)
- 打开Modbus Slave。
- 点击菜单
Connection -> Connect,选择连接方式。为简单起见,我们选择Modbus TCP/IP,端口号默认502,点击OK。 - 软件界面会出现一个寄存器表格。点击菜单
Setup -> Slave Definition。 - 在定义窗口中:
Slave ID: 设置为1(从站地址)。Function: 选择03 Holding Register(保持寄存器)。Address: 设置为100(对应PLC的VW100开始)。Quantity: 设置为10(我们模拟从VW100到VW118的10个寄存器)。- 点击OK。
- 现在表格中地址从100开始,显示了10个寄存器的值(默认是0)。我们手动修改几个值用于测试:
- 在地址100的
Value列双击,输入12345。 - 在地址102的
Value列双击,输入6789。 (注意:此时我们是以16位整数形式写入,用于测试整数读写。浮点数测试需要特殊处理)。
- 在地址100的
4.2 Python主站程序开发
我们将使用pymodbus这个强大的库。首先安装它:
pip install pymodbus示例1:读取单个寄存器(16位整数)
# read_single_register.py from pymodbus.client import ModbusTcpClient def read_v_area(): # 1. 连接到从站 # 参数:主机IP(本机模拟就用127.0.0.1), 端口号 client = ModbusTcpClient('127.0.0.1', port=502) if not client.connect(): print("无法连接到Modbus从站") return try: # 2. 读取保持寄存器 # 参数:从站地址, 起始地址(十进制,从0开始), 寄存器数量 # 注意:我们要读PLC的VW100,对应MODBUS地址是100。 # 但pymodbus的`read_holding_registers`方法使用从0开始的地址。 # 在Modbus Slave中我们设置起始地址为100,所以这里读地址0,实际对应Slave中的地址100。 # 这是一个常见的混淆点!更通用的做法是:Slave定义从100开始,代码中也从100开始读。 # 我们需要统一基准。这里假设我们统一使用“协议地址”,即Slave中的100就是100。 # 对于pymodbus,传入的地址参数就是协议地址。 response = client.read_holding_registers(address=100, count=1, slave=1) if response.isError(): print(f"读取寄存器错误: {response}") else: # 返回的是一个寄存器列表,即使只读一个 value = response.registers[0] print(f"读取地址100(VW100)的值为: {value} (十六进制: 0x{value:04X})") # 预期输出应为我们在Modbus Slave中设置的12345 except Exception as e: print(f"通信过程中发生异常: {e}") finally: # 3. 关闭连接 client.close() if __name__ == "__main__": read_v_area()运行此脚本,应该能成功读取到我们在Modbus Slave中设置的12345。
示例2:写入单个寄存器
# write_single_register.py from pymodbus.client import ModbusTcpClient from pymodbus.payload import BinaryPayloadBuilder, Endian def write_v_area(): client = ModbusTcpClient('127.0.0.1', port=502) if not client.connect(): print("连接失败") return try: # 写入一个16位整数到地址104(VW104) write_value = 5555 response = client.write_register(address=104, value=write_value, slave=1) if response.isError(): print(f"写入寄存器错误: {response}") else: print(f"成功写入值 {write_value} 到地址104") # 立即读取验证 verify_response = client.read_holding_registers(address=104, count=1, slave=1) if not verify_response.isError(): print(f"验证读取,地址104的值为: {verify_response.registers[0]}") except Exception as e: print(f"发生异常: {e}") finally: client.close() if __name__ == "__main__": write_v_area()示例3:读写32位浮点数(处理字节顺序)这是核心难点。假设我们要将浮点数123.456写入PLC的VD100(占用VW100和VW102)。
# read_write_float.py from pymodbus.client import ModbusTcpClient from pymodbus.payload import BinaryPayloadBuilder, BinaryPayloadDecoder from pymodbus.constants import Endian def float_operations(): client = ModbusTcpClient('127.0.0.1', port=502) if not client.connect(): return try: # --- 写入浮点数 --- float_to_write = 123.456 builder = BinaryPayloadBuilder(byteorder=Endian.Little, wordorder=Endian.Little) # 设置字节序为小端,字序为小端(模拟西门子PLC的DCBA顺序) builder.add_32bit_float(float_to_write) payload = builder.build() # 写入到起始地址100(VD100) response = client.write_registers(address=100, values=payload, slave=1, skip_encode=True) if response.isError(): print("写入浮点数失败") else: print(f"已写入浮点数: {float_to_write}") # --- 读取浮点数 --- # 从地址100开始读2个寄存器(32位) read_response = client.read_holding_registers(address=100, count=2, slave=1) if read_response.isError(): print("读取浮点数失败") else: decoder = BinaryPayloadDecoder.fromRegisters( read_response.registers, byteorder=Endian.Little, wordorder=Endian.Little ) read_float = decoder.decode_32bit_float() print(f"读取到的浮点数为: {read_float}") except Exception as e: print(f"错误: {e}") finally: client.close() if __name__ == "__main__": float_operations()关键解释:
BinaryPayloadBuilder和BinaryPayloadDecoder是pymodbus中处理复杂数据类型的利器。byteorder和wordorder参数必须与从站设备(PLC)的字节/字顺序严格匹配。示例中使用的Endian.Little, Endian.Little对应西门子的 DCBA 顺序。skip_encode=True在write_registers中很重要,因为builder.build()返回的已经是编码好的寄存器值列表。
4.3 C#主站程序开发(使用NModbus4)
对于.NET开发者,NModbus4是一个优秀的选择。首先通过NuGet安装:
Install-Package NModbus4C#示例:读写整数和浮点数
// Program.cs using System; using System.Net.Sockets; using Modbus.Device; namespace ModbusVAreaDemo { class Program { static void Main(string[] args) { string ipAddress = "127.0.0.1"; int port = 502; byte slaveId = 1; // 1. 创建TCP客户端并连接 using (TcpClient client = new TcpClient(ipAddress, port)) { // 2. 创建Modbus主站实例 var factory = new ModbusFactory(); IModbusMaster master = factory.CreateMaster(client); // 示例1:读取单个寄存器 (VW100) ushort startAddress = 100; // MODBUS地址 ushort numRegisters = 1; try { ushort[] registers = master.ReadHoldingRegisters(slaveId, startAddress, numRegisters); Console.WriteLine($"读取地址{startAddress}的值: {registers[0]}"); } catch (Exception ex) { Console.WriteLine($"读取失败: {ex.Message}"); } // 示例2:写入单个寄存器 (VW104) ushort writeAddress = 104; ushort valueToWrite = 8888; try { master.WriteSingleRegister(slaveId, writeAddress, valueToWrite); Console.WriteLine($"成功写入{valueToWrite}到地址{writeAddress}"); } catch (Exception ex) { Console.WriteLine($"写入失败: {ex.Message}"); } // 示例3:写入并读取32位浮点数 (VD100) // 假设PLC字节顺序为小端,字顺序为小端 (DCBA) float floatValue = 123.456f; ushort floatStartAddress = 100; // 将float转换为ushort数组(2个寄存器) byte[] floatBytes = BitConverter.GetBytes(floatValue); // 如果是小端字节序,直接转换 ushort highWord = BitConverter.ToUInt16(floatBytes, 2); // 高字(在C#小端序中,高字在后) ushort lowWord = BitConverter.ToUInt16(floatBytes, 0); // 低字 // 注意:对于MODBUS(大端网络字节序)和西门子(小端字序),顺序需要调整 // 更通用的方法是使用NModbus的Utility类 ushort[] floatRegisters = new ushort[2]; // 根据设备调整顺序。这里假设设备期望 [低字, 高字] 且每个字内字节不交换(与网络序一致较复杂,通常需要反转) // 实际项目中,必须根据设备手册确定顺序! // 以下是一种常见情况(设备期望大端字序,大端字节序)的转换: // floatRegisters[0] = (ushort)((floatValueBits >> 16) & 0xFFFF); // floatRegisters[1] = (ushort)(floatValueBits & 0xFFFF); // 简化演示:直接写入两个寄存器(顺序可能错误,仅演示流程) ushort[] valuesToWrite = { lowWord, highWord }; // 此顺序需实测调整 try { master.WriteMultipleRegisters(slaveId, floatStartAddress, valuesToWrite); Console.WriteLine($"已写入浮点数: {floatValue}"); // 读取验证 ushort[] readBack = master.ReadHoldingRegisters(slaveId, floatStartAddress, 2); // 将读取的寄存器数组转换回float(需要逆转换) byte[] bytesBack = new byte[4]; Buffer.BlockCopy(readBack, 0, bytesBack, 0, 4); // 注意:BlockCopy默认按数组顺序拷贝,可能需根据字节序调整 float readFloat = BitConverter.ToSingle(bytesBack, 0); Console.WriteLine($"读取回的浮点数: {readFloat}"); } catch (Exception ex) { Console.WriteLine($"浮点数操作失败: {ex.Message}"); } } Console.ReadLine(); } } }C#代码关键点:
BitConverter类用于处理字节序列与基础类型的转换,但其结果依赖于本机(通常是Little-Endian)字节序。- 工业通信中,必须显式处理网络字节序(Big-Endian)与设备字节序的转换。上述示例中的转换顺序是简化版,实际项目必须根据设备文档编写特定的转换函数或使用成熟的工业通信库(如S7NetPlus等)来处理西门子PLC的复杂数据类型。
4.4 使用Modbus Poll进行快速测试
在编写代码前后,用Modbus Poll进行手动测试能极大提高效率。
- 打开Modbus Poll。
Connection -> Connect, 选择TCP,连接127.0.0.1:502。- 点击菜单
Setup -> Read/Write Definition。 - 设置
Slave ID=1,Function=03 Holding Registers,Address=100,Quantity=10,点击OK。 - 表格中会显示从地址100开始的寄存器值。你可以双击单元格修改值,并点击工具栏上的“Write”按钮(图标像一支笔)写入。
- 尝试写入一个浮点数:选中两个连续的单元格(如100和101),右键 ->
Float,再右键 ->Format选择CDAB或ABCD等格式,然后输入浮点数值。观察Modbus Slave中的值是否变化正确。这是调试字节顺序最直观的方法。
5. 常见问题与排查思路
MODBUS通信问题千奇百怪,但大多遵循以下排查路径。
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 连接失败 | 1. IP地址/端口错误 2. 从站设备未上电或网络不通 3. 防火墙拦截 4. 串口参数错误(RTU) | 1. 使用ping命令测试网络。2. 确认设备指示灯状态。 3. 关闭防火墙或添加出入站规则。 4. 核对波特率、数据位、停止位、校验位。 |
| 读取数据全为0或65535 | 1. 从站地址(Slave ID)错误 2. MODBUS功能码错误 3. 起始地址计算错误 4. 数据区映射错误(如读了输入寄存器而非保持寄存器) | 1. 核对设备手册中的从站地址。 2. 确认是读保持寄存器(03)还是输入寄存器(04)。 3.重点检查地址偏移:是0-based还是1-based?用Modbus Poll从地址0开始扫描。 4. 确认PLC中V区数据是否已成功下载并处于运行状态。 |
| 读取数据错误(非预期值) | 1.字节/字顺序错误(最常见) 2. 数据类型理解错误(如把32位数据当16位读) 3. 寄存器数量不足 | 1. 使用Modbus Poll以不同格式(Float, 32-bit Integer)读取,看哪个值正确。 2. 确认数据在PLC中的真实类型(是INT、DINT还是REAL)。 3. 确保读取的寄存器数量足够覆盖整个数据。 |
| 写入成功但PLC值不变 | 1. PLC处于“运行”模式但V区被程序覆盖 2. 写入的地址是只读区域 3. 写入值超出范围(如给16位寄存器写65536) 4. PLC有写保护 | 1. 在线监控PLC程序,看是否有其他逻辑在修改该V区地址。 2. 确认地址是可写的保持寄存器。 3. 确保写入值在0-65535(对于16位寄存器)之间。 4. 检查PLC的写保护开关或软件设置。 |
| 间歇性通信失败或超时 | 1. 网络抖动或干扰(RS485线路问题) 2. 主站轮询频率过快,从站响应不及 3. 报文长度超过限制(RTU模式) | 1. 检查RS485线路终端电阻、屏蔽层接地。 2. 降低主站的轮询间隔。 3. MODBUS RTU一帧最多读125个寄存器,TCP最多读125个。分批读取。 |
| Modbus Poll能通,自编程序不通 | 1. 库的使用方式错误(如地址参数理解错误) 2. 套接字未正确关闭导致端口占用 3. 线程同步问题 | 1.使用Wireshark抓包对比!对比Modbus Poll发出的报文和你程序发出的报文,逐字节比较。 2. 确保程序中使用 using语句或finally块关闭连接。3. 避免在多线程中共享同一个未加锁的Modbus客户端实例。 |
终极排查工具——Wireshark抓包分析:
- 打开Wireshark,选择正确的网卡(如果是本机通信,选“Adapter for loopback traffic”)。
- 设置过滤条件:
tcp.port == 502。 - 开始抓包,运行你的通信程序或Modbus Poll进行操作。
- 分析抓到的包。一个成功的MODBUS TCP请求/响应如下:
- 请求帧: 事务标识符、协议标识符(0)、长度、单元标识符(Slave ID)、功能码(03)、起始地址高/低字节、寄存器数量高/低字节。
- 响应帧: 事务标识符(与请求匹配)、协议标识符、长度、单元标识符、功能码、字节数、寄存器数据。 对比正常和异常的报文,能精准定位是地址错误、长度错误还是数据错误。
6. 最佳实践与工程建议
将MODBUS通信集成到实际项目中时,遵循以下原则可以提升代码的健壮性和可维护性。
6.1 配置化与抽象
- 地址映射表: 不要将MODBUS地址硬编码在代码中。应使用配置文件(如JSON、YAML)或数据库来维护“变量名-PLC地址-数据类型-转换系数”的映射关系。
// address_mapping.json [ { "name": "Temperature", "plcAddress": "VD100", "modbusAddress": 100, "dataType": "float32", "byteOrder": "DCBA", "scale": 0.1 }, { "name": "MotorSpeed", "plcAddress": "VW104", "modbusAddress": 104, "dataType": "uint16", "scale": 1 } ] - 通信层抽象: 将MODBUS的读写操作封装成一个独立的服务类(如
ModbusClientService)。这个类负责连接管理、超时重试、数据打包/解包、异常处理,并对上层业务提供如ReadFloat(string variableName)、WriteInt(string variableName, int value)等语义清晰的接口。
6.2 健壮性设计
- 连接管理与重连机制: 实现心跳检测(定期读一个固定寄存器),在连接断开时自动尝试重连,并记录重连日志。
- 超时与重试: 为每次读写操作设置合理的超时时间(如3秒)。对于重要数据,实现有限次数的重试逻辑(如3次)。
- 数据校验与缓存: 对读取的数据进行合理性校验(如范围、变化率)。实现数据缓存,避免因单次通信失败导致业务逻辑获取到空值或旧值。
- 异常分级处理: 区分网络异常、从站无响应、数据校验错误等不同级别的异常,并采取不同策略(重试、告警、降级处理)。
6.3 性能优化
- 批量读取: 绝对避免在循环中逐点读取。将需要监控的多个变量地址整理成连续的块,使用
ReadHoldingRegisters一次读取大量寄存器,然后在本地解析。这能极大减少网络交互和从站处理压力。 - 异步通信: 对于需要高频读写的场景,使用异步API(如
pymodbus的异步客户端,或C#的async/await)避免阻塞主线程。 - 合理的轮询周期: 根据数据变化频率设置不同的轮询间隔。实时数据(如电机转速)可以100ms读一次,静态参数(如设备型号)可以只在启动时读一次。
6.4 安全与生产环境注意事项
- 网络隔离: 确保工业控制网络与办公网络进行物理或逻辑隔离,防止来自互联网的攻击。
- 访问权限: 如果PLC支持,设置MODBUS从站的访问密码或IP白名单。
- 写操作保护: 对写入操作(尤其是控制命令、关键参数)增加二次确认或权限校验。记录所有写操作的日志,包括操作者、时间、旧值、新值。
- 优雅降级: 当通信持续失败时,系统应能进入安全模式(如设备停机、保持最后有效值、发出严重警报),而不是崩溃或产生不可控行为。
- 版本与备份: 在修改PLC的V区地址定义或通信参数前,务必对PLC程序进行备份。通信程序的配置变更也应纳入版本管理。
掌握MODBUS与V区的通信,是打开工业自动化数据交互大门的关键钥匙。它要求开发者不仅理解协议本身,更要深入理解PLC的数据存储模型和设备的字节序约定。从使用Modbus Poll/Slave进行基础测试,到用Python/C#编写健壮的通信服务,再到利用Wireshark进行深度排错,这是一个从工具使用到原理掌握,再到工程化实践的完整闭环。建议读者按照本文的步骤,亲手搭建仿真环境,完成一遍代码编写和调试,遇到问题时,善用抓包工具进行比对分析。在实际项目中,务必重视配置管理、异常处理和性能优化,这样才能构建出稳定可靠的工业通信系统。