Files
JBao_Temp_Humi_Fan_FW/Doc/温湿度风扇_固件需求规格书.md
T

752 lines
39 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 温湿度风扇固件需求规格书
| 项 | 内容 |
|---|---|
| 产品 | 温湿度风扇(爬宠) |
| 芯片 | JL7016 / AC7016CBR28 |
| 工程 | `温湿度风扇_soc_JieLi-JL7016` |
| 产品码 | ProdtCode=`CBR3`productCode=`PRCBR3` |
| 协议 | 见宝 SOC 55AAV1.0.02026-09-02 重生:删条件 DP,加联动 / 运行记录 / 心跳预留) |
| 文档版本 | V3.0.1 |
| 日期 | 2026.09.04 |
| 作者 | cyWu |
| 读者 | WorkBuddy / 固件开发 |
| 参考工程 | `D:\Jianbao\Project\产品\宠物\门夹弹力绳\门夹弹力绳_soc_JieLi-JL7016` |
| 新协议生成工程 | `D:\Jianbao\Project\产品\爬宠\温湿度风扇\温湿度风扇_soc_JieLi-JL7016_新协议` |
> **V3.0.1 变更(2026-09-04):** DP 号收紧为连续 0~24。`time_en` 13→7`time_config1~7` 14~20→8~14`run_record` 21→15`heartbeat` 22→16`fault_en/code` 23/24→17/18`alarm_en/code` 25/26→19/20`factory_reset` 27→21`reserva_dp1~3` 28~30→22~24。712 不再空置。以 `jb_protocol.h` / `dp_definition.json` 为准。
>
> **V3.0.0 变更(2026-09-02):** 协议 DP 重生。删除 `cond_en` / `cond_config1~7`7 组条件引擎整段作废)。新增 DP5 `linkage_delete`、DP6 `linkage_config`20B,缸体 + 温湿度 min/max)、当时号为 DP21 `run_record`(现 DP15)、DP22 `heartbeat`(现 DP16,本版不做)。故障/告警/复位号后移,当时 `factory_reset` 为 DP27(现 DP21)。联动按实时温湿度回差开 1 档 / 关风扇,不改 mode;仲裁改为手动 > 定时 > 联动。运行记录按风扇实际在转累加秒,满 10 分钟写 VM。生成协议 `MAX_PACKAGE_LEN` 必须改回 **256**124B 的 `run_record` 装不进 128)。旧条件 VM **不迁移**。
>
> **V2.2.0 变更(2026-08-31):** 顺着 V2.1.0 删 `app_power` 的思路,继续清掉同一类"保留但无调用者"的死代码:删除 `app_function_soft_off()` / `app_function_is_power_on()` / `app_function_power_on_silent()``gDevData.power` 恒为 1、写死在 `app_function_init()`(不再需要 `s_func.app_power` 状态字段);`usr_on_key_short()` 里对应的"软关态唤醒"死分支一并删除。
>
> **V2.1.0 变更(2026-08-31):** 关机功能确认彻底不存在(关机=拔电),**整模块删除 `app_power.c/.h`**(原 request/finalize 深睡钩子已无任何触发源,纯累赘代码);`usr_jb_main.c`/`app_function.c`/`app_disp.c` 里所有 `app_power_poweroff_pending()` 判断一并清理。同时把配对/OTA/NVM 落盘的"ISR 打标志"统一收进新模块 `app_defer.c/.h`(见 16 章)。
>
> **V2.0.0 变更(2026-08-31):** **上电即自动开机**,删除 `app_boot`(原阻塞式 3s/5s/12s 开机判定整模块移除)。按键语义改为运行态按住分级:**短按切档;按住 5s 松手清绑定进配对;按住 12s 到点即进 OTA;取消长按 3s 软关机**(拔电即关)。详见 5.1/5.2/5.3。
>
> **V1.2.0 变更(2026-08-31):** 风扇档位占空比按用户拍板改为 **1档 50%5000/10000)、2档 75%7500/10000**(原 35%/65%,第 5.3/7/17/18 章同步)。
>
> **V1.1.0 变更(2026-08-31):** IO 分离。CT1642 改走 **PG0(CLK)/PG1(DATA)**AHT20 独占 **PC4(SCL)/PC5(SDA)**,两路物理隔离,**删除 `bsp_bus` 共线分时机制**(原 8.3 节整体替换为"IO 独立")。同时把 2.2 节显示协议修正为实测 **18bit 帧**。
本文是 **已拍板的共同理解**,不是草稿。实现时以本文为准;DP 表以 `dp_definition.json` + 本文第 10.2 / 11.1 / 15 章为准。Excel / JSON 描述与本文冲突时:**多字节字段的偏移与开停行为以本文为准**(JSON 对 DP6 只写了类型摘要,min/max 顺序以 10.2 为准)。
---
## 0. WorkBuddy 开干前必读(硬约束)
1. **先改协议再写业务。**`_新协议` 工程拷贝已生成的 `jb_protocol.*` / `dp_definition.json` / `jb_product` 骨架,再把现有 `jb_product.c` 业务回调迁过去(禁止用空壳 TODO 覆盖业务)。拷贝后立刻把 `MAX_PACKAGE_LEN` 从生成值 **128 改回 256**,否则 DP15 `run_record`124B)组不出完整帧。
2. **不要把业务算法写进 `jb_product.c`。** 只把 DP 写入 `gDevData` 并转调 `app_*`。组合根是 `usr_jb_main.c`(照门夹)。
3. **不要抄门夹的电池 / 低电拒绝开机。** 本机 USB 常电、无电池。**上电即开机**(V2.0.0 起,不再长按 3s 才开机);拔电即关,无软关机深睡。
4. **不要把调试 UART 放在 PC5。** 调试 UART TX = **PG6**`IO_PORTG_06`)。PC5 是 AHT20 SDA(独立使用)。
5. **DP0 `power` 预留,不当开关。** 上电恒为开机态(`power=1`),运行中上报 `power=1`,写忽略;停机靠短按切到 0 档或拔电。
6. **CT1642 与 AHT20 已 IO 分离(V1.1.0)。** CT1642 走 PG0(CLK)/PG1(DATA)AHT20 走 PC4(SCL)/PC5(SDA),两路独立、无总线竞争,**不存在分时/总线锁**(见 8.3)。
7. 每个模块必须能用 `board_pin.h``BOARD_xxx_ENABLE` **单独关掉** 仍能编过、能配网上报(关掉的模块返回空操作)。
8. 日志全部 **英文**。中文只出现在注释和本文。
9. 编码风格对齐门夹 + 工程师规范:文件小写+下划线,`g_` / `s_` 前缀,枚举 `Pascal_t`,接口 `xxx_init` / `xxx_run` 或 tick / `xxx_get_status`
10. **禁止继续实现 7 组条件引擎。** `app_cond` 整模块替换为联动(`app_linkage`);旧 `CFG_USER_CBR3_COND` 作废,不读旧 49B。
11. **DP16 心跳本版忽略。** 温湿度仍按协议 2s 变化防抖 / 10min 全量上报,不因 12s 无心跳停报。
---
## 1. 产品一句话
USB 供电的爬宠温湿度风扇:本地按键调档,双两位一体数码管显示整数温湿度,BLE 连 APP;APP 可下发通风/强力模式、1 条缸体联动配置、7 组定时。联动根据实时温湿度与 min/max 回差开 1 档或关风扇;定时还必须 RTC 已对时。风扇实际在转的秒数记 31 天运行记录。
---
## 2. 硬件资源
### 2.1 引脚(以用户原理图说明 + 本文为准)
| 功能 | MCU | 电平 / 说明 |
|---|---|---|
| CT1642 DATA | **PG1** `IO_PORTG_01` | 推挽输出(V1.1.0 改板飞线,独立) |
| CT1642 CLK | **PG0** `IO_PORTG_00` | 推挽输出(同上) |
| AHT20 SDA | **PC5** `IO_PORTC_05` | 开漏+内部上拉(外部 4.7~10k 双保险),独立 |
| AHT20 SCLK | **PC4** `IO_PORTC_04` | 推挽驱动,独立 |
| 风扇 PWM | **PC3** `IO_PORTC_03` | **高电平导通**MCPWM |
| 状态 LED | **PC2** `IO_PORTC_02` | **低电平亮** |
| 按键 | **PB1** `IO_PORTB_01` | 与门夹相同,低电平按下(若实测相反只改 `BOARD_KEY_ACTIVE_LEVEL` |
| 调试 UART TX | **PG6** `IO_PORTG_06` | 只发打印,不占用 PC5 |
| USB | Type-C → LDO 3.3V | 无电池、无充电管理 |
板级文件仍指向 `CONFIG_BOARD_JL701N_DEMO`。产品 GPIO **全部集中到** `apps/usr_periph/board_pin.h`,并改 `board_jl701n_demo_cfg.h`
```c
#define TCFG_UART0_TX_PORT IO_PORTG_06
```
关掉会与 PC3/PC4/PC5 冲突的 demo 外设(SD0、HW SPI1 等)。PWM LED 走产品 `app_led`,保持 `TCFG_PWMLED_ENABLE=DISABLE`。PG0/PG1 无 demo 外设占用(`cpu/br28/mcpwm.c``io_ext_interrupt_test()` 是 while(1) 演示函数,产品不调用),可直接作 CT1642 引脚。
### 2.2 显示器件
- 驱动:类 CT1642CLK+DATA 串行(原理图位号 S1)。
- 两块 2 位 7 段数码管:温度一块、湿度一块(含 DP 段,本产品 **不显示小数**DP 恒灭)。
- **帧协议(2026-08-29 实测定型,与 CSDN yitx 一致)**:一帧 **18bit**,CLK 上升沿采样 DATA,顺序
**C1-C4 位选(4bit,低有效,入参 ~com 先发 C1)→ 6bit 保留(固定填 1)→ Q2-Q9 段码(8bitLSB 先,seg bit0 → Q2**
- DATA 只允许在 **CLK=低** 时改变。
- 发完 18bitCLK=高时 DATA 上升沿 = **锁存**CLK=低时 DATA 上升沿 = **输出刷新**(一次只点亮锁存的那 1 位)。
- CT1642 内部只有 1 个 18 位移位 + 1 个 12 位锁存,**无按位显示 RAM、不自动扫描 4 位**,必须 MCU 持续快节拍轮询(`BOARD_DISP_SCAN_MS=4`4 位才能同亮不闪。
段码与 COM 的 PCB 网络为 `SET1~8``CAN1~4`。P0 必须用写死数字扫清 **哪几 bit 对应温度十位/个位、湿度十位/个位、A~G**。扫不清之前禁止把随机码当产品逻辑。
### 2.3 AHT20
- I2C 7 位地址 **0x38**(写 0x70 / 读 0x71)。
- 上电后 ≥5ms 再操作总线。
- 测量:`0xAC 0x33 0x00`,等 ≥80ms,读 Status + 20bit 湿度 + 20bit 温度 + CRC8。
- `T = (ST / 1048576) * 200 - 50`(℃);湿度按手册 20bit 公式,显示与 DP 用 0~100 整数 %。
- 建议 I2C 100kHz;采集周期 **2s**(手册要求 >1s)。
- CRC 多项式 0x31,初值 0xFF。CRC 失败当本次采样失败。
---
## 3. 软件架构(对齐门夹,删电池,加风扇/显示/传感)
```
apps/earphone/app_main.c
└─ usr_rtc_init()
└─ usr_jb_init() ← 唯一组合根
bsp_hw_init()
app_led_init() ← 灯效事件表最先装载
jbInit() + userInit()
(上电即开机:无按键等待,不再有 app_boot)
app_fan_init() / app_disp_init()
usr_timer_add(app_disp_scan, 4ms, priority=1) ← CT1642 扫描,常驻
app_th_init() / app_mode_init() / app_arbiter_init()
app_linkage_init() / app_timer_init() / app_runrec_init()
app_nvm_init() + app_nvm_load() ← 恢复 VM
app_function_init()
user_key_init() + lock_if_held
usr_timer_add(usr_timer_tick, 20ms, priority=1) ← 常驻,永不重挂
```
**20ms 业务节拍**`usr_timer_tick`,硬件定时器中断上下文,禁止再开 1ms ISR):
1. `jbTimerIrq()` × dt
2. `user_key_scan` + `user_key_handle`
3. `app_th_poll`(内部 2s 才真正采;IO 分离后采集与显示互不干扰)
4. `app_mode_run`(通风/强力状态机;联动/定时引擎在 500ms 任务上下文,见下)
5. `app_disp_run`(闪烁、刷新)
6. `app_function_poll`(同步 DP
7. `app_runrec_tick`(风扇在转则累加秒;满 10 分钟只打脏,不写 flash)
8. `app_nvm_tick`(变化检测 + 500ms 防抖计时,不写 flash
9. `app_led_run`
**priority 固定 1、只注册一次,不动态重挂**:本产品 Type-C 长供电,不需要低功耗;
`app_disp_scan` 本身就以 priority=1 常驻,系统本来就永远无法进入低功耗——
按业务状态动态切换 `usr_timer_tick` 自己的 priority 没有任何实际效果,已删除
(原 `usr_timer_pick_mode`/`usr_timer_rearm`,规格书 V2.3.0 起废弃)。
`usr_timer_loop()`BLE 500ms 任务上下文):`userHandle()` 填温湿度 DP、
`jbProtocolHandle()``app_linkage_run()`/`app_timer_run()`(联动/定时引擎,
仲裁见第 12 章)、`app_runrec_rotate_if_needed()``app_function_loop()`
(VM 落盘 + 配对/OTA 落地)。
可直接复用门夹源码(拷贝后改引脚/事件表,不要改框架语义):
| 复用 | 路径 | 改什么 |
|---|---|---|
| 按键 FSM | `key.c` / `key.h` / `key_manager.c` | 短按切档;**按住分级 5s 配对 / 12s OTA**V2.0.0,无长按关机) |
| LED 事件库 | `led.c` / `led.h` / `led_manager.c` | 单灯 PC2,删红绿双通道 |
| ~~开机判定~~ | ~~`app_boot.c`~~ | **V2.0.0 已整模块删除**(上电即开机,判定下沉到 key_manager |
| 配对会话 | `app_pair.c` | 120s 超时,灯效改单灯 |
| ~~软关机~~ | ~~`app_power.c`~~ | **V2.1.0 已整模块删除**:无电池、无充电,关机=拔电,无软件关机路径 |
| RTC | `usr_rtc.c` | 原样 |
| 协议骨架 | `usr_jb_proto` | 从 `_新协议` 工程拷贝后改 `MAX_PACKAGE_LEN=256`,迁回业务回调 |
**不要拷贝:** `app_battery.c`、充电 GPIO、电机 `app_mode` 步进表(本产品 mode 是通风/强力,重写)。
---
## 4. 模块开关(独立使能 / 屏蔽调试)
全部放 `apps/usr_periph/board_pin.h``0` 时:对应 `init/run` 直接 return,GPIO 不配,日志打一次 `xxx disabled`
```c
#define BOARD_LED_ENABLE 1
#define BOARD_KEY_ENABLE 1
#define BOARD_FAN_ENABLE 1
#define BOARD_DISP_ENABLE 1
#define BOARD_TH_ENABLE 1
#define BOARD_MODE_ENABLE 1 /* 通风/强力状态机 */
#define BOARD_LINKAGE_ENABLE 1 /* 联动引擎;替换原 BOARD_COND_ENABLE */
#define BOARD_TIMER_ENABLE 1 /* 定时引擎;P0/P1 可先 0 */
#define BOARD_RUNREC_ENABLE 1 /* 31 天运行记录 */
#define BOARD_NVM_ENABLE 1 /* VM 持久化;P0 可 0 */
```
总线约定(V1.1.0:IO 独立,无共享总线):
- `DISP=1, TH=0`PG0/PG1 由显示独占,按 CT1642 时序刷;PC4/PC5 保持高阻不配。
- `DISP=0, TH=1`PG0/PG1 不配;PC4/PC5 当软件 I2CAHT20)。
- 都为 1:**两路各走各的**,无锁、无分时(见 8.3)。
- 都为 0:两组脚保持输入/高阻,可单独调 KEY/LED/FAN/BLE。
P0 建议先 `MODE/LINKAGE/TIMER/RUNREC/NVM=0`,台上只验按键、灯、PWM、数码管、传感器。
---
## 5. 电源与按键
### 5.1 上电 / 断电(V2.0.0 改:上电即开机)
- **插 USB / 冷启动直接进入工作态**,无需按键,无 3s 等待。上电流程:
`bsp_hw_init()``app_led_init()``jbInit()/userInit()` → 各 `app_*_init()`
`app_function_init()` 里写死 `gDevData.power=1`)→ `app_nvm_load()`
`user_key_init()` + `user_key_lock_if_held()` → 起 20ms 业务节拍。
- **关机 = 拔电。** 已取消按键长按 3s 软关机(原 3s 与新的 5s/12s 时间轴直接冲突)。
`app_power.c` 的软关机能力(request/finalize + 关机前落盘)**V2.1.0 已整模块删除**
(无任何触发源,纯累赘代码);**V2.2.0** 进一步删除 `app_function_soft_off` /
`app_function_is_power_on` / `app_function_power_on_silent`(同样无调用者的
死代码,`power` 恒 1 不再需要状态字段)。
- 上电若键仍按着:`user_key_lock_if_held()` 只锁**短按**(防一上电就切档),
不锁长按分级 —— 所以"插电时就按住 5s"照样进配对。
- DP0 不参与开关机,运行中恒上报 `power=1`(写死,非每 tick 同步),写忽略。
- 无电池、无充电逻辑。
### 5.2 按住分级(运行态,`key_manager.c` 直接复用 `key.c` 长按/连按/释放事件)
上电后**任意时刻**按住 PB1 都按同一套时序判定(不再有"关机态/开机态"两套):
| 按住时长 | 行为 |
|---|---|
| < 5000 ms 松开 | 短按:切档(见 5.3) |
| 满 5000 ms(仍按住) | **只**切配对提示灯(100ms 快闪),**不清绑定** |
| 满 5s 后、12s 前松开 | 提交配对:`usr_goto_pair_mode()`(断连+清绑定+**重发绑定广播**)+ `app_pair_start()` 起 120s 会话(换手机用这条) |
| 满 12000 ms(仍按住,到点即触发) | 进 OTA(`usr_app_ota_init`),不等松手;同时撤销本次配对 |
设计理由:配对刻意不在 5s 到点提交 —— 用户可能想继续按到 12s 进 OTA,
提前清绑定会误伤已配对的手机。
**上下文约束**:这几个回调跑在 20ms 硬件定时器中断上下文,只准置标志 +
切灯效(纯 RAM)。清绑定(写 VM)与 `usr_app_ota_init`BLE 广播切换)
一律延后到 `app_function_loop()`(500ms 任务上下文)执行 —— 违反会栈溢出软复位
2026-08-31 事故定论)。因此配对/OTA 的实际生效有最多 500ms 延迟。
按住过程 LED 提示(单灯):
- ≥5s:100ms 闪(将配网,松手生效)
- ≥12s200ms 闪(已进入 OTA
### 5.3 开机态按键
| 事件 | 行为 |
|---|---|
| 短按(<1s 松开) | 风扇档位循环:1(50%) → 2(75%) → 3(100%) → 0(关风扇) → 1 … **立刻退出通风/强力**,改为手动档;**联动本轮作废**(第 12 章)。设备仍开机,数码管继续刷 |
| 按住 5s 后松开 | 清绑定 + 进配对(见 5.2) |
| 按住 12s | 进 OTA(见 5.2 |
| ~~长按 3s 软关机~~ | **V2.0.0 已取消**,拔电即关 |
OTA 进行中:忽略配对/短按之外的重复分级(`app_led_is_ota()` 为 1 时不再重入)。
配对进行中:短按仍可切档。
---
## 6. LED(仅 1 颗,PC2 低亮)
| 事件 | 图案 | 优先级(高者覆盖) |
|---|---|---|
| 软关机 / 未开机 | 灭 | — |
| 正常开机 | 常亮 | 低 |
| 按住满 5s(待配网,未松手) | 100ms 亮 / 100ms 灭 | 中 |
| 配对中 | 100ms 亮 / 100ms 灭,直到成功或 120s 失败 | 中 |
| 配对成功 | 常亮(可保持 3s 再回到常亮,允许直接常亮) | 中 |
| 配对失败 | 200ms 闪 3s,然后常亮 | 中 |
| OTA | 200ms 亮 / 200ms 灭,无限 | 最高 |
LED **不** 表示风扇档位,**不** 表示温湿度告警(告警走数码管闪)。
---
## 7. 风扇 PWM
- 引脚 PC3**高有效**,频率 **20 kHz**,占空比单位 0~10000。
- 档位:
| `fan_gear` | 占空比 | 说明 |
|---|---|---|
| 0 | 0% | 关风扇,MCPWM 可关 |
| 1 | 50% | 5000/10000(用户拍板 2026-08-31 |
| 2 | 75% | 7500/10000(用户拍板 2026-08-31 |
| 3 | 100% | 10000/10000 |
- 模块接口:`app_fan_init` / `app_fan_set_gear(uint8_t)` / `app_fan_get_gear`
- DP2 必须等于 **当前真实档位**(通风停 5 分钟那段报 0)。
- 换档无需软爬坡(P0 不做,也不做占位接口)。若 100% 上电冲击过大,后续要加
200ms 爬升时再新增 `app_fan_run` 接口即可,不作为 P0 验收。
---
## 8. 数码管 + AHT20IO 独立)
### 8.1 显示内容
| 管子 | 内容 | 范围 |
|---|---|---|
| 温度 2 位 | 整数 ℃ | 0~99 显示绝对值;&lt;0 显示 `-9``-1`(负号用 G 段或自定义,P0 扫段后定);&lt;−9 钳位 `-9`&gt;99 钳位 `99` |
| 湿度 2 位 | 整数 % | 0~99&gt;99 钳位 `99` |
| 读失败 | 双管 `--` | 连续失败 **3** 次后;`fault_code=1` |
APP DP3 仍是 0.1℃ 精度 float;显示不跟小数。
### 8.2 告警闪烁(与吹风解耦)
**有效联动配置**(第 10.1 节)时,按 **是否落在 [min, max] 闭区间内** 闪,**不等风扇真的开**
- 温度管:`T > Tmax` **或** `T < Tmin`**500ms 亮 / 500ms 灭**`Tmin ≤ T ≤ Tmax`(含等于)→ 常亮。
- 湿度管:`H > Hmax` **或** `H < Hmin` → 同样 500ms 闪。`Hmin ≤ H ≤ Hmax` → 常亮。
- 两路可同时闪。过冷、过干 **只闪不开风扇**(风扇开停见 10.3)。
- 跟联动是否被手动作废无关:超范围仍闪,方便看见超标。
- 无有效联动:不因温湿度闪。
- 温湿度出范围只驱动对应管子闪烁,**不上报** `fault_code`
- 传感器连续失败 3 次:显示 `--`,同步 `fault_code=1``fault_code=2` 预留。
- `alarm_code` 预留,不上报具体码(保持 0)。
- `BOARD_DISP_ENABLE=0` 时不闪、不驱动管子,fault 仍可上报。
### 8.3 IO 独立(V1.1.02026-08-31 改板分离)
CT1642 用 **PG0(CLK)/PG1(DATA)**(推挽,`bsp_ct1642_init()` 配置),AHT20 用 **PC4(SCL)/PC5(SDA)**`bsp_aht20_init()` 配置:PC5 内部上拉 + 外部 4.7~10k 双保险)。两路物理隔离、无总线竞争,**删除 `bsp_bus` 模块**,不存在 lock/unlock/模式切换。
运行时序:
```
需要刷显示(每 4ms 扫描回调):
直接发帧(CT1642 单锁存,一次只亮 1 位,靠快节拍轮询 4 位同亮)
鬼影靠"值不变不刷新"或提速分时压(见 board_pin.h 注释与 bsp_ct1642.c
需要采 AHT20(每 2s,一次完成):
发 0xAC 0x33 0x00 → 等 ≥80ms(期间 I2C 空闲,显示照常刷新,互不影响)
→ 读 7 字节 + CRC8
```
规则(V1.1.0 起):
- **无总线锁**:显示扫描(priority=1 定时器 ISR)与 AHT20 采样可同时进行,不再有"采样期冻结显示"(原共线时代每 2s 闪一下的根因已物理消除)。
- `bsp_aht20_measure_start/finish` 不再返回 -2(总线忙),上层无 busy 重试。
- 等待 AHT20 的 80ms 由 `app_th_poll` 状态机跨 tick 完成(期间 `clr_wdt`),不阻塞 20ms 主节拍。
- 共线时代遗留约束(已作废,勿再套用):原"锁只包裹真实硬件交互窗口、长等待必放锁"适用于 PC4/PC5 共线方案,IO 分离后不需要。
### 8.4 采样失败与联动
- 联动只在 **本次采样成功** 时用新值比较;采样失败保持上一有效值继续判(与显示一致),直到连续 3 次失败改 `--`
- `--` 期间联动视为不成立:若当前是联动占着风扇 → **关掉**`fan_gear=0`),避免盲吹。
- 过冷、过干可以闪管子,**不会因为过冷/过干去开风扇**。
---
## 9. 工作模式(DP1
| 值 | 名称 | 行为 |
|---|---|---|
| 0 | 关闭模式 | 不按模式表驱动风扇;风扇跟手动档 / 定时 / 联动 |
| 1 | 通风 | 强制 1 档,开 **25min**,停 **5min**,循环,直到退出 |
| 2 | 强力 | 强制 3 档,连续 **15min** 后:风扇 0mode 回到 0 |
退出方式(任一即退):
- 短按切档(含切到 0
- APP 写 `fan_gear`
- APP 写 `mode=0`
- 定时窗结束(第 11 章)
联动 **不改 `mode`**:开只把 `fan_gear` 写成 1,关只写成 0。通风/强力仍只由手动和定时启动。
**开机恢复(不存相位):** VM 里 `mode=1` 则从 25min 开段 **从头** 计;`mode=2` 则从 15min **从头** 计。
DP 同步:`mode` = 逻辑模式;`fan_gear` = 实际 PWM 档(通风停段为 0)。
---
## 10. 联动引擎(替换原 7 组条件)
`cond_en`。设备只有 **1 条** 20 字节联动配置(DP6)。不切通风/强力,不恢复档位快照。
### 10.1 有效配置
同时满足才跑联动开停(闪烁仍见 8.2,只要有效配置就判闪):
- 缸体编号 ≠ 0Byte01uint16 大端)
- `Tmin ≤ Tmax``Hmin ≤ Hmax`
- `BOARD_LINKAGE_ENABLE=1`
否则视为无联动:不自动开 1 档;若原先是联动占着风扇则关掉。
缸体编号只存、不参与开停判断(本机一路 AHT20)。配置版本(Byte6)只存不解释。Byte7–19 预留:整包 20B 原样落盘,控制逻辑忽略。
### 10.2 20 字节布局(DP6 `linkage_config`
| 偏移 | 字段 | 编码 |
|---|---|---|
| Byte01 | 缸体编号 | uint16 **大端** |
| Byte2 | 最低温度 `Tmin` | 有符号 int8,单位 ℃ |
| Byte3 | 最高温度 `Tmax` | 有符号 int8,单位 ℃ |
| Byte4 | 最低湿度 `Hmin` | uint8,单位 % |
| Byte5 | 最高湿度 `Hmax` | uint8,单位 % |
| Byte6 | 配置版本 | uint8,只存不用 |
| Byte719 | 预留 | 只存不用 |
温度比较用设备内部 0.1℃(`temp_x10`):`T > Tmax` 等价 `temp_x10 > Tmax * 10`。湿度用整数 %。**没有等于比较**:`T == Tmax` 不算超最高,也不算低于最低。
### 10.3 开停(回差 + 超最高优先)
在有效配置、未被手动作废、且仲裁允许联动占风扇时,每个 500ms 任务 tick 判一次:
**开(一侧满足即可):** `T > Tmax` **或** `H > Hmax``fan_gear = 1`**不改 `mode`**。一直保持到关条件成立。
**关(一侧满足即可,但超最高优先):**
1. 若仍有一侧 `> max`**保持开**(例:33℃ 且湿度已低于 `Hmin` → 继续吹)。
2. 否则,若至少一侧 `< min``fan_gear = 0`
3. 否则(两侧都落在 `[min, max]` 闭区间,含等于)→ **保持上一态**
过冷、过干 **不开风扇**`T < Tmin``H < Hmin` 只参与「可以关」和数码管闪)。
不恢复触发前档位:联动开 = 写 1,联动关 = 写 0。
### 10.4 联动删除(DP5
| 写入 | 行为 |
|---|---|
| 0 | 无操作 |
| 1 | 清 RAM + VM 里 20 字节配置(全 0);若当前是联动占着风扇 → `fan_gear=0`;然后把 `linkage_delete` **清回 0** 再上报(脉冲) |
### 10.5 APP 重写 DP6
`memcpy` 20 字节 → 存 VM → **清手动作废标志** → 立刻按新 min/max 重判(该开开、该关关)。缸体编号写成 0:按无效配置处理,等同删除(联动占着则关)。
---
## 11. 定时引擎(P27 组)
### 11.1 8 字节(Excel 已定义)
| 偏移 | 字段 |
|---|---|
| Byte0 | 使能 |
| Byte1 | 模式 0/1/2 |
| Byte2 | 类型:0=单次,1=每天,2=每周 |
| Byte3 | 星期位图 Bit0=周一 … Bit6=周日(仅类型 2) |
| Byte4 | 开始小时 0~23 |
| Byte5 | 开始分钟 0~59 |
| Byte6 | 结束小时 |
| Byte7 | 结束分钟 |
- 支持跨午夜:开始 22:00 结束 06:00 = 当晚 22 点到次日 6 点。
- 类型 1:每天该窗。
- 类型 2:仅位图置位的星期(跨午夜的窗,以 **开始时刻所在的本地星期** 判断是否该跑)。
- 类型 0 单次:结束时刻过后把 **Byte0 清 0 并写 VM**,不再跑。
### 11.2 RTC 门闩(强制)
```
usr_rtc_get_time() 失败 → 全部定时不执行(等同 time_en=0)
仅 BLE 已连接但从未对时 → 同上
对时成功瞬间 → 立刻补判:若当前落在某条有效窗内,进入该窗模式
```
无电池,拔电后 RTC 丢失。开机后必须等 APP/网关 `usr_rtc_set_from_unix` 成功。
`time_en=0`:所有窗立刻视为结束(停风扇、mode=0),直到再置 1 并重新到开始沿或补窗。
### 11.3 窗内行为
- 进入窗:切到 Byte1 模式(通风循环或强力一轮)。窗内 **联动不插嘴**
- 强力:只跑 **一轮 15min**,自行停下(mode=0,fan=0),**等到结束时刻再做一次收口**(已经停则保持停)。窗内不循环强力。
- 通风:窗内一直 25/5,直到结束时刻。
- 结束时刻:风扇 0,mode=0。然后 **立刻按第 10.3 节判一次联动**(不是手动作废):仍超最高 → 开 1 档。
---
## 12. 仲裁(唯一决策表)
优先级 **高 → 低**
1. **手动**:短按、APP 写 `fan_gear`、APP 写 `mode`
2. **定时窗**RTC 有效且 `time_en=1` 且当前落在窗内)
3. **联动**(有效配置且已因超最高占着风扇,或本 tick 满足开条件)
规则:
| 冲突 | 胜者 | 副作用 |
|---|---|---|
| 手动 vs 正在跑的模式 / 联动 | 手动 | 立刻退模式;若原是联动占着 → **本轮联动作废**RAM,不写 VM |
| 手动 vs 定时窗 | 手动 | 本窗剩余时间不再自动抢回;等到下一窗开始沿或下次对时补窗 |
| 定时窗 vs 联动 | 定时 | 窗内联动不开、不改档;窗结束收口后立刻按 10.3 重判(见 11.3) |
| 联动 vs 空闲 | 联动 | 开则写 `fan_gear=1`,关则写 0;不改 mode、不快照 |
作废标志不存 VM。
**手动作废后的再武装:** 必须先走到第 10.3 节的「关」条件(没有一侧还超最高,且至少一侧 `< min`),再重新出现 `T > Tmax``H > Hmax`,才允许再开 1 档。作废期间数码管仍按 8.2 闪。
**冷启动:** 作废标志是 RAM,上电为空。VM 里即使恢复的是手动 2/3 档,只要已超最高且无定时窗占用,**允许联动立刻改成 1 档**。用户不想吹再短按,那一次才作废。
`time_en` 运行中被写成 0:立刻停定时(窗结束语义)。无联动总开关;无效配置 / DP5 删除即停联动。
---
## 13. 持久化(VM
存(冷启动保留):
- `mode``fan_gear`
- `linkage_config`20B
- `time_en``time_config1~7`(各 8B
- `fault_en``alarm_en`(若有写入)
- `run_record`31 × uint32 秒 + slot[0] 对应的本地日期(对时后才有意义)
不存:
- DP0(忽略)
- 联动作废标志、模式已运行秒数、AHT20 原始采样
- `linkage_delete`(脉冲,RAM 瞬态)
- BLE 三元组(走现有 `CFG_USER_PAIR_INFO`,出厂重置 **不清**
上电后:加载上述字段,**按上次 mode/gear 从周期起点继续工作**,再让联动按第 12 章冷启动规则判一次。
首次上电(VM 无效):mode=0gear=0,联动 20B 全 0,定时全 0,`time_en=0`31 天运行记录全 0。
`CFG_USER_CBR3_COND`(49B 条件)**作废不读**。联动 / 运行记录换新 VM key 或升 `ver`,当新设备。
写 VM 时机:
- APP 改联动/定时、单次定时自毁、档位/模式稳定变化:500ms 防抖(避免短按循环打爆 Flash)
- 运行记录:风扇累计转满 **10 分钟** 写一次;**跨天旋转时立刻写**(见 13.1)
### 13.1 运行记录(DP15 `run_record`RO124B
31 天 × 4 字节,每格 uint32 **大端**,单位秒:
| 偏移 | 含义 |
|---|---|
| Byte0–3 | 今天风扇运行总秒数 |
| Byte47 | 昨天 |
| … | … |
| Byte120123 | 前 30 天 |
**什么叫在运行:** 风扇实际在转(PWM 占空比 > 0 / `fan_gear != 0` 的真实输出)。手动、通风开段、强力、定时、联动都算。通风停 5 分钟那段 **不算**
**日期轴:**
- 未对时:只往 slot[0] 累加,**不旋转**。
- 对时成功:按本地日历把数组左移 N 天(跨了几天就移几天),新今天从 0 或当天已累加值继续。
- 掉电再上电:先把 VM 里 31 格灌进 `gDevData.run_record`;未对时继续往 slot[0] 加。
**上报:** 今天累计秒数相对上次 0x07 上报满 **60 秒** 才进差分(`JB_RUNREC_DIFF_SEC`);跨天旋转立刻带 DP15。APP 0x05 读、10 分钟全量仍带完整 124B。其它 DP 上报时不刷新运行记录基准。
**落盘:** 每累计运行满 600 秒写一次 VM。跨天立刻写。拔电没有软关机钩子,**接受最多丢不满 10 分钟的秒数**。
`BOARD_RUNREC_ENABLE=0`:不累加、DP15 报全 0。
---
## 14. 出厂重置(DP21 写 1
- **清全部业务 VM**(第 13 章列表,含联动 20B 与 31 天运行记录)。
- **保留 BLE 配对,不断开、不软复位。**
- RAM 中 mode=0gear=0,风扇停,联动/定时停止,作废标志清掉。
- 换机:运行中 **按住 5s 后松手** 清配对进绑定广播(见 5.2,无需关机)。
---
## 15. BLE / DP 行为摘要
身份:`CBR3` / `PRCBR3`。帧格式见已生成 `jb_protocol.h`(以 `_新协议` 拷贝后、`MAX_PACKAGE_LEN=256` 的头文件为准)。DP 号 0–24 连续,不要填回条件配置。
| DP | 处理 |
|---|---|
| 0 power | 预留。运行中上报 1,写忽略 |
| 1 mode | 写则手动抢权,切通风/强力/关 |
| 2 fan_gear | 写则手动抢权,退模式,联动作废 |
| 3/4 温湿度 | 仅上报。变化防抖 2s,全量 10min(协议已有) |
| 5 linkage_delete | 第 10.4 节,脉冲 |
| 6 linkage_config | 20 字节 memcpy + 存 VM + 第 10.5 节立刻重判 |
| 7 time_en | 见第 11 章 |
| 8~14 time_config | 8 字节 + 存 VM |
| 15 run_record | RO,由 `app_runrec` 填 124B,禁止 APP 写。差分满 1 分钟或跨天才 0x07 |
| 16 heartbeat | 本版忽略(写了也不停温湿度上报) |
| 17 fault_en | 原样存,P0 不做位级过滤 |
| 18 fault_code | 0 无 / 1 传感器故障 / 2 预留 |
| 19 alarm_en | 原样存 |
| 20 alarm_code | 保持 0 |
| 21 factory_reset | 第 14 章 |
| 22~24 reserva | 存 RAM,可不实现动作 |
OTA:运行态按住 12s 走门夹 `usr_app_ota_init`(在 `app_function_loop` 任务上下文调用,灯效 200ms 闪)。`jbOnOtaNotify` 等协议 OTA **P2 可继续 stub**,注释写明未实现,禁止假装升级成功后擦用户区。
配对超时 120s,失败保持开机。
---
## 16. 建议文件清单(新建,门夹已有的拷贝改)
```
apps/usr_periph/
board_pin.h # 引脚、时序、BOARD_xxx_ENABLE
bsp_hw.c/.h # LED/KEY/FAN GPIO+MCPWM、sleep IO
bsp_ct1642.c/.h # PG0/PG1,移位、锁存、送出
bsp_aht20.c/.h # PC4/PC5,软件 I2C + 测量
usr_rtc.c/.h # 已有则保留
apps/usr_app/
key.c/.h, key_manager.c/.h # 拷贝门夹;短按切档 + 按住 5s/12s 分级
led.c/.h, led_manager.c/.h # 改单灯
app_led.c/.h # 含长按 5s "将配网"提示灯
app_defer.c/.h # ISR→任务上下文延后执行标志位(NVM/配对/OTA 统一收口)
app_pair.c/.h
app_fan.c/.h
app_disp.c/.h # 数值、闪烁、--
app_th.c/.h # 2s 采样、失败计数
app_mode.c/.h # 通风/强力(重写,勿用门夹电机表)
app_linkage.c/.h # 替换 app_condDP5/DP6 + 回差开停
app_runrec.c/.h # 31 天秒数、10 分钟落盘、跨天旋转
app_timer_task.c/.h # P2,避免与 usr_rtc 文件名混
app_nvm.c/.h # 换掉 cond 49B,改为 linkage 20B + run_record
app_function.c/.h # DP 与按键总装
app_arbiter.c/.h # 第 12 章:COND owner 改为 LINKAGE
```
`usr_jb_main.c` 只编排,不写算法。删除或抽空 `app_cond.c/.h`,禁止两套引擎并存。
---
## 17. 分阶段交付与验收
### P0 — 台上 Bring-up(不依赖 APP
**目标:** 两路 IOCT1642 显示 / AHT20 采集)、PWM、按键、灯可独立开关验证。
- [ ] `board_pin.h` + UART TX=PG6,打印正常,且不影响 PC5
- [ ] 插电即开机(不按键,直接进工作态、数码管亮、LED 常亮)
- [ ] 长按 3s **不再**关机(已取消软关机按键入口)
- [ ] 短按(<5s 松手)四态循环,示波器/听感确认 50/75/100/0%
- [ ] 按住满 5s → 100ms 快闪;此时松手 → 清绑定进配对(APP 可搜到)
- [ ] 按住满 5s 不松手继续到 12s → 不进配对,直接进 OTA(200ms 闪)
- [ ] LED 开机常亮、模拟配对闪、模拟 OTA 闪(可用宏强制)
- [ ] CT1642 两管能稳定显示写死的 `25``60`,无鬼影
- [ ] AHT20 串口打印 ℃/%,CRC 正确;采数时数码管不闪、不乱码(IO 独立,无需锁存)
- [ ] `BOARD_TH_ENABLE=0` 时显示仍可刷;`BOARD_DISP_ENABLE=0` 时传感器仍可打印
- [ ] 12s 能进 OTA 广播(可先只打日志 + LED)
- [ ] 长按进配对/OTA 后不复位(验证动作确实走的任务上下文)
**P0 关闭:** `BOARD_MODE/LINKAGE/TIMER/RUNREC/NVM_ENABLE=0`
### P1 — 最小产品闭环
- [ ] 5s 配对,APP 能读温湿度,能写档位/模式
- [ ] 通风 25/5、强力 15min 后停;短按立即退模式
- [ ] 失败 `--`;有联动配置时超范围闪(可用调试宏灌假温度)
- [ ] DP0 写 0 风扇不变、设备不关
- [ ] VM:改档/改模式/写下联动与定时后断电,重新上电从周期头恢复 mode
- [ ] 定时引擎可只存不跑(`BOARD_TIMER_ENABLE=0` 仍能保存 DP
- [ ] 新协议 DP 号:读不到旧的 cond_config;能读写 DP6 20B、能读 DP15
### P2 — 自动化
- [ ] DP6 按 10.2 解析;缸体号 0 / min>max 不跑联动
- [ ] 超最高开 1 档;回差保持;超最高优先(热着湿已低于最低仍吹)
- [ ] 过冷过干不吹,但出范围闪对应管子
- [ ] 手动作废后须先到「关」条件再超最高才再开
- [ ] `--` 时联动占着则关风扇
- [ ] 定时 RTC 门闩:未对时绝不跑;对时后补窗
- [ ] 跨午夜、单次自毁使能
- [ ] 定时强力只一轮;窗结束收口后立刻把联动判一次
- [ ] DP5 脉冲删除;DP6 重写立刻重判
- [ ] 运行记录:转 10 分钟落盘;对时后跨天左移;通风停段不计入
- [ ] 出厂重置清业务(含运行记录)留配对
- [ ] `MAX_PACKAGE_LEN=256`,APP 能读完整 124B 运行记录
- [ ] 仲裁表第 12 章逐条用例(见下)
### P2 仲裁最小用例(必须写进测试笔记)
1. 空闲超最高 → 风扇 1 档;短按到 0 → 保持停,即使仍超最高。
2. 用例 1 之后先到「关」条件(至少一侧 `< min` 且没有一侧超最高),再超最高,才再次 1 档。
3. 定时窗启动强力 → 15min 停 → 窗未结束前保持停 → 结束时刻仍停;若此时仍超最高 → **开 1 档**(联动接手)。
4. 定时窗内再短按切 2 档 → 保持 2 档到窗结束也不要被定时抢回;窗结束若仍超最高且已被手动作废 → **不要**被联动立刻改回 1 档。
5. 未对时,定时配置使能也不转。对时后若在窗内立即进模式。
6. 上电 VM 为 2 档且已超最高、无定时窗 → 联动立刻改 1 档。
7. 33℃ + 湿度已低于 `Hmin` → 风扇保持 1 档(超最高优先)。
8. 20℃、湿度在带内、风扇已关 → 保持关(过冷不吹),温度管闪。
---
## 18. `board_pin.h` 时序宏(实现时抄这份数)
```c
#define BOARD_UART_TX_PIN IO_PORTG_06
#define BOARD_DISP_CLK_PIN IO_PORTG_00
#define BOARD_DISP_DATA_PIN IO_PORTG_01
#define BOARD_TH_SCL_PIN IO_PORTC_04
#define BOARD_TH_SDA_PIN IO_PORTC_05
#define BOARD_FAN_PIN IO_PORTC_03
#define BOARD_LED_PIN IO_PORTC_02
#define BOARD_KEY_PIN IO_PORTB_01
#define BOARD_LED_ON_LEVEL 0
#define BOARD_FAN_ON_LEVEL 1
#define BOARD_KEY_ACTIVE_LEVEL 0
#define BOARD_KEY_POWER_MS 3000
#define BOARD_KEY_PAIR_MS 5000
#define BOARD_KEY_OTA_MS 12000
#define BOARD_PAIR_TIMEOUT_MS (120u * 1000u)
#define BOARD_LED_FAST_ON_MS 100
#define BOARD_LED_FAST_OFF_MS 100
#define BOARD_LED_OTA_ON_MS 200
#define BOARD_LED_OTA_OFF_MS 200
#define BOARD_DISP_BLINK_MS 500
#define BOARD_FAN_PWM_HZ 20000
#define BOARD_FAN_DUTY_GEAR1 5000
#define BOARD_FAN_DUTY_GEAR2 7500
#define BOARD_FAN_DUTY_GEAR3 10000
#define BOARD_TH_PERIOD_MS 2000
#define BOARD_TH_FAIL_MAX 3
#define BOARD_AHT20_WAIT_MS 80
#define BOARD_MODE_VENT_ON_MS (25u * 60u * 1000u)
#define BOARD_MODE_VENT_OFF_MS (5u * 60u * 1000u)
#define BOARD_MODE_POWER_ON_MS (15u * 60u * 1000u)
#define BOARD_RUN_RECORD_DAYS 31
#define BOARD_RUN_RECORD_SAVE_SEC 600 /* 累计运行 10 分钟写一次 VM */
```
---
## 19. 风险与不要擅自发挥的点
| 风险 | 要求 |
|---|---|
| JSON 对 DP6 只写了「温度 int8、湿度/版本 uint8」 | 以本文第 10.2 节 **先 min 后 max** 为准,不要猜成单阈值 |
| 生成协议 `MAX_PACKAGE_LEN=128` | **必须改 256**,否则 DP15 组帧失败 |
| 旧条件 49B VM | 不迁移、不解析;当无联动 |
| CT1642 鬼影/异显微亮 | IO 独立不解决:单锁存 + 移位实时输出的固有特性,软件靠"值不变不刷新"/提速分时压;残余微亮需 Q2-Q9 加 1~10k 硬件下拉 |
| 10k 上拉偏弱 | 软件 I2C 放慢;不行再提硬件改 4.7k(SDA 建议外部 4.7k |
| 联动开 1 档可能一直吹 | 已拍板回差 + 超最高优先,不要擅自加时长或改切通风 |
| 门夹 USB 插入直通开机 | **禁止** 移植 |
| 协议 OTA stub 返回 OK | 注释标明未升级;P2 不要装作已刷写 |
| `Makefile` 引用缺失 board 文件 | 产品只编实际存在的 `board_jl701n_demo.c` |
| 负温度段码 | P0 扫段后在 `app_disp` 注释画真值表 |
| 心跳 12s 停报 | 本版明确不做,不要「顺手实现」 |
---
## 20. 给 WorkBuddy 的推荐开工顺序
1.`_新协议` 拷贝 `jb_protocol.*` / `dp_definition.json``MAX_PACKAGE_LEN` 改 256;把现有 `jb_product.c` 业务迁到新 DP 号(不要用空壳覆盖)。
2. `app_nvm`:丢掉 cond 49B,改为 linkage 20B + run_record;升版本。
3. 新建 `app_linkage`(替换 `app_cond`),仲裁 owner 改为 LINKAGE。
4. 新建 `app_runrec`:20ms 累加、10 分钟脏、对时旋转、灌 DP15。
5. `app_disp` 闪烁改 8.2(出 [min,max] 就闪)。
6. `jbEventProcess`:DP5 脉冲删除、DP6 重写重判、DP16 忽略、DP21 出厂重置。
7. 按第 17 章 P2 用例逐条验。
每一步都保持对应 `BOARD_xxx_ENABLE` 可关。
---
**文档结束。** 行为争议以本文第 5~13 章为准;字节布局以第 10.2 / 11.1 / 13.1 节为准。