简介:面向需要在桌面端与西门子S7系列PLC通信的Qt开发者,这份压缩包给出Snap7库接入Qt工程的参考实现,可用于工业自动化上位机、设备状态监控与远程读写等场景,适合具备C++与Qt基础、希望快速打通PLC通信链路的初中级开发者。包内共8个文件,以h/cpp源码和ui界面文件为主体,并附dll、lib动态/静态库及一份库添加说明,压缩包约123KB,体积轻便,便于直接放入现有工程对照学习。目前已有4842人学习下载,说明其在Qt与PLC通信主题下具有一定参考热度。通过包中的界面工程与Snap7调用示例,读者可重点理解连接参数设置、I/O与变量区域读写、界面展示与通信逻辑分离等关键环节,并结合库添加说明梳理依赖配置和常见异常处理思路,为后续扩展监控功能、优化通信性能提供可复用的代码骨架。
1. 上位机界面八个数据全停在零:这套 Qt + snap7 通信方案要解决什么
现场反馈"数据不动",第一反应是网线松了,换交换机、改 IP、重启设备折腾两天,最后发现问题只是读 DB 块时起始偏移写错了 2 个字节。用 Qt 配合 snap7 跟 S7 系列 PLC 做通信,协议本身不难,难的是把连接参数、寻址偏移、字节序、线程边界和异常恢复这几件事一次性做对。这套方案面向需要在工控机上跑一个长期稳定运行的采集与下发上位机的人:它把通信驱动层从界面里剥离出来,用点位表配置替代硬编码地址,用独立线程承载全部网络 IO,并给出断线重连、字节解析、写值回读和验收测试的完整做法。读完你可以照着搭一套能在现场连续跑三天不掉线的采集程序。
2. 总体架构:三层结构、四个模块与一条可复现的数据流
2.1 为什么选 snap7,而不是 OPC UA 或厂商自带 DLL
技术选型这一步经常被跳过,直接找一个能连上的库就开工,等到要换操作系统或者交付到另一台机器上才发现麻烦。snap7 是一个开源的 S7 协议实现,提供原生 C++ 接口,一个头文件加一个静态库就能编进 Qt 工程,Windows 和 Linux 下都能用,不依赖额外的运行时环境。它走的是 ISO-on-TCP,默认端口 102,属于单边客户端通信——上位机主动去读 PLC 的内存区,PLC 侧不需要写任何通信程序块,这对改造类项目非常友好。
对比另外两条常见路线,差别在交付成本上。OPC UA 更"正规",但需要在 PLC 侧开启服务、配置证书、部分型号还要额外授权,一个课程设计或小改造项目往往卡在环境准备阶段就耗掉大半时间。厂商自带 DLL 封装度高,调用简单,但基本绑定 Windows 平台,版本升级后接口可能变,跨平台部署直接翻车。snap7 的短板也很明确:它只做协议搬运,不做数据建模,点位含义、数据类型、单位全得自己在业务层定义;另外它是单边读写,PLC 侧必须允许 PUT/GET 访问,DB 块必须关掉"优化的块访问",这两条是后面避坑章节的重点。
提示:选型时把"PLC 侧需要做什么配置"列成一张清单,和项目经理或现场工程师提前确认。很多"连不上"不是代码问题,是 PLC 侧那两三个勾没打。
Qt 这边承担的职责是界面、定时调度、数据缓存和图形绘制,snap7 只负责把字节搬进搬出。两者之间隔一层自己写的封装,好处是将来换库(比如换成某个国产 PLC 的驱动)时只改驱动层,业务和界面不动。
2.2 三层职责划分:UI 层、业务层、通信驱动层
结构上分三层,自下而上是通信驱动层、业务层、UI 层,依赖方向单向向上,下层不知道上层的存在。
| 层级 | 核心职责 | 典型类 | 允许依赖 |
|---|---|---|---|
| UI 层 | 数值展示、趋势曲线、报警列表、参数配置对话框 | MainWindow、TrendChart、ConfigDialog | 业务层 |
| 业务层 | 点位表管理、采集调度、缓存、报警判定、写值队列 | PointTable、PollScheduler、DataCache、AlarmEngine | 通信驱动层 |
| 通信驱动层 | 连接管理、DB 区读写、字节解析、重连状态机 | CommWorker、PlcClient、Codec | snap7 |
驱动层只暴露三个语义化接口:连接、读一个连续区间、写一个字段。业务层拿到的是QByteArray加一份点位描述,由 Codec 负责把字节翻译成double、bool、QString。UI 层永远不直接持有TS7Client指针,这条纪律守住了,后面加线程、加重连、加日志都不会牵一发动全身。
四个模块是通信驱动、点位配置、采集调度、数据展示。这里刻意没有做成"每个功能一个类"的细粒度划分,因为工控上位机的功能边界本来就不清晰,过度拆分会让信号槽连成一团。按"谁负责跟外界打交道、谁负责保存配置、谁负责时间节奏、谁负责画"来切,改起来最省事。
2.3 线程模型与数据流:一条从 DB 块到曲线控件的路径
界面卡死是这个项目里最容易被投诉的问题,根源几乎都是把DBRead放在了 UI 线程。snap7 的读写是同步阻塞的,网络抖动一次就可能阻塞几百毫秒到几秒,主线程一停,窗口就白了。
正确做法是把通信对象移进独立线程,用moveToThread而不是继承QThread重写run。前者能继续用信号槽,后者只能在run里写死循环,事件循环没了,定时器和跨线程调用都得自己造。
// 主线程中装配,注意 parent 必须为空,否则 moveToThread 会失败 CommWorker *worker = new CommWorker(); // 无父对象 QThread *commThread = new QThread(this); worker->moveToThread(commThread); // 跨线程连接,参数用 Qt 元类型或已注册的自定义类型 connect(commThread, &QThread::started, worker, &CommWorker::start); connect(worker, &CommWorker::blockReady, this, &MainWindow::onBlockReady, Qt::QueuedConnection); connect(worker, &CommWorker::stateChanged, this, &MainWindow::onStateChanged, Qt::QueuedConnection); commThread->start();blockReady每次抛出一个完整的 DB 块字节数组和一个时间戳,业务层解析后写入缓存,再通过另一个信号通知曲线控件重绘。整条路径是:驱动线程读字节 → 队列连接投递到主线程 → 业务层解码 → 缓存追加 → 曲线控件按需取点绘制。中间不共享可变状态,只有不可变的QByteArray和值类型在传,天然避开锁。
参数上有一点要注意:队列连接的参数必须能被 Qt 的元对象系统复制。QByteArray、QString、QDateTime都支持,自定义结构体需要Q_DECLARE_METATYPE加qRegisterMetaType,否则运行时会在控制台刷一行警告然后信号静默丢失,这种问题排查起来非常玄学。
3. 通信驱动层落地:连接参数、DB 块读写与字节序解析
3.1 连接建立:IP、机架、槽位与连接类型的正确填法
连接失败有一半出在参数上。ConnectTo需要三个参数:IP、机架号 rack、槽位 slot。rack 基本都是 0;slot 跟 CPU 系列有关,S7-1200 和 S7-1500 通常是 1,S7-300 常见 2,S7-400 要看机架配置。填错了不会报"参数错误",只会连接超时,非常容易误判成网络问题。
bool CommWorker::connectPlc(const PlcConfig &cfg) { if (client_ == nullptr) { client_ = new TS7Client(); } // 1=PG(编程器) 2=OP(操作面板) 3=S7Basic(单边客户端),采集程序用 3 client_->SetConnectionType(3); // 连接超时与收发超时,单位毫秒,现场网络差时可放宽到 3000/3000 client_->SetConnectionParams(cfg.ip.toUtf8().constData(), 0x0100, 0x0100); int rc = client_->ConnectTo(cfg.ip.toUtf8().constData(), cfg.rack, cfg.slot); if (rc != 0) { // CliErrorText 把错误码翻成可读文本,务必写进日志 lastError_ = QString::fromUtf8(CliErrorText(rc)); emit stateChanged(Offline, lastError_); return false; } emit stateChanged(Online, QStringLiteral("connected")); return true; }SetConnectionType(3)表示以普通客户端身份连,占用的连接资源比 PG/OP 少,PLC 侧允许的连接数有限,这一点在多台上位机同时采集时要算清楚。SetConnectionParams里的两个0x0100是发送和接收超时,单位是毫秒的十六进制表示,不设置就用默认值。失败时一定要把CliErrorText(rc)落日志,只记一个整数错误码,事后排查等于没有线索。
3.2 DB 区批量读写封装与超时控制
性能上的血泪经验:不要一个点位读一次。64 个点位逐点调用DBRead,200ms 周期下每秒 320 次请求,PLC 的通信资源会被打满,表现出来就是连接时断时续、界面数据跳变。
正确做法是按 DB 块归并。同一 DB 块内,把点位按偏移排序,取最小偏移到最大偏移加类型长度的连续区间,一次读回来。中间夹杂的无关字节也一起读,浪费几个字节换来请求数下降一个数量级,非常划算。
bool CommWorker::readDbBlock(int dbNumber, int start, int size, QByteArray &out) { out.resize(size); int rc = client_->DBRead(dbNumber, start, size, out.data()); if (rc != 0) { continuousFail_++; lastError_ = QString::fromUtf8(CliErrorText(rc)); return false; } continuousFail_ = 0; lastOkMs_ = QDateTime::currentMSecsSinceEpoch(); return true; }DBRead的size参数单次不宜超过 512 字节,部分型号对单次 PDU 长度有限制,超了会返回错误码。一个 DB 块超过 512 字节就拆成两段读,两次请求间隔几毫秒,不要背靠背连发。continuousFail_是后面状态机的输入:连续失败到阈值就判定离线,成功一次就清零,不要用"最近一次是否成功"判断,否则网络抖动会让状态频繁翻转。
3.3 字节序与数据类型解析:Real、DInt、Bool、String 的踩坑点
S7 协议在网络传输中是大端序,而 x86 机器是小端,直接把 4 个字节memcpy到float里会得到一个天文数字。这是新手最常翻的车,也是最容易查出原因的一个——因为错得太明显。
// 大端 4 字节转 quint32 static inline quint32 beU32(const char *p) { return (quint32(quint8(p[0])) << 24) | (quint32(quint8(p[1])) << 16) | (quint32(quint8(p[2])) << 8) | quint32(quint8(p[3])); } // Real:先按大端拼成整数,再按位复制成 float static float toReal(const QByteArray &buf, int offset) { quint32 raw = beU32(buf.constData() + offset); float f = 0.0f; memcpy(&f, &raw, sizeof(f)); return f; } // DInt:32 位有符号整数 static qint32 toDInt(const QByteArray &buf, int offset) { return static_cast<qint32>(beU32(buf.constData() + offset)); } // Bool:按字节偏移 + 位号取,bit 取值 0~7 static bool toBool(const QByteArray &buf, int offset, int bit) { return (quint8(buf.at(offset)) >> bit) & 0x01; } // S7 String:第 0 字节是最大长度,第 1 字节是实际长度,从第 2 字节起是字符 static QString toS7String(const QByteArray &buf, int offset) { int maxLen = quint8(buf.at(offset)); int actLen = quint8(buf.at(offset + 1)); if (actLen > maxLen) actLen = maxLen; // 防御异常数据 return QString::fromLatin1(buf.constData() + offset + 2, actLen); }offset是相对本次读取起始地址的偏移,不是 DB 内的绝对地址。假设从 DB100.DBB4 开始读 64 字节,点位在 DB100.DBD8,那传进来的 offset 是 4。这个换算层级一定要在业务层做掉,驱动层只认相对偏移,否则改一次读取起点就要改一堆点位配置。
Bool 的位号从 0 开始,和 TIA 里看到的DBX8.0到DBX8.7一一对应。写的时候如果只改一个位,要先读出整字节、改位、再整字节写回,不能只写 1 个字节的某一位,协议没有位写接口。
3.4 心跳保活与断线重连的状态机
长连接不是连上就一劳永逸。交换机端口老化、PLC 重启、网线被叉车压一下,都会让连接进入"假在线"状态——Connected()还返回 true,但读请求全部超时。所以判定在线不能只看连接标志,要看最近一次成功读的时间戳。
状态机四个态:Idle(未启动)、Connecting(正在连)、Online(最近 3 个周期内有成功读)、Offline(连续失败超过 5 次或时长超过 3 秒)。Online和Offline之间切换时发信号给 UI,界面上的指示灯和数据区做相应处理,不要让用户看着停在零的数据猜是设备停了还是程序挂了。
重连用退避策略:第 1 次等 1 秒,第 2 次 2 秒,之后 4、8、16 秒,上限 30 秒。固定 500ms 死循环重连会把 PLC 的连接资源占满,尤其是 PLC 侧只允许 8 个连接的时候,多台上位机一起重连可能谁都连不上。
void CommWorker::onTick() { if (state_ == Offline) { qint64 now = QDateTime::currentMSecsSinceEpoch(); if (now - lastTryMs_ < backoffMs_) return; // 未到重试时间 lastTryMs_ = now; if (connectPlc(cfg_)) { backoffMs_ = 1000; // 连上就重置退避 state_ = Online; } else { backoffMs_ = qMin(backoffMs_ * 2, 30000); } return; } // 在线状态正常采集,失败计数由 readDbBlock 维护 if (continuousFail_ > 5) { state_ = Offline; client_->Disconnect(); emit stateChanged(Offline, lastError_); } }退避上限设 30 秒是经验值:现场运维处理一次故障通常以分钟计,30 秒的探测间隔既能及时恢复,又不会在长时间断开期间制造无谓流量。
4. 业务层实现:点位表、采集调度、缓存曲线与配置持久化
4.1 点位表设计:一份 JSON 配置如何描述全部采集点
地址硬编码在代码里,换一台设备就要重新编译,这是课程设计最常见、工程上最不能接受的做法。点位表的做法是把"哪个 DB 块、哪个偏移、什么类型、叫什么名字、什么单位、什么量程"全部外置成配置文件,程序启动时加载。
{ "plc": { "ip": "192.168.0.10", "rack": 0, "slot": 1, "pollMs": 200 }, "blocks": [ { "db": 100, "start": 0, "size": 64, "points": [ { "name": "主轴转速", "type": "Real", "offset": 0, "unit": "rpm", "scale": 1.0 }, { "name": "进给速度", "type": "Real", "offset": 4, "unit": "mm/min" }, { "name": "运行状态", "type": "Bool", "offset": 8, "bit": 0 }, { "name": "报警字", "type": "Word", "offset": 10 }, { "name": "工件编号", "type": "String","offset": 12 } ] }, { "db": 101, "start": 0, "size": 32, "points": [ { "name": "目标温度", "type": "Real", "offset": 0, "writable": true, "min": 0, "max": 300 } ] } ] }字段含义:db是数据块号,start和size定义本次批量读取的连续区间,offset是相对start的偏移,bit只对 Bool 有效,writable标记是否允许下发,min/max是写值前的范围校验。加载时用QJsonDocument解析,解析失败要给出具体行号和字段名,不要只弹一个"配置错误"。
这套结构的好处是可扩展:将来加一个 DB 块只是往数组里追加一项,加一个点位只是往points里加一行,界面上的表格控件直接按点位表生成,不需要为每个点位写一个控件。
4.2 采集调度:定时器、批量读与错峰策略
采集周期用QTimer起在驱动线程里,但QTimer的精度受事件循环影响,实际间隔会在设定值上下浮动。如果需要严格等间隔的时间戳(趋势曲线要按时间轴画),用QElapsedTimer记录起点,每次读取完成后计算下一次的补偿睡眠。
void CommWorker::start() { timer_ = new QTimer(this); timer_->setInterval(cfg_.pollMs); // 例如 200ms connect(timer_, &QTimer::timeout, this, &CommWorker::onTick); timer_->start(); } void CommWorker::pollOnce() { for (const BlockCfg &blk : cfg_.blocks) { QByteArray buf; if (!readDbBlock(blk.db, blk.start, blk.size, buf)) { continue; // 失败不阻塞下一个块 } emit blockReady(blk.db, buf, QDateTime::currentDateTime()); QThread::msleep(5); // 块之间留几毫秒间隔 } }多个 DB 块之间加 5 到 10 毫秒间隔,避免瞬时打出一串请求。周期设定上,200ms 适合人机界面展示,50ms 以下对普通 S7 通信意义不大,反而增加 PLC 负载。真正需要高速采样的场合应该用 PLC 侧的中断或高速计数器,而不是靠上位机拉高轮询频率。
4.3 缓存与趋势:环形缓冲与降采样
趋势曲线最多同时显示 8 条线、每条保留 3600 个样本,用QVector加写指针做环形缓冲,避免频繁removeFirst造成的内存搬移。
struct RingBuffer { QVector<double> value; QVector<qint64> stamp; int head = 0, count = 0, cap = 0; void init(int n) { value.resize(n); stamp.resize(n); cap = n; head = 0; count = 0; } void push(double v, qint64 ms) { value[head] = v; stamp[head] = ms; head = (head + 1) % cap; if (count < cap) ++count; } // 按时间窗口取点,用于绘制 int snapshot(QVector<double> &out, qint64 fromMs) const { out.clear(); int start = (head - count + cap) % cap; for (int i = 0; i < count; ++i) { int idx = (start + i) % cap; if (stamp[idx] >= fromMs) { out.append(value[idx]); } } return out.size(); } };绘制时如果样本数远大于控件像素宽度,做降采样:把窗口内样本按像素列分组,每组取最大值和最小值两个点画竖线。这样曲线形状不失真,绘制耗时也从几千个点降到几百个点。不做降采样直接drawPolyline几千个点,界面在低配工控机上会明显掉帧。
4.4 写值下发与配置日志落盘
写 PLC 比读危险得多,写错一个字可能让设备动作异常。三条规矩:写前校验范围、写后回读确认、写操作单独排队。
bool CommWorker::writeReal(int db, int start, float value) { quint32 raw = 0; memcpy(&raw, &value, sizeof(raw)); // 转成大端字节序 char buf[4] = { char(raw >> 24), char(raw >> 16), char(raw >> 8), char(raw) }; if (client_->DBWrite(db, start, 4, buf) != 0) { return false; } // 回读确认,避免"写成功但被 PLC 程序立刻覆盖" QByteArray echo; if (!readDbBlock(db, start, 4, echo)) return false; return qFuzzyCompare(toReal(echo, 0) + 1.0f, value + 1.0f); }qFuzzyCompare直接比较浮点数会因为精度问题误判,加 1.0 是规避接近零的比较陷阱。回读失败不代表写入失败,可能是 PLC 逻辑在这个扫描周期内改写了该地址,这种情况要在日志里区分记录,不要一律报"通信失败"。
日志按天滚动,文件名带日期,单文件超过 10MB 就切分,保留 30 天。日志内容至少包含时间戳、操作类型、DB 号与偏移、结果。配置文件的每次保存都先写临时文件再原子替换,避免掉电时配置被写坏。
5. 避坑与排查:现场最容易翻车的五个点
5.1 能 ping 通但连不上 PLC
现象:命令行 ping PLC 的 IP 正常,程序里ConnectTo一直返回超时或者错误码,换一台电脑又能连上。原因通常不在网络,而在 PLC 侧的访问配置。S7-1200/1500 需要在硬件配置里勾选"允许来自远程伙伴的 PUT/GET 通信访问",不勾选时 TCP 能建立但 S7 层的连接请求会被拒。另外 rack/slot 填错、连接类型填成 1(PG)导致连接资源不足、以及 102 端口被防火墙或其他上位机软件占用,都会表现成同一个现象。
解决:按顺序排查——先用抓包工具确认 TCP 三次握手是否完成,握手成功说明网络和端口没问题,问题在 S7 层;核对 rack/slot;确认 PUT/GET 已勾选;确认同时连接的上位机数量没超过 PLC 允许的连接数。把这四步写成一张排查表贴在项目文档里,下次现场直接照着走。
5.2 读回来的 Real 是天文数字或者明显不合理
现象:一个本应是 25.5 的温度值,显示成4.6e-41或者-1.7e+38这种数。原因基本是两个:没有做大端转换,或者偏移算错。前者是字节序问题,后者常见于 DB 块起始偏移不是从 0 开始的情况——配置里写start: 4,点位偏移却按绝对地址填了offset: 4,实际读到的是 DB100.DBB8。
解决:先写一个最小验证程序,只读一个已知值的 Real 点位,把原始字节打印成十六进制,手工对照。25.5 的 IEEE754 大端表示是41 CC 00 00,如果打印出来是00 00 CC 41,就是没转字节序;如果是别的值,就是偏移错了。这个验证程序不要省,五分钟能省掉半天。
注意:DB 块必须关闭"优化的块访问",否则块内变量没有固定的绝对偏移,snap7 按偏移读到的内容跟你在变量表里看到的对不上。这是 S7-1200/1500 特有的坑。
5.3 界面卡死几十秒后恢复,或者直接无响应
现象:点击"连接"按钮后窗口白屏,几秒到几十秒后恢复,任务管理器显示未响应。原因是在 UI 线程里调用了同步阻塞的ConnectTo或DBRead。网络不通时ConnectTo会一直等到超时,默认超时可能长达几十秒,主线程全程被占住,重绘和事件处理全部停摆。
解决:所有 snap7 调用只能在驱动线程里执行,UI 层通过信号槽发指令。连接按钮点击后立刻把按钮置灰、显示"连接中",等驱动线程回stateChanged再恢复。这条纪律一旦破例,后面会以各种形式反复出现——比如在析构函数里调Disconnect阻塞关闭,或者在属性对话框里直接读一个点位做校验。
5.4 跑几个小时或几天后连接静默断开
现象:程序刚启动一切正常,运行几小时后数据不再更新,但界面上的状态灯还是绿的,重启程序立刻恢复。原因是"在线"判定只看了Connected()标志,而实际连接已经被中间设备或 PLC 侧回收。PLC 对空闲连接有超时回收机制,某些交换机在长时间无流量后会清掉会话表项,表现就是连接对象还在,但数据包发不出去。
解决:用"最近成功读时间"判定在线,超过 3 个采集周期没有成功读就主动Disconnect并进入重连流程。同时在采集循环里维护连续失败计数,超过阈值立即判定离线。这套机制上线后,同样的现场环境从"每天手动重启一次"变成连续运行一周无人工干预。
5.5 写值写不进去,或者写进去立刻被改回去
现象:调用DBWrite返回 0(成功),但 PLC 里监视到的值没有变化,或者变化一瞬间又回到原值。原因有两类:一是该地址在 PLC 程序里每个扫描周期都被赋值,上位机写的值下一个周期就被覆盖,这种情况不是通信问题,要跟做 PLC 程序的人确认地址用途;二是写入的数据长度或类型不对,比如往 String 里写的时候没有按"最大长度 + 实际长度 + 字符"的格式构造,写进去的内容被解析成乱码。
解决:先做写后回读,回读值和写入值不一致就判定为"被覆盖",在日志里明确区分"写入失败"和"写入被覆盖",这两种问题的处理人完全不同。String 写入要严格按 S7 String 格式构造缓冲区,第一个字节填最大长度,第二个字节填实际长度,后面跟字符,长度不够补零。
6. 验收与答辩:怎么用一张测试表证明通信链路真的通了
代码能跑起来不等于方案能交付。验收要拿证据说话,把下面这张表填满,基本就能应对任何追问。
| 验收项 | 测试方法 | 通过标准 |
|---|---|---|
| 连接建立 | 冷启动程序,观察状态灯 | 3 秒内进入在线状态,日志有连接成功记录 |
| 数据准确性 | 选 3 个不同类型点位(Real/Bool/String),在 PLC 侧强制改值 | 界面上 1 个采集周期内同步变化,数值与 PLC 监视一致 |
| 断线恢复 | 拔掉网线 30 秒后插回 | 30 秒内自动重连成功,数据恢复刷新,无需人工干预 |
| 长时间稳定性 | 连续运行 72 小时,期间正常操作界面 | 无崩溃、无内存持续增长、数据不中断超过 10 秒 |
| 写值下发 | 通过界面向可写点位下发一个合法值和一个越界值 | 合法值回读一致,越界值被拦截并提示 |
| 异常配置 | 故意把 slot 改成 9、把 DB 号改成不存在的值 | 程序不崩溃,给出明确错误提示,日志有记录 |
答辩演示建议分三段走,控制在 8 分钟内。第一段讲结构:打开工程目录,指出驱动层、业务层、界面层的文件分布,说明为什么把 snap7 关在独立线程里,这一段回答的是"你懂不懂自己在写什么"。第二段跑正常流程:连接、读数据、改一个 PLC 侧的值看界面跟着变、在趋势曲线上看到变化,这一段证明东西是活的。第三段演异常:现场拔网线,让评委看到状态灯变红、日志出现重连记录、插回后自动恢复,这一段最能加分,因为它证明你考虑过真实场景。
如果时间允许,再补一个关于字节序的小演示:在界面上加一个"原始字节"调试面板,点开某个 Real 点位,显示41 CC 00 00和解析后的25.5,讲一句"大端转小端就是在这里做的"。这个细节小,但评委一听就知道你是真读过协议,不是抄了个能跑的 demo。
从那以后我每次接通信类项目,都会在开工第一天先写一个只读单点、只打印十六进制的最小验证程序,跑通了再往上堆业务。这个习惯救过我不止一次——它能让你在半小时内分清问题出在网络、配置还是解析,而不是在一个几万行的工程里靠猜。希望帮到你。
本文还有配套的精品资源,点击获取