近两年工业控制领域的国产化替代进程明显加速,从电厂、轨道交通到智能制造产线,越来越多的核心监控系统开始从Windows+X86平台向国产CPU+国产操作系统迁移。对于大量基于C#开发的存量工控软件而言,如何低成本、低风险地完成跨架构适配,是很多团队面临的现实问题。

去年我们团队承接了某能源企业辅机监控系统的国产化改造项目,原有系统基于.NET Framework开发,已在Windows工控机上稳定运行五年,核心数据采集依赖Modbus TCP/RTU协议对接现场几十台PLC与仪表。改造目标是将整套系统无缝迁移至统信UOS + 鲲鹏920架构的国产工控机,要求功能完全对齐、性能不低于原有水平、现场改造不影响正常生产。

本文从工程实战角度,完整拆解Modbus协议栈在ARM64架构国产系统上的适配过程,包括架构设计、核心代码改造、踩坑排查与性能验证,希望给正在做工控信创迁移的同行提供一些可复用的经验。

一、迁移背景与技术选型

1.1 为什么选择继续用C#

很多人觉得国产化就要用C++或者Java,但对于存量项目来说,完全重写的成本和风险都极高。我们评估过三个技术路线:

  • 全量C++重写:性能最优,但开发周期长达半年,现场调试与业务逻辑复刻风险大
  • Java技术栈重构:生态完善,但团队技术栈不匹配,原有工控业务逻辑迁移工作量巨大
  • 升级至.NET 8跨平台:依托.NET原生跨平台能力迁移,核心业务代码复用率超80%,周期最短

最终选择了第三条路线。.NET 8对ARM64 Linux做了深度优化,原生支持鲲鹏架构,统信UOS也在官方兼容列表内,整体迁移成本最低、风险可控,是存量工控软件国产化的最优解之一。

1.2 核心环境与协议选型

Modbus是工业现场应用最广的事实标准协议,本次适配同时覆盖Modbus TCP以太网与Modbus RTU串口两种主流传输方式,整体环境配置如下:

  • 硬件平台:国产工控机,鲲鹏920 8核处理器,8GB内存,4路RS485串口,双千兆网口
  • 操作系统:统信UOS 专业版 1050a(ARM64架构)
  • 运行时:.NET 8.0,采用自包含部署模式,降低现场环境依赖
  • 协议实现:自主封装轻量Modbus协议栈,替换原有Windows专属第三方库

二、系统整体分层架构设计

为了最大限度复用原有业务代码,我们采用分层解耦的架构设计,把协议相关的适配逻辑全部收敛到驱动层,上层业务逻辑几乎不需要修改。整体架构从上到下分为四层,职责清晰,边界明确。

业务应用层
数据采集/报警/报表/人机交互

协议抽象层
IModbusMaster统一接口

Modbus TCP实现

Modbus RTU实现

.NET Socket 跨平台网络栈

System.IO.Ports 串口适配层

统信UOS ARM64 系统层

鲲鹏920 硬件层

工业以太网交换机

RS485串口总线

支持Modbus TCP的PLC/仪表

支持Modbus RTU的传感器/设备

2.1 分层设计思路

  • 业务应用层:保留原有的数据采集、报警处理、参数下发、历史曲线、报表输出等业务逻辑,代码复用率90%以上,仅修改少量Windows专属UI控件。
  • 协议抽象层:定义统一的IModbusMaster接口,封装读保持寄存器、写单个寄存器、批量读写等通用方法。上层业务只依赖接口,不关心底层是TCP还是RTU,也不关心运行平台。
  • 平台适配层:分别实现Modbus TCP和Modbus RTU的跨平台传输逻辑,处理不同操作系统下的Socket、串口API差异,以及字节序、编码等架构相关问题。
  • 硬件系统层:统信UOS操作系统 + 鲲鹏硬件,提供底层网络、串口硬件驱动支持。

这种分层架构的优势在于,后续如果要适配其他国产系统(如麒麟OS)或其他国产CPU(飞腾、龙芯),仅需修改适配层少量代码,上层业务完全不受影响。

三、核心适配难点拆解

正式开发前我们先做了一轮原型验证,发现了几个核心兼容性问题,也是本次迁移的主要工作量所在:

  1. 串口API跨架构差异大:Windows下的SerialPort类很多默认行为在Linux ARM下并不一致,包括设备命名规则、流控制默认值、超时机制、访问权限等,直接移植必然报错。
  2. 第三方库存在架构依赖:原有系统使用的第三方Modbus库,老版本串口实现调用了Windows API,编译到ARM64下直接报错,运行时也会出现接收无响应问题。
  3. 内存对齐与字节序隐患:Modbus协议规定多字节数据采用大端传输,鲲鹏ARM与X86同为小端架构,基础转换逻辑可复用;但结构体批量映射时,ARM的内存对齐规则与X86有差异,直接用非托管转换容易出现字段偏移错误。
  4. 系统依赖与环境差异:统信UOS最小化安装缺少.NET运行时依赖的原生库,防火墙、安全策略也可能拦截Modbus TCP端口与串口访问,很多时候程序跑不起来不是代码问题,是环境没配好。

