新闻详情

新闻详情

首页 / 资讯中心 / 详情

三菱 PLC MC 协议(SLMP / QnA 兼容 3E 帧)深度解析:从帧结构到 C++/Go 双语言采集实战

发布时间:2026/9/29 20:15:14来源:尧图网络
三菱 PLC MC 协议(SLMP / QnA 兼容 3E 帧)深度解析:从帧结构到 C++/Go 双语言采集实战
适用对象需要与三菱 Q/L/FX5U 等系列 PLC 做以太网直连采集的开发者CNC 设备直连、产线数据采集、边缘网关。 本文以QnA 兼容 3E 帧二进制为主线给出帧级协议说明与 C/Go 双语言完整可编译示例并整理 20 条实战踩坑记录。 协议细节以三菱官方《MC 协议参考手册》Q 系列SH-080356FX5U/iQ-FJY997D56201为准本文表项已与多个现场抓包/工业驱动实现交叉核对。1. 背景与定位1.1 三菱 PLC 通信方式全景三菱 MELSEC 系列 PLC 对外通信主要有三条路线路线介质特点适用编程口协议串口/USB简单但速度慢、一机一主调试、小批量MC 协议MELSEC Communication Protocol串口C24 模块/ 以太网内置网口 / 以太网模块官方标准协议速度快、帧格式公开、跨系列上位机采集、SCADA、网关本文主线开放式网络OPC UA / Modbus / PROFINET 等以太网/现场总线通用标准但部分机型需模块或网关转换多品牌异构集成MC 协议是三菱官方为外部设备上位机访问 PLC CPU 制定的通信协议支持 QnA 兼容 3E/3C/4C 帧、4E 帧以及新一代 SLMPSeamless Message Protocol帧。以太网场景下最常见的是QnA 兼容 3E 帧二进制TCP 承载这也是本文的主线FX5U / iQ-R 等新机型在 QnA 兼容 3E 帧基础上还提供 SLMP 新帧设备头 5 字节见 2.5 节与坑点 #5。1.2 适用场景CNC 设备直连采集三菱 M80/M70、iQ-R 系列 CNC 控制器开放 MC 协议口Q/L/FX5U PLC 的 D 寄存器、M 位元件、特殊寄存器SM/SD实时采集边缘网关PLC 点位 → MC 协议 → 网关 → Kafka/MQTT → 时序库2. 协议全景3E 帧格式详解API 说明2.1 传输模型总览承载TCP默认端口 2000可在 GX Works3 / GX Developer 中配置常见 1025/4999帧形态无帧头、无帧尾、无校验和的裸 TCP 载荷长度字段自带定界请求/响应副头部不同请求帧副头部固定 0x5000响应帧副头部固定 0xD000字节序见 2.4多字节字段一律小端LSB first这是与网络字节序直觉最大的冲突点也是最多人踩的坑见坑点 #12.2 请求帧字段表QnA 兼容 3E 帧二进制字段长度字节说明典型值副头部2请求固定 0x50 0x000x500050 00网络号1目标网络单机通常 000PC 号1PLC 编号通常 255FF请求目标模块 I/O 号2CPU 模块固定 0xFF03小端发送FF 03请求目标模块站号1通常 000请求数据长度2从监视定时器字段开始到请求数据末尾的字节数不含长度字段自身可变监视定时器2单位 250ms0x00104000ms10 00命令2见 2.6 命令表01 040x0401子命令20字单位1位单位00 00软元件头4QnA/ 5SLMP设备编号 3 字节小端 设备代码QnA 1 字节 / SLMP 2 字节见 2.5点数2读/写软元件点数0A 0010数据区可变仅写命令携带字元件每点 2 字节小端位元件每点 1 字节0x00/0x01可变请求数据长度计算示例批量读 D100 起 10 点监视定时器 2 命令 2 子命令 2 软元件头 4 点数 2 12 字节即 0x000C发送字节 0C 00。2.3 响应帧字段表字段长度字节说明副头部2响应固定 0xD0 0x000xD000网络号 / PC 号 / IO 号 / 站号1121与请求一致响应数据长度2从结束代码字段开始到响应数据末尾的字节数 2 数据区长度结束代码20x0000 正常非 0 为 PLC 错误码见 2.9响应数据可变批量读字每点 2 字节小端批量读位每点 1 字节0x00/0x01响应帧读取流程先读固定 9 字节头副头 2网络 1PC 1IO 2站 1长度 2解析长度 L 后再读 L 字节其中前 2 字节是结束代码、其余为数据区。2.4 字节序规则核心中的核心二进制 3E 帧所有多字节字段按LSB first低字节在前发送命令 0x0401 → 字节 01 04长度 0x000C → 0C 00软元件代码 0x00A8 → A8设备编号 0x0064 → 64 00 00点数 0x000A → 0A 00。数据区字数据 16 位小端读回 0x1234 → 数据字节 34 1232 位浮点/整数占 2 个字低字在前64 位占 4 个字。ASCII 帧4E所有字段用 ASCII 十六进制字符表示MSB first字符顺序即十六进制书写顺序且数据区字符每字 4 字符高字节字符在前。两种帧的字段值方向恰好相反混用必然出错坑点 #8。2.5 软元件设备体系与代码表MC 协议按「软元件代码 软元件编号」寻址同一编号 0 可以同时是 D0 / M0 / X0全靠代码区分。QnA 兼容 3E 帧常用软元件代码二进制帧1 字节软元件说明代码十六进制编号进制X输入继电器0x9C视系列Q 十六进制 / FX 八进制见坑点 #11Y输出继电器0x9D同上M内部继电器位0x90十进制S步进继电器位0x98十进制L锁存继电器位0x92十进制F报警器位0x93十进制V边沿继电器位0x94十进制B链接继电器位0xA0十六进制SM特殊继电器位/字0x91十进制D数据寄存器字0xA8十进制W链接寄存器字0xB4十六进制R文件寄存器字0xAF十进制ZR扩展文件寄存器字0xB0十六进制Z变址寄存器字0xCC十进制SD特殊寄存器字0xA9十进制TN定时器当前值字0xC2十进制CN计数器当前值字0xC5十进制TS / TC / CS / CC定时器/计数器触点/线圈位0xC1/0xC0/0xC4/0xC3十进制设备代码因 CPU 系列而异尤其 T/C 定时器计数器、FX 系列现场联调以对应手册与抓包为准。设备头格式两代差异重要帧类型设备头布局长度QnA 兼容 3E 帧Q/L 等设备编号 3 字节小端 设备代码 1 字节4 字节SLMP 新帧FX5U / iQ-R 等设备代码 2 字节小端 设备编号 3 字节小端5 字节现场最常见两种实现都在运行老式 QnA 3E 驱动普遍用 4 字节头pymcprotocol 等新库常用 5 字节头。联调失败时优先核对设备头长度与顺序坑点 #5用 Wireshark 抓包对照官方手册即可快速定位。2.6 命令 / 子命令表命令子命令功能说明0x04010x0000批量读字单位最常用读 D/W/R 等0x04010x0001批量读位单位读 X/Y/M/L 等每点 1 字节0x14010x0000批量写字单位请求携带数据区0x14010x0001批量写位单位请求携带数据区每点 1 字节 0x00/0x010x04030x0000随机读读不连续软元件1~192 点0x14020x0000随机写写不连续软元件0x04060x0000多块批量读一次读多段连续块采集效率最高0x14060x0000多块批量写一次写多段连续块0x0801 / 0x08020x0000监视登录 / 监视执行主动上报式采集部分系列0x0901 / 0x09020x0000远程 RUN / 远程 STOP部分 L 系列改用 0x0B01/0x0B02命令号随系列有差异QnA 兼容 3E 帧与 L/FX 的 SLMP 表不完全一致以对应 CPU 手册为准。批量写命令是 0x1401 而不是 0x0402坑点 #6。2.7 常用报文构造示例批量读 D100 起 10 字请求共 91221 字节50 00 | 00 | FF | FF 03 | 00 | 0C 00 | 10 00 | 01 04 | 00 00 | 64 00 00 A8 | 0A 00 副头部 网络 PC IO 站 长度 定时器 命令 子命令 编号3B代码 点数响应正常D0 00 | 00 | FF | FF 03 | 00 | 16 00 | 00 00 | [20 字节数据每字小端] 副头部 网络 PC IO 站 长度 结束码 D100..D109 共 10 字响应长度 0x0016 结束代码 2 字节 数据 20 字节。批量写 D200 写 1 字 0x010225850 00 | 00 | FF | FF 03 | 00 | 0E 00 | 10 00 | 01 14 | 00 00 | C8 00 00 A8 | 01 00 | 02 01 └─ 数据 0x0102 小端2.8 ASCII 帧4E 帧简要对比所有字段为 ASCII 十六进制字符副头部字符 5000 / D000长度字段表示后续字符个数不是字节数。设备头字符顺序设备代码 2 字符 设备编号 6 字符MSB first与二进制帧顺序相反。数据区每字 4 字符如 3412 表示 0x1234。体积约为二进制的 2 倍带宽紧张/点数多时优先二进制帧。2.9 错误码结束代码速查结束代码含义0x0000正常0xC051请求目标站号错误未连接/站号超范围0xC056 / 0xC057请求数据长度错误0xC05B请求数据错误0xC05D命令 / 子命令错误0xC05E软元件类型错误0xC05F点数超上限0xC060软元件编号错误0xC061软元件范围错误0xC0FF其他错误0xC105PLC CPU 通信异常CPU 停止/正在初始化0xC10FPLC CPU 处于其他状态不能执行如 RUN 状态写禁止0xC120时间限制错误监视定时器超时错误码按系列略有差异完整表以官方手册附录为准。写程序时务必把结束代码打印成十六进制否则 C05F 被当成 49247 很难排查。3. 环境准备与联调工具3.1 PLC 侧设置GX Works3 / GX Developer给 PLC 分配固定 IP如 192.168.1.10子网掩码 255.255.255.0。内置以太网端口 → 通信数据代码二进制Binary帧类型QnA 兼容 3E 帧FX5U 默认也是此帧。打开 MC 协议端口默认 TCP2000记下端口号。简单场景无需设置站号默认网络 0、PC 号 255、IO 号 03FF、站号 0。3.2 PC 侧工具工具用途Wireshark抓 TCP 包逐字节核对帧格式联调必备坑点 #22网络调试助手NetAssist 等手动发帧验证快速二分「协议错」还是「代码错」GX Works3 软元件监视在 PLC 侧核对数据是否真的写进/读出正确地址三菱 MX Component官方组件可用于对照行为但授权与安装较重多数采集场景自己实现更轻4. 详细使用说明实战代码4.1 Go 完整实现主推贴合 Go 采集网关场景package main import ( encoding/binary errors fmt io math net sync time ) // ---------- 软元件代码QnA 兼容 3E 帧1 字节 ---------- const ( DevX 0x9C // 输入继电器 X DevY 0x9D // 输出继电器 Y DevM 0x90 // 内部继电器 M DevS 0x98 // 步进继电器 S DevL 0x92 // 锁存继电器 L DevF 0x93 // 报警器 F DevV 0x94 // 边沿继电器 V DevB 0xA0 // 链接继电器 B DevD 0xA8 // 数据寄存器 D DevW 0xB4 // 链接寄存器 W DevR 0xAF // 文件寄存器 R DevSM 0x91 // 特殊继电器 SM DevSD 0xA9 // 特殊寄存器 SD DevTN 0xC2 // 定时器当前值 TN DevCN 0xC5 // 计数器当前值 CN ) // ---------- 命令 / 子命令 ---------- const ( CmdBatchRead 0x0401 CmdBatchWrite 0x1401 CmdRandomRead 0x0403 CmdRandomWrite 0x1402 SubWord 0x0000 SubBit 0x0001 ) // Client 三菱 MC 协议 QnA 兼容 3E 帧客户端4 字节设备头 type Client struct { addr string conn net.Conn timeout time.Duration timer uint16 // 监视定时器单位 250ms mu sync.Mutex } func NewClient(addr string, timeout time.Duration) *Client { return Client{addr: addr, timeout: timeout, timer: 0x0010} // 4 秒 } func (c *Client) Connect() error { conn, err : net.DialTimeout(tcp, c.addr, c.timeout) if err ! nil { return err } c.conn conn return nil } func (c *Client) Close() error { if c.conn ! nil { return c.conn.Close() } return nil } // buildFrame 构造请求帧设备头编号 3 字节小端 代码 1 字节 func (c *Client) buildFrame(cmd, subcmd uint16, devCode byte, devNo uint32, points uint16, data []byte) []byte { buf : make([]byte, 0, 64) buf append(buf, 0x50, 0x00) // 副头部 0x5000 buf append(buf, 0x00, 0xFF, 0xFF, 0x03, 0x00) // 网络 0 / PC FF / IO 03FF / 站 0 lenPos : len(buf) buf append(buf, 0, 0) // 请求数据长度占位 buf binary.LittleEndian.AppendUint16(buf, c.timer) // 监视定时器 buf binary.LittleEndian.AppendUint16(buf, cmd) // 命令 buf binary.LittleEndian.AppendUint16(buf, subcmd) // 子命令 buf append(buf, byte(devNo), byte(devNo8), byte(devNo16)) // 设备编号 3 字节小端 buf append(buf, devCode) // 设备代码 1 字节 buf binary.LittleEndian.AppendUint16(buf, points) // 点数 buf append(buf, data...) // 写数据读命令为空 l : len(buf) - lenPos - 2 // 回填长度从定时器起 buf[lenPos], buf[lenPos1] byte(l), byte(l8) return buf } func (c *Client) writeAll(p []byte) error { c.conn.SetWriteDeadline(time.Now().Add(c.timeout)) for len(p) 0 { n, err : c.conn.Write(p) if err ! nil { return err } p p[n:] } return nil } // readResponse 读取并解析响应返回结束代码与数据区 func (c *Client) readResponse() (uint16, []byte, error) { c.conn.SetReadDeadline(time.Now().Add(c.timeout)) head : make([]byte, 9) if _, err : io.ReadFull(c.conn, head); err ! nil { return 0, nil, err } if head[0] ! 0xD0 || head[1] ! 0x00 { return 0, nil, errors.New(invalid response header) } length : binary.LittleEndian.Uint16(head[7:9]) // 响应数据长度 结束码2 数据N body : make([]byte, length) if _, err : io.ReadFull(c.conn, body); err ! nil { return 0, nil, err } return binary.LittleEndian.Uint16(body[0:2]), body[2:], nil } // exec 统一执行一帧请求带互斥避免多 goroutine 帧交错 func (c *Client) exec(cmd, subcmd uint16, devCode byte, devNo uint32, points uint16, data []byte) ([]byte, error) { c.mu.Lock() defer c.mu.Unlock() if err : c.writeAll(c.buildFrame(cmd, subcmd, devCode, devNo, points, data)); err ! nil { return nil, err } endCode, resp, err : c.readResponse() if err ! nil { return nil, err } if endCode ! 0 { return nil, fmt.Errorf(PLC error 0x%04X, endCode) } return resp, nil } // ---------- 高层 API ---------- // ReadWords 批量读字元件D/W/R/SD 等 func (c *Client) ReadWords(devCode byte, devNo uint32, points uint16) ([]uint16, error) { resp, err : c.exec(CmdBatchRead, SubWord, devCode, devNo, points, nil) if err ! nil { return nil, err } if len(resp) ! int(points)*2 { return nil, fmt.Errorf(unexpected resp len %d, len(resp)) } out : make([]uint16, points) for i : range out { out[i] binary.LittleEndian.Uint16(resp[i*2:]) } return out, nil } // WriteWords 批量写字元件data 每元素对应一个字 func (c *Client) WriteWords(devCode byte, devNo uint32, vals []uint16) error { data : make([]byte, len(vals)*2) for i, v : range vals { binary.LittleEndian.PutUint16(data[i*2:], v) } _, err : c.exec(CmdBatchWrite, SubWord, devCode, devNo, uint16(len(vals)), data) return err } // ReadBits 批量读位元件X/Y/M/L 等返回每点 bool func (c *Client) ReadBits(devCode byte, devNo uint32, points uint16) ([]bool, error) { resp, err : c.exec(CmdBatchRead, SubBit, devCode, devNo, points, nil) if err ! nil { return nil, err } if len(resp) ! int(points) { return nil, fmt.Errorf(unexpected resp len %d, len(resp)) } out : make([]bool, points) for i, b : range resp { out[i] b ! 0 } return out, nil } // WriteBits 批量写位元件每点 0/1 func (c *Client) WriteBits(devCode byte, devNo uint32, vals []bool) error { data : make([]byte, len(vals)) for i, v : range vals { if v { data[i] 1 } } _, err : c.exec(CmdBatchWrite, SubBit, devCode, devNo, uint16(len(vals)), data) return err } // ReadFloat32 读 32 位浮点占 2 个连续字低字在前IEEE754 小端 func (c *Client) ReadFloat32(devCode byte, devNo uint32) (float32, error) { words, err : c.ReadWords(devCode, devNo, 2) if err ! nil { return 0, err } b : []byte{byte(words[0]), byte(words[0] 8), byte(words[1]), byte(words[1] 8)} return math.Float32frombits(binary.LittleEndian.Uint32(b)), nil } // WriteFloat32 写 32 位浮点 func (c *Client) WriteFloat32(devCode byte, devNo uint32, v float32) error { b : make([]byte, 4) binary.LittleEndian.PutUint32(b, math.Float32bits(v)) vals : []uint16{binary.LittleEndian.Uint16(b[0:2]), binary.LittleEndian.Uint16(b[2:4])} return c.WriteWords(devCode, devNo, vals) }使用示例func main() { c : NewClient(192.168.1.10:2000, 3*time.Second) if err : c.Connect(); err ! nil { panic(err) } defer c.Close() // 读 D100~D109 十个数据寄存器 words, err : c.ReadWords(DevD, 100, 10) if err ! nil { panic(err) } fmt.Println(D100..D109 , words) // 读 M0~M7 八个内部继电器 bits, err : c.ReadBits(DevM, 0, 8) if err ! nil { panic(err) } fmt.Println(M0..M7 , bits) // 写 D200 258D201 0x1234 if err : c.WriteWords(DevD, 200, []uint16{258, 0x1234}); err ! nil { panic(err) } }4.2 C 最小实现WinSock / POSIX 通用思路#include cstdint #include cstring #include iostream #include stdexcept #include vector #ifdef _WIN32 #include winsock2.h #pragma comment(lib, ws2_32.lib) #else #include arpa/inet.h #include sys/socket.h #include unistd.h #endif // 软元件代码 enum DevCode : uint8_t { D 0xA8, M 0x90, X 0x9C, Y 0x9D, SD 0xA9, W 0xB4 }; class MCClient { public: MCClient(const std::string ip, uint16_t port, uint32_t timeoutMs 3000) : ip_(ip), port_(port), timeoutMs_(timeoutMs), sock_(-1) {} ~MCClient() { close(); } void connect() { #ifdef _WIN32 WSADATA wsa; WSAStartup(MAKEWORD(2, 2), wsa); #endif sock_ ::socket(AF_INET, SOCK_STREAM, 0); sockaddr_in addr{}; addr.sin_family AF_INET; addr.sin_port htons(port_); inet_pton(AF_INET, ip_.c_str(), addr.sin_addr); if (::connect(sock_, (sockaddr*)addr, sizeof(addr)) ! 0) { throw std::runtime_error(connect failed); } } void close() { if (sock_ 0) { #ifdef _WIN32 closesocket(sock_); #else ::close(sock_); #endif sock_ -1; } } // 批量读字返回每字 16 位 std::vectoruint16_t readWords(uint8_t devCode, uint32_t devNo, uint16_t points) { std::vectoruint8_t frame buildFrame(0x0401, 0x0000, devCode, devNo, points, {}); sendAll(frame); auto resp readResponse(); if (resp.size() ! points * 2) throw std::runtime_error(resp length mismatch); std::vectoruint16_t out(points); for (size_t i 0; i points; i) { out[i] (uint16_t)resp[i*2] | ((uint16_t)resp[i*21] 8); } return out; } private: std::vectoruint8_t buildFrame(uint16_t cmd, uint16_t subcmd, uint8_t devCode, uint32_t devNo, uint16_t points, const std::vectoruint8_t data) { std::vectoruint8_t f; auto push16 [](uint16_t v) { f.push_back(v 0xFF); f.push_back(v 8); }; // LSB first f.push_back(0x50); f.push_back(0x00); // 副头部 f.insert(f.end(), {0x00, 0xFF, 0xFF, 0x03, 0x00}); // 网络/PC/IO/站 size_t lenPos f.size(); f.push_back(0); f.push_back(0); // 长度占位 push16(0x0010); // 监视定时器 4s push16(cmd); push16(subcmd); f.push_back(devNo 0xFF); f.push_back((devNo 8) 0xFF); f.push_back((devNo 16) 0xFF); f.push_back(devCode); // 设备代码 1 字节 push16(points); f.insert(f.end(), data.begin(), data.end()); uint16_t len (uint16_t)(f.size() - lenPos - 2); // 从定时器起 f[lenPos] len 0xFF; f[lenPos 1] len 8; return f; } void sendAll(const std::vectoruint8_t buf) { size_t sent 0; while (sent buf.size()) { int n ::send(sock_, (const char*)buf.data() sent, (int)(buf.size() - sent), 0); if (n 0) throw std::runtime_error(send failed); sent n; } } std::vectoruint8_t readResponse() { uint8_t head[9]; recvAll(head, 9); if (head[0] ! 0xD0 || head[1] ! 0x00) throw std::runtime_error(bad resp header); uint16_t len (uint16_t)head[7] | ((uint16_t)head[8] 8); std::vectoruint8_t body(len); recvAll(body.data(), len); uint16_t endCode (uint16_t)body[0] | ((uint16_t)body[1] 8); if (endCode ! 0) throw std::runtime_error(PLC error 0x hex(endCode)); return std::vectoruint8_t(body.begin() 2, body.end()); } void recvAll(uint8_t* p, size_t n) { size_t got 0; while (got n) { int r ::recv(sock_, (char*)p got, (int)(n - got), 0); if (r 0) throw std::runtime_error(recv failed); got r; } } static std::string hex(uint16_t v) { char b[8]; snprintf(b, sizeof(b), %04X, v); return b; } std::string ip_; uint16_t port_; uint32_t timeoutMs_; int sock_; }; // 使用示例 // MCClient c(192.168.1.10, 2000); // c.connect(); // auto vals c.readWords(D, 100, 10);4.3 位元件与 32 位数据读写要点位元件批量读子命令 0x0001响应每点 1 字节0x00OFF0x01ON写入同理。32 位整数 / 浮点占 2 个连续字。低地址字存低 16 位小端字节序与字序均为小端Go 侧用 binary.LittleEndian 拼 4 字节即可见 4.1 的 ReadFloat32。64 位占 4 个连续字同样小端拼接。BCD 数据三菱某些元件如部分 FX 系列地址、时钟 D 区按 BCD 编码读取后需自行 BCD→二进制转换(v 0xF0) 4 * 10 (v 0x0F) 等这是容易算错的一类数据坑点 #18。4.4 采集工程实践贴合数采项目连接管理每条链路一个 TCP 连接 一个采集 goroutine用 sync.Mutex 串行化帧收发帧间不允许并发交错见坑点 #20。断线后按指数退避重连1s→2s→4s…上限 30s并复位监视定时器。批量最大化D/W 等连续点位尽量一次读满字上限 960 点把 100 次 1 点请求合并成 1 次 100 点请求吞吐可提升一个数量级不连续点位用 0x0403 随机读一次打包上限约 192 点比逐点批量读更快。点位表驱动把「设备代码起始号点数缩放系数」做成配置文件采集循环按表轮询新增点位不改代码。链路落库采集值 → 打时间戳 → 写 Kafkakafka-go / librdkafka→ 消费端落 TDenginePLC 侧一次批量读的结果作为一条消息体减少消息量与延迟。写操作安全写命令加白名单地址校验 写前读回确认生产环境尽量只读RUN 状态下部分系列写被禁止会返回 0xC10F。5. 常错点 / 坑20 条实战记录帧格式类字节序搞反最高频二进制 3E 帧所有多字节字段是小端命令 0x0401 发 01 04。拿网络字节序大端拼帧PLC 直接回 0xC05D命令错误或 0xC05E软元件类型错误。请求长度字段算错请求数据长度是从监视定时器字段开始算到请求数据末尾不含长度字段本身、也不含前面 7 字节头部。算多了/算少了都会导致 0xC056/C057 长度错误。响应长度则是从结束代码开始算 2 数据。副头部写错方向请求副头部是 0x5000响应才是 0xD000。不少网文示例把请求头也写成 D0 00照抄后永远读不到正常响应。正确的请求帧前 9 字节应为 50 00 00 FF FF 03 00 0C 00读取 10 点时。把 4EASCII和 3E二进制字段混用ASCII 帧字段是字符、MSB first、长度单位是字符数二进制帧是字节、LSB first。PLC 侧通信数据码设置成「二进制」却发 ASCII 帧或反之行为不可预测。用 Wireshark 一眼可辨。设备头格式不匹配QnA 4 字节 vs SLMP 5 字节QnA 兼容 3E 帧设备头是「编号 3 字节 代码 1 字节」4 字节SLMP 新帧是「代码 2 字节 编号 3 字节」5 字节。两者编号和代码的顺序还相反。用错后典型现象读到的值全部错位或报 0xC05E 软元件类型错误。现场用官方手册 抓包对照确认。批量写命令写成 0x0402批量写是 0x1401字/ 0x1401子命令1位0x0402 不是标准批量写命令。很多博客示例写错照抄后 PLC 回 0xC05D。监视定时器设为 0 或过小监视定时器单位 250ms设为 0 时部分系列按默认值约 5s处理但设得比 PLC 实际响应时间还小会提前报 0xC120 时间限制错误。批量写/远程 STOP 等耗时操作建议 4~10s。响应长度解析位置错响应固定头是 9 字节副头 2网络 1PC 1IO 2站 1长度 2很多人按 7 字节读把结束代码当成长度解析数据全错。正确流程读 9 字节 → 解析长度 → 再读 N 字节 → 前 2 字节结束码。寻址类软元件编号进制混用帧中软元件编号按 16 进制编码。D100 → 编号 100 → 0x0064发 64 00 00。有人直接填 100 的十六进制显示习惯或直接写 BCD读出的地址完全不对。X/Y 编号进制随系列变化Q/L 系列 X/Y 用十六进制编号X0X1FFFFX 系列 X/Y 用八进制编号X0X7、X10~X17…。FX 系列要把八进制地址的数字值当十进制转十六进制编码如 X20 八进制 → 编号 20 → 0x14编码错误会读到错误点位或越界。位元件当字读 / 字元件当位读子命令 0x0000字单位、0x0001位单位。M 等位元件用字单位读会得到「16 点打包」的位模式每点16 位与直觉不符D 等字元件用位单位读则直接报错或返回 1 字节/点。点数量级错误批量读字元件每点是2 字节响应长度 点数×2位元件每点 1 字节。按「点数字节数」解析会把数据整体错位。还有位元件批量读上限按「位」计超过上限回 0xC05F。随机读写点数上限记错随机读最多约 192 点、随机写约 64192 点按系列超过上限报 0xC05F。批量读写字元件上限 960 点、位元件上限 35847168 点按帧类型/系列。上限不满足时拆分请求。数据语义类浮点/32 位数据字节序与字序搞错32 位浮点占 2 个字低地址字在前小端4 字节整体也是小端。按「高字在前」或按大端拼字节读出的 float 是天文数字或 NaN。64 位同理。BCD 数据未转换PLC 时钟、部分计数器/地址参数默认 BCD 编码如 0x25 表示 25 而不是 37。采集 D 区时钟类数据不做 BCD→二进制转换数值全部翻车。判断依据PLC 侧数据格式设置与数据实际含义。PLC 停机/RUN 状态与写权限CPU 处于 STOP/初始化时读会回 0xC105 或 0xC10FRUN 状态下部分系列禁止写写失败要区分「协议错」和「状态错」不要盲目重试写操作。读回值与 PLC 监视不一致先确认你读的地址类型D 是字、M 是位与 PLC 侧软元件监视窗口一致再确认采集程序里有没有把位当字、把 16 位当 32 位、漏掉缩放系数工程值 原始值 × 系数 偏移。工程实现类多 goroutine/线程并发收发不互斥TCP 上连续两个请求的响应是按顺序返回的并发写帧会导致响应错配A 请求的响应被 B 解析。必须串行化整帧收发互斥锁包裹 writeread 全过程或每个连接独占一个 goroutine。io.ReadFull 缺失 / recv 半包TCP 是字节流一次 recv 可能只收到半个响应帧。必须循环读满「9 字节头 长度字段声明的 N 字节」用 io.ReadFull/recvAll 而不是单次 Read/recv。连接断开不重连PLC 重启、网线抖动、CPU 模块复位都会断开 TCP。采集程序要监听错误并指数退避重连否则静默丢数。重连后建议重读一次全量点位做基线对齐。不检查结束代码就解析数据结束代码非 0 时响应数据区可能为空或为错误信息直接按正常数据解析会产生「幽灵数据」。务必先判断 endCode ! 0 并返回带错误码的错误。凭感觉调帧不抓包MC 协议字节级细节多肉眼排错效率极低。正确姿势先 Wireshark 抓一帧官方工具GX Works3 监视/网络调试助手发出的正常帧逐字节对照自己代码的帧差异即 bug。端口号写错默认 TCP 2000但很多项目把 PLC 端口改成 1025/4999 或按模块不同。连不上先 ping 测试端口连通性Test-NetConnection / nc。ASCII 帧长度单位误解若用 4E 帧长度字段是后续字符数一个字节十六进制占 2 字符不是字节数差一倍。二进制帧才是字节数。6. 性能与实践建议实践收益连续点位合并批量读一次 ≤960 字吞吐提升 10~100 倍减少 PLC 通信负荷不连续点位用随机读0x0403打包减少请求次数延迟更低连接复用 每链路单 goroutine 串行收发稳定、无帧错配多 PLC 并行每 PLC 一条 TCP站点数线性扩展读值→时间戳→Kafka批量消息→TDengine端到端低延迟落库写操作白名单 写前读回安全生产点位规划建议把高频点位≤100ms与低频点位1s~1min分不同批次轮询避免低频慢点拖累高频链路多块批量读0x0406在点数多且分段时最省。7. FAQ 速查Q1连不上 PLC 常见原因先 ping IP再用 Test-NetConnection ip -Port 2000 测端口确认 GX Works3 里 MC 协议端口已打开、通信数据码与你的帧类型一致二进制 3E ↔ 二进制。Q2能 ping 通但读不到数据大概率帧格式问题字节序、请求长度、设备头格式4/5 字节、软元件代码。抓包对照官方工具帧排查。Q3读 D100 得到 0xFFFF / 0x0000 正常吗可能是未初始化或该地址无数据返回 0也可能是地址或类型错读错元件。用 GX Works3 软元件监视核对同一地址。Q4MC 协议一次最多读多少个点字批量读 ≤960 点位批量读 3584~7168 点按系列/帧类型随机读 ≤192 点。超过需拆分。Q5读浮点数据乱码检查字序32 位浮点占 2 个字、低字在前、整体小端确认 PLC 侧该区域确实是 float 类型而非整数/BCD。Q6写操作返回 C10F 是什么CPU 处于 RUN 且该系列 RUN 状态禁止写或当前站无写权限。改到 STOP 或给对应系列开启 RUN 中写入需 PLC 侧参数支持。Q7必须用三菱官方 MX Component 吗不是。MC 协议帧格式公开自研 TCP 客户端完全可行本文即例。MX Component 适合快速出原型但部署重、跨平台差生产采集网关自研更轻。Q8FX5U 和 Q 系列帧一样吗基本一致都支持 QnA 兼容 3E 帧但设备代码/命令号/点数上限有细微差异部分功能需 SLMP 新帧5 字节设备头。以 FX5U 手册JY997D56201核对。8. 总结MC 协议QnA 兼容 3E 帧是三菱 PLC 以太网直连的事实标准裸 TCP 无校验和 全小端多字节字段 长度自定界。掌握五个要点即可稳定落地帧结构9 字节固定头 请求长度从定时器起/ 响应长度从结束码起字节序二进制帧多字节字段全部 LSB first数据区字/双字同样小端寻址软元件代码 编号16 进制编码X/Y 进制随系列设备头注意 QnA 4 字节 vs SLMP 5 字节命令批量读 0x0401 / 批量写 0x1401 / 随机读 0x0403 / 随机写 0x1402子命令 0字 1位工程串行收发 ReadFull 结束码检查 断线重连 批量最大化 抓包验证。这条链路与既有系列篇目自然衔接Qt SerialPort串口/ libmodbusModbus 设备→ MC 协议三菱 PLC/CNC 直连→ paho MQTT / librdkafka / kafka-go传输缓冲→ TDengine时序落库→ NumPy / Pandas / PyTorch / ONNX Runtime分析与 AI构成完整的工业数采技术栈。9. 参考资料三菱 MELSEC 通信协议参考手册Q 系列SH-080356FX5U/iQ-FJY997D56201三菱 GX Works3 / GX Developer 帮助文档MC 协议设置、软元件监视各 PLC 型号 CPU 编程手册软元件编号规则、点数上限Wireshark 抓包实测联调标准流程
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

