diff --git a/.gitignore b/.gitignore index 9955a91..fb83704 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ obj/ # ----------------------------------------------------------------------------- # 链接 / 编译中间产物(cpu/br28/tools 下生成) +# 注意:不要用 cpu/*/tools/*.bin —— 根目录 aac.bin/ota.bin 等是 SDK 要入库的 # ----------------------------------------------------------------------------- cpu/*/tools/sdk.elf cpu/*/tools/sdk.elf.* @@ -32,15 +33,22 @@ cpu/*/tools/data_code.bin cpu/*/tools/p11_code.bin cpu/*/tools/*.bc -# 打包下载目录中的固件产物(保留脚本 .bat / 密钥 .key) +# 打包下载目录:每次烧录会拷贝固件、改写 .bat(可能注入 -key),不要入库 cpu/*/tools/download/**/*.bin cpu/*/tools/download/**/*.fw cpu/*/tools/download/**/*.ufw cpu/*/tools/download/**/*.hex +cpu/*/tools/download/**/*.bat # 配置工具本地数据库 cpu/*/tools/local.db +# ----------------------------------------------------------------------------- +# WorkBuddy / Cursor 本地会话(记忆、产物、规则缓存) +# ----------------------------------------------------------------------------- +.workbuddy/ +.cursor/ + # ----------------------------------------------------------------------------- # IDE / 编辑器 # ----------------------------------------------------------------------------- @@ -90,7 +98,10 @@ Release_Project_*/ # ----------------------------------------------------------------------------- # 注意:以下内容请保留上传,不要忽略 # - cpu/br28/liba/*.a SDK 预编译库 +# - apps/usr_le_code/lib/*.a 见宝 BLE 预编译库(无 intentional 改动不要提交) # - AC701N.cbp / Makefile 工程文件 # - cpu/br28/tools/*.exe 等 下载/打包工具 +# - cpu/br28/tools/*.bin SDK 资源(ota.bin 等) # - include_lib/ / apps/ 头文件与应用源码 +# - Doc/ 需求规格书 # ----------------------------------------------------------------------------- diff --git a/AC701N.cbp b/AC701N.cbp index f2b466b..2473078 100644 --- a/AC701N.cbp +++ b/AC701N.cbp @@ -177,6 +177,7 @@ + @@ -782,6 +783,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Doc/温湿度风扇_固件需求规格书.md b/Doc/温湿度风扇_固件需求规格书.md new file mode 100644 index 0000000..16b44d0 --- /dev/null +++ b/Doc/温湿度风扇_固件需求规格书.md @@ -0,0 +1,640 @@ +# 温湿度风扇固件需求规格书 + +| 项 | 内容 | +|---|---| +| 产品 | 温湿度风扇(爬宠) | +| 芯片 | JL7016 / AC7016C(BR28) | +| 工程 | `温湿度风扇_soc_JieLi-JL7016` | +| 产品码 | ProdtCode=`CBR3`,productCode=`PRCBR3` | +| 协议 | 见宝 SOC 55AA,V1.0.0 | +| 文档版本 | V1.2.0 | +| 日期 | 2026.08.31 | +| 作者 | cyWu | +| 读者 | WorkBuddy / 固件开发 | +| 参考工程 | `D:\Jianbao\Project\产品\宠物\门夹弹力绳\门夹弹力绳_soc_JieLi-JL7016` | + +> **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 帧**。 + +本文是 **已拍板的共同理解**,不是草稿。实现时以本文 + `Doc\温湿度风扇DP配置.xlsx` 为准。Excel 与工程代码冲突时:**Excel 字节布局优先,本文行为优先**。 + +--- + +## 0. WorkBuddy 开干前必读(硬约束) + +1. **先改协议再写业务。** Excel 里 `cond_config1~7` 已是 **7 字节**,当前工程 `jb_protocol.*` / `dp_definition.json` 仍是 6 字节。必须按 Excel 重新生成协议,禁止继续按 6 字节解析。 +2. **不要把业务算法写进 `jb_product.c`。** 只把 DP 写入 `gDevData` 并转调 `app_*`。组合根是 `usr_jb_main.c`(照门夹)。 +3. **不要抄门夹的电池 / USB 直通开机 / 低电拒绝开机。** 本机 USB 常电、无电池。插上电 **不会** 自动工作,必须长按 3s。 +4. **不要把调试 UART 放在 PC5。** 调试 UART TX = **PG6**(`IO_PORTG_06`)。PC5 是 AHT20 SDA(独立使用)。 +5. **DP0 `power` 预留,不当开关。** 真开关只有按键 3s 软开关机。运行中上报 `power=1`,写忽略。 +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`。 + +--- + +## 1. 产品一句话 + +USB 供电的爬宠温湿度风扇:本地按键开关机和调档,双两位一体数码管显示整数温湿度,BLE 连 APP;APP 可下发通风/强力模式、7 组温湿度条件、7 组定时。条件与定时仅在设备处于开机态执行;定时还必须 RTC 已对时。 + +--- + +## 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 显示器件 + +- 驱动:类 CT1642,CLK+DATA 串行(原理图位号 S1)。 +- 两块 2 位 7 段数码管:温度一块、湿度一块(含 DP 段,本产品 **不显示小数**,DP 恒灭)。 +- **帧协议(2026-08-29 实测定型,与 CSDN yitx 一致)**:一帧 **18bit**,CLK 上升沿采样 DATA,顺序 + **C1-C4 位选(4bit,低有效,入参 ~com 先发 C1)→ 6bit 保留(固定填 1)→ Q2-Q9 段码(8bit,LSB 先,seg bit0 → Q2)**。 +- DATA 只允许在 **CLK=低** 时改变。 +- 发完 18bit:CLK=高时 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() ← 必须在开机等待之前 + app_boot_wait_key() ← 3s/5s/12s,失败则 soft poweroff 不返回 + jbInit() + userInit() + 若 usr_goto_updata → OTA + app_nvm_load() ← 恢复 VM + app_fan_init() + app_disp_init() + app_th_init() + app_mode_init() + app_cond_init() + app_timer_init() + app_function_init() + user_key_init() + lock_if_held + usr_timer_rearm(20ms) +``` + +**20ms 业务节拍**(`usr_timer_tick`,与门夹相同,禁止再开 1ms ISR): + +1. `jbTimerIrq()` × dt +2. `user_key_scan` + `user_key_handle` +3. `app_th_poll`(内部 2s 才真正采;IO 分离后采集与显示互不干扰) +4. `app_cond_run` / `app_timer_run` / `app_mode_run`(仲裁见第 12 章) +5. `app_fan_run` +6. `app_disp_run`(闪烁、刷新) +7. `app_led_run` +8. `app_function_poll`(同步 DP) +9. 重装定时器(按键按住 / 风扇转 / 关机流程中 → 不可睡) + +`usr_timer_loop()`(BLE 500ms)只做:`userHandle()` 填温湿度 DP + `jbProtocolHandle()`。 + +可直接复用门夹源码(拷贝后改引脚/事件表,不要改框架语义): + +| 复用 | 路径 | 改什么 | +|---|---|---| +| 按键 FSM | `key.c` / `key.h` / `key_manager.c` | 短按切档、长按关机 | +| LED 事件库 | `led.c` / `led.h` / `led_manager.c` | 单灯 PC2,删红绿双通道 | +| 开机判定 | `app_boot.c` | **删除 USB 直通、低电拒绝** | +| 配对会话 | `app_pair.c` | 120s 超时,灯效改单灯 | +| 软关机 | `app_power.c` 精简 | 无电池、无充电;只保留 request/finalize | +| RTC | `usr_rtc.c` | 原样 | +| 协议骨架 | `usr_jb_proto` | 重生 7 字节条件 | + +**不要拷贝:** `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_COND_ENABLE 1 /* 条件引擎;P0/P1 可先 0 */ +#define BOARD_TIMER_ENABLE 1 /* 定时引擎;P0/P1 可先 0 */ +#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 当软件 I2C(AHT20)。 +- 都为 1:**两路各走各的**,无锁、无分时(见 8.3)。 +- 都为 0:两组脚保持输入/高阻,可单独调 KEY/LED/FAN/BLE。 + +P0 建议先 `MODE/COND/TIMER/NVM=0`,台上只验按键、灯、PWM、数码管、传感器。 + +--- + +## 5. 电源与按键 + +### 5.1 关机 / 开机 + +- 关机 = `power_set_soft_poweroff()`:风扇 PWM=0 并关 MCPWM,数码管锁存灭(全段灭),LED 灭,BLE 停。 +- **插 USB / 冷启动不会自动进入工作态。** 必须按住 3s。 +- DP0 不参与。不要用 APP 开关机。 +- 无电池逻辑。 + +### 5.2 关机态按住(`app_boot.c`,用 `timer_get_ms()` 轮询,与运行态 FSM 分开) + +| 松开时已按住 | 行为 | +|---|---| +| < 3000 ms | 继续软关机 | +| ≥ 3000 且 < 5000 ms | 正常开机,加载 VM,恢复上次 mode/gear 并从周期起点开跑 | +| ≥ 5000 且 < 12000 ms | 开机 + `usr_clear_pair_info` + 进配对(换手机用这条) | +| ≥ 12000 ms | 进 OTA(门夹 `usr_app_ota_init` 路径) | + +按住过程 LED 提示(单灯): + +- ≥3s:常亮(将开机) +- ≥5s:100ms 闪(将配对) +- ≥12s:200ms 闪(将 OTA) + +开机后若键仍按着:`user_key_lock_if_held()`,直到松开才接受短按/长按。 + +### 5.3 开机态按键 + +| 事件 | 行为 | +|---|---| +| 短按 | 风扇档位循环:1(50%) → 2(75%) → 3(100%) → 0(关风扇) → 1 … **立刻退出通风/强力**,改为手动档。设备仍开机,数码管继续刷 | +| 长按 3s | 请求软关机。VM **不清** | + +配对进行中:长按关机可忽略(与门夹一致,避免配到一半睡死)。短按仍可切档。 + +--- + +## 6. LED(仅 1 颗,PC2 低亮) + +| 事件 | 图案 | 优先级(高者覆盖) | +|---|---|---| +| 软关机 / 未开机 | 灭 | — | +| 正常开机 | 常亮 | 低 | +| 配对中 | 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` / `app_fan_run`。 +- DP2 必须等于 **当前真实档位**(通风停 5 分钟那段报 0)。 +- 换档无需软爬坡(P0 不做)。若 100% 上电冲击过大,允许后续加 200ms 爬升,不作为 P0 验收。 + +--- + +## 8. 数码管 + AHT20(IO 独立) + +### 8.1 显示内容 + +| 管子 | 内容 | 范围 | +|---|---|---| +| 温度 2 位 | 整数 ℃ | 0~99 显示绝对值;<0 显示 `-9`~`-1`(负号用 G 段或自定义,P0 扫段后定);<−9 钳位 `-9`;>99 钳位 `99` | +| 湿度 2 位 | 整数 % | 0~99,>99 钳位 `99` | +| 读失败 | 双管 `--` | 连续失败 **3** 次后;`fault_code=2` | + +APP DP3 仍是 0.1℃ 精度 float;显示不跟小数。 + +### 8.2 告警闪烁(与吹风解耦) + +- 任一 **已使能** 且 `cond_en=1` 的条件组,其温度比较 **当前已成立**(**不等待** Byte4 时长)→ 温度管 **500ms 亮 / 500ms 灭**。 +- 湿度同理。两路可同时闪。 +- 比较不再成立 → 该侧停闪,常亮显示数值。 +- 同步:`fault_code=1`(温湿度异常)。传感器故障优先于 1:失败达 3 次则 `fault_code=2` 且 `--`。 +- `alarm_code` 预留,不上报具体码(保持 0)。 +- `BOARD_DISP_ENABLE=0` 时不闪、不驱动管子,fault 仍可上报。 + +### 8.3 IO 独立(V1.1.0,2026-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 次失败改 `--`。 +- `--` 期间条件比较视为不成立(不误触发吹风)。 + +--- + +## 9. 工作模式(DP1) + +| 值 | 名称 | 行为 | +|---|---|---| +| 0 | 关闭模式 | 不按模式表驱动风扇;风扇跟手动档 / 其它仲裁胜者 | +| 1 | 通风 | 强制 1 档,开 **25min**,停 **5min**,循环,直到退出 | +| 2 | 强力 | 强制 3 档,连续 **15min** 后:风扇 0,mode 回到 0 | + +退出方式(任一即退): + +- 短按切档(含切到 0) +- APP 写 `fan_gear` +- APP 写 `mode=0` +- 定时窗结束(第 11 章) +- 条件恢复快照(第 10 章,若当前模式是条件开的) + +**开机恢复(不存相位):** VM 里 `mode=1` 则从 25min 开段 **从头** 计;`mode=2` 则从 15min **从头** 计。 + +DP 同步:`mode` = 逻辑模式;`fan_gear` = 实际 PWM 档(通风停段为 0)。 + +--- + +## 10. 条件引擎(P2,7 组) + +### 10.1 总开关 + +- `cond_en=0`:不启动新条件;若正在因条件跑模式,视为条件结束,**按 Q9 A3 恢复触发前快照**。 +- `cond_en=1`:扫描 config1~7。 + +### 10.2 7 字节布局(Excel 枚举列;备注里 Byte5 位域是笔误,以本表 Byte6 为准) + +| 偏移 | 字段 | 编码 | +|---|---|---| +| Byte0 | 使能 | 0=关,非 0=开 | +| Byte1–2 | 温度阈值 | **有符号 int16 大端**,传输值 = ℃ × 10。例 30.5℃ → `0x01 0x31`;−5.0℃ → `0xFF 0xCE` | +| Byte3 | 湿度阈值 | 0~100,单位 % | +| Byte4 | 持续分钟 | 1~255 按分钟计;**0 = 比较一成立立即触发** | +| Byte5 | 动作 | **模式**:0=关闭模式,1=通风,2=强力。**不是档位** | +| Byte6 | 条件 | Bit7–5 温度比较,Bit4–2 湿度比较,Bit1–0 保留 | + +比较编码(3 bit): + +| 值 | 含义 | +|---|---| +| 000 | 无条件(忽略该侧) | +| 001 | < | +| 010 | <= | +| 011 | == | +| 100 | >= | +| 101 | > | +| 110/111 | 视为无条件 | + +两侧都「无条件」的组视为无效,不触发。 +两侧都有效时 **AND**。一侧无条件则只判另一侧。 +无回差,比较式怎么写怎么判。 + +### 10.3 触发与保持 + +1. 组使能且 `cond_en=1`,比较成立,开始累加持续时间(采样周期 2s,满 `Byte4` 分钟)。 +2. 未满时长时比较变不成立 → 该组计时清零。 +3. 满时长 → **触发**:快照当前 `mode`+`fan_gear`(若尚无「条件占用」),然后切到 Byte5 模式。 +4. 触发后比较仍成立 → **保持**(C2)。强力 15min 到点若仍成立 → **立即再开一轮 15min 强力**(Q16 A1),不要恢复快照。 +5. 比较变不成立 → 本轮条件结束,**恢复快照**(Q9 A3),该组重新进入「需满时长」。 +6. 另一组后触发 → **覆盖** 当前条件模式,**不回退条件栈**(D1)。快照仍是 **第一次条件抢权之前** 的用户/定时状态(后覆盖不改快照)。后者失效且比较不成立 → 恢复这份原始快照,即使前者比较仍成立也不自动回去(D1)。前者若要再开,必须重新满时长。 +7. 手动/APP 介入:见第 12 章,本轮条件作废(RAM 标志,**不写 VM**)。必须先比较不成立,再成立并满时长,才允许再次触发。 + +通风由条件启动时:按 25/5 循环,直到第 5/6/7 条退出。 + +--- + +## 11. 定时引擎(P2,7 组) + +### 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 门闩(Q20,强制) + +``` +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),**等到结束时刻再做一次收口**(已经停则保持停)。窗内不循环强力(Q16 B1)。 +- 通风:窗内一直 25/5,直到结束时刻。 +- 结束时刻:风扇 0,mode=0。 +- 结束时若条件比较仍成立:**不要** 立刻让条件接手,必须重新满时长(第 12 章)。 + +--- + +## 12. 仲裁(唯一决策表) + +优先级 **高 → 低**: + +1. **手动**:短按、APP 写 `fan_gear`、APP 写 `mode` +2. **定时窗**(RTC 有效且 `time_en=1` 且当前落在窗内) +3. **条件**(`cond_en=1` 且已触发保持中) + +规则: + +| 冲突 | 胜者 | 副作用 | +|---|---|---| +| 手动 vs 正在跑的模式(无论谁开的) | 手动 | 立刻退模式;若原是条件开的 → 条件本轮作废(RAM) | +| 手动 vs 定时窗 | 手动 | 本窗剩余时间不再自动抢回;等到下一窗开始沿或下次对时补窗 | +| 定时窗 vs 条件 | 定时 | 条件即使已满时长也不开;窗结束后条件要重新计时长 | +| 条件 vs 空闲 | 条件 | 快照空闲态的 mode/gear | +| 两路条件 | 后触发覆盖 | 不回栈 | + +作废标志不存 VM。冷启动后条件从 0 重新计时(即使拔电前用户刚按停)。 + +`cond_en`/`time_en` 运行中被写成 0:立刻停对应自动化;条件停 → 恢复快照;定时停 → 窗结束语义。 + +--- + +## 13. 持久化(VM) + +存(冷启动 / 软关都保留): + +- `mode`、`fan_gear` +- `cond_en`、`cond_config1~7`(各 7B) +- `time_en`、`time_config1~7`(各 8B) +- `fault_en`、`alarm_en`(若有写入) + +不存: + +- DP0(忽略) +- 条件作废标志、模式已运行秒数、AHT20 原始采样 +- BLE 三元组(走现有 `CFG_USER_PAIR_INFO`,出厂重置 **不清**) + +开机 3s 成功后:加载上述字段,**按上次 mode/gear 从周期起点继续工作**。 + +首次上电(VM 无效):mode=0,gear=0,条件/定时全 0,`cond_en=time_en=0`。 + +写 VM 时机:APP 改配置后、单次定时自毁使能时、软关机前、档位/模式稳定变化时(可做 500ms 防抖,避免短按循环打爆 Flash)。 + +--- + +## 14. 出厂重置(DP25 写 1) + +- **清全部业务 VM**(第 13 章列表)。 +- **保留 BLE 配对,不断开、不软复位。** +- RAM 中 mode=0,gear=0,风扇停,条件/定时停止。 +- 换机:用户关机后 **长按 5s** 清配对进绑定广播。 + +--- + +## 15. BLE / DP 行为摘要 + +身份:`CBR3` / `PRCBR3`。帧格式见已生成 `jb_protocol.h`(重生后以新头文件为准)。 + +| DP | 处理 | +|---|---| +| 0 power | 预留。运行中上报 1,写忽略 | +| 1 mode | 写则手动抢权,切通风/强力/关 | +| 2 fan_gear | 写则手动抢权,退模式 | +| 3/4 温湿度 | 仅上报。变化防抖 2s,全量 10min(协议已有) | +| 5 cond_en | 见第 10/12 章 | +| 6~12 cond_config | 7 字节 memcpy + 存 VM + 条件状态机复位该组计时 | +| 13 time_en | 见第 11 章 | +| 14~20 time_config | 8 字节 + 存 VM | +| 21/23 en | 原样存,P0 不做位级过滤 | +| 22 fault_code | 0 无 / 1 温湿度比较异常 / 2 传感器故障 | +| 24 alarm_code | 保持 0 | +| 25 factory_reset | 第 14 章 | +| 26~28 | 存 RAM/VM 可不实现动作 | + +OTA:12s 开机走门夹 `usr_app_ota_init`。`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 # 拷贝门夹 + led.c/.h, led_manager.c/.h # 改单灯 + app_boot.c/.h # 拷贝后删 USB/电池 + app_led.c/.h + app_power.c/.h # 精简 + app_pair.c/.h + app_fan.c/.h + app_disp.c/.h # 数值、闪烁、-- + app_th.c/.h # 2s 采样、失败计数 + app_mode.c/.h # 通风/强力(重写,勿用门夹电机表) + app_cond.c/.h # P2 + app_timer_task.c/.h # P2,避免与 usr_rtc 文件名混 + app_nvm.c/.h + app_function.c/.h # DP 与按键总装 + app_arbiter.c/.h # 可选:把第 12 章收成一处,避免 mode/cond/timer 互调成环 +``` + +`usr_jb_main.c` 只编排,不写算法。 + +--- + +## 17. 分阶段交付与验收 + +### P0 — 台上 Bring-up(不依赖 APP) + +**目标:** 两路 IO(CT1642 显示 / AHT20 采集)、PWM、按键、灯可独立开关验证。 + +- [ ] `board_pin.h` + UART TX=PG6,打印正常,且不影响 PC5 +- [ ] 关机态 3s 开机、<3s 仍睡;开机态 3s 关机 +- [ ] 短按四态循环,示波器/听感确认 50/75/100/0% +- [ ] LED 开机常亮、模拟配对闪、模拟 OTA 闪(可用宏强制) +- [ ] CT1642 两管能稳定显示写死的 `25` 和 `60`,无鬼影 +- [ ] AHT20 串口打印 ℃/%,CRC 正确;采数时数码管不闪、不乱码(IO 独立,无需锁存) +- [ ] `BOARD_TH_ENABLE=0` 时显示仍可刷;`BOARD_DISP_ENABLE=0` 时传感器仍可打印 +- [ ] 12s 能进 OTA 广播(可先只打日志 + LED) + +**P0 关闭:** `BOARD_MODE/COND/TIMER/NVM_ENABLE=0` + +### P1 — 最小产品闭环 + +- [ ] 5s 配对,APP 能读温湿度,能写档位/模式 +- [ ] 通风 25/5、强力 15min 后停;短按立即退模式 +- [ ] 超限闪(可用调试宏灌假温度)、失败 `--` +- [ ] DP0 写 0 风扇不变、设备不关 +- [ ] VM:改档/改模式/写下条件定时后断电,3s 开机从周期头恢复 mode +- [ ] 定时引擎可只存不跑(`BOARD_TIMER_ENABLE=0` 仍能保存 DP) + +### P2 — 自动化 + +- [ ] 条件 7 字节解析与 AND、时长、快照恢复、后覆盖不回栈 +- [ ] 条件强力循环 15min(仍超限再来一轮) +- [ ] 手动作废条件,须回安全再满时长 +- [ ] 定时 RTC 门闩:未对时绝不跑;对时后补窗 +- [ ] 跨午夜、单次自毁使能 +- [ ] 定时强力只一轮;窗结束收口;结束后条件重新计时 +- [ ] 出厂重置清业务留配对 +- [ ] 仲裁表第 12 章逐条用例(见下) + +### P2 仲裁最小用例(必须写进测试笔记) + +1. 手动 2 档 → 条件触发通风 → 应切通风;短按到 0 → 风扇停且保持停,即使仍超限。 +2. 用例 1 之后温湿度回到安全再超限,满时长才再次通风。 +3. 定时窗启动强力 → 15min 停 → 窗未结束前保持停 → 结束时刻仍停。 +4. 定时窗内再短按切 1 档 → 保持 1 档到窗结束也不要被定时抢回。 +5. 未对时,把设备时钟拨乱或不对时 → 定时配置使能也不转。对时后若在窗内立即进模式。 +6. 两组条件先后触发,后者强力覆盖通风;后者失效恢复 **手动快照** 而不是第一组通风。 + +--- + +## 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) +``` + +--- + +## 19. 风险与不要擅自发挥的点 + +| 风险 | 要求 | +|---|---| +| Excel 备注 Byte5 vs 枚举 Byte5/6 | 以本文第 10.2 节为准 | +| CT1642 鬼影/异显微亮 | IO 独立不解决:单锁存 + 移位实时输出的固有特性,软件靠"值不变不刷新"/提速分时压;残余微亮需 Q2-Q9 加 1~10k 硬件下拉 | +| 10k 上拉偏弱 | 软件 I2C 放慢;不行再提硬件改 4.7k(SDA 建议外部 4.7k) | +| 条件强力循环可能把箱体吹干 | 已拍板(Q16 A1),不要擅自改成通风兜底 | +| 门夹 USB 插入直通开机 | **禁止** 移植 | +| 协议 OTA stub 返回 OK | 注释标明未升级;P2 不要装作已刷写 | +| `Makefile` 引用缺失 board 文件 | 产品只编实际存在的 `board_jl701n_demo.c` | +| 负温度段码 | P0 扫段后在 `app_disp` 注释画真值表 | + +--- + +## 20. 给 WorkBuddy 的推荐开工顺序 + +1. 按 Excel 重生 7 字节 `jb_protocol` / `dp_definition.json`。 +2. 新建 `board_pin.h`,UART 改 PG6,关掉 PC3/4/5 上的 demo 外设。 +3. 移植 key/led/boot/power/pair(删电池与 USB 直通)。 +4. `bsp_hw` 风扇 PWM + LED + KEY。 +5. `bsp_ct1642`(PG0/PG1)写死 25/60。 +6. `bsp_aht20` 串口打印,再接到显示。 +7. P1:`app_mode` + DP 同步 + NVM。 +8. P2:`app_cond` + `app_timer_task` + 仲裁用例。 + +每一步都保持对应 `BOARD_xxx_ENABLE` 可关。 + +--- + +**文档结束。** 行为争议以本文第 5~12 章为准;字节布局以第 10.2 / 11.1 节与 Excel「枚举/值定义」列为准。 diff --git a/Makefile b/Makefile index c4761fe..2919771 100644 --- a/Makefile +++ b/Makefile @@ -251,6 +251,7 @@ INCLUDES := \ -Iapps/usr_jb_proto/Protocol \ -Iapps/usr_jb_proto/Utils \ -Iapps/usr_periph \ + -Iapps/usr_app \ -I$(SYS_INC_DIR) \ @@ -265,17 +266,9 @@ c_SRC_FILES := \ apps/common/audio/audio_utils.c \ apps/common/audio/decode/audio_key_tone.c \ apps/common/audio/decode/decode.c \ - apps/common/audio/demo/audio_demo.c \ - apps/common/audio/online_debug/aud_data_export.c \ - apps/common/audio/online_debug/aud_mic_dut.c \ - apps/common/audio/online_debug/aud_spatial_effect_dut.c \ - apps/common/audio/online_debug/audio_online_debug.c \ apps/common/audio/sine_make.c \ apps/common/audio/uartPcmSender.c \ - apps/common/audio/wm8978/iic.c \ - apps/common/audio/wm8978/wm8978.c \ apps/common/bt_common/bt_test_api.c \ - apps/common/cJSON/cJSON.c \ apps/common/config/app_config.c \ apps/common/config/bt_profile_config.c \ apps/common/config/ci_transport_uart.c \ @@ -285,35 +278,6 @@ c_SRC_FILES := \ apps/common/dev_manager/dev_manager.c \ apps/common/dev_manager/dev_reg.c \ apps/common/dev_manager/dev_update.c \ - apps/common/device/gSensor/SC7A20.c \ - apps/common/device/gSensor/STK8321.c \ - apps/common/device/gSensor/da230.c \ - apps/common/device/gSensor/gSensor_manage.c \ - apps/common/device/gSensor/mpu6050.c \ - apps/common/device/gx8002_npu/gx8002_enc/gx8002_enc.c \ - apps/common/device/gx8002_npu/gx8002_npu.c \ - apps/common/device/gx8002_npu/gx8002_npu_event_deal.c \ - apps/common/device/gx8002_npu/gx8002_upgrade/app_upgrade/gx_uart_upgrade_app.c \ - apps/common/device/gx8002_npu/gx8002_upgrade/app_upgrade/gx_uart_upgrade_tws.c \ - apps/common/device/gx8002_npu/gx8002_upgrade/gx_uart_upgrade.c \ - apps/common/device/gx8002_npu/gx8002_upgrade/gx_uart_upgrade_porting.c \ - apps/common/device/gx8002_npu/gx8002_upgrade/sdfile_upgrade/gx_uart_upgrade_sdfile.c \ - apps/common/device/gx8002_npu/gx8002_upgrade/spp_upgrade/gx_fifo.c \ - apps/common/device/gx8002_npu/gx8002_upgrade/spp_upgrade/gx_uart_upgrade_spp.c \ - apps/common/device/imu_sensor/icm_42670p/icm_42670p.c \ - apps/common/device/imu_sensor/icm_42670p/inv_imu_apex.c \ - apps/common/device/imu_sensor/icm_42670p/inv_imu_driver.c \ - apps/common/device/imu_sensor/icm_42670p/inv_imu_transport.c \ - apps/common/device/imu_sensor/imuSensor_manage.c \ - apps/common/device/imu_sensor/lsm6dsl/lsm6dsl.c \ - apps/common/device/imu_sensor/mpu6887/mpu6887p.c \ - apps/common/device/imu_sensor/mpu9250/mpu9250.c \ - apps/common/device/imu_sensor/qmi8658/qmi8658c.c \ - apps/common/device/imu_sensor/sh3001/sh3001.c \ - apps/common/device/in_ear_detect/in_ear_detect.c \ - apps/common/device/in_ear_detect/in_ear_manage.c \ - apps/common/device/ir_sensor/ir_manage.c \ - apps/common/device/ir_sensor/jsa1221.c \ apps/common/device/key/adkey.c \ apps/common/device/key/adkey_rtcvdd.c \ apps/common/device/key/ctmu_touch_key.c \ @@ -323,7 +287,6 @@ c_SRC_FILES := \ apps/common/device/key/touch_key.c \ apps/common/device/key/uart_key.c \ apps/common/device/norflash/norflash_sfc.c \ - apps/common/device/ntc/ntc_det.c \ apps/common/device/usb/device/cdc.c \ apps/common/device/usb/device/descriptor.c \ apps/common/device/usb/device/hid.c \ @@ -345,43 +308,11 @@ c_SRC_FILES := \ apps/common/device/usb/host/usb_storage.c \ apps/common/device/usb/usb_config.c \ apps/common/device/usb/usb_host_config.c \ - apps/common/ezxml/ezxml.c \ - apps/common/ezxml/ezxml_example.c \ apps/common/fat_nor/cfg_private.c \ apps/common/fat_nor/virfat_flash.c \ apps/common/file_operate/file_bs_deal.c \ apps/common/file_operate/file_manager.c \ - apps/common/icsd/adt/icsd_adt.c \ - apps/common/icsd/adt/icsd_adt_app.c \ - apps/common/icsd/anc/icsd_anc_app.c \ - apps/common/icsd/anc/icsd_anc_board.c \ - apps/common/icsd/anc/icsd_anc_data.c \ - apps/common/jl_kws/jl_kws_algo.c \ - apps/common/jl_kws/jl_kws_audio.c \ - apps/common/jl_kws/jl_kws_event.c \ - apps/common/jl_kws/jl_kws_main.c \ - apps/common/music/breakpoint.c \ - apps/common/music/music_decrypt.c \ - apps/common/music/music_id3.c \ - apps/common/music/music_player.c \ apps/common/temp_trim/dtemp_pll_trim.c \ - apps/common/test/fs_test.c \ - apps/common/test/os_test.c \ - apps/common/third_party_profile/Tecent_LL/tecent_ll_demo/ll_demo.c \ - apps/common/third_party_profile/Tecent_LL/tecent_ll_demo/ll_task.c \ - apps/common/third_party_profile/Tecent_LL/tecent_protocol/ble_qiot_import.c \ - apps/common/third_party_profile/Tecent_LL/tecent_protocol/ble_qiot_llsync_data.c \ - apps/common/third_party_profile/Tecent_LL/tecent_protocol/ble_qiot_llsync_device.c \ - apps/common/third_party_profile/Tecent_LL/tecent_protocol/ble_qiot_llsync_event.c \ - apps/common/third_party_profile/Tecent_LL/tecent_protocol/ble_qiot_llsync_ota.c \ - apps/common/third_party_profile/Tecent_LL/tecent_protocol/ble_qiot_service.c \ - apps/common/third_party_profile/Tecent_LL/tecent_protocol/ble_qiot_template.c \ - apps/common/third_party_profile/Tecent_LL/tecent_protocol/ble_qiot_utils_base64.c \ - apps/common/third_party_profile/Tecent_LL/tecent_protocol/ble_qiot_utils_crc.c \ - apps/common/third_party_profile/Tecent_LL/tecent_protocol/ble_qiot_utils_hmac.c \ - apps/common/third_party_profile/Tecent_LL/tecent_protocol/ble_qiot_utils_log.c \ - apps/common/third_party_profile/Tecent_LL/tecent_protocol/ble_qiot_utils_md5.c \ - apps/common/third_party_profile/Tecent_LL/tecent_protocol/ble_qiot_utils_sha1.c \ apps/common/third_party_profile/common/3th_profile_api.c \ apps/common/third_party_profile/common/custom_cfg.c \ apps/common/third_party_profile/common/mic_rec.c \ @@ -398,7 +329,6 @@ c_SRC_FILES := \ apps/common/third_party_profile/jieli/JL_rcsp/adv_app_setting/adv_anc_voice_key.c \ apps/common/third_party_profile/jieli/JL_rcsp/adv_app_setting/adv_bt_name_setting.c \ apps/common/third_party_profile/jieli/JL_rcsp/adv_app_setting/adv_eq_setting.c \ - apps/common/third_party_profile/jieli/JL_rcsp/adv_app_setting/adv_hearing_aid_setting.c \ apps/common/third_party_profile/jieli/JL_rcsp/adv_app_setting/adv_high_low_vol_setting.c \ apps/common/third_party_profile/jieli/JL_rcsp/adv_app_setting/adv_key_setting.c \ apps/common/third_party_profile/jieli/JL_rcsp/adv_app_setting/adv_led_setting.c \ @@ -415,85 +345,26 @@ c_SRC_FILES := \ apps/common/third_party_profile/jieli/JL_rcsp/rcsp_updata/rcsp_adv_user_update.c \ apps/common/third_party_profile/jieli/JL_rcsp/rcsp_updata/rcsp_ch_loader_download.c \ apps/common/third_party_profile/jieli/JL_rcsp/rcsp_updata/rcsp_user_update.c \ - apps/common/third_party_profile/jieli/le_hogp.c \ apps/common/third_party_profile/jieli/online_db/online_db_deal.c \ apps/common/third_party_profile/jieli/online_db/spp_online_db.c \ - apps/common/third_party_profile/jieli/trans_data_demo/le_trans_data.c \ - apps/common/third_party_profile/jieli/trans_data_demo/spp_trans_data.c \ - apps/common/third_party_profile/jieli/wireless_mic/le_wireless_mic_client.c \ - apps/common/third_party_profile/jieli/wireless_mic/le_wireless_mic_server.c \ - apps/common/third_party_profile/tuya_protocol/app/demo/tuya_ble_app_demo.c \ - apps/common/third_party_profile/tuya_protocol/app/demo/tuya_ota.c \ - apps/common/third_party_profile/tuya_protocol/app/product_test/tuya_ble_app_production_test.c \ - apps/common/third_party_profile/tuya_protocol/app/uart_common/tuya_ble_app_uart_common_handler.c \ - apps/common/third_party_profile/tuya_protocol/extern_components/mbedtls/aes.c \ - apps/common/third_party_profile/tuya_protocol/extern_components/mbedtls/ccm.c \ - apps/common/third_party_profile/tuya_protocol/extern_components/mbedtls/hmac.c \ - apps/common/third_party_profile/tuya_protocol/extern_components/mbedtls/md5.c \ - apps/common/third_party_profile/tuya_protocol/extern_components/mbedtls/sha1.c \ - apps/common/third_party_profile/tuya_protocol/extern_components/mbedtls/sha256.c \ - apps/common/third_party_profile/tuya_protocol/port/tuya_ble_port.c \ - apps/common/third_party_profile/tuya_protocol/port/tuya_ble_port_JL.c \ - apps/common/third_party_profile/tuya_protocol/port/tuya_ble_port_peripheral.c \ - apps/common/third_party_profile/tuya_protocol/sdk/src/tuya_ble_api.c \ - apps/common/third_party_profile/tuya_protocol/sdk/src/tuya_ble_bulk_data.c \ - apps/common/third_party_profile/tuya_protocol/sdk/src/tuya_ble_data_handler.c \ - apps/common/third_party_profile/tuya_protocol/sdk/src/tuya_ble_event.c \ - apps/common/third_party_profile/tuya_protocol/sdk/src/tuya_ble_event_handler.c \ - apps/common/third_party_profile/tuya_protocol/sdk/src/tuya_ble_event_handler_weak.c \ - apps/common/third_party_profile/tuya_protocol/sdk/src/tuya_ble_feature_weather.c \ - apps/common/third_party_profile/tuya_protocol/sdk/src/tuya_ble_gatt_send_queue.c \ - apps/common/third_party_profile/tuya_protocol/sdk/src/tuya_ble_heap.c \ - apps/common/third_party_profile/tuya_protocol/sdk/src/tuya_ble_main.c \ - apps/common/third_party_profile/tuya_protocol/sdk/src/tuya_ble_mem.c \ - apps/common/third_party_profile/tuya_protocol/sdk/src/tuya_ble_mutli_tsf_protocol.c \ - apps/common/third_party_profile/tuya_protocol/sdk/src/tuya_ble_queue.c \ - apps/common/third_party_profile/tuya_protocol/sdk/src/tuya_ble_storage.c \ - apps/common/third_party_profile/tuya_protocol/sdk/src/tuya_ble_unix_time.c \ - apps/common/third_party_profile/tuya_protocol/sdk/src/tuya_ble_utils.c \ - apps/common/ui/lcd_simple/lcd_simple_api.c \ - apps/common/ui/lcd_simple/ui.c \ - apps/common/ui/lcd_simple/ui_mainmenu.c \ apps/common/update/testbox_update.c \ apps/common/update/update.c \ apps/common/update/update_tws.c \ apps/common/update/update_tws_new.c \ - apps/earphone/aec/br28/audio_aec.c \ - apps/earphone/aec/br28/audio_aec_demo.c \ - apps/earphone/aec/br28/audio_aec_dms.c \ - apps/earphone/aec/br28/audio_aec_online.c \ - apps/earphone/aec/br28/audio_cvp_3mic.c \ - apps/earphone/aec/br28/audio_cvp_ais_3mic.c \ - apps/earphone/app_ancbox.c \ - apps/earphone/app_anctool.c \ apps/earphone/app_main.c \ - apps/earphone/app_protocol_deal.c \ apps/earphone/app_task_switch.c \ apps/earphone/app_testbox.c \ - apps/earphone/audio_enc_mpt_cvp_ctr.c \ - apps/earphone/audio_enc_mpt_self.c \ apps/earphone/ble_adv.c \ - apps/earphone/board/br28/board_jl7016g_hybrid.c \ - apps/earphone/board/br28/board_jl7018f_demo.c \ - apps/earphone/board/br28/board_jl701n_anc.c \ - apps/earphone/board/br28/board_jl701n_btemitter.c \ apps/earphone/board/br28/board_jl701n_demo.c \ apps/earphone/bt_auto_test.c \ apps/earphone/bt_background.c \ apps/earphone/bt_ble.c \ - apps/earphone/bt_ble_hid.c \ - apps/earphone/bt_emitter.c \ - apps/earphone/bt_tws.c \ apps/earphone/default_event_handler.c \ apps/earphone/dual_update_demo.c \ apps/earphone/earphone.c \ apps/earphone/eartch_event_deal.c \ - apps/earphone/font/fontinit.c \ apps/earphone/idle.c \ apps/earphone/key_event_deal.c \ - apps/earphone/kws_voice_event_deal.c \ - apps/earphone/linein/linein.c \ - apps/earphone/ll_sync_demo/ll_sync_demo.c \ apps/earphone/log_config/app_config.c \ apps/earphone/log_config/lib_btctrler_config.c \ apps/earphone/log_config/lib_btstack_config.c \ @@ -501,53 +372,38 @@ c_SRC_FILES := \ apps/earphone/log_config/lib_media_config.c \ apps/earphone/log_config/lib_system_config.c \ apps/earphone/log_config/lib_update_config.c \ - apps/earphone/music/sd_music.c \ - apps/earphone/pbg_demo.c \ apps/earphone/pc/pc.c \ apps/earphone/power_manage/app_charge.c \ - apps/earphone/power_manage/app_chargestore.c \ apps/earphone/power_manage/app_power_manage.c \ - apps/earphone/power_manage/app_umidigi_chargestore.c \ apps/earphone/rcsp/jl_phone_app.c \ apps/earphone/rcsp/rcsp_adv.c \ apps/earphone/tone_table.c \ - apps/earphone/trans_data_demo/trans_data_demo.c \ - apps/earphone/tuya/tuya_app.c \ - apps/earphone/tuya/tuya_demo.c \ - apps/earphone/ui/lcd_simple/my_demo.c \ apps/earphone/ui_manage.c \ apps/earphone/user_cfg.c \ apps/earphone/version.c \ - apps/earphone/vol_sync.c \ - apps/earphone/wireless_mic/app_main_wireless_mic.c \ - apps/earphone/wireless_mic/audio/adapter_adc.c \ - apps/earphone/wireless_mic/audio/adapter_media.c \ - apps/earphone/wireless_mic/audio/wireless/adapter_wireless_dec.c \ - apps/earphone/wireless_mic/audio/wireless/adapter_wireless_enc.c \ - apps/earphone/wireless_mic/bt/bt_edr_fun.c \ - apps/earphone/wireless_mic/bt/bt_status_event.c \ - apps/earphone/wireless_mic/idev/adapter_idev.c \ - apps/earphone/wireless_mic/idev/idev_bt/adapter_idev_bt.c \ - apps/earphone/wireless_mic/idev/idev_bt/idev_ble/adapter_idev_ble.c \ - apps/earphone/wireless_mic/idev/idev_mic/adapter_idev_mic.c \ - apps/earphone/wireless_mic/odev/adapter_odev.c \ - apps/earphone/wireless_mic/odev/odev_bt/adapter_odev_bt.c \ - apps/earphone/wireless_mic/odev/odev_bt/odev_ble/adapter_odev_ble.c \ - apps/earphone/wireless_mic/odev/odev_bt/odev_ble/odev_ble_wireless.c \ - apps/earphone/wireless_mic/odev/odev_bt/odev_edr/adapter_odev_edr.c \ - apps/earphone/wireless_mic/odev/odev_dac/adapter_odev_dac.c \ - apps/earphone/wireless_mic/process/adapter_process.c \ + apps/usr_jb_proto/Protocol/jb_product.c \ + apps/usr_jb_proto/Protocol/jb_protocol.c \ + apps/usr_jb_proto/Utils/jb_common.c \ + apps/usr_jb_proto/Utils/jb_ringbuffer.c \ + apps/usr_jb_proto/usr_jb_main.c \ + apps/usr_periph/usr_rtc.c \ + apps/usr_periph/bsp_hw.c \ + apps/usr_app/app_led.c \ + apps/usr_app/led.c \ + apps/usr_app/led_manager.c \ + apps/usr_app/key.c \ + apps/usr_app/key_manager.c \ + apps/usr_app/app_mode.c \ + apps/usr_app/app_power.c \ + apps/usr_app/app_pair.c \ + apps/usr_app/app_boot.c \ + apps/usr_app/app_function.c \ cpu/br28/adc_api.c \ - cpu/br28/aec_tool.c \ cpu/br28/app_audio.c \ - cpu/br28/audio_anc.c \ - cpu/br28/audio_anc_fade_ctr.c \ - cpu/br28/audio_anc_mult_scene.c \ cpu/br28/audio_capture.c \ cpu/br28/audio_codec_clock.c \ cpu/br28/audio_common/audio_mic_codec.c \ cpu/br28/audio_cvp_dut.c \ - cpu/br28/audio_cvp_sync.c \ cpu/br28/audio_dec.c \ cpu/br28/audio_dec/audio_dec_file.c \ cpu/br28/audio_dec/audio_dec_iis.c \ @@ -555,17 +411,8 @@ c_SRC_FILES := \ cpu/br28/audio_dec/audio_dec_pcm.c \ cpu/br28/audio_dec/audio_usb_mic.c \ cpu/br28/audio_dec_eff.c \ - cpu/br28/audio_demo/audio_adc_demo.c \ - cpu/br28/audio_demo/audio_dac_demo.c \ - cpu/br28/audio_demo/audio_fft_demo.c \ - cpu/br28/audio_demo/audio_matrix_demo.c \ - cpu/br28/audio_demo/audio_wind_detect_demo.c \ - cpu/br28/audio_effect_develop.c \ cpu/br28/audio_enc.c \ cpu/br28/audio_general.c \ - cpu/br28/audio_hearing/audio_hearing_aid.c \ - cpu/br28/audio_hearing/audio_hearing_aid_lp.c \ - cpu/br28/audio_hearing/audio_sidetone.c \ cpu/br28/audio_link.c \ cpu/br28/audio_sync.c \ cpu/br28/charge.c \ @@ -573,7 +420,6 @@ c_SRC_FILES := \ cpu/br28/clock_manager.c \ cpu/br28/eq_config.c \ cpu/br28/hw_fft.c \ - cpu/br28/icsd_anc_user.c \ cpu/br28/iic_hw.c \ cpu/br28/iic_soft.c \ cpu/br28/irflt.c \ @@ -594,41 +440,21 @@ c_SRC_FILES := \ cpu/br28/pwm_led.c \ cpu/br28/rdec.c \ cpu/br28/setup.c \ - cpu/br28/smart_voice/aispeech_asr.c \ - cpu/br28/smart_voice/jl_kws_platform.c \ - cpu/br28/smart_voice/kws_event.c \ - cpu/br28/smart_voice/nn_vad.c \ - cpu/br28/smart_voice/smart_voice_config.c \ - cpu/br28/smart_voice/smart_voice_core.c \ - cpu/br28/smart_voice/user_asr.c \ - cpu/br28/smart_voice/vad_clock_trim.c \ - cpu/br28/smart_voice/vad_mic.c \ - cpu/br28/smart_voice/voice_mic_data.c \ cpu/br28/sound_device.c \ - cpu/br28/spatial_effect/spatial_effect.c \ - cpu/br28/spatial_effect/spatial_effect_imu.c \ - cpu/br28/spatial_effect/spatial_effect_test.c \ - cpu/br28/spatial_effect/spatial_effect_tws.c \ - cpu/br28/spatial_effect/spatial_imu_trim.c \ cpu/br28/spi.c \ cpu/br28/tone_player.c \ cpu/br28/tws_audio.c \ cpu/br28/uart_dev.c \ - cpu/br28/umidigi_chargestore.c \ - apps/usr_jb_proto/Protocol/jb_product.c \ - apps/usr_jb_proto/Protocol/jb_protocol.c \ - apps/usr_jb_proto/Utils/jb_common.c \ - apps/usr_jb_proto/Utils/jb_ringbuffer.c \ - apps/usr_jb_proto/usr_jb_main.c \ - apps/usr_periph/usr_rtc.c \ - - -# usr_le_code 预编译静态库(Release 不附带协议源码,禁止本地重建) -USR_LE_LIB := apps/usr_le_code/lib/libusr_le_code.a - - - -# 需要编译的 .S 文件 + cpu/br28/uart_test.c \ + apps/usr_periph/bsp_ct1642.c \ + apps/usr_periph/bsp_aht20.c \ + apps/usr_app/app_fan.c \ + apps/usr_app/app_disp.c \ + apps/usr_app/app_th.c \ + apps/usr_app/app_cond.c \ + apps/usr_app/app_timer_task.c \ + apps/usr_app/app_arbiter.c \ + apps/usr_app/app_nvm.c S_SRC_FILES := \ apps/earphone/sdk_version.z.S \ @@ -646,7 +472,6 @@ cc_SRC_FILES := # 需要编译的 .cxx 文件 -cxx_SRC_FILES := # 链接参数 @@ -863,4 +688,5 @@ $(BUILD_DIR)/%.cc.o : %.cc $(QUITE) $(CXX) $(CXXFLAGS) $(CFLAGS) $(DEFINES) $(INCLUDES) -MM -MT $@ $< -o $(@:.o=.d) $(QUITE) $(CXX) $(CXXFLAGS) $(CFLAGS) $(DEFINES) $(INCLUDES) -c $< -o $@ + cpu/br28/uart_test.c \ -include $(DEP_FILES) diff --git a/apps/earphone/app_main.c b/apps/earphone/app_main.c index 4aca9ac..cc77ba4 100644 --- a/apps/earphone/app_main.c +++ b/apps/earphone/app_main.c @@ -326,14 +326,7 @@ void app_main() usr_var.usr_power_charge_flag=2; } - /* - * 上电强制进入配对模式: - * 1) usr_goto_pair=1:后续 usr_ble_adv_init() 走绑定广播(encode4bind),不走重连广播 - * 2) usr_clear_pair_info_noreset():清 VM 中三元组,保证 paired_flag=0 - * 注意:须在 BLE 协议栈调用 usr_ble_adv_init() 之前设置;此处不清复位,避免上电死循环复位 - */ - usr_var.usr_goto_pair = 1; - usr_clear_pair_info_noreset(); + /* 配对只由 app_boot 长按 5s 置 usr_goto_pair;禁止上电清三元组(规格书 5.2 / 14 章) */ ui_manage_init(); ui_update_status(STATUS_POWERON); diff --git a/apps/earphone/board/br28/board_jl701n_demo_cfg.h b/apps/earphone/board/br28/board_jl701n_demo_cfg.h index c1297a6..3fc19d0 100644 --- a/apps/earphone/board/br28/board_jl701n_demo_cfg.h +++ b/apps/earphone/board/br28/board_jl701n_demo_cfg.h @@ -23,7 +23,7 @@ //*********************************************************************************// #define TCFG_UART0_ENABLE ENABLE_THIS_MOUDLE //串口打印模块使能 #define TCFG_UART0_RX_PORT NO_CONFIG_PORT //串口接收脚配置(用于打印可以选择NO_CONFIG_PORT) -#define TCFG_UART0_TX_PORT IO_PORTC_05 //串口发送脚配置 +#define TCFG_UART0_TX_PORT IO_PORTG_06 //串口发送脚配置(PC5 是 CT1642 DATA + AHT20 SDA,调试 UART 必须挪到 PG6) #define TCFG_UART0_BAUDRATE 1000000 //串口波特率配置 //*********************************************************************************// diff --git a/apps/usr_app/app_arbiter.c b/apps/usr_app/app_arbiter.c new file mode 100644 index 0000000..a18f4a4 --- /dev/null +++ b/apps/usr_app/app_arbiter.c @@ -0,0 +1,159 @@ +/****************************************************************************** + * @file app_arbiter.c + * @brief CBR3 模式仲裁器实现(规格书第 12 章:手动 > 定时 > 条件) + * @author cyWu <1917507415@qq.com> + * @date 2026.08.31 + * @version V1.0.0 + * @history + * - V1.0.0, 2026.08.31, cyWu, 首次发布(P2) + * + * @note 本模块是唯一模式占用源,运行在 usr_timer_loop(任务上下文)。 + * 规则落地(规格书 12 章): + * - 手动动作 → manual_intervene():owner=NONE; + * 条件引擎感知后作废本轮(RAM,须回安全再满时长才重新触发); + * 定时引擎感知后本窗剩余时间不自动抢回(等下一窗开始沿)。 + * - 定时窗开始 → timer_take()(定时 > 条件,条件即使满时长也不开); + * - 条件触发 → cond_take()(仅 owner!=TIMER 允许;快照只存第一次); + * - 两路条件后触发覆盖 → 仍走 cond_take(),快照不变(不回栈)。 + ******************************************************************************/ + +#include "app_arbiter.h" +#include "app_mode.h" +#include "app_fan.h" + +#define LOG_TAG_CONST APP +#define LOG_TAG "[ARB]" +#define LOG_INFO_ENABLE +#include "debug.h" + +/** 仲裁器上下文 */ +typedef struct { + ArbOwner_t owner; /**< 当前占用者 */ + uint8_t snap_valid; /**< 条件快照是否有效 */ + uint8_t snap_mode; /**< 快照模式(第一次条件抢权前) */ + uint8_t snap_gear; /**< 快照档位 */ +} AppArbiterCtx_t; + +static AppArbiterCtx_t s_arb; + +/** + * @brief 初始化仲裁器 + * @return 无 + */ +void app_arbiter_init(void) +{ + s_arb.owner = ARB_OWNER_NONE; + s_arb.snap_valid = 0; + s_arb.snap_mode = 0; + s_arb.snap_gear = 0; + log_info("arbiter init\n"); +} + +/** + * @brief 当前模式占用者 + * @return ArbOwner_t + */ +uint8_t app_arbiter_get_owner(void) +{ + return (uint8_t)s_arb.owner; +} + +/** + * @brief 手动动作统一入口:释放一切自动化占用 + * @note 条件/定时引擎下个 tick 查 owner 感知后各自作废;不恢复快照 + * (手动已接管,快照对应的用户状态已过时) + * @return 无 + */ +void app_arbiter_manual_intervene(void) +{ + if (s_arb.owner != ARB_OWNER_NONE) { + log_info("manual intervene, owner %d -> none\n", s_arb.owner); + } + s_arb.owner = ARB_OWNER_NONE; +} + +/** + * @brief 条件引擎尝试占用 + * @return 1=成功;0=定时占用中(规格书 12:定时 vs 条件 → 定时胜) + */ +uint8_t app_arbiter_cond_take(void) +{ + if (s_arb.owner == ARB_OWNER_TIMER) { + return 0; /* 定时窗内条件不触发 */ + } + if (!s_arb.snap_valid) { + /* 第一次条件抢权前快照用户状态;后覆盖/多次触发不改快照(10.3-6) */ + s_arb.snap_mode = app_mode_get_active(); + s_arb.snap_gear = app_fan_get_gear(); + s_arb.snap_valid = 1; + log_info("cond take, snapshot mode=%d gear=%d\n", s_arb.snap_mode, s_arb.snap_gear); + } + s_arb.owner = ARB_OWNER_COND; + return 1; +} + +/** + * @brief 条件是否仍在占用 + * @return 1=owner==COND + */ +uint8_t app_arbiter_cond_active(void) +{ + return (s_arb.owner == ARB_OWNER_COND) ? 1 : 0; +} + +/** + * @brief 条件自然失效:恢复快照后调用(清 owner 与快照) + * @return 无 + */ +void app_arbiter_cond_release(void) +{ + if (s_arb.owner == ARB_OWNER_COND) { + s_arb.owner = ARB_OWNER_NONE; + } + s_arb.snap_valid = 0; +} + +/** + * @brief 取条件快照 + * @param mode 输出模式 + * @param gear 输出档位 + * @return 无 + */ +void app_arbiter_cond_get_snapshot(uint8_t *mode, uint8_t *gear) +{ + *mode = s_arb.snap_mode; + *gear = s_arb.snap_gear; +} + +/** + * @brief 条件被手动打断:只清快照(不恢复、不清 owner) + * @return 无 + */ +void app_arbiter_cond_snapshot_clear(void) +{ + s_arb.snap_valid = 0; +} + +/** + * @brief 定时窗开始占用(优先级高于条件) + * @return 1=成功 + */ +uint8_t app_arbiter_timer_take(void) +{ + if (s_arb.owner != ARB_OWNER_TIMER) { + log_info("timer take\n"); + } + s_arb.owner = ARB_OWNER_TIMER; + return 1; +} + +/** + * @brief 定时窗结束释放(调用方负责收口:风扇 0、mode 0) + * @return 无 + */ +void app_arbiter_timer_release(void) +{ + if (s_arb.owner == ARB_OWNER_TIMER) { + s_arb.owner = ARB_OWNER_NONE; + } +} diff --git a/apps/usr_app/app_arbiter.h b/apps/usr_app/app_arbiter.h new file mode 100644 index 0000000..fa248ca --- /dev/null +++ b/apps/usr_app/app_arbiter.h @@ -0,0 +1,105 @@ +/****************************************************************************** + * @file app_arbiter.h + * @brief CBR3 模式仲裁器(规格书第 12 章):手动 > 定时 > 条件 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.31 + * @version V1.0.0 + * @history + * - V1.0.0, 2026.08.31, cyWu, 首次发布(P2) + * + * @note 把"谁在驱动模式"收成一处,避免 mode/cond/timer 互调成环。 + * 唯一状态源是 owner(NONE/COND/TIMER)+ 条件快照; + * 条件/定时引擎各自维护占用细节,通过查询 owner 感知被打断: + * - 手动动作(短按/APP 写档/mode)→ app_arbiter_manual_intervene() + * 清 owner;条件引擎感知后作废本轮(RAM),定时引擎感知后本窗 + * 不再抢回(规格书 12 章规则 1/2)。 + * - 条件自然失效(比较不成立)→ 条件引擎恢复快照后 + * app_arbiter_cond_release()。 + * - 定时窗结束 → app_arbiter_timer_release() + 收口。 + * 快照:第一次条件抢权前保存用户 mode+gear(规格书 10.3-3), + * 后覆盖/手动介入均不更新它(10.3-6),只在条件自然失效恢复后清空。 + ******************************************************************************/ + +#ifndef __APP_ARBITER_H__ +#define __APP_ARBITER_H__ + +#include + +/** 当前模式占用者 */ +typedef enum { + ARB_OWNER_NONE = 0, /**< 空闲 / 手动接管 */ + ARB_OWNER_COND = 1, /**< 条件引擎占用 */ + ARB_OWNER_TIMER = 2 /**< 定时引擎占用 */ +} ArbOwner_t; + +/** + * @brief 初始化仲裁器 + * @return 无 + */ +void app_arbiter_init(void); + +/** + * @brief 当前模式占用者 + * @return ArbOwner_t + */ +uint8_t app_arbiter_get_owner(void); + +/** + * @brief 手动动作统一入口(短按 / APP 写 fan_gear / APP 写 mode) + * @note 立即释放一切自动化占用;条件/定时引擎下个 tick 感知后各自作废 + * @return 无 + */ +void app_arbiter_manual_intervene(void); + +/* ═══ 条件引擎接口 ═══ */ + +/** + * @brief 条件引擎尝试占用(触发模式前调用) + * @note 定时占用中返回 0(规格书 12:定时窗 vs 条件 → 定时胜,条件不开); + * 占用成功时若尚无快照则保存当前用户 mode+gear(仅第一次) + * @return 1=占用成功,0=被定时占用拒绝 + */ +uint8_t app_arbiter_cond_take(void); + +/** + * @brief 条件是否仍在占用 + * @return 1=owner==COND + */ +uint8_t app_arbiter_cond_active(void); + +/** + * @brief 条件自然失效:恢复快照后调用(清 owner 与快照) + * @return 无 + */ +void app_arbiter_cond_release(void); + +/** + * @brief 取条件快照(恢复用) + * @param mode 输出快照模式 + * @param gear 输出快照档位 + * @return 无 + */ +void app_arbiter_cond_get_snapshot(uint8_t *mode, uint8_t *gear); + +/** + * @brief 条件被手动打断:只清快照(快照对应的用户状态已过时), + * 不恢复、不清 owner(owner 已被 manual_intervene 清掉) + * @return 无 + */ +void app_arbiter_cond_snapshot_clear(void); + +/* ═══ 定时引擎接口 ═══ */ + +/** + * @brief 定时引擎窗开始占用 + * @return 1=成功(恒成功,定时优先级高于条件) + */ +uint8_t app_arbiter_timer_take(void); + +/** + * @brief 定时窗结束释放 + * @return 无 + */ +void app_arbiter_timer_release(void); + +#endif /* __APP_ARBITER_H__ */ diff --git a/apps/usr_app/app_boot.c b/apps/usr_app/app_boot.c new file mode 100644 index 0000000..41826bc --- /dev/null +++ b/apps/usr_app/app_boot.c @@ -0,0 +1,132 @@ +/****************************************************************************** + * @file app_boot.c + * @brief CBR3 上电按键判定实现(3s 开机 / 5s 配对 / 12s OTA) + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V1.0.0 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹:删 USB 直通、删低电拒绝) + ******************************************************************************/ + +#include "app_boot.h" +#include "app_pair.h" +#include "bsp_hw.h" +#include "board_pin.h" +#include "app_main.h" +#include "usr_le_api.h" +#include "led_manager.h" +#include "asm/power/power_api.h" +#include "asm/power/power_reset.h" + +#define LOG_TAG_CONST APP +#define LOG_TAG "[APP_BOOT]" +#define LOG_INFO_ENABLE +#include "debug.h" + +extern void clr_wdt(void); +extern u32 timer_get_ms(void); + +/** + * @brief 忙等若干毫秒,并喂狗 + * @param ms 等待时长 + * @return 无 + */ +static void boot_delay_ms(uint32_t ms) +{ + uint32_t t0 = timer_get_ms(); + + while ((timer_get_ms() - t0) < ms) { + clr_wdt(); + } +} + +/** + * @brief BLE 起来前进深睡(业务定时器尚未启动),原地喂狗不返回 + * @note power_set_soft_poweroff() 只是向 SDK 底层发起软关机请求(置个标志位), + * 不是同步断电——真正切电源是底层电源管理在之后某个时机才执行的。 + * 此时 BLE 栈、业务定时器都还没起来,如果直接 return,代码会接着把 + * usr_jb_init 剩下的初始化跑完,等于白判定。所以必须用 while(1) 卡住: + * 一边喂狗防止看门狗打断,一边等底层真正把电断掉。 + * @return 无(不会返回) + */ +static void boot_sleep(void) +{ + log_info("boot sleep\n"); + bsp_hw_enter_sleep_io(); + power_set_soft_poweroff(); + while (1) { + clr_wdt(); + } +} + +/** + * @brief 等待并判定开机按键 + * @note 配对(5s)不在到点时立即提交:继续按住观察是否会按到 12s 触发 OTA, + * 真正的配对提交放在松手那一刻做。按满 5s 灯先切到配对快闪提示。 + * 3s 常亮 / 5s 快闪直接复用 led_manager 的 LED_EVENT_STANDBY / + * LED_EVENT_PAIRING 事件。LED 事件库由调用方(usr_jb_init)在本函数 + * 之前完成初始化,这里直接假定可用。 + * @return 无(深睡分支不返回) + */ +void app_boot_wait_key(void) +{ + uint32_t t0; + uint32_t elapsed; + uint8_t pair_armed = 0; + + usr_var.usr_goto_pair = 0; + + /* ⚠ 无 USB 直通:插电/冷启动一律走按键判定(规格书 5.1)。 */ + + if (!bsp_key_is_pressed()) { + log_info("boot no key, sleep\n"); + boot_sleep(); + } + + t0 = timer_get_ms(); + log_info("boot key hold, wait 3s on / 5s pair / 12s ota\n"); + + while (bsp_key_is_pressed()) { + elapsed = timer_get_ms() - t0; + clr_wdt(); + + /* 12s 优先于配对:按满 12s 直接进 OTA,不再等松手 */ + if (elapsed >= BOARD_KEY_OTA_MS) { + log_info("boot hold %ums, power on + ota\n", (unsigned)elapsed); + usr_app_ota_init(); + return; + } + + /* 5s 只武装配对并切到快闪,真正提交放到松手那一刻 */ + if (!pair_armed && (elapsed >= BOARD_KEY_PAIR_MS)) { + pair_armed = 1; + LED_EventDelete(LED_EVENT_STANDBY); + LED_EventAdd(LED_EVENT_PAIRING); + log_info("boot hold %ums, pair hint blink (release now for pair, " + "keep holding to %ums for ota)\n", + (unsigned)elapsed, (unsigned)BOARD_KEY_OTA_MS); + } else if (!pair_armed && !LED_IsEventExist(LED_EVENT_STANDBY) + && (elapsed >= BOARD_KEY_POWER_MS)) { + LED_EventAdd(LED_EVENT_STANDBY); + log_info("boot hold 3s, will power on\n"); + } + + user_led_handle(); /* 按事件表当前状态刷新一次灯 */ + } + + elapsed = timer_get_ms() - t0; + if (elapsed < BOARD_KEY_POWER_MS) { + log_info("boot hold %ums < 3s, sleep\n", (unsigned)elapsed); + boot_sleep(); + } + + /* 松手时已过 5s(且没按到 12s):提交配对;否则只是正常开机 */ + if (pair_armed) { + usr_var.usr_goto_pair = 1; + usr_clear_pair_info_noreset(); + app_pair_start(); + log_info("boot hold %ums, power on + pair\n", (unsigned)elapsed); + } else { + log_info("boot hold %ums, power on\n", (unsigned)elapsed); + } +} diff --git a/apps/usr_app/app_boot.h b/apps/usr_app/app_boot.h new file mode 100644 index 0000000..5dc180e --- /dev/null +++ b/apps/usr_app/app_boot.h @@ -0,0 +1,28 @@ +/****************************************************************************** + * @file app_boot.h + * @brief CBR3 上电按键判定:3s 开机,5s 配对,12s OTA,不足 3s 松手再睡 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V1.0.0 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹:删 USB 直通开机、删低电拒绝开机) + * + * @note 必须在 BLE 协议栈起来、业务定时器启动之前调用。杰理 jiffies 此时 + * 往往 10ms 才跳一次,所以内部一律用 timer_get_ms 直接量时间。 + * ⚠ 本产品 USB 常电、无电池:插电/冷启动【不会】自动进入工作态, + * 必须按住 3s(规格书 5.1/5.2)。门夹的 USB 直通开机逻辑禁止移植。 + ******************************************************************************/ + +#ifndef __APP_BOOT_H__ +#define __APP_BOOT_H__ + +/** + * @brief 等待并判定开机按键:不足 3s 松手、或全程未按键,直接原地深睡(不返回); + * 满 3s 视为正常开机;按满 5s 绿灯改快闪提示"松手即配网",真正清绑定 / + * 开绑定广播仍等松手(内部调用 app_pair_start);持续按满 12s 直接开机 + * 并进入 OTA(调用 usr_app_ota_init)。 + * @return 无(深睡分支通过 while(1) 喂狗不返回;其余情况正常返回) + */ +void app_boot_wait_key(void); + +#endif /* __APP_BOOT_H__ */ diff --git a/apps/usr_app/app_cond.c b/apps/usr_app/app_cond.c new file mode 100644 index 0000000..788e9d4 --- /dev/null +++ b/apps/usr_app/app_cond.c @@ -0,0 +1,464 @@ +/****************************************************************************** + * @file app_cond.c + * @brief CBR3 条件引擎(P2):7 组 7 字节条件,比较成立满时长触发模式 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V2.0.0 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布(P0 stub) + * - V1.1.0, 2026.08.31, cyWu, P1:set_en/set_config 存 VM(引擎仍关) + * - V2.0.0, 2026.08.31, cyWu, P2 实现完整条件引擎(规格书第 10 章): + * 7 字节解析/AND 比较/时长累计(采样成功才累计)/触发/保持/ + * 条件强力循环/后覆盖不回栈/手动作废(RAM)/快照恢复/闪烁喂真值 + * + * @note 运行上下文:app_cond_run 由 usr_timer_loop(500ms,任务上下文)调用 + * (规格书 8.4 时长按采样周期 2s 计,500ms 粒度足够;且触发时可能 + * 要写 VM——单次定时在 app_timer_task,条件本身不写 VM,但保持 + * 任务上下文与仲裁器同侧,避免跨上下文竞态)。 + * 字节布局(规格书 10.2):Byte0=使能 Byte1-2=温度阈值(int16 大端×10) + * Byte3=湿度 Byte4=持续分钟(0=立即) Byte5=动作模式 + * Byte6=Bit7-5 温度比较 / Bit4-2 湿度比较 / Bit1-0 保留。 + * 比较编码:1=< 2=<= 3-== 4->= 5-> 0/6/7=无条件。 + ******************************************************************************/ + +#include "app_cond.h" +#include "app_arbiter.h" +#include "app_mode.h" +#include "app_fan.h" +#include "app_th.h" +#include "app_disp.h" +#include "app_nvm.h" +#include "jb_product.h" +#include "board_pin.h" + +#define LOG_TAG_CONST APP +#define LOG_TAG "[APP_COND]" +#define LOG_INFO_ENABLE +#include "debug.h" + +/** 每组运行时状态(配置本体在 gDevData.cond_config1~7) */ +typedef struct { + uint32_t acc_ms; /**< 比较成立且采样成功的累计毫秒(未满时长) */ + uint8_t voided; /**< 1=被手动作废(RAM,不写 VM,须回安全再满时长) */ + uint8_t trig_by; /**< 触发占用时 Byte5 动作(0=未在占用) */ +} CondState_t; + +static CondState_t s_cond[7]; /**< [0]~[6] 对应 config1~7 */ +static uint8_t s_occupy_idx;/**< 当前占用组 0~6,0xFF=无 */ + +/** + * @brief 比较编码是否有效(1~5;0/6/7 视为无条件) + * @param cmp 3bit 比较编码 + * @return 1=有效,0=无条件 + */ +static uint8_t cond_cmp_valid(uint8_t cmp) +{ + return ((cmp >= 1) && (cmp <= 5)) ? 1 : 0; +} + +/** + * @brief 温度比较(当前温度 ×10 vs 阈值 ×10) + * @param cmp 比较编码 + * @param th 温度阈值 ×10 + * @return 1=成立 + */ +static uint8_t cond_cmp_temp(uint8_t cmp, int16_t th) +{ + int16_t cur = app_th_get_temp_x10(); + + switch (cmp) { + case 1: return (cur < th) ? 1 : 0; + case 2: return (cur <= th) ? 1 : 0; + case 3: return (cur == th) ? 1 : 0; + case 4: return (cur >= th) ? 1 : 0; + case 5: return (cur > th) ? 1 : 0; + default: return 1; /* 无条件 */ + } +} + +/** + * @brief 湿度比较(当前 % vs 阈值 %) + * @param cmp 比较编码 + * @param th 湿度阈值 % + * @return 1=成立 + */ +static uint8_t cond_cmp_humi(uint8_t cmp, uint8_t th) +{ + uint8_t cur = app_th_get_humi(); + + switch (cmp) { + case 1: return (cur < th) ? 1 : 0; + case 2: return (cur <= th) ? 1 : 0; + case 3: return (cur == th) ? 1 : 0; + case 4: return (cur >= th) ? 1 : 0; + case 5: return (cur > th) ? 1 : 0; + default: return 1; /* 无条件 */ + } +} + +/** + * @brief 取第 idx 组条件配置(gDevData 里是 7 个独立数组字段) + * @param idx 0~6 + * @return 配置指针;越界返回 NULL + */ +static const uint8_t *cond_cfg_ptr(uint8_t idx) +{ + const uint8_t *tbl[7]; + + tbl[0] = gDevData.cond_config1; + tbl[1] = gDevData.cond_config2; + tbl[2] = gDevData.cond_config3; + tbl[3] = gDevData.cond_config4; + tbl[4] = gDevData.cond_config5; + tbl[5] = gDevData.cond_config6; + tbl[6] = gDevData.cond_config7; + return (idx < 7) ? tbl[idx] : NULL; +} + +/** + * @brief 整组比较是否成立(两侧 AND / 单侧 / 双侧无条件=无效组) + * @param cfg 7 字节配置 + * @return 1=成立,0=不成立或无效组 + */ +static uint8_t cond_group_match(const uint8_t *cfg) +{ + uint8_t temp_cmp = (uint8_t)((cfg[6] >> 5) & 0x7); + uint8_t humi_cmp = (uint8_t)((cfg[6] >> 2) & 0x7); + uint8_t temp_ok = cond_cmp_valid(temp_cmp); + uint8_t humi_ok = cond_cmp_valid(humi_cmp); + int16_t temp_th = (int16_t)((cfg[1] << 8) | cfg[2]); /* int16 大端 ×10 */ + uint8_t humi_th = cfg[3]; + + /* 规格书 8.4:-- 或尚无有效采样时比较视为不成立,避免用旧值误触发 */ + if (app_th_is_fault() || !app_th_get_valid()) { + return 0; + } + if (!temp_ok && !humi_ok) { + return 0; /* 两侧都无条件:无效组,不触发(10.2) */ + } + if (temp_ok && humi_ok) { + return (cond_cmp_temp(temp_cmp, temp_th) && cond_cmp_humi(humi_cmp, humi_th)) ? 1 : 0; + } + if (temp_ok) { + return cond_cmp_temp(temp_cmp, temp_th); + } + return cond_cmp_humi(humi_cmp, humi_th); +} + +/** + * @brief 触发时长(毫秒);Byte4=0 立即触发 + * @param cfg 7 字节配置 + * @return 时长 ms + */ +static uint32_t cond_duration_ms(const uint8_t *cfg) +{ + return (uint32_t)cfg[4] * 60000u; +} + +/** + * @brief 清全部组累计(定时窗抢权后须重新满时长,规格书 12 章) + * @return 无 + */ +static void cond_reset_acc_all(void) +{ + uint8_t i; + + for (i = 0; i < 7; i++) { + s_cond[i].acc_ms = 0; + s_cond[i].trig_by = 0; + } +} + +/** + * @brief 条件动作切模式。Byte5=0 是关风扇,不能走 set_manual(0)(那会恢复旧档) + * @param mode 0=关闭,1=通风,2=强力 + * @return 无 + */ +static void cond_apply_mode(uint8_t mode) +{ + if (mode == 0) { + app_mode_stop(); + app_fan_set_gear(0); + } else { + app_mode_set_manual(mode); + } +} + +/** + * @brief 恢复快照并释放条件占用(比较不成立时,10.3-5) + * @return 无 + */ +static void cond_restore_and_release(void) +{ + uint8_t idx = s_occupy_idx; + uint8_t m, g; + + app_arbiter_cond_get_snapshot(&m, &g); + app_mode_set_manual(m); /* 恢复快照模式(mode=0 时不动档位) */ + app_fan_set_gear(g); /* 恢复快照档位 */ + app_arbiter_cond_release(); + s_cond[idx].acc_ms = 0; + s_cond[idx].trig_by = 0; + s_occupy_idx = 0xFF; + log_info("cond[%d] release, restore mode=%d gear=%d\n", idx + 1, m, g); +} + +/** + * @brief 一组触发(占用 + 快照 + 切模式) + * @param idx 组索引 0~6 + * @return 1=已触发,0=被定时占用拒绝 + */ +static uint8_t cond_fire(uint8_t idx) +{ + const uint8_t *cfg = cond_cfg_ptr(idx); + + if (!app_arbiter_cond_take()) { + return 0; /* 定时占用中,条件不开(12 章) */ + } + s_occupy_idx = idx; + s_cond[idx].trig_by = cfg[5]; + s_cond[idx].acc_ms = 0; + cond_apply_mode(cfg[5]); + log_info("cond[%d] fire mode=%d\n", idx + 1, cfg[5]); + return 1; +} + +/** + * @brief 扫描阶段:找满时长的组触发(owner==NONE 时) + * @param dt 距上次调用的毫秒数 + * @return 无 + */ +static void cond_scan_trigger(uint16_t dt) +{ + uint8_t idx; + + for (idx = 0; idx < 7; idx++) { + const uint8_t *cfg = cond_cfg_ptr(idx); + uint8_t ok; + + if (!cfg || (cfg[0] == 0)) { + continue; /* 组未使能 */ + } + if (s_cond[idx].voided) { + /* 作废组:等比较回安全(不成立)才解除作废(10.3-7) */ + if (!cond_group_match(cfg)) { + s_cond[idx].voided = 0; + } + continue; + } + + ok = app_th_last_sample_ok(); /* 8.4:只在采样成功时累加 */ + if (cond_group_match(cfg)) { + if (ok) { + s_cond[idx].acc_ms += dt; + if (s_cond[idx].acc_ms >= cond_duration_ms(cfg)) { + if (cond_fire(idx)) { + return; /* 一拍只触发一个,下一拍进保持 */ + } + } + } + /* 采样失败且比较成立:不累加、不清零(8.4) */ + } else { + s_cond[idx].acc_ms = 0; /* 比较不成立:计时清零(10.3-2) */ + } + } +} + +/** + * @brief 保持阶段:扫描其它组后覆盖(10.3-6,不回栈、快照不变) + * @param dt 距上次调用的毫秒数 + * @return 无 + */ +static void cond_scan_override(uint16_t dt) +{ + uint8_t idx; + + for (idx = 0; idx < 7; idx++) { + const uint8_t *cfg = cond_cfg_ptr(idx); + + if (idx == s_occupy_idx) { + continue; + } + if (!cfg || (cfg[0] == 0) || s_cond[idx].voided) { + continue; + } + if (cond_group_match(cfg)) { + if (app_th_last_sample_ok()) { + s_cond[idx].acc_ms += dt; + if (s_cond[idx].acc_ms >= cond_duration_ms(cfg)) { + /* 后触发覆盖当前条件模式,不回栈(D1) */ + s_occupy_idx = idx; + s_cond[idx].trig_by = cfg[5]; + s_cond[idx].acc_ms = 0; + cond_apply_mode(cfg[5]); + log_info("cond[%d] override mode=%d\n", idx + 1, cfg[5]); + return; + } + } + } else { + s_cond[idx].acc_ms = 0; + } + } +} + +/** + * @brief 更新告警闪烁输入(规格书 8.2:任一使能组温度/湿度比较成立 → 对应管闪) + * @note 只看单侧比较成立,不看整组 AND;fault "--" 优先由 app_disp 处理 + * @return 无 + */ +static void cond_update_disp_alarm(void) +{ + uint8_t t_alarm = 0; + uint8_t h_alarm = 0; + uint8_t idx; + + if (app_th_is_fault() || !app_th_get_valid() || !gDevData.cond_en) { + app_disp_set_temp_alarm(0); + app_disp_set_humi_alarm(0); + return; + } + + for (idx = 0; idx < 7; idx++) { + const uint8_t *cfg = cond_cfg_ptr(idx); + + if (!cfg || (cfg[0] == 0) || s_cond[idx].voided) { + continue; + } + { + uint8_t temp_cmp = (uint8_t)((cfg[6] >> 5) & 0x7); + uint8_t humi_cmp = (uint8_t)((cfg[6] >> 2) & 0x7); + int16_t temp_th = (int16_t)((cfg[1] << 8) | cfg[2]); + + if (cond_cmp_valid(temp_cmp) && cond_cmp_temp(temp_cmp, temp_th)) { + t_alarm = 1; + } + if (cond_cmp_valid(humi_cmp) && cond_cmp_humi(humi_cmp, cfg[3])) { + h_alarm = 1; + } + } + } + app_disp_set_temp_alarm(t_alarm); + app_disp_set_humi_alarm(h_alarm); +} + +/** + * @brief 初始化条件引擎 + * @return 无 + */ +void app_cond_init(void) +{ + uint8_t i; + +#if !BOARD_COND_ENABLE + log_info("cond engine disabled\n"); + return; +#endif + for (i = 0; i < 7; i++) { + s_cond[i].acc_ms = 0; + s_cond[i].voided = 0; + s_cond[i].trig_by = 0; + } + s_occupy_idx = 0xFF; + log_info("cond init\n"); +} + +/** + * @brief 推进条件引擎(500ms,任务上下文,由 usr_timer_loop 调用) + * @param dt 距上次调用的毫秒数 + * @return 无 + */ +void app_cond_run(uint16_t dt) +{ +#if !BOARD_COND_ENABLE + (void)dt; + return; +#endif + /* 总开关:cond_en=0 不扫描;若正在跑视为条件结束恢复快照(10.1) */ + if (!gDevData.cond_en) { + if (s_occupy_idx != 0xFF) { + cond_restore_and_release(); + } + app_disp_set_temp_alarm(0); + app_disp_set_humi_alarm(0); + return; + } + + if (s_occupy_idx != 0xFF) { + /* ── 占用保持阶段 ── */ + if (!app_arbiter_cond_active()) { + uint8_t vidx = s_occupy_idx; + + if (app_arbiter_get_owner() == ARB_OWNER_TIMER) { + /* 定时抢走:重新计时长,不作废(规格书 12:窗结束须重新满时长) */ + cond_reset_acc_all(); + s_occupy_idx = 0xFF; + log_info("cond[%d] preempted by timer, re-time\n", vidx + 1); + } else { + /* 手动/APP 介入:本轮作废,须回安全再满时长(10.3-7) */ + s_cond[vidx].voided = 1; + app_arbiter_cond_snapshot_clear(); + s_occupy_idx = 0xFF; + log_info("cond[%d] voided by manual\n", vidx + 1); + } + cond_update_disp_alarm(); + return; + } + + /* 其它组满时长 → 后触发覆盖(不回栈) */ + cond_scan_override(dt); + + /* 当前占用组:成立保持 / 强力到点再开一轮 / 不成立恢复快照 */ + { + const uint8_t *cfg = cond_cfg_ptr(s_occupy_idx); + + if (cfg && cond_group_match(cfg)) { + /* 条件强力:15min 到点(mode 已回 0)且仍成立 → 立即再开一轮(10.3-4) */ + if ((s_cond[s_occupy_idx].trig_by == 2) && (app_mode_get_active() == 0)) { + app_mode_set_manual(2); + log_info("cond[%d] power 15min over, restart\n", s_occupy_idx + 1); + } + /* 通风 25/5 循环由 app_mode 自理 */ + } else { + cond_restore_and_release(); /* 比较不成立 → 恢复快照,重新计时 */ + } + } + cond_update_disp_alarm(); + return; + } + + /* ── 空闲扫描触发阶段 ── */ + if (app_arbiter_get_owner() == ARB_OWNER_TIMER) { + /* 定时占用中:条件即使满时长也不开;累计清零,窗结束重新计(12 章) */ + cond_reset_acc_all(); + cond_update_disp_alarm(); + return; + } + cond_scan_trigger(dt); + cond_update_disp_alarm(); +} + +/** + * @brief APP 写 DP5 条件总开关:存 gDevData + 触发 VM 持久化 + * @param en 0=关,1=开 + * @return 无 + */ +void app_cond_set_en(uint8_t en) +{ + app_nvm_mark_dirty(); + log_info("cond_en=%d (stored)\n", en); +} + +/** + * @brief APP 写 DP6~12 条件配置:触发 VM 持久化 + 复位该组计时(15 章) + * @param idx 1~7 + * @note 引擎关闭时仍保存;作废标志不回安全不主动清(10.3-7 语义) + * @return 无 + */ +void app_cond_set_config(uint8_t idx) +{ + if ((idx >= 1) && (idx <= 7)) { + s_cond[idx - 1].acc_ms = 0; /* 条件状态机复位该组计时(15 章) */ + } + app_nvm_mark_dirty(); + log_info("cond_config[%d] stored\n", idx); +} diff --git a/apps/usr_app/app_cond.h b/apps/usr_app/app_cond.h new file mode 100644 index 0000000..f2a0f69 --- /dev/null +++ b/apps/usr_app/app_cond.h @@ -0,0 +1,51 @@ +/****************************************************************************** + * @file app_cond.h + * @brief CBR3 条件引擎(P2):7 组 7 字节条件,比较成立满时长触发模式 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V1.0.0 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布(P0 仅接口与 stub) + * + * @note P2 实现(BOARD_COND_ENABLE=1)。字节布局见规格书 10.2: + * Byte0=使能 Byte1-2=温度阈值(int16 大端 ×10) Byte3=湿度阈值 + * Byte4=持续分钟(0=立即) Byte5=动作模式 Byte6=Bit7-5 温度比较 / + * Bit4-2 湿度比较 / Bit1-0 保留。比较编码:1=< 2=<= 3-== 4->= 5-> + * 0/6/7=无条件;两侧都无条件=无效组;两侧有效=AND。 + * ⚠ app_cond_run 由 usr_timer_loop(500ms,任务上下文)调用, + * 与仲裁器/定时引擎同侧;不放在 20ms usr_timer_tick(中断上下文)。 + ******************************************************************************/ + +#ifndef __APP_COND_H__ +#define __APP_COND_H__ + +#include + +/** + * @brief 初始化条件引擎 + * @return 无 + */ +void app_cond_init(void); + +/** + * @brief 推进条件引擎,每个业务 tick 调用一次 + * @param dt 距上次调用的毫秒数 + * @return 无 + */ +void app_cond_run(uint16_t dt); + +/** + * @brief APP 写 DP5 条件总开关 + * @param en 0=关,1=开 + * @return 无 + */ +void app_cond_set_en(uint8_t en); + +/** + * @brief APP 写 DP6~12 条件配置(7 字节) + * @param idx 1~7 + * @return 无 + */ +void app_cond_set_config(uint8_t idx); + +#endif /* __APP_COND_H__ */ diff --git a/apps/usr_app/app_disp.c b/apps/usr_app/app_disp.c new file mode 100644 index 0000000..aa12dcd --- /dev/null +++ b/apps/usr_app/app_disp.c @@ -0,0 +1,278 @@ +/****************************************************************************** + * @file app_disp.c + * @brief CBR3 数码管显示实现 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V1.1.0 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布 + * - V1.0.1, 2026.08.29, cyWu, 改动态扫描:20ms 节拍只算数据写缓冲, + * 另起 BOARD_DISP_SCAN_MS 快节拍定时器轮询点亮 1 位,解决闪烁 + * - V1.1.0, 2026.08.31, cyWu, P1 加告警闪烁:温度/湿度管独立 500ms 亮灭, + * BOARD_DISP_ALARM_TEST 宏可强制模拟超限;fault 时 "--" 优先 + * + * @note ⚠ CT1642 内部只有单个 12 位锁存器(4 位选 + 8 段码),无按位显示 RAM、 + * 不自动扫描,一次只点亮当前锁存的那 1 位。要让 4 位都亮且不闪,必须由 MCU + * 持续轮询:本模块用 app_disp_run()(20ms)算好 4 位段码放进 s_disp_buf, + * 用 app_disp_scan()(BOARD_DISP_SCAN_MS)每次点亮 1 位、4 位轮询。 + * 告警闪烁(规格书 8.2):比较成立侧 500ms 亮/500ms 灭,与吹风解耦; + * 闪烁触发源由 app_disp_set_temp/humi_alarm 喂入(P2 条件引擎), + * 传感器故障(连续 3 次失败)优先显示 "--" 不闪。 + ******************************************************************************/ + +#include "app_disp.h" +#include "app_th.h" +#include "app_power.h" +#include "bsp_ct1642.h" +#include "board_pin.h" + +#define LOG_TAG_CONST APP +#define LOG_TAG "[APP_DISP]" +#define LOG_INFO_ENABLE +#include "debug.h" + +static uint8_t s_inited; + +/* 4 位段码缓冲,下标顺序 = com 顺序:0 温度十 / 1 温度个 / 2 湿度十 / 3 湿度个 */ +static uint8_t s_disp_buf[4]; +static const uint8_t s_com_tbl[4] = { + BOARD_DISP_COM_TEMP_TEN, BOARD_DISP_COM_TEMP_ONE, + BOARD_DISP_COM_HUMI_TEN, BOARD_DISP_COM_HUMI_ONE +}; +static uint8_t s_scan_idx; /* 动态扫描轮询指针 0..3 */ + +/* 告警闪烁(P1,规格书 8.2):比较成立侧 500ms 亮/500ms 灭 */ +static uint8_t s_temp_alarm; /* 1=温度比较成立(P2 条件引擎喂) */ +static uint8_t s_humi_alarm; /* 1=湿度比较成立(P2 条件引擎喂) */ +static uint8_t s_blink_phase; /* 0=亮半周期,1=灭半周期 */ +static uint16_t s_blink_ms; /* 半周期内累计毫秒 */ + +/* 段码索引:bsp_ct1642_seg(0~9)=数字,10='-',11=灭 */ +#define SEG_MINUS 10 +#define SEG_OFF 11 + +/** + * @brief 温度值 → 两位段码(0~99;<0 显示 -9~-1;<-9 钳位 -9;>99 钳位 99) + * @param temp_x10 温度 ×10 + * @param out_t1 十位段码索引 + * @param out_t0 个位段码索引 + * @return 无 + */ +static void disp_split_temp(int16_t temp_x10, uint8_t *out_t1, uint8_t *out_t0) +{ + int16_t t = temp_x10 / 10; /* 取整数 ℃(P0 不显示小数) */ + uint8_t tens; + uint8_t ones; + + if (t < 0) { + if (t < -9) { + t = -9; /* 钳位 */ + } + *out_t1 = SEG_MINUS; + *out_t0 = (uint8_t)(-t % 10); + return; + } + if (t > 99) { + t = 99; + } + tens = (uint8_t)(t / 10); + ones = (uint8_t)(t % 10); + *out_t1 = tens; + *out_t0 = ones; +} + +/** + * @brief 湿度值 → 两位段码(0~99;>99 钳位 99) + * @param humi 湿度 % + * @param out_h1 十位段码索引 + * @param out_h0 个位段码索引 + * @return 无 + */ +static void disp_split_humi(uint8_t humi, uint8_t *out_h1, uint8_t *out_h0) +{ + uint8_t h = humi; + + if (h > 99) { + h = 99; + } + *out_h1 = (uint8_t)(h / 10); + *out_h0 = (uint8_t)(h % 10); +} + +/** + * @brief 初始化显示模块;注册快节拍动态扫描定时器 + * @return 无 + */ +void app_disp_init(void) +{ +#if !BOARD_DISP_ENABLE + log_info("disp disabled\n"); + return; +#endif + if (s_inited) { + return; + } + s_inited = 1; + s_scan_idx = 0; + bsp_ct1642_init(); /* PG0(CLK)/PG1(DATA) 推挽输出 */ + /* 默认写死 25 / 60,供 P0 台上扫段验证(规格书 P0 验收) */ + s_disp_buf[0] = bsp_ct1642_seg(2); + s_disp_buf[1] = bsp_ct1642_seg(5); + s_disp_buf[2] = bsp_ct1642_seg(6); + s_disp_buf[3] = bsp_ct1642_seg(0); + log_info("disp init, scan %ums\n", (unsigned)BOARD_DISP_SCAN_MS); + /* 注意:快节拍动态扫描定时器在 usr_jb_init 里注册(usr_timer_add 声明在那) */ +} + +/** + * @brief 业务节拍(20ms):计算 4 位要显示什么,写入段码缓冲(不直接打管) + * @note 软关机由 app_disp_scan 处理。本函数只更新 s_disp_buf。 + * @return 无 + */ +void app_disp_run(uint16_t dt) +{ +#if !BOARD_DISP_ENABLE + (void)dt; + return; +#endif + if (!s_inited) { + return; + } + + /* 闪烁相位推进:满 BOARD_DISP_BLINK_MS 切换亮/灭半周期 */ + s_blink_ms = (uint16_t)(s_blink_ms + dt); + if (s_blink_ms >= BOARD_DISP_BLINK_MS) { + s_blink_ms = 0; + s_blink_phase ^= 1; + } + +#if BOARD_DISP_ALARM_TEST == 1 + s_temp_alarm = 1; /* P1 调试:强制温度管闪 */ +#elif BOARD_DISP_ALARM_TEST == 2 + s_humi_alarm = 1; /* P1 调试:强制湿度管闪 */ +#endif + +#if BOARD_DISP_SCAN_TEST == 1 + /* 段扫描:4 位同时点亮同一段,每 600ms 切 A→B→...→G→灭→全亮,定性段码位序 */ + { + static uint32_t tick; + static uint8_t step; + static const uint8_t seg_seq[] = { + 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x00, 0x7F + }; + if (++tick >= 30) { /* 30*20ms=600ms */ + tick = 0; + step = (uint8_t)((step + 1) % (sizeof(seg_seq) / sizeof(seg_seq[0]))); + } + uint8_t s = seg_seq[step]; + s_disp_buf[0] = s; s_disp_buf[1] = s; s_disp_buf[2] = s; s_disp_buf[3] = s; + return; + } +#elif BOARD_DISP_SCAN_TEST == 2 + /* 位扫描:4 位分别显示 1/2/3/4,定性 C1~C4 与物理管位对应 */ + s_disp_buf[0] = bsp_ct1642_seg(1); + s_disp_buf[1] = bsp_ct1642_seg(2); + s_disp_buf[2] = bsp_ct1642_seg(3); + s_disp_buf[3] = bsp_ct1642_seg(4); + return; +#endif + + if (app_th_is_fault()) { + /* 连续失败 3 次:双管 "--"(规格书 8.1;故障优先于闪烁,8.2) */ + uint8_t m = bsp_ct1642_seg(SEG_MINUS); + s_disp_buf[0] = m; s_disp_buf[1] = m; s_disp_buf[2] = m; s_disp_buf[3] = m; + } else if (app_th_get_valid()) { + /* 有有效采样:显示真实值;比较成立且当前在灭半周期 → 该管熄灭 */ + uint8_t t1, t0, h1, h0; + disp_split_temp(app_th_get_temp_x10(), &t1, &t0); + disp_split_humi(app_th_get_humi(), &h1, &h0); + if (s_temp_alarm && s_blink_phase) { + t1 = SEG_OFF; + t0 = SEG_OFF; + } + if (s_humi_alarm && s_blink_phase) { + h1 = SEG_OFF; + h0 = SEG_OFF; + } + s_disp_buf[0] = bsp_ct1642_seg(t1); + s_disp_buf[1] = bsp_ct1642_seg(t0); + s_disp_buf[2] = bsp_ct1642_seg(h1); + s_disp_buf[3] = bsp_ct1642_seg(h0); + } else { + /* 尚未有有效采样:写死 25 / 60,供 P0 台上扫段验证(规格书 P0 验收) */ + s_disp_buf[0] = bsp_ct1642_seg(2); + s_disp_buf[1] = bsp_ct1642_seg(5); + s_disp_buf[2] = bsp_ct1642_seg(6); + s_disp_buf[3] = bsp_ct1642_seg(0); + } +} + +/** + * @brief 显示动态扫描(BOARD_DISP_SCAN_MS 快节拍定时器回调):点亮 1 位、4 位轮询 + * @note 必须短、不可阻塞。每次只发 1 帧,靠高频轮询让 4 位视觉同亮不闪。 + * IO 分离后 CT1642 独占 PG0/PG1,与 AHT20 的 PC4/PC5 无总线竞争, + * 扫描回调无需锁、无跳过。 + * 软关机流程:4 位全灭(com=0x00 → 所有 Cx 高,位选全关)。 + * @return 无 + */ +void app_disp_scan(void) +{ +#if !BOARD_DISP_ENABLE + return; +#endif + if (!s_inited) { + return; + } + + /* 软关机流程:数码管锁存灭(规格书 5.1:风扇 0、数码管锁存灭、LED 灭) */ + if (app_power_poweroff_pending()) { + bsp_ct1642_send_frame(0x00, 0x00); /* com=0x00 → 4 位全灭 */ + return; + } + +#if BOARD_DISP_SCAN_TEST == 1 + /* 段扫描:4 位同显一段。⭐ 值变化才刷(无鬼影),com=0xF 四位全选同显。 */ + static uint8_t seg = 0; + if (seg == s_disp_buf[0]) { + return; + } + seg = s_disp_buf[0]; + + bsp_ct1642_send_frame(s_disp_buf[s_scan_idx], 0xF); + s_scan_idx = (uint8_t)((s_scan_idx + 1) % 4); +#else + /* 正常显示(0) / 位选验证(2):4 位分显不同数字,必须分时轮询。 + * 鬼影(切位杂段)靠 ct1642_delay 8→2 提速缩短杂段窗口来减轻。 */ + bsp_ct1642_send_frame(s_disp_buf[s_scan_idx], s_com_tbl[s_scan_idx]); + s_scan_idx = (uint8_t)((s_scan_idx + 1) % 4); +#endif +} + +/** + * @brief 设置温度比较成立标志:成立时温度管 500ms 闪烁(P2 条件引擎喂) + * @param en 1=成立(闪),0=不成立(常亮数值) + * @return 无 + */ +void app_disp_set_temp_alarm(uint8_t en) +{ + s_temp_alarm = en ? 1 : 0; +} + +/** + * @brief 设置湿度比较成立标志:成立时湿度管 500ms 闪烁(P2 条件引擎喂) + * @param en 1=成立(闪),0=不成立(常亮数值) + * @return 无 + */ +void app_disp_set_humi_alarm(uint8_t en) +{ + s_humi_alarm = en ? 1 : 0; +} + +/** + * @brief 温度或湿度管是否处于告警闪烁 + * @return 1=任一侧告警 + */ +uint8_t app_disp_is_alarm(void) +{ + return (s_temp_alarm || s_humi_alarm) ? 1 : 0; +} diff --git a/apps/usr_app/app_disp.h b/apps/usr_app/app_disp.h new file mode 100644 index 0000000..283bab8 --- /dev/null +++ b/apps/usr_app/app_disp.h @@ -0,0 +1,68 @@ +/****************************************************************************** + * @file app_disp.h + * @brief CBR3 数码管显示:温度/湿度整数、负号、-- 故障、告警闪烁 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V1.1.0 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布 + * - V1.1.0, 2026.08.31, cyWu, P1 加告警闪烁:温度/湿度管独立 500ms 亮灭, + * 由 app_disp_set_temp/humi_alarm 驱动(P2 条件引擎喂真值, + * P1 可用 BOARD_DISP_ALARM_TEST 宏强制验证) + * + * @note 两块 2 位 7 段数码管:温度管整数 ℃、湿度管整数 %。DP 恒灭。 + * 0~99 显示绝对值;<0 显示 -9~-1(G 段负号);<-9 钳位 -9;>99 钳位 99。 + * 连续失败 3 次双管 "--"(优先于闪烁)。告警闪烁(规格书 8.2) + * 与吹风解耦:比较成立侧 500ms 亮/500ms 灭,不成立侧常亮数值。 + ******************************************************************************/ + +#ifndef __APP_DISP_H__ +#define __APP_DISP_H__ + +#include + +/** + * @brief 初始化显示模块 + * @return 无 + */ +void app_disp_init(void); + +/** + * @brief 业务节拍:计算 4 位要显示什么,写入段码缓冲(不直接打管) + * @param dt 距上次调用的毫秒数(闪烁相位累加) + * @note 软关机由 app_disp_scan 处理;本函数只算数据。 + * @return 无 + */ +void app_disp_run(uint16_t dt); + +/** + * @brief 显示动态扫描(快节拍定时器回调):每次点亮 1 位,4 位轮询刷新 + * @note 由 app_disp_init 注册的 BOARD_DISP_SCAN_MS 定时器调用; + * 必须短、不可阻塞;软关机时 4 位全灭。 + * CT1642 无内部扫描 RAM,靠本函数持续轮询维持 4 位同亮不闪。 + * @return 无 + */ +void app_disp_scan(void); + +/** + * @brief 设置温度比较成立标志:成立时温度管 500ms 闪烁(P2 条件引擎喂) + * @param en 1=成立(闪),0=不成立(常亮数值) + * @return 无 + */ +void app_disp_set_temp_alarm(uint8_t en); + +/** + * @brief 设置湿度比较成立标志:成立时湿度管 500ms 闪烁(P2 条件引擎喂) + * @param en 1=成立(闪),0=不成立(常亮数值) + * @return 无 + */ +void app_disp_set_humi_alarm(uint8_t en); + +/** + * @brief 温度或湿度管是否处于告警闪烁(供 fault_code=1) + * @return 1=任一侧告警,0=无 + */ +uint8_t app_disp_is_alarm(void); + +#endif /* __APP_DISP_H__ */ + diff --git a/apps/usr_app/app_fan.c b/apps/usr_app/app_fan.c new file mode 100644 index 0000000..be73b1f --- /dev/null +++ b/apps/usr_app/app_fan.c @@ -0,0 +1,82 @@ +/****************************************************************************** + * @file app_fan.c + * @brief CBR3 风扇档位实现 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V1.0.0 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布 + ******************************************************************************/ + +#include "app_fan.h" +#include "bsp_hw.h" +#include "board_pin.h" + +#define LOG_TAG_CONST APP +#define LOG_TAG "[APP_FAN]" +#define LOG_INFO_ENABLE +#include "debug.h" + +static uint8_t s_gear; + +/** + * @brief 初始化风扇(档位 0,PWM 关) + * @return 无 + */ +void app_fan_init(void) +{ +#if !BOARD_FAN_ENABLE + log_info("fan disabled\n"); + return; +#endif + s_gear = 0; + bsp_fan_set_duty(BOARD_FAN_DUTY_GEAR0); + log_info("fan init gear=0\n"); +} + +/** + * @brief 设置档位并立即生效(P0 无软爬坡) + * @param gear 0=关,1=50%,2=75%,3=100%;>3 钳位到 3 + * @return 无 + */ +void app_fan_set_gear(uint8_t gear) +{ +#if !BOARD_FAN_ENABLE + (void)gear; + s_gear = 0; + return; +#endif + static const uint16_t duty_tbl[4] = { + BOARD_FAN_DUTY_GEAR0, BOARD_FAN_DUTY_GEAR1, + BOARD_FAN_DUTY_GEAR2, BOARD_FAN_DUTY_GEAR3 + }; + + if (gear > 3) { + gear = 3; + } + if (gear == s_gear) { + return; + } + s_gear = gear; + bsp_fan_set_duty(duty_tbl[gear]); + log_info("fan gear=%d duty=%u/10000\n", gear, (unsigned)duty_tbl[gear]); +} + +/** + * @brief 查询当前真实档位(DP2 必须等于它,通风停段时为 0) + * @return 0~3 + */ +uint8_t app_fan_get_gear(void) +{ + return s_gear; +} + +/** + * @brief 业务节拍(P0 无动作,保留接口供 P1 爬坡) + * @param dt 距上次调用的毫秒数 + * @return 无 + */ +void app_fan_run(uint16_t dt) +{ + (void)dt; +} diff --git a/apps/usr_app/app_fan.h b/apps/usr_app/app_fan.h new file mode 100644 index 0000000..2f4f112 --- /dev/null +++ b/apps/usr_app/app_fan.h @@ -0,0 +1,42 @@ +/****************************************************************************** + * @file app_fan.h + * @brief CBR3 风扇档位:0/1/2/3 → 0%/50%/75%/100% + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V1.0.0 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布 + ******************************************************************************/ + +#ifndef __APP_FAN_H__ +#define __APP_FAN_H__ + +#include + +/** + * @brief 初始化风扇(档位 0,PWM 关) + * @return 无 + */ +void app_fan_init(void); + +/** + * @brief 设置档位并立即生效(P0 无软爬坡) + * @param gear 0=关,1=50%,2=75%,3=100%;>3 钳位到 3 + * @return 无 + */ +void app_fan_set_gear(uint8_t gear); + +/** + * @brief 查询当前真实档位(DP2 必须等于它,通风停段时为 0) + * @return 0~3 + */ +uint8_t app_fan_get_gear(void); + +/** + * @brief 业务节拍(P0 无动作,保留接口供 P1 爬坡) + * @param dt 距上次调用的毫秒数 + * @return 无 + */ +void app_fan_run(uint16_t dt); + +#endif /* __APP_FAN_H__ */ diff --git a/apps/usr_app/app_function.c b/apps/usr_app/app_function.c new file mode 100644 index 0000000..783ec5e --- /dev/null +++ b/apps/usr_app/app_function.c @@ -0,0 +1,220 @@ +/****************************************************************************** + * @file app_function.c + * @brief CBR3 功能总装实现 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V1.1.0 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布 + * - V1.1.0, 2026.08.31, cyWu, 长按关机改为延后到任务上下文执行:按键在 + * usr_timer 20ms(硬件定时器中断上下文)里处理,直接调 + * app_power_request_poweroff 会在中断里跑 VM 落盘(栈溢出复位); + * 现改为置 off_pending,由 app_function_loop(usr_timer_loop, + * BLE 线程)先落盘再关机 + ******************************************************************************/ + +#include "app_function.h" +#include "app_power.h" +#include "app_pair.h" +#include "app_mode.h" +#include "app_fan.h" +#include "app_nvm.h" +#include "app_arbiter.h" +#include "app_cond.h" +#include "app_timer_task.h" +#include "app_th.h" +#include "app_disp.h" +#include "app_led.h" +#include "jb_product.h" +#include "system/includes.h" +#include + +#define LOG_TAG_CONST APP +#define LOG_TAG "[APP_FUNC]" +#define LOG_INFO_ENABLE +#include "debug.h" + +/** 功能模块状态 */ +typedef struct { + uint8_t app_power; /**< DP0:1=开机态(本产品无 APP 软关) */ + uint8_t off_pending; /**< 1=长按已触发关机,等任务上下文落盘后再关机 */ +} AppFunctionCtx_t; + +static AppFunctionCtx_t s_func; + +/** + * @brief 初始化功能模块状态 + * @return 无 + */ +void app_function_init(void) +{ + s_func.app_power = 0; + s_func.off_pending = 0; +} + +/** + * @brief 任务上下文周期处理(由 usr_timer_loop 500ms 调用) + * @note ⚠ 本函数是唯一允许做 VM 落盘与关机收口的地方:usr_timer_loop 跑在 + * BLE 线程(sys_timer 同步接口,任务上下文,栈充足)。20ms 的 + * usr_timer_tick 是硬件定时器中断上下文,禁止做这两件事。 + * @return 无 + */ +void app_function_loop(void) +{ + app_nvm_flush(); /* 防抖满后真正写 VM(syscfg_write 必须在任务上下文) */ + + if (s_func.off_pending) { + s_func.off_pending = 0; + app_power_request_poweroff(); /* 落盘后再走关机流程 */ + } +} + +/** + * @brief 同步 DP(power 恒 1、fan_gear、mode、fault_code),每个业务 tick 调用 + * @note DP3/DP4 温湿度在 userHandle(500ms)填;DP0 写忽略,运行中恒上报 1 + * @return 无 + */ +void app_function_poll(void) +{ + gDevData.power = (s_func.app_power) ? 1 : 0; + gDevData.fan_gear = app_fan_get_gear(); /* DP2 必须等于当前真实档位 */ + gDevData.mode = app_mode_get_active(); + /* 规格书 8.2/15:传感器故障优先 2;否则温湿度比较成立报 1 */ + if (app_th_is_fault()) { + gDevData.fault_code = 2; + } else if (app_disp_is_alarm()) { + gDevData.fault_code = 1; + } else { + gDevData.fault_code = 0; + } +} + +/** + * @brief 停手动 + 模式,风扇关(关机请求 / 软关时用) + * @return 无 + */ +void app_function_stop_all(void) +{ + app_mode_stop(); + app_fan_set_gear(0); +} + +/** + * @brief 软关:风扇停、DP0=0(本产品 DP0 写忽略,软关仅按键 3s 触发) + * @return 无 + */ +void app_function_soft_off(void) +{ + s_func.app_power = 0; + gDevData.power = 0; + app_function_stop_all(); + log_info("soft off\n"); +} + +/** + * @brief 上电默认开启功能(DP0=1),不带开机灯效提示 + * @return 无 + */ +void app_function_power_on_silent(void) +{ + s_func.app_power = 1; + gDevData.power = 1; +} + +/** + * @brief 查询功能开关状态(DP0) + * @return 1=开,0=关 + */ +uint8_t app_function_is_power_on(void) +{ + return s_func.app_power; +} + +/** + * @brief 短按:风扇档位循环 1→2→3→0→1…(规格书 5.3) + * @note 立刻退出通风/强力改为手动档:用 app_mode_stop() 只清模式、 + * 不动档位(若用 set_manual(0) 会恢复模式前快照档,导致通风 1 档 + * 短按跳到旧档+1 而非 2 档);配对进行中短按仍可切档 + * @return 无 + */ +void usr_on_key_short(void) +{ + uint8_t g; + + if (app_power_poweroff_pending()) { + return; + } + if (!s_func.app_power) { + /* 软关态短按只唤醒,不切档(P0 无 APP 软关,保留分支) */ + s_func.app_power = 1; + gDevData.power = 1; + app_led_hint_power_on(); + return; + } + + app_arbiter_manual_intervene(); /* 手动动作:释放条件/定时占用(12 章) */ + app_mode_stop(); /* 退模式(不动档位,下面立刻切循环下一档) */ + g = app_fan_get_gear(); /* 当前实际档:手动=手动档;模式中=模式强制档(1/3) */ + g = (uint8_t)((g + 1) % 4); /* 1→2→3→0→1 */ + app_fan_set_gear(g); + log_info("key short gear=%d\n", g); +} + +/** + * @brief 长按 3s:请求软关机(配对中忽略,避免配到一半睡死,规格书 5.3) + * @note ⚠ 本回调在 usr_timer 20ms(硬件定时器中断上下文)执行,禁止直接 + * 调 app_power_request_poweroff(其钩子会做 VM 落盘 → 栈溢出)。 + * 只置 off_pending,由 app_function_loop(任务上下文)落盘后关机。 + * @return 无 + */ +void usr_on_key_long(void) +{ + if (app_power_poweroff_pending()) { + return; + } + if (app_pair_is_active()) { + log_info("key hold 3s ignored, pairing\n"); + return; + } + log_info("key hold 3s, poweroff pending (wait vm flush)\n"); + s_func.off_pending = 1; +} + +/** + * @brief 出厂重置:清业务 VM 与 RAM,保留 BLE 配对(规格书 14 章) + * @return 无 + */ +void app_function_factory_reset(void) +{ + uint8_t i; + uint8_t *c[7] = { + gDevData.cond_config1, gDevData.cond_config2, gDevData.cond_config3, + gDevData.cond_config4, gDevData.cond_config5, gDevData.cond_config6, + gDevData.cond_config7 + }; + uint8_t *tm[7] = { + gDevData.time_config1, gDevData.time_config2, gDevData.time_config3, + gDevData.time_config4, gDevData.time_config5, gDevData.time_config6, + gDevData.time_config7 + }; + + app_nvm_reset(); + app_arbiter_init(); + app_cond_init(); + app_timer_init(); + app_mode_stop(); + app_fan_set_gear(0); + gDevData.mode = 0; + gDevData.fan_gear = 0; + gDevData.cond_en = 0; + gDevData.time_en = 0; + gDevData.fault_en = 0; + gDevData.alarm_en = 0; + gDevData.fault_code = 0; + gDevData.alarm_code = 0; + for (i = 0; i < 7; i++) { + memset(c[i], 0, 7); + memset(tm[i], 0, 8); + } + log_info("factory reset: business vm cleared, pairing kept\n"); +} diff --git a/apps/usr_app/app_function.h b/apps/usr_app/app_function.h new file mode 100644 index 0000000..6b50799 --- /dev/null +++ b/apps/usr_app/app_function.h @@ -0,0 +1,82 @@ +/****************************************************************************** + * @file app_function.h + * @brief CBR3 功能总装:按键动作(短按切档/长按关机)+ DP 同步 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V1.0.0 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹语义重写) + * + * @note 本模块只做"按键 → 动作"和"状态 → DP",不写业务算法。 + * 档位/模式/条件的执行在各自 app_* 模块(规格书第 0 章第 2 条)。 + ******************************************************************************/ + +#ifndef __APP_FUNCTION_H__ +#define __APP_FUNCTION_H__ + +#include + +/** + * @brief 初始化功能模块状态 + * @return 无 + */ +void app_function_init(void); + +/** + * @brief 同步 DP(power 恒 1、fan_gear、mode、fault_code),每个业务 tick 调用 + * @return 无 + */ +void app_function_poll(void); + +/** + * @brief 任务上下文周期处理(由 usr_timer_loop 500ms 调用) + * @note ⚠ VM 落盘(syscfg_write)与关机收口只允许在这里做:usr_timer_loop + * 跑在 BLE 线程(任务上下文,栈充足);usr_timer_tick 是硬件定时器 + * 中断上下文,做这两件事会栈溢出复位。 + * @return 无 + */ +void app_function_loop(void); + +/** + * @brief 停手动 + 模式,风扇关(关机请求 / 软关时用) + * @return 无 + */ +void app_function_stop_all(void); + +/** + * @brief 软关:风扇停、DP0=0(本产品 DP0 写忽略,软关仅按键 3s 触发) + * @return 无 + */ +void app_function_soft_off(void); + +/** + * @brief 上电默认开启功能(DP0=1),不带开机灯效提示 + * @return 无 + */ +void app_function_power_on_silent(void); + +/** + * @brief 查询功能开关状态(DP0) + * @return 1=开,0=关 + */ +uint8_t app_function_is_power_on(void); + +/** + * @brief 业务层短按回调(key_manager 触发):档位 1→2→3→0→1… + * @return 无 + */ +void usr_on_key_short(void); + +/** + * @brief 业务层长按 3s 回调(key_manager 触发):请求软关机 + * @return 无 + */ +void usr_on_key_long(void); + +/** + * @brief 出厂重置:清业务 VM 与 RAM,保留 BLE 配对(规格书 14 章) + * @return 无 + */ +void app_function_factory_reset(void); + +#endif /* __APP_FUNCTION_H__ */ diff --git a/apps/usr_app/app_led.c b/apps/usr_app/app_led.c new file mode 100644 index 0000000..de88492 --- /dev/null +++ b/apps/usr_app/app_led.c @@ -0,0 +1,143 @@ +/****************************************************************************** + * @file app_led.c + * @brief CBR3 灯效适配:单灯 PC2,OTA/配对/常亮 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V1.0.0 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹,删红绿双通道/充电/低电/空电) + ******************************************************************************/ + +#include "app_led.h" +#include "led_manager.h" +#include "bsp_hw.h" +#include "board_pin.h" +#include "app_pair.h" + +static uint8_t s_initialized; +static uint8_t s_ota; /* 1=OTA 灯效中,app_led_run 不再叠其它灯 */ + +/** + * @brief 重新播放一次「一次性/限时」灯效,让它从第 0 帧重新开始 + * @note 先 Delete 清空槽位再 Add,强制重新计时(见 led.c LED_EventAdd 语义) + * @param evt 要重启的事件 + * @return 无 + */ +static void led_restart(LED_Event_t evt) +{ + LED_EventDelete(evt); + LED_EventAdd(evt); +} + +/** + * @brief 灯效初始化 + * @return APP_LED_OK=成功 + */ +AppLed_Ret_t app_led_init(void) +{ + if (s_initialized) { + return APP_LED_OK; + } + user_led_init(); + s_initialized = 1; + return APP_LED_OK; +} + +/** + * @brief 查询模块是否已初始化 + * @return APP_LED_OK=已初始化,APP_LED_ERR_NOT_INIT=未初始化 + */ +AppLed_Ret_t app_led_get_status(void) +{ + return s_initialized ? APP_LED_OK : APP_LED_ERR_NOT_INIT; +} + +/** + * @brief 重新挂开机常亮 + * @return 无 + */ +void app_led_hint_power_on(void) +{ + if (!s_initialized || s_ota) { + return; + } + led_restart(LED_EVENT_STANDBY); +} + +/** + * @brief 进入 OTA:清掉其它灯,挂 200ms 闪一直闪 + * @return 无 + */ +void app_led_request_ota(void) +{ + if (!s_initialized) { + return; + } + s_ota = 1; + LED_EventAllDelete(); + bsp_led_set(0); + LED_EventAdd(LED_EVENT_OTA); +} + +/** + * @brief 查询是否处于 OTA 灯效 + * @return 1=OTA 灯效中,0=否 + */ +uint8_t app_led_is_ota(void) +{ + return s_ota; +} + +/** + * @brief 按优先级刷新灯 + * @param dt 距上次调用的毫秒数 + * @param power_on 1=开机态(挂常亮),0=关机态(灯灭) + * @return 无 + */ +void app_led_run(uint16_t dt, uint8_t power_on) +{ + AppPairEvt_t pair_evt; + + if (!s_initialized) { + return; + } + + /* OTA 期间只跑 200ms 闪 */ + if (s_ota) { + LED_EventAdd(LED_EVENT_OTA); + user_led_handle(); + return; + } + + /* 配对成功/失败是边沿事件,只在那一拍切灯效 */ + pair_evt = app_pair_tick_1ms(dt); + if (pair_evt == APP_PAIR_EVT_OK) { + LED_EventDelete(LED_EVENT_PAIRING); + led_restart(LED_EVENT_PAIR_OK); + } else if (pair_evt == APP_PAIR_EVT_FAIL) { + LED_EventDelete(LED_EVENT_PAIRING); + led_restart(LED_EVENT_PAIR_FAIL); + } + + if (!power_on) { + LED_EventDelete(LED_EVENT_STANDBY); + LED_EventDelete(LED_EVENT_PAIRING); + } else if (app_pair_is_active()) { + LED_EventAdd(LED_EVENT_PAIRING); + LED_EventDelete(LED_EVENT_STANDBY); + } else if (!LED_IsEventExist(LED_EVENT_PAIR_OK) && !LED_IsEventExist(LED_EVENT_PAIR_FAIL)) { + LED_EventAdd(LED_EVENT_STANDBY); + } + + user_led_handle(); +} + +/** + * @brief 清空全部灯事件并灭灯 + * @return 无 + */ +void app_led_clear(void) +{ + LED_EventAllDelete(); + bsp_led_set(0); +} diff --git a/apps/usr_app/app_led.h b/apps/usr_app/app_led.h new file mode 100644 index 0000000..9b3dc14 --- /dev/null +++ b/apps/usr_app/app_led.h @@ -0,0 +1,65 @@ +/****************************************************************************** + * @file app_led.h + * @brief CBR3 灯效:单灯 PC2,OTA/配对/常亮按优先级 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V1.0.0 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹,删红绿双通道/充电/低电) + ******************************************************************************/ + +#ifndef __APP_LED_H__ +#define __APP_LED_H__ + +#include + +typedef enum { + APP_LED_OK = 0, + APP_LED_ERR_NOT_INIT +} AppLed_Ret_t; + +/** + * @brief 灯效初始化,先全灭 + * @return APP_LED_OK=成功 + */ +AppLed_Ret_t app_led_init(void); + +/** + * @brief 按优先级刷新灯:配对从 app_pair 取,功能开关由调用方传入 + * @param dt 距上次调用的毫秒数 + * @param power_on 1=开机态,0=关机态(灯灭) + * @return 无 + */ +void app_led_run(uint16_t dt, uint8_t power_on); + +/** + * @brief 重新挂开机常亮 + * @return 无 + */ +void app_led_hint_power_on(void); + +/** + * @brief 进入 OTA:清掉其它灯,挂 200ms 闪一直闪 + * @return 无 + */ +void app_led_request_ota(void); + +/** + * @brief 查询是否处于 OTA 灯效 + * @return 1=OTA 灯效中,0=否 + */ +uint8_t app_led_is_ota(void); + +/** + * @brief 清空全部灯事件并灭灯(关机前调用) + * @return 无 + */ +void app_led_clear(void); + +/** + * @brief 查询是否已初始化 + * @return APP_LED_OK=已初始化,APP_LED_ERR_NOT_INIT=未初始化 + */ +AppLed_Ret_t app_led_get_status(void); + +#endif /* __APP_LED_H__ */ diff --git a/apps/usr_app/app_mode.c b/apps/usr_app/app_mode.c new file mode 100644 index 0000000..bed390e --- /dev/null +++ b/apps/usr_app/app_mode.c @@ -0,0 +1,177 @@ +/****************************************************************************** + * @file app_mode.c + * @brief CBR3 工作模式实现(P1):通风 25/5 循环、强力 15min 一轮 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V1.1.0 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布(P0 stub) + * - V1.1.0, 2026.08.31, cyWu, P1 实现状态机(规格书第 9 章): + * 通风 25/5、强力 15min 后停、手动档快照、退出语义、开机从周期头 + * + * @note 模式激活时强制覆盖风扇档位(通风=1 档、强力=3 档、通风停段=0), + * DP2 由 app_fan_get_gear 上报真实档位(app_function_poll 同步)。 + * 退出方式(规格书 9 章):短按切档(app_mode_stop)、APP 写档位、 + * APP 写 mode=0(恢复进入模式前的手动档快照)、强力自然到点(风扇 0)。 + * 开机恢复不存相位:从 25min 开段 / 15min 从头计。 + ******************************************************************************/ + +#include "app_mode.h" +#include "app_fan.h" +#include "app_nvm.h" +#include "board_pin.h" + +#define LOG_TAG_CONST APP +#define LOG_TAG "[APP_MODE]" +#define LOG_INFO_ENABLE +#include "debug.h" + +/** 通风循环段 */ +typedef enum { + MODE_VENT_ON = 0, /**< 开段 25min,强制 1 档 */ + MODE_VENT_OFF /**< 停段 5min,风扇 0 */ +} ModeVentPhase_t; + +/** 模式状态机上下文 */ +typedef struct { + uint8_t mode; /**< 0=关闭 1=通风 2=强力 */ + uint8_t phase; /**< 通风段;其它模式恒 0 */ + uint32_t run_ms; /**< 当前段已运行毫秒 */ + uint8_t saved_gear;/**< 进入模式前的手动档快照(APP 写 mode=0 时恢复) */ +} AppModeCtx_t; + +static AppModeCtx_t s_mode; + +/** + * @brief 初始化模式状态机 + * @return 无 + */ +void app_mode_init(void) +{ +#if !BOARD_MODE_ENABLE + log_info("mode disabled\n"); + return; +#endif + s_mode.mode = 0; + s_mode.phase = 0; + s_mode.run_ms = 0; + s_mode.saved_gear = 0; + log_info("mode init\n"); +} + +/** + * @brief 推进模式状态机(每个业务 tick 调用) + * @param dt 距上次调用的毫秒数 + * @return 无 + */ +void app_mode_run(uint16_t dt) +{ +#if !BOARD_MODE_ENABLE + (void)dt; + return; +#endif + switch (s_mode.mode) { + case 1: /* 通风:开 25min 强制 1 档,停 5min 风扇 0,循环 */ + if (s_mode.phase == MODE_VENT_ON) { + app_fan_set_gear(1); + s_mode.run_ms += dt; + if (s_mode.run_ms >= BOARD_MODE_VENT_ON_MS) { + s_mode.phase = MODE_VENT_OFF; + s_mode.run_ms = 0; + app_fan_set_gear(0); + app_nvm_mark_dirty(); + log_info("vent on 25min over, off 5min\n"); + } + } else { + s_mode.run_ms += dt; + if (s_mode.run_ms >= BOARD_MODE_VENT_OFF_MS) { + s_mode.phase = MODE_VENT_ON; + s_mode.run_ms = 0; + app_fan_set_gear(1); + app_nvm_mark_dirty(); + log_info("vent off 5min over, on 25min\n"); + } + } + break; + + case 2: /* 强力:强制 3 档,15min 到点风扇 0、mode 回 0 */ + app_fan_set_gear(3); + s_mode.run_ms += dt; + if (s_mode.run_ms >= BOARD_MODE_POWER_ON_MS) { + log_info("power 15min over, mode=0 fan=0\n"); + s_mode.mode = 0; + s_mode.phase = 0; + s_mode.run_ms = 0; + app_fan_set_gear(0); + app_nvm_mark_dirty(); + } + break; + + default: + break; + } +} + +/** + * @brief APP 写 DP1 / 手动抢权 / 开机恢复:切到指定模式或退出 + * @param mode 0=关闭模式(恢复手动档快照),1=通风,2=强力 + * @note 模式 1/2 进入时快照当前实际档(即进入前的手动档),并立即强制档位; + * mode=0 时若原在模式中则恢复快照档。短按退出走 app_mode_stop() + * (不恢复快照,档位由短按逻辑立刻切到循环下一档)。 + * @return 无 + */ +void app_mode_set_manual(uint8_t mode) +{ +#if !BOARD_MODE_ENABLE + (void)mode; + return; +#endif + if (mode > 2) { + mode = 0; + } + if (mode != 0) { + s_mode.saved_gear = app_fan_get_gear(); /* 快照进入前的手动档 */ + s_mode.mode = mode; + s_mode.phase = MODE_VENT_ON; + s_mode.run_ms = 0; /* 不存相位:开机/手动切入都从周期头计 */ + app_fan_set_gear((mode == 1) ? 1 : 3); + log_info("mode set %d, gear=%d\n", mode, app_fan_get_gear()); + } else { + if (s_mode.mode != 0) { + /* 只在实际退出模式时恢复快照档,避免 load(0) 把恢复档清零 */ + app_fan_set_gear(s_mode.saved_gear); + log_info("mode exit, restore gear=%d\n", s_mode.saved_gear); + } + s_mode.mode = 0; + s_mode.phase = 0; + s_mode.run_ms = 0; + } + app_nvm_mark_dirty(); +} + +/** + * @brief 查询当前逻辑模式 + * @return 0/1/2 + */ +uint8_t app_mode_get_active(void) +{ + return s_mode.mode; +} + +/** + * @brief 停止当前模式(短按退模式用:只清模式状态,不动风扇档位, + * 由调用方立刻切到循环下一档;关机流程则另行停风扇) + * @return 无 + */ +void app_mode_stop(void) +{ +#if !BOARD_MODE_ENABLE + return; +#endif + if (s_mode.mode != 0) { + app_nvm_mark_dirty(); + } + s_mode.mode = 0; + s_mode.phase = 0; + s_mode.run_ms = 0; +} diff --git a/apps/usr_app/app_mode.h b/apps/usr_app/app_mode.h new file mode 100644 index 0000000..88ea2d2 --- /dev/null +++ b/apps/usr_app/app_mode.h @@ -0,0 +1,54 @@ +/****************************************************************************** + * @file app_mode.h + * @brief CBR3 工作模式:通风(25/5 循环)/ 强力(15min 一轮) + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V1.0.0 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布(P0 仅接口与 stub,P1 实现状态机) + * + * @note P1 语义(规格书第 9 章):mode=1 强制 1 档开 25min 停 5min 循环; + * mode=2 强制 3 档跑 15min 后风扇 0、mode 回 0。短按切档走 + * app_mode_stop(档位由短按逻辑续切);APP 写档 / 写 mode 走 + * app_mode_set_manual 手动抢权(mode=0 恢复进入前的手动档快照)。 + * 开机恢复不存相位,从头计。BOARD_MODE_ENABLE=0 时全部空操作。 + ******************************************************************************/ + +#ifndef __APP_MODE_H__ +#define __APP_MODE_H__ + +#include + +/** + * @brief 初始化模式状态机 + * @return 无 + */ +void app_mode_init(void); + +/** + * @brief 推进模式状态机,每个业务 tick 调用一次 + * @param dt 距上次调用的毫秒数 + * @return 无 + */ +void app_mode_run(uint16_t dt); + +/** + * @brief APP 写 DP1 / 手动抢权:切到指定模式或退出(P0 关闭时忽略) + * @param mode 0=关闭模式,1=通风,2=强力 + * @return 无 + */ +void app_mode_set_manual(uint8_t mode); + +/** + * @brief 查询当前逻辑模式 + * @return 0/1/2 + */ +uint8_t app_mode_get_active(void); + +/** + * @brief 停止当前模式(风扇由 app_fan 接管,这里只清模式状态) + * @return 无 + */ +void app_mode_stop(void); + +#endif /* __APP_MODE_H__ */ diff --git a/apps/usr_app/app_nvm.c b/apps/usr_app/app_nvm.c new file mode 100644 index 0000000..cccf49e --- /dev/null +++ b/apps/usr_app/app_nvm.c @@ -0,0 +1,423 @@ +/****************************************************************************** + * @file app_nvm.c + * @brief CBR3 VM 持久化实现(P1):mode/fan_gear/条件/定时/使能 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V1.1.0 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布(P0 stub) + * - V1.1.0, 2026.08.31, cyWu, P1 实现:3 个 VM item + 防抖 + 立即写 + * + * @note VM item 分配在 syscfg 用户自定义区 [1~49](syscfg_id.h 已占用 + * CFG_USER_PAIR_INFO=2 / CFG_USER_TIM_FLAG=3 / VM_ALARM*=4~24 / + * CFG_USER_DEVICE_STA=25 / CFG_USER_PLAN_INFO1=26 / + * CFG_USER_BATTERY_LEVEL=27),本产品从 28 开始: + * CFG_USER_CBR3_STATE=28 状态(magic+ver+mode+gear+en 等,10B) + * CFG_USER_CBR3_COND =29 条件配置 cond_config[7][7](49B) + * CFG_USER_CBR3_TIME =30 定时配置 time_config[7][8](56B) + * 拆 3 个 item:改档/改模式只擦写 10B 小 item,不重写 105B 配置区, + * 减少 flash 磨损;首次上电(magic 不符)保持 gDevData 全 0 默认。 + * + * 写入时机(规格书第 13 章):APP 改配置后(mark_dirty)、档位/模式 + * 稳定变化(500ms 防抖)、软关机前(save 立即写)。 + * + * ⚠⚠ 上下文硬约束(2026-08-31 复位事故定论,勿再踩): + * usr_timer_add() 是**异步接口**,回调在**硬件定时器中断上下文**执行 + * (栈极小);sys_timer_add() 是同步接口,回调在 add 它的线程执行。 + * 因此: + * - app_nvm_tick() 跑在 20ms 中断上下文 → **只能做变化检测与计时, + * 绝对不能调 syscfg_write**(VM 擦写 flash 栈用量大且中断里做 + * flash 操作 → 栈溢出软复位;实测 APP 下发数据后 500ms 必崩)。 + * - 真正写入由 app_nvm_flush() 在 usr_timer_loop()(BLE 线程、 + * 任务上下文、栈充足)执行;usr_poweroff_request_hook 里的 + * app_nvm_save() 也必须保证在任务上下文被调用(见 app_function_loop)。 + * 另:nvm_write_item 的比较缓冲改 static,再省 128B 栈。 + ******************************************************************************/ + +#include "app_nvm.h" +#include "app_mode.h" +#include "app_fan.h" +#include "jb_product.h" +#include "board_pin.h" +#include "system/includes.h" +#include "syscfg_id.h" + +#define LOG_TAG_CONST APP +#define LOG_TAG "[APP_NVM]" +#define LOG_ERROR_ENABLE +#define LOG_INFO_ENABLE +#include "debug.h" + +/* ── VM item id(用户区 1~49 内申请)── */ +#define CFG_USER_CBR3_STATE 28 +#define CFG_USER_CBR3_COND 29 +#define CFG_USER_CBR3_TIME 30 + +#define CBR3_VM_MAGIC 0xCB03 +#define CBR3_VM_VER 1 +#define CBR3_VM_DEBOUNCE_MS 500 /* 档位/模式稳定变化防抖(规格书 13 章) */ + +/** 状态 item(10 字节) */ +typedef struct { + uint16_t magic; /**< CBR3_VM_MAGIC */ + uint8_t ver; /**< CBR3_VM_VER */ + uint8_t mode; /**< 逻辑模式 0/1/2 */ + uint8_t fan_gear; /**< 真实档位 0~3(模式未激活时即手动档) */ + uint8_t cond_en; /**< DP5 条件总开关 */ + uint8_t time_en; /**< DP13 定时总开关 */ + uint8_t fault_en; /**< DP21 */ + uint8_t alarm_en; /**< DP23 */ +} __packed Cbr3StateVm_t; + +/** 条件配置 item(49 字节) */ +typedef struct { + uint8_t cond_config[7][7]; +} __packed Cbr3CondVm_t; + +/** 定时配置 item(56 字节) */ +typedef struct { + uint8_t time_config[7][8]; +} __packed Cbr3TimeVm_t; + +#if BOARD_NVM_ENABLE +static uint8_t s_dirty; /**< 1=防抖计时中 */ +static uint8_t s_need_save; /**< 1=防抖已满,待任务上下文落盘 */ +static uint16_t s_deb_ms; /**< 防抖计时 */ + +/* 上次已同步到 VM 的状态快照:变化检测基准(覆盖按键/APP/模式引擎所有入口) */ +static uint8_t s_last_gear; +static uint8_t s_last_mode; +static uint8_t s_last_cond_en; +static uint8_t s_last_time_en; +#endif + +/** + * @brief 从 gDevData 收集条件配置到 VM 结构 + * @note gDevData.cond_config1~7 是 7 个独立数组字段,用指针表统一搬运 + * @param out 输出结构 + * @return 无 + */ +static void nvm_collect_cond(Cbr3CondVm_t *out) +{ + const uint8_t *src[7] = { + gDevData.cond_config1, gDevData.cond_config2, gDevData.cond_config3, + gDevData.cond_config4, gDevData.cond_config5, gDevData.cond_config6, + gDevData.cond_config7 + }; + uint8_t i; + + for (i = 0; i < 7; i++) { + memcpy(out->cond_config[i], src[i], 7); + } +} + +/** + * @brief 从 gDevData 收集定时配置到 VM 结构 + * @param out 输出结构 + * @return 无 + */ +static void nvm_collect_time(Cbr3TimeVm_t *out) +{ + const uint8_t *src[7] = { + gDevData.time_config1, gDevData.time_config2, gDevData.time_config3, + gDevData.time_config4, gDevData.time_config5, gDevData.time_config6, + gDevData.time_config7 + }; + uint8_t i; + + for (i = 0; i < 7; i++) { + memcpy(out->time_config[i], src[i], 8); + } +} + +/** + * @brief 写单个 item:先读回比较,内容相同跳过,避免无谓擦写 flash + * @param item_id VM item id + * @param buf 新数据 + * @param len 数据长度 + * @return 无 + */ +static void nvm_write_item(u16 item_id, void *buf, u16 len) +{ + /* static:比较缓冲不放栈上(省 128B 栈);本函数只在任务上下文调用,无重入 */ + static uint8_t old[128]; + + if (len > sizeof(old)) { + log_error("nvm item %d len %d too big\n", item_id, len); + return; + } + if (syscfg_read(item_id, old, len) == len) { + if (memcmp(old, buf, len) == 0) { + return; /* 没变化,不擦写 */ + } + } + if (syscfg_write(item_id, buf, len) > 0) { + log_info("nvm item %d saved %dB\n", item_id, len); + } else { + log_error("nvm item %d write fail\n", item_id); + } +} + +/** + * @brief 写状态 item(mode/gear/使能) + * @return 无 + */ +static void nvm_write_state(void) +{ + Cbr3StateVm_t st; + + st.magic = CBR3_VM_MAGIC; + st.ver = CBR3_VM_VER; + st.mode = app_mode_get_active(); + st.fan_gear = app_fan_get_gear(); + st.cond_en = gDevData.cond_en ? 1 : 0; + st.time_en = gDevData.time_en ? 1 : 0; + st.fault_en = gDevData.fault_en ? 1 : 0; + st.alarm_en = gDevData.alarm_en ? 1 : 0; + nvm_write_item(CFG_USER_CBR3_STATE, &st, sizeof(st)); +} + +/** + * @brief 写条件配置 item + * @return 无 + */ +static void nvm_write_cond(void) +{ + Cbr3CondVm_t cf; + + nvm_collect_cond(&cf); + nvm_write_item(CFG_USER_CBR3_COND, &cf, sizeof(cf)); +} + +/** + * @brief 写定时配置 item + * @return 无 + */ +static void nvm_write_time(void) +{ + Cbr3TimeVm_t tf; + + nvm_collect_time(&tf); + nvm_write_item(CFG_USER_CBR3_TIME, &tf, sizeof(tf)); +} + +/** + * @brief 初始化:校验 VM 有效性并打日志(加载在 app_nvm_load) + * @return 无 + */ +void app_nvm_init(void) +{ +#if !BOARD_NVM_ENABLE + log_info("nvm disabled\n"); + return; +#else + Cbr3StateVm_t st; + int ret = syscfg_read(CFG_USER_CBR3_STATE, &st, sizeof(st)); + + if ((ret == (int)sizeof(st)) && (st.magic == CBR3_VM_MAGIC) && (st.ver == CBR3_VM_VER)) { + log_info("nvm init, vm valid (mode=%d gear=%d cond_en=%d time_en=%d)\n", + st.mode, st.fan_gear, st.cond_en, st.time_en); + } else { + log_info("nvm init, vm invalid (ret=%d), use defaults\n", ret); + } + s_dirty = 0; + s_deb_ms = 0; + s_need_save = 0; +#endif +} + +/** + * @brief 同步"已落盘状态"快照为当前值(save/load 后调用) + * @return 无 + */ +static void nvm_sync_snapshot(void) +{ +#if BOARD_NVM_ENABLE + s_last_gear = app_fan_get_gear(); + s_last_mode = app_mode_get_active(); + s_last_cond_en = gDevData.cond_en ? 1 : 0; + s_last_time_en = gDevData.time_en ? 1 : 0; +#endif +} + +/** + * @brief 立即保存全部业务字段(软关机前调用) + * @return 无 + */ +void app_nvm_save(void) +{ +#if !BOARD_NVM_ENABLE + return; +#else + nvm_write_state(); + nvm_write_cond(); + nvm_write_time(); + s_dirty = 0; + s_deb_ms = 0; + s_need_save = 0; + nvm_sync_snapshot(); +#endif +} + +/** + * @brief 从 VM 加载并应用(开机 3s 后由 usr_jb_init 调用) + * @note 顺序:先恢复档位再恢复模式(app_mode_set_manual 内部会快照当前档、 + * 并让通风/强力从周期头开跑,规格书第 9 章"开机恢复不存相位")。 + * VM 无效时保持 gDevData 全 0(首次上电默认 mode=0 gear=0)。 + * @return 无 + */ +void app_nvm_load(void) +{ +#if !BOARD_NVM_ENABLE + return; +#else + Cbr3StateVm_t st; + Cbr3CondVm_t cf; + Cbr3TimeVm_t tf; + uint8_t i; + + if ((syscfg_read(CFG_USER_CBR3_STATE, &st, sizeof(st)) != (int)sizeof(st)) + || (st.magic != CBR3_VM_MAGIC) || (st.ver != CBR3_VM_VER)) { + log_info("nvm load: no valid vm, defaults (mode=0 gear=0)\n"); + return; + } + + /* 条件/定时配置回填 gDevData(只存不跑,P2 引擎再消费) */ + if (syscfg_read(CFG_USER_CBR3_COND, &cf, sizeof(cf)) == (int)sizeof(cf)) { + uint8_t *dst[7] = { + gDevData.cond_config1, gDevData.cond_config2, gDevData.cond_config3, + gDevData.cond_config4, gDevData.cond_config5, gDevData.cond_config6, + gDevData.cond_config7 + }; + for (i = 0; i < 7; i++) { + memcpy(dst[i], cf.cond_config[i], 7); + } + } + if (syscfg_read(CFG_USER_CBR3_TIME, &tf, sizeof(tf)) == (int)sizeof(tf)) { + uint8_t *dst[7] = { + gDevData.time_config1, gDevData.time_config2, gDevData.time_config3, + gDevData.time_config4, gDevData.time_config5, gDevData.time_config6, + gDevData.time_config7 + }; + for (i = 0; i < 7; i++) { + memcpy(dst[i], tf.time_config[i], 8); + } + } + + gDevData.cond_en = st.cond_en ? 1 : 0; + gDevData.time_en = st.time_en ? 1 : 0; + gDevData.fault_en = st.fault_en ? 1 : 0; + gDevData.alarm_en = st.alarm_en ? 1 : 0; + + /* 先档位后模式;mode 激活时引擎会从周期头强制档位 */ + if (st.fan_gear > 3) { + st.fan_gear = 0; + } + app_fan_set_gear(st.fan_gear); + app_mode_set_manual(st.mode); + nvm_sync_snapshot(); /* 恢复值即为已落盘值,同步基准 */ + log_info("nvm load: mode=%d gear=%d cond_en=%d time_en=%d\n", + st.mode, st.fan_gear, st.cond_en, st.time_en); +#endif +} + +/** + * @brief 业务节拍:做档位/模式/总开关变化检测 + 防抖计时,**不执行写入** + * @param dt 距上次调用的毫秒数 + * @note ⚠ 本函数跑在 usr_timer_add 的 20ms 回调里 = **硬件定时器中断上下文** + * (异步接口,栈极小)。这里只允许做轻量比较与计时;真正的 + * syscfg_write 由 app_nvm_flush() 在 usr_timer_loop(BLE 线程、 + * 任务上下文)执行。曾在此直接调 app_nvm_save 导致栈溢出软复位。 + * @return 无 + */ +void app_nvm_tick(uint16_t dt) +{ +#if !BOARD_NVM_ENABLE + (void)dt; + return; +#else + uint8_t gear = app_fan_get_gear(); + uint8_t mode = app_mode_get_active(); + uint8_t cond_en = gDevData.cond_en ? 1 : 0; + uint8_t time_en = gDevData.time_en ? 1 : 0; + + if ((gear != s_last_gear) || (mode != s_last_mode) + || (cond_en != s_last_cond_en) || (time_en != s_last_time_en)) { + s_last_gear = gear; + s_last_mode = mode; + s_last_cond_en = cond_en; + s_last_time_en = time_en; + s_dirty = 1; + s_deb_ms = 0; /* 变化重新计满防抖窗口 */ + } + + if (!s_dirty) { + return; + } + s_deb_ms = (uint16_t)(s_deb_ms + dt); + if (s_deb_ms >= CBR3_VM_DEBOUNCE_MS) { + s_deb_ms = 0; + s_dirty = 0; + s_need_save = 1; /* 只置标志:等任务上下文 app_nvm_flush 真正落盘 */ + } +#endif +} + +/** + * @brief 任务上下文落盘:防抖满后真正写 VM(由 usr_timer_loop 500ms 调用) + * @note 必须且只能在**非中断上下文**调用(sys_timer 回调 / BLE 线程 / + * app_core 任务)。VM 擦写 flash 栈用量大,禁止放进 20ms 业务节拍。 + * @return 无 + */ +void app_nvm_flush(void) +{ +#if !BOARD_NVM_ENABLE + return; +#else + if (!s_need_save) { + return; + } + s_need_save = 0; + app_nvm_save(); +#endif +} + +/** + * @brief 标记业务字段有变化,防抖计时归零(重新计满 CBR3_VM_DEBOUNCE_MS 才写) + * @return 无 + */ +void app_nvm_mark_dirty(void) +{ +#if BOARD_NVM_ENABLE + s_dirty = 1; + s_deb_ms = 0; +#endif +} + +/** + * @brief 出厂重置:清全部业务 VM(保留 BLE 配对,规格书第 14 章) + * @note 3 个 item 写全 0 → magic 失效,下次开机走首次上电默认值; + * BLE 配对三元组在 CFG_USER_PAIR_INFO(item 2),不受影响 + * @return 无 + */ +void app_nvm_reset(void) +{ +#if !BOARD_NVM_ENABLE + return; +#else + Cbr3StateVm_t st; + Cbr3CondVm_t cf; + Cbr3TimeVm_t tf; + + memset(&st, 0, sizeof(st)); + memset(&cf, 0, sizeof(cf)); + memset(&tf, 0, sizeof(tf)); + syscfg_write(CFG_USER_CBR3_STATE, &st, sizeof(st)); + syscfg_write(CFG_USER_CBR3_COND, &cf, sizeof(cf)); + syscfg_write(CFG_USER_CBR3_TIME, &tf, sizeof(tf)); + s_dirty = 0; + s_deb_ms = 0; + s_need_save = 0; + log_info("nvm factory reset, business vm cleared (pairing kept)\n"); +#endif +} diff --git a/apps/usr_app/app_nvm.h b/apps/usr_app/app_nvm.h new file mode 100644 index 0000000..cfce212 --- /dev/null +++ b/apps/usr_app/app_nvm.h @@ -0,0 +1,73 @@ +/****************************************************************************** + * @file app_nvm.h + * @brief CBR3 VM 持久化(P1 起):mode/fan_gear/条件/定时/使能 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V1.1.0 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布(P0 仅接口与 stub) + * - V1.1.0, 2026.08.31, cyWu, P1 实现:3 个 VM item(状态/条件/定时)+ 500ms + * 防抖写入 + 立即写接口 + * + * @note 存:mode、fan_gear、cond_en、cond_config1~7、time_en、time_config1~7、 + * fault_en、alarm_en。不存:DP0、条件作废标志、模式已运行秒数、原始采样。 + * 写 VM 时机:APP 改配置后、单次定时自毁、软关机前、档位/模式稳定变化 + * (500ms 防抖)。 + ******************************************************************************/ + +#ifndef __APP_NVM_H__ +#define __APP_NVM_H__ + +#include + +/** + * @brief 初始化(校验 VM 有效性,仅日志;数据加载在 app_nvm_load) + * @return 无 + */ +void app_nvm_init(void); + +/** + * @brief 立即保存全部业务字段到 VM(软关机前等关键时机调用) + * @note syscfg 写入不能在裸中断里调用;本函数由业务时基回调触发, + * 与 SDK norflash 缓存同步(sys_timeout 回调)同级,可安全执行 + * @return 无 + */ +void app_nvm_save(void); + +/** + * @brief 从 VM 加载业务字段并应用(开机 3s 后调用): + * mode/fan_gear 恢复、条件/定时配置回填 gDevData;mode 从周期头开跑 + * @return 无 + */ +void app_nvm_load(void); + +/** + * @brief 业务节拍(中断上下文):变化检测 + 防抖计时,**不执行写入** + * @param dt 距上次调用的毫秒数 + * @note ⚠ 跑在 usr_timer 20ms 回调 = 硬件定时器中断上下文,栈极小, + * 只允许轻量比较计时;禁止在此调 syscfg_write(会栈溢出复位)。 + * @return 无 + */ +void app_nvm_tick(uint16_t dt); + +/** + * @brief 任务上下文落盘:防抖满后真正写 VM(由 usr_timer_loop 500ms 调用) + * @note 必须且只能在非中断上下文调用(VM 擦写 flash 栈用量大) + * @return 无 + */ +void app_nvm_flush(void); + +/** + * @brief 标记业务字段有变化,防抖计时归零(档位/模式/配置变化时调用) + * @return 无 + */ +void app_nvm_mark_dirty(void); + +/** + * @brief 出厂重置:清全部业务 VM(保留 BLE 配对,规格书第 14 章) + * @note 由 jb_product DP25 调用;RAM 复位由调用方完成 + * @return 无 + */ +void app_nvm_reset(void); + +#endif /* __APP_NVM_H__ */ diff --git a/apps/usr_app/app_pair.c b/apps/usr_app/app_pair.c new file mode 100644 index 0000000..8730f6f --- /dev/null +++ b/apps/usr_app/app_pair.c @@ -0,0 +1,76 @@ +/****************************************************************************** + * @file app_pair.c + * @brief 配对会话计时实现 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.25 + * @version V1.0.0 + * @history + * - V1.0.0, 2026.08.25, cyWu, 首次发布,实现配对会话计时与成功/超时判定 + ******************************************************************************/ + +#include "app_pair.h" +#include "board_pin.h" +#include "usr_le_api.h" +#include "system/includes.h" + +#define LOG_TAG_CONST APP +#define LOG_TAG "[APP_PAIR]" +#define LOG_INFO_ENABLE +#include "debug.h" + +/** 配对会话状态,替代原来散落的 s_pairing / s_pair_ms */ +typedef struct { + uint8_t active; + uint32_t elapsed_ms; +} AppPairCtx_t; + +static AppPairCtx_t s_pair; + +/** + * @brief 开始一次配对会话(计时清零) + * @return 无 + */ +void app_pair_start(void) +{ + s_pair.active = 1; + s_pair.elapsed_ms = 0; +} + +/** + * @brief 推进配对计时并检查结果,每个业务 tick 调用一次 + * @param dt 距上次调用的毫秒数 + * @return APP_PAIR_EVT_OK=配对成功,APP_PAIR_EVT_FAIL=超时,否则 NONE + */ +AppPairEvt_t app_pair_tick_1ms(uint16_t dt) +{ + if (!s_pair.active) { + return APP_PAIR_EVT_NONE; + } + + /* BLE 侧绑定成功:结束本次配对会话 */ + if (usr_get_pair_flag()) { + s_pair.active = 0; + s_pair.elapsed_ms = 0; + log_info("pair ok\n"); + return APP_PAIR_EVT_OK; + } + + s_pair.elapsed_ms += dt; + if (s_pair.elapsed_ms >= BOARD_PAIR_TIMEOUT_MS) { + s_pair.active = 0; + s_pair.elapsed_ms = 0; + log_info("pair timeout 120s, stay on\n"); + return APP_PAIR_EVT_FAIL; + } + + return APP_PAIR_EVT_NONE; +} + +/** + * @brief 查询是否仍在配对会话中 + * @return 1=进行中,0=未在配对 + */ +uint8_t app_pair_is_active(void) +{ + return s_pair.active; +} diff --git a/apps/usr_app/app_pair.h b/apps/usr_app/app_pair.h new file mode 100644 index 0000000..2ab1baa --- /dev/null +++ b/apps/usr_app/app_pair.h @@ -0,0 +1,45 @@ +/****************************************************************************** + * @file app_pair.h + * @brief 配对会话计时:开机配对窗口是否成功/超时,与 BLE 配对本身解耦 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.25 + * @version V1.0.0 + * @history + * - V1.0.0, 2026.08.25, cyWu, 首次发布,实现配对会话计时与成功/超时判定 + * + * @note 真正的 BLE 广播/绑定逻辑在 usr_le_code;本模块只负责“进入配对后 + * 120s 内 usr_get_pair_flag() 是否变 1”这件事,纯状态,不碰灯/按键。 + ******************************************************************************/ + +#ifndef __APP_PAIR_H__ +#define __APP_PAIR_H__ + +#include + +/** 配对结果边沿事件,仅在状态变化那一次返回一次 */ +typedef enum { + APP_PAIR_EVT_NONE = 0, + APP_PAIR_EVT_OK, + APP_PAIR_EVT_FAIL +} AppPairEvt_t; + +/** + * @brief 开始一次配对会话(计时清零) + * @return 无 + */ +void app_pair_start(void); + +/** + * @brief 推进配对计时并检查结果,每个业务 tick 调用一次 + * @param dt 距上次调用的毫秒数 + * @return APP_PAIR_EVT_OK=配对成功,APP_PAIR_EVT_FAIL=超时,否则 NONE + */ +AppPairEvt_t app_pair_tick_1ms(uint16_t dt); + +/** + * @brief 查询是否仍在配对会话中 + * @return 1=进行中,0=未在配对 + */ +uint8_t app_pair_is_active(void); + +#endif /* __APP_PAIR_H__ */ diff --git a/apps/usr_app/app_power.c b/apps/usr_app/app_power.c new file mode 100644 index 0000000..64973b8 --- /dev/null +++ b/apps/usr_app/app_power.c @@ -0,0 +1,100 @@ +/****************************************************************************** + * @file app_power.c + * @brief CBR3 软关机生命周期实现:无电池、无充电 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V1.0.0 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹精简:删 USB/充电/电量/低电) + ******************************************************************************/ + +#include "app_power.h" +#include "system/includes.h" + +#define LOG_TAG_CONST APP +#define LOG_TAG "[APP_PWR]" +#define LOG_INFO_ENABLE +#include "debug.h" + +/** 电源模块总状态 */ +typedef struct { + uint8_t req_poweroff; + AppPowerHook_t on_request; + AppPowerHook_t on_finalize; +} AppPowerCtx_t; + +static AppPowerCtx_t s_power; + +static void power_do_poweroff(void *priv); + +/** + * @brief 初始化电源模块状态 + * @return 无 + */ +void app_power_init(void) +{ + s_power.req_poweroff = 0; + s_power.on_request = NULL; + s_power.on_finalize = NULL; +} + +/** + * @brief 注册深睡请求的两段式回调 + * @param on_request 刚请求深睡时调用(停风扇;灯由调用方按关机类型决定) + * @param on_finalize 延时到点后调用(真正进深睡,不会返回) + * @return 无 + */ +void app_power_set_hooks(AppPowerHook_t on_request, AppPowerHook_t on_finalize) +{ + s_power.on_request = on_request; + s_power.on_finalize = on_finalize; +} + +/** + * @brief 请求深睡(按键 3s 等):立即触发 on_request 钩子,50ms 后触发 on_finalize + * @return 无 + */ +void app_power_request_poweroff(void) +{ + if (s_power.req_poweroff) { + return; /* 已经在关机流程里,不要重复挂定时器 */ + } + s_power.req_poweroff = 1; + log_info("request deep off\n"); + if (s_power.on_request) { + s_power.on_request(); /* 立刻停风扇、灯全灭 */ + } + sys_timeout_add(NULL, power_do_poweroff, 50); /* 给 GPIO 落稳一点再真正掉电 */ +} + +/** + * @brief 查询深睡请求是否已发起 + * @return 1=已发起,0=未发起 + */ +uint8_t app_power_poweroff_pending(void) +{ + return s_power.req_poweroff; +} + +/** + * @brief 当前这次关机是否为空电路径(本产品无电池,恒 0) + * @return 0 + */ +uint8_t app_power_empty_off_pending(void) +{ + return 0; +} + +/** + * @brief 50ms 延时到点:真正进入深睡 + * @param priv 未使用 + * @return 无 + */ +static void power_do_poweroff(void *priv) +{ + (void)priv; + log_info("enter deep off\n"); + if (s_power.on_finalize) { + s_power.on_finalize(); + } +} diff --git a/apps/usr_app/app_power.h b/apps/usr_app/app_power.h new file mode 100644 index 0000000..39ad2cd --- /dev/null +++ b/apps/usr_app/app_power.h @@ -0,0 +1,55 @@ +/****************************************************************************** + * @file app_power.h + * @brief CBR3 软关机生命周期:无电池、无充电,只保留 request/finalize + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V1.0.0 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹精简:删 USB/充电/电量/低电) + * + * @note 本产品 USB 常电、无电池,插电不会自动开机,必须长按 3s。 + * SDK 层 TCFG_CHARGE 相关配置保留为板级模板默认,但业务层完全不 + * 参与充电/电量逻辑(规格书第 0 章第 3 条)。 + ******************************************************************************/ + +#ifndef __APP_POWER_H__ +#define __APP_POWER_H__ + +#include + +/** 深睡请求的钩子:on_request 立刻停执行体并按关机类型处理灯;on_finalize 真正掉电 */ +typedef void (*AppPowerHook_t)(void); + +/** + * @brief 初始化电源模块状态 + * @return 无 + */ +void app_power_init(void); + +/** + * @brief 注册深睡请求的两段式回调 + * @param on_request 刚请求深睡时调用(停风扇;灯由调用方按关机类型决定) + * @param on_finalize 延时到点后调用(真正进深睡,不会返回) + * @return 无 + */ +void app_power_set_hooks(AppPowerHook_t on_request, AppPowerHook_t on_finalize); + +/** + * @brief 请求深睡(按键 3s 等):立即触发 on_request 钩子,50ms 后触发 on_finalize + * @return 无 + */ +void app_power_request_poweroff(void); + +/** + * @brief 查询深睡请求是否已发起(发起后其它模块应停止响应新操作) + * @return 1=已发起,0=未发起 + */ +uint8_t app_power_poweroff_pending(void); + +/** + * @brief 当前这次关机是否为空电路径(本产品无电池,恒 0,保留接口) + * @return 0 + */ +uint8_t app_power_empty_off_pending(void); + +#endif /* __APP_POWER_H__ */ diff --git a/apps/usr_app/app_th.c b/apps/usr_app/app_th.c new file mode 100644 index 0000000..af05ead --- /dev/null +++ b/apps/usr_app/app_th.c @@ -0,0 +1,171 @@ +/****************************************************************************** + * @file app_th.c + * @brief CBR3 温湿度采集实现 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V1.0.0 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布 + ******************************************************************************/ + +#include "app_th.h" +#include "bsp_aht20.h" +#include "board_pin.h" +#include "system/includes.h" + +#define LOG_TAG_CONST APP +#define LOG_TAG "[APP_TH]" +#define LOG_ERROR_ENABLE +#define LOG_INFO_ENABLE +#include "debug.h" + +extern u32 timer_get_ms(void); +extern void clr_wdt(void); + +/** 采样状态机 */ +typedef enum { + TH_ST_IDLE = 0, /**< 空闲,等待下一个采样周期 */ + TH_ST_WAIT_DATA /**< 命令已发出,等 ≥80ms 后读数 */ +} ThState_t; + +typedef struct { + uint8_t inited; + ThState_t state; + uint16_t poll_ms; /**< 距上次采样的累计毫秒 */ + uint32_t wait_t0; /**< 测量命令发出时刻 */ + int16_t temp_x10; /**< 最近有效温度 ×10 */ + uint8_t humi; /**< 最近有效湿度 % */ + uint8_t valid; /**< 1=有有效采样 */ + uint8_t fail_cnt; /**< 连续失败次数 */ + uint8_t last_ok; /**< 最近一次采样是否成功(P2 条件引擎 8.4 用) */ +} AppThCtx_t; + +static AppThCtx_t s_th; + +/** + * @brief 初始化温湿度模块 + * @return 无 + */ +void app_th_init(void) +{ +#if !BOARD_TH_ENABLE + log_info("th disabled\n"); + return; +#endif + if (s_th.inited) { + return; + } + bsp_aht20_init(); + s_th.state = TH_ST_IDLE; + s_th.poll_ms = 0; + s_th.valid = 0; + s_th.fail_cnt = 0; + s_th.last_ok = 0; + s_th.inited = 1; + log_info("th init period=%ums\n", (unsigned)BOARD_TH_PERIOD_MS); +} + +/** + * @brief 业务节拍:内部 2s 才真正采一次;IO 分离后采集与显示互不干扰 + * @param dt 距上次调用的毫秒数 + * @return 无 + */ +void app_th_poll(uint16_t dt) +{ +#if !BOARD_TH_ENABLE + (void)dt; + return; +#endif + if (!s_th.inited) { + return; + } + + if (s_th.state == TH_ST_IDLE) { + s_th.poll_ms = (uint16_t)(s_th.poll_ms + dt); + if (s_th.poll_ms >= BOARD_TH_PERIOD_MS) { + s_th.poll_ms = 0; + if (bsp_aht20_measure_start() == 0) { + s_th.state = TH_ST_WAIT_DATA; + s_th.wait_t0 = timer_get_ms(); + } + /* measure_start 非 0=通讯失败,跳过本次采样 */ + } + } else if (s_th.state == TH_ST_WAIT_DATA) { + clr_wdt(); + if ((timer_get_ms() - s_th.wait_t0) >= BOARD_AHT20_WAIT_MS) { + int16_t t = 0; + uint8_t h = 0; + int8_t ret = bsp_aht20_measure_finish(&t, &h); + + /* IO 分离后无总线竞争,finish 不会返回 -2;0=成功,-1=通讯失败 */ + s_th.state = TH_ST_IDLE; + if (ret == 0) { + s_th.temp_x10 = t; + s_th.humi = h; + s_th.fail_cnt = 0; + s_th.valid = 1; + s_th.last_ok = 1; + log_info("th %d.%dC %d%%\n", + (int)(t / 10), (int)((t < 0) ? (-t % 10) : (t % 10)), h); + } else { + s_th.fail_cnt++; + s_th.last_ok = 0; + log_error("th sample fail %d\n", s_th.fail_cnt); + } + } + } +} + +/** + * @brief 是否已有有效采样 + * @return 1=有效,0=从未成功 + */ +uint8_t app_th_get_valid(void) +{ + return s_th.valid; +} + +/** + * @brief 最近一次有效温度 + * @return 温度 ×10 + */ +int16_t app_th_get_temp_x10(void) +{ + return s_th.temp_x10; +} + +/** + * @brief 最近一次有效湿度 + * @return 湿度 0~100 整数 % + */ +uint8_t app_th_get_humi(void) +{ + return s_th.humi; +} + +/** + * @brief 连续失败次数 + * @return 连续失败次数 + */ +uint8_t app_th_get_fail_cnt(void) +{ + return s_th.fail_cnt; +} + +/** + * @brief 传感器故障(连续失败 ≥ BOARD_TH_FAIL_MAX) + * @return 1=故障(显示 --、fault_code=2),0=正常 + */ +uint8_t app_th_is_fault(void) +{ + return (s_th.fail_cnt >= BOARD_TH_FAIL_MAX) ? 1 : 0; +} + +/** + * @brief 最近一次采样是否成功(P2 条件引擎 8.4:时长只在采样成功时累加) + * @return 1=成功,0=失败/未采样过 + */ +uint8_t app_th_last_sample_ok(void) +{ + return s_th.last_ok; +} diff --git a/apps/usr_app/app_th.h b/apps/usr_app/app_th.h new file mode 100644 index 0000000..768f2d4 --- /dev/null +++ b/apps/usr_app/app_th.h @@ -0,0 +1,69 @@ +/****************************************************************************** + * @file app_th.h + * @brief CBR3 温湿度采集:2s 周期、失败计数、最近有效值 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V1.0.0 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布 + * + * @note 规格书 8.4:条件"持续时长"只在本次采样成功且比较成立时累加; + * 采样失败不累加、不清零;连续 3 次失败显示 "--" 且 fault_code=2。 + * P0 不做条件比较,只做采样与失败计数。 + ******************************************************************************/ + +#ifndef __APP_TH_H__ +#define __APP_TH_H__ + +#include + +/** + * @brief 初始化温湿度模块 + * @return 无 + */ +void app_th_init(void); + +/** + * @brief 业务节拍:内部 2s 才真正采一次;采的时候走总线互斥 + * @param dt 距上次调用的毫秒数 + * @return 无 + */ +void app_th_poll(uint16_t dt); + +/** + * @brief 是否已有有效采样 + * @return 1=有效,0=从未成功 + */ +uint8_t app_th_get_valid(void); + +/** + * @brief 最近一次有效温度 + * @return 温度 ×10(如 25.3℃ → 253) + */ +int16_t app_th_get_temp_x10(void); + +/** + * @brief 最近一次有效湿度 + * @return 湿度 0~100 整数 % + */ +uint8_t app_th_get_humi(void); + +/** + * @brief 连续失败次数 + * @return 连续失败次数 + */ +uint8_t app_th_get_fail_cnt(void); + +/** + * @brief 传感器故障(连续失败 ≥ BOARD_TH_FAIL_MAX) + * @return 1=故障(显示 --、fault_code=2),0=正常 + */ +uint8_t app_th_is_fault(void); + +/** + * @brief 最近一次采样是否成功(P2 条件引擎用,规格书 8.4) + * @return 1=成功,0=失败/未采样过 + */ +uint8_t app_th_last_sample_ok(void); + +#endif /* __APP_TH_H__ */ diff --git a/apps/usr_app/app_timer_task.c b/apps/usr_app/app_timer_task.c new file mode 100644 index 0000000..ee74db8 --- /dev/null +++ b/apps/usr_app/app_timer_task.c @@ -0,0 +1,264 @@ +/****************************************************************************** + * @file app_timer_task.c + * @brief CBR3 定时引擎(P2):7 组 8 字节定时窗,RTC 门闩 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V2.0.0 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布(P0 stub) + * - V1.1.0, 2026.08.31, cyWu, P1:set_en/set_config 存 VM(引擎仍关) + * - V2.0.0, 2026.08.31, cyWu, P2 实现完整定时引擎(规格书第 11 章): + * RTC 门闩(未对时一律不跑)/对时补窗/7 组窗判断(跨午夜、星期位图)/ + * 窗内切模式/结束收口(风扇 0 mode 0)/单次自毁清使能写 VM + * + * @note 运行上下文:app_timer_run 由 usr_timer_loop(500ms,任务上下文) + * 调用——窗判断分钟级精度足够,且单次自毁要写 VM(禁止中断上下文)。 + * 字节布局(规格书 11.1):Byte0=使能 Byte1=模式 Byte2=类型(0单次/ + * 1每天/2每周) Byte3=星期位图(Bit0=周一…Bit6=周日,仅类型2) + * Byte4-5=开始时分 Byte6-7=结束时分。 + * 12 章仲裁:定时 > 条件;手动介入后本窗剩余时间不自动抢回, + * 等下一窗开始沿或对时补窗;窗结束时若定时仍在占用才收口。 + ******************************************************************************/ + +#include "app_timer_task.h" +#include "app_arbiter.h" +#include "app_mode.h" +#include "app_fan.h" +#include "app_nvm.h" +#include "usr_rtc.h" +#include "jb_product.h" +#include "board_pin.h" + +#define LOG_TAG_CONST APP +#define LOG_TAG "[APP_TMR]" +#define LOG_INFO_ENABLE +#include "debug.h" + +/** 定时引擎运行时状态 */ +typedef struct { + uint8_t taken; /**< 1=本窗已接管过(手动打断后也不抢回,直到窗结束) */ + uint8_t win_idx; /**< 当前/最近命中组 0~6,0xFF=无 */ + uint8_t last_rtc_ok; /**< 上次 RTC 门闩状态(对时补窗沿检测) */ +} TimerState_t; + +static TimerState_t s_timer; + +/** + * @brief 取第 idx 组定时配置(gDevData 里是 7 个独立数组字段) + * @param idx 0~6 + * @return 配置指针;越界返回 NULL + */ +static uint8_t *timer_cfg_ptr(uint8_t idx) +{ + switch (idx) { + case 0: return gDevData.time_config1; + case 1: return gDevData.time_config2; + case 2: return gDevData.time_config3; + case 3: return gDevData.time_config4; + case 4: return gDevData.time_config5; + case 5: return gDevData.time_config6; + case 6: return gDevData.time_config7; + default: return NULL; + } +} + +/** + * @brief 计算某日期是星期几 + * @param y 年 + * @param m 月 1~12 + * @param d 日 + * @return 0=周一 … 6=周日(规格书 11.1 位图 Bit0=周一) + */ +static uint8_t timer_calc_dow(uint16_t y, uint8_t m, uint8_t d) +{ + uint8_t mm = m; + uint16_t yy = y; + uint16_t k; + int h; + + if (mm < 3) { + mm += 12; + yy--; + } + k = yy % 100; + /* 蔡勒公式(格里高利历):h=0 周六 … 6 周五 */ + h = (d + 13 * (mm + 1) / 5 + k + k / 4 + (yy / 100) / 4 + 5 * (yy / 100)) % 7; + return (uint8_t)((h + 5) % 7); /* 0=周一 … 6=周日 */ +} + +/** + * @brief 当前时刻是否在窗口内 + * @param cfg 8 字节配置 + * @param t 本地时间 + * @return 1=在窗内 + */ +static uint8_t timer_in_window(const uint8_t *cfg, const struct sys_time *t) +{ + uint8_t type = cfg[2]; + int16_t start = (int16_t)cfg[4] * 60 + cfg[5]; + int16_t end = (int16_t)cfg[6] * 60 + cfg[7]; + int16_t now = (int16_t)t->hour * 60 + t->min; + + if (start == end) { + return 0; /* 非法窗(零时长) */ + } + + if (type == 2) { + /* 每周:星期位图;跨午夜窗以"开始时刻所在本地星期"判断(11.1) */ + uint8_t dow = timer_calc_dow(t->year, t->month, t->day); + uint8_t dow_bit; + + if (start > end) { + /* 跨午夜窗:凌晨 [0,end) 属于昨晚的窗,用昨天星期 */ + if (now < end) { + dow = (uint8_t)((dow + 6) % 7); /* 昨天 */ + } + } + dow_bit = (uint8_t)(1u << dow); + if (!(cfg[3] & dow_bit)) { + return 0; + } + } + + if (start < end) { + return ((now >= start) && (now < end)) ? 1 : 0; + } + return ((now >= start) || (now < end)) ? 1 : 0; /* 跨午夜 */ +} + +/** + * @brief 窗结束收口:释放定时占用 + 风扇 0 / mode 0 + 单次自毁 + * @note 仅定时仍在占用时才收口(手动打断后不干预,12 章规则 2); + * 单次(类型 0)无论谁占用,结束时刻后清 Byte0 并写 VM(11.1) + * @return 无 + */ +static void timer_window_close(void) +{ + uint8_t idx = s_timer.win_idx; + + s_timer.taken = 0; + s_timer.win_idx = 0xFF; + + if (app_arbiter_get_owner() == ARB_OWNER_TIMER) { + app_arbiter_timer_release(); + app_mode_stop(); + app_fan_set_gear(0); + log_info("timer window out, stop (mode=0 fan=0)\n"); + } else { + log_info("timer window out (manual taken)\n"); + } + + /* 单次自毁:结束时刻过后 Byte0 清 0 并写 VM,不再跑(11.1) */ + if ((idx != 0xFF) && (timer_cfg_ptr(idx)[2] == 0)) { + timer_cfg_ptr(idx)[0] = 0; + app_nvm_mark_dirty(); + log_info("timer[%d] once done, en cleared\n", idx + 1); + } +} + +/** + * @brief 初始化定时引擎 + * @return 无 + */ +void app_timer_init(void) +{ +#if !BOARD_TIMER_ENABLE + log_info("timer engine disabled\n"); + return; +#endif + s_timer.taken = 0; + s_timer.win_idx = 0xFF; + s_timer.last_rtc_ok = 0; + log_info("timer init\n"); +} + +/** + * @brief 推进定时引擎(500ms,任务上下文,由 usr_timer_loop 调用) + * @param dt 距上次调用的毫秒数 + * @return 无 + */ +void app_timer_run(uint16_t dt) +{ +#if !BOARD_TIMER_ENABLE + (void)dt; + return; +#endif + struct sys_time t; + uint8_t rtc_ok = usr_rtc_get_time(&t); + uint8_t win_idx = 0xFF; + uint8_t i; + + (void)dt; + + /* 对时补窗(11.2):RTC 从无效→有效瞬间立即重判窗口 */ + if (rtc_ok && !s_timer.last_rtc_ok) { + log_info("timer rtc synced, re-check window\n"); + } + s_timer.last_rtc_ok = rtc_ok; + + /* RTC 门闩 / time_en=0:等同所有窗立刻结束(11.2) */ + if (!rtc_ok || !gDevData.time_en) { + if (s_timer.taken) { + timer_window_close(); + } + return; + } + + /* 找第一个命中的使能窗 */ + for (i = 0; i < 7; i++) { + const uint8_t *cfg = timer_cfg_ptr(i); + + if (!cfg || (cfg[0] == 0)) { + continue; + } + if (timer_in_window(cfg, &t)) { + win_idx = i; + break; + } + } + + if (win_idx != 0xFF) { + /* ── 窗内 ── */ + if (!s_timer.taken) { + /* 窗开始沿:接管(定时优先级最高,条件让位由条件引擎感知) */ + const uint8_t *cfg = timer_cfg_ptr(win_idx); + + app_arbiter_timer_take(); + app_mode_set_manual(cfg[1]); /* 切 Byte1 模式(通风循环/强力一轮) */ + s_timer.taken = 1; + s_timer.win_idx = win_idx; + log_info("timer[%d] window in, mode=%d\n", win_idx + 1, cfg[1]); + } + /* 已接管(或手动打断后不抢回,12 章规则 2):保持; + * 通风循环由 app_mode 自理;强力只一轮 15min 自停(11.3), + * 结束时刻由本函数窗结束沿收口 */ + } else { + if (s_timer.taken) { + timer_window_close(); + } + } +} + +/** + * @brief APP 写 DP13 定时总开关:存 gDevData + 触发 VM 持久化 + * @param en 0=关,1=开 + * @note 引擎关闭时仍保存(P1 验收);置 1 后下个 tick 补窗判断 + * @return 无 + */ +void app_timer_set_en(uint8_t en) +{ + app_nvm_mark_dirty(); + log_info("time_en=%d (stored)\n", en); +} + +/** + * @brief APP 写 DP14~20 定时配置:触发 VM 持久化(数据已在 gDevData) + * @param idx 1~7 + * @note 引擎关闭时仍保存 + * @return 无 + */ +void app_timer_set_config(uint8_t idx) +{ + app_nvm_mark_dirty(); + log_info("time_config[%d] stored\n", idx); +} diff --git a/apps/usr_app/app_timer_task.h b/apps/usr_app/app_timer_task.h new file mode 100644 index 0000000..ead9b41 --- /dev/null +++ b/apps/usr_app/app_timer_task.h @@ -0,0 +1,49 @@ +/****************************************************************************** + * @file app_timer_task.h + * @brief CBR3 定时引擎(P2):7 组 8 字节定时窗,RTC 门闩 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V1.0.0 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布(P0 仅接口与 stub) + * + * @note 文件名避开 usr_rtc。字节布局见规格书 11.1:Byte0=使能 Byte1=模式 + * Byte2=类型(0单次/1每天/2每周) Byte3=星期位图 Byte4-5=开始时/分 + * Byte6-7=结束时/分。RTC 未对时一律不执行(规格书 11.2)。 + * ⚠ app_timer_run 由 usr_timer_loop(500ms,任务上下文)调用 + * (单次自毁要写 VM,禁止放 20ms 中断上下文)。 + ******************************************************************************/ + +#ifndef __APP_TIMER_TASK_H__ +#define __APP_TIMER_TASK_H__ + +#include + +/** + * @brief 初始化定时引擎 + * @return 无 + */ +void app_timer_init(void); + +/** + * @brief 推进定时引擎,每个业务 tick 调用一次 + * @param dt 距上次调用的毫秒数 + * @return 无 + */ +void app_timer_run(uint16_t dt); + +/** + * @brief APP 写 DP13 定时总开关 + * @param en 0=关,1=开 + * @return 无 + */ +void app_timer_set_en(uint8_t en); + +/** + * @brief APP 写 DP14~20 定时配置(8 字节) + * @param idx 1~7 + * @return 无 + */ +void app_timer_set_config(uint8_t idx); + +#endif /* __APP_TIMER_TASK_H__ */ diff --git a/apps/usr_app/key.c b/apps/usr_app/key.c new file mode 100644 index 0000000..411d0a0 --- /dev/null +++ b/apps/usr_app/key.c @@ -0,0 +1,267 @@ +/****************************************************************************** + * @file key.c + * @brief 按键状态机实现 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.25 + * @version V1.0.0 + * @history + * - V1.0.0, 2026.08.25, cyWu, 首次发布,实现按键短按/长按/释放状态机 + ******************************************************************************/ + +#include "key.h" +#include + +static uint32_t s_key_count_time; +static uint8_t s_key_num = USR_KEY_COUNT; +keyCategory_t keyTable[USR_KEY_COUNT]; + +/** + * @brief 读按键电平到 fsm + * @param key 状态机 + * @return 1=已读,0=被屏蔽 + */ +static uint8_t get_key_level(keyFSM_t *key) +{ + if (key->keyShield == KEY_DISABLE) { + return 0; + } + if (key->keyReadValue == NULL) { + return 0; + } + key->keyLevel = (key->keyReadValue() == key->keyDownLevel) ? Bit_SET : Bit_RESET; + return 1; +} + +/** + * @brief 单击 / 双击间隔判定 + * @param click 状态机 + * @return 1=可以出事件 + */ +static uint8_t key_click_frequency(keyFSM_t *click) +{ + click->keyInterval++; + switch (click->keyFrequency) { + case CLICK: + if (click->keyInterval >= (KEY_INTERVAL / (DEBOUNCE_TIME * KEY_TIMER_MS))) { + click->eventType = SHORT_Event; + return 1; + } + break; + case DbLCLICK: + if (click->keyInterval >= (KEY_INTERVAL / (DEBOUNCE_TIME * KEY_TIMER_MS))) { + click->eventType = DBCL_Event; + return 1; + } + break; + default: + return 1; + } + return 0; +} + +/** + * @brief 按键状态机步进 + * @param key_buf 状态机 + * @return 1=已处理 + */ +static uint8_t read_key_status(keyFSM_t *key_buf) +{ + if (!get_key_level(key_buf)) { + return 0; + } + + switch (key_buf->keyStatus) { + case KEY_ST_NULL: + if (key_buf->keyLevel == Bit_SET) { + key_buf->keyStatus = KEY_ST_SURE; + } + break; + case KEY_ST_SURE: + if (key_buf->keyLevel == Bit_SET) { + key_buf->eventType = DOWN_Event; + key_buf->keyStatus = KEY_ST_DOWN; + key_buf->keyCount = 0; + key_buf->keyLongFlag = 1; + } else { + key_buf->keyStatus = KEY_ST_NULL; + } + break; + case KEY_ST_DOWN: + if (key_buf->keyLevel != Bit_SET) { + if (key_buf->keyShortFlag == 0) { + key_buf->keyShortFlag = 1; + key_buf->keyFrequency++; + key_buf->keyInterval = 0; + } + if (key_click_frequency(key_buf)) { + key_buf->keyFrequency = 0; + key_buf->keyStatus = KEY_ST_NULL; + } + if ((key_buf->keyCount >= key_buf->keyLongTime / DEBOUNCE_TIME) + && (key_buf->keyCount < key_buf->keyLastTime / DEBOUNCE_TIME)) { + key_buf->keyFrequency = 0; + key_buf->keyStatus = KEY_ST_NULL; + key_buf->eventType = RELEASE_Event; + } + key_buf->keyCount = 0; + key_buf->keyLongFlag = 1; + } else { + if ((++key_buf->keyCount >= key_buf->keyLastTime / DEBOUNCE_TIME)) { + key_buf->keyCount = 0; + key_buf->keyFrequency = 0; + key_buf->keyStatus = KEY_ST_LONG; + key_buf->eventType = LAST_Event; + key_buf->keyLongFlag = 1; + } + if ((key_buf->keyCount >= key_buf->keyLongTime / DEBOUNCE_TIME) + && (key_buf->keyCount < key_buf->keyLastTime / DEBOUNCE_TIME)) { + key_buf->keyFrequency = 0; + if (key_buf->keyLongFlag == 1) { + key_buf->eventType = LONG_Event; + key_buf->keyLongFlag = 0; + } + } + key_buf->keyShortFlag = 0; + } + break; + case KEY_ST_LONG: + if (key_buf->keyLevel != Bit_SET) { + key_buf->keyStatus = KEY_ST_NULL; + key_buf->eventType = RELEASE_Event; + } + break; + default: + break; + } + return 1; +} + +/** + * @brief 扫描全部按键状态机 + * @return 无 + */ +static void key_event_process(void) +{ + size_t i; + + for (i = 0; i < s_key_num; i++) { + (void)read_key_status(&keyTable[i].fsm); + } +} + +/** + * @brief 1ms 调用一次;满 DEBOUNCE_TIME 才真正采样 + * @return 无 + */ +void keyCheckProcess(void) +{ + s_key_count_time++; + if (s_key_count_time >= (DEBOUNCE_TIME / KEY_TIMER_MS)) { + s_key_count_time = 0; + key_event_process(); + } +} + +/** + * @brief 拷贝产品按键表 + * @param keys 按键数组,长度 USR_KEY_COUNT + * @return 无 + */ +void keyParaInit(keyCategory_t *keys) +{ + if (keys == NULL) { + return; + } + if (USR_KEY_COUNT >= KEY_MAX_NUMBER) { + s_key_num = KEY_MAX_NUMBER; + } + memcpy(keyTable, keys, sizeof(keyCategory_t) * s_key_num); +} + +/** + * @brief 运行中改长按/连按时长并复位状态 + * @param key_event 按键索引 + * @param key_param 新参数 + * @return 无 + */ +void setKeyEventParams(uint8_t key_event, keyCategory_t key_param) +{ + if (key_event >= s_key_num) { + return; + } + keyTable[key_event].fsm.keyShield = key_param.fsm.keyShield; + keyTable[key_event].fsm.keyLongTime = key_param.fsm.keyLongTime; + keyTable[key_event].fsm.keyLastTime = key_param.fsm.keyLastTime; + keyTable[key_event].fsm.keyStatus = KEY_ST_NULL; + keyTable[key_event].fsm.eventType = NULL_Event; + keyTable[key_event].fsm.keyCount = 0; +} + +/** + * @brief 派发按键事件回调 + * @return 无 + */ +void keyHandle(void) +{ + size_t i; + + for (i = 0; i < s_key_num; i++) { + if (keyTable[i].fsm.eventType == NULL_Event) { + continue; + } + switch (keyTable[i].fsm.eventType) { + case RELEASE_Event: + if (keyTable[i].func.releasePressCb != NULL) { + keyTable[i].func.releasePressCb(); + } + keyTable[i].fsm.eventType = NULL_Event; + break; + case SHORT_Event: + if (keyTable[i].func.ShortPressCb != NULL) { + keyTable[i].func.ShortPressCb(); + } + keyTable[i].fsm.eventType = NULL_Event; + break; + case DOWN_Event: + if (keyTable[i].func.downPressCb != NULL) { + keyTable[i].func.downPressCb(); + } + keyTable[i].fsm.eventType = NULL_Event; + break; + case LONG_Event: + if (keyTable[i].func.longPressCb != NULL) { + keyTable[i].func.longPressCb(); + } + keyTable[i].fsm.eventType = NULL_Event; + break; + case LAST_Event: + if (keyTable[i].func.lastPressCb != NULL) { + keyTable[i].func.lastPressCb(); + } + break; + case DBCL_Event: + if (keyTable[i].func.dbclPressCb != NULL) { + keyTable[i].func.dbclPressCb(); + } + keyTable[i].fsm.eventType = NULL_Event; + break; + default: + break; + } + } +} + +/** + * @brief 复位指定按键状态机 + * @param key_index 按键索引 + * @return 无 + */ +void reset_key_Status(keyList key_index) +{ + if (key_index >= USR_KEY_COUNT) { + return; + } + keyTable[key_index].fsm.keyStatus = KEY_ST_NULL; + keyTable[key_index].fsm.eventType = NULL_Event; + keyTable[key_index].fsm.keyCount = 0; +} diff --git a/apps/usr_app/key.h b/apps/usr_app/key.h new file mode 100644 index 0000000..9634cb1 --- /dev/null +++ b/apps/usr_app/key.h @@ -0,0 +1,100 @@ +/****************************************************************************** + * @file key.h + * @brief 按键状态机:短按 / 长按 / 释放 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.25 + * @version V1.0.0 + * @history + * - V1.0.0, 2026.08.25, cyWu, 首次发布,实现按键短按/长按/释放状态机 + ******************************************************************************/ + +#ifndef __KEY_H__ +#define __KEY_H__ + +#include +#include + +/*============================================================================*/ +/* 宏定义 */ +/*============================================================================*/ + +#define KEY_TIMER_MS 1 +#define KEY_MAX_NUMBER 12 +#define DEBOUNCE_TIME 30 +#define KEY_INTERVAL 30 + +#define CLICK 1 +#define DbLCLICK 2 + +typedef enum { + USR_KEY_USER = 0, + USR_KEY_COUNT +} keyList; + +typedef enum { + Bit_RESET = 0, + Bit_SET +} BitAction_t; + +typedef enum { + KEY_ST_NULL = 0, + KEY_ST_RELEASE, + KEY_ST_SURE, + KEY_ST_UP, + KEY_ST_DOWN, + KEY_ST_LONG +} keyStatus_t; + +typedef enum { + NULL_Event = 0, + DOWN_Event, + SHORT_Event, + LONG_Event, + LAST_Event, + DBCL_Event, + RELEASE_Event +} keyEvent_t; + +typedef enum { + KEY_DISABLE = 0, + KEY_ENABLE = !KEY_DISABLE +} keyEnable_t; + +typedef struct { + uint8_t keyShortFlag; + uint8_t keyLongFlag; + uint8_t keyInterval; + uint8_t keyFrequency; + uint16_t keyLongTime; + uint16_t keyLastTime; + uint32_t keyCount; + keyEnable_t keyShield; + BitAction_t keyLevel; + BitAction_t keyDownLevel; + keyStatus_t keyStatus; + keyEvent_t eventType; + uint8_t (*keyReadValue)(void); +} keyFSM_t; + +typedef struct { + void (*nullPressCb)(void); + void (*releasePressCb)(void); + void (*downPressCb)(void); + void (*ShortPressCb)(void); + void (*longPressCb)(void); + void (*lastPressCb)(void); + void (*dbclPressCb)(void); +} keyFunc_t; + +typedef struct { + keyFSM_t fsm; + keyFunc_t func; +} keyCategory_t; + +void keyParaInit(keyCategory_t *keys); +void setKeyEventParams(uint8_t key_event, keyCategory_t key_param); +void keyCheckProcess(void); +void keyHandle(void); +void reset_key_Status(keyList key_index); + +#endif /* __KEY_H__ */ diff --git a/apps/usr_app/key_manager.c b/apps/usr_app/key_manager.c new file mode 100644 index 0000000..9514159 --- /dev/null +++ b/apps/usr_app/key_manager.c @@ -0,0 +1,148 @@ +/****************************************************************************** + * @file key_manager.c + * @brief CBR3 按键:读 PB1,短按切档、长按 3s 关机交给 usr_jb_main + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V1.0.0 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹,短按语义改切档) + ******************************************************************************/ + +#include "system/includes.h" +#include "key_manager.h" +#include "bsp_hw.h" +#include "board_pin.h" + +#define LOG_TAG_CONST APP +#define LOG_TAG "[KEY]" +#define LOG_INFO_ENABLE +#include "debug.h" + +/** 模块状态,替代原来 2 个散落的 static */ +typedef struct { + uint8_t initialized; + uint8_t boot_key_lock; /**< 1=开机判定时键还按着,等松开才认短按/长按 */ +} KeyManagerCtx_t; + +static KeyManagerCtx_t s_km; + +/** + * @brief 读按键:1=按下 + * @return 1=按下,0=松开 + */ +static uint8_t key_user_read(void) +{ + return bsp_key_is_pressed(); +} + +/** + * @brief 松开:清掉开机按着键的锁 + * @return 无 + */ +static void key_user_release(void) +{ + if (s_km.boot_key_lock) { + s_km.boot_key_lock = 0; + log_info("boot key released\n"); + } +} + +/** + * @brief 短按:开机未松手前忽略 + * @return 无 + */ +static void key_user_short(void) +{ + if (s_km.boot_key_lock) { + s_km.boot_key_lock = 0; /* 开机松手当成释放,不当短按切档 */ + log_info("boot key released\n"); + return; + } + usr_on_key_short(); +} + +/** + * @brief 长按 3s:开机未松手前忽略,避免开机手还按着就关机 + * @return 无 + */ +static void key_user_long(void) +{ + if (s_km.boot_key_lock) { + return; /* 开机手还按着,忽略这次 3s 长按,避免一开机就关机 */ + } + usr_on_key_long(); +} + +static keyCategory_t s_keys[USR_KEY_COUNT] = { + [USR_KEY_USER] = { + .fsm.keyShield = KEY_ENABLE, + .fsm.keyDownLevel = Bit_SET, + .fsm.eventType = NULL_Event, + .fsm.keyLongTime = BOARD_KEY_POWER_MS, + .fsm.keyLastTime = (BOARD_KEY_POWER_MS + 7000), /* 大于长按即可,不使用持续按 */ + .fsm.keyReadValue = key_user_read, + .func.ShortPressCb = key_user_short, + .func.longPressCb = key_user_long, + .func.releasePressCb = key_user_release, + }, +}; + +/** + * @brief 装载按键表 + * @return 无 + */ +void user_key_init(void) +{ + if (s_km.initialized) { + return; + } + keyParaInit(s_keys); + s_km.boot_key_lock = 0; + s_km.initialized = 1; +} + +/** + * @brief 开机判定结束后:键还按着则锁住,松开才认短按/长按 + * @return 无 + */ +void user_key_lock_if_held(void) +{ + reset_key_Status(USR_KEY_USER); + if (bsp_key_is_pressed()) { + s_km.boot_key_lock = 1; /* 开机手还按着,等松开才认短按/长按,避免立刻关机 */ + log_info("boot key held, wait release\n"); + } +} + +/** + * @brief 按真实毫秒推进状态机 + * @param dt 距上次调用的毫秒数 + * @return 无 + */ +void user_key_scan(uint16_t dt) +{ + uint16_t i; + + if (!s_km.initialized) { + return; + } + if (dt == 0) { + dt = 1; + } + /* 按键库按 1ms 步进,业务节拍是 20ms,这里循环 dt 次对齐真实时间 */ + for (i = 0; i < dt; i++) { + keyCheckProcess(); + } +} + +/** + * @brief 派发按键回调 + * @return 无 + */ +void user_key_handle(void) +{ + if (!s_km.initialized) { + return; + } + keyHandle(); +} diff --git a/apps/usr_app/key_manager.h b/apps/usr_app/key_manager.h new file mode 100644 index 0000000..eb6dd6a --- /dev/null +++ b/apps/usr_app/key_manager.h @@ -0,0 +1,53 @@ +/****************************************************************************** + * @file key_manager.h + * @brief CBR3 单按键:短按切档,长按 3s 关机;开机 3s/5s/12s 不走本库 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V1.0.0 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹,语义改切档/关机) + ******************************************************************************/ + +#ifndef __KEY_MANAGER_H__ +#define __KEY_MANAGER_H__ + +#include "key.h" + +/** + * @brief 业务层短按回调(app_function 实现) + * @return 无 + */ +void usr_on_key_short(void); + +/** + * @brief 业务层长按 3s 回调(app_function 实现) + * @return 无 + */ +void usr_on_key_long(void); + +/** + * @brief 装载按键表 + * @return 无 + */ +void user_key_init(void); + +/** + * @brief 开机判定结束后:若键还按着则锁住,松开才认短按/长按 + * @return 无 + */ +void user_key_lock_if_held(void); + +/** + * @brief 按真实毫秒推进状态机(20ms 节拍会循环 dt 次) + * @param dt 距上次调用的毫秒数 + * @return 无 + */ +void user_key_scan(uint16_t dt); + +/** + * @brief 派发按键回调 + * @return 无 + */ +void user_key_handle(void); + +#endif /* __KEY_MANAGER_H__ */ diff --git a/apps/usr_app/led.c b/apps/usr_app/led.c new file mode 100644 index 0000000..8fc5823 --- /dev/null +++ b/apps/usr_app/led.c @@ -0,0 +1,328 @@ +/****************************************************************************** + * @file led.c + * @brief LED 事件框架实现:只改 ledMask 对应通道,红绿互不抢 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.25 + * @version V1.0.0 + * @history + * - V1.0.0, 2026.08.25, cyWu, 首次发布,实现按通道掩码独立开关的 LED 事件框架 + ******************************************************************************/ + +#include "led.h" +#include + +static LED_EventTableItem_t s_led_priority_table[PRIORITY_MAX]; +static LED_EventTableItem_t s_led_event_table[LED_EVENT_MAX]; +static LED_SetState_t s_led_set_state; +static TIME_Get_t s_time_get; + +static uint32_t get_tick_diff(uint32_t meiosis); +static uint8_t led_lighting_update(LED_Config_t *led); +#if (BLINK_ENABLE) +static uint8_t led_blinking_update(LED_Config_t *led); +#endif +#if (ALTERNATING_BLINK_ENABLE) +static uint8_t led_alternating_update(LED_Config_t *led); +#endif + +/** + * @brief 注册写灯回调,只对掩码里的通道动手 + * @param setState 回调,led=通道掩码,state=LED_ON/OFF + * @return 无 + */ +void LED_SetStateInit(LED_SetState_t setState) +{ + s_led_set_state = setState; +} + +/** + * @brief 注册毫秒时间戳回调 + * @param getTime 回调 + * @return 无 + */ +void SET_TimeGetInit(TIME_Get_t getTime) +{ + s_time_get = getTime; +} + +/** + * @brief 拷贝产品事件表 + * @param ledEvent 长度必须为 LED_EVENT_MAX + * @return 无 + */ +void LED_EventTableInit(LED_EventTableItem_t ledEvent[]) +{ + if (ledEvent == NULL) { + return; + } + memset(s_led_priority_table, 0, sizeof(s_led_priority_table)); + memset(s_led_event_table, 0, sizeof(s_led_event_table)); + memcpy(s_led_event_table, ledEvent, sizeof(s_led_event_table)); +} + +/** + * @brief 把事件挂到对应优先级槽;同优先级会被覆盖 + * @param ledEvent 事件号 + * @return 无 + */ +void LED_EventAdd(LED_Event_t ledEvent) +{ + size_t i; + LED_EventTableItem_t *item; + LED_EventPriority_t prio; + + if ((s_time_get == NULL) || (s_led_set_state == NULL)) { + return; + } + if (ledEvent >= LED_EVENT_MAX) { + return; + } + + for (i = 0; i < LED_EVENT_MAX; i++) { + item = &s_led_event_table[i]; + if (item->event != ledEvent) { + continue; + } + prio = item->priority; + if (prio >= PRIORITY_MAX) { + return; + } + if (s_led_priority_table[prio].event != ledEvent) { + item->config.currentTime = s_time_get(); + if (s_led_priority_table[prio].event != LED_EVENT_NONE) { + /* 同优先级换事件:只关掉旧灯里新事件用不到的通道 */ + s_led_set_state((uint16_t)(s_led_priority_table[prio].config.ledMask + ^ item->config.ledMask), + LED_OFF); + } + } + s_led_priority_table[prio] = *item; + return; + } +} + +/** + * @brief 清空优先级表(不改 GPIO,由调用方灭灯) + * @return 无 + */ +void LED_EventAllDelete(void) +{ + memset(s_led_priority_table, 0, sizeof(s_led_priority_table)); +} + +/** + * @brief 删除指定事件并关掉它占用的通道 + * @param ledEvent 事件号 + * @return 无 + */ +void LED_EventDelete(LED_Event_t ledEvent) +{ + size_t i; + + for (i = 0; i < PRIORITY_MAX; i++) { + if (s_led_priority_table[i].event != ledEvent) { + continue; + } + memset(&s_led_priority_table[i], 0, sizeof(LED_EventTableItem_t)); + if ((s_led_set_state != NULL) && (ledEvent < LED_EVENT_MAX)) { + s_led_set_state(s_led_event_table[ledEvent].config.ledMask, LED_OFF); + } + return; + } +} + +/** + * @brief 从高优先级往下跑事件 + * @note handler 返回 1 会拦住更低优先级。红绿要共存时 handler 必须返回 0 + * @return 无 + */ +void LED_EventHandle(void) +{ + int i; + uint8_t ret; + + for (i = (int)PRIORITY_MAX - 1; i >= 0; i--) { + if (s_led_priority_table[i].event == LED_EVENT_NONE) { + continue; + } + if (s_led_priority_table[i].ledEventHandler == NULL) { + continue; + } + ret = s_led_priority_table[i].ledEventHandler(&s_led_priority_table[i].config); + if (ret) { + return; + } + } +} + +/** + * @brief 是否有事件在跑 + * @return 1=有,0=无 + */ +uint8_t LED_IsEventActive(void) +{ + size_t i; + + for (i = 0; i < PRIORITY_MAX; i++) { + if (s_led_priority_table[i].event != LED_EVENT_NONE) { + return 1; + } + } + return 0; +} + +/** + * @brief 指定事件是否挂在优先级表里 + * @param ledEvent 事件号 + * @return 1=在,0=不在 + */ +uint8_t LED_IsEventExist(LED_Event_t ledEvent) +{ + size_t i; + + for (i = 0; i < PRIORITY_MAX; i++) { + if (s_led_priority_table[i].event == ledEvent) { + return 1; + } + } + return 0; +} + +/** + * @brief 按配置刷新一次灯效 + * @param led 配置,不可为空 + * @return 1=还在跑,0=次数用完 + */ +uint8_t LED_Update(LED_Config_t *led) +{ + if (led == NULL) { + return 0; + } + + switch (led->state) { + case LED_LIGHT: + return led_lighting_update(led); +#if (BLINK_ENABLE) + case LED_BLINKING: + return led_blinking_update(led); +#endif +#if (ALTERNATING_BLINK_ENABLE) + case LED_ALTERNATING_BLINK: + return led_alternating_update(led); +#endif + default: + return 0; + } +} + +/** + * @brief 毫秒时间差,兼容溢出 + * @param meiosis 起点时间戳 + * @return 差值 ms + */ +static uint32_t get_tick_diff(uint32_t meiosis) +{ + uint32_t now; + + if (s_time_get == NULL) { + return 0; + } + now = s_time_get(); + if (now >= meiosis) { + return now - meiosis; + } + return (0xFFFFFFFFu - meiosis) + now; +} + +/** + * @brief 常亮;runNum 按秒计,0=一直亮 + * @param led 配置 + * @return 1=还在亮,0=到点灭 + */ +static uint8_t led_lighting_update(LED_Config_t *led) +{ + uint16_t sec_count; + + if (s_led_set_state == NULL) { + return 0; + } + sec_count = (uint16_t)(get_tick_diff(led->currentTime) / 1000u); + if ((led->runNum > 0) && (sec_count >= led->runNum)) { + s_led_set_state(led->ledMask, LED_OFF); + return 0; + } + s_led_set_state(led->ledMask, LED_ON); + return 1; +} + +#if (BLINK_ENABLE) +/** + * @brief 闪烁;runNum 按完整周期计,0=一直闪 + * @param led 配置 + * @return 1=还在闪,0=到点灭 + */ +static uint8_t led_blinking_update(LED_Config_t *led) +{ + uint32_t run_cycle; + uint16_t blink_count; + uint32_t sec_left; + + if (s_led_set_state == NULL) { + return 0; + } + run_cycle = led->blinkParams.onTime + led->blinkParams.offTime; + if (run_cycle == 0) { + return 0; + } + blink_count = (uint16_t)(get_tick_diff(led->currentTime) / run_cycle); + sec_left = get_tick_diff(led->currentTime) % run_cycle; + if ((led->runNum > 0) && (blink_count >= led->runNum)) { + s_led_set_state(led->ledMask, LED_OFF); + return 0; + } + if (sec_left <= led->blinkParams.onTime) { + s_led_set_state(led->ledMask, led->blinkParams.orDer ? LED_OFF : LED_ON); + } else { + s_led_set_state(led->ledMask, led->blinkParams.orDer ? LED_ON : LED_OFF); + } + return 1; +} +#endif + +#if (ALTERNATING_BLINK_ENABLE) +/** + * @brief 两组灯按时间片轮流亮 + * @param led 配置 + * @return 1=还在跑,0=到点灭 + */ +static uint8_t led_alternating_update(LED_Config_t *led) +{ + uint16_t blink_count; + uint32_t sec_left; + uint32_t time_slice; + size_t i; + + if (s_led_set_state == NULL) { + return 0; + } + if (led->alternatingBlinkParams.Cycle == 0) { + return 0; + } + blink_count = (uint16_t)(get_tick_diff(led->currentTime) + / led->alternatingBlinkParams.Cycle); + sec_left = get_tick_diff(led->currentTime) % led->alternatingBlinkParams.Cycle; + if ((led->runNum > 0) && (blink_count >= led->runNum)) { + s_led_set_state(led->ledMask, LED_OFF); + return 0; + } + time_slice = led->alternatingBlinkParams.Cycle / ALTERNAT_GROUP; + s_led_set_state(led->ledMask, LED_OFF); + for (i = 0; i < ALTERNAT_GROUP; i++) { + if ((sec_left > (time_slice * i)) && (sec_left <= (time_slice * (i + 1)))) { + s_led_set_state(led->alternatingBlinkParams.Group[i], LED_ON); + break; + } + } + return 1; +} +#endif diff --git a/apps/usr_app/led.h b/apps/usr_app/led.h new file mode 100644 index 0000000..4393f96 --- /dev/null +++ b/apps/usr_app/led.h @@ -0,0 +1,120 @@ +/****************************************************************************** + * @file led.h + * @brief LED 事件框架:按通道掩码独立开关 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.31 + * @version V1.2.0 + * @history + * - V1.0.0, 2026.08.25, cyWu, 首次发布,实现按通道掩码独立开关的 LED 事件框架 + * - V1.1.0, 2026.08.27, cyWu, 新增 LED_EVENT_BOOT_REJECT + * - V1.2.0, 2026.08.31, cyWu, CBR3 单灯:裁掉充电/低电/空电等门夹事件 + ******************************************************************************/ + +#ifndef __LED_H__ +#define __LED_H__ + +#include +#include + +/*============================================================================*/ +/* 宏定义 */ +/*============================================================================*/ + +#define BLINK_ENABLE 1 +#define ALTERNATING_BLINK_ENABLE 0 +#define BREATH_ENABLE 0 +#define RUNNING_ENABLE 0 +#define MARQUEE_ENABLE 0 + +#define LED_ON 1 +#define LED_OFF 0 + +#define LED_CHANNEL(user_led) (1u << (user_led)) + +#define ledPrintf(level, fmt, ...) ((void)0) + +/*============================================================================*/ +/* 类型 */ +/*============================================================================*/ + +typedef enum { + LED_LIGHT = 0, /**< 常亮 */ + LED_BLINKING, /**< 闪烁 */ + LED_ALTERNATING_BLINK /**< 交替闪(本产品未用) */ +} LED_State_t; + +typedef enum { + LED_EVENT_NONE = 0, + LED_EVENT_STANDBY, /**< 开机常亮 */ + LED_EVENT_PAIRING, /**< 配对快闪 */ + LED_EVENT_PAIR_OK, /**< 配对成功常亮若干秒 */ + LED_EVENT_PAIR_FAIL, /**< 配对失败闪若干秒 */ + LED_EVENT_OTA, /**< OTA 闪,直到升级结束/复位 */ + LED_EVENT_MAX +} LED_Event_t; + +/** + * 同优先级会互相覆盖 + */ +typedef enum { + PRIORITY_1 = 0, /**< 开机常亮 */ + PRIORITY_2, /**< 配对中 / 配对结果 */ + PRIORITY_3, /**< OTA */ + PRIORITY_MAX +} LED_EventPriority_t; + +#if (BLINK_ENABLE) +typedef struct { + uint8_t orDer; /**< 1=先灭后亮,0=先亮后灭 */ + uint32_t onTime; /**< 亮时间 ms */ + uint32_t offTime; /**< 灭时间 ms */ +} LED_BlinkParams_t; +#endif + +#if (ALTERNATING_BLINK_ENABLE) +#define ALTERNAT_GROUP 2 +typedef struct { + uint16_t Group[ALTERNAT_GROUP]; /**< 交替两组的通道掩码 */ + uint32_t Cycle; /**< 一整轮交替周期 ms */ +} LED_AlternatingBlinkParams_t; +#endif + +typedef struct { + LED_State_t state; + uint16_t ledMask; /**< 本事件只动这些通道,其它灯不受影响 */ + uint16_t runNum; /**< 0=无限;常亮按秒计,闪烁按周期计 */ + uint32_t currentTime; +#if (BLINK_ENABLE) + LED_BlinkParams_t blinkParams; +#endif +#if (ALTERNATING_BLINK_ENABLE) + LED_AlternatingBlinkParams_t alternatingBlinkParams; +#endif +} LED_Config_t; + +typedef struct { + LED_Event_t event; + LED_EventPriority_t priority; + LED_Config_t config; + uint8_t (*ledEventHandler)(LED_Config_t *config); +} LED_EventTableItem_t; + +typedef void (*LED_SetState_t)(uint16_t led, uint8_t state); +typedef uint32_t (*TIME_Get_t)(void); + +/*============================================================================*/ +/* 外部接口 */ +/*============================================================================*/ + +void LED_SetStateInit(LED_SetState_t setState); +void SET_TimeGetInit(TIME_Get_t getTime); +void LED_EventTableInit(LED_EventTableItem_t ledEvent[]); +void LED_EventAdd(LED_Event_t ledEvent); +void LED_EventDelete(LED_Event_t ledEvent); +void LED_EventAllDelete(void); +void LED_EventHandle(void); +uint8_t LED_IsEventActive(void); +uint8_t LED_IsEventExist(LED_Event_t ledEvent); +uint8_t LED_Update(LED_Config_t *led); + +#endif /* __LED_H__ */ diff --git a/apps/usr_app/led_manager.c b/apps/usr_app/led_manager.c new file mode 100644 index 0000000..bad623e --- /dev/null +++ b/apps/usr_app/led_manager.c @@ -0,0 +1,193 @@ +/****************************************************************************** + * @file led_manager.c + * @brief CBR3 灯事件:单灯 PC2,高优先级覆盖低优先级 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V1.0.0 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹,删红绿双通道改单灯) + * + * @note 灯效优先级(见规格书第 6 章,高者覆盖): + * OTA(200ms 闪) > 配对中(100ms 快闪) / 配对结果 > 正常开机常亮。 + * 单灯无充电/低电/空电事件。 + ******************************************************************************/ + +#include "led_manager.h" +#include "system/includes.h" +#include "bsp_hw.h" +#include "board_pin.h" + +static uint8_t led_keep_handler(LED_Config_t *led_config); +static uint8_t led_standby_handler(LED_Config_t *led_config); +static uint8_t led_pairing_handler(LED_Config_t *led_config); +static uint8_t led_pair_ok_handler(LED_Config_t *led_config); +static uint8_t led_pair_fail_handler(LED_Config_t *led_config); +static uint8_t led_ota_handler(LED_Config_t *led_config); + +/** + * @brief 只改掩码里的通道(单灯即唯一通道) + * @param led 通道掩码 + * @param state LED_ON / LED_OFF + * @return 无 + */ +static void set_led_multi(uint16_t led, uint8_t state) +{ + if (led & LED_CHANNEL(LED_CH_ON)) { + bsp_led_set(state); + } +} + +/** + * @brief 常亮/闪烁一直跑:返回 1 独占,单灯不允许叠加 + * @param led_config 配置 + * @return 1 + */ +static uint8_t led_keep_handler(LED_Config_t *led_config) +{ + (void)LED_Update(led_config); + return 1; +} + +/** + * @brief 待机常亮;runNum>0 时亮满后删事件 + * @param led_config 配置 + * @return 1 + */ +static uint8_t led_standby_handler(LED_Config_t *led_config) +{ + if (LED_Update(led_config) == 0) { + LED_EventDelete(LED_EVENT_STANDBY); + } + return 1; +} + +/** + * @brief 配对快闪(无限) + * @param led_config 配置 + * @return 1 + */ +static uint8_t led_pairing_handler(LED_Config_t *led_config) +{ + (void)LED_Update(led_config); + return 1; +} + +/** + * @brief 配对成功常亮若干秒后删事件 + * @param led_config 配置 + * @return 1 + */ +static uint8_t led_pair_ok_handler(LED_Config_t *led_config) +{ + if (LED_Update(led_config) == 0) { + LED_EventDelete(LED_EVENT_PAIR_OK); + } + return 1; +} + +/** + * @brief 配对失败 200ms 闪若干秒后删事件 + * @param led_config 配置 + * @return 1 + */ +static uint8_t led_pair_fail_handler(LED_Config_t *led_config) +{ + if (LED_Update(led_config) == 0) { + LED_EventDelete(LED_EVENT_PAIR_FAIL); + } + return 1; +} + +/** + * @brief OTA 200ms 闪(无限):最高优先级 + * @param led_config 配置 + * @return 1 + */ +static uint8_t led_ota_handler(LED_Config_t *led_config) +{ + (void)LED_Update(led_config); + return 1; +} + +/** + * @brief 注册回调并装载 CBR3 事件表(GPIO 已在 bsp_hw_init) + * @return 无 + */ +void user_led_init(void) +{ + static LED_EventTableItem_t led_event_category[LED_EVENT_MAX] = { + [LED_EVENT_STANDBY] = { + /* 正常开机常亮 */ + .event = LED_EVENT_STANDBY, + .priority = PRIORITY_1, + .config.ledMask = LED_CHANNEL(LED_CH_ON), + .config.state = LED_LIGHT, +#if BOARD_LED_STANDBY_ON + .config.runNum = 0, /* 0=一直亮 */ +#else + .config.runNum = 1, +#endif + .ledEventHandler = led_standby_handler, + }, + [LED_EVENT_PAIRING] = { + /* 配对中 100ms 快闪(无限) */ + .event = LED_EVENT_PAIRING, + .priority = PRIORITY_2, + .config.ledMask = LED_CHANNEL(LED_CH_ON), + .config.state = LED_BLINKING, + .config.blinkParams.orDer = 0, + .config.blinkParams.onTime = BOARD_LED_FAST_ON_MS, + .config.blinkParams.offTime = BOARD_LED_FAST_OFF_MS, + .config.runNum = 0, + .ledEventHandler = led_pairing_handler, + }, + [LED_EVENT_PAIR_OK] = { + /* 配对成功常亮若干秒(允许直接回到常亮) */ + .event = LED_EVENT_PAIR_OK, + .priority = PRIORITY_2, + .config.ledMask = LED_CHANNEL(LED_CH_ON), + .config.state = LED_LIGHT, + .config.runNum = (uint16_t)((BOARD_LED_PAIR_OK_MS + 999u) / 1000u), + .ledEventHandler = led_pair_ok_handler, + }, + [LED_EVENT_PAIR_FAIL] = { + /* 配对失败 200ms 闪提示若干秒 */ + .event = LED_EVENT_PAIR_FAIL, + .priority = PRIORITY_2, + .config.ledMask = LED_CHANNEL(LED_CH_ON), + .config.state = LED_BLINKING, + .config.blinkParams.orDer = 0, + .config.blinkParams.onTime = BOARD_LED_OTA_ON_MS, + .config.blinkParams.offTime = BOARD_LED_OTA_OFF_MS, + .config.runNum = (uint16_t)(BOARD_LED_PAIR_FAIL_MS + / (BOARD_LED_OTA_ON_MS + BOARD_LED_OTA_OFF_MS)), + .ledEventHandler = led_pair_fail_handler, + }, + [LED_EVENT_OTA] = { + /* OTA:200ms 闪一直闪,直到升级结束或复位 */ + .event = LED_EVENT_OTA, + .priority = PRIORITY_3, + .config.ledMask = LED_CHANNEL(LED_CH_ON), + .config.state = LED_BLINKING, + .config.blinkParams.orDer = 0, + .config.blinkParams.onTime = BOARD_LED_OTA_ON_MS, + .config.blinkParams.offTime = BOARD_LED_OTA_OFF_MS, + .config.runNum = 0, /* 0=无限 */ + .ledEventHandler = led_ota_handler, + }, + }; + + LED_SetStateInit(set_led_multi); + SET_TimeGetInit((TIME_Get_t)timer_get_ms); + LED_EventTableInit(led_event_category); + bsp_led_set(0); +} + +/** + * @brief 跑一轮 LED 事件 + * @return 无 + */ +void user_led_handle(void) +{ + LED_EventHandle(); +} diff --git a/apps/usr_app/led_manager.h b/apps/usr_app/led_manager.h new file mode 100644 index 0000000..790109d --- /dev/null +++ b/apps/usr_app/led_manager.h @@ -0,0 +1,33 @@ +/****************************************************************************** + * @file led_manager.h + * @brief CBR3 单 LED(PC2)通道与事件表 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V1.0.0 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布(单灯版,删红绿双通道) + ******************************************************************************/ + +#ifndef __LED_MANAGER_H__ +#define __LED_MANAGER_H__ + +#include "led.h" + +typedef enum { + LED_CH_ON = 0, /**< 单灯唯一通道 */ + LED_CH_MAX +} LedChannel_t; + +/** + * @brief 注册 GPIO 回调并装载 CBR3 事件表 + * @return 无 + */ +void user_led_init(void); + +/** + * @brief 跑一轮 LED 事件 + * @return 无 + */ +void user_led_handle(void); + +#endif /* __LED_MANAGER_H__ */ diff --git a/apps/usr_jb_proto/Protocol/jb_product.c b/apps/usr_jb_proto/Protocol/jb_product.c index 23f7693..1e3845b 100644 --- a/apps/usr_jb_proto/Protocol/jb_product.c +++ b/apps/usr_jb_proto/Protocol/jb_product.c @@ -1,18 +1,30 @@ /* ================================================================ - * jb_product.c — 用户层实现(请填补空白处) - * 设备: 温湿度风扇 | 生成时间: 2026-08-29 10:31:50 + * jb_product.c — 用户层实现(薄适配:只写 DP,不写业务算法) + * 设备: 温湿度风扇 | 生成时间: 2026-08-29(cond_config 已按 Excel 重生 7 字节) * * ══ 开发流程 ══ * 1. 实现硬件抽象: jbGetTimerCount(定时器由 SOC SDK 提供) - * 发送由 SOC SDK 的 ble_send_data 承担,协议层已直接调用,无需 uartWrite - * 2. userInit() 设置 DP 默认值与外设初始化 - * 3. userHandle() 用户数据采集 - * 4. jbEventProcess() 处理 APP 下发的控制命令 - * 5. 主动上报 / OTA 的网关应答在收到 REPORTCMD_V2 后调用 jbReportAckCheck 清除等待 + * 发送由 SOC SDK 的 ble_send_data 承担,协议层已直接调用 + * 2. userInit() 设置 DP 默认值 + * 3. userHandle() 填温湿度 DP(500ms) + * 4. jbEventProcess() APP 下行:只写 gDevData 并转调 app_*,不在这里写业务 * ================================================================ */ #include "jb_product.h" +#include "app_th.h" +#include "app_fan.h" +#include "app_mode.h" +#include "app_cond.h" +#include "app_timer_task.h" +#include "app_arbiter.h" +#include "app_nvm.h" +#include "app_function.h" + +#define LOG_TAG_CONST APP +#define LOG_TAG "[JB_PRD]" +#define LOG_INFO_ENABLE +#include "debug.h" /* ════════════════════════════════════════════════════════════════ * 全局变量 @@ -28,19 +40,19 @@ void userInit(void) { memset(&gDevData, 0, sizeof(jb_data_point_t)); - gDevData.power = 0; /* DP0 开关 0=关 1=开 */ + gDevData.power = 0; /* DP0 预留:运行中由 app_function 上报 1,写忽略 */ gDevData.mode = 0; /* MODE_0 (关闭模式) */ gDevData.fan_gear = 0; /* FAN_GEAR_0 (关闭风扇) */ gDevData.current_temp = 0.0f; /* DP3 当前温度 (float) */ gDevData.current_humi = 0; /* DP4 当前湿度 */ gDevData.cond_en = 0; /* DP5 条件使能 0=关 1=开 */ - memset(gDevData.cond_config1, 0, 6); /* DP6 条件配置1 */ - memset(gDevData.cond_config2, 0, 6); /* DP7 条件配置2 */ - memset(gDevData.cond_config3, 0, 6); /* DP8 条件配置3 */ - memset(gDevData.cond_config4, 0, 6); /* DP9 条件配置4 */ - memset(gDevData.cond_config5, 0, 6); /* DP10 条件配置5 */ - memset(gDevData.cond_config6, 0, 6); /* DP11 条件配置6 */ - memset(gDevData.cond_config7, 0, 6); /* DP12 条件配置7 */ + memset(gDevData.cond_config1, 0, 7); /* DP6 条件配置1(7 字节) */ + memset(gDevData.cond_config2, 0, 7); /* DP7 条件配置2 */ + memset(gDevData.cond_config3, 0, 7); /* DP8 条件配置3 */ + memset(gDevData.cond_config4, 0, 7); /* DP9 条件配置4 */ + memset(gDevData.cond_config5, 0, 7); /* DP10 条件配置5 */ + memset(gDevData.cond_config6, 0, 7); /* DP11 条件配置6 */ + memset(gDevData.cond_config7, 0, 7); /* DP12 条件配置7 */ gDevData.time_en = 0; /* DP13 定时使能 0=关 1=开 */ memset(gDevData.time_config1, 0, 8); /* DP14 定时配置1 */ memset(gDevData.time_config2, 0, 8); /* DP15 定时配置2 */ @@ -52,7 +64,7 @@ void userInit(void) gDevData.fault_en = 0; /* DP21 设备故障使能 */ gDevData.fault_code = 0; /* DP22 设备故障 */ gDevData.alarm_en = 0; /* DP23 设备告警使能 */ - gDevData.alarm_code = 0; /* DP24 设备告警 */ + gDevData.alarm_code = 0; /* DP24 设备告警(保持 0) */ gDevData.factory_reset = 0; /* DP25 恢复出厂设置 0=关 1=开 */ gDevData.reserva_dp1 = 0; /* DP26 预留DP1 */ gDevData.reserva_dp2 = 0; /* DP27 预留DP2 */ @@ -60,48 +72,23 @@ void userInit(void) } /* ════════════════════════════════════════════════════════════════ - * userHandle — 用户数据采集 - * 协议层自动检测变化并上报(注意:此处是上报,不是下发控制) + * userHandle — 用户数据采集(500ms 由 usr_timer_loop 调用) + * 只填温湿度上报 DP;档位/模式/fault 由 app_function_poll(20ms)同步 * ════════════════════════════════════════════════════════════════ */ void userHandle(void) { - /* ── 可上报 DP (RO / RW): 用户数据采集 ── */ - // gDevData.power = read_sensor(); /* DP0 开关 */ - // gDevData.mode = read_sensor(); /* DP1 模式 */ - // gDevData.fan_gear = read_sensor(); /* DP2 风速挡位 */ - // gDevData.current_temp = 23.5f; /* DP3 当前温度 (float, scale=0.1), 直接填物理量 */ - // gDevData.current_humi = read_sensor(); /* DP4 当前湿度 */ - // gDevData.cond_en = read_sensor(); /* DP5 条件使能 */ - // memcpy(gDevData.cond_config1, sensor_read(), 6); /* DP6 条件配置1 */ - // memcpy(gDevData.cond_config2, sensor_read(), 6); /* DP7 条件配置2 */ - // memcpy(gDevData.cond_config3, sensor_read(), 6); /* DP8 条件配置3 */ - // memcpy(gDevData.cond_config4, sensor_read(), 6); /* DP9 条件配置4 */ - // memcpy(gDevData.cond_config5, sensor_read(), 6); /* DP10 条件配置5 */ - // memcpy(gDevData.cond_config6, sensor_read(), 6); /* DP11 条件配置6 */ - // memcpy(gDevData.cond_config7, sensor_read(), 6); /* DP12 条件配置7 */ - // gDevData.time_en = read_sensor(); /* DP13 定时使能 */ - // memcpy(gDevData.time_config1, sensor_read(), 8); /* DP14 定时配置1 */ - // memcpy(gDevData.time_config2, sensor_read(), 8); /* DP15 定时配置2 */ - // memcpy(gDevData.time_config3, sensor_read(), 8); /* DP16 定时配置3 */ - // memcpy(gDevData.time_config4, sensor_read(), 8); /* DP17 定时配置4 */ - // memcpy(gDevData.time_config5, sensor_read(), 8); /* DP18 定时配置5 */ - // memcpy(gDevData.time_config6, sensor_read(), 8); /* DP19 定时配置6 */ - // memcpy(gDevData.time_config7, sensor_read(), 8); /* DP20 定时配置7 */ - // gDevData.fault_en = read_sensor(); /* DP21 设备故障使能 */ - // gDevData.fault_code = read_sensor(); /* DP22 设备故障 */ - // gDevData.alarm_en = read_sensor(); /* DP23 设备告警使能 */ - // gDevData.alarm_code = read_sensor(); /* DP24 设备告警 */ - // gDevData.reserva_dp1 = read_sensor(); /* DP26 预留DP1 */ - // gDevData.reserva_dp2 = read_sensor(); /* DP27 预留DP2 */ - // gDevData.reserva_dp3 = read_sensor(); /* DP28 预留DP3 */ + if (app_th_get_valid()) { + /* DP3 链路传 uint16(scale=0.1),直接填物理量 float */ + gDevData.current_temp = (float)app_th_get_temp_x10() / 10.0f; + gDevData.current_humi = app_th_get_humi(); + } } /* ════════════════════════════════════════════════════════════════ * jbEventProcess — 0x03 APP 控制事件处理(下行 / DP 赋值方向) * - * 协议层自动解包 0x03 帧后调用本函数。 - * info->dp_ids[] = 被控制的 DP ID 列表 - * ctrl 已填入下发的值;复制到 gDevData 并执行动作 + * ⚠ 只做两件事:DP 写入 gDevData + 转调 app_* 模块。业务算法一律 + * 不进本文件(规格书第 0 章第 2 条)。 * ════════════════════════════════════════════════════════════════ */ int8_t jbEventProcess(jb_event_info_t *info, jb_data_point_t *ctrl) { @@ -114,216 +101,122 @@ int8_t jbEventProcess(jb_event_info_t *info, jb_data_point_t *ctrl) { switch (info->dp_ids[i]) { - - /* ── DP0 开关 (bool) ── */ + /* ── DP0 开关:预留,写忽略(规格书第 15 章)── */ case DP_ID_POWER: - if (ctrl->power) - { - gDevData.power = 1; - // TODO: 开 — 执行动作 - } - else - { - gDevData.power = 0; - // TODO: 关 — 执行动作 - } break; - /* ── DP1 模式 (enum) ── */ + /* ── DP1 模式:写则手动抢权(仲裁器释放自动化占用)── */ case DP_ID_MODE: gDevData.mode = ctrl->mode; - switch (ctrl->mode) - { - case MODE_0: /* 关闭模式 */ - // TODO: 处理 关闭模式 - break; - case MODE_1: /* 通风模式 */ - // TODO: 处理 通风模式 - break; - case MODE_2: /* 强力模式 */ - // TODO: 处理 强力模式 - break; - default: - break; - } + app_arbiter_manual_intervene(); /* 手动动作:条件/定时让位 */ + app_mode_set_manual(ctrl->mode); break; - /* ── DP2 风速挡位 (enum) ── */ + /* ── DP2 风速挡位:写则手动抢权,退模式 ── */ case DP_ID_FAN_GEAR: gDevData.fan_gear = ctrl->fan_gear; - switch (ctrl->fan_gear) - { - case FAN_GEAR_0: /* 关闭风扇 */ - // TODO: 处理 关闭风扇 - break; - case FAN_GEAR_1: /* 1档风速 */ - // TODO: 处理 1档风速 - break; - case FAN_GEAR_2: /* 2档风速 */ - // TODO: 处理 2档风速 - break; - case FAN_GEAR_3: /* 3档风速 */ - // TODO: 处理 3档风速 - break; - default: - break; - } + app_arbiter_manual_intervene(); /* 手动动作:条件/定时让位 */ + app_mode_set_manual(0); + app_fan_set_gear(ctrl->fan_gear); break; - /* ── DP5 条件使能 (bool) ── */ + /* ── DP5 条件使能 ── */ case DP_ID_COND_EN: - if (ctrl->cond_en) - { - gDevData.cond_en = 1; - // TODO: 开 — 执行动作 - } - else - { - gDevData.cond_en = 0; - // TODO: 关 — 执行动作 - } + gDevData.cond_en = ctrl->cond_en ? 1 : 0; + app_cond_set_en(gDevData.cond_en); break; - /* ── DP6 条件配置1 (raw) ── */ + /* ── DP6~12 条件配置(7 字节 memcpy + 条件状态机复位该组计时)── */ case DP_ID_COND_CONFIG1: - memcpy(gDevData.cond_config1, ctrl->cond_config1, 6); - // TODO: 根据 gDevData.cond_config1 执行动作 (长度 6 字节) + memcpy(gDevData.cond_config1, ctrl->cond_config1, 7); + app_cond_set_config(1); break; - - /* ── DP7 条件配置2 (raw) ── */ case DP_ID_COND_CONFIG2: - memcpy(gDevData.cond_config2, ctrl->cond_config2, 6); - // TODO: 根据 gDevData.cond_config2 执行动作 (长度 6 字节) + memcpy(gDevData.cond_config2, ctrl->cond_config2, 7); + app_cond_set_config(2); break; - - /* ── DP8 条件配置3 (raw) ── */ case DP_ID_COND_CONFIG3: - memcpy(gDevData.cond_config3, ctrl->cond_config3, 6); - // TODO: 根据 gDevData.cond_config3 执行动作 (长度 6 字节) + memcpy(gDevData.cond_config3, ctrl->cond_config3, 7); + app_cond_set_config(3); break; - - /* ── DP9 条件配置4 (raw) ── */ case DP_ID_COND_CONFIG4: - memcpy(gDevData.cond_config4, ctrl->cond_config4, 6); - // TODO: 根据 gDevData.cond_config4 执行动作 (长度 6 字节) + memcpy(gDevData.cond_config4, ctrl->cond_config4, 7); + app_cond_set_config(4); break; - - /* ── DP10 条件配置5 (raw) ── */ case DP_ID_COND_CONFIG5: - memcpy(gDevData.cond_config5, ctrl->cond_config5, 6); - // TODO: 根据 gDevData.cond_config5 执行动作 (长度 6 字节) + memcpy(gDevData.cond_config5, ctrl->cond_config5, 7); + app_cond_set_config(5); break; - - /* ── DP11 条件配置6 (raw) ── */ case DP_ID_COND_CONFIG6: - memcpy(gDevData.cond_config6, ctrl->cond_config6, 6); - // TODO: 根据 gDevData.cond_config6 执行动作 (长度 6 字节) + memcpy(gDevData.cond_config6, ctrl->cond_config6, 7); + app_cond_set_config(6); break; - - /* ── DP12 条件配置7 (raw) ── */ case DP_ID_COND_CONFIG7: - memcpy(gDevData.cond_config7, ctrl->cond_config7, 6); - // TODO: 根据 gDevData.cond_config7 执行动作 (长度 6 字节) + memcpy(gDevData.cond_config7, ctrl->cond_config7, 7); + app_cond_set_config(7); break; - /* ── DP13 定时使能 (bool) ── */ + /* ── DP13 定时使能 ── */ case DP_ID_TIME_EN: - if (ctrl->time_en) - { - gDevData.time_en = 1; - // TODO: 开 — 执行动作 - } - else - { - gDevData.time_en = 0; - // TODO: 关 — 执行动作 - } + gDevData.time_en = ctrl->time_en ? 1 : 0; + app_timer_set_en(gDevData.time_en); break; - /* ── DP14 定时配置1 (raw) ── */ + /* ── DP14~20 定时配置(8 字节)── */ case DP_ID_TIME_CONFIG1: memcpy(gDevData.time_config1, ctrl->time_config1, 8); - // TODO: 根据 gDevData.time_config1 执行动作 (长度 8 字节) + app_timer_set_config(1); break; - - /* ── DP15 定时配置2 (raw) ── */ case DP_ID_TIME_CONFIG2: memcpy(gDevData.time_config2, ctrl->time_config2, 8); - // TODO: 根据 gDevData.time_config2 执行动作 (长度 8 字节) + app_timer_set_config(2); break; - - /* ── DP16 定时配置3 (raw) ── */ case DP_ID_TIME_CONFIG3: memcpy(gDevData.time_config3, ctrl->time_config3, 8); - // TODO: 根据 gDevData.time_config3 执行动作 (长度 8 字节) + app_timer_set_config(3); break; - - /* ── DP17 定时配置4 (raw) ── */ case DP_ID_TIME_CONFIG4: memcpy(gDevData.time_config4, ctrl->time_config4, 8); - // TODO: 根据 gDevData.time_config4 执行动作 (长度 8 字节) + app_timer_set_config(4); break; - - /* ── DP18 定时配置5 (raw) ── */ case DP_ID_TIME_CONFIG5: memcpy(gDevData.time_config5, ctrl->time_config5, 8); - // TODO: 根据 gDevData.time_config5 执行动作 (长度 8 字节) + app_timer_set_config(5); break; - - /* ── DP19 定时配置6 (raw) ── */ case DP_ID_TIME_CONFIG6: memcpy(gDevData.time_config6, ctrl->time_config6, 8); - // TODO: 根据 gDevData.time_config6 执行动作 (长度 8 字节) + app_timer_set_config(6); break; - - /* ── DP20 定时配置7 (raw) ── */ case DP_ID_TIME_CONFIG7: memcpy(gDevData.time_config7, ctrl->time_config7, 8); - // TODO: 根据 gDevData.time_config7 执行动作 (长度 8 字节) + app_timer_set_config(7); break; - /* ── DP21 设备故障使能 (uint8) ── */ + /* ── DP21/23 使能:原样存,P0 不做位级过滤 ── */ case DP_ID_FAULT_EN: gDevData.fault_en = ctrl->fault_en; - // TODO: 根据 ctrl->fault_en 执行动作 break; - - /* ── DP23 设备告警使能 (uint8) ── */ case DP_ID_ALARM_EN: gDevData.alarm_en = ctrl->alarm_en; - // TODO: 根据 ctrl->alarm_en 执行动作 break; - /* ── DP25 恢复出厂设置 (bool) ── */ + /* ── DP25 恢复出厂设置:清业务 VM(留配对)+ RAM 复位,不断开不软复位(14 章)── */ case DP_ID_FACTORY_RESET: - if (ctrl->factory_reset) - { - gDevData.factory_reset = 1; - // TODO: 开 — 执行动作 - } - else - { - gDevData.factory_reset = 0; - // TODO: 关 — 执行动作 + gDevData.factory_reset = ctrl->factory_reset ? 1 : 0; + if (gDevData.factory_reset) { + app_function_factory_reset(); } break; - /* ── DP26 预留DP1 (uint32) ── */ + /* ── DP26~28 预留:存 RAM,可不实现动作 ── */ case DP_ID_RESERVA_DP1: gDevData.reserva_dp1 = ctrl->reserva_dp1; - // TODO: 根据 ctrl->reserva_dp1 执行动作 break; - - /* ── DP27 预留DP2 (uint32) ── */ case DP_ID_RESERVA_DP2: gDevData.reserva_dp2 = ctrl->reserva_dp2; - // TODO: 根据 ctrl->reserva_dp2 执行动作 break; - - /* ── DP28 预留DP3 (uint32) ── */ case DP_ID_RESERVA_DP3: gDevData.reserva_dp3 = ctrl->reserva_dp3; - // TODO: 根据 ctrl->reserva_dp3 执行动作 break; default: @@ -337,13 +230,12 @@ int8_t jbEventProcess(jb_event_info_t *info, jb_data_point_t *ctrl) * 返回 0=接受升级(协议层应答 JB_OK + MaxDataLen) * 非 0=拒绝,返回值即错误码(JB_OTA_*) * - * ⚠ SOC 侧 OTA 尚未实现:当前仅应答,不执行任何升级动作。 */ + * ⚠ SOC 侧 OTA 尚未实现(规格书第 15 章:P2 可继续 stub,禁止假装升级 + * 成功后擦用户区):当前仅应答,不执行任何升级动作。 */ int8_t jbOnOtaNotify(uint32_t fwVersion, uint32_t fwLength, uint32_t fwCrc32) { (void)fwVersion; (void)fwLength; (void)fwCrc32; - // TODO: 检查 Flash 空间与版本,决定是否接收升级 - // 允许: 返回 JB_OK(协议层应答 JB_OK + MaxDataLen) - // 拒绝: 返回对应 JB_OTA_* 错误码(如 JB_OTA_VER_LOW / JB_OTA_SPACE_ERR) + /* TODO(P2): 检查 Flash 空间与版本(拒绝降级/同版本),决定是否接收升级 */ return JB_OK; } @@ -352,35 +244,32 @@ void jbOnOtaData(const uint8_t *data, uint16_t len, uint16_t shardIdx, uint16_t totalShards) { (void)data; (void)len; (void)shardIdx; (void)totalShards; - // TODO: 写入 Flash,累计 CRC32 - // 全部数据接收完成后调用 jbSendOtaResult(JB_OK, mcuVersion) + /* TODO(P2): 写入 Flash,累计 CRC32;全部接收后调用 jbSendOtaResult */ } /* 0x36 / 0x38 停止 OTA */ void jbOnOtaStop(void) { - // TODO: 终止 OTA,释放资源 + /* TODO(P2): 终止 OTA,释放资源 */ } -/* 任意 MCU→BLE 请求的 ACK 回调(cmd 标识是哪条请求的应答,result 为结果码) - - SOC 仅保留 OTA 主动请求应答: OTA完成 0x35 / 停止 0x39 - - 可在本回调中设置"XX 完成/失败"标志,供主循环判断"确认成功才继续" */ +/* 任意 MCU→BLE 请求的 ACK 回调 */ void jbOnAck(uint8_t cmd, uint8_t result) { (void)cmd; (void)result; - // TODO: switch(cmd){ case CMD_OTA_COMPLETE_ACK: gOtaDone = (result==JB_OK); break; case CMD_OTA_STOP_MCU_ACK: ... } + /* TODO(P2): switch(cmd){ case CMD_OTA_COMPLETE_ACK: ... } */ } /* ════════════════════════════════════════════════════════════════ * 硬件抽象(按实际平台实现) * ════════════════════════════════════════════════════════════════ */ -/* ── 定时器 ── */ +/* ── 定时器:由 usr_jb_main 的 20ms 节拍按 1ms 步进调用 jbTimerIrq() ── */ void jbTimerIrq(void) { timerMs++; -} /* 在 1ms 中断中调用本函数 */ +} uint32_t jbGetTimerCount(void) { diff --git a/apps/usr_jb_proto/Protocol/jb_protocol.c b/apps/usr_jb_proto/Protocol/jb_protocol.c index de105d8..691b388 100644 --- a/apps/usr_jb_proto/Protocol/jb_protocol.c +++ b/apps/usr_jb_proto/Protocol/jb_protocol.c @@ -194,54 +194,54 @@ static uint16_t jbPackDps(const jb_data_point_t *dp, bitmap[DP_ID_COND_EN / 8] |= (1 << (DP_ID_COND_EN % 8)); out[valIdx++] = dp->cond_en ? 1 : 0; } - /* DP6 条件配置1 (raw, 6B, RW) */ + /* DP6 条件配置1 (raw, 7B, RW) */ if (mask[DP_ID_COND_CONFIG1 / 8] & (1 << (DP_ID_COND_CONFIG1 % 8))) { bitmap[DP_ID_COND_CONFIG1 / 8] |= (1 << (DP_ID_COND_CONFIG1 % 8)); - memcpy(&out[valIdx], dp->cond_config1, 6); - valIdx += 6; + memcpy(&out[valIdx], dp->cond_config1, 7); + valIdx += 7; } - /* DP7 条件配置2 (raw, 6B, RW) */ + /* DP7 条件配置2 (raw, 7B, RW) */ if (mask[DP_ID_COND_CONFIG2 / 8] & (1 << (DP_ID_COND_CONFIG2 % 8))) { bitmap[DP_ID_COND_CONFIG2 / 8] |= (1 << (DP_ID_COND_CONFIG2 % 8)); - memcpy(&out[valIdx], dp->cond_config2, 6); - valIdx += 6; + memcpy(&out[valIdx], dp->cond_config2, 7); + valIdx += 7; } - /* DP8 条件配置3 (raw, 6B, RW) */ + /* DP8 条件配置3 (raw, 7B, RW) */ if (mask[DP_ID_COND_CONFIG3 / 8] & (1 << (DP_ID_COND_CONFIG3 % 8))) { bitmap[DP_ID_COND_CONFIG3 / 8] |= (1 << (DP_ID_COND_CONFIG3 % 8)); - memcpy(&out[valIdx], dp->cond_config3, 6); - valIdx += 6; + memcpy(&out[valIdx], dp->cond_config3, 7); + valIdx += 7; } - /* DP9 条件配置4 (raw, 6B, RW) */ + /* DP9 条件配置4 (raw, 7B, RW) */ if (mask[DP_ID_COND_CONFIG4 / 8] & (1 << (DP_ID_COND_CONFIG4 % 8))) { bitmap[DP_ID_COND_CONFIG4 / 8] |= (1 << (DP_ID_COND_CONFIG4 % 8)); - memcpy(&out[valIdx], dp->cond_config4, 6); - valIdx += 6; + memcpy(&out[valIdx], dp->cond_config4, 7); + valIdx += 7; } - /* DP10 条件配置5 (raw, 6B, RW) */ + /* DP10 条件配置5 (raw, 7B, RW) */ if (mask[DP_ID_COND_CONFIG5 / 8] & (1 << (DP_ID_COND_CONFIG5 % 8))) { bitmap[DP_ID_COND_CONFIG5 / 8] |= (1 << (DP_ID_COND_CONFIG5 % 8)); - memcpy(&out[valIdx], dp->cond_config5, 6); - valIdx += 6; + memcpy(&out[valIdx], dp->cond_config5, 7); + valIdx += 7; } - /* DP11 条件配置6 (raw, 6B, RW) */ + /* DP11 条件配置6 (raw, 7B, RW) */ if (mask[DP_ID_COND_CONFIG6 / 8] & (1 << (DP_ID_COND_CONFIG6 % 8))) { bitmap[DP_ID_COND_CONFIG6 / 8] |= (1 << (DP_ID_COND_CONFIG6 % 8)); - memcpy(&out[valIdx], dp->cond_config6, 6); - valIdx += 6; + memcpy(&out[valIdx], dp->cond_config6, 7); + valIdx += 7; } - /* DP12 条件配置7 (raw, 6B, RW) */ + /* DP12 条件配置7 (raw, 7B, RW) */ if (mask[DP_ID_COND_CONFIG7 / 8] & (1 << (DP_ID_COND_CONFIG7 % 8))) { bitmap[DP_ID_COND_CONFIG7 / 8] |= (1 << (DP_ID_COND_CONFIG7 % 8)); - memcpy(&out[valIdx], dp->cond_config7, 6); - valIdx += 6; + memcpy(&out[valIdx], dp->cond_config7, 7); + valIdx += 7; } /* DP13 定时使能 (bool, 1B, RW) */ if (mask[DP_ID_TIME_EN / 8] & (1 << (DP_ID_TIME_EN % 8))) @@ -409,91 +409,91 @@ static void jbUnpackDps(const uint8_t *bitmap, const uint8_t *values, info->dp_ids[info->count++] = DP_ID_COND_EN; } } - /* DP6 条件配置1 (raw, 6B, RW) */ + /* DP6 条件配置1 (raw, 7B, RW) */ if (bitmap[DP_ID_COND_CONFIG1 / 8] & (1 << (DP_ID_COND_CONFIG1 % 8))) { - if (valIdx + 6 <= valLen) + if (valIdx + 7 <= valLen) { - memcpy(dp->cond_config1, &values[valIdx], 6); - valIdx += 6; + memcpy(dp->cond_config1, &values[valIdx], 7); + valIdx += 7; } if (info && info->count < DP_ID_MAX) { info->dp_ids[info->count++] = DP_ID_COND_CONFIG1; } } - /* DP7 条件配置2 (raw, 6B, RW) */ + /* DP7 条件配置2 (raw, 7B, RW) */ if (bitmap[DP_ID_COND_CONFIG2 / 8] & (1 << (DP_ID_COND_CONFIG2 % 8))) { - if (valIdx + 6 <= valLen) + if (valIdx + 7 <= valLen) { - memcpy(dp->cond_config2, &values[valIdx], 6); - valIdx += 6; + memcpy(dp->cond_config2, &values[valIdx], 7); + valIdx += 7; } if (info && info->count < DP_ID_MAX) { info->dp_ids[info->count++] = DP_ID_COND_CONFIG2; } } - /* DP8 条件配置3 (raw, 6B, RW) */ + /* DP8 条件配置3 (raw, 7B, RW) */ if (bitmap[DP_ID_COND_CONFIG3 / 8] & (1 << (DP_ID_COND_CONFIG3 % 8))) { - if (valIdx + 6 <= valLen) + if (valIdx + 7 <= valLen) { - memcpy(dp->cond_config3, &values[valIdx], 6); - valIdx += 6; + memcpy(dp->cond_config3, &values[valIdx], 7); + valIdx += 7; } if (info && info->count < DP_ID_MAX) { info->dp_ids[info->count++] = DP_ID_COND_CONFIG3; } } - /* DP9 条件配置4 (raw, 6B, RW) */ + /* DP9 条件配置4 (raw, 7B, RW) */ if (bitmap[DP_ID_COND_CONFIG4 / 8] & (1 << (DP_ID_COND_CONFIG4 % 8))) { - if (valIdx + 6 <= valLen) + if (valIdx + 7 <= valLen) { - memcpy(dp->cond_config4, &values[valIdx], 6); - valIdx += 6; + memcpy(dp->cond_config4, &values[valIdx], 7); + valIdx += 7; } if (info && info->count < DP_ID_MAX) { info->dp_ids[info->count++] = DP_ID_COND_CONFIG4; } } - /* DP10 条件配置5 (raw, 6B, RW) */ + /* DP10 条件配置5 (raw, 7B, RW) */ if (bitmap[DP_ID_COND_CONFIG5 / 8] & (1 << (DP_ID_COND_CONFIG5 % 8))) { - if (valIdx + 6 <= valLen) + if (valIdx + 7 <= valLen) { - memcpy(dp->cond_config5, &values[valIdx], 6); - valIdx += 6; + memcpy(dp->cond_config5, &values[valIdx], 7); + valIdx += 7; } if (info && info->count < DP_ID_MAX) { info->dp_ids[info->count++] = DP_ID_COND_CONFIG5; } } - /* DP11 条件配置6 (raw, 6B, RW) */ + /* DP11 条件配置6 (raw, 7B, RW) */ if (bitmap[DP_ID_COND_CONFIG6 / 8] & (1 << (DP_ID_COND_CONFIG6 % 8))) { - if (valIdx + 6 <= valLen) + if (valIdx + 7 <= valLen) { - memcpy(dp->cond_config6, &values[valIdx], 6); - valIdx += 6; + memcpy(dp->cond_config6, &values[valIdx], 7); + valIdx += 7; } if (info && info->count < DP_ID_MAX) { info->dp_ids[info->count++] = DP_ID_COND_CONFIG6; } } - /* DP12 条件配置7 (raw, 6B, RW) */ + /* DP12 条件配置7 (raw, 7B, RW) */ if (bitmap[DP_ID_COND_CONFIG7 / 8] & (1 << (DP_ID_COND_CONFIG7 % 8))) { - if (valIdx + 6 <= valLen) + if (valIdx + 7 <= valLen) { - memcpy(dp->cond_config7, &values[valIdx], 6); - valIdx += 6; + memcpy(dp->cond_config7, &values[valIdx], 7); + valIdx += 7; } if (info && info->count < DP_ID_MAX) { @@ -728,43 +728,43 @@ static uint8_t jbBuildDiffMask(const jb_data_point_t *last, hasChange = 1; } /* DP6 条件配置1 */ - if (memcmp(last->cond_config1, cur->cond_config1, 6)) + if (memcmp(last->cond_config1, cur->cond_config1, 7)) { mask[DP_ID_COND_CONFIG1 / 8] |= (1 << (DP_ID_COND_CONFIG1 % 8)); hasChange = 1; } /* DP7 条件配置2 */ - if (memcmp(last->cond_config2, cur->cond_config2, 6)) + if (memcmp(last->cond_config2, cur->cond_config2, 7)) { mask[DP_ID_COND_CONFIG2 / 8] |= (1 << (DP_ID_COND_CONFIG2 % 8)); hasChange = 1; } /* DP8 条件配置3 */ - if (memcmp(last->cond_config3, cur->cond_config3, 6)) + if (memcmp(last->cond_config3, cur->cond_config3, 7)) { mask[DP_ID_COND_CONFIG3 / 8] |= (1 << (DP_ID_COND_CONFIG3 % 8)); hasChange = 1; } /* DP9 条件配置4 */ - if (memcmp(last->cond_config4, cur->cond_config4, 6)) + if (memcmp(last->cond_config4, cur->cond_config4, 7)) { mask[DP_ID_COND_CONFIG4 / 8] |= (1 << (DP_ID_COND_CONFIG4 % 8)); hasChange = 1; } /* DP10 条件配置5 */ - if (memcmp(last->cond_config5, cur->cond_config5, 6)) + if (memcmp(last->cond_config5, cur->cond_config5, 7)) { mask[DP_ID_COND_CONFIG5 / 8] |= (1 << (DP_ID_COND_CONFIG5 % 8)); hasChange = 1; } /* DP11 条件配置6 */ - if (memcmp(last->cond_config6, cur->cond_config6, 6)) + if (memcmp(last->cond_config6, cur->cond_config6, 7)) { mask[DP_ID_COND_CONFIG6 / 8] |= (1 << (DP_ID_COND_CONFIG6 % 8)); hasChange = 1; } /* DP12 条件配置7 */ - if (memcmp(last->cond_config7, cur->cond_config7, 6)) + if (memcmp(last->cond_config7, cur->cond_config7, 7)) { mask[DP_ID_COND_CONFIG7 / 8] |= (1 << (DP_ID_COND_CONFIG7 % 8)); hasChange = 1; diff --git a/apps/usr_jb_proto/Protocol/jb_protocol.h b/apps/usr_jb_proto/Protocol/jb_protocol.h index d8a7615..7b7730b 100644 --- a/apps/usr_jb_proto/Protocol/jb_protocol.h +++ b/apps/usr_jb_proto/Protocol/jb_protocol.h @@ -23,7 +23,7 @@ * ════════════════════════════════════════════════════════════════ */ #define PKT_HEAD_0 0x55 #define PKT_HEAD_1 0xAA -#define MAX_PACKAGE_LEN 128 +#define MAX_PACKAGE_LEN 256 #define JB_OTA_MAX_PAYLOAD 96 /* OTA 单包 Payload 上限(Byte); 须 <= MAX_PACKAGE_LEN - 11(0x32 帧开销) */ #define DP_COUNT 29 @@ -157,13 +157,13 @@ typedef enum #define DP_LEN_CURRENT_TEMP 2 #define DP_LEN_CURRENT_HUMI 1 #define DP_LEN_COND_EN 1 -#define DP_LEN_COND_CONFIG1 6 -#define DP_LEN_COND_CONFIG2 6 -#define DP_LEN_COND_CONFIG3 6 -#define DP_LEN_COND_CONFIG4 6 -#define DP_LEN_COND_CONFIG5 6 -#define DP_LEN_COND_CONFIG6 6 -#define DP_LEN_COND_CONFIG7 6 +#define DP_LEN_COND_CONFIG1 7 +#define DP_LEN_COND_CONFIG2 7 +#define DP_LEN_COND_CONFIG3 7 +#define DP_LEN_COND_CONFIG4 7 +#define DP_LEN_COND_CONFIG5 7 +#define DP_LEN_COND_CONFIG6 7 +#define DP_LEN_COND_CONFIG7 7 #define DP_LEN_TIME_EN 1 #define DP_LEN_TIME_CONFIG1 8 #define DP_LEN_TIME_CONFIG2 8 @@ -214,13 +214,13 @@ typedef struct float current_temp; /* DP3 当前温度 (float, scale=0.1) */ uint8_t current_humi; /* DP4 当前湿度 */ uint8_t cond_en; /* DP5 条件使能 */ - uint8_t cond_config1[6];/* DP6 条件配置1 */ - uint8_t cond_config2[6];/* DP7 条件配置2 */ - uint8_t cond_config3[6];/* DP8 条件配置3 */ - uint8_t cond_config4[6];/* DP9 条件配置4 */ - uint8_t cond_config5[6];/* DP10 条件配置5 */ - uint8_t cond_config6[6];/* DP11 条件配置6 */ - uint8_t cond_config7[6];/* DP12 条件配置7 */ + uint8_t cond_config1[7];/* DP6 条件配置1 (7B: en/tempH/tempL/humi/dur/action/cond) */ + uint8_t cond_config2[7];/* DP7 条件配置2 */ + uint8_t cond_config3[7];/* DP8 条件配置3 */ + uint8_t cond_config4[7];/* DP9 条件配置4 */ + uint8_t cond_config5[7];/* DP10 条件配置5 */ + uint8_t cond_config6[7];/* DP11 条件配置6 */ + uint8_t cond_config7[7];/* DP12 条件配置7 */ uint8_t time_en; /* DP13 定时使能 */ uint8_t time_config1[8];/* DP14 定时配置1 */ uint8_t time_config2[8];/* DP15 定时配置2 */ diff --git a/apps/usr_jb_proto/usr_jb_main.c b/apps/usr_jb_proto/usr_jb_main.c index ebf80b1..d3830d9 100644 --- a/apps/usr_jb_proto/usr_jb_main.c +++ b/apps/usr_jb_proto/usr_jb_main.c @@ -1,18 +1,59 @@ +/****************************************************************************** + * @file usr_jb_main.c + * @brief CBR3 业务组合根:只做"先后调用谁、谁通知谁",不含具体业务算法 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V1.0.0 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹:删电池/USB,加风扇/显示/传感) + * + * @note 协议生成文件 jb_product.c 不要填业务。APP 下行写 gDevData。 + * usr_timer_loop 由 BLE 回调,勿再注册第二份。 + * 具体业务逻辑去对应模块找: + * app_boot —— 开机 3s/5s/12s 按键判定 + * app_power —— 软关机请求(无电池/充电) + * app_pair —— 配对会话计时 + * app_fan —— 风扇档位 + * app_th —— AHT20 采样 + * app_disp —— 数码管显示 + * app_function —— 按键动作 + DP 同步 + ******************************************************************************/ + #include "system/includes.h" #include "app_config.h" #include "app_main.h" #include "app_task.h" #include "user_cfg.h" -#include "vm.h" -#include "sys_time.h" #include "jb_protocol.h" #include "jb_product.h" #include "jb_ringbuffer.h" #include "jb_common.h" #include "usr_le_product.h" +#include "usr_le_api.h" +#include "bsp_hw.h" +#include "app_power.h" +#include "app_pair.h" +#include "app_boot.h" +#include "app_function.h" +#include "app_fan.h" +#include "app_th.h" +#include "app_disp.h" +#include "app_mode.h" +#include "app_cond.h" +#include "app_timer_task.h" +#include "app_arbiter.h" +#include "app_nvm.h" +#include "app_led.h" +#include "key_manager.h" +#include "asm/power/power_api.h" -/* 向 usr_le_code 库提供产品身份(换产品只改 jb_protocol.h 宏即可) */ +#define LOG_TAG_CONST APP +#define LOG_TAG "[USR_JB]" +#define LOG_INFO_ENABLE +#include "debug.h" + +/* 向 usr_le_code 提供产品身份(换产品只改 jb_protocol.h 宏即可) */ const uint8_t g_usr_le_prodcode[USR_LE_PRODCODE_LEN] = { JB_CATEGORY_CODE[0], JB_CATEGORY_CODE[1], JB_CATEGORY_CODE[2], JB_CATEGORY_CODE[3] @@ -22,28 +63,219 @@ const uint8_t g_usr_le_pcode[USR_LE_PCODE_LEN] = { JB_PRODUCT_CODE[3], JB_PRODUCT_CODE[4], JB_PRODUCT_CODE[5] }; -void usr_timer_1_ms(); -static int jb_timer_id=0; +#define USR_TIMER_IDLE_MS 20 /**< 全局统一节拍(规格书第 3 章,禁止再开 1ms ISR) */ -void usr_jb_init() +/** + * @brief 业务节拍档位 + * @note 全系统只用一种节拍周期(USR_TIMER_IDLE_MS),区别只在"能不能休眠" + */ +typedef enum { + USR_TICK_IDLE = 0, /**< 可休眠 */ + USR_TICK_BLOCK_SLEEP /**< 禁止休眠(按键按住/风扇转/关机流程/配对/OTA) */ +} UsrTickMode_t; + +/** 调度器状态 */ +typedef struct { + int timer_id; + UsrTickMode_t mode; +} UsrSchedulerCtx_t; + +static UsrSchedulerCtx_t s_sched; + +static void usr_timer_tick(void *priv); +static void usr_timer_rearm(UsrTickMode_t mode); +static void usr_log_boot_version(void); + +/** + * @brief 软关机请求钩子:先落盘当前 mode/gear/配置,再停风扇、灯全灭 + * @note ⚠ app_nvm_save() 必须在 app_function_stop_all() 之前调用: + * stop_all 会把 mode/gear 清 0,先存才能保住用户最后的运行状态 + * (规格书 13 章:软关机前写 VM,开机恢复上次 mode/gear) + * @return 无 + */ +static void usr_poweroff_request_hook(void) { + app_nvm_save(); /* 软关机前立即写 VM(不等待 500ms 防抖) */ + app_function_stop_all(); + app_led_clear(); +} + +/** + * @brief 软关机最终钩子:50ms 到点,真正进入深睡 + * @note 不走 sys_enter_soft_poweroff():那是耳机 SDK 的优雅关机流程,实测慢。 + * 本产品无 DAC/音频,直接进软关机(规格书 5.1:风扇 0、数码管锁存灭、 + * LED 灭、BLE 停)。 + * @return 无 + */ +static void usr_poweroff_finalize_hook(void) +{ + bsp_hw_enter_sleep_io(); + power_set_soft_poweroff(); +} + +/** + * @brief 按当前业务状态判断能不能休眠 + * @note 按键按住、风扇转、关机流程、配对中、OTA 灯效中 → 不能睡 + * @return 本次应使用的档位 + */ +static UsrTickMode_t usr_timer_pick_mode(void) +{ + if (bsp_key_is_pressed() || app_power_poweroff_pending() + || (app_fan_get_gear() != 0) || app_mode_get_active() + || app_pair_is_active() || app_led_is_ota()) { + return USR_TICK_BLOCK_SLEEP; + } + return USR_TICK_IDLE; +} + +/** + * @brief 切换业务定时器。priority=1 时系统无法 sleep;节拍周期恒为 USR_TIMER_IDLE_MS + * @param mode 目标档位 + * @return 无 + */ +static void usr_timer_rearm(UsrTickMode_t mode) +{ + if (s_sched.timer_id) { + usr_timer_del((u16)s_sched.timer_id); + s_sched.timer_id = 0; + } + s_sched.mode = mode; + s_sched.timer_id = (int)usr_timer_add(NULL, usr_timer_tick, USR_TIMER_IDLE_MS, + (mode == USR_TICK_BLOCK_SLEEP) ? 1 : 0); +} + +/** + * @brief 业务定时器:固定 USR_TIMER_IDLE_MS 一拍;忙时禁睡,空闲可睡 + * @param priv 未使用 + * @note ⚠ 运行在硬件定时器时基上下文:禁止阻塞等待、禁止耗时操作。 + * 关机请走 app_power 的钩子,不要在这里直接 power_set_soft_poweroff。 + * @return 无 + */ +static void usr_timer_tick(void *priv) +{ + uint16_t dt = USR_TIMER_IDLE_MS; + uint16_t i; + UsrTickMode_t want; + + (void)priv; + for (i = 0; i < dt; i++) { + jbTimerIrq(); + } + + if (!app_power_poweroff_pending()) { + user_key_scan(dt); + user_key_handle(); /* 长按 3s 会在这里同步触发 app_power_request_poweroff() */ + } + + app_th_poll(dt); /* 内部 2s 才真正采;IO 分离后采集与显示互不干扰 */ + app_mode_run(dt); /* 通风/强力状态机(20ms 计时精度;条件/定时在 500ms 任务上下文) */ + app_fan_run(dt); /* P0 无动作 */ + app_disp_run(dt); /* 刷新显示(告警闪烁) */ + app_function_poll(); /* 同步 DP */ + app_nvm_tick(dt); /* 档位/模式/配置变化检测 + 500ms 防抖计时(不写 flash!) */ + + /* OTA 灯效期间最高优先级;关机流程中灯灭 */ + if (app_led_is_ota()) { + app_led_run(dt, app_function_is_power_on()); + } else if (app_power_poweroff_pending()) { + app_led_clear(); + } else { + app_led_run(dt, app_function_is_power_on()); + } + + want = usr_timer_pick_mode(); /* 判断能不能休眠 */ + if (want != s_sched.mode) { + usr_timer_rearm(want); + } +} + +/** + * @brief 上电打印固件版本与编译时间,方便串口对应当前是哪一包 + * @return 无 + */ +static void usr_log_boot_version(void) +{ + log_info("sw V%d.%d.%d hw V%d.%d.%d build %s %s\n", + JB_MCU_SW_VERSION_MAJOR, JB_MCU_SW_VERSION_MINOR, JB_MCU_SW_VERSION_PATCH, + JB_MCU_HW_VERSION_MAJOR, JB_MCU_HW_VERSION_MINOR, JB_MCU_HW_VERSION_PATCH, + __DATE__, __TIME__); +} + +/** + * @brief 业务初始化(在 BLE 栈起来之前由 app_main 调用) + * @note 内部会等按键 3s/5s/12s;不足 3s 直接深睡,不会启动业务定时器。 + * app_led_init() 排在 app_boot_wait_key() 前面:3s/5s 提示灯要用 + * LED 事件表(规格书 5.2)。boot 深睡分支不返回,后面初始化不会浪费。 + * @return 无 + */ +void usr_jb_init(void) +{ + usr_log_boot_version(); /* UART 已在 app_main 起来,按键判定前就能看到版本 */ + + bsp_hw_init(); + app_led_init(); /* 提前到按键判定之前:3s 常亮/5s 配对闪都要用灯效事件表 */ + + app_boot_wait_key(); /* 深睡分支不会返回;返回即代表确定要开机 */ + jbInit(); userInit(); - if(jb_timer_id==0)jb_timer_id = sys_s_hi_timer_add(NULL, usr_timer_1_ms, TIMER_PERIOD_MS); + if (usr_var.usr_goto_updata) { + /* 开机 12s 已进 OTA:立刻挂 200ms 闪 */ + app_led_request_ota(); + } + + app_fan_init(); + app_disp_init(); + /* CT1642 无内部扫描 RAM,4ms 快节拍轮询 4 位。priority=1 会挡住系统 sleep, + * USB 常电产品故意如此:管子必须持续扫描,否则肉眼闪。勿改成 priority=0。 */ + usr_timer_add(NULL, app_disp_scan, BOARD_DISP_SCAN_MS, 1); + app_th_init(); + app_mode_init(); + app_arbiter_init(); /* 模式仲裁(手动>定时>条件)先于条件/定时引擎 */ + app_cond_init(); + app_timer_init(); + app_nvm_init(); + app_nvm_load(); + + app_power_init(); + app_power_set_hooks(usr_poweroff_request_hook, usr_poweroff_finalize_hook); + app_function_init(); + + user_key_init(); + user_key_lock_if_held(); + app_function_power_on_silent(); + + usr_timer_rearm(USR_TICK_IDLE); + log_info("timer idle %ums, sleep ok\n", (unsigned)USR_TIMER_IDLE_MS); } -void usr_timer_1_ms() +/** + * @brief BLE 收到见宝载荷,送入协议环形缓冲 + * @param data 载荷指针 + * @param len 载荷长度,单位字节 + * @return 无 + */ +void usr_jb_le_recieve_data(u8 *data, u16 len) { - jbTimerIrq(); + jbPutData(data, len); } -void usr_jb_le_recieve_data(u8* data,u16 len) -{ - jbPutData(data,len); -} - -void usr_timer_loop()///500ms +/** + * @brief 约 500ms:协议收包与 DP 上报 + 自动化引擎(由 usr_le_code 回调) + * @note userHandle 只负责填温湿度 DP(规格书第 3 章)。 + * ⚠ 本函数跑在 sys_timer 回调(usr_ble_adv_updata 500ms)= **任务上下文** + * (BLE 线程,栈充足),与 20ms 的 usr_timer_tick(usr_timer_add 异步 + * 接口 = 硬件定时器中断上下文,栈极小)不同。 + * **所有重量级/自动化引擎必须放这里**:条件/定时引擎(500ms 粒度对 + * 2s 采样与分钟级时长足够,且单次自毁要写 VM)、VM 落盘、关机收口。 + * 禁止放进 usr_timer_tick——曾因在 20ms 中断里调 syscfg_write 栈溢出软复位。 + * @return 无 + */ +void usr_timer_loop(void) { userHandle(); jbProtocolHandle(&gDevData); + app_cond_run(500); /* 条件引擎(任务上下文;条件/定时仲裁见 app_arbiter) */ + app_timer_run(500); /* 定时引擎(任务上下文;RTC 门闩 + 窗判断) */ + app_function_loop(); /* 任务上下文:VM 防抖落盘 + 延后关机收口 */ } diff --git a/apps/usr_le_code/lib/libusr_le_code.a b/apps/usr_le_code/lib/libusr_le_code.a index f337979..40e6f2b 100644 Binary files a/apps/usr_le_code/lib/libusr_le_code.a and b/apps/usr_le_code/lib/libusr_le_code.a differ diff --git a/apps/usr_periph/board_pin.h b/apps/usr_periph/board_pin.h new file mode 100644 index 0000000..27d835b --- /dev/null +++ b/apps/usr_periph/board_pin.h @@ -0,0 +1,156 @@ +/****************************************************************************** + * @file board_pin.h + * @brief CBR3 温湿度风扇板级引脚、电平与时序(换板只改本文件) + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V1.0.0 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布(P0 Bring-up) + * + * @note 产品 GPIO 全部集中到本文件;板级 board_jl701n_demo_cfg.h 只保留 + * UART0_TX=PG6 一项改动。CT1642 用 PG0(CLK)/PG1(DATA),AHT20 用 + * PC4(SCL)/PC5(SDA),两路 IO 独立、无总线共享(2026-08-29 改板飞线分离, + * 原 PC4/PC5 共线 + bsp_bus 分时机制整体删除)。 + ******************************************************************************/ + +#ifndef __BOARD_PIN_H__ +#define __BOARD_PIN_H__ + +#include "asm/gpio.h" + +/*============================================================================*/ +/* 功能开关:某路硬件异常时可单独关掉,不影响配网 */ +/* 0 时对应模块 init/run 直接 return,GPIO 不配,日志打一次 xxx disabled */ +/*============================================================================*/ + +#define BOARD_LED_ENABLE 1 /**< 1=状态 LED PC2(低亮) */ +#define BOARD_KEY_ENABLE 1 /**< 1=按键 PB1(低按下) */ +#define BOARD_FAN_ENABLE 1 /**< 1=风扇 MCPWM PC3(高导通) */ +#define BOARD_DISP_ENABLE 1 /**< 1=CT1642 数码管(PG0/PG1 独立) */ +#define BOARD_TH_ENABLE 1 /**< 1=AHT20 温湿度(PC4/PC5 独立) */ +#define BOARD_MODE_ENABLE 1 /* 通风/强力状态机;P1 开 */ +#define BOARD_COND_ENABLE 1 /* 条件引擎;P2 开 */ +#define BOARD_TIMER_ENABLE 1 /* 定时引擎;P2 开 */ +#define BOARD_NVM_ENABLE 1 /* VM 持久化;P1 开 */ + +/*============================================================================*/ +/* GPIO */ +/*============================================================================*/ + +#define BOARD_UART_TX_PIN IO_PORTG_06 /**< 调试 UART TX(只发不占 PC5) */ +#define BOARD_DISP_CLK_PIN IO_PORTG_00 /**< CT1642 CLK(独立,飞线 G0) */ +#define BOARD_DISP_DATA_PIN IO_PORTG_01 /**< CT1642 DATA(独立,飞线 G1) */ +#define BOARD_TH_SCL_PIN IO_PORTC_04 /**< AHT20 SCLK(独立,推挽驱动) */ +#define BOARD_TH_SDA_PIN IO_PORTC_05 /**< AHT20 SDA(开漏+内部上拉,外部 4.7~10k 双保险) */ +#define BOARD_FAN_PIN IO_PORTC_03 /**< 风扇 PWM,高电平导通,MCPWM */ +#define BOARD_LED_PIN IO_PORTC_02 /**< 状态 LED,低电平亮 */ +#define BOARD_KEY_PIN IO_PORTB_01 /**< 按键,低电平按下 */ + +#define BOARD_LED_ON_LEVEL 0 /**< LED 点亮电平:0=低亮 */ +#define BOARD_FAN_ON_LEVEL 1 /**< 风扇有效电平:1=高导通 */ +#define BOARD_KEY_ACTIVE_LEVEL 0 /**< 按键有效电平:0=按下为低 */ + +/*============================================================================*/ +/* 按键 / 开机 */ +/*============================================================================*/ + +#define BOARD_KEY_CLICK_MS 1000 /**< 开机后:按下 <1s 松开为短按切档 */ +#define BOARD_KEY_POWER_MS 3000 /**< 关机态:按住 3s 开机;开机态:按住 3s 关机 */ +#define BOARD_KEY_PAIR_MS 5000 /**< 关机态:按住 5s 开机并进配对 */ +#define BOARD_KEY_OTA_MS 12000 /**< 关机态:按住 12s 开机并进 OTA */ +#define BOARD_KEY_DEBOUNCE_MS 20 /**< 按键消抖 */ +#define BOARD_PAIR_TIMEOUT_MS (120u * 1000u) /**< 配对超时 120s,失败后保持开机 */ + +/*============================================================================*/ +/* LED(单灯 PC2,事件表见 led_manager.c) */ +/*============================================================================*/ + +#define BOARD_LED_FAST_ON_MS 100 /**< 配对快闪:亮 */ +#define BOARD_LED_FAST_OFF_MS 100 /**< 配对快闪:灭 */ +#define BOARD_LED_OTA_ON_MS 200 /**< OTA 闪:亮 */ +#define BOARD_LED_OTA_OFF_MS 200 /**< OTA 闪:灭 */ +#define BOARD_LED_PAIR_OK_MS 3000 /**< 配对成功常亮时长(允许直接常亮) */ +#define BOARD_LED_PAIR_FAIL_MS 3000 /**< 配对失败 200ms 闪提示时长 */ +#define BOARD_LED_STANDBY_ON 1 /**< 1=开机常亮 */ + +/*============================================================================*/ +/* 风扇 PWM */ +/*============================================================================*/ + +#define BOARD_FAN_PWM_HZ 20000 /**< MCPWM 频率(Hz),20k 避开可听啸叫 */ +#define BOARD_FAN_DUTY_GEAR0 0 /**< 0% */ +#define BOARD_FAN_DUTY_GEAR1 5000 /**< 50%(用户拍板,2026-08-31) */ +#define BOARD_FAN_DUTY_GEAR2 7500 /**< 75%(用户拍板,2026-08-31) */ +#define BOARD_FAN_DUTY_GEAR3 10000 /**< 100% */ + +/*============================================================================*/ +/* 显示(CT1642) */ +/*============================================================================*/ + +/** + * CT1642 动态扫描节拍(ms)。 + * ⚠ CT1642 内部只有单个 12 位锁存器(4 位选 + 8 段码),无按位显示 RAM、不自动扫描, + * 一次只点亮当前锁存的那 1 位。必须 MCU 持续轮询 4 位才能 4 位同亮、不闪。 + * 本节拍每 BOARD_DISP_SCAN_MS 点亮 1 位、4 位轮询 → 每位 4*BOARD_DISP_SCAN_MS 一轮。 + * 4ms:每位 16ms/62.5Hz、25% 占空,均匀不闪,且不饿死 priority=1 扫描中断里的 + * 主循环喂狗(实测 2ms 节拍会导致上电硬复位,禁止)。 + * 注:SCAN_TEST=1 段扫描用"值变化才刷"机制,本节拍不敏感;正常显示(0)与 + * 位选验证(2)用分时轮询,必须 ≤4ms 否则肉眼可见闪烁。 + */ +#define BOARD_DISP_SCAN_MS 4 + +#define BOARD_DISP_BLINK_MS 500 /**< 告警闪烁半周期(P1 用) */ + +/** + * 告警闪烁调试宏(P1 台上验证用,正常固件保持 0): + * 0 = 关:闪烁由 app_disp_set_temp/humi_alarm 驱动(P2 条件引擎喂) + * 1 = 强制温度管闪(模拟温度比较成立) + * 2 = 强制湿度管闪(模拟湿度比较成立) + */ +#define BOARD_DISP_ALARM_TEST 0 + +/** + * CT1642 协议(18bit/帧,CLK 上升沿采样,DATA 仅在 CLK=低时改变): + * [0:3] C1-C4 位选(低有效,入参 com 取反后先发 C1) + * [4:9] 6 位保留位(固定填 1) + * [10:17] Q2-Q9 段码(LSB 先,seg 的 bit0 → Q2 引脚) + * 发完 18bit:CLK=高 时 DATA 0→1 = 锁存;CLK=低 时 DATA 0→1 = 输出刷新。 + * 刷新一位数码管 = 发一帧;4 位连发 4 帧(每帧仅一个 Cx 位选生效)。 + * + * ⚠ P0 扫段:段码/位选与硬件物理映射为"待实测"值,台上用 SCAN_TEST 扫清后修正。 + * seg_tbl 段码位定义:bit7=DP bit6=G bit5=F bit4=E bit3=D bit2=C bit1=B bit0=A + * (bit0 先发 → 落到 Q2 引脚;PCB 上 Q2 接哪段即"软件 A 段"对应的物理段) + * COM 位选 → 物理管位映射(2026-08-29 SCAN_TEST=2 实测定型): + * C1=湿度十位 C2=湿度个位 C3=温度十位 C4=温度个位(默认假定温/湿相反,已对调) + */ +#define BOARD_DISP_COM_TEMP_TEN 0x04 /**< C3 = 温度十位(实测定型) */ +#define BOARD_DISP_COM_TEMP_ONE 0x08 /**< C4 = 温度个位(实测定型) */ +#define BOARD_DISP_COM_HUMI_TEN 0x01 /**< C1 = 湿度十位(实测定型) */ +#define BOARD_DISP_COM_HUMI_ONE 0x02 /**< C2 = 湿度个位(实测定型) */ + +/** + * 扫段调试模式(P0 台上定型用,2026-08-29 已全部定型,正常固件保持 0): + * 0 = 正常:无采样时写死 25/60(分时轮询 + 提速移位) + * 1 = 段扫描:4 位同显一段,值变化才刷(无鬼影);段序已实测确认 A..G 顺序正确 + * 2 = 位扫描:4 位分显 1/2/3/4(分时轮询);位序已实测确认 C1=湿十/C2=湿个/C3=温十/C4=温个 + */ +#define BOARD_DISP_SCAN_TEST 0 + +/*============================================================================*/ +/* 传感器(AHT20) */ +/*============================================================================*/ + +#define BOARD_TH_PERIOD_MS 2000 /**< 采集周期 2s(手册要求 >1s,规格书第 2.3/18 章) */ +#define BOARD_TH_FAIL_MAX 3 /**< 连续失败 3 次判传感器故障 */ +#define BOARD_AHT20_WAIT_MS 80 /**< 测量等待时长 */ +#define BOARD_AHT20_I2C_HZ 100000 /**< 软件 I2C 时钟 */ + +/*============================================================================*/ +/* 模式(P1 起用,P0 关闭) */ +/*============================================================================*/ + +#define BOARD_MODE_VENT_ON_MS (25u * 60u * 1000u) /**< 通风开段 25min */ +#define BOARD_MODE_VENT_OFF_MS (5u * 60u * 1000u) /**< 通风停段 5min */ +#define BOARD_MODE_POWER_ON_MS (15u * 60u * 1000u) /**< 强力一轮 15min */ + +#endif /* __BOARD_PIN_H__ */ diff --git a/apps/usr_periph/bsp_aht20.c b/apps/usr_periph/bsp_aht20.c new file mode 100644 index 0000000..a7c6a96 --- /dev/null +++ b/apps/usr_periph/bsp_aht20.c @@ -0,0 +1,436 @@ +/****************************************************************************** + * @file bsp_aht20.c + * @brief CBR3 AHT20 软件 I2C 驱动实现 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V1.0.3 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布 + * - V1.0.1, 2026.08.29, cyWu, 修复 SCL 高电平期间推挽输出 SDA 导致的假 STOP + * (读数据必 CRC fail 的根因);补 AHT20 0x71 校准检查 + 0xBE 初始化 + * - V1.0.2, 2026.08.29, cyWu, 排障结束:回退诊断代码(电平自检/地址扫描/IN 直读), + * 保留时序修复、内部上拉依赖、总线恢复、0x71/0xBE 初始化流程 + * - V1.0.3, 2026.08.29, cyWu, IO 分离:AHT20 独占 PC4(SCL)/PC5(SDA),删除 + * bsp_bus 共线锁与 measure_start/finish 拆分锁,采集恢复一次完成; + * PC4/PC5 上拉配置从 bsp_bus_init 移入本文件 init + ******************************************************************************/ + +#include "bsp_aht20.h" +#include "board_pin.h" +#include "asm/gpio.h" + +#define LOG_TAG_CONST APP +#define LOG_TAG "[AHT20]" +#define LOG_ERROR_ENABLE +#define LOG_INFO_ENABLE +#include "debug.h" + +#define AHT20_ADDR_W 0x70 /**< 写地址(7bit 0x38 << 1) */ +#define AHT20_ADDR_R 0x71 /**< 读地址 */ +#define AHT20_CMD_MEASURE 0xAC +#define AHT20_CMD_INIT 0xBE /**< 初始化校准寄存器命令 */ +#define AHT20_CMD_SOFTRESET 0xBA /**< 软复位命令(备用) */ +#define AHT20_READ_LEN 7 /**< status + 20bit humi + 20bit temp + crc */ + +static uint8_t s_inited; + +/* 软件 I2C:PC5=SDA(三态开漏,内部上拉 + 外部 4.7~10k 双保险),PC4=SCL(推挽) + * + * ⚠ 时序铁律:SDA 的方向切换 / 推挽输出只能在 SCL=0 时进行! + * 若在 SCL=1 期间把 SDA 推挽输出为高,而从机正拉低 SDA(数据位 0 或 ACK), + * 会在 SCL 高电平期间制造 SDA 上升沿 = 从机误判 STOP,传输中止 → + * 之后读到的全是上拉的垃圾字节 → CRC fail(V1.0.0 实测 bug)。 + * + * ⚠ IO 分离(V1.0.3):CT1642 已移到 PG0/PG1,本驱动独占 PC4/PC5, + * 无总线锁、无模式切换,可直接调用。 + */ + +/** + * @brief ~2us 延时(24MHz,约 48 条指令),对应 I2C ≤250kHz + * @return 无 + */ +static void i2c_delay(void) +{ + volatile uint32_t i; + + for (i = 0; i < 16; i++) { + /* nop */ + } +} + +/** + * @brief SCL 拉高/拉低(SCL 推挽输出,主控独占驱动,无方向切换) + * @param level 1=高,0=低 + * @return 无 + */ +static void i2c_scl(uint8_t level) +{ + gpio_direction_output(BOARD_TH_SCL_PIN, level ? 1 : 0); + i2c_delay(); +} + +/** + * @brief 设置 SDA 输出电平(内部含"输出高=推 1 后切高阻") + * @note ⚠ 仅允许在 SCL=0 时调用(START/STOP 条件除外,那两处从机不驱动总线) + * @param level 1=高(推 1 后切高阻,靠外部上拉保持),0=低(推挽拉低) + * @return 无 + */ +static void i2c_sda_out(uint8_t level) +{ + if (level) { + gpio_direction_output(BOARD_TH_SDA_PIN, 1); + gpio_set_direction(BOARD_TH_SDA_PIN, 1); /* 高阻,外部上拉拉高 */ + } else { + gpio_direction_output(BOARD_TH_SDA_PIN, 0); + } + i2c_delay(); +} + +/** + * @brief 释放 SDA 为输入(高阻) + * @note ⚠ 仅允许在 SCL=0 时调用;释放后用 i2c_sda_read() 采样 + * @return 无 + */ +static void i2c_sda_release(void) +{ + gpio_direction_output(BOARD_TH_SDA_PIN, 1); + gpio_set_direction(BOARD_TH_SDA_PIN, 1); + i2c_delay(); +} + +/** + * @brief 采样 SDA 电平(调用时 SDA 必处于输入态) + * @return 1=高,0=低 + */ +static uint8_t i2c_sda_read(void) +{ + return (uint8_t)(gpio_read(BOARD_TH_SDA_PIN) ? 1 : 0); +} + +/** + * @brief I2C START:SCL 高时 SDA 高→低 + * @note 起始时 SCL 必为低(上一帧收尾),先释放 SDA 再抬 SCL + * @return 无 + */ +static void i2c_start(void) +{ + i2c_scl(0); + i2c_sda_out(1); + i2c_scl(1); + i2c_sda_out(0); /* SCL 高时 1→0 = START */ + i2c_scl(0); +} + +/** + * @brief I2C STOP:SCL 高时 SDA 低→高 + * @return 无 + */ +static void i2c_stop(void) +{ + i2c_scl(0); + i2c_sda_out(0); + i2c_scl(1); + i2c_sda_out(1); /* SCL 高时 0→1 = STOP;此时从机已释放总线,推 1 无冲突 */ +} + +/** + * @brief 写一个字节(MSB 先;每位 SDA 都在 SCL 低电平期间变化) + * @param byte 数据 + * @return 无 + */ +static void i2c_write_byte(uint8_t byte) +{ + uint8_t i; + + for (i = 0; i < 8; i++) { + i2c_sda_out((uint8_t)((byte & 0x80u) ? 1 : 0)); + byte <<= 1; + i2c_scl(1); + i2c_scl(0); + } +} + +/** + * @brief 等待从机 ACK(第 9 个时钟;SDA 方向切换只在 SCL=0 时做) + * @return 0=收到 ACK,1=NACK + */ +static uint8_t i2c_wait_ack(void) +{ + uint8_t ack; + + i2c_scl(0); + i2c_sda_release(); /* SCL 低时切输入,绝不污染 SCL 高电平区间 */ + i2c_scl(1); + ack = i2c_sda_read(); + i2c_scl(0); + return ack; /* 0=ACK(从机拉低),1=NACK */ +} + +/** + * @brief 读一个字节(先在 SCL=0 时释放 SDA,循环内只采样不切方向) + * @param ack 1=收完发 ACK(继续读),0=发 NACK(最后一字节) + * @return 读到的字节 + */ +static uint8_t i2c_read_byte(uint8_t ack) +{ + uint8_t i; + uint8_t byte = 0; + + i2c_sda_release(); + for (i = 0; i < 8; i++) { + i2c_scl(1); + byte = (uint8_t)((byte << 1) | i2c_sda_read()); + i2c_scl(0); + } + i2c_sda_out(ack ? 0 : 1); /* 第 9 位:ACK 拉低 / NACK 释放(SCL 低时设置) */ + i2c_scl(1); + i2c_scl(0); + i2c_sda_out(1); + return byte; +} + +/** + * @brief CRC8(poly 0x31,init 0xFF) + * @param data 数据指针 + * @param len 长度 + * @return CRC 值 + */ +static uint8_t aht20_crc8(const uint8_t *data, uint8_t len) +{ + uint8_t crc = 0xFF; + uint8_t i, b; + + for (i = 0; i < len; i++) { + crc ^= data[i]; + for (b = 0; b < 8; b++) { + crc = (uint8_t)((crc & 0x80u) ? ((crc << 1) ^ 0x31u) : (crc << 1)); + } + } + return crc; +} + +/** + * @brief I2C 总线恢复:发 9 个 SCL 时钟 + STOP,解开"从机拉死 SDA"的卡死态 + * @note 卡死场景:之前某次传输中途被复位/打断,从机停在"还有 N 位没发完", + * 持续拉低 SDA,此后所有 START/ACK 全废。标准 I2C 恢复手段。 + * @return 无 + */ +static void i2c_bus_recover(void) +{ + uint8_t i; + + i2c_scl(0); + i2c_sda_release(); /* 释放 SDA(SCL=低时切输入) */ + for (i = 0; i < 9; i++) { + i2c_scl(1); + if (i2c_sda_read()) { + break; /* SDA 回到高:从机已释放 */ + } + i2c_scl(0); + } + i2c_stop(); /* 补一个 STOP 收尾 */ +} + +/** + * @brief 读状态寄存器(0x71):检查校准使能位,未校准则发 0xBE 0x08 0x00 + * @note IO 分离后 AHT20 独占 PC4/PC5,无需总线锁,直接调用 + * @return 0=就绪(已校准),-1=通讯失败 + */ +static int8_t aht20_check_and_calib(void) +{ + uint8_t status; + + /* 写 0x71 读状态命令 */ + i2c_start(); + i2c_write_byte(AHT20_ADDR_W); + if (i2c_wait_ack()) { + goto fail; + } + i2c_write_byte(0x71); + if (i2c_wait_ack()) { + goto fail; + } + i2c_stop(); + + /* 读 1 字节状态 */ + i2c_start(); + i2c_write_byte(AHT20_ADDR_R); + if (i2c_wait_ack()) { + goto fail; + } + status = i2c_read_byte(0); + i2c_stop(); + + if (status & 0x08u) { + return 0; /* Bit3 Calibration Enabled:出厂默认已校准 */ + } + + /* 未校准:发初始化命令 0xBE 0x08 0x00 */ + log_error("aht20 not calib(0x%02x), init 0xBE\n", status); + i2c_start(); + i2c_write_byte(AHT20_ADDR_W); + if (i2c_wait_ack()) { + goto fail; + } + i2c_write_byte(AHT20_CMD_INIT); + if (i2c_wait_ack()) { + goto fail; + } + i2c_write_byte(0x08); + if (i2c_wait_ack()) { + goto fail; + } + i2c_write_byte(0x00); + if (i2c_wait_ack()) { + goto fail; + } + i2c_stop(); + return 0; + +fail: + i2c_stop(); + return -1; +} + +/** + * @brief 初始化:配置 PC4/PC5 + 软件 I2C 总线复位 + 0x71 校准检查 + 0xBE 初始化 + * @return 无 + */ +void bsp_aht20_init(void) +{ + if (s_inited) { + return; + } +#if BOARD_TH_ENABLE + /* 上电后等待 ≥100ms 由 app 启动节奏保证,此处直接操作 */ + /* PC5(SDA) 开内部上拉:I2C 释放 SDA 后必须有上拉,否则浮空 + * (实测无上拉时:init 读状态=0xFF 假 init ok;等 80ms 后漏电读全 0 → crc fail)。 + * 板上有外部 4.7~10k 上拉时双保险。PC4(SCL) 推挽驱动,上拉无害防浮空。 */ + gpio_set_pull_up(BOARD_TH_SDA_PIN, 1); + gpio_set_pull_down(BOARD_TH_SDA_PIN, 0); + gpio_set_die(BOARD_TH_SDA_PIN, 1); + gpio_set_pull_up(BOARD_TH_SCL_PIN, 1); + gpio_set_pull_down(BOARD_TH_SCL_PIN, 0); + gpio_set_die(BOARD_TH_SCL_PIN, 1); + + /* 总线复位:先 9 clock + STOP 解开可能的从机卡死,再补一次 STOP */ + i2c_bus_recover(); + i2c_stop(); + + if (aht20_check_and_calib() == 0) { + log_info("aht20 init ok\n"); + } else { + /* 校准检查失败不阻塞测量(多数 AHT20 出厂已校准),后续采样重试; + * 顺带做一次总线恢复,解开可能的从机卡死 */ + log_error("aht20 calib check fail\n"); + i2c_bus_recover(); + } + s_inited = 1; +#else + log_info("aht20 disabled\n"); +#endif +} + +/** + * @brief 发起一次测量:发 0xAC 0x33 0x00 + * @note 调用后必须等 ≥80ms 再 bsp_aht20_measure_finish。等待期间 I2C 总线 + * 空闲(AHT20 内部测量),CT1642 在 PG0/PG1 独立刷新,互不影响。 + * @return 0=命令已发出,非 0=失败 + */ +int8_t bsp_aht20_measure_start(void) +{ +#if !BOARD_TH_ENABLE + return -1; +#else + if (!s_inited) { + bsp_aht20_init(); /* init 失败时此处补做 */ + if (!s_inited) { + return -1; + } + } + + i2c_start(); + i2c_write_byte(AHT20_ADDR_W); + if (i2c_wait_ack()) { + log_error("addr nack\n"); + goto fail; + } + i2c_write_byte(AHT20_CMD_MEASURE); + if (i2c_wait_ack()) { + log_error("cmd nack\n"); + goto fail; + } + i2c_write_byte(0x33); + if (i2c_wait_ack()) { + log_error("param1 nack\n"); + goto fail; + } + i2c_write_byte(0x00); + if (i2c_wait_ack()) { + log_error("param2 nack\n"); + goto fail; + } + i2c_stop(); + return 0; + +fail: + i2c_stop(); + return -1; +#endif +} + +/** + * @brief 读取测量结果:读 7 字节 + CRC8 校验 + 换算 + * @param temp_x10 输出温度 ×10(如 25.3℃ → 253) + * @param humi_pct 输出湿度 0~100 整数 % + * @return 0=成功;-1=通讯失败(CRC 错/超时/忙) + */ +int8_t bsp_aht20_measure_finish(int16_t *temp_x10, uint8_t *humi_pct) +{ +#if !BOARD_TH_ENABLE + return -1; +#else + uint8_t buf[AHT20_READ_LEN]; + uint8_t i; + uint32_t humi_raw; + uint32_t temp_raw; + int32_t temp_10; + int8_t ret = -1; + + i2c_start(); + i2c_write_byte(AHT20_ADDR_R); + if (i2c_wait_ack()) { + log_error("read addr nack\n"); + goto out; + } + for (i = 0; i < AHT20_READ_LEN; i++) { + buf[i] = i2c_read_byte((uint8_t)((i < (AHT20_READ_LEN - 1)) ? 1 : 0)); + } + i2c_stop(); + + if (aht20_crc8(buf, AHT20_READ_LEN - 1) != buf[AHT20_READ_LEN - 1]) { + log_error("crc fail: %02X %02X %02X %02X %02X %02X crc=%02X\n", + buf[0], buf[1], buf[2], buf[3], buf[4], buf[5], buf[6]); + goto out; + } + if (buf[0] & 0x80u) { /* busy 位 */ + log_error("sensor busy\n"); + goto out; + } + + humi_raw = ((uint32_t)buf[1] << 12u) | ((uint32_t)buf[2] << 4u) | ((uint32_t)buf[3] >> 4u); + temp_raw = (((uint32_t)buf[3] & 0x0Fu) << 16u) | ((uint32_t)buf[4] << 8u) | (uint32_t)buf[5]; + + /* T = raw * 200 / 2^20 - 50(℃);×10 输出。整数运算:raw*2000 最大 ~2e7,int32 安全 */ + temp_10 = (int32_t)((temp_raw * 2000u) / 1048576u) - 500; + *temp_x10 = (int16_t)temp_10; + *humi_pct = (uint8_t)((humi_raw * 100u) / 1048576u); + if (*humi_pct > 100) { + *humi_pct = 100; + } + ret = 0; + +out: + return ret; +#endif +} diff --git a/apps/usr_periph/bsp_aht20.h b/apps/usr_periph/bsp_aht20.h new file mode 100644 index 0000000..56c9d41 --- /dev/null +++ b/apps/usr_periph/bsp_aht20.h @@ -0,0 +1,48 @@ +/****************************************************************************** + * @file bsp_aht20.h + * @brief CBR3 AHT20 温湿度传感器:软件 I2C + 测量 + CRC8 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V1.0.3 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布 + * - V1.0.1, 2026.08.29, cyWu, 修复 SCL 高电平期间推挽输出 SDA 导致的假 STOP + * - V1.0.2, 2026.08.29, cyWu, 回退诊断代码,保留时序修复/上拉/总线恢复/0x71 校准 + * - V1.0.3, 2026.08.29, cyWu, IO 分离:AHT20 独占 PC4(SCL)/PC5(SDA),删除 + * bsp_bus 共线锁与 measure_start/finish 拆分锁,恢复一次完成采集 + * + * @note I2C 7 位地址 0x38(写 0x70 / 读 0x71)。测量命令 0xAC 0x33 0x00, + * 等 ≥80ms 后读 7 字节:Status + 20bit 湿度 + 20bit 温度 + CRC8。 + * CRC8 多项式 0x31,初值 0xFF,覆盖前 6 字节。 + * PC4=SCL(推挽)/ PC5=SDA(开漏+内部上拉),与 CT1642 的 PG0/PG1 + * 完全独立,无总线竞争(2026-08-29 改板分离)。 + ******************************************************************************/ + +#ifndef __BSP_AHT20_H__ +#define __BSP_AHT20_H__ + +#include + +/** + * @brief 初始化:配置 PC4/PC5(上拉+die)+ 软件 I2C 总线复位 + 0x71 校准检查 + * @return 无 + */ +void bsp_aht20_init(void); + +/** + * @brief 发起一次测量:发 0xAC 0x33 0x00 + * @note 调用后必须等 ≥80ms 再 bsp_aht20_measure_finish。 + * 等待期间 I2C 总线空闲(AHT20 内部测量),CT1642 显示不受影响。 + * @return 0=命令已发出,非 0=失败(未初始化/通讯失败) + */ +int8_t bsp_aht20_measure_start(void); + +/** + * @brief 读取测量结果:读 7 字节 + CRC8 校验 + 换算 + * @param temp_x10 输出温度 ×10(如 25.3℃ → 253) + * @param humi_pct 输出湿度 0~100 整数 % + * @return 0=成功,非 0=失败(CRC 错/时序错,本次采样视为失败) + */ +int8_t bsp_aht20_measure_finish(int16_t *temp_x10, uint8_t *humi_pct); + +#endif /* __BSP_AHT20_H__ */ diff --git a/apps/usr_periph/bsp_ct1642.c b/apps/usr_periph/bsp_ct1642.c new file mode 100644 index 0000000..300cc21 --- /dev/null +++ b/apps/usr_periph/bsp_ct1642.c @@ -0,0 +1,153 @@ +/****************************************************************************** + * @file bsp_ct1642.c + * @brief CBR3 CT1642 类数码管驱动实现(软件移位) + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V1.0.1 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布 + * - V1.0.1, 2026.08.29, cyWu, IO 分离:CT1642 独立 PG0=CLK/PG1=DATA,删除 + * bsp_bus 共线机制;删调试遗留 latch/show4/scan_seg/scan_com, + * 新增 bsp_ct1642_init() + * + * @note 驱动协议严格参照 CT1642 手册(与 CSDN yitx 一致): + * 一帧 = 18bit,CLK 上升沿采样 DATA(DATA 只在 CLK=低时改变): + * [0:3] C1-C4 位选(低有效,入参 com 取反后发送,先发 C1) + * [4:9] 6 位保留位(固定填 1) + * [10:17] Q2-Q9 段码(LSB 先,seg 的 bit0 → Q2 引脚) + * 发完 18bit 后:CLK=高时 DATA 0→1 = 锁存;CLK=低时 DATA 0→1 = 输出。 + * 段码/位选物理映射由 PCB 决定,SCAN_TEST 扫段/扫位已定型(见 board_pin.h)。 + * 本驱动独立使用 PG0/PG1,与 AHT20 的 PC4/PC5 互不干扰。 + ******************************************************************************/ + +#include "bsp_ct1642.h" +#include "board_pin.h" +#include "asm/gpio.h" + +/* 软件延时:1 次空循环(24MHz 下 ~0.1us 量级,gpio 调用本身开销另计)。 + * ⚠ 提速历程:8→2(鬼影明显→一点点)→1(继续压残余微亮)。 + * 若出现显示乱码/错位(低于 CT1642 最小脉宽),回调到 2。 + * 软件压到极限后残余的微亮 = 硬件层(Q2-Q9 段线加 1~10k 下拉可根除)。 */ +static void ct1642_delay(void) +{ + volatile uint32_t i; + + for (i = 0; i < 1; i++) { + /* nop */ + } +} + +/** + * @brief 初始化 PG0(CLK)/PG1(DATA) 为推挽输出,初始 0 + * @note ⚠ 必须在第一次发帧前调用(app_disp_init 内)。 + * CT1642 为纯输入从机,两根线全程推挽驱动即可,无需上拉/开漏。 + * @return 无 + */ +void bsp_ct1642_init(void) +{ + gpio_set_pull_up(BOARD_DISP_CLK_PIN, 0); + gpio_set_pull_down(BOARD_DISP_CLK_PIN, 0); + gpio_set_die(BOARD_DISP_CLK_PIN, 1); + gpio_direction_output(BOARD_DISP_CLK_PIN, 0); + + gpio_set_pull_up(BOARD_DISP_DATA_PIN, 0); + gpio_set_pull_down(BOARD_DISP_DATA_PIN, 0); + gpio_set_die(BOARD_DISP_DATA_PIN, 1); + gpio_direction_output(BOARD_DISP_DATA_PIN, 0); +} + +static void ct1642_clk(uint8_t level) +{ + gpio_direction_output(BOARD_DISP_CLK_PIN, level); + ct1642_delay(); +} + +static void ct1642_data(uint8_t level) +{ + gpio_direction_output(BOARD_DISP_DATA_PIN, level); + ct1642_delay(); +} + +/** + * @brief 7 段数码管数字转段码(共阴,A=bit0 ... G=bit6,DP=bit7) + * @param digit 0~9;10='-'(G 段);11=全灭 + * @return 段码 + */ +uint8_t bsp_ct1642_seg(uint8_t digit) +{ + static const uint8_t seg_tbl[12] = { + 0x3F, /* 0: abcdef */ + 0x06, /* 1: bc */ + 0x5B, /* 2: abdeg */ + 0x4F, /* 3: abcdg */ + 0x66, /* 4: bcfg */ + 0x6D, /* 5: acdfg */ + 0x7D, /* 6: acdefg */ + 0x07, /* 7: abc */ + 0x7F, /* 8: abcdefg */ + 0x6F, /* 9: abcdfg */ + 0x40, /* '-': g */ + 0x00, /* 灭 */ + }; + + if (digit > 11) { + digit = 11; + } + return seg_tbl[digit]; +} + +/** + * @brief 发一帧 18bit(不含消影):C1-C4 → 6保留 → Q2-Q9,再锁存+输出 + * @note 内部函数;com/seg 的消影由 bsp_ct1642_send_frame 负责。 + */ +static void ct1642_xfer(uint8_t seg, uint8_t com) +{ + uint8_t i; + uint8_t posn = (uint8_t)(~com); /* C1-C4 低有效:位0=选中位 */ + + /* 起始空闲:CLK=低,DATA=高(规格“上电 CLK 低、DATA 高”) */ + ct1642_data(1); + ct1642_clk(0); + + /* 1. 移位 18bit:C1-C4(4) → 6位保留(填1) → Q2-Q9(8,LSB=Q2=seg bit0) */ + for (i = 0; i < 4; i++) { /* C1-C4,先发 C1 */ + ct1642_data((uint8_t)(posn & 0x01u)); + ct1642_clk(0); ct1642_clk(1); /* 下降沿改完,上升沿采样 */ + posn >>= 1; + } + for (i = 0; i < 6; i++) { /* 6 位保留,固定填 1 */ + ct1642_data(1); + ct1642_clk(0); ct1642_clk(1); + } + for (i = 0; i < 8; i++) { /* Q2-Q9 段码,先发 Q2(=seg bit0) */ + ct1642_data((uint8_t)(seg & 0x01u)); + ct1642_clk(0); ct1642_clk(1); + seg >>= 1; + } + + /* 2. 锁存:CLK 保持高,DATA 给 0→1 上升沿 */ + ct1642_clk(1); + ct1642_data(0); + ct1642_data(1); + + /* 3. 输出:CLK 拉低,DATA 给 0→1 上升沿(刷新该位显示) */ + ct1642_clk(0); + ct1642_data(0); + ct1642_data(1); +} + +/** + * @brief 发一帧刷新一位数码管(CT1642 18bit 协议) + * @param seg 段码(bit7=DP ... bit0=A,与 seg_tbl 一致;先发 bit0→Q2) + * @param com COM 位选掩码(bit3=C4 ... bit0=C1,仅 1 位置位;硬件低有效内部取反) + * @note ⚠ 不做任何消影预帧:CT1642 移位过程实时输出,“持续刷新”才是鬼影根因 + * (切位瞬间杂段),预帧/全灭帧都无法根除,且每帧翻倍会饿死 ISR 喂狗导致 + * 上电复位(实测)。鬼影的正确解法在应用层:数字没变就不刷新(值变化才 + * 刷一次,其余时间锁存静止显示)。本函数保持单帧、极短,适配 ISR 上下文。 + * @return 无 + */ +void bsp_ct1642_send_frame(uint8_t seg, uint8_t com) +{ + // ct1642_xfer(0, 0); + ct1642_xfer(seg, com); /* 单帧:18bit 移位 + 锁存 + 输出,无消影预帧 */ +} diff --git a/apps/usr_periph/bsp_ct1642.h b/apps/usr_periph/bsp_ct1642.h new file mode 100644 index 0000000..6c1933d --- /dev/null +++ b/apps/usr_periph/bsp_ct1642.h @@ -0,0 +1,46 @@ +/****************************************************************************** + * @file bsp_ct1642.h + * @brief CBR3 CT1642 类数码管驱动:18bit 移位、锁存、送出 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V1.0.1 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布 + * - V1.0.1, 2026.08.29, cyWu, IO 分离:CT1642 独立 PG0=CLK/PG1=DATA(飞线), + * AHT20 独占 PC4/PC5,删除 bsp_bus 共线机制;删调试遗留 + * latch/show4/scan_seg/scan_com,新增 bsp_ct1642_init() + * + * @note PG0=CLK,PG1=DATA,独立引脚、无总线锁,可随时调用。 + * 协议:一帧 18bit(C1-C4 位选 + 6 位保留 + Q2-Q9 段码),CLK 上升沿采样 + * DATA(DATA 只在 CLK=低时改变);发完 CLK=高 时 DATA 上升沿=锁存, + * CLK=低 时 DATA 上升沿=输出刷新。 + ******************************************************************************/ + +#ifndef __BSP_CT1642_H__ +#define __BSP_CT1642_H__ + +#include + +/** + * @brief 初始化 PG0/PG1 为推挽输出(CLK/DATA 初始 0) + * @note app_disp_init() 内调用;被 BOARD_DISP_ENABLE 编译开关保护 + * @return 无 + */ +void bsp_ct1642_init(void); + +/** + * @brief 7 段数码管数字转段码(共阴,A=bit0 ... G=bit6,DP=bit7) + * @param digit 0~9;10='-'(G 段);11=全灭 + * @return 段码 + */ +uint8_t bsp_ct1642_seg(uint8_t digit); + +/** + * @brief 发一帧刷新一位:18bit(C1-C4 位选 + 6 保留 + Q2-Q9 段码,LSB 先) + * @param seg 段码(bit7=DP ... bit0=A,先发 bit0→Q2) + * @param com COM 位选掩码(bit3=C4 ... bit0=C1,仅 1 位置位;硬件低有效内部取反) + * @return 无 + */ +void bsp_ct1642_send_frame(uint8_t seg, uint8_t com); + +#endif /* __BSP_CT1642_H__ */ diff --git a/apps/usr_periph/bsp_hw.c b/apps/usr_periph/bsp_hw.c new file mode 100644 index 0000000..7a75983 --- /dev/null +++ b/apps/usr_periph/bsp_hw.c @@ -0,0 +1,225 @@ +/****************************************************************************** + * @file bsp_hw.c + * @brief CBR3 板级 GPIO / MCPWM:LED、按键、风扇 + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V1.0.0 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹:删电机/充电/电量,改单 LED + 风扇 PWM) + * + * @note CT1642 在 PG0/PG1、AHT20 在 PC4/PC5,各自独立驱动, + * bsp_hw 只管 LED/KEY/FAN,不碰这两组脚。 + ******************************************************************************/ + +#include "system/includes.h" +#include "asm/gpio.h" +#include "asm/mcpwm.h" +#include "bsp_hw.h" + +#define LOG_TAG_CONST APP +#define LOG_TAG "[BSP_HW]" +#define LOG_ERROR_ENABLE +#define LOG_INFO_ENABLE +#include "debug.h" + +static uint8_t s_initialized; + +/** 风扇 PWM 状态 */ +typedef struct { + uint8_t pwm_clk_on; /**< 1=MCPWM 时钟已打开 */ + uint16_t duty; /**< 当前占空比 */ +} BspFanCtx_t; + +static BspFanCtx_t s_fan; + +#if (BOARD_LED_ENABLE || BOARD_KEY_ENABLE) +/** + * @brief 配置 GPIO 为推挽输出 + * @param pin GPIO 编号 + * @param value 输出初值 + * @return 无 + */ +static void bsp_pin_output(u32 pin, int value) +{ + gpio_set_pull_up(pin, 0); + gpio_set_pull_down(pin, 0); + gpio_set_die(pin, 1); + gpio_direction_output(pin, value); +} +#endif + +#if BOARD_KEY_ENABLE +/** + * @brief 配置 GPIO 为数字输入(可内部上拉) + * @param pin GPIO 编号 + * @param pull_up 1=内部上拉,0=不上拉 + * @return 无 + */ +static void bsp_pin_input(u32 pin, int pull_up) +{ + gpio_direction_output(pin, 1); + gpio_set_direction(pin, 1); + gpio_set_pull_down(pin, 0); + gpio_set_pull_up(pin, pull_up ? 1 : 0); + gpio_set_die(pin, 1); +} +#endif + +#if BOARD_FAN_ENABLE +/** + * @brief 初始化风扇 MCPWM ch0,初始占空比 0(关) + * @return 无 + */ +static void bsp_fan_pwm_init(void) +{ + struct pwm_platform_data pwm; + + gpio_set_pull_up(BOARD_FAN_PIN, 0); + gpio_set_pull_down(BOARD_FAN_PIN, 0); + gpio_set_die(BOARD_FAN_PIN, 1); + + pwm.pwm_aligned_mode = pwm_edge_aligned; + pwm.frequency = BOARD_FAN_PWM_HZ; + pwm.duty = 0; + pwm.l_pin = (u8) - 1; + pwm.complementary_en = 0; + pwm.pwm_ch_num = pwm_ch0; + pwm.h_pin = BOARD_FAN_PIN; + mcpwm_init(&pwm); + + mcpwm_set_duty(pwm_ch0, 0); + s_fan.pwm_clk_on = 1; + s_fan.duty = 0; + log_info("fan pwm %uHz duty=0\n", (unsigned)BOARD_FAN_PWM_HZ); +} + +/** + * @brief 打开 MCPWM 并绑回 PC3 + * @return 无 + */ +static void bsp_fan_pwm_clock_on(void) +{ + if (s_fan.pwm_clk_on) { + return; + } + mcpwm_open(pwm_ch0); + gpio_och_sel_output_signal(BOARD_FAN_PIN, OUTPUT_CH_SIGNAL_MC_PWM0_H); + gpio_set_direction(BOARD_FAN_PIN, 0); + s_fan.pwm_clk_on = 1; +} + +/** + * @brief 关闭 MCPWM 时钟并拉低(待机/关机必关) + * @return 无 + */ +static void bsp_fan_pwm_clock_off(void) +{ + mcpwm_set_duty(pwm_ch0, 0); + if (s_fan.pwm_clk_on) { + mcpwm_close(pwm_ch0); + gpio_och_disable_output_signal(BOARD_FAN_PIN, OUTPUT_CH_SIGNAL_MC_PWM0_H); + JL_MCPWM->MCPWM_CON0 = 0; + s_fan.pwm_clk_on = 0; + } + gpio_direction_output(BOARD_FAN_PIN, 0); /* 关 = 拉低(风扇高导通) */ +} +#endif + +/** + * @brief 初始化 GPIO / 风扇 MCPWM + * @return BSP_HW_OK=成功 + */ +BspHw_Ret_t bsp_hw_init(void) +{ + if (s_initialized) { + return BSP_HW_OK; + } + +#if BOARD_LED_ENABLE + bsp_pin_output(BOARD_LED_PIN, !BOARD_LED_ON_LEVEL); /* 初始灭 */ +#endif + +#if BOARD_KEY_ENABLE + bsp_pin_input(BOARD_KEY_PIN, 1); +#endif + +#if BOARD_FAN_ENABLE + bsp_fan_pwm_init(); + bsp_fan_pwm_clock_off(); /* 初始关,待机不耗电 */ +#endif + + s_initialized = 1; + log_info("hw init led=%d key=%d fan=%d\n", + BOARD_LED_ENABLE, BOARD_KEY_ENABLE, BOARD_FAN_ENABLE); + return BSP_HW_OK; +} + +/** + * @brief 查询板级硬件是否已初始化 + * @return BSP_HW_OK=已初始化,BSP_HW_ERR_NOT_INIT=未初始化 + */ +BspHw_Ret_t bsp_hw_get_status(void) +{ + return s_initialized ? BSP_HW_OK : BSP_HW_ERR_NOT_INIT; +} + +/** + * @brief 状态 LED 开关(PC2,低电平亮) + * @param on 1=亮,0=灭 + * @return 无 + */ +void bsp_led_set(uint8_t on) +{ +#if BOARD_LED_ENABLE + gpio_direction_output(BOARD_LED_PIN, on ? BOARD_LED_ON_LEVEL : !BOARD_LED_ON_LEVEL); +#else + (void)on; +#endif +} + +/** + * @brief 风扇 PWM 占空比(PC3,高有效,20kHz) + * @param duty 0~10000(0%=关 MCPWM 并拉低;>0 打开时钟输出) + * @return 无 + */ +void bsp_fan_set_duty(uint16_t duty) +{ +#if BOARD_FAN_ENABLE + if (duty > 10000) { + duty = 10000; + } + if (duty == 0) { + bsp_fan_pwm_clock_off(); + s_fan.duty = 0; + return; + } + bsp_fan_pwm_clock_on(); + mcpwm_set_duty(pwm_ch0, duty); + s_fan.duty = duty; +#else + (void)duty; +#endif +} + +/** + * @brief 读取按键 + * @return 1=按下,0=未按下 + */ +uint8_t bsp_key_is_pressed(void) +{ +#if BOARD_KEY_ENABLE + return (uint8_t)(gpio_read(BOARD_KEY_PIN) == BOARD_KEY_ACTIVE_LEVEL); +#else + return 0; +#endif +} + +/** + * @brief 进深睡前:风扇 PWM 关掉并拉低,灯全灭 + * @return 无 + */ +void bsp_hw_enter_sleep_io(void) +{ + bsp_fan_set_duty(0); + bsp_led_set(0); +} diff --git a/apps/usr_periph/bsp_hw.h b/apps/usr_periph/bsp_hw.h new file mode 100644 index 0000000..f8207aa --- /dev/null +++ b/apps/usr_periph/bsp_hw.h @@ -0,0 +1,61 @@ +/****************************************************************************** + * @file bsp_hw.h + * @brief CBR3 板级硬件:LED / 按键 / 风扇 PWM + * @author cyWu <1917507415@qq.com> + * @date 2026.08.29 + * @version V1.0.0 + * @history + * - V1.0.0, 2026.08.29, cyWu, 首次发布(移植门夹:删电机/充电/电量,改单 LED + 风扇 PWM) + ******************************************************************************/ + +#ifndef __BSP_HW_H__ +#define __BSP_HW_H__ + +#include +#include "board_pin.h" + +typedef enum { + BSP_HW_OK = 0, + BSP_HW_ERR_PARAM, + BSP_HW_ERR_NOT_INIT +} BspHw_Ret_t; + +/** + * @brief 初始化 GPIO / 风扇 MCPWM + * @return BSP_HW_OK=成功 + */ +BspHw_Ret_t bsp_hw_init(void); + +/** + * @brief 查询是否已初始化 + * @return BSP_HW_OK=已初始化,BSP_HW_ERR_NOT_INIT=未初始化 + */ +BspHw_Ret_t bsp_hw_get_status(void); + +/** + * @brief 状态 LED 开关(PC2,低电平亮) + * @param on 1=亮,0=灭 + * @return 无 + */ +void bsp_led_set(uint8_t on); + +/** + * @brief 风扇 PWM 占空比(PC3,高有效,20kHz) + * @param duty 0~10000(0%=关 MCPWM 并拉低;>0 打开时钟输出) + * @return 无 + */ +void bsp_fan_set_duty(uint16_t duty); + +/** + * @brief 读取按键 + * @return 1=按下,0=未按下 + */ +uint8_t bsp_key_is_pressed(void); + +/** + * @brief 进深睡前:风扇 PWM 关掉并拉低,灯全灭 + * @return 无 + */ +void bsp_hw_enter_sleep_io(void); + +#endif /* __BSP_HW_H__ */ diff --git a/dp_definition.json b/dp_definition.json index 11e2b2b..c6cb4e6 100644 --- a/dp_definition.json +++ b/dp_definition.json @@ -99,10 +99,10 @@ "key": "cond_config1", "kind": "normal", "type": "raw", - "len": 6, + "len": 7, "scale": 1.0, "dir": "rw", - "description": "温度\n温度精度是0.1,需要*10换算成整数进行传输\nByte 5\nBit7 ~ Bit5:温度比较关系\nBit4 ~ Bit2:湿度比较关系\nBit1 :Reserved\nBit0 :Reserved\n比较关系建议统一定义\n000 = 无条件\n001 = <\n010 = <=\n011 = =\n100 = >=\n101 = >\n110 = Reserved\n111 = Reserved", + "description": "温度阈值:有符号int16大端,值=℃×10;Byte0=使能;Byte1-2=温度阈值;Byte3=湿度阈值;Byte4=持续分钟(0=立即);Byte5=动作模式(0/1/2);Byte6=Bit7-5温度比较, Bit4-2湿度比较, Bit1-0保留", "values": {} }, { @@ -111,10 +111,10 @@ "key": "cond_config2", "kind": "normal", "type": "raw", - "len": 6, + "len": 7, "scale": 1.0, "dir": "rw", - "description": "同上", + "description": "同上(7字节)", "values": {} }, { @@ -123,10 +123,10 @@ "key": "cond_config3", "kind": "normal", "type": "raw", - "len": 6, + "len": 7, "scale": 1.0, "dir": "rw", - "description": "同上", + "description": "同上(7字节)", "values": {} }, { @@ -135,10 +135,10 @@ "key": "cond_config4", "kind": "normal", "type": "raw", - "len": 6, + "len": 7, "scale": 1.0, "dir": "rw", - "description": "同上", + "description": "同上(7字节)", "values": {} }, { @@ -147,10 +147,10 @@ "key": "cond_config5", "kind": "normal", "type": "raw", - "len": 6, + "len": 7, "scale": 1.0, "dir": "rw", - "description": "同上", + "description": "同上(7字节)", "values": {} }, { @@ -159,10 +159,10 @@ "key": "cond_config6", "kind": "normal", "type": "raw", - "len": 6, + "len": 7, "scale": 1.0, "dir": "rw", - "description": "同上", + "description": "同上(7字节)", "values": {} }, { @@ -171,10 +171,10 @@ "key": "cond_config7", "kind": "normal", "type": "raw", - "len": 6, + "len": 7, "scale": 1.0, "dir": "rw", - "description": "同上", + "description": "同上(7字节)", "values": {} }, {