HAL 分层重构执行需求书(V1.1 档案)
这是给 CodeBuddy(嵌入式 + C 方向 AI 执行者)的执行需求书 V1.1 完整档案:把 garden-driver 重构为标准五层架构、新增 HAL 设备抽象层,全程不改坏现有已验证代码。文档自包含,执行前通读全文,尤其第九节「已知坑位」。方法论解读见博客文章《我是怎么给 AI 写执行需求书的》。
#一、元信息
| 字段 | 值 |
|---|---|
| 版本 | V1.1(2026-10-08),相对 V1.0(09-28)的更新见第十一节 |
| 交付对象 | CodeBuddy(嵌入式 + C 方向) |
| 目标 | garden-driver 重构为五层架构 + HAL 设备抽象层,不改坏已验证代码 |
| 预计工作量 | 1~1.5 天(CLI 已较 09-28 版扩大一倍,含板上全量回归) |
| 状态 | ✅ 需求定稿,待执行 / 验收 |
| 读者注意 | 第 4 节签名表按 10-08 现状整理,允许与代码有出入——必须先做任务 0 对齐 |
#二、背景与目标(30 秒版)
C 语言驱动工程 garden-driver 已完成全部模块驱动并在板上实测通过(环境 / 电源 / 水位 / 灯带 / 指示灯 / 水泵 / 电机全链路)。当前问题:上层入口(main.c 演示程序 / cli_main.c 面板后端)直接调用具体芯片驱动(如 aht20_read()),更换传感器或某驱动出问题时,上层要跟着改。
目标:引入 HAL 设备抽象层,上层只认 hal_temp_read() 这类统一接口,芯片驱动是接口的「实现」。换 AHT20 → DHT22 = 新增一个驱动文件,上层零改动。
自 V1.0 之后系统已经长成了完整产品,这些都是 HAL 重构的「既成事实约束」——重构只许换工程内部结构,不许动任何对外行为:
| 上层消费方 | 与 garden-cli 的关系 | 重构要求 |
|---|---|---|
| garden-server(Flask :8080 面板) | subprocess 调 CLI,解析 JSON | JSON 契约逐字节不变 |
| garden-voice(离线语音) | 经 server API(call_api_async) | 不感知重构 |
| garden-mqtt-bridge(HA 仪表盘) | 经 server API | 不感知重构 |
| 华为云 IoTDA 桥 / 微信小程序 | 经 server API / 云规则 | 不感知重构 |
| garden-local(物理按键接管) | 旁路,直接驱动层 | 本次不动它 |
#三、现状资产(全部已验证,不要破坏)
| 资产 | 位置 | 状态 |
|---|---|---|
| 板子 SSH | ssh orangepi@192.168.1.107(IP 会漂移,见坑 4;外网 Tailscale 100.93.100.17) | 在线 |
| sudo 密码 | orangepi(用法:echo orangepi | sudo -S <cmd>) | 可用 |
| 板上工程 | ~/garden-driver/(garden 演示 + garden-cli 双二进制) | 运行中 |
| 本地工程 | D:\ai\TREA\orangepi\garden-driver\(同步副本) | 与板同步 |
| 驱动文件 | src/{i2c_bus,aht20,bh1750,ina219,ads1115,pca9685,ultrasonic,pump,stepper,zdt_x42s,...}.c | 实测通过 |
| 硬件描述 | include/config.h(I2C_BUS_PATH=/dev/i2c-2,MOTOR_UART_PATH=/dev/ttyS5,MOTOR_ADDR=0x01,INA219 0x40/0x44/0x45,超声波 TRIG=259/ECHO=260,限位 271 上/272 下,水泵 270) | 定稿 |
| 面板后端 | ~/garden-server/server.py 经 subprocess 调 garden-cli(JSON 契约) | 运行中 |
| 板端数据 | /home/orangepi/garden-data/(标定 + 运动计数,丢不得,见坑 12) | 在用 |
电机子系统现状(09-25 全链路打通,重构必须原样保留行为):行程 429mm / 导程 47.1mm、软限位余量 3%、每 50 次运动自动重标定、全自动标定、堵转保护自恢复、急停三步(杀进程+绕锁+重试)、「继续」断点续走。水位标定已实测定稿:空 40cm / 满 12cm(V1.0 里写的 5/25 占位值已作废)。
#四、架构契约(五层,依赖方向单向向下)
text应用层 app/ 浇花决策 · 电机状态机 · 降级策略 (M4 阶段实现, 本次不建)
服务层 srv/ garden-server(Flask API) · garden-cli · 状态持久化 (已有)
设备抽象层 hal/ hal_temp · hal_lux · hal_power · hal_strip · hal_ind
· hal_pump · hal_water · hal_motor (★ 本次新建)
驱动层 drv/ aht20 · bh1750 · ina219 · ads1115 · pca9685
· ultrasonic · pump · stepper · zdt_x42s (已有, 零改动)
总线/BSP i2c_bus · gpiod · ttyS5 · pwmchip sysfs · config.h (已有, 零改动)
铁律:上层只 include 下一层的头文件。cli_main.c 禁止再 include aht20.h 等具体芯片头文件;hal_impl.c 是唯一允许 include 全部驱动头文件的文件。
V1.1 灯光接口划分变化:24V 灯带调光已迁移到硬件 PWM(pwmchip sysfs,左右两路 GPIO 268/267),PCA9685 现在只驱动两个按键指示灯。因此 hal_strip_* = 灯带(硬件 PWM sysfs,两路)、hal_ind_* = 按键指示灯(PCA9685)。划分以 cli_main.c 现状为准,见任务 0 的核对流程。
#五、HAL 接口定义(契约级签名)
所有函数返回 0 = 成功,-1 = 设备离线 / 未配置 / 读取失败。统一语义:惰性初始化(首次调用自动 open/init);跨进程状态用文件(亮度 / 速度在 /tmp,标定与计数在 /home/orangepi/garden-data/)。以下为 V1.1 目标签名(按 10-08 现状整理)。凡与 cli_main.c 现有 drv_*/cmd_* 实际不符之处,以现状为准修订签名,并把差异写进提交说明。
include/hal/hal_temp.h — AHT20 温湿度
cint hal_temp_read(float *temp_c, float *rh);
include/hal/hal_lux.h — BH1750 光照
cint hal_lux_read(float *lux);
include/hal/hal_power.h — INA219 x3 电压电流
c#define HAL_POWER_CH_COUNT 3 /* 0=A(0x40) 1=B(0x44) 2=C(0x45) */
int hal_power_read(int ch, uint16_t *bus_mv, float *current_ma);
/* 注意: ADC 已改 128 次采样平均 (PWM 负载测量修复), 搬运时保持该配置不动 PGA */
include/hal/hal_strip.h — 24V 灯带(硬件 PWM,左右两路)
cint hal_strip_set(int ch, int duty_pct); /* ch: 0=左(268) 1=右(267); 0~100 */
int hal_strip_get(int ch, int *duty_pct); /* 读 /tmp 状态记忆 */
/* 注意: period/enable 幂等写 (值不变不重写), 只有 duty 每次写——共享 PWM
* 控制器的兄弟通道会被 period 重写冲掉 (坑 9 实录), 此约束必须进 hal_impl */
include/hal/hal_ind.h — 按键指示灯(PCA9685,两路三色)
cint hal_ind_set(int ch, int r, int g, int b); /* 0~100 每通道 */
/* 注意: 必须保留「MODE1 僵死两级自愈」逻辑 (普通纠正+读回校验, 弹回则软复位
* 0x06 重新配置) 与极性宏 (共阴); ALL_LED_OFF_H 全局关断位检查保留 */
include/hal/hal_pump.h — GPIO 水泵
cint hal_pump_set(bool on);
/* 注意: garden-local 有「启动 500ms 内松开沿不触发」的防幽灵自开逻辑 (在
* garden-local 进程内, 不属 CLI); HAL 层不需要复刻, 但不许破坏其依赖的状态文件 */
include/hal/hal_water.h — RCWL-1670 超声波液位
cint hal_water_level(float *distance_cm, int *level_pct);
/* level% = (D_EMPTY - d) / (D_EMPTY - D_FULL) * 100, 截 0~100
* 已实测定稿: D_FULL=12cm, D_EMPTY=40cm (写入 hal_impl 时保留出处注释) */
int hal_water_calibrate(float d_full_cm, float d_empty_cm); /* 持久化到 garden-data */
include/hal/hal_motor.h — ZDT X42S(UART,固件 FW_Ver=201 X 固件 开环模式)
cint hal_motor_position(float *pos_deg, float *err_deg, bool *calibrated);
int hal_motor_drive_status(/* 驱动器状态 JSON 输入: 堵转/编码器/校准表/固件/开闭环 */);
int hal_motor_move_pct(int pct, int speed_pct); /* 依赖标定文件; 软限位映射 [3%,97%] */
int hal_motor_jog(const char *dir); /* "up"/"down", 走到软限位自停 */
int hal_motor_stop(void); /* 急停 (保留梯形收尾降速) */
int hal_motor_protect_clear(void); /* 堵转自锁解除 (0E 52) */
int hal_motor_calibrate(void); /* 全自动标定 */
int hal_motor_resume(void); /* 「继续」断点续走 */
int hal_motor_set_speed(int speed_pct); /* 记忆设定, 不启动 */
#六、电机层移植红线(7 条,全部是已验证行为,逐条对照 09-25 交接文档)
- 0x36 位置解析字节序(resp[2] 符号 + resp[3..6] 数据)——曾有 256 倍 bug,已修,不许回退;
- 速度帧加速度 = 目标 RPM/0.6 反算(恒定加速时间,不是恒定加速度);
- 定位单次探测 + 真实位移回填速率 + 单段按剩余封顶 x1.2(防高速震荡);
- 收尾梯形降速(缓冲区按比例降速,最低 8%);
- 同构判据(任一限位触发即报错)在 jog 与 goto 两处都必须存在;
- tcflush 必须发生在 flock 成功之后(先锁后清);
- garden-cli 用完串口立即 zdt_close 释放 flock(常驻 garden-local 也要借锁)。
#七、实施任务(按序)
任务 0:现状盘点(先做)
grep -n "drv_\|cmd_" cli_main.c提取现有适配函数与命令全集,与第五节签名表逐一核对,多退少补,差异清单写进提交说明;grep -n "garden-cli" ~/garden-server/server.py提取 server 实际调用的全部 CLI 命令 = 回归清单(权威来源,不许遗漏);- 备份:
cp ~/garden-driver/garden-cli ~/garden-cli.bak.$(date +%m%d)(板上,回滚用);tar czf /tmp/garden-data-bak.tgz /home/orangepi/garden-data/。
任务 1:创建 HAL 接口头文件
按第五节(任务 0 对齐后)创建 include/hal/ 下 8 个头文件。每个文件头部写契约注释:返回值语义、对应芯片、离线行为。
任务 2:创建 src/hal_impl.c(唯一 include 驱动头文件的实现文件)
把 cli_main.c 中以下内容原样搬运(已在板上验证过,逻辑不许改):ensure_i2c() / ensure_motor() 惰性初始化(含先锁后清的顺序);状态文件读写(/tmp)与标定读取(garden-data);全部 drv_* 适配函数体(以任务 0 清单为准);灯带 / 指示灯 / 水位 / 电机的全部魔法数字与宏(迁移进 hal_impl 并注释出处);PCA9685 两级自愈、X42S 帧解析、梯形降速、自动重标定计数等既有机制。然后按签名表改名为 hal_* 导出。
任务 3:改造 cli_main.c
删除搬运走的代码(cli_main.c 只剩命令分发 + JSON 组装);所有 drv_xxx(...) 调用改 hal_xxx(...),include 改 hal/hal_*.h;JSON 输出格式一个字符都不能变(server / voice / bridge / 华为云 / 小程序全在消费);main.c 演示程序保持原样不动;run_garden_tracked / _kill_motor_job(急停三步依赖)机制原样保留。
任务 4:Makefile 更新
makeCLI_OBJS := $(filter-out %main.o,$(OBJS)) src/cli_main.o src/hal_impl.o
注意:filter-out 必须带 % 通配(无 % 是精确匹配,前人踩过);garden 目标继续 filter-out %cli_main.o,hal_impl.o 不进演示二进制。
任务 5:板上部署与回归
部署三板斧(本地改完):① powershell -File board_find2.ps1 找板子(默认先试 192.168.1.107);② scp -r garden-driver orangepi@<IP>:~/;③ ssh 后 cd ~/garden-driver && sed -i 's/\r$//' src/*.c include/*.h **/*.h && make clean && make。复杂远端命令一律写 .sh 文件 scp 过去执行(PowerShell 会吃 $() 和嵌套引号)。
回归 = 对任务 0 提取的 server 调用命令清单逐条跑,输出与重构前 diff:status / env / power / water / light / motor position|drive|move 全量;外加 systemctl restart garden-server 后 curl -s :8080/api/status 对比;语音回归(对麦克风说「开灯」「电机到 50%」全链路);HA 回归(bridge 日志无新错误);急停回归(面板急停按钮 → ps 确认 garden-cli motor 进程被杀 → 「继续」键可用)。
#八、验收标准(DoD 10 条)
- make 双二进制,-Wall -Wextra 零警告零错误;
- 回归清单全部命令 JSON 与重构前逐字段一致(含离线降级结构);
- env/power/water 读数与重构前同数量级(power 三通道空载 ~4.4V / 几十 mA);
grep -n "aht20\|bh1750\|ina219\|zdt_\|pump_\|ultrasonic_\|pca9685" cli_main.c零匹配;- include/hal/ 8 个接口文件齐全,每个有契约注释;
- garden 演示二进制行为不变;
- 电机回归:move 25/50/75/100 实测误差 <2%,软限位零撞限位开关,标定往返成功;
- 语音 / HA / 华为云 / 小程序零感知(各跑一轮冒烟);
- 急停三步与「继续」在重构后行为不变;
- 更新工程 README 架构图(五层)+ 本文档勾选完成项。
#九、已知坑位(14 条,前人真实踩过,违反必炸)
- 本机 AI 工具 Write 写中文落盘是 GBK:中文 md 一律用 python 显式 encoding='utf-8' 读写;每写完中文文件跑 fix_encoding.ps1 兜底(幂等);
- 板上部署后必须清 CRLF:
sed -i 's/\r$//'(Makefile 对 CRLF 零容忍); - PowerShell 远程命令:禁止裸写 2>/dev/null 与嵌套双引号,统一「写 .sh → scp → 执行」(本项目 140+ 个 board_*.sh 为此而生);
- 板子 IP 会漂:先试 192.168.1.107,不通用 board_find2.ps1 / TCP 扫 22 端口;
- GNU make filter-out 无 % 是精确匹配;
- sudo 要密码:
echo orangepi | sudo -S <cmd>; - JSON 输出只允许英文(板子 C locale),中文提示由前端渲染;中文文案里的引号一律用「」,ASCII 双引号会截断 Python/bash 字符串;
- printf 格式串里 % 要写 %%;
- systemd 沙箱:garden-server.service 的 NoNewPrivileges=true 会让服务链路里 sudo 全失效(直连正常、经服务失败的第一嫌疑);ReadWritePaths 必须含 /home/orangepi/garden-data(否则标定写盘 EROFS)。本次重构不改服务文件,但 hal_impl 若新增写盘路径必须落在这两个已放行目录内;
- garden-local 是常驻进程且直接走驱动层(旁路 CLI):本次不改它的代码,但 hal_impl 改动不得破坏它依赖的文件 / 锁语义;它每 10 分钟自动轮换重启是设计行为不是故障;
- 长电机命令持锁问题已用 run_garden_tracked + _kill_motor_job 解决,搬运时保持;重构后必须验证急停仍能杀掉 garden-cli motor 进程;
- /home/orangepi/garden-data/ 是标定+运动计数,删了要重新标定:动手前备份(任务 0.3),任何 clean 类操作禁止波及该目录;
- pkill -f 会匹配到自己的命令行(外层 bash -c 含同字符串):按 PID 杀或用 [x]xx 正则技巧;
- 板子 USB 5V 供电即可跑全链路,但 24V 域(电机/水泵)需上电:若 motor/water 离线,先查 24V 供电再怀疑代码。
#十、范围外(本次不做)
- 应用层 app/ 浇花决策逻辑(M4);
- 语音(garden-voice)/ 华为云桥 / 小程序 / HA 仪表盘的任何代码改动;
- garden-local(物理按键接管)的任何改动;
- OTA / 看门狗 / systemd 服务文件变更;
- 任何对 src/ 现有驱动文件逻辑的修改(aht20.c 等必须零 diff);
- 任何公网暴露。
#十一、版本记录
| 日期 | 类型 | 更新内容 |
|---|---|---|
| 2026-10-08 | V1.1 | 按《接管交接文档 20261007》全面更新:IP 102→107;水位标定定稿 40/12;灯光接口拆分 hal_strip(硬件 PWM)/ hal_ind(PCA9685 指示灯);hal_motor 扩展 5 个接口并固化电机层 7 条移植红线;新增任务 0 现状盘点与备份;回归清单改为以 server.py 实际调用为准;坑位 8→14(新增 systemd 沙箱 / 急停机制 / garden-data 备份 / pkill 自匹配 / 24V 常电);消费方清单化;工作量半天 → 1~1.5 天;DoD 8→10 条 |
| 2026-09-28 | 新增 | V1.0 初版 |
#十二、相关链接
- 我是怎么给 AI 写执行需求书的——本文档背后的方法论:六个部件为什么缺一不可
- 项目总览——这份需求书在整套系统里的位置
- garden-cli 命令速查与急救卡——任务 0 回归清单的命令底表
- 踩坑索引——第九节 14 条坑位的完整版与来龙去脉
- 一次嵌入式联调踩的 17 个坑——坑位清单里前 17 个坑的完整复盘