极简博客
简历
联系方式
实习第一阶段总结
发布时间:2026-05-14
## 上位机优化与硬件模拟器:我的实习复盘笔记 这篇不是给别人看的故事,是写给我自己的。三个月,两个项目,踩过的坑比写过的代码多。趁着还没忘,把技术细节、心理活动、犯过的蠢和想通的事都记下来。 --- ### 一、不敢改代码的第一周 刚拿到项目的时候,我其实什么都不敢动。 项目已经是能跑的状态——虽然有小毛病,但整体功能能用。我看着满屏几万行代码,几百个文件,连依赖关系都理不清。哪里能改、哪里不能碰、改了会不会把别的地方搞崩,我心里完全没底。 于是第一个星期,我基本只是看。看代码、看日志、看文档。鼠标在变量名上悬着,就是不敢敲键盘。我知道应该加心跳机制,但不知道加在哪里合适。打开 IDE 想写,脑子里转一圈又关掉了。 后来 mentor 可能看不下去了,找我聊了一次。原话我记不太清,大意是: > 你要大胆地改,改错了回滚就行了。及时做好 commit,怕什么?如果你不敢改代码,尽早别吃这行饭了。 他说这话时语气不重,但我听进去了一句话:**不敢改代码的人,做不了这行。** 他接着教了我一套工作流:不要在主分支上改,先切一个新分支出去;在新分支上改完,提交,push,申请 Pull Request;如果改崩了,直接回滚到上一个 commit,什么损失都没有。分支就是给你试错用的。 我记得他当时还加了一句:“版本控制不是你交作业用的,是你犯错用的。” 这话对我来说很新鲜。在学校用 Git,就是提交作业、备份代码,从没想过它可以作为“犯错的安全网”。我之前的心理模式是“一改错了就完蛋”,mentor 给我换了一个模式:**改错了只是多了一次 commit,回滚就是。** 那天下午,我切了人生中第一个真正意义上的 feature 分支,开始动手改心跳的代码。 改的过程也没那么顺利。第一次提交编译报错,回滚,重来。第二次跑起来了,但心跳间隔设错了,PLC 那边日志刷屏,又回滚。来回三次,才把第一版心跳逻辑写完。 但神奇的是,我不慌了。因为我知道这条分支是我自己的,主分支干干净净。改烂了就删分支重开,没人知道,也不影响别人。 回头看,敢不敢改代码,本质不是技术问题,是心理问题。怕犯错,怕担责,怕暴露自己不懂。mentor 用“分分支 + 频繁 commit + 回滚兜底”这套流程,直接把犯错的成本降到了零。成本没了,心理包袱也就没了。 这件事我写在这里,因为它太基础、太前置了。没有这第一周的心理破冰,后面所有的排查、优化、重构都无从谈起。**敢动手,才有资格谈技术。** --- ### 二、协议的真相,只能从抓包里找 心跳机制需要给三个硬件发命令并接收响应。海康 4K 相机没问题,调 SDK 正常工作。塔石 PLC 和致德程控电源,各有各的麻烦。 先说塔石 PLC。 这个 PLC 同时支持 Modbus RTU 和 Modbus TCP。代码里写的是走 Modbus TCP 协议,但我用第三方工具测试的时候,发现事情不对劲。 我用的工具是 Modbus Poll,先发标准的 Modbus TCP 报文——功能码 03 读保持寄存器,MBAP 头 + PDU,规规矩矩。PLC 没反应。 我换成 Modbus RTU 的格式,地址码 + 功能码 + 数据 + CRC,走串口工具发,也没反应。 两种标准格式都不响应,但程序偶尔能正常工作。这就矛盾了:如果 PLC 严格遵循协议,要么 TCP 通,要么 RTU 通,不可能两个标准都不认、却认我们程序的格式。 问题只能出在“我们程序发的东西”既不是标准 TCP、也不是标准 RTU,而是某种变体。 为了验证,我打开 Wireshark,设了过滤规则只抓发往 PLC 对应 IP 和端口 502 的包。让程序正常运行,抓了几轮通信包。 展开 Payload 的时候我愣了几秒。 外层是标准的 Modbus TCP 帧:事务标识符 00 01、协议标识符 00 00、长度、单元标识符 01。这些都对。 但长度字段后面的 Payload,不是标准的 PDU。里面直接塞了一个完整的 Modbus RTU 帧——地址码、功能码、数据、CRC 校验,原封不动。 也就是说,前任开发者在 Modbus TCP 的壳里,跑了一个完整的 Modbus RTU 帧。RTU 帧里已经带了地址码和 CRC,TCP 的外壳又套了一层自己的地址和长度字段。两层地址,两层校验。 这就像你寄快递,箱子上写了收件地址,打开箱子,里面的东西自己也贴了一张快递单。 我当时的心情很复杂。先是哭笑不得——这人到底在看什么文档?然后是不安——如果这种写法都有,代码里还有多少我没发现的奇怪实现? 技术上能解释为什么偶尔能跑通:PLC 的固件在 502 端口上同时兼容两种协议解析。它收到数据包后,发现 TCP 的 Payload 部分不符合标准 PDU 格式,fallback 到 RTU 解析,碰巧解析成功。但这个“碰巧”依赖于 PLC 固件的容错实现,换一个严格遵循标准的设备,直接丢包。 我后来重构的时候,把 TCP 的 Payload 改成了标准 PDU,去掉了冗余的地址码和 CRC,只用 MBAP 头 + 标准功能码和数据。PLC 正常响应,再也没出现协议层面的偶发异常。 这件事在我脑子里刻下了一条规则:**接手别人代码,不要信注释,不要信变量名,不要信“文档里写了用 Modbus TCP”。直接抓包看实际发出的字节流。** 代码可以说谎,抓包不会。 --- ### 三、“你只是软件层面发出去了” 致德程控电源的偶发故障,是我整个实习里耗时最长、精神内耗最多的一次排查。 现象很明确:流程偶尔失败。查应用日志,日志显示发送命令的语句已经执行,但响应超时,没有收到返回数据。 我第一反应:日志都打印了,函数调用了,`write()` 返回了成功,那肯定是发出去了。问题应该在电源硬件或者通信链路上,不是软件的事。 当时的心态说白了就是自证清白——日志在手,软件没问题,你们查硬件去吧。 老板和我单独聊的时候说了句话: > “你只是软件层面发出去了,不代表电脑硬件真的发出去了。” 我嘴上是应了,心里没完全服。操作系统还能把我 `write()` 系统调用的数据吞了不成?协议栈不就是一层层往下传吗? 但后来我静下来想,确实没法自证。应用层 `write()` 返回成功,只代表数据从用户态拷贝到了内核缓冲区。内核什么时候把数据交给网卡驱动?网卡驱动什么时候把数据打包成以太网帧送出去?物理层信号有没有被网线正常传输?电源那边的网口有没有正常接收?每一层都可能掉链子,而我的日志只覆盖了最上面的应用层。 **日志告诉我的是“我说了”,不是“对方听到了”。** 这两者之间的差距,学校里没人讲过。学校作业里 `printf` 打印出来就等于程序执行了。但工程里,一个通信链路上下七层,你只在自己的用户态里证明自己做了事,离“事情完成”还有很长一段路。 带着这个意识重新排查。老板教我用控制变量法:**每次只改一个东西,观察现象,确定定位再往下走。** 第一步,换网线。拿了一根确定在别的设备上正常使用的网线替换现有网线,问题依然偶发。排除网线物理故障。 第二步,换软件。不用我们自己的上位机,用第三方 Modbus 工具直接给电源发相同的命令——读电压、设电流。连续发一百次,收发正常,没有无响应。说明命令本身没问题,应用层逻辑也是对的。 第三步,压力测试。写了一个小脚本,只做两件事:发读电压命令、发设电流命令。然后分别用单线程串行发和双线程并发发做对比测试。 单线程模式:每 100ms 发一条,收到响应才发下一条。跑了 500 轮,0 失败。 双线程模式:两个线程各自独立发送,时间间隔随机。跑了不到 50 轮就出现大量无响应,比例和三周前用户反馈的失败频率基本一致。 到这里,定位已经很清晰了:电源无法处理并发命令。 后补查了电源的技术手册,里面没写“不支持并发”,只写了“核心处理器为单线程架构”。这句话换个说法就是——同一时间只能处理一件事。两条命令同时过来,它随机丢掉一条。 这件事让我深刻理解了控制变量法的价值。排查最开始,我对问题的归因完全错误——上来就判断“软件没问题”,但实际上我没排除软件发送方式的问题。如果我在第一步就认定“日志都有了肯定是硬件的事”,后面所有的排查都不会发生。 真正客观的排查方法是:**不预设“哪一层是好的”,一层一层验证,一次只动一个变量,用排除法逼近根因。** 找出来是谁的问题不重要,重要的是找出来。 根因找到后,解决方案反倒是明确的:命令必须串行化。我用了消息队列,所有命令入队,单线程消费。收到当前命令的响应后,队列才取下一个命令发送。 单次命令响应时间:大部分 30ms,个别 80ms,平均约 40ms。 电源启动本身需要 1-2 秒的电容充电时间。整个流程有三组灯,每组灯都要走一遍“供电→开灯→拍照→关灯→断电”,总耗时 30-60 秒。 所以加上这 40ms 的串行延迟,对整体流程的影响可以忽略不计。 但我有一个要对自己诚实的地方:我当时改消息队列方案的时候,并没有提前计算延迟影响。不是因为胸有成竹,是因为脑子里只有“让命令别并发”这一件事,没顾上评估性能。事后补了测试数据,验证了方案合理。 这是经验不够的表现。如果是现在,我会在做方案决策前先拿两个数据:当前流程总耗时、消息队列引入的平均延迟。用数据对比来判断方案是否可行,而不是改完再测。 --- ### 四、让数据库活着的双保险 用户在工控机上连续运行软件好几天,反馈偶尔弹出数据库连接失败的报错。 看日志,MySQL 返回的是 `Lost connection to MySQL server during query`。查 MySQL 配置,`wait_timeout` 是 28800 秒,正好 8 小时。 问题很清楚:8 小时不做数据库操作,连接自动断开。用户长期不关软件,超过了这个时间窗口。 我意识到需要保活机制,但具体怎么保活,我当时没概念。 和 AI 讨论后确定了方案:开一个独立线程,每 30 分钟发一次 `SELECT 1`。30 分钟远小于 8 小时,足够保证连接不断。`SELECT 1` 是最轻量的查询,几乎没有负载。 写完给 mentor 看了,mentor 说思路对,然后加了一句: > 再加一个连接池,保持 3 个连接。光靠心跳保一个连接不够,万一那个连接因为别的原因断了呢? 我一开始只想到“让连接不超时”,mentor 想的是“万一有其他原因导致断开怎么办”。两种思路的差别在于对“失效模式”的覆盖范围。我只防了超时断开这一种,mentor 想的是连接可能在任意时刻因为任意原因断开,需要有备用连接随时顶上。 最终方案是两个机制的叠加: - **定时心跳**:每 30 分钟 `SELECT 1`,解决空闲超时断开 - **连接池冗余**:始终保持 3 个活跃连接,一个连接意外断开,立刻切到下一个 这算是我第一次在实际项目中接触“冗余设计”的概念。单点防护的思路是“让这一个东西不出问题”,冗余设计的思路是“假设一个东西出问题,还有另一个顶着”。后者的可靠性不在单点的质量,而在备选的数量。 这个认知后来在硬件模拟器的 DAD 问题里也起了作用——当一种方案走不通,我有意识和习惯去寻找另一种完全不同的路径。 --- ### 五、学会看代码的结构,而不是看代码的量 项目有几万行代码,几百个文件。界面用 QML 写,业务逻辑用 C++,数据访问单独一层,硬件通信封装在协议层里,第三方 SDK(海康相机)又是一个独立目录。 刚拿到的时候打开一看,脑子里就四个字:无从下手。 头一个星期我试图通读代码,从 main 函数开始一路往下追。追到第三天就放弃了——太多文件,太多类之间的继承和依赖,根本理不清。 mentor 看我在那儿死磕,走过来问了一句:**“你现在要解决的问题是什么?”** 我说心跳机制。 他说:**“那就找心跳相关的代码路径,没关系的文件先别看。”** 这句话把我从“阅读全量代码”的负担里拉了出来。我之前的下意识反应是“要把所有代码看懂了才能改”,但这是学校作业的思维——作业只有几百行,能看完。工业项目几万行,等你全看完,用户投诉都堆成山了。 我换了策略:从用户反馈的 Bug 入手。心跳断连——跟踪心跳发送的函数调用——找到协议层构建命令的位置——在命令构建前后加心跳逻辑。这条路径涉及的文件不到十个,其他几百个文件暂时和我无关。 大概半个月后,我开始能自然地在代码里找到对应功能的实现位置。不是全看懂了,是**建立了一个“功能-文件”的心理地图**,知道哪类功能大概在哪个目录、哪几个文件里。大部分代码我至今没读过,也不需要读。 另一个让我印象很深的是分层架构。 项目里发一条命令,不是在一个函数里一条龙写完的。业务层构建命令参数,传给协议层;协议层打包成指定格式的帧(Modbus TCP 或自定义协议),加上帧头帧尾 CRC;传输层负责 Socket 发送和接收;接收回来后反过来拆包——传输层收原始数据、协议层解帧校验、业务层处理语义。 我第一次 debug 通信问题的时候,收到数据但解析报错。因为分层清楚,我直接在协议层的解帧函数里打断点,发现是 CRC 校验失败。如果所有逻辑混在一起,我得从头看到尾才能定位。分层让我几秒就缩小了排查范围。 之前学校写代码,全是平铺直叙,一个函数从头干到尾。出问题就只能一行行打日志。现在我理解每一层存在的意义了:不是为了看起来高级,是为了出问题时能快速隔离。 --- ### 六、协议不只是帧格式,是语义规则 硬件模拟器用 Go 写的。Go 我之前完全没写过,mentor 指定让我学 Go,说他觉得 Go 适合做网络服务。 上手的方式很粗暴:对着 Go by Example 敲了两天基础语法,然后直接开始写调试工具。不懂就查,报错就搜,边写边学。 第一版模拟器的设计思路很朴素,其实就是做一个“应答机器人”。我把上位机会发的所有命令列了一张表,每条命令对应一个固定响应。模拟器收到命令后,用 `switch-case` 匹配命令码,把预存的字节数组原样返回。 我觉得这个设计没问题——命令就那些,响应也固定,比对就行了。 然后心跳命令的响应里包含 CPU 温度。 CPU 温度是一个实时变化的值。48 度、49 度、50 度,随便变一下,我预存的字节数组就对不上了。 这件事听起来很简单,但我当时是在测试的时候盯着日志发现的。模拟器返回的 CPU 温度一直是 46 度,而实际硬件那边已经 51 度了。上位机拿到这个固定值,触发了温度异常的假告警。 我只能把这部分逻辑拆开:心跳响应的其他部分(帧头、设备地址、功能码、版本号)用固定模板,CPU 温度那个偏移位置的四个字节单独拿出来,从系统读取实时温度然后动态填入。 改完心跳之后我突然意识到,同样的问题会出现在几乎所有命令上。 IO 状态查询命令更复杂。它返回两个 ACK 帧,第二个 ACK 里包含 3 路 PWM 的当前值。PWM 值不是固定的,可以通过设置命令修改。用户先调了 PWM 值,再查 IO 状态,模拟器如果还返回初始值,上位机就会判定状态异常。 但最让我头疼的不是 PWM 值本身会变,而是一条查询命令会触发两次回复,第二次回复里还夹着一个需要动态校验的 PWM 字段。 我原本的设计是按命令码一对一匹配响应,一条命令对应一个固定的字节数组。但现在出现了**一条命令触发两条响应**的情况,而且第二条响应的内容还依赖于设备当前状态。 `switch-case` 搞不定了。我需要让模拟器在处理查询命令时,先去读取自己维护的 PWM 状态变量,然后动态构造第一条 ACK 和第二条 ACK。这意味着模拟器不能只是一个“应答机器人”,它必须是一个有状态的设备。 后来我把所有需要动态解析的响应都重构了一遍,抽出一个公共函数,专门负责“根据命令类型和设备当前状态,构造对应的响应字节流”。不再有一个预存的固定字节数组,所有响应都在收到命令的那一刻实时生成。 这个过程让我重新认识了“协议”两个字。 以前我对协议的理解就是帧格式:帧头几个字节、地址几个字节、数据区多长、CRC 怎么算。只要格式对,协议就通。 做完模拟器才意识到,格式只是协议的骨架。协议真正的复杂性在语义层:哪个字段是动态的、哪个是静态的、什么条件下触发什么响应、一条命令对应几条回复、回复之间是否有顺序依赖、某些字段之间是否存在关联约束(比如改了 PWM 值之后,后续查询必须返回新值)。 **你只看帧格式,你只能让数据不丢包。你理解了语义规则,你才能让模拟器的行为和真实硬件一样。** --- ### 七、换了一种方式用 AI 我在两个项目里用 AI 的方式完全不同。 上位机项目的时候,我是一句一句问。每次遇到问题就问 AI,拿到代码贴进去,跑不通再问。改 A 的时候经常不小心动了 B,然后又开始修 B。来回折腾,效率极低。 后来我发现问题不在 AI,在我。我给的指令太零碎、太随意,AI 看不到全局,只能猜我的意图。 硬件模拟器项目开始前,我决定换方式。 我从早上 10 点开始,打开一个空白文档,开始和 AI 对齐需求。 我把协议的每一个字节拆开来讲:第 0 字节是什么、取值范围、是否可变;第 1-2 字节是什么、大端还是小端、和前面字段的关系;第 3 字节的命令码枚举值、每个值对应的响应类型;响应的构造规则、哪些字段从状态变量读取、哪些固定写入。 AI 根据我的描述生成需求描述,我再读,发现不准确的就纠正。来回了不知道多少轮,很多细节我也是在描述的过程中才想清楚的。比如“IO 状态查询命令到底在什么条件下返回第一条 ACK、什么条件下返回第二条”——这个我之前没细想过,写需求的时候才被迫把它理清楚。 到下午 3 点多,文档成型了,将近 2000 字。我存成 README,当成事实来源。 然后把 README 交给工作 AI,让它生成代码框架。十几分钟就出完了。 跑之前我没直接编译。先让同一个 AI 自查代码,检查类型错误、边界条件、遗漏的处理分支。它找出几个问题,改了。然后把代码和 README 一起丢给另一个 AI,让它交叉审查逻辑是否符合需求文档。又找出一个 README 里写明白了但代码漏掉的处理逻辑,补上了。 两轮审查结束,我才编译运行。整个过程没有遇到需要大面积返工的情况。 我复盘这次和上位机项目的区别: 上次我把 AI 当代码生成器,一句一句要。它不知道上下文,我也不知道自己要什么,两个都不清楚的人凑在一起,效率不可能高。 这次我把 AI 当执行器,在让它动手之前,我先花了 5 个小时把自己要什么搞清楚了。README 成了唯一的真相来源。代码对不对,拿 README 对照一目了然。AI 审查也是审代码和 README 的一致性,而不是凭空判断。 核心变化在这里:**我把脑力从“纠正代码”转移到了“定义正确性”。** 纠正错误代码是无限循环——改了一个 bug 可能引入另一个。但定义清楚什么是正确的之后,验证就变成了简单的比对。5 个小时写 README,十几分钟生成代码,两轮审查。总耗时大概 6 小时。 如果是用上位机项目那种老方式做模拟器,我估计至少两三天,而且 bug 更多。 不是 AI 变强了,是我学会了怎么当一个能写清楚需求的甲方。 --- ### 八、当 AI 也帮不了你 模拟器需要给每个虚拟硬件分配独立 IP,这样上位机才能分别给两个设备发心跳——发给 A 只有 A 收,发给 B 只有 B 收,和真实网络环境一致。 我的电脑只有一个物理网卡,配了一个 IP。怎么再搞出两个独立 IP? 所有 AI 给的都是同一个方案:在物理网卡的高级设置里添加辅助 IP。Windows 支持一个网卡绑多个 IP,Go 程序分别绑定到不同 IP 上就行。 我照做了。添加辅助 IP 192.168.1.100,子网掩码 255.255.255.0。点确定。 打开终端,`ipconfig` 一看,状态显示 `Tentative`。 `Tentative` 是 Windows 的 DAD(Duplicate Address Detection,重复地址检测)机制的状态标记。在 Tentative 状态下,IP 还不能被应用程序绑定使用,系统要先发 ARP 探测这个 IP 在局域网里有没有被占用。没人回应,状态才转为 Preferred,地址才可用。 问题是我的环境是两台机器直连,没有路由器,ARP 探测的响应机制可能不正常。Tentative 状态一直不转 Preferred,Go 程序绑定就报错:`address already in use` 或直接绑定失败。 我开始搜解决方案。主流答案是改注册表禁用 DAD:`HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters` 下面加两个 DWORD 值,一个叫 `ArpRetryCount` 设为 0,一个叫 `EnableDeadGWDetect` 之类。重启网卡。 试了。没用。重启电脑,又回到 Tentative。 换个思路,改子网掩码、改用不同网段的 IP、禁用再启用网卡。全试了,Tentative 纹丝不动。 折腾了两天多。AI 开始反复建议我放弃虚拟 IP 路线: > “要不改成监听 0.0.0.0,用一个 Socket 收所有数据,然后在应用层根据数据包里的设备地址字段区分是给哪个设备的。” 技术上可行,但业务逻辑不对。真实环境中两个硬件分别有独立 IP,上位机向各自的 IP 发心跳命令。如果模拟器用单一 Socket 监听所有 IP,两个虚拟硬件就会收到完全相同的数据流。心跳命令是广播语义的——上位机同时给两个设备发相同的心跳帧——如果两个模拟器实例都收到,就无法区分上位机是想单独查询 A 的状态,还是两个都要查。 这不是实现方式的问题,是需求定义的问题。模拟器要模拟的是“两个独立 IP 的独立设备”,不是“一个 IP 上跑两个逻辑实例”。**需求在这个点上不能妥协。** AI 的方案一直在绕,因为它的知识库里没有在我的具体环境下能走通的路。 我放弃了问 AI,直接用浏览器搜真人帖子。关键词来回换:`Windows 11 multiple IP tentative`、`Windows loopback adapter static IP`、`DAD tentative fix not working`。 翻了很多页之后,在一篇讲 Windows 8 网络配置的技术博客里看到一句话:用 Microsoft Loopback Adapter 创建虚拟网卡,在虚拟网卡上配 IP,不经过物理网卡,DAD 行为不同。 我不确定 Windows 11 还有没有这个功能,但这是唯一还没试过的方案。 我把帖子发给 AI,让它帮我整理成 Windows 11 的操作步骤。AI 照做了,但态度很明确——每条指令下面都附了一句“建议还是放弃,改用应用层区分设备的方式”。 我没理。跟着步骤一步步走:打开设备管理器,添加过时硬件,手动选择网络适配器,厂商选 Microsoft,型号选 Microsoft KM-TEST Loopback Adapter。装完,网络连接里多了一个“以太网 2”。 在以太网 2 的 IPv4 设置里配静态 IP 192.168.1.100,子网掩码 255.255.255.0,网关留空。点确定。 终端里 `ipconfig`,以太网 2 的 IP 状态:**Preferred**。 那一刻的心情很复杂。不是兴奋,是一种“终于对了”的放松,混合着一点点对 AI 的重新认识。AI 不是不想帮我,它只是在我的具体环境、具体驱动、具体 Windows 版本、具体网络拓扑的组合下,给不出正确路径。它给的永远是概率最高的解,不是必然正确的解。 **当概率最高的解在具体环境里失效时,能往上顶的,只有人的判断和坚持。** 两个虚拟 IP 都配好之后,Go 程序成功绑定,分别监听各自的 IP 和端口。上位机发心跳到 IP A,只有模拟器 A 收到;发到 IP B,只有 B 收到。和真实硬件行为一致。 这个 DAD 问题从发现到解决,前后折腾了差不多三天。时间不算长,但给我的冲击很大。学校里习惯了“所有问题都有标准答案”,但工程里很多事情是第一次遇到——没有人踩过你的坑,没有人写你的教程,连 AI 都劝你放弃。这时候,你相不相信自己的判断,就成了唯一的分界线。 CCD 数据转换和连续采样的功能最后没有做完,因为硬件模拟器的需求来了,优先级更高,只能把调试工具的开发先搁置。这算一个遗憾。如果后面有时间,我想把这两个功能补完整——不是为了交付,是想给自己一个交代。 --- ### 九、两个月,我变了什么 刚来的时候,我连切分支改代码都不敢,怕改崩了被骂。mentor 一句“不敢改代码就别吃这行饭”算是把我推过了那道心理门槛。Git 分支 + commit + 回滚这套流程,把犯错的成本降到零之后,我才真正开始动手。 动手之后发现,工程和学校最大的区别不是技术难度的差距,是问题归属的边界不同。 学校里的 Bug 一定是你的代码逻辑写错了。工程里的 Bug 可能在网线、在电源的单线程架构、在数据库的 8 小时超时策略、在 Windows 的 DAD 机制、在前任开发者塞进 TCP 壳里的那个多余的 CRC 校验码。问题在哪一层都有可能。你能做的不是猜,是一层层排查,直到找到它。 排查也不是漫无目的地试,要讲方法。老板教的控制变量法——每次只改一个变量,观察结果——已经变成了我的固定工作模式。出问题先列变量:软件、网线、发送方式、硬件固件、操作系统配置。然后一次只动一个,排除一个再动下一个。 上位机优化教会我怎么排查问题,硬件模拟器教会我怎么用 AI 和自己解决问题。上位机项目我是一句一句问 AI,效率低还容易偏。模拟器项目我花了 5 个小时写 README,把“我想要什么”定义清楚,再让 AI 执行,效率翻了几倍。但当所有 AI 都劝我放弃虚拟 IP 的时候,我用浏览器搜到了一篇 Windows 8 的教程,自己判断可行,一步步配通了虚拟网卡。 **AI 给的是概率最高的解,不是必然正确的解。** 当概率最高的解在具体环境里失效,你相不相信自己的判断,就成了唯一的分界线。 还有一件小事,mentor 在某次 code review 的时候跟我说:你的代码风格干净,命名也规范,这个好习惯要保持。我当时愣了下,因为没觉得自己特别在意这个——可能就是以前写作业养成的。mentor 接着说了句:“代码是写给人看的,顺便能跑而已。你能从一开始就注意这个,说明你知道以后看这段代码的人不是你一个人。” 这句话我一直记着。 最后写几条给自己的总结,是这两个月确实长在身上的东西: 1. 接手别人的代码,不要信注释和文档,信抓包工具。Wireshark 不会骗你。 2. 日志打出来了不等于发出去了。应用层的成功和链路层的成功是两件事。 3. 排查问题用控制变量法,每次只动一个变量,不猜,不凭感觉。 4. 敢改代码的前提是掌握分支管理和回滚。犯错成本为零,心理包袱就为零。 5. 和 AI 协作的正确姿势是先把需求想清楚、写清楚,而不是一句一句让它猜。 6. AI 给的是主流解,你的具体环境可能需要非主流解。AI 说放弃的时候,你自己判断。 7. 代码是给人看的。命名规范、结构清楚,不是给别人行方便,是给三个月后的自己行方便。
← 返回博客列表