本节摘要:经验要能积累,代码就得能复用——这一节讲两件让代码"活过单个项目"的手艺:把功能封装成自定义库的完整工艺(接口设计、依赖隔离、示例文档),以及用状态机驯服"标志位加嵌套判断"式的复杂行为。两者结合,你会得到一批越用越厚、拿来就用的代码资产。
4.2 节把单个项目切成了干净的模块;这一节往外再走一步:让模块跨项目存活。它收束第 4 章,也因为它是旧版教程里"库开发规范"与"行为设计"两个话题的合流——分着讲不如合着讲,因为好的库几乎总有一个状态机芯。
封装成库的时机不必早,但要有硬信号:同一段逻辑在第二个项目被复制时还可以容忍,第三遍就该停下来做库。库的工艺并不神秘,一个合格的 Arduino 库由四部分组成:
接口设计有一条黄金判据:先写示例,再写实现。想象半年后的自己拿到这个库,希望怎么用?把理想中的调用代码写成示例,接口就从示例里长出来——而不是从实现里挤出来。凡是示例里需要写注释解释"这行为什么要这么调"的接口,都是需要重新设计的接口。
依赖隔离是第二关:库不许偷懒直接摸全局的 Serial、Wire 或具体引脚号,而是让使用者把依赖注入进来。下面是一个按键库的头文件,注意构造函数的设计:
// Button.h:一个支持单击、长按、双击的按键库接口 #ifndef Button_h #define Button_h #include <Arduino.h> class Button { public: Button(uint8_t pin, uint16_t longPressMs = 600); // 依赖通过参数进来 void begin(); // 初始化引脚 // 事件查询:每轮调用一次,返回发生了什么(0 无 1 单击 2 长按 3 双击) uint8_t read(); private: uint8_t _pin; uint16_t _longPressMs; uint8_t _state; // 状态机当前状态 uint32_t _tMark; // 状态进入时刻 bool _lastRaw; // 上次原始电平 }; #endif
使用者传入引脚号与长按阈值,库内部自己维护状态——调用方完全不感知消抖与时序细节。这就是 4.2 节"状态跟模块走"的库版本:状态私有,接口无状态。read 函数每次返回"发生了什么事件",把"怎么判断长按"的全部复杂度关在实现文件里。
写行为逻辑最容易腐烂的方式是堆标志位:一个 bool 表示"正在长按检测",一个 bool 表示"等待双击窗口",再一个表示"上次是否刚单击"——三个标志位组合出八种局面,半年后没人说得清哪个组合合法。状态机把"局面"收敛成一个变量:系统任何时刻只处于一个命名状态,合法的去处在转移表里写得清清楚楚。
以按键为例,行为天然就是一台状态机:空闲、按下消抖、等待双击、长按确认四个状态,事件(电平变化、超时)驱动转移。用 mermaid 把转移关系画出来,评审与排错都直观:
三段式写法把图翻译成代码:状态变量、事件输入、转移执行,各归各位:
// Button.cpp:read 函数的状态机核心(节选) uint8_t Button::read() { bool raw = (digitalRead(_pin) == LOW); // 低电平为按下 uint8_t event = 0; uint32_t now = millis(); switch (_state) { case 0: // 空闲 if (raw) { _state = 1; _tMark = now; } break; case 1: // 消抖确认 if (!raw) { _state = 0; } else if (now - _tMark >= 30) { _state = 2; _tMark = now; } break; case 2: // 等待双击窗口 if (raw) { event = 3; _state = 0; } // 双击 else if (now - _tMark >= 250) { if (now - _tMark < _longPressMs + 250 - 250) { event = 1; } // 单击 _state = 0; } break; } if (_state == 2 && !raw && now - _tMark >= _longPressMs) { event = 2; _state = 0; } // 长按 return event; }
对照标志位写法自查一遍:任何时刻状态只有一个值,不可能出现"既在长按又在等双击"的矛盾局面;新增一种手势(比如三击)只需加一个状态与两条转移,老逻辑一行不动。状态机的可维护性来自"非法局面不可表达"——这正是标志位方案永远做不到的。
背景:三个项目先后需要按键交互:数据记录仪、云台控制器、温控器。第一版按键代码在记录仪里写成 60 行散装逻辑。
操作:第二个项目开工时发现又要写按键,触发封装条件。按"先写示例再写实现"定接口——示例里只允许出现三行核心调用:构造、begin、循环里 read 判断事件值。实现把散装逻辑重写为状态机。第三个项目直接复用,并且顺手加了一个需求:支持"长按期间周期上报"(温控器要长按连续调温)。
结果:第三个项目的按键部分只花二十分钟接入;增加周期上报只改了状态机一个状态。三个项目共享同一份库,缺陷修复一处生效。
解读:这笔账的收益不只在省下的工时,更在缺陷收敛——三个项目曾经各自为政的消抖参数、双击窗口,现在统一为库的默认值,体验一致、测试一遍全覆盖。库的本质是"把决策固化",让同类问题不再被反复决策。
变式:状态机的思想远不止按键:通信协议解析(等帧头、收长度、收载荷、验校验,就是一台四状态机)、菜单导航、设备工作模式切换,都是同一套手艺。第 6 章的 MQTT 连接管理与本章的状态机会再会一次。
⚠️ 常见坑:库的示例里出现"请根据你的项目修改这几行",说明依赖没收干净。示例必须开箱即跑——凡是需要使用者理解内部结构才能调通的库,都没有封装完成。
💡 关键直觉:库是给半年后健忘的自己、以及接手的同事写的。判断一个库好不好,看新人不看示例文档能不能在十分钟内用起来。