蓝绿发布--无停机发布 2026/9/29 20:59:36

蓝绿发布--无停机发布

什么是蓝绿发布在发布的过程中不影响用户的使用,系统不会因发布而暂停对外服务,不会造成用户短暂性无法访问;保障服务一直可以持续使用双环境切换同时维护蓝、绿两套相同环境,平滑切换,安全上线!零停机切换…

阅读更多 →
汽车电子环境可靠性测试实操指南:从标准选型到失效排查 2026/9/29 20:59:36

汽车电子环境可靠性测试实操指南:从标准选型到失效排查

做了快十年汽车电子环境可靠性测试,说实话这行不像软件测试那么热闹,但它对一辆车的安全影响比大多数人想象中要大得多。一个看似不起眼的ECU在零下三十度的极寒地区无法启动,或者车机屏幕在暴晒后出现裂纹,背后往往就是环境可靠性…

阅读更多 →
MCP 意味着工具平权!用 TaoToken 统一 Key 打通 Agent 与 Function Call 2026/9/29 20:59:35

MCP 意味着工具平权!用 TaoToken 统一 Key 打通 Agent 与 Function Call

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
使用Node.js开发服务端接口:TaoToken统一Key接入与config.toml配置骨架 2026/9/29 20:59:35

使用Node.js开发服务端接口:TaoToken统一Key接入与config.toml配置骨架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Model-Optimizer实战:多模型路由与调度优化指南 2026/9/29 20:59:29

Model-Optimizer实战:多模型路由与调度优化指南

说到底,这个项目最初只是我自己的一个“偷懒”工具。手头接了好几个开源模型,API 也买了不止一家,时间一长就发现一个问题:模型越多,反而越不知道该调哪个。同样的请求,有时候大模型效果好但贵得离谱&#…

阅读更多 →
被「演唱会散场后」这类心情戳中的人,把《行李箱的梦想》放进耳机 2026/9/29 20:59:28

被「演唱会散场后」这类心情戳中的人,把《行李箱的梦想》放进耳机

当刚说完再见、还在回味的人走到分别后的电梯、回程的沉默,《行李箱的梦想》往往会比空泛安慰更先开口——不是要你热闹起来,而是把说不清的那截情绪,轻轻按进旋律里。周末不想社交、只想把自己收回来的下午,职场新人,…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