接入温湿度风扇产品固件(仲裁/条件/定时/显示),并补齐规格书与外设驱动

新增 usr_app 产品逻辑:手动>定时>条件仲裁、条件控温湿、定时任务、数码管显示、
按键/LED、风扇档位、NVM 掉电恢复、配对与开关机,上电按规格恢复上次状态。
新增 usr_periph:AHT20、CT1642、板级引脚与硬件封装;见宝协议 cond_config
改为 7 字节编解码,并接入产品 DP。
补充《温湿度风扇_固件需求规格书》,工程加入对应源文件。
.gitignore 忽略 WorkBuddy/Cursor 本地会话
This commit is contained in:
2026-08-31 15:17:27 +08:00
parent 4cb7418a02
commit 5cc7e9dbd6
53 changed files with 7028 additions and 513 deletions
+12 -1
View File
@@ -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/ 需求规格书
# -----------------------------------------------------------------------------
+82
View File
@@ -177,6 +177,7 @@
<Add directory="apps/usr_jb_proto/Protocol" />
<Add directory="apps/usr_jb_proto/Utils" />
<Add directory="apps/usr_periph" />
<Add directory="apps/usr_app" />
</Compiler>
<Linker>
<Add option="-flto" />
@@ -782,6 +783,87 @@
<Unit filename="apps/usr_periph/usr_rtc.c">
<Option compilerVar="CC" />
</Unit>
<Unit filename="apps/usr_periph/board_pin.h" />
<Unit filename="apps/usr_periph/bsp_hw.c">
<Option compilerVar="CC" />
</Unit>
<Unit filename="apps/usr_periph/bsp_hw.h" />
<Unit filename="apps/usr_periph/bsp_ct1642.c">
<Option compilerVar="CC" />
</Unit>
<Unit filename="apps/usr_periph/bsp_ct1642.h" />
<Unit filename="apps/usr_periph/bsp_aht20.c">
<Option compilerVar="CC" />
</Unit>
<Unit filename="apps/usr_periph/bsp_aht20.h" />
<Unit filename="apps/usr_app/app_boot.c">
<Option compilerVar="CC" />
</Unit>
<Unit filename="apps/usr_app/app_boot.h" />
<Unit filename="apps/usr_app/app_cond.c">
<Option compilerVar="CC" />
</Unit>
<Unit filename="apps/usr_app/app_cond.h" />
<Unit filename="apps/usr_app/app_disp.c">
<Option compilerVar="CC" />
</Unit>
<Unit filename="apps/usr_app/app_disp.h" />
<Unit filename="apps/usr_app/app_fan.c">
<Option compilerVar="CC" />
</Unit>
<Unit filename="apps/usr_app/app_fan.h" />
<Unit filename="apps/usr_app/app_function.c">
<Option compilerVar="CC" />
</Unit>
<Unit filename="apps/usr_app/app_function.h" />
<Unit filename="apps/usr_app/app_led.c">
<Option compilerVar="CC" />
</Unit>
<Unit filename="apps/usr_app/app_led.h" />
<Unit filename="apps/usr_app/app_mode.c">
<Option compilerVar="CC" />
</Unit>
<Unit filename="apps/usr_app/app_mode.h" />
<Unit filename="apps/usr_app/app_arbiter.c">
<Option compilerVar="CC" />
</Unit>
<Unit filename="apps/usr_app/app_arbiter.h" />
<Unit filename="apps/usr_app/app_nvm.c">
<Option compilerVar="CC" />
</Unit>
<Unit filename="apps/usr_app/app_nvm.h" />
<Unit filename="apps/usr_app/app_pair.c">
<Option compilerVar="CC" />
</Unit>
<Unit filename="apps/usr_app/app_pair.h" />
<Unit filename="apps/usr_app/app_power.c">
<Option compilerVar="CC" />
</Unit>
<Unit filename="apps/usr_app/app_power.h" />
<Unit filename="apps/usr_app/app_th.c">
<Option compilerVar="CC" />
</Unit>
<Unit filename="apps/usr_app/app_th.h" />
<Unit filename="apps/usr_app/app_timer_task.c">
<Option compilerVar="CC" />
</Unit>
<Unit filename="apps/usr_app/app_timer_task.h" />
<Unit filename="apps/usr_app/key.c">
<Option compilerVar="CC" />
</Unit>
<Unit filename="apps/usr_app/key.h" />
<Unit filename="apps/usr_app/key_manager.c">
<Option compilerVar="CC" />
</Unit>
<Unit filename="apps/usr_app/key_manager.h" />
<Unit filename="apps/usr_app/led.c">
<Option compilerVar="CC" />
</Unit>
<Unit filename="apps/usr_app/led.h" />
<Unit filename="apps/usr_app/led_manager.c">
<Option compilerVar="CC" />
</Unit>
<Unit filename="apps/usr_app/led_manager.h" />
<Unit filename="apps/usr_periph/usr_rtc.h" />
<Unit filename="apps/usr_le_code/ble_proto.h" />
<Unit filename="apps/usr_le_code/bleproto.h" />
@@ -0,0 +1,640 @@
# 温湿度风扇固件需求规格书
| 项 | 内容 |
|---|---|
| 产品 | 温湿度风扇(爬宠) |
| 芯片 | JL7016 / AC7016CBR28 |
| 工程 | `温湿度风扇_soc_JieLi-JL7016` |
| 产品码 | ProdtCode=`CBR3`productCode=`PRCBR3` |
| 协议 | 见宝 SOC 55AAV1.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 显示器件
- 驱动:类 CT1642CLK+DATA 串行(原理图位号 S1)。
- 两块 2 位 7 段数码管:温度一块、湿度一块(含 DP 段,本产品 **不显示小数**DP 恒灭)。
- **帧协议(2026-08-29 实测定型,与 CSDN yitx 一致)**:一帧 **18bit**,CLK 上升沿采样 DATA,顺序
**C1-C4 位选(4bit,低有效,入参 ~com 先发 C1)→ 6bit 保留(固定填 1)→ Q2-Q9 段码(8bitLSB 先,seg bit0 → Q2**
- DATA 只允许在 **CLK=低** 时改变。
- 发完 18bitCLK=高时 DATA 上升沿 = **锁存**CLK=低时 DATA 上升沿 = **输出刷新**(一次只点亮锁存的那 1 位)。
- CT1642 内部只有 1 个 18 位移位 + 1 个 12 位锁存,**无按位显示 RAM、不自动扫描 4 位**,必须 MCU 持续快节拍轮询(`BOARD_DISP_SCAN_MS=4`4 位才能同亮不闪。
段码与 COM 的 PCB 网络为 `SET1~8``CAN1~4`。P0 必须用写死数字扫清 **哪几 bit 对应温度十位/个位、湿度十位/个位、A~G**。扫不清之前禁止把随机码当产品逻辑。
### 2.3 AHT20
- I2C 7 位地址 **0x38**(写 0x70 / 读 0x71)。
- 上电后 ≥5ms 再操作总线。
- 测量:`0xAC 0x33 0x00`,等 ≥80ms,读 Status + 20bit 湿度 + 20bit 温度 + CRC8。
- `T = (ST / 1048576) * 200 - 50`(℃);湿度按手册 20bit 公式,显示与 DP 用 0~100 整数 %。
- 建议 I2C 100kHz;采集周期 **2s**(手册要求 >1s)。
- CRC 多项式 0x31,初值 0xFF。CRC 失败当本次采样失败。
---
## 3. 软件架构(对齐门夹,删电池,加风扇/显示/传感)
```
apps/earphone/app_main.c
└─ usr_rtc_init()
└─ usr_jb_init() ← 唯一组合根
bsp_hw_init()
app_led_init() ← 必须在开机等待之前
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 当软件 I2CAHT20)。
- 都为 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:常亮(将开机)
- ≥5s100ms 闪(将配对)
- ≥12s200ms 闪(将 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. 数码管 + AHT20IO 独立)
### 8.1 显示内容
| 管子 | 内容 | 范围 |
|---|---|---|
| 温度 2 位 | 整数 ℃ | 0~99 显示绝对值;&lt;0 显示 `-9``-1`(负号用 G 段或自定义,P0 扫段后定);&lt;−9 钳位 `-9`&gt;99 钳位 `99` |
| 湿度 2 位 | 整数 % | 0~99&gt;99 钳位 `99` |
| 读失败 | 双管 `--` | 连续失败 **3** 次后;`fault_code=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.02026-08-31 改板分离)
CT1642 用 **PG0(CLK)/PG1(DATA)**(推挽,`bsp_ct1642_init()` 配置),AHT20 用 **PC4(SCL)/PC5(SDA)**`bsp_aht20_init()` 配置:PC5 内部上拉 + 外部 4.7~10k 双保险)。两路物理隔离、无总线竞争,**删除 `bsp_bus` 模块**,不存在 lock/unlock/模式切换。
运行时序:
```
需要刷显示(每 4ms 扫描回调):
直接发帧(CT1642 单锁存,一次只亮 1 位,靠快节拍轮询 4 位同亮)
鬼影靠"值不变不刷新"或提速分时压(见 board_pin.h 注释与 bsp_ct1642.c
需要采 AHT20(每 2s,一次完成):
发 0xAC 0x33 0x00 → 等 ≥80ms(期间 I2C 空闲,显示照常刷新,互不影响)
→ 读 7 字节 + CRC8
```
规则(V1.1.0 起):
- **无总线锁**:显示扫描(priority=1 定时器 ISR)与 AHT20 采样可同时进行,不再有"采样期冻结显示"(原共线时代每 2s 闪一下的根因已物理消除)。
- `bsp_aht20_measure_start/finish` 不再返回 -2(总线忙),上层无 busy 重试。
- 等待 AHT20 的 80ms 由 `app_th_poll` 状态机跨 tick 完成(期间 `clr_wdt`),不阻塞 20ms 主节拍。
- 共线时代遗留约束(已作废,勿再套用):原"锁只包裹真实硬件交互窗口、长等待必放锁"适用于 PC4/PC5 共线方案,IO 分离后不需要。
### 8.4 采样失败与条件计时
- 条件「持续时长」只在 **本次采样成功且比较成立** 时累加。
- 采样失败:不累加、不清零已累计时长;显示保持上次有效值,直到连续 3 次失败改 `--`
- `--` 期间条件比较视为不成立(不误触发吹风)。
---
## 9. 工作模式(DP1
| 值 | 名称 | 行为 |
|---|---|---|
| 0 | 关闭模式 | 不按模式表驱动风扇;风扇跟手动档 / 其它仲裁胜者 |
| 1 | 通风 | 强制 1 档,开 **25min**,停 **5min**,循环,直到退出 |
| 2 | 强力 | 强制 3 档,连续 **15min** 后:风扇 0mode 回到 0 |
退出方式(任一即退):
- 短按切档(含切到 0
- APP 写 `fan_gear`
- APP 写 `mode=0`
- 定时窗结束(第 11 章)
- 条件恢复快照(第 10 章,若当前模式是条件开的)
**开机恢复(不存相位):** VM 里 `mode=1` 则从 25min 开段 **从头** 计;`mode=2` 则从 15min **从头** 计。
DP 同步:`mode` = 逻辑模式;`fan_gear` = 实际 PWM 档(通风停段为 0)。
---
## 10. 条件引擎(P27 组)
### 10.1 总开关
- `cond_en=0`:不启动新条件;若正在因条件跑模式,视为条件结束,**按 Q9 A3 恢复触发前快照**。
- `cond_en=1`:扫描 config1~7。
### 10.2 7 字节布局(Excel 枚举列;备注里 Byte5 位域是笔误,以本表 Byte6 为准)
| 偏移 | 字段 | 编码 |
|---|---|---|
| Byte0 | 使能 | 0=关,非 0=开 |
| Byte12 | 温度阈值 | **有符号 int16 大端**,传输值 = ℃ × 10。例 30.5℃ → `0x01 0x31`;−5.0℃ → `0xFF 0xCE` |
| Byte3 | 湿度阈值 | 0~100,单位 % |
| Byte4 | 持续分钟 | 1~255 按分钟计;**0 = 比较一成立立即触发** |
| Byte5 | 动作 | **模式**:0=关闭模式,1=通风,2=强力。**不是档位** |
| Byte6 | 条件 | Bit75 温度比较,Bit42 湿度比较,Bit1–0 保留 |
比较编码(3 bit):
| 值 | 含义 |
|---|---|
| 000 | 无条件(忽略该侧) |
| 001 | &lt; |
| 010 | &lt;= |
| 011 | == |
| 100 | &gt;= |
| 101 | &gt; |
| 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. 定时引擎(P27 组)
### 11.1 8 字节(Excel 已定义)
| 偏移 | 字段 |
|---|---|
| Byte0 | 使能 |
| Byte1 | 模式 0/1/2 |
| Byte2 | 类型:0=单次,1=每天,2=每周 |
| Byte3 | 星期位图 Bit0=周一 … Bit6=周日(仅类型 2) |
| Byte4 | 开始小时 0~23 |
| Byte5 | 开始分钟 0~59 |
| Byte6 | 结束小时 |
| Byte7 | 结束分钟 |
- 支持跨午夜:开始 22:00 结束 06:00 = 当晚 22 点到次日 6 点。
- 类型 1:每天该窗。
- 类型 2:仅位图置位的星期(跨午夜的窗,以 **开始时刻所在的本地星期** 判断是否该跑)。
- 类型 0 单次:结束时刻过后把 **Byte0 清 0 并写 VM**,不再跑。
### 11.2 RTC 门闩(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,直到结束时刻。
- 结束时刻:风扇 0mode=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=0gear=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 可不实现动作 |
OTA12s 开机走门夹 `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
**目标:** 两路 IOCT1642 显示 / AHT20 采集)、PWM、按键、灯可独立开关验证。
- [ ] `board_pin.h` + UART TX=PG6,打印正常,且不影响 PC5
- [ ] 关机态 3s 开机、&lt;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「枚举/值定义」列为准。
+29 -203
View File
@@ -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)
+1 -8
View File
@@ -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);
@@ -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 //串口波特率配置
//*********************************************************************************//
+159
View File
@@ -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;
}
}
+105
View File
@@ -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 互调成环。
* 唯一状态源是 ownerNONE/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 <stdint.h>
/** 当前模式占用者 */
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 条件被手动打断:只清快照(快照对应的用户状态已过时),
* 不恢复、不清 ownerowner 已被 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__ */
+132
View File
@@ -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);
}
}
+28
View File
@@ -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__ */
+464
View File
@@ -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, P1set_en/set_config 存 VM(引擎仍关)
* - V2.0.0, 2026.08.31, cyWu, P2 实现完整条件引擎(规格书第 10 章):
* 7 字节解析/AND 比较/时长累计(采样成功才累计)/触发/保持/
* 条件强力循环/后覆盖不回栈/手动作废(RAM)/快照恢复/闪烁喂真值
*
* @note 运行上下文:app_cond_run 由 usr_timer_loop500ms,任务上下文)调用
* (规格书 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~60xFF=无 */
/**
* @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 只看单侧比较成立,不看整组 ANDfault "--" 优先由 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);
}
+51
View File
@@ -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_loop500ms,任务上下文)调用,
* 与仲裁器/定时引擎同侧;不放在 20ms usr_timer_tick(中断上下文)。
******************************************************************************/
#ifndef __APP_COND_H__
#define __APP_COND_H__
#include <stdint.h>
/**
* @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__ */
+278
View File
@@ -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;
}
+68
View File
@@ -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~-1G 段负号);<-9 钳位 -9>99 钳位 99。
* 连续失败 3 次双管 "--"(优先于闪烁)。告警闪烁(规格书 8.2)
* 与吹风解耦:比较成立侧 500ms 亮/500ms 灭,不成立侧常亮数值。
******************************************************************************/
#ifndef __APP_DISP_H__
#define __APP_DISP_H__
#include <stdint.h>
/**
* @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__ */
+82
View File
@@ -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 初始化风扇(档位 0PWM 关)
* @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;
}
+42
View File
@@ -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 <stdint.h>
/**
* @brief 初始化风扇(档位 0PWM 关)
* @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__ */
+220
View File
@@ -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_loopusr_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 <string.h>
#define LOG_TAG_CONST APP
#define LOG_TAG "[APP_FUNC]"
#define LOG_INFO_ENABLE
#include "debug.h"
/** 功能模块状态 */
typedef struct {
uint8_t app_power; /**< DP01=开机态(本产品无 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(); /* 防抖满后真正写 VMsyscfg_write 必须在任务上下文) */
if (s_func.off_pending) {
s_func.off_pending = 0;
app_power_request_poweroff(); /* 落盘后再走关机流程 */
}
}
/**
* @brief 同步 DPpower 恒 1、fan_gear、mode、fault_code),每个业务 tick 调用
* @note DP3/DP4 温湿度在 userHandle500ms)填;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");
}
+82
View File
@@ -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 <stdint.h>
/**
* @brief 初始化功能模块状态
* @return 无
*/
void app_function_init(void);
/**
* @brief 同步 DPpower 恒 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__ */
+143
View File
@@ -0,0 +1,143 @@
/******************************************************************************
* @file app_led.c
* @brief CBR3 灯效适配:单灯 PC2OTA/配对/常亮
* @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);
}
+65
View File
@@ -0,0 +1,65 @@
/******************************************************************************
* @file app_led.h
* @brief CBR3 灯效:单灯 PC2OTA/配对/常亮按优先级
* @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 <stdint.h>
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__ */
+177
View File
@@ -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;
}
+54
View File
@@ -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 <stdint.h>
/**
* @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__ */
+423
View File
@@ -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 章) */
/** 状态 item10 字节) */
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;
/** 条件配置 item49 字节) */
typedef struct {
uint8_t cond_config[7][7];
} __packed Cbr3CondVm_t;
/** 定时配置 item56 字节) */
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 写状态 itemmode/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_loopBLE 线程、
* 任务上下文)执行。曾在此直接调 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_INFOitem 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
}
+73
View File
@@ -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 <stdint.h>
/**
* @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__ */
+76
View File
@@ -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;
}
+45
View File
@@ -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 <stdint.h>
/** 配对结果边沿事件,仅在状态变化那一次返回一次 */
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__ */
+100
View File
@@ -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();
}
}
+55
View File
@@ -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 <stdint.h>
/** 深睡请求的钩子: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__ */
+171
View File
@@ -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;
}
+69
View File
@@ -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 <stdint.h>
/**
* @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__ */
+264
View File
@@ -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, P1set_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_loop500ms,任务上下文)
* 调用——窗判断分钟级精度足够,且单次自毁要写 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~60xFF=无 */
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);
}
+49
View File
@@ -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.1Byte0=使能 Byte1=模式
* Byte2=类型(0单次/1每天/2每周) Byte3=星期位图 Byte4-5=开始时/分
* Byte6-7=结束时/分。RTC 未对时一律不执行(规格书 11.2)。
* ⚠ app_timer_run 由 usr_timer_loop500ms,任务上下文)调用
* (单次自毁要写 VM,禁止放 20ms 中断上下文)。
******************************************************************************/
#ifndef __APP_TIMER_TASK_H__
#define __APP_TIMER_TASK_H__
#include <stdint.h>
/**
* @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__ */
+267
View File
@@ -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 <stddef.h>
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;
}
+100
View File
@@ -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 <stdint.h>
#include <string.h>
/*============================================================================*/
/* 宏定义 */
/*============================================================================*/
#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__ */
+148
View File
@@ -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();
}
+53
View File
@@ -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__ */
+328
View File
@@ -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 <stddef.h>
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
+120
View File
@@ -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 <stdint.h>
#include <string.h>
/*============================================================================*/
/* 宏定义 */
/*============================================================================*/
#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__ */
+193
View File
@@ -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();
}
+33
View File
@@ -0,0 +1,33 @@
/******************************************************************************
* @file led_manager.h
* @brief CBR3 单 LEDPC2)通道与事件表
* @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__ */
+89 -200
View File
@@ -1,18 +1,30 @@
/* ================================================================
* jb_product.c — 用户层实现(请填补空白处
* 设备: 温湿度风扇 | 生成时间: 2026-08-29 10:31:50
* jb_product.c — 用户层实现(薄适配:只写 DP,不写业务算法
* 设备: 温湿度风扇 | 生成时间: 2026-08-29cond_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() 填温湿度 DP500ms
* 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 条件配置17 字节) */
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_poll20ms)同步
* ════════════════════════════════════════════════════════════════ */
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 链路传 uint16scale=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)
{
+56 -56
View File
@@ -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;
+15 -15
View File
@@ -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 */
+243 -11
View File
@@ -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)
{
jbInit();
userInit();
if(jb_timer_id==0)jb_timer_id = sys_s_hi_timer_add(NULL, usr_timer_1_ms, TIMER_PERIOD_MS);
app_nvm_save(); /* 软关机前立即写 VM(不等待 500ms 防抖) */
app_function_stop_all();
app_led_clear();
}
void usr_timer_1_ms()
/**
* @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 (usr_var.usr_goto_updata) {
/* 开机 12s 已进 OTA:立刻挂 200ms 闪 */
app_led_request_ota();
}
app_fan_init();
app_disp_init();
/* CT1642 无内部扫描 RAM4ms 快节拍轮询 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);
}
/**
* @brief BLE 收到见宝载荷,送入协议环形缓冲
* @param data 载荷指针
* @param len 载荷长度,单位字节
* @return 无
*/
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_tickusr_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 防抖落盘 + 延后关机收口 */
}
Binary file not shown.
+156
View File
@@ -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 直接 returnGPIO 不配,日志打一次 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 引脚)
* 发完 18bitCLK=高 时 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__ */
+436
View File
@@ -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 failV1.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 STARTSCL 高时 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 STOPSCL 高时 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=收到 ACK1=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 CRC8poly 0x31init 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(); /* 释放 SDASCL=低时切输入) */
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 最大 ~2e7int32 安全 */
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
}
+48
View File
@@ -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 <stdint.h>
/**
* @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__ */
+153
View File
@@ -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 一致):
* 一帧 = 18bitCLK 上升沿采样 DATADATA 只在 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=bit6DP=bit7
* @param digit 0~910='-'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. 移位 18bitC1-C4(4) → 6位保留(填1) → Q2-Q9(8LSB=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 移位 + 锁存 + 输出,无消影预帧 */
}
+46
View File
@@ -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=CLKPG1=DATA,独立引脚、无总线锁,可随时调用。
* 协议:一帧 18bitC1-C4 位选 + 6 位保留 + Q2-Q9 段码),CLK 上升沿采样
* DATADATA 只在 CLK=低时改变);发完 CLK=高 时 DATA 上升沿=锁存,
* CLK=低 时 DATA 上升沿=输出刷新。
******************************************************************************/
#ifndef __BSP_CT1642_H__
#define __BSP_CT1642_H__
#include <stdint.h>
/**
* @brief 初始化 PG0/PG1 为推挽输出(CLK/DATA 初始 0
* @note app_disp_init() 内调用;被 BOARD_DISP_ENABLE 编译开关保护
* @return 无
*/
void bsp_ct1642_init(void);
/**
* @brief 7 段数码管数字转段码(共阴,A=bit0 ... G=bit6DP=bit7
* @param digit 0~910='-'G 段);11=全灭
* @return 段码
*/
uint8_t bsp_ct1642_seg(uint8_t digit);
/**
* @brief 发一帧刷新一位:18bitC1-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__ */
+225
View File
@@ -0,0 +1,225 @@
/******************************************************************************
* @file bsp_hw.c
* @brief CBR3 板级 GPIO / MCPWMLED、按键、风扇
* @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~100000%=关 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);
}
+61
View File
@@ -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 <stdint.h>
#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~100000%=关 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__ */
+14 -14
View File
@@ -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大端,值=℃×10Byte0=使能;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": {}
},
{