Files
stc32g128k/work-order/wo-04
edisondeng 4a89aef71e fix(rf): true single-edge P3.6 trigger + pull-down, restore edge decode
Root cause found via datasheet: PxIM0/PxIM1 has no dual-edge mode, only
falling/rising/low-level/high-level. (IM1=1,IM0=1) was actually
high-level interrupt, refiring continuously for the whole high-pulse
duration (confirmed by scope: no real HF noise, and edge_count=0 when
P3.6 grounded). Fix: start in rising-edge mode, software-toggle to the
opposite edge on every trigger (ping-pong) to emulate true dual-edge
triggering, feeding exact edge direction into the incremental decode
state machine (no glitch filter needed). Also switch P3.6 idle bias
from pull-up to pull-down, since floating-high under the old high-level
mode caused the interrupt-storm boot issue; removed the now-conflicting
pull-up restore in Wakeup_Restore().
2026-07-31 16:39:06 +08:00

223 lines
26 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 📋 工单 wo-04: 多国语言支持(法/英)与 RF 接收中断化改造
**工单编号**: WO-04
**创建时间**: 2026-07-31
**模块组件**: 菜单/UI (app_menu.c, ui.c) / 数据持久化 (database.c) / 射频驱动 (rf.c) / 定时器 (Timer0)
**优先级**: 中
**状态**: 进行中(阶段 1 已实机验证通过;阶段 2 代码已完成,待编译烧录验证)
---
## 零、阶段 2 开发过程中顺带发现并修复的既有 bug
在设计 RF 中断化的看门狗超时判断(需要用 `ms_tick` 判断是否卡在半成品帧状态太久)时,发现 **`ms_tick` 全局变量从未被递增过**,永远等于 0
* `App/main.c` 声明 `volatile u16 ms_tick = 0;`,全项目除此之外没有任何地方对它做 `++`。
* 用 `git log -S "ms_tick++"` 定位到 commit `c0d1fe7`(App 框架重构):重构前 `Timer1_Isr` 里确实有 `ms_tick++;` 及配套的 1000 归零逻辑,重构时把时钟相关代码抽到 `Clock_IncMS()`(用它自己的私有计数器 `ms_cnt`),误删了 `ms_tick++` 这一行,此后再未补回。
* 影响范围广:`app_menu.c` 呼吸灯与 IPC 绑定帧重发、`app_pair.c` 对码等待动画与 30 秒超时、`app_alarm.c`/`app_sos.c` 灯光闪烁与马达节拍、`app_clock.c` 时间设置光标闪烁、`event.c` 报警红框闪烁,全部依赖 `ms_tick` 计时,此前实际上都是冻结/不生效的。
* **修复**:在 `App/event.c` 的 `Timer1_Isr` 中补回 `ms_tick++;`(0~999 循环,与原逻辑一致)。已完成,不在此工单原定范围内,但顺带修复。
---
## 一、需求概述
本工单包含两项互相独立的改造,按顺序分阶段实施,每阶段单独提交、单独验证:
1. **阶段 1 — 多国语言支持**:菜单与界面文案支持法语(FR)/英语(EN)切换,断电重启后保留上次选择。
2. **阶段 2 — RF 接收中断化**:把现在阻塞轮询的 `EV1527_Decode()` 改造成外部中断驱动 + Timer0 时间戳测量,消除主循环阻塞。
---
## 二、阶段 1: 多国语言支持(法/英)
### 1.1 背景与结论
- 目标语言为法语 + 英语,均为纯 ASCII 字符,现有 `FONT_8x16` 字库已完全覆盖,**不需要新增任何字模资源**。
- 参考了 `100_smart_boat` 项目 `xtell_remote_control/language` 的 X-Macro 字符串表模式,去掉了其中中文字库相关的部分(本工单不涉及中文)。
### 1.2 设计要点(已与用户确认)
| 决策项 | 结论 |
|---|---|
| 语言选择是否持久化 | 需要。断电重启后恢复上次选择 |
| 持久化存储位置 | **不单开新 Flash 扇区**,追加存入 `sensor_list` 所在的同一扇区(该扇区 512 字节仅用了 352 字节,剩余空间足够),复用现有 `Load_Database`/`Save_Database` 读写流程 |
| 菜单位置与顺序 | 语言切换项放在**主菜单第 1 项**,原有 3 项("HORLOGE 时间/APPAIRAGE 对码/LIAISON 绑定")依次后移一位 |
| 交互方式 | **不做二级选择子菜单**。菜单高亮在语言项时,直接按■(确认键)在 FR/EN 间原地切换并立即保存,同时用新语言重绘当前菜单 |
### 1.3 实现内容
1. 新增 `App/language.h` + `App/language.c`:
- `typedef enum { LANG_FR = 0, LANG_EN, LANG_COUNT } Language;`
- `void Lang_Init(Language lang);` / `void Lang_Set(Language lang);` / `Language Lang_Current(void);`
- `const char* Lang_Get(StringID id);`
2. 新增 `App/strings.h`:用 X-Macro (`WRISTBAND_STRING_LIST`)登记 `ui.c` 中全部界面文案、`database.c` 的 `sensor_names_fr[5]`、`app_menu.c` 的 `menu_items[5]`,每条包含 FR/EN 两列译文。
3. `App/ui.c`:所有 `LCD_ShowStringCentered(y, "字面量", ...)` / `LCD_ShowString(...)` 改为经 `Lang_Get(STR_XXX)` 取值。
4. `App/app_menu.c`:
- 主菜单项数从 3 改为 4,`menu_select` 上限从 2 改为 3。
- 序号重排:0-语言切换,1-HORLOGE,2-APPAIRAGE,3-LIAISON。
- `menu_level == 0` 分支中,`menu_select == 0` 时按 CONFIRM_CLICK 直接切换语言 + 保存 + 重绘,不进入新的 `menu_level`。
5. `App/ui.c` 的 `UI_ShowMainMenu()`:新增语言项对应的图标与文案(法语环境显示 "LANGUE",英语环境显示 "LANGUAGE"),原有 3 个图标绘制的 `selected_index` 判断值整体 +1。
6. `App/database.c`:
- `Load_Database()`/`Save_Database()` 扩展,在 `sensor_list` 数组之后追加 1 字节语言标志的读写(同一扇区、同一次擦写周期)。
- 开机时 `Load_Database()` 读到的语言字节传给 `Lang_Init()`。
### 1.4 验收标准
- [ ] 开机默认语言为法语(或 Flash 中已保存的上次选择)。
- [ ] 主菜单第 1 项显示"语言"选项,上下键可以正常移动到该项(不影响其余 3 项的原有功能)。
- [ ] 在语言项上按确认键,菜单文案立即从法语切换为英语(或反之),无需退出重进菜单。
- [ ] 切换语言后,退出菜单进入其余各页面(时钟页、对码流程、报警页、SOS页)文案均正确跟随当前语言显示。
- [ ] 切换语言后断电重启,开机后菜单语言与断电前一致。
- [ ] 现有传感器数据库(已绑定的传感器列表)在语言切换与断电重启后不受影响、不丢失。
---
## 三、阶段 2: RF 接收中断化改造
### 2.1 背景
- 现状:`EV1527_Decode()`([Drivers/rf.c](Drivers/rf.c))为阻塞轮询实现,同步头捕获阶段最坏情况阻塞达 30ms,由后台 `RfMonitorApp_onRun()` 在主循环中无条件调用,可能干扰按键长按计时的准确性(按键计数按"调用次数"而非"真实时间"累加)。
- 确认结论:STC32G12K128(至少本项目 `stc32g.h` 覆盖的寄存器范围)**没有硬件捕获(CAP/PCA)寄存器**,只能用"外部中断 + 自由运行定时器时间戳"的软件方案实现等效功能。
### 2.2 设计要点
| 决策项 | 结论 |
|---|---|
| 边沿检测 | 复用现有 P3.6(`RF_RX_DATA`)端口中断(`interrupt 16`),从"只在休眠时开启"改为"常态双边沿触发常驻开启" |
| 时间戳来源 | 复用现有 Timer0(已用于 `RF_Delay_us`/`GetPulseDuration`),精度 0.5us/tick(24MHz 晶振 12T 模式下),来自晶振本身,精度足够,不需要改 1T 模式 |
| 计时范围 | Timer0 为 16 位,0.5us/tick 约 32.768ms 溢出一次,而同步头低电平上限要求测到 60ms —— 需要加 Timer0 溢出中断,把 16 位硬件计数扩展为软件维护的更宽时间戳,避免溢出截断 |
| 解码逻辑归属 | 沿用现有的同步头判定 + 24 位比值解调算法,只是从"主循环阻塞轮询"搬到"中断内增量式状态机推进";中断中不直接操作事件队列,只写标志位/结果变量,由主循环取用后生成 `SystemEvent` |
### 2.3 顺带修复
- `GetPulseDuration()`([Drivers/rf.c:160-172](Drivers/rf.c#L160-L172))中 `timeout_ticks` 用 `u16` 存储,60ms 超时换算成 tick(120000)超出 `u16` 上限被截断为约 27.2ms,与注释/调用方预期不符。本次改造一并修正(改用能容纳完整量程的类型)。
### 2.4 实现内容
1. Timer0 改为开机后常驻自由运行(不再是 `RF_Delay_us`/`GetPulseDuration` 里那种每次启停复位的一次性用法),新增 Timer0 溢出中断以扩展时间戳位宽。
2. P3.6 端口中断改为常态开启双边沿触发;`Port3_Isr` 内根据系统是否处于休眠切换态,分别处理"仅唤醒"和"正常解码"两种职责。
3. 中断内维护一个小状态机(等待同步头 / 累计第几位 / 暂存 addr 和 data),每次边沿中断推进一步,solved 24 位后把结果写入类似 `p0_wakeup_flag` 模式的标志变量。
4. `RfMonitorApp_onRun()` 及 `PairApp_onRun()` 改为检查该标志变量,而不是直接调用阻塞的 `EV1527_Decode()`。
5. 增加"半成品帧超时看门狗":同步头之后若在预期时间内未凑够 24 位,自动放弃并复位状态机,防止被干扰卡死。
### 2.5 验收标准
- [ ] 主循环轮询 RF 不再有阻塞现象(可用示波器/计时对比主循环单次迭代耗时)。
- [ ] 对码(`PairApp`)、后台报警监听(`RfMonitorApp`)功能行为与改造前一致,能正确捕获/解码射频信号。
- [ ] 按键长按计时(3 秒判定)在有射频信号持续输入时不再受干扰、计时准确。
- [ ] 休眠状态下 P3.6 收到射频信号仍能正常唤醒,唤醒功能不受本次改造影响。
- [ ] 干扰噪声下(无有效信号)状态机不会卡死在"半成品帧"状态。
---
## 四、代码修改记录
| 阶段 | 文件 | 修改内容 | 状态 |
|---|---|---|---|
| 1 | App/language.h / App/language.c | 新建,语言状态管理与取字符串接口(`Lang_Init/Lang_Set/Lang_Current/Lang_Get`) | ✔ 已完成 |
| 1 | App/strings.h | 新建,`WRISTBAND_STRING_LIST` X-Macro 字符串表(法/英) | ✔ 已完成 |
| 1 | App/config.h | 移除旧的 `sensor_names_fr` extern 声明 | ✔ 已完成 |
| 1 | App/database.h / App/database.c | 新增 `sensor_type_name_ids[5]` 字符串 ID 表;`Load_Database`/`Save_Database` 追加读写语言字节(与 `sensor_list` 共用同一扇区,偏移地址 `sizeof(sensor_list)`);`Add_Sensor_With_Zone` 改用 `Lang_Get()` 生成名称 | ✔ 已完成 |
| 1 | App/app_menu.c | 主菜单改 4 项并重排序号(0-语言/1-HORLOGE/2-APPAIRAGE/3-LIAISON);语言项按确认键原地切换 FR/EN 并调用 `Save_Database()` 持久化;`menu_items` 改名并改造为 `device_menu_ids` 字符串 ID 表 | ✔ 已完成 |
| 1 | App/ui.c | 全部界面文案字面量替换为 `Lang_Get(STR_XXX)`;`UI_ShowMainMenu` 新增第 0 项语言图标(直接显示当前语言 "FR"/"EN" 缩写) | ✔ 已完成 |
| 1 | wristband.uvproj | 新增 `App/language.c` 参与 Keil 工程编译 | ✔ 已完成 |
| 2 | Drivers/rf.c | 新增 Timer0 常驻自由运行 + 溢出中断 (`Timer0_Isr`) 扩展为 32 位时间戳;新增 `RF_HandleEdgeInterrupt()` 边沿增量解码状态机;`RF_Delay_us` 改为对运行中 Timer0 的快照式等待 (不再启停复位)`EV1527_Decode()` 改为非阻塞检查中断解码结果 (签名不变);移除死代码 `mock_rf_*` 与旧版 `GetPulseDuration` | ✔ 已完成 |
| 2 | Drivers/rf.h | 移除 `GetPulseDuration` 声明,新增 `RF_HandleEdgeInterrupt` 声明,更新 `EV1527_Decode` 注释 | ✔ 已完成 |
| 2 | App/main.c | `Port3_Isr` 去掉旧版"触发一次即自禁用 P3INTE"的逻辑,改为每次都调用 `RF_HandleEdgeInterrupt()`,身兼休眠唤醒与常态解码两职 | ✔ 已完成 |
| 2 | App/system.c | `Enter_Low_Power_Sleep`P3.6 双边沿触发与中断允许改为常态 (在 `RF_Init` 一次性配置),此处只保留重新武装 `P3WKUE`;新增休眠前禁用 `ET0`。`Wakeup_Restore``P3INTE` 唤醒后保留 P3.6 位 (不再清零),新增唤醒后恢复 `ET0` | ✔ 已完成 |
| 2 | App/app_rf_monitor.c / App/app_pair.c | 无需改动 —— `EV1527_Decode()` 函数签名与调用方式保持不变 | 不涉及 |
---
## 五之一、后续结构调整:新增 Drivers/timer.c 统一存放定时器驱动代码
用户指出 `Timer0_Isr`/`Timer1_Isr` 属于底层驱动代码,不应该分散在 `Drivers/rf.c` 和 `App/event.c` 里,要求集中放到 `Drivers/timer.c`。据此做了以下调整(纯代码搬家,行为不变)
* 新建 `Drivers/timer.h` / `Drivers/timer.c`,收纳:
- `Timer0_Init()` / `Timer0_Isr()` / `Timer0_GetTimestamp()`(原来内联在 `RF_Init()` 里 + `rf.c` 的 `Timer0_Isr`/`RF_GetTimestamp`)
- `Timer1_Init()` / `Timer1_Isr()` / `key_scan_flag` 的定义(原来在 `App/event.c`)
* `Drivers/rf.c``RF_Init()` 改为调用 `Timer0_Init()``RF_Delay_us`/`RF_HandleEdgeInterrupt` 里的 `RF_GetTimestamp()` 改调用 `Timer0_GetTimestamp()`。
* `App/event.c`:删除 `Timer1_Init`/`Timer1_Isr`/`key_scan_flag` 定义与不再需要的 `clock.h` 引入;`Event_KeyScan_Poll` 等其余逻辑不变。
* `App/event.h`:移除 `Timer1_Init` 声明(改由 `Drivers/timer.h` 提供)`key_scan_flag` 的 extern 声明保留(定义位置变了,对外接口不变)。
* `App/main.c`:新增 `#include "../Drivers/timer.h"` 以取得 `Timer1_Init` 声明。
* `wristband.uvproj`:新增 `Drivers/timer.c` 参与编译。
## 五、阶段 2 实现备注
* Timer0 改为开机后 (`RF_Init()`) 启动一次、永不停止的自由运行计数器,配合 `Timer0_Isr` 溢出中断把 16 位硬件计数扩展为 32 位软件时间戳 (`RF_GetTimestamp()`),解决了原 `GetPulseDuration()` 里 `u16 timeout_ticks` 存不下 60ms 对应的 120000 tick、被截断成约 27.2ms 的既有 bug。
* `EV1527_TxFrame()` 用到的 `RF_Delay_us()` 原本会启停复位 Timer0与"常驻自由运行"直接冲突 (会导致发射一次后 RX 时间戳永久停摆),已改为对运行中的 Timer0 做起止快照差值等待,纯只读,不影响其自由运行状态。
* `EV1527_Transmit()` 整个发射过程是 `EA=0` 屏蔽全局中断执行的 (原有设计,未改动),期间 Timer0 硬件仍在计数但溢出中断进不来,`rf_timer_ovf_count` 会短暂漏计;发射结束后的第一条 RX 边沿会因为时间差异常大而被判定超出合法区间、直接丢弃,随后自动恢复正常——这是状态机自愈的正常代价,不需要额外处理。开机阶段 `Self_Test()`/`Load_Database()` 耗时较长同理,也只会导致开机后第一条边沿被丢弃一次。
* 看门狗超时阈值取 200ms (`RF_DECODE_TIMEOUT_MS`),明显大于一帧最坏情况的理论总耗时 (~95ms同步头最长 61.5ms + 24 位数据最长约 33.6ms),避免正常长帧被误杀。
* P3.6 双边沿中断现在身兼两职:休眠期间做唤醒源 (`p3_wakeup_flag`),正常运行期间驱动 `RF_HandleEdgeInterrupt()` 解码;`Enter_Low_Power_Sleep`/`Wakeup_Restore` 中对应调整为"不再清零 P3.6 的中断允许位",避免唤醒后 RX 解码功能失效。
* 尚未实机编译烧录验证,第三节验收标准仍需在真实硬件上逐项确认 (尤其是干扰环境下的解码成功率、休眠唤醒功能是否受影响)。
### 2.7 实机验证发现的真正根因Port3 中断向量重定向缺失 + isr_jump.asm 从未参与编译
在 2.6 节的排查过程中,通过逐步二分法(先屏蔽 `RF_HandleEdgeInterrupt` 函数体、再屏蔽 `P3INTE`)最终定位到**真正的病根**2.6 节"P3.6 悬空"的诊断被推翻,记录如下供后续参考:
* `Drivers/stc32g.h` 里的官方向量表显示P3 端口中断的真实硬件向量号是 **40**(地址 `0x000143``P3INT_VECTOR`),而 [App/main.c](App/main.c) 里 `Port3_Isr` 声明的是 `interrupt 16`,对应的其实是向量 16 号 `INT4_VECTOR`(地址 `0x000083`"外部中断 4",与 P3 端口毫无关系)。
* 向量号 40 超出 Keil C251 `interrupt` 关键字原生支持的 0~31 范围,跟 Port0(37 号)、Port2(39 号)一样,必须靠 [App/isr_jump.asm](App/isr_jump.asm) 里的汇编跳转表把硬件真实向量地址重定向到 C 代码能声明的低位向量——但这个文件里**只有 Port0、Port2 的重定向Port3 这一条从建立以来就一直缺失**。
* 更严重的是,进一步检查发现 **`isr_jump.asm` 这个文件本身从未被加入 `wristband.uvproj` 工程**,也就是说不止 Port3Port0/Port2 的重定向指令这么多年也从未被真正汇编链接进过固件——这是这份代码库里一直存在、此前从未暴露的问题。
* 之所以此前测试(尤其是低功耗休眠唤醒功能)没有暴露这个问题,推测是因为 P0/P2/P3 的中断使能窗口一直很短(仅在睡眠前临时打开、触发一次就自行关闭),硬件的"从掉电模式被中断请求唤醒"这个动作可能不需要真正完成向量跳转就能生效,狭窄的时间窗口侥幸没有触发跳转到未写入指令的地址。本次工单把 P3.6 的中断改为开机后永久常开、且芯片持续真实接收产生大量边沿,第一次让这个跳转被稳定触发,问题才暴露出来。
* **修复**(两处缺一不可)
1. 在 `App/isr_jump.asm` 里补上 Port3 的重定向条目:`CSEG AT 000143H` → `LJMP 000083H`(跳转到 `interrupt 16` 即 `Port3_Isr` 的入口)。
2. 把 `App/isr_jump.asm` 加入 `wristband.uvproj` 工程文件(`FileType=2`,汇编源文件),确保它真正参与编译链接。
* 修复后2.6 节里为排查而加的调试代码(`RF_HandleEdgeInterrupt` 开头的 `return;`、注释掉的 `P3INTE |= 0x40;`)均已撤销,恢复正常逻辑。
### 2.6 实机验证过程记录P3.6 悬空猜想(已被 2.7 节推翻,仅作排查过程存档)
用户实测反馈:改动后开机背光闪一下就灭,按住开机键不放背光反复闪烁。排查确认根因:
* LR690L 芯片在 `SHUT=1`(休眠/发射时也会关断接收,见 `RF_SetMode(0)`/`RF_SetMode(2)`)期间,数据脚 (P3.6) 为高阻输出。原来的 P3.6 配置是纯高阻输入、**没有内部上拉**。
* 开机时射频芯片默认关闭 (`RF_Init()` 里 `SHUT=1`),要到 `AppManager_StartApp(APP_ID_CLOCK)` 里才会调用 `RF_SetMode(1)` 真正打开接收——这段时间 P3.6 处于悬空状态,容易受旁边射频电路干扰而电平乱跳。
* 旧版设计里 P3.6 中断只在临睡前临时开启且触发一次自动关闭,悬空乱跳最多误触发一次,代价很小;本次改造把它变成开机后永久常开、不再自动关闭,悬空噪声在 `EA=1` 之后会引发中断风暴CPU 完全无法执行主循环,导致开机画面卡在初始化那一下就再也画不出时钟界面。
* **修复**:给 P3.6 加内部上拉 (`P3PU |= 0x40;`),在悬空 (芯片输出高阻) 时把电平钳在稳定的高电平,不再乱跳;芯片主动驱动数据时上拉阻值足够弱,不影响正常接收。
- `Drivers/rf.c` 的 `RF_Init()` 中新增。
- `App/system.c` 的 `Wakeup_Restore()` 中新增恢复 (因为 `Enter_Low_Power_Sleep()` 休眠前为了省电特意把这个上拉关掉了,唤醒后要重新打开)。
### 2.8 对照实验:轮询解码 + 中断纯计数,验证毛刺/降噪分析是否成立
背景:中断增量解码状态机(2.2 节设计)接入真机后,`sync_seen` 长期为 0 或解码一直失败;调整"毛刺过滤下限" (`RF_GLITCH_MIN_TICKS`) 从 10us 逐步提高到 100us 的过程中,`ss_dur_max`/`ss_high_max` 等诊断量时而逼近理论值、时而超出合法区间,未能收敛到稳定结论。用户明确要求停止继续盲目调参,并提出一个可以直接证伪/证实"毛刺导致解码状态机误判"这一假设的对照实验:
* 解码逻辑临时改回 commit `d8df5d4` 已验证长期稳定工作的阻塞轮询实现 (`GetPulseDuration()` + `EV1527_Decode()`,算法/阈值原样保留)。
* P3.6 边沿中断 (`RF_HandleEdgeInterrupt()`) 同时保持使能,但只做纯计数 (`dbg_isr_edge_count++`),不参与任何解码判断。
* 轮询解码一旦成功,立即打印本次解码尝试开始前、成功那一刻的中断边沿计数快照及差值 (`[RF-ISR-CHECK] edges_before=.. edges_after=.. delta=..`),与 EV1527 协议一帧的理论边沿数 (同步头 2 个 + 24 位数据每位 2 个 = 50) 比对。
* 若 `delta` 稳定等于 50 (或在合理误差范围内):说明真实硬件产生的边沿是干净、可信的,此前中断状态机解码失败的原因不在"信号本身有毛刺",而在状态机代码逻辑本身;应转向复查状态机实现而非继续调整过滤阈值。
* 若 `delta` 明显偏离 50 (尤其是远大于 50):则支持此前"存在真实高频毛刺/回响,被边沿中断如实捕获,但被轮询的每次循环开销意外滤除"的分析,说明中断驱动方案需要额外做硬件或软件去抖,才能达到轮询版本的可靠性。
改动仅限 `Drivers/rf.c`(`Drivers/rf.h` 同步更新函数注释)
* `GetPulseDuration()`:算法/阈值与原始版本一致,但测量方式改为对 `Drivers/timer.c` 提供的自由运行 `Timer0_GetTimestamp()` 做快照差值,而不是原始版本那样直接停表/清零/重启 `TR0`——因为现在 Timer0 已经是 `RF_Delay_us()`(TX 发射计时) 共用的自由运行时间戳源,若在这里停表清零会导致解码尝试之后的 TX 发射永远等不到时间戳前进而卡死。这是相对原始实现唯一的必要改动,不影响被验证的解码算法本身。
* `EV1527_Decode()`:算法与原始版本一致,新增开始前/成功后的边沿计数快照打印。
* `RF_HandleEdgeInterrupt()`:从三态状态机简化为纯计数器 (`dbg_isr_edge_count++`)。
* `RF_PrintDebugCounters()`:简化为只打印 `isr_edge_count`。
* 本实验阶段之前为诊断中断状态机新增的大量 `dbg_*` 计数器/`RF_CheckDecodeWatchdog`/状态机相关代码已随之整体移除;若后续证实需要回到中断驱动方案,需要从 git 历史重新引入或重新设计,不建议直接依赖本次删除前的调参结果。
状态:代码已完成,待实机烧录,触发一次真实发射并观察 `[RF-ISR-CHECK]` 日志的 `delta` 是否稳定等于 50。
**实测结果**:连续 4 次独立发射,`delta` 稳定在 6963~7043 之间(约理论值 50 的 140 倍,四次误差仅约 1%),远超"偶发毛刺"能解释的量级,且波动极小,不像随机噪声,更像由代码本身某个确定性行为决定。用户随后用示波器直接实测 P3.6 波形,**确认没有对应的高频噪声/振铃**,彻底排除了 2.6/2.7 节以来"信号有毛刺、需要软件去抖"的整个分析方向。
### 2.9 真正根因定位:`PxIM0/PxIM1` 没有"双边沿触发"模式,`(1,1)` 实际是"高电平中断"
在排除毛刺假设后,用户质疑"会不会中断接错了引脚"。核对 `App/isr_jump.asm`/`P3IM0`/`P3IM1`/`P3INTE` 全仓库唯一赋值点、原理图 Pin19=P3.6=RF_RX_DATA 网络标签确认引脚本身没有接错。随后查阅《STC32G 系列技术手册》"15.1.3 端口中断模式配置寄存器 (PxIM0PxIM1)"真值表,发现关键事实:
| PnIM1.x | PnIM0.x | 中断模式 | 掉电唤醒支持 |
|---|---|---|---|
| 0 | 0 | 下降沿中断 | 支持 |
| 0 | 1 | 上升沿中断 | 支持 |
| 1 | 0 | 低电平中断 | 不支持 |
| 1 | 1 | 高电平中断 | 不支持 |
这颗芯片的端口中断硬件**只有下降沿/上升沿/低电平/高电平四选一,根本没有"双边沿都触发"这个模式**。而 `RF_Init()` 里一直写的是 `P3IM1|=0x40; P3IM0|=0x40;`——对照表格,这实际配置的是**高电平中断**,跟代码注释"双边沿触发"完全对不上:只要 P3.6 处于高电平,中断就会在整段高电平期间被硬件持续、大量地重新触发,这才是边沿计数远超协议理论值(50)、且四次测量高度稳定(由代码执行速度决定,而非随机噪声)的真正原因从一开始就与信号质量无关。用户随后做了接地实验直接验证P3.6 直接短接到地,`isr_edge_count` 恒为 0(满足不了"高电平"这个触发条件);正常收码时该值持续变化——与"高电平中断"模型完全吻合。
同时发现一个关联问题:`P3.6` 还兼职做掉电唤醒源,而上表显示"高电平中断"**不支持掉电唤醒**,说明之前 `P3.6` 唤醒功能大概率也没有真正按预期工作。
### 2.10 修复:软件乒乓单边沿切换模拟双边沿触发 + P3.6 内部下拉
由于硬件不提供真正的双边沿模式,改为标准的软件模拟方案:`RF_Init()` 里初始配置成单一边沿(上升沿)触发;`RF_HandleEdgeInterrupt()` 每次触发后,先根据触发前的 `P3IM0` 值判断刚刚发生的是上升沿还是下降沿(乒乓切换保证了高/低电平段严格交替出现,不需要再读引脚电平猜测方向),再立即把 `P3IM0` 翻转到相反边沿,为下一次真实翻转做准备。这样每次真实电平翻转都会精确触发一次中断,不多不少,不再需要任何毛刺过滤器。
顺带修正一个关联的悬空钳位问题:`P3.6` 内部上拉 (`P3PU`) 改为内部下拉 (`P3PD`),在 `RF_Init()` 里一次性永久开启——`SHUT=1`(芯片关断)时 `DATA` 脚高阻悬空,配合"高电平中断"误配置,之前的上拉方案正好会把悬空态钳在会触发中断的那一侧,是当年开机中断风暴/背光闪烁问题的更深层成因;改用下拉后悬空态落在"不触发"一侧。同时清理了 `App/system.c` 里 `Wakeup_Restore()` 遗留的、每次唤醒都重新打开 P3.6 上拉的代码——留着会导致上拉和下拉同时使能、相互打架,现已移除(下拉在 `RF_Init()` 一次性配置好Power Down 期间 SFR 状态不丢失,休眠/唤醒不需要重新配置)。
改动范围:`Drivers/rf.c`(重写 `RF_Init` 引脚上下拉与初始边沿配置、重写 `RF_HandleEdgeInterrupt` 为乒乓单边沿版本、恢复三态增量解码状态机 `RF_HandleHighSegmentEnd`/`RF_HandleLowSegmentEnd`、`RF_CheckDecodeWatchdog`、非阻塞 `EV1527_Decode()`,移除本次调试实验用的 `GetPulseDuration`/阻塞轮询版 `EV1527_Decode`)、`Drivers/rf.h`(同步更新函数注释)、`App/main.c`(`Port3_Isr` 注释同步)、`App/system.c`(`Wakeup_Restore()` 移除上拉恢复)。
状态:代码已完成,待实机烧录验证——重点关注解码是否能稳定成功,以及 `RF_PrintDebugCounters()` 里 `aborts` 计数是否合理(偶发因真实干扰打断可以理解,持续大量 abort 则说明状态机或阈值仍有问题)。
## 六、阶段 1 实现备注
* 语言字节与传感器数据库共用同一 Flash 扇区(偏移地址为 `sizeof(sensor_list)`,当前为 352,扇区剩余空间足够),复用现有 `Load_Database`/`Save_Database`,未新增独立扇区。
* 未做二级选择子菜单:主菜单第 1 项选中"语言"时直接按■原地切换 FR/EN,立即保存并用新语言重绘当前菜单。
* 首次开机(Flash 从未写入过语言字节,读到擦除态 `0xFF`)时 `Lang_Init` 会自动回退为 `LANG_FR`,不会出现非法枚举值。
* 本阶段仅替换界面文案与新增语言状态管理,未改动任何按键状态机、事件队列或射频协议相关代码逻辑。
* 尚未实机编译烧录验证,第四节验收标准仍需在真实硬件上逐项确认。