简介这份C#实例源码包专为工业自动化开发者定制解决欧姆龙PLC通过HostLink协议进行串口通讯的需求适用于上位机开发、设备数据采集、产线监控等场景。源码以读取D存储区为示例清晰展示帧格式组织、FCS校验码计算、串口收发与响应解析等关键环节其他数据区域可仿照改写已确认在CP1E PLC上稳定运行。包内共33个文件、压缩后3.35MB含6个.cs核心代码、完整解决方案与工程文件、3份协议相关PDF文档分别覆盖HostLink协议规范、串行通讯讲义与OMRON应用手册、可执行程序和调试符号以及编译生成的缓存、资源配置文件三类文档配合源码既能帮助新手从零理解通讯原理也能让有经验者快速复用工程框架通过解决方案可一键还原开发环境。当前该资源已有1264人学习下载适合作为项目起步参考。1. 选 HostLink 协议做 C# 串口通讯是欧姆龙 PLC 上位机开发里最直接的一条路工控现场遇到欧姆龙 PLC网口没组好、CX-Programmer 连不上、组态王授权又过期时最保底的通讯方案就是 PLC 侧面那个 DB9 串口加一条直连 RS-232 线。HostLink 是欧姆龙自带的上位机链路协议协议本身不含复杂握手本质是发一帧 ASCII 命令、收一帧 ASCII 响应所以用 C# 的 SerialPort 类就能完整实现不需要任何第三方组件。本文围绕一个可直接落地的 C# HostLink 串口通讯源码实例展开覆盖帧结构、FCS 校验、DM/CIO 区读写、批量采集、扫码枪触发事件整合以及帧日志调试。适合手里有欧姆龙 CP/CJ 系列 PLC、准备自己写上位机数据采集或小规模控制程序的工程师阅读也适合刚接触串口通讯、想理解 HostLink 协议细节的 C# 开发者。2. HostLink 协议帧结构解析与 C 模式下的 FCS 校验实现2.1 一帧 HostLink 报文到底长什么样HostLink 的 C 模式是默认也是最常用的操作模式报文以 ASCII 字符形式在 RS-232 链路上传输。一次完整请求由 5 段组成起始符、两位十六进制单元号、命令代码、参数和数据区、FCS 校验码、终止符*。FCS 之前的字段全是可打印 ASCII 字符所以可以把整个帧当字符串处理。举个例子读取 DM 区 0000 起连续 1 个字请求帧为00RD00000001FCS*其中00表示 PLC 单元号RD是读取 DM 区的命令代码紧随其后的0000是起始地址0001是读取字数FCS 是根据00RD00000001这段内容算出来的两位十六进制校验码。响应帧同样以开头正常读响应是00RD0001FCS*RD之后直接跟数据区一个字的长度是 4 个十六进制字符读取 N 个字就是 4×N 个字符。如果通讯出错响应会变成00RD13FCS*这种形式其中13是错误码。2.1.1 单元号、地址和数据长度的边界条件单元号在 PLC 侧由 DIP 开关或 CX-Programmer 设定PLC 默认就是00除非一台 PLC 挂了多个 Host Link 单元否则不用改。地址是四位十六进制不是十进制DM 区从0000到FFFFCIO 区同理。读写长度用四位十六进制表示单位是字最大FFFF字但一次性读太多会超过串口缓冲区实际工程中单帧读取建议不超过 32 字。2.2 FCS 校验的 C# 实现按字节异或后转大写十六进制FCS 全称 Frame Check Sequence计算方法是把从单元号开始到数据区结束的所有 ASCII 字符逐字节异或异或结果转换成两位大写十六进制字符串拼到报文末尾。这个校验是按字节做位异或和 CRC 没关系实现量很小public static string CalcFcs(string content) { byte fcs 0; byte[] bytes Encoding.ASCII.GetBytes(content); foreach (byte b in bytes) { fcs ^ b; } return fcs.ToString(X2); }比如计算00RD00000001的 FCS代码会把字符0、0、R、D等逐个 XOR最后ToString(X2)得到两位大写十六进制。需要注意计算范围不能带上帧头和帧尾*否则 FCS 永远是错的这是初次做 HostLink 最容易踩的坑。组装一个完整请求帧的封装方法如下public string BuildFrame(string command) { string fcs CalcFcs(command); return ${command}{fcs}*; }调用BuildFrame(00RD00000001)就会输出00RD00000001XX*。上述代码可以直接放进协议层类里和串口收发分离后续换 RS-485 或换命令不用动逻辑。2.3 常用读写命令与内存区对应关系HostLink C 模式命令按内存区划分最常用的是下面这张表命令目标区域读写方向地址单位RRCIO继电器区读字WRCIO继电器区写字RDDM数据存储区读字WDDM数据存储区写字RI输入继电器读字RO输出继电器读字RH保持继电器写字工程里 DI 数据多来自 PLC 程序已经汇总好的内部地址直接读 CIO 或 DM 就够了极少需要按位读 I/O。写入命令要特别注意WD 是整字写入如果只想改某个位要用读-改-写三步而不是构造位指令。最后一句提醒HostLink 没有自动拆包机制响应帧是否完整只能靠*终止符和自己统计预定长度来判定这部分在第三章解析代码里落实。3. 用 C# SerialPort 实现 HostLink 帧的收发与响应解析3.1 串口参数配置与连接管理欧姆龙 PLC 串口出厂常用参数为波特率 9600、7 位数据位、偶校验、2 位停止位这组参数在 C# SerialPort 中要显式设置。很多做 MES 对接的工程师直接套用默认值结果收回来的是乱码。C# 端完整配置如下using System.IO.Ports; SerialPort plcPort new SerialPort { PortName COM3, BaudRate 9600, DataBits 7, Parity Parity.Even, StopBits StopBits.Two, ReadTimeout 1000, WriteTimeout 1000 }; plcPort.Open();DataBits7和Parity.Even要配套出现因为 7 位数据加 1 位校验位正好凑成 8 位。如果 PLC 侧被改成 8-N-1那 C# 也要同步改。建议把参数集中放到appsettings.json或者一个公共配置类里不要散落在各个窗口的代码中否则换一台 PLC 就要动多处。3.1.1 串口打开失败的常见原因Open()抛UnauthorizedAccessException通常是串口被其他软件占用比如 CX-Simulator、组态王或者上一个没释放的调试进程。另外DtrEnable和RtsEnable默认是 false部分欧姆龙 PLC 或 RS-232 转接头需要拉高可以尝试在 Open 之后把两个属性设成 true但不要无脑设有些第三方转换器反而会因为电平冲突导致通讯不稳定。3.2 收发一帧数据的核心方法封装串口通讯要保证发一整帧、收一整帧不能发到一半切换线程。读写命令的完整发送方法如下public string SendReceive(string command) { string frame BuildFrame(command); byte[] sendBytes Encoding.ASCII.GetBytes(frame); plcPort.Write(sendBytes, 0, sendBytes.Length); StringBuilder sb new StringBuilder(); string endMarker *; int timeout 1000; int elapsed 0; while (elapsed timeout) { if (plcPort.BytesToRead 0) { sb.Append(plcPort.ReadExisting()); if (sb.ToString().EndsWith(endMarker)) { return sb.ToString(); } } Thread.Sleep(20); elapsed 20; } throw new TimeoutException(HostLink response timeout); }这个方法的判断逻辑是收到完整*才算一帧结束避免 PLC 响应分多个 TCP/IP 或者 USB 协议包到达时只读到半个帧。BytesToRead轮询加Sleep(20)的方式虽然简单但在现场是稳定可用的因为 HostLink 响应通常在 50ms 内就能收齐20ms 轮询粒度和ReadTimeout配合不会造成明显等待。3.3 响应内容解析与错误码识别拿到完整响应帧后首先要检查帧头、单元号和 FCS然后判断响应是数据还是错误码。读 DM 命令响应结构是00RD数据区FCS*其中数据区长度是可预测的于是解析可以写成public string ReadDm(int address, int wordCount) { string cmd $00RD{address:X4}{wordCount:X4}; string resp SendReceive(cmd); string body resp.Substring(1, resp.Length - 3); string actualFcs resp.Substring(resp.Length - 3, 2); if (CalcFcs(body) ! actualFcs) { throw new Exception(FCS mismatch); } string dataPart resp.Substring(7, wordCount * 4); return dataPart; }resp.Length - 3是去掉*和两位 FCS 后的边界dataPart从00RD之后的第 7 个字符开始取。正常响应的数据区全是十六进制字符如果响应里出现13、14这类短代码说明是错误响应可以通过开头 4 位命令代码和长度比对来区分更保险的做法是判断dataPart是否全部是十六进制字符。建议在解析前先输出原始响应帧到调试窗口这个习惯能省掉一半的联调时间。4. HostLink 批量数据读取与扫码枪触发事件整合实现4.1 多区连续读取的组帧技巧生产数据通常分布在多个地址区间比如挤出机温度在 DM 区 0100 到 010F转速在 DM 0200设备状态在 CIO 0000。逐条发送单字读取命令会带来大量等待正确做法是把地址设计成连续块一个命令读整段。读连续 16 字温度数据的组帧方式int startAddr 0x0100; int count 0x0010; string command $00RD{startAddr:X4}{count:X4}; string resp SendReceive(command); for (int i 0; i count; i) { string word resp.Substring(7 i * 4, 4); short value Convert.ToInt16(word, 16); Console.WriteLine($Address 0x{startAddr i:X4}: {value}); }Convert.ToInt16拿到的是无符号十六进制转出的值如果 PLC 侧是负数要按两字节补码处理也就是当结果大于 0x7FFF 时减去 0x10000。实际项目里温度、压力这些模拟量全是非负数直接用Convert.ToInt16不会出错但计数器的差值可能出现负值所以要区分数据语义。4.2 循环采集与 UI 刷新卡顿问题高频轮询 PLC 时如果直接在 UI 线程里发命令并更新界面界面会卡得没法点按钮这是典型的串口轮询 WinForms/WPF 同步刷新性能瓶颈。正确做法是用后台采集线程持续读数然后用BeginInvoke把更新封送到 UI 线程核心逻辑如下private async Task PollLoop(CancellationToken token) { while (!token.IsCancellationRequested) { string dmValue ReadDm(0x0100, 1); double temp Convert.ToInt16(dmValue, 16) / 10.0; textBox1.Invoke(new Action(() textBox1.Text temp.ToString(F1))); await Task.Delay(200); } }await Task.Delay(200)把轮询频率限制在 5Hz同时让出线程控制权Invoke只做文本更新不做 PLC 通讯这两者分离后 WinForms 界面即使后台有重负载采集也能保持响应。需要注意Task.Delay不是定时器实际间隔会受 PLC 响应时间影响若采集周期要求严格改成Stopwatch循环比较更可靠。4.3 扫码枪触发事件如何插入 HostLink 轮询流程扫码枪一般被模拟成键盘设备在 C# 里通过KeyDown事件或串口读入条码内容。关键不是去读扫码枪而是让扫码结果和 HostLink 查询动作解耦扫码事件只管把条码写到一个Queuestring采集线程处理完当前循环后再取队列里的条码执行数据匹配。private readonly ConcurrentQueuestring barcodeQueue new ConcurrentQueuestring(); private void OnBarcodeReceived(string barcode) { barcodeQueue.Enqueue(barcode); } private void PollLoopWithBarcode(CancellationToken token) { while (!token.IsCancellationRequested) { if (barcodeQueue.TryDequeue(out string barcode)) { string result WriteReadTrackingData(barcode); ProcessResult(result); } // 即使没有扫码也保持对 PLC 状态的常规采集 string status ReadDm(0x0300, 1); UpdateStatusLabel(status); } }扫码枪触发事件如果走键盘 Hook会非常快地连续触发多个KeyDown在事件里直接发 HostLink 命令会导致串口发送队列拥塞正确姿势永远是事件只入队线程负责发帧。从上面可以看到扫码不打断 PLC 周期采集两者在同一个串口线程里有序排队。5. HostLink 串口通讯的原始帧日志调试技巧5.1 打印发送和接收的每一帧数据现场联调时大多数问题来自帧格式错误、FCS 不匹配和串口参数不对肉眼排查十六进制字符效率太低。最好在协议的SendReceive方法里加一段日志输出把发送帧和接收帧连同校验结果写到文件发送帧是访问时拼出来的 ASCII 字符串接收帧是本机收到的原始字符串这两个数据一旦对齐问题定位非常快。File.AppendAllText(hostlink.log, $[{DateTime.Now:HH:mm:ss.fff}] TX: {frame} RX: {resp}{Environment.NewLine});日志要放在SendReceive函数内不能放在窗口事件回调里否则多窗口调用时会漏帧。日志文件建议用AppendAllText或自带的StreamWriter独立线程不要和主采集共用文件句柄。用原始帧日志能直接看出 PLC 是否回了终止符*、FCS 是否因为编码差异导致每次出错这是判断是协议问题还是串口参数问题的最快方法。5.2 不连 PLC 也能验证协议行为的模拟器思路如果手头没有 PLC 硬件可以用一个实现同样串口协议的模拟端来验证 C# 客户端。做法是用虚拟串口工具转出一对 COM 口一个给上位机程序另一个给自定义模拟程序模拟程序收到帧后按 HostLink 规则回响应。模拟进程只需要三件事解析收到的 ASCII 帧、读地址和数量、按格式拼响应并计算 FCS 返回。// 模拟端处理收到的帧 if (command.StartsWith(RD)) { string addr command.Substring(2, 4); string count command.Substring(6, 4); string data GenerateFakeDmData(int.Parse(count, NumberStyles.HexNumber)); string respCmd 00RD data; string fcs CalcFcs(respCmd); port.Write(${respCmd}{fcs}*); }GenerateFakeDmData可以按地址返回固定模式数据比如地址每加一数据加一这样上位机读回来后能肉眼判断地址解析是否正确。模拟器最大的价值不在跑通流程而在验证批量和边界情况比如把数据长度设为 32 字时是否丢帧、响应故意延迟 500ms 时上位机的超时重试是否正常。5.3 从日志帧内容反推通讯故障的三种典型情况日志现象可能原因处理动作TX 有帧但 RX 无任何响应串口参数与 PLC 实际参数不一致或接线 RX/TX 反接核对 PLC 侧串口设置交换 2、3 引脚测试RX 返回错误码13FCS 计算错误或帧被截断打印计算范围内的字符串并手动异或验证RX 返回错误码01或11地址越界或命令代码与内存区不匹配检查地址是否超出 PLC 型号支持的 DM 范围日志中出现01响应时优先检查地址出现11时要看命令是否写错了内存区代码比如想读 DM 却用了RR。这两个错误码最常见的来源是把 PLC 型号的地址范围套错CP 系列和 CJ 系列的 DM 区上限不同查对应型号的操作手册确认范围后再修改初始地址。最后补一条实用细节调试时在日志里同时输出Encoding.ASCII.GetString(sendBytes)因为某些串口防火墙类软件会在传输过程中把终止符*转成换行通过日志能第一时间看出来是物理链路改坏了数据还是程序组帧本身就是坏的。本文还有配套的精品资源点击获取