project/hal-requirements.md

HAL 分层重构执行需求书(V1.1 档案)

知识库 · 项目卷宗 📅 初版 2026-10-08

这是给 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,解析 JSONJSON 契约逐字节不变
garden-voice(离线语音)经 server API(call_api_async)不感知重构
garden-mqtt-bridge(HA 仪表盘)经 server API不感知重构
华为云 IoTDA 桥 / 微信小程序经 server API / 云规则不感知重构
garden-local(物理按键接管)旁路,直接驱动层本次不动它

#三、现状资产(全部已验证,不要破坏)

资产位置状态
板子 SSHssh 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 交接文档)

  1. 0x36 位置解析字节序(resp[2] 符号 + resp[3..6] 数据)——曾有 256 倍 bug,已修,不许回退;
  2. 速度帧加速度 = 目标 RPM/0.6 反算(恒定加速时间,不是恒定加速度);
  3. 定位单次探测 + 真实位移回填速率 + 单段按剩余封顶 x1.2(防高速震荡);
  4. 收尾梯形降速(缓冲区按比例降速,最低 8%);
  5. 同构判据(任一限位触发即报错)在 jog 与 goto 两处都必须存在;
  6. tcflush 必须发生在 flock 成功之后(先锁后清);
  7. garden-cli 用完串口立即 zdt_close 释放 flock(常驻 garden-local 也要借锁)。

#七、实施任务(按序)

任务 0:现状盘点(先做)

  1. grep -n "drv_\|cmd_" cli_main.c 提取现有适配函数与命令全集,与第五节签名表逐一核对,多退少补,差异清单写进提交说明;
  2. grep -n "garden-cli" ~/garden-server/server.py 提取 server 实际调用的全部 CLI 命令 = 回归清单(权威来源,不许遗漏);
  3. 备份: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 条)

  1. make 双二进制,-Wall -Wextra 零警告零错误;
  2. 回归清单全部命令 JSON 与重构前逐字段一致(含离线降级结构);
  3. env/power/water 读数与重构前同数量级(power 三通道空载 ~4.4V / 几十 mA);
  4. grep -n "aht20\|bh1750\|ina219\|zdt_\|pump_\|ultrasonic_\|pca9685" cli_main.c 零匹配;
  5. include/hal/ 8 个接口文件齐全,每个有契约注释;
  6. garden 演示二进制行为不变;
  7. 电机回归:move 25/50/75/100 实测误差 <2%,软限位零撞限位开关,标定往返成功;
  8. 语音 / HA / 华为云 / 小程序零感知(各跑一轮冒烟);
  9. 急停三步与「继续」在重构后行为不变;
  10. 更新工程 README 架构图(五层)+ 本文档勾选完成项。

#九、已知坑位(14 条,前人真实踩过,违反必炸)

  1. 本机 AI 工具 Write 写中文落盘是 GBK:中文 md 一律用 python 显式 encoding='utf-8' 读写;每写完中文文件跑 fix_encoding.ps1 兜底(幂等);
  2. 板上部署后必须清 CRLF:sed -i 's/\r$//'(Makefile 对 CRLF 零容忍);
  3. PowerShell 远程命令:禁止裸写 2>/dev/null 与嵌套双引号,统一「写 .sh → scp → 执行」(本项目 140+ 个 board_*.sh 为此而生);
  4. 板子 IP 会漂:先试 192.168.1.107,不通用 board_find2.ps1 / TCP 扫 22 端口;
  5. GNU make filter-out 无 % 是精确匹配;
  6. sudo 要密码:echo orangepi | sudo -S <cmd>;
  7. JSON 输出只允许英文(板子 C locale),中文提示由前端渲染;中文文案里的引号一律用「」,ASCII 双引号会截断 Python/bash 字符串;
  8. printf 格式串里 % 要写 %%;
  9. systemd 沙箱:garden-server.service 的 NoNewPrivileges=true 会让服务链路里 sudo 全失效(直连正常、经服务失败的第一嫌疑);ReadWritePaths 必须含 /home/orangepi/garden-data(否则标定写盘 EROFS)。本次重构不改服务文件,但 hal_impl 若新增写盘路径必须落在这两个已放行目录内;
  10. garden-local 是常驻进程且直接走驱动层(旁路 CLI):本次不改它的代码,但 hal_impl 改动不得破坏它依赖的文件 / 锁语义;它每 10 分钟自动轮换重启是设计行为不是故障;
  11. 长电机命令持锁问题已用 run_garden_tracked + _kill_motor_job 解决,搬运时保持;重构后必须验证急停仍能杀掉 garden-cli motor 进程;
  12. /home/orangepi/garden-data/ 是标定+运动计数,删了要重新标定:动手前备份(任务 0.3),任何 clean 类操作禁止波及该目录;
  13. pkill -f 会匹配到自己的命令行(外层 bash -c 含同字符串):按 PID 杀或用 [x]xx 正则技巧;
  14. 板子 USB 5V 供电即可跑全链路,但 24V 域(电机/水泵)需上电:若 motor/water 离线,先查 24V 供电再怀疑代码。

#十、范围外(本次不做)

  • 应用层 app/ 浇花决策逻辑(M4);
  • 语音(garden-voice)/ 华为云桥 / 小程序 / HA 仪表盘的任何代码改动;
  • garden-local(物理按键接管)的任何改动;
  • OTA / 看门狗 / systemd 服务文件变更;
  • 任何对 src/ 现有驱动文件逻辑的修改(aht20.c 等必须零 diff);
  • 任何公网暴露。

#十一、版本记录

📝共 2 条记录
日期类型更新内容
2026-10-08V1.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 初版

#十二、相关链接