四、Modbus协议栈的跨平台实现

针对上述问题,我们没有继续依赖第三方库,而是基于.NET标准库重新封装了轻量版Modbus协议栈,只保留工业现场最常用的功能,确保全平台兼容。

4.1 协议抽象层设计

首先定义统一的Modbus主站接口,彻底隔离上层业务与底层传输实现:

public interface IModbusMaster : IDisposable
{
    // 读保持寄存器
    Task<ushort[]> ReadHoldingRegistersAsync(byte slaveId, ushort startAddr, ushort count);
    
    // 写单个寄存器
    Task WriteSingleRegisterAsync(byte slaveId, ushort addr, ushort value);
    
    // 批量写多个寄存器
    Task WriteMultipleRegistersAsync(byte slaveId, ushort startAddr, ushort[] values);
    
    bool IsConnected { get; }
    Task OpenAsync();
    Task CloseAsync();
}

上层业务只依赖该接口,后续替换传输方式或适配新平台,都不会影响业务逻辑。整体数据收发流程如下:

TCP

RTU

上层业务发起读写请求

协议层组装Modbus ADU报文

大端字节序标准化处理

传输类型?

Socket发送报文

串口适配层参数校验

SerialPort发送二进制报文

等待响应数据

接收原始字节流

CRC/LRC校验与报文解析

字节序转换为本地格式

返回业务层结构化数据

4.2 字节序统一处理

字节序是跨架构最容易踩的基础坑。我们封装了统一的字节转换工具类,无论运行在什么架构下,都强制输出大端字节序,解析时再转回本地字节序,从根本上避免架构差异导致的数值错误。

public static class ModbusByteConverter
{
    public static byte[] GetBytesBigEndian(ushort value)
    {
        var bytes = BitConverter.GetBytes(value);
        if (BitConverter.IsLittleEndian)
            Array.Reverse(bytes);
        return bytes;
    }

    public static ushort ToUInt16BigEndian(byte[] buffer, int offset)
    {
        if (BitConverter.IsLittleEndian)
        {
            return BitConverter.ToUInt16(new[] { buffer[offset + 1], buffer[offset] }, 0);
        }
        return BitConverter.ToUInt16(buffer, offset);
    }
}

Modbus TCP与RTU的报文组装、解析全部基于该工具类,确保鲲鹏ARM与X86平台输出的报文完全一致。

4.3 Modbus TCP实现

TCP部分的跨平台性最好,.NET的Socket原生支持Linux ARM,几乎不需要大改。核心工作是处理连接断开自动重连、超时控制,以及配合上面的字节序工具类完成报文组装。

需要特别注意的是长连接保活。工业现场网络环境复杂,TCP连接空闲一段时间后容易被防火墙或系统断开。我们显式开启了Socket的KeepAlive选项,并调整心跳间隔,有效降低了空闲断连导致的通讯超时。

4.4 Modbus RTU串口适配

RTU串口是适配的重点,也是坑最多的部分。我们基于System.IO.Ports重新实现了传输层,针对统信UOS+ARM架构做了专项适配。

最关键的一点:所有串口参数必须显式指定,绝对不能依赖默认值。Windows与Linux下的串口默认行为差异很大,依赖默认值必然出问题。

public class ModbusRtuMaster : IModbusMaster
{
    private readonly SerialPort _serialPort;
    private readonly string _portName;
    private readonly int _baudRate;

    public bool IsConnected => _serialPort?.IsOpen ?? false;

    public ModbusRtuMaster(string portName, int baudRate, Parity parity, int dataBits, StopBits stopBits)
    {
        _portName = portName;
        _baudRate = baudRate;
        _serialPort = new SerialPort();
    }

    public Task OpenAsync()
    {
        return Task.Run(() =>
        {
            _serialPort.PortName = _portName;
            _serialPort.BaudRate = _baudRate;
            _serialPort.Parity = _parity;
            _serialPort.DataBits = _dataBits;
            _serialPort.StopBits = _stopBits;
            
            // 关键:Linux ARM下必须显式关闭流控,否则只发不收
            _serialPort.DtrEnable = false;
            _serialPort.RtsEnable = false;
            _serialPort.Handshake = Handshake.None;
            
            // 超时适配Linux,加大最小阈值避免误触发
            _serialPort.ReadTimeout = 50;
            _serialPort.WriteTimeout = 50;
            
            _serialPort.Open();
            _serialPort.DiscardInBuffer();
            _serialPort.DiscardOutBuffer();
        });
    }
}

这里两个核心细节:一是必须显式关闭DTR、RTS与硬件流控,统信下很多串口驱动默认开启流控,会导致RTU通讯只有发送没有接收;二是读超时不能设太小,Linux串口调度精度不如Windows,10ms以内的超时很容易误触发,现场调到50ms兼顾实时性与稳定性。

至于CRC16校验部分,属于纯算法逻辑,与平台无关,直接复用原有代码即可。

4.5 规避内存对齐坑

对于批量寄存器转结构体的场景,我们没有使用Marshal非托管转换,而是手动按偏移量解析每个字段。虽然代码量略多,但彻底规避了ARM与X86的内存对齐差异,工业软件稳定优先,可控性比简洁性更重要。

五、统信UOS+鲲鹏环境部署配置

代码写好只是第一步,现场部署还有很多系统层面的配置要做。很多时候程序跑不起来不是代码问题,是环境没配到位。

5.1 自包含部署模式

工业现场环境复杂,我们推荐采用自包含(Self-Contained)部署模式,把.NET运行时和程序打包在一起,不需要在目标机器上安装SDK,最大限度减少环境依赖。

发布命令指定ARM64 Linux目标平台:

dotnet publish -c Release -r linux-arm64 --self-contained true /p:PublishSingleFile=true

生成的单个可执行文件拷贝到统信机器上,赋予执行权限即可运行。

5.2 串口权限配置

统信UOS下普通用户默认没有串口访问权限,最规范的做法是把运行程序的用户加入dialout用户组:

sudo usermod -aG dialout 运行用户名

如果是USB转串口设备频繁插拔,可以编写udev规则固定设备名与权限,避免每次插拔后设备号变化。

5.3 系统依赖补全

统信最小化安装缺少部分原生依赖,即使是自包含部署也需要这些系统库。执行以下命令安装必备依赖:

sudo apt install libicu-dev libssl-dev libgdiplus -y

其中libgdiplus是System.Drawing相关功能需要的,纯后端服务无UI的场景可以不安装。

5.4 开机自启与进程守护

工业现场要求无人值守,我们用systemd配置系统服务,实现开机自启与异常崩溃自动重启。配置重启策略为always,确保程序异常退出后自动拉起,保障现场连续运行。

六、现场踩坑实录与解决方案

整个适配过程踩了不少典型坑,这里挑几个最具代表性的分享,大家做同类迁移时可以直接避坑。

6.1 串口打开提示“拒绝访问”

刚部署时一打开串口就抛IOException,提示拒绝访问。一开始以为是设备路径写错了,核对/dev/ttyUSB0无误,后来才定位到是普通用户没有串口权限。加入dialout组后注销重登即可解决,不建议图省事用root运行程序,工业现场存在安全风险。

6.2 RTU只发不收,报文正确但无响应

这个问题排查了整整一天。用串口抓包工具确认请求报文完全正确,但就是收不到从站响应,同一台设备接到Windows上通讯正常。最终定位到是RtsEnable默认值问题,Linux下默认开启了RTS流控,而我们的485转换器不需要流控,显式设为false后通讯立刻恢复正常。

6.3 中文日志与界面乱码

统信默认字符集是UTF-8,原有代码里日志文件用了GB2312编码,导致中文全是乱码。解决方案是在程序入口注册编码提供程序,同时统一将日志与界面编码改为UTF-8:

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

6.4 自包含程序启动提示缺少libicu

统信服务器最小化安装没有预装国际化组件,.NET需要libicu处理全球化功能。安装对应ARM64版本的libicu-dev即可解决;如果不需要多语言支持,也可以在发布时指定InvariantGlobalization=true禁用全球化功能,减少依赖。

6.5 Modbus TCP偶发空闲断连

长稳测试发现,TCP连接空闲一段时间后会偶发超时。抓包确认是系统防火墙自动断开了空闲连接。解决方案是开启Socket层的KeepAlive心跳,同时调整系统tcp_keepalive_time参数缩短心跳间隔,彻底解决空闲断连问题。

七、性能与稳定性实测

改造完成后我们在现场做了72小时连续运行测试,并与原有Windows+X86方案做了横向对比,核心指标如下:

测试项 统信UOS + 鲲鹏920 Windows + X86 i5
Modbus TCP单次请求响应(10寄存器) 11ms 13ms
Modbus RTU轮询周期(8台从站) 420ms 450ms
72小时通讯成功率 99.99% 99.98%
稳态内存占用 76MB 89MB
稳态CPU占用率 3.2% 4.7%

从测试结果看,鲲鹏架构下的整体表现甚至略优于原有X86方案,这主要得益于.NET 8对ARM64的深度优化,以及鲲鹏多核的性能优势。连续72小时运行无内存泄漏、无通讯中断,完全满足工业现场的稳定性要求。

八、总结与后续规划

这次国产化迁移项目的落地,验证了C# + .NET在统信UOS+鲲鹏架构上开发工业控制软件的可行性。Modbus作为工业领域最通用的协议,经过适配层的封装改造后可以实现无缝兼容,存量代码复用率超过80%,整体迁移周期不到两个月,远低于完全重写的方案。

工控软件国产化不是简单的代码搬家,核心是要理解不同硬件架构、不同操作系统的底层差异,把传输层、协议层的差异彻底屏蔽,给上层业务提供一致的运行环境。对于大量存量的C#工控软件来说,这是一条成本低、风险可控的自主可控路径。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