diff --git a/App/app_menu.c b/App/app_menu.c
index 0f97e61..43b1289 100644
--- a/App/app_menu.c
+++ b/App/app_menu.c
@@ -4,15 +4,17 @@
#include "rgb.h"
#include "event.h"
#include "system.h"
+#include "database.h"
+#include "language.h"
#include "../Drivers/rf.h"
-// 二级传感器对码菜单中,各类传感器的法语显示名称定义
-char *code menu_items[5] = {
- "DETEC. PORTE", // 0: 门磁探测器 (法语: 门)
- "DETEC. PIR", // 1: 红外探测器
- "DETEC. FUMEE", // 2: 烟雾传感器
- "DETEC. GAZ", // 3: 燃气探测器
- "DETEC. EAU" // 4: 水浸探测器
+// 二级传感器对码菜单中,各类传感器显示名称的字符串 ID,实际文案由 Lang_Get() 按当前语言取出
+const StringID device_menu_ids[5] = {
+ STR_DEV_PORTE, // 0: 门磁探测器
+ STR_DEV_PIR, // 1: 红外探测器
+ STR_DEV_FUMEE, // 2: 烟雾传感器
+ STR_DEV_GAZ, // 3: 燃气探测器
+ STR_DEV_EAU // 4: 水浸探测器
};
// 外部全局变量引用
@@ -100,7 +102,7 @@ static EventResult MenuApp_onEvent(SystemEvent *evt)
{
if (menu_level == 0)
{
- // ====== 处于一级主菜单层 (0-HORLOGE, 1-APPAIRAGE, 2-LIAISON) ======
+ // ====== 处于一级主菜单层 (0-语言切换, 1-HORLOGE, 2-APPAIRAGE, 3-LIAISON) ======
if (evt->key_event == KEY_EVENT_UP_CLICK) {
// 短按 ▲ 键向上移动焦点
if (menu_select > 0) {
@@ -111,7 +113,7 @@ static EventResult MenuApp_onEvent(SystemEvent *evt)
}
if (evt->key_event == KEY_EVENT_DOWN_CLICK) {
// 短按 ▼ 键向下移动焦点
- if (menu_select < 2) {
+ if (menu_select < 3) {
menu_select++;
UI_ShowMainMenu(menu_select);
}
@@ -120,15 +122,22 @@ static EventResult MenuApp_onEvent(SystemEvent *evt)
if (evt->key_event == KEY_EVENT_CONFIRM_CLICK) {
// 短按 ■ 确认键,进入选定功能的子页面
if (menu_select == 0) {
+ // 功能 0:语言切换。原地在 FR/EN 间切换,立即保存并用新语言重绘当前菜单
+ Language new_lang = (Lang_Current() == LANG_FR) ? LANG_EN : LANG_FR;
+ Lang_Set(new_lang);
+ Save_Database(); // 语言字节与传感器数据库共用同一扇区持久化
+ MOTOR = 1; Delay_ms(50); MOTOR = 0; // 短震反馈,与其它确认操作风格一致
+ UI_ShowMainMenu(menu_select);
+ } else if (menu_select == 1) {
// 功能 1:设置时间。启动待机时钟应用,并调用微调编辑入口
AppManager_SwitchToForeground(&clock_app);
Start_Time_Edit();
- } else if (menu_select == 1) {
+ } else if (menu_select == 2) {
// 功能 2:传感器学习。进入二级传感器类别选择菜单
menu_level = 1;
menu_select = 0;
UI_ShowPairMenuPage(menu_select); // 绘制二级设备分类页
- } else if (menu_select == 2) {
+ } else if (menu_select == 3) {
// 功能 3:相机配对。进入摄像机一键绑定发射状态
menu_level = 2;
last_bind_tx_ms = ms_tick; // 记录初始发射时间
@@ -174,7 +183,7 @@ static EventResult MenuApp_onEvent(SystemEvent *evt)
if (evt->key_event == KEY_EVENT_DOWN_LONG) {
// 长按 ▼ 键,从二级菜单退回到一级主菜单
menu_level = 0;
- menu_select = 1; // 回到 APPAIRAGE 选项上
+ menu_select = 2; // 回到 APPAIRAGE 选项上
UI_ShowMainMenu(menu_select);
return EVENT_HANDLED;
}
@@ -185,7 +194,7 @@ static EventResult MenuApp_onEvent(SystemEvent *evt)
// 长按 ■ 确认键 或 长按 ▼ 键,均表示取消绑定,退出发射并返回一级主菜单
if (evt->key_event == KEY_EVENT_CONFIRM_LONG || evt->key_event == KEY_EVENT_DOWN_LONG) {
menu_level = 0;
- menu_select = 2; // 回到 LIAISON 选项上
+ menu_select = 3; // 回到 LIAISON 选项上
current_state = STATE_PAIR_MENU;
RGB_Send(0, 0, 0, 0, 0, 0); // 关闭呼吸灯
UI_ShowMainMenu(menu_select);
diff --git a/App/config.h b/App/config.h
index 54fa3ba..5f1eb0c 100644
--- a/App/config.h
+++ b/App/config.h
@@ -146,7 +146,6 @@ extern u8 captured_type; // 正在配对的传感器类型
extern u8 alarm_sensor_slot; // 当前触发警报的传感器在数据库列表中的槽位索引
extern Sensor_Slot xdata sensor_list[16];// 传感器数据库数组 (存储在 xdata 区,共 16 个槽位)
-extern char *code sensor_names_fr[5]; // 法语传感器设备类型默认名称字串数组
// 系统滴答与定时计数器 (均为 1ms 中断服务中更新)
extern volatile u16 ms_tick; // 毫秒滴答计数器 (0~999循环)
diff --git a/App/database.c b/App/database.c
index 651c16e..bf99762 100644
--- a/App/database.c
+++ b/App/database.c
@@ -1,19 +1,23 @@
#include "database.h"
+#include "language.h"
// 内部 IAP 命令寄存器指令定义
#define IAP_CMD_READ 1 // IAP 字节读指令
#define IAP_CMD_WRITE 2 // IAP 字节写指令
#define IAP_CMD_ERASE 3 // IAP 扇区擦除指令
-// 定义法语传感器默认名称常量数组,分别对应类型 0~4
-char *code sensor_names_fr[5] = {
- "PORTE", // 0: 门磁 (法语: 门)
- "PIR", // 1: 红外探测器
- "FUMEE", // 2: 烟雾传感器 (法语: 烟雾)
- "GAZ", // 3: 燃气泄漏传感器 (法语: 燃气)
- "EAU" // 4: 漏水/水浸传感器 (法语: 水)
+// 传感器类型简称的字符串 ID,分别对应类型 0~4,实际文案由 Lang_Get() 按当前语言取出
+const StringID sensor_type_name_ids[5] = {
+ STR_NAME_PORTE, // 0: 门磁
+ STR_NAME_PIR, // 1: 红外探测器
+ STR_NAME_FUMEE, // 2: 烟雾传感器
+ STR_NAME_GAZ, // 3: 燃气泄漏传感器
+ STR_NAME_EAU // 4: 漏水/水浸传感器
};
+// 紧随 sensor_list 之后存放的 1 字节界面语言设置在 Flash 扇区内的偏移地址
+#define LANGUAGE_FLASH_ADDR ((u16)sizeof(sensor_list))
+
/**
* @brief 关闭 IAP 寄存器控制并清理地址
* @details 每次 IAP 闪存操作后调用,确保安全,防止意外指针写入损坏 EEPROM 中的关键配置数据。
@@ -87,7 +91,8 @@ void IAP_EraseSector(u16 addr)
/**
* @brief 从 EEPROM 的扇区 254 中加载数据库
- * @details 逐字节读取 Flash 物理数据,并直接通过指针拷贝装入 RAM 中的全局 `sensor_list` 数组。
+ * @details 逐字节读取 Flash 物理数据,并直接通过指针拷贝装入 RAM 中的全局 `sensor_list` 数组;
+ * 随后读取紧随其后的 1 字节界面语言设置,恢复上次保存的语言选择。
*/
void Load_Database(void)
{
@@ -95,15 +100,20 @@ void Load_Database(void)
u8 *ptr = (u8 *)&sensor_list; // 获取传感器 RAM 数组的首地址指针
u16 size = sizeof(sensor_list); // 获取结构体数组的全部占用尺寸
u16 i;
+ u8 lang_byte;
for (i = 0; i < size; i++) {
ptr[i] = IAP_ReadByte(addr++); // 循环读取字节并还原到 RAM
}
+
+ // 读取语言设置字节 (首次开机、从未保存过时 Flash 为擦除态 0xFF,Lang_Init 会自动回退为 LANG_FR)
+ lang_byte = IAP_ReadByte(LANGUAGE_FLASH_ADDR);
+ Lang_Init((Language)lang_byte);
}
/**
* @brief 将 RAM 中的最新传感器列表数据持久化保存到 EEPROM
* @details 因为 Flash 写操作不能把 0 写回 1,所以必须先调用 `IAP_EraseSector` 擦除整个扇区,
- * 接着遍历整个数据结构,逐字节写入最新的 RAM 内容。
+ * 接着遍历整个数据结构,逐字节写入最新的 RAM 内容,最后追加写入当前界面语言设置字节。
*/
void Save_Database(void)
{
@@ -115,6 +125,9 @@ void Save_Database(void)
for (i = 0; i < size; i++) {
IAP_WriteByte(addr++, ptr[i]); // 循环写入字节数据
}
+
+ // 紧随传感器数据之后追加写入当前界面语言设置
+ IAP_WriteByte(LANGUAGE_FLASH_ADDR, (u8)Lang_Current());
}
/**
@@ -155,11 +168,14 @@ void Add_Sensor_With_Zone(u32 addr, u8 type, u8 zone)
sensor_list[slot_to_use].type = type; // 记录传感器硬件类型
sensor_list[slot_to_use].zone = zone; // 记录对应的防区号
- // 生成该槽位传感器的法语名称
- for (i = 0; i < 15; i++) {
- // 从默认名称常量数组中复制对应名字的字符
- sensor_list[slot_to_use].name_gbk[i] = sensor_names_fr[type][i];
- if (sensor_names_fr[type][i] == '\0') break; // 遇零终止
+ // 生成该槽位传感器的名称 (取当前界面语言下的简称)
+ {
+ const char *type_name = Lang_Get(sensor_type_name_ids[type]);
+ for (i = 0; i < 15; i++) {
+ // 从当前语言名称字符串中复制对应名字的字符
+ sensor_list[slot_to_use].name_gbk[i] = type_name[i];
+ if (type_name[i] == '\0') break; // 遇零终止
+ }
}
sensor_list[slot_to_use].name_gbk[15] = '\0'; // 尾部安全截断,防止字符越界
diff --git a/App/database.h b/App/database.h
index 0b4a41e..17a9f40 100644
--- a/App/database.h
+++ b/App/database.h
@@ -2,6 +2,10 @@
#define __DATABASE_H__
#include "config.h"
+#include "strings.h"
+
+// 传感器类型简称的字符串 ID 表 (索引 0~4 对应门磁/PIR/烟感/燃气/水浸),供 UI 层按当前语言取用
+extern const StringID sensor_type_name_ids[5];
/* ====== 底层 IAP (在应用编程 Flash) 读写驱动接口 ====== */
@@ -39,13 +43,15 @@ void IAP_EraseSector(u16 addr);
/**
* @brief 从 Flash 扇区 254 (0xFE0000) 载入全部传感器槽位配置至 RAM 变量
- * @details 系统初始化时调用,将 EEPROM 中的传感器绑定数据拷贝到 RAM 中的 `sensor_list` 数组。
+ * @details 系统初始化时调用,将 EEPROM 中的传感器绑定数据拷贝到 RAM 中的 `sensor_list` 数组,
+ * 并读取紧随其后存放的 1 字节界面语言设置,调用 `Lang_Init()` 恢复上次选择的语言。
*/
void Load_Database(void);
/**
* @brief 将 RAM 中的全部传感器配置保存/写入到 Flash 扇区中
- * @details 在完成对码、添加新传感器或微调防区号后调用,该函数会先擦除第 254 扇区,然后逐字节写入当前最新的 `sensor_list` 数组。
+ * @details 在完成对码、添加新传感器、微调防区号或切换界面语言后调用,该函数会先擦除第 254 扇区,
+ * 然后逐字节写入当前最新的 `sensor_list` 数组,并在其后追加写入当前界面语言 (`Lang_Current()`)。
*/
void Save_Database(void);
diff --git a/App/event.c b/App/event.c
index 216a8a4..9310a9c 100644
--- a/App/event.c
+++ b/App/event.c
@@ -2,7 +2,6 @@
#include "app_manager.h"
#include "database.h"
#include "system.h"
-#include "clock.h"
#include "xtell.h"
#include "../Drivers/adc.h"
@@ -142,47 +141,6 @@ void Event_Dispatcher_Loop(void)
AppManager_DispatchEvent(evt);
}
-/**
- * @brief 定时器 1 初始化,设定 1ms 中断周期
- */
-void Timer1_Init(void)
-{
- AUXR |= 0x40; // 设定定时器 1 为 1T 模式 (无分频,主频直接输入)
- TMOD &= 0x0F; // 设置定时器 1 模式寄存器,保留低四位 (定时器0配置)
- {
- // 按照系统主频 (MAIN_Fosc) 计算 1ms 对应的重装载值
- u16 reload = (u16)(65536UL - (MAIN_Fosc / 1000UL));
- TL1 = (u8)reload; // 载入低 8 位
- TH1 = (u8)(reload >> 8);// 载入高 8 位
- }
- ET1 = 1; // 开启定时器 1 中断允许
- TR1 = 1; // 启动定时器 1 计数
- EA = 1; // 开启全局中断允许
-}
-
-// 全局按键扫描触发标志
-volatile bit key_scan_flag = 0;
-
-/**
- * @brief 定时器 1 中断服务程序 (每 1ms 触发一次)
- * @details 承担系统软时钟计算,并每 10ms 置位主循环按键扫描标志,不直接在中断中进行 ADC 多次采样。
- */
-void Timer1_Isr(void) interrupt 3
-{
- // 1ms 定时更新软时钟
- Clock_IncMS();
-
- // 报警屏幕红边框以 100ms 周期亮灭,进行颜色爆闪指示
- alarm_flash_flag = (ms_tick / 100) % 2;
-
- /* 物理按键定时计数 (每 10ms 置位一次标志位) */
- key_scan_timer++;
- if (key_scan_timer >= 10) {
- key_scan_timer = 0; // 重置扫描分频计数器
- key_scan_flag = 1; // 仅在中断里置起标志位,留待主循环中执行实际读取与消抖
- }
-}
-
/**
* @brief 主循环按键扫描与消抖处理函数
* @details 由主循环每 10ms 检查并执行,完全在主线程中运行,绝无中断重入冲突。
diff --git a/App/event.h b/App/event.h
index 645734d..8ae8923 100644
--- a/App/event.h
+++ b/App/event.h
@@ -53,7 +53,7 @@ bit EventQueue_IsFull(void);
u8 EventQueue_GetCount(void);
-/* ====== 事件分发环与定时器驱动初始化 ====== */
+/* ====== 事件分发环 ====== */
/**
* @brief 事件分发轮询主处理程序
@@ -62,13 +62,7 @@ u8 EventQueue_GetCount(void);
*/
void Event_Dispatcher_Loop(void);
-/**
- * @brief 初始化 Timer1 定时器,生成系统基准时钟 (1ms 中断)
- * @details 配置定时器 1 为 16 位自动重装载模式,设定中断周期为 1.0 毫秒,开启 Timer1 中断并启动计数。
- */
-void Timer1_Init(void);
-
-// 主循环按键扫描定时标志 (由 1ms 中断置 1,主循环响应清除)
+// 主循环按键扫描定时标志 (由 Drivers/timer.c 的 Timer1 1ms 中断置 1,主循环响应清除)
extern volatile bit key_scan_flag;
/**
diff --git a/App/language.c b/App/language.c
new file mode 100644
index 0000000..7f01c3f
--- /dev/null
+++ b/App/language.c
@@ -0,0 +1,48 @@
+#include "language.h"
+
+// 生成语言字符串数据表所用的临时结构体 (fr/en 两列)
+typedef struct {
+ char *fr;
+ char *en;
+} LangEntry;
+
+//////////////////////////////////////////////////////////
+// !!!!!! 不要动下面的代码 !!!!!!
+#ifdef DEFINE_STRING
+#undef DEFINE_STRING
+#endif
+#define DEFINE_STRING(name, fr, en) { fr, en },
+// !!!!!! 不要动上面的代码 !!!!!!
+//////////////////////////////////////////////////////////
+
+// 复用 strings.h 里的同一份登记表,展开成 {法语, 英语} 数据数组,存放于 Flash (code 区)
+LangEntry code language_pack[STR_COUNT] = {
+ WRISTBAND_STRING_LIST
+};
+
+static Language current_lang = LANG_FR;
+
+void Lang_Init(Language lang)
+{
+ current_lang = (lang < LANG_COUNT) ? lang : LANG_FR;
+}
+
+void Lang_Set(Language lang)
+{
+ if (lang < LANG_COUNT) {
+ current_lang = lang;
+ }
+}
+
+Language Lang_Current(void)
+{
+ return current_lang;
+}
+
+char *Lang_Get(StringID id)
+{
+ if (id >= STR_COUNT) {
+ return "";
+ }
+ return (current_lang == LANG_EN) ? language_pack[id].en : language_pack[id].fr;
+}
diff --git a/App/language.h b/App/language.h
new file mode 100644
index 0000000..9326e3d
--- /dev/null
+++ b/App/language.h
@@ -0,0 +1,41 @@
+#ifndef __LANGUAGE_H__
+#define __LANGUAGE_H__
+
+#include "config.h"
+#include "strings.h"
+
+/* ====== 界面语言选择 ====== */
+typedef enum {
+ LANG_FR = 0, // 法语 (默认)
+ LANG_EN, // 英语
+ LANG_COUNT
+} Language;
+
+/**
+ * @brief 语言模块初始化 (开机时由 Load_Database 读取 Flash 中保存的语言字节后调用)
+ * @param lang 要设为当前语言的枚举值。若超出 LANG_COUNT 范围 (如 Flash 未写过时的 0xFF),回退为 LANG_FR。
+ */
+void Lang_Init(Language lang);
+
+/**
+ * @brief 切换当前界面语言 (不负责持久化,调用方需自行决定是否写入 Flash)
+ * @param lang 目标语言
+ */
+void Lang_Set(Language lang);
+
+/**
+ * @brief 查询当前界面语言
+ * @return Language 当前语言枚举值
+ */
+Language Lang_Current(void);
+
+/**
+ * @brief 取指定字符串 ID 在当前语言下的文本
+ * @param id 字符串 ID (见 strings.h 中 WRISTBAND_STRING_LIST 登记表)
+ * @return char* 当前语言对应的文案指针;id 越界时返回空字符串
+ * @note 返回类型不加 const,是为了直接兼容 LCD_ShowString 系列接口的 char* 参数,
+ * 与本项目其余字符串接口 (Uart_SendString 等) 的风格保持一致。
+ */
+char *Lang_Get(StringID id);
+
+#endif // __LANGUAGE_H__
diff --git a/App/main.c b/App/main.c
index 18cc371..5c82eaa 100644
--- a/App/main.c
+++ b/App/main.c
@@ -7,6 +7,7 @@
#include "system.h"
#include "../Drivers/rf.h"
#include "../Drivers/lcd.h"
+#include "../Drivers/timer.h"
/* =========================================================================
* 系统全局变量定义
@@ -193,9 +194,46 @@ void Debug_ProcessCommand(char cmd)
}
}
+// ====== 调试用最小化启动开关 ======
+// 置 1 时 main() 只做点亮屏幕这一件事,RF/自检/数据库/事件框架/Timer1 全部跳过,
+// 用于定位"开机反复重启"问题的范围。定位完成后改回 0,恢复正常启动流程。
+#define DEBUG_MINIMAL_BOOT 1
+
/* =========================================================================
* 主函数 (入口)
* ========================================================================= */
+#if DEBUG_MINIMAL_BOOT
+
+void main(void)
+{
+ // ====== STC32G 特殊功能寄存器内核基础初始化 ======
+ WTST = 0; // 设置程序 Flash 访问等待时间为 0 (最高速运行)
+ EAXFR = 1; // 允许访问扩展特殊功能寄存器 (XSFR)
+ CKCON = 0; // 外部总线时钟设定为最快
+ WDT_CONTR = 0x00; // 关闭看门狗定时器
+
+ // ====== 步骤 A:加回 RF_Init() 与 Timer1_Init(),先不打开 RF 接收 ======
+ GPIO_Init(); // 配置各个引脚的推挽输出/准双向模式及内部上拉 (锁存 PWR_HOLD=1)
+ RGB_Send(0, 0, 0, 0, 0, 0); // 上电第一时间强制发送 0 码关闭灯条,熄灭上电暂态引起的随机白光
+ RF_Init(); // 加回来:Timer0 常驻自由运行 + P3.6 常态双边沿中断 + 上拉,此时芯片仍 SHUT=1 关闭
+ Delay_ms(500); // 软启动延时,等待供电电容稳定
+ LCD_Init(); // 初始化 LHS114TC-IF03 (ST7789V) 屏幕控制寄存器序列并开启背光
+ Uart1_Init(); // 初始化串口 1 波特率 115200 供调试日志
+ Uart_SendString("Wristband System Initialized! (DEBUG_MINIMAL_BOOT=1, step A: RF_Init+Timer1_Init)\r\n");
+
+ Timer1_Init(); // 全局中断 EA=1 从这里开始生效,Timer0 溢出中断与 P3.6 边沿中断真正开始工作
+ // 注意:已恢复到步骤 A,不调用 RF_SetMode(1),RF 芯片保持关闭状态
+
+ LCD_ShowStringCentered(100, "STEP A OK", COLOR_WHITE, COLOR_BLACK);
+ LCD_ShowStringCentered(130, "RF_Init+Timer1", COLOR_GRAY, COLOR_BLACK);
+
+ while (1) {
+ Delay_ms(1);
+ }
+}
+
+#else
+
void main(void)
{
char cmd;
@@ -268,6 +306,8 @@ void main(void)
}
}
+#endif // DEBUG_MINIMAL_BOOT
+
// 唤醒源事件标志,由对应的外部中断 ISR 写入
volatile u8 p0_wakeup_flag = 0; // P0 端口按键唤醒标志
volatile u8 p2_wakeup_flag = 0; // P2 端口按键唤醒标志
@@ -304,12 +344,15 @@ void Port2_Isr(void) interrupt 15
/**
* @brief Port3 端口掉电中断服务函数 (由 isr_jump.asm 引导定位至此)
- * @details 负责处理 P3.6 (新射频数据脚) 在休眠模式下的物理跳变唤醒。
+ * @details P3.6 (RF_RX_DATA) 双边沿中断现在改为常态开启 (见 RF_Init()),身兼两职:
+ * 1. 休眠期间作为唤醒源,记录 p3_wakeup_flag 供 Wakeup_Restore() 判定唤醒来源;
+ * 2. 正常运行期间驱动 RF_HandleEdgeInterrupt() 增量解码 EV1527 信号。
+ * 不再像旧版那样在首次触发后自行关闭 P3INTE,否则第 2 项功能会在第一次边沿后失效。
*/
void Port3_Isr(void) interrupt 16
{
EAXFR = 1; // 使能访问扩展寄存器
p3_wakeup_flag = P3INTF | 0x01;// 读取并备份 P3 中断标志寄存器值 (0x01作安全掩码)
P3INTF = 0x00; // 清除 P3 端口中断悬挂标志位
- P3INTE = 0x00; // 立即关闭 P3 中断通道,防止反复重入
+ RF_HandleEdgeInterrupt(); // 驱动 EV1527 边沿增量解码状态机
}
diff --git a/App/strings.h b/App/strings.h
new file mode 100644
index 0000000..e146f6f
--- /dev/null
+++ b/App/strings.h
@@ -0,0 +1,66 @@
+#ifndef __STRINGS_H__
+#define __STRINGS_H__
+
+//////////////////////////////////////////////////////////
+// !!!!!! 不要动下面的代码 !!!!!!
+#ifdef DEFINE_STRING
+#undef DEFINE_STRING
+#endif
+#define DEFINE_STRING(name, fr, en) STR_##name,
+// !!!!!! 不要动上面的代码 !!!!!!
+//////////////////////////////////////////////////////////
+
+/**
+ * @brief 界面全部可翻译文案登记表 (法语/英语)
+ * @details 新增文案:在 WRISTBAND_STRING_LIST 里加一行
+ * DEFINE_STRING(NAME, "法语文案", "English Text")
+ * 取用:Lang_Get(STR_NAME) 返回当前语言下对应的文案指针
+ */
+#define WRISTBAND_STRING_LIST \
+ /* ---- 主菜单 (含序号前缀,与原硬编码保持一致的排版风格) ---- */ \
+ DEFINE_STRING(MENU_LANGUAGE, "1. LANGUE", "1. LANGUAGE") \
+ DEFINE_STRING(MENU_HORLOGE, "2. HORLOGE", "2. CLOCK") \
+ DEFINE_STRING(MENU_APPAIRAGE, "3. APPAIRAGE", "3. PAIRING") \
+ DEFINE_STRING(MENU_LIAISON, "4. LIAISON", "4. LINK") \
+ /* ---- 对码二级菜单标题与传感器分类项 ---- */ \
+ DEFINE_STRING(PAIR_MENU_TITLE, "MENU APPAI.", "PAIRING MENU") \
+ DEFINE_STRING(DEV_PORTE, "DETEC. PORTE", "DOOR SENSOR") \
+ DEFINE_STRING(DEV_PIR, "DETEC. PIR", "PIR SENSOR") \
+ DEFINE_STRING(DEV_FUMEE, "DETEC. FUMEE", "SMOKE SENSOR") \
+ DEFINE_STRING(DEV_GAZ, "DETEC. GAZ", "GAS SENSOR") \
+ DEFINE_STRING(DEV_EAU, "DETEC. EAU", "WATER SENSOR") \
+ /* ---- 传感器类型简称 (对码确认页 / 报警页使用) ---- */ \
+ DEFINE_STRING(NAME_PORTE, "PORTE", "DOOR") \
+ DEFINE_STRING(NAME_PIR, "PIR", "PIR") \
+ DEFINE_STRING(NAME_FUMEE, "FUMEE", "SMOKE") \
+ DEFINE_STRING(NAME_GAZ, "GAZ", "GAS") \
+ DEFINE_STRING(NAME_EAU, "EAU", "WATER") \
+ /* ---- 对码流程页面 ---- */ \
+ DEFINE_STRING(PAIRING_TITLE, "APPAIRAGE", "PAIRING") \
+ DEFINE_STRING(PAIR_SEARCHING, "RECHERCHE", "SEARCHING") \
+ DEFINE_STRING(PAIR_TRIGGER, "DECLENCH. CAPT", "TRIGGER SENSOR") \
+ DEFINE_STRING(PAIR_RECEIVED, "v RECU", "v RECEIVED") \
+ DEFINE_STRING(PAIR_CONFIRM, "CONFIRMER", "CONFIRM") \
+ DEFINE_STRING(PAIR_SUCCESS, "SUCCES", "SUCCESS") \
+ DEFINE_STRING(PAIR_SAVED, "ENREGISTRE", "SAVED") \
+ DEFINE_STRING(PAIR_FAILED, "ECHEC", "FAILED") \
+ DEFINE_STRING(PAIR_TIMEOUT, "DELAI DEPASSE", "TIMEOUT") \
+ DEFINE_STRING(PAIR_WRONG_TYPE, "TYPE INCORRECT", "WRONG TYPE") \
+ /* ---- 报警 / SOS 页面 ---- */ \
+ DEFINE_STRING(ALARM_INTRUSION, "INTRUSION!", "INTRUSION!") \
+ DEFINE_STRING(SOS_TITLE, "SOS", "SOS") \
+ DEFINE_STRING(SOS_SENDING, "ENVOI", "SENDING") \
+ DEFINE_STRING(SOS_CALLING, "APPEL...", "CALLING...") \
+ /* ---- 摄像机绑定页面 ---- */ \
+ DEFINE_STRING(LINK_TITLE, "LIAISON", "LINK") \
+ DEFINE_STRING(BIND_SENDING, "ENVOI EN COURS", "SENDING...") \
+ /* ---- 时钟页公共元素 ---- */ \
+ DEFINE_STRING(CLOCK_AMPM, "AM", "AM") \
+ DEFINE_STRING(CLOCK_DATE, "VEN 10-07", "FRI 10-07")
+
+typedef enum {
+ WRISTBAND_STRING_LIST
+ STR_COUNT
+} StringID;
+
+#endif // __STRINGS_H__
diff --git a/App/system.c b/App/system.c
index 3b2abbf..c102602 100644
--- a/App/system.c
+++ b/App/system.c
@@ -342,11 +342,10 @@ void Enter_Low_Power_Sleep(void)
P2INTE |= 0x40; // 开启 P2.6 中断允许
P2WKUE |= 0x40; // 使能 P2.6 掉电唤醒
- // 9. 配置 Port3 中断唤醒源 (RF_RX_DATA=P3.6)
- P3IM1 |= 0x40;
- P3IM0 |= 0x40; // 双边沿触发
+ // 9. 配置 Port3 掉电唤醒源 (RF_RX_DATA=P3.6)
+ // 双边沿触发与 P3INTE 中断允许现已在 RF_Init() 中常态开启 (供正常运行期间的边沿解码使用),
+ // 此处只需重新武装 P3WKUE (掉电唤醒使能,会被 Wakeup_Restore() 清零) 与清除挂起标志
P3INTF = 0x00; // 清除挂起标志
- P3INTE |= 0x40; // 开启 P3.6 中断允许
P3WKUE |= 0x40; // 使能 P3.6 掉电唤醒
// 确保引脚上拉和输入使能,防止因浮空产生的功耗泄漏
@@ -365,7 +364,10 @@ void Enter_Low_Power_Sleep(void)
p3_wakeup_flag = 0;
rf_wakeup_flag = 0;
- // 10. 关闭系统滴答定时器 Timer1 中断,防止休眠时被定时器误唤醒
+ // 10. 关闭系统滴答定时器 Timer1 与 RF 时间戳基准 Timer0 的中断,防止休眠时被定时器误唤醒
+ // (Power Down 模式下主振荡器本应整体停振,Timer0/1 都会随之停止计数;这里仍显式关闭
+ // 中断允许作为防御性保险,与 ET1 的处理方式保持一致)
+ ET0 = 0;
ET1 = 0;
EA = 1; // 开启全局中断
@@ -383,10 +385,15 @@ void Enter_Low_Power_Sleep(void)
void Wakeup_Restore(void)
{
EAXFR = 1;
- // 1. 立即关闭所有掉电外部中断允许与唤醒允许,防止重入
+ // 1. 立即关闭 P0/P2 掉电外部中断允许与全部唤醒允许,防止重入;
+ // P3INTE 的 P3.6 位不清零 —— 该中断常态用于正常运行期间的 RF 边沿解码,唤醒后仍需保持开启
P0INTE = 0x00;
P2INTE = 0x00;
- P3INTE = 0x00;
+ P3INTE = 0x40;
+ // 恢复休眠前被 Enter_Low_Power_Sleep() 关闭的 P3.6 内部上拉:LR690L 在 SHUT=1 期间数据脚
+ // 高阻悬空,配合常态开启的 P3.6 中断会引发中断风暴,唤醒后必须立刻用上拉钳住电平
+ P3PU |= 0x40;
+
P0WKUE = 0x00;
P2WKUE = 0x00;
P3WKUE = 0x00;
@@ -398,8 +405,9 @@ void Wakeup_Restore(void)
WKTCH = 0x00;
WKTCL = 0x00;
- // 3. 开启全局中断并重启系统滴答定时器
+ // 3. 开启全局中断并重启系统滴答定时器 Timer1 与 RF 时间戳基准 Timer0
EA = 1;
+ ET0 = 1;
ET1 = 1;
// 4. 恢复射频芯片工作模式
diff --git a/App/ui.c b/App/ui.c
index 3bcf4bd..f22ab6c 100644
--- a/App/ui.c
+++ b/App/ui.c
@@ -1,9 +1,11 @@
#include "ui.h"
+#include "database.h"
+#include "language.h"
#include "../Drivers/lcd.h"
#include "../Drivers/adc.h"
-// 引入外部主菜单对码项名称定义 (例如 "DETEC. PORTE" 等,法语表达防区类型名称)
-extern char *code menu_items[5];
+// 引入外部主菜单对码项名称的字符串 ID 表 (例如门磁/PIR/烟感等防区类型)
+extern const StringID device_menu_ids[5];
/**
* @brief 渲染并显示系统待机时钟主页面
@@ -70,10 +72,10 @@ void UI_ShowClockPage(SystemState state, u8 hour, u8 min)
LCD_ShowString16x32Centered(100, time_str, COLOR_WHITE, COLOR_BLACK); // 使用 16x32 特大字体居中绘制时间
// ====== 绘制 AM/PM 状态 ======
- LCD_ShowStringCentered(150, "AM", COLOR_WHITE, COLOR_BLACK);
+ LCD_ShowStringCentered(150, Lang_Get(STR_CLOCK_AMPM), COLOR_WHITE, COLOR_BLACK);
// ====== 绘制日期星期栏 ======
- LCD_ShowStringCentered(195, "VEN 10-07", COLOR_GRAY, COLOR_BLACK);
+ LCD_ShowStringCentered(195, Lang_Get(STR_CLOCK_DATE), COLOR_GRAY, COLOR_BLACK);
}
/**
@@ -87,21 +89,21 @@ void UI_ShowPairMenuPage(u8 selected_index)
{
LCD_Clear(COLOR_BLACK);
- // 绘制标题 "MENU APPAI." (法语: 配对/对码设置菜单)
- LCD_ShowStringCentered(30, "MENU APPAI.", COLOR_AMBER, COLOR_BLACK);
-
+ // 绘制标题 "MENU APPAI." (配对/对码设置菜单)
+ LCD_ShowStringCentered(30, Lang_Get(STR_PAIR_MENU_TITLE), COLOR_AMBER, COLOR_BLACK);
+
// 渲染上方相邻项 (如果当前选择的不是第一行)
if (selected_index > 0) {
- LCD_ShowStringCentered(78, menu_items[selected_index - 1], COLOR_GRAY, COLOR_BLACK);
+ LCD_ShowStringCentered(78, Lang_Get(device_menu_ids[selected_index - 1]), COLOR_GRAY, COLOR_BLACK);
}
-
+
// 渲染当前选中的高亮中间项
LCD_DrawRectBorder(15, 110, 104, 28, COLOR_CYAN); // 青色对齐外框 (在 135px 宽度下居中)
- LCD_ShowStringCentered(118, menu_items[selected_index], COLOR_CYAN, COLOR_BLACK);
-
+ LCD_ShowStringCentered(118, Lang_Get(device_menu_ids[selected_index]), COLOR_CYAN, COLOR_BLACK);
+
// 渲染下方相邻项 (如果当前选择的不是最后一行)
if (selected_index < 4) {
- LCD_ShowStringCentered(158, menu_items[selected_index + 1], COLOR_GRAY, COLOR_BLACK);
+ LCD_ShowStringCentered(158, Lang_Get(device_menu_ids[selected_index + 1]), COLOR_GRAY, COLOR_BLACK);
}
}
@@ -115,7 +117,7 @@ void UI_ShowPairWaitPage(u8 frame_index)
LCD_Clear(COLOR_BLACK);
// 绘制头部 "APPAIRAGE" (对码中)
- LCD_ShowStringCentered(35, "APPAIRAGE", COLOR_GRAY, COLOR_BLACK);
+ LCD_ShowStringCentered(35, Lang_Get(STR_PAIRING_TITLE), COLOR_GRAY, COLOR_BLACK);
// 根据 Timer1 节拍算得的帧号,切换渲染雷达的 3 种点阵图形,形成扩散动态视觉 (在 135px 下居中 X = 51)
if (frame_index == 0)
@@ -126,8 +128,8 @@ void UI_ShowPairWaitPage(u8 frame_index)
LCD_DrawMonoBitmap(51, 75, 32, 32, bmp_radar_32x32_f3, COLOR_CYAN, COLOR_BLACK);
// 绘制状态描述
- LCD_ShowStringCentered(165, "RECHERCHE", COLOR_WHITE, COLOR_BLACK); // 法语: 搜寻中
- LCD_ShowStringCentered(200, "DECLENCH. CAPT", COLOR_CYAN, COLOR_BLACK); // 法语: 请触发传感器以发射射频信号
+ LCD_ShowStringCentered(165, Lang_Get(STR_PAIR_SEARCHING), COLOR_WHITE, COLOR_BLACK); // 搜寻中
+ LCD_ShowStringCentered(200, Lang_Get(STR_PAIR_TRIGGER), COLOR_CYAN, COLOR_BLACK); // 请触发传感器以发射射频信号
}
/**
@@ -140,8 +142,8 @@ void UI_ShowPairConfirmPage(u8 type, u8 zone_index)
char zone_str[3];
LCD_Clear(COLOR_BLACK);
- // 顶部显示捕获回馈标志 "v RECU" (法语: 信号已接收)
- LCD_ShowStringCentered(32, "v RECU", COLOR_GREEN, COLOR_BLACK);
+ // 顶部显示捕获回馈标志 "v RECU" (信号已接收)
+ LCD_ShowStringCentered(32, Lang_Get(STR_PAIR_RECEIVED), COLOR_GREEN, COLOR_BLACK);
// 在上部居中绘制设备类别所对应的图形 (135px 下居中 X = 51)
if (type == 0) {
@@ -153,7 +155,7 @@ void UI_ShowPairConfirmPage(u8 type, u8 zone_index)
}
// 渲染传感器类型的文字
- LCD_ShowStringCentered(105, sensor_names_fr[type], COLOR_WHITE, COLOR_BLACK);
+ LCD_ShowStringCentered(105, Lang_Get(sensor_type_name_ids[type]), COLOR_WHITE, COLOR_BLACK);
// 绘制一个青色边框,在中间以 16x32 大号字体展示当前防区号 (在 135px 下居中 X=22, 宽度 90)
LCD_DrawRectBorder(22, 140, 90, 44, COLOR_CYAN);
@@ -163,7 +165,7 @@ void UI_ShowPairConfirmPage(u8 type, u8 zone_index)
LCD_ShowString16x32(51, 146, zone_str, COLOR_CYAN, COLOR_BLACK); // 渲染两位防区编号 (135px 下居中 X = 51)
// 底部提示按键动作
- LCD_ShowStringCentered(215, "CONFIRMER", COLOR_GRAY, COLOR_BLACK); // 法语: 短按以确认保存
+ LCD_ShowStringCentered(215, Lang_Get(STR_PAIR_CONFIRM), COLOR_GRAY, COLOR_BLACK); // 短按以确认保存
}
/**
@@ -174,8 +176,8 @@ void UI_ShowPairSuccessPage(void)
LCD_Clear(COLOR_BLACK);
// 渲染 32x32 对勾图标 (135px 下居中 X = 51)
LCD_DrawMonoBitmap(51, 75, 32, 32, bmp_checkmark_32x32, COLOR_GREEN, COLOR_BLACK);
- LCD_ShowStringCentered(145, "SUCCES", COLOR_GREEN, COLOR_BLACK); // 法语: 成功
- LCD_ShowStringCentered(180, "ENREGISTRE", COLOR_GRAY, COLOR_BLACK); // 法语: 配置已保存
+ LCD_ShowStringCentered(145, Lang_Get(STR_PAIR_SUCCESS), COLOR_GREEN, COLOR_BLACK); // 成功
+ LCD_ShowStringCentered(180, Lang_Get(STR_PAIR_SAVED), COLOR_GRAY, COLOR_BLACK); // 配置已保存
}
/**
@@ -187,11 +189,11 @@ void UI_ShowPairFailPage(u8 is_timeout)
LCD_Clear(COLOR_BLACK);
// 渲染 32x32 红色叉号图标 (135px 下居中 X = 51)
LCD_DrawMonoBitmap(51, 75, 32, 32, bmp_cross_32x32, COLOR_RED, COLOR_BLACK);
- LCD_ShowStringCentered(145, "ECHEC", COLOR_RED, COLOR_BLACK); // 法语: 失败
+ LCD_ShowStringCentered(145, Lang_Get(STR_PAIR_FAILED), COLOR_RED, COLOR_BLACK); // 失败
if (is_timeout) {
- LCD_ShowStringCentered(180, "DELAI DEPASSE", COLOR_GRAY, COLOR_BLACK); // 法语: 对码超时
+ LCD_ShowStringCentered(180, Lang_Get(STR_PAIR_TIMEOUT), COLOR_GRAY, COLOR_BLACK); // 对码超时
} else {
- LCD_ShowStringCentered(180, "TYPE INCORRECT", COLOR_GRAY, COLOR_BLACK); // 法语: 传感器类型不符
+ LCD_ShowStringCentered(180, Lang_Get(STR_PAIR_WRONG_TYPE), COLOR_GRAY, COLOR_BLACK); // 传感器类型不符
}
}
@@ -224,7 +226,7 @@ void UI_ShowAlarmPage(u8 type, u8 zone_index)
// 拼接字符串: "设备类型名 两位防区号"
{
u8 len = 0;
- char *p = sensor_names_fr[type];
+ char *p = Lang_Get(sensor_type_name_ids[type]);
while(*p) {
zone_info[len++] = *p++;
}
@@ -234,7 +236,7 @@ void UI_ShowAlarmPage(u8 type, u8 zone_index)
zone_info[len] = '\0';
}
LCD_ShowStringCentered(135, zone_info, COLOR_RED, COLOR_BLACK); // 红字绘制设备防区
- LCD_ShowStringCentered(175, "INTRUSION!", COLOR_WHITE, COLOR_BLACK); // 白字入侵提示
+ LCD_ShowStringCentered(175, Lang_Get(STR_ALARM_INTRUSION), COLOR_WHITE, COLOR_BLACK); // 白字入侵提示
}
/**
@@ -260,10 +262,10 @@ void UI_ShowSosPage(void)
LCD_FillRect(53, 38, 4, 4, COLOR_RED); // 左侧散射闪光
LCD_FillRect(77, 38, 4, 4, COLOR_RED); // 右侧散射闪光
- // 绘制求救中法文字样
- LCD_ShowStringCentered(120, "SOS", COLOR_RED, COLOR_BLACK);
- LCD_ShowStringCentered(150, "ENVOI", COLOR_RED, COLOR_BLACK); // 法语: 求救中
- LCD_ShowStringCentered(195, "APPEL...", COLOR_WHITE, COLOR_BLACK); // 法语: 紧急呼叫中
+ // 绘制求救中文字样
+ LCD_ShowStringCentered(120, Lang_Get(STR_SOS_TITLE), COLOR_RED, COLOR_BLACK);
+ LCD_ShowStringCentered(150, Lang_Get(STR_SOS_SENDING), COLOR_RED, COLOR_BLACK); // 求救中
+ LCD_ShowStringCentered(195, Lang_Get(STR_SOS_CALLING), COLOR_WHITE, COLOR_BLACK); // 紧急呼叫中
}
/**
@@ -282,8 +284,8 @@ void UI_ShowBindingPage(u32 addr)
LCD_FillRect(77, 50, 2, 18, COLOR_CYAN); // 镜头固定翼 1 (135px 下居中等比右移 X = 77)
LCD_FillRect(79, 52, 2, 14, COLOR_CYAN); // 镜头固定翼 2 (135px 下居中等比右移 X = 79)
- LCD_ShowStringCentered(120, "LIAISON", COLOR_WHITE, COLOR_BLACK); // 法语: 连接/绑定
- LCD_ShowStringCentered(155, "ENVOI EN COURS", COLOR_CYAN, COLOR_BLACK); // 法语: 发送对码帧中
+ LCD_ShowStringCentered(120, Lang_Get(STR_LINK_TITLE), COLOR_WHITE, COLOR_BLACK); // 连接/绑定
+ LCD_ShowStringCentered(155, Lang_Get(STR_BIND_SENDING), COLOR_CYAN, COLOR_BLACK); // 发送对码帧中
// 拼接要对外广播的手环 24 位克隆物理 ID 十六进制字符串并显示在最下方
id_str[0] = 'I'; id_str[1] = 'D'; id_str[2] = ':'; id_str[3] = ' ';
@@ -311,14 +313,14 @@ void UI_ShowBindingPage(u32 addr)
/**
* @brief 渲染显示设置主菜单界面
- * @param selected_index 当前菜单高亮行索引 (0:时钟修改, 1:对码匹配, 2:摄像机绑定)
+ * @param selected_index 当前菜单高亮行索引 (0:语言切换, 1:时钟修改, 2:对码匹配, 3:摄像机绑定)
* @details 在顶部和底部绘制向上/向下箭头提示滑轮,中部绘制选中项的大几何图形,
- * 并在下方打印对应的法文字串描述。
+ * 并在下方打印对应当前语言的文字描述。
*/
void UI_ShowMainMenu(u8 selected_index)
{
LCD_Clear(COLOR_BLACK);
-
+
// ====== 绘制顶部的向上箭头三角标记 (在 135px 宽度下居中) ======
LCD_FillRect(65, 20, 4, 2, COLOR_GRAY);
LCD_FillRect(63, 22, 8, 2, COLOR_GRAY);
@@ -326,27 +328,33 @@ void UI_ShowMainMenu(u8 selected_index)
if (selected_index == 0)
{
- // 菜单 0:时间修改设置。中部绘制一个青色闹钟样式 (135px 下居中 X = 51)
- LCD_DrawRectBorder(51, 70, 32, 32, COLOR_CYAN); // 闹钟外圈
- LCD_FillRect(66, 76, 2, 10, COLOR_CYAN); // 闹钟时针
- LCD_FillRect(66, 85, 8, 2, COLOR_CYAN); // 闹钟分针
- LCD_ShowStringCentered(150, "1. HORLOGE", COLOR_WHITE, COLOR_BLACK); // 法语: 时钟/时间修改
+ // 菜单 0:语言切换。用当前语言的两字母缩写代替图标,直接展示切换后的效果
+ LCD_ShowString16x32Centered(75, (Lang_Current() == LANG_FR) ? "FR" : "EN", COLOR_CYAN, COLOR_BLACK);
+ LCD_ShowStringCentered(150, Lang_Get(STR_MENU_LANGUAGE), COLOR_WHITE, COLOR_BLACK);
}
else if (selected_index == 1)
{
- // 菜单 1:无线对码匹配。中部绘制一个青色同心圆雷达波纹图形 (135px 下居中 X = 51)
- LCD_DrawMonoBitmap(51, 70, 32, 32, bmp_radar_32x32_f3, COLOR_CYAN, COLOR_BLACK);
- LCD_ShowStringCentered(150, "2. APPAIRAGE", COLOR_WHITE, COLOR_BLACK); // 法语: 配对
+ // 菜单 1:时间修改设置。中部绘制一个青色闹钟样式 (135px 下居中 X = 51)
+ LCD_DrawRectBorder(51, 70, 32, 32, COLOR_CYAN); // 闹钟外圈
+ LCD_FillRect(66, 76, 2, 10, COLOR_CYAN); // 闹钟时针
+ LCD_FillRect(66, 85, 8, 2, COLOR_CYAN); // 闹钟分针
+ LCD_ShowStringCentered(150, Lang_Get(STR_MENU_HORLOGE), COLOR_WHITE, COLOR_BLACK); // 时钟/时间修改
}
else if (selected_index == 2)
{
- // 菜单 2:网络摄像机绑定。中部绘制一个相机/联通关系图 (135px 下居中等比右移 X = 57)
+ // 菜单 2:无线对码匹配。中部绘制一个青色同心圆雷达波纹图形 (135px 下居中 X = 51)
+ LCD_DrawMonoBitmap(51, 70, 32, 32, bmp_radar_32x32_f3, COLOR_CYAN, COLOR_BLACK);
+ LCD_ShowStringCentered(150, Lang_Get(STR_MENU_APPAIRAGE), COLOR_WHITE, COLOR_BLACK); // 配对
+ }
+ else if (selected_index == 3)
+ {
+ // 菜单 3:网络摄像机绑定。中部绘制一个相机/联通关系图 (135px 下居中等比右移 X = 57)
LCD_DrawRectBorder(57, 74, 20, 16, COLOR_CYAN);
LCD_FillRect(65, 80, 4, 4, COLOR_CYAN);
LCD_FillRect(77, 76, 2, 12, COLOR_CYAN);
LCD_FillRect(79, 78, 2, 8, COLOR_CYAN);
LCD_FillRect(81, 80, 2, 4, COLOR_CYAN);
- LCD_ShowStringCentered(150, "3. LIAISON", COLOR_WHITE, COLOR_BLACK); // 法语: 摄像机绑定
+ LCD_ShowStringCentered(150, Lang_Get(STR_MENU_LIAISON), COLOR_WHITE, COLOR_BLACK); // 摄像机绑定
}
// ====== 绘制底部的向下箭头三角标记 (在 135px 宽度下居中) ======
@@ -411,6 +419,6 @@ void UI_ShowSetTimePage(u8 hour, u8 min, u8 selection, u8 blink_on)
LCD_ShowString16x32(75, 100, time_str, COLOR_CYAN, COLOR_BLACK); // 采用青色特大字描绘分钟 (在 135px 下整体居中 X = 75)
// 补齐底部的固定辅助元素
- LCD_ShowStringCentered(150, "AM", COLOR_WHITE, COLOR_BLACK);
- LCD_ShowStringCentered(195, "VEN 10-07", COLOR_GRAY, COLOR_BLACK);
+ LCD_ShowStringCentered(150, Lang_Get(STR_CLOCK_AMPM), COLOR_WHITE, COLOR_BLACK);
+ LCD_ShowStringCentered(195, Lang_Get(STR_CLOCK_DATE), COLOR_GRAY, COLOR_BLACK);
}
diff --git a/Drivers/rf.c b/Drivers/rf.c
index 730a74c..9db9173 100644
--- a/Drivers/rf.c
+++ b/Drivers/rf.c
@@ -1,4 +1,5 @@
#include "rf.h"
+#include "timer.h"
#include "../App/config.h"
#include "../App/rgb.h"
#include "intrins.h"
@@ -14,7 +15,6 @@ sbit RF_RX_DATA = P3^6; // LR690L 接收解调数据输出引脚 (DATA) - 物
// 声明外部延时及串口打印函数 (在 system.c 中定义)
extern void Delay_us(u16 us);
extern void Delay_ms(u16 ms);
-extern u16 GetTimer0_Safe(void);
extern void Uart_SendString(char *s);
extern void Uart_SendHex4(u8 val);
extern void Uart_SendHex8(u8 val);
@@ -22,9 +22,12 @@ extern void Uart_SendHex16(u16 val);
extern void Uart_SendHex20(u32 val);
extern void Uart_SendHex32(u32 val);
extern void Uart_SendByte(u8 dat);
+extern volatile u16 ms_tick; // 全局毫秒滴答 (0~999 循环),用于半成品帧看门狗超时判断
/**
* @brief 初始化无线射频芯片相关的 GPIO 引脚方向与状态
+ * @details 同时在此完成 Timer0 自由运行时间戳基准与 P3.6 (RF_RX_DATA) 常态双边沿中断的
+ * 一次性开机配置,供 RF_HandleEdgeInterrupt() 增量解码状态机全程使用。
*/
void RF_Init(void)
{
@@ -42,11 +45,27 @@ void RF_Init(void)
P3M0 &= ~(1 << 6);
P3M0 |= ((1 << 5) | (1 << 7));
+ // 2.1 开启 P3.6 内部上拉:LR690L 在 SHUT=1 (休眠/发射时也会关断接收) 时数据脚为高阻输出,
+ // 若不加上拉会悬空乱跳。P3.6 中断现在常态开启,悬空噪声会引发中断风暴,必须靠上拉钳位。
+ P3PU |= 0x40;
+
// 3. 设置系统默认的初始电平
RF_TX = 0; // 断开发射天线通路
SHUT = 1; // 默认拉高 SHUT,使 LR690L 处于休眠关闭状态
RF_RX = 0; // 断开接收天线通路
RF_TX_DAT = 0; // 调制发送数据线置低
+
+ // 4. 启动 Timer0 常驻自由运行,作为边沿中断增量解码的时间戳基准 (实现见 Drivers/timer.c)
+ Timer0_Init();
+
+ // 5. P3.6 (RF_RX_DATA) 双边沿中断常态开启 (原来只在 Enter_Low_Power_Sleep 里临时开启,
+ // 现在改为开机后永久开启,休眠时复用同一路中断做唤醒源,不再需要单独切换)
+ P3IM1 |= 0x40;
+ P3IM0 |= 0x40; // 双边沿触发
+ P3INTF = 0x00; // 清除挂起的中断标志
+ // DEBUG: 临时屏蔽 P3.6 中断允许,测试是否是 Port3_Isr 触发本身(不管里面执行什么)导致重启,
+ // 定位完成后改回 P3INTE |= 0x40;
+ // P3INTE |= 0x40; // 开启 P3.6 中断允许 (全局 EA 由 Timer1_Init() 统一开启)
}
/**
@@ -76,18 +95,195 @@ void RF_SetMode(u8 mode)
}
/**
- * @brief 使用单片机硬件 Timer0 实现高精度发射电平时延控制
+ * @brief 使用 Timer0 自由运行时间戳实现的高精度微秒延时
* @param us 延时微秒数
- * @note STC32G 在 24MHz 晶振 12T 模式下,Timer0 的 1 tick 正好等于 0.5us (2MHz)
+ * @note 旧版实现会启停复位 Timer0,与本文件新增的常驻自由运行用法冲突 (会导致 RX 时间戳永久停摆),
+ * 改为对运行中的 Timer0 做起止快照比较,纯只读、不影响其自由运行状态。
*/
void RF_Delay_us(u16 us)
{
- u16 ticks = us * 2;
- TL0 = 0;
- TH0 = 0;
- TR0 = 1; // 启动定时器 0
- while (GetTimer0_Safe() < ticks);
- TR0 = 0; // 停止定时器 0
+ u32 start = Timer0_GetTimestamp();
+ u32 wait_ticks = (u32)us * 2UL; // 0.5us/tick
+ while ((u32)(Timer0_GetTimestamp() - start) < wait_ticks);
+}
+
+/* =========================================================================
+ * EV1527 边沿中断增量解码状态机
+ * ========================================================================= */
+
+// 计时阈值 (单位: tick,0.5us/tick),与原阻塞版 EV1527_Decode 的判定区间保持一致
+#define RF_SYNC_HIGH_MIN_TICKS (50UL * 2UL) // 50us
+#define RF_SYNC_HIGH_MAX_TICKS (2500UL * 2UL) // 2500us
+#define RF_SYNC_LOW_MIN_TICKS (1500UL * 2UL) // 1500us
+#define RF_SYNC_LOW_MAX_TICKS (60000UL* 2UL) // 60000us
+#define RF_T_MIN_TICKS (100UL * 2UL) // 100us
+#define RF_T_MAX_TICKS (800UL * 2UL) // 800us
+
+#define RF_ISR_STATE_IDLE 0 // 搜索同步头候选高电平
+#define RF_ISR_STATE_SYNC_SEEN 1 // 已见候选同步高电平,等待其后的低电平确认
+#define RF_ISR_STATE_COLLECTING 2 // 正在逐位收集 24 位数据
+
+#define RF_DECODE_TIMEOUT_MS 200 // 半成品帧看门狗超时 (远大于一帧最坏情况约 95ms 的总耗时)
+
+static volatile u8 rf_isr_state = RF_ISR_STATE_IDLE;
+static volatile u32 rf_isr_last_edge_ts = 0;
+static volatile u32 rf_isr_pending_high = 0; // 候选同步头高电平宽度 / 收集阶段暂存的本位高电平宽度
+static volatile u32 rf_isr_bit_T = 0; // 本帧的基准单位时间 T
+static volatile u8 rf_isr_bit_index = 0;
+static volatile u32 rf_isr_addr_acc = 0;
+static volatile u8 rf_isr_dat_acc = 0;
+static volatile u16 rf_isr_state_enter_ms = 0;
+
+// 中断解码出的结果,供 EV1527_Decode() 非阻塞取用
+static volatile bit rf_decoded_ready = 0;
+static volatile u32 rf_decoded_addr = 0;
+static volatile u8 rf_decoded_data = 0;
+
+static void RF_ResetDecodeState(void)
+{
+ rf_isr_state = RF_ISR_STATE_IDLE;
+ rf_isr_bit_index = 0;
+ rf_isr_addr_acc = 0;
+ rf_isr_dat_acc = 0;
+}
+
+/**
+ * @brief P3.6 (RF_RX_DATA) 边沿中断增量解码处理 (由 main.c 的 Port3_Isr 调用)
+ * @details 每次电平跳变调用一次。用本次跳变与上次跳变的时间差,还原刚刚结束的那段电平的宽度,
+ * 驱动一个三态状态机 (空闲搜索 -> 同步头确认 -> 逐位收集) 增量推进解码,
+ * 不在中断内做任何阻塞等待,无论是否解码成功都会在很短时间内返回。
+ */
+void RF_HandleEdgeInterrupt(void)
+{
+ // DEBUG: 临时直接返回,先确认是否是本函数内部逻辑导致的重启,定位完成后删除这行
+ return;
+
+ u32 now = Timer0_GetTimestamp();
+ u32 duration = now - rf_isr_last_edge_ts; // 刚结束那段电平的宽度
+ u8 new_level = RF_RX_DATA; // 跳变后的当前电平 (1: 刚结束的是低电平, 0: 刚结束的是高电平)
+ rf_isr_last_edge_ts = now;
+
+ if (rf_isr_state == RF_ISR_STATE_IDLE)
+ {
+ // 空闲搜索阶段:只关心刚结束的高电平脉宽是否落在同步头候选区间
+ if (new_level == 0 &&
+ duration >= RF_SYNC_HIGH_MIN_TICKS && duration <= RF_SYNC_HIGH_MAX_TICKS)
+ {
+ rf_isr_pending_high = duration;
+ rf_isr_state = RF_ISR_STATE_SYNC_SEEN;
+ rf_isr_state_enter_ms = ms_tick;
+ }
+ return;
+ }
+
+ if (rf_isr_state == RF_ISR_STATE_SYNC_SEEN)
+ {
+ // 期望本次为上升沿,duration 为同步头后续低电平宽度
+ if (new_level == 1 &&
+ duration >= RF_SYNC_LOW_MIN_TICKS && duration <= RF_SYNC_LOW_MAX_TICKS &&
+ rf_isr_pending_high >= RF_T_MIN_TICKS && rf_isr_pending_high <= RF_T_MAX_TICKS)
+ {
+ rf_isr_bit_T = rf_isr_pending_high; // 基准单位时间 T 取自同步头高电平宽度
+ rf_isr_bit_index = 0;
+ rf_isr_addr_acc = 0;
+ rf_isr_dat_acc = 0;
+ rf_isr_state = RF_ISR_STATE_COLLECTING;
+ rf_isr_state_enter_ms = ms_tick;
+ }
+ else
+ {
+ RF_ResetDecodeState(); // 不满足同步条件,放弃候选,回到空闲搜索
+ }
+ return;
+ }
+
+ // ====== RF_ISR_STATE_COLLECTING:逐位收集数据 ======
+ if (new_level == 0)
+ {
+ // 高电平刚结束,先暂存宽度,等对应的低电平结束后一起判定这一位
+ rf_isr_pending_high = duration;
+ }
+ else
+ {
+ u32 total = rf_isr_pending_high + duration;
+ if (total < rf_isr_bit_T * 3UL || total > rf_isr_bit_T * 6UL)
+ {
+ RF_ResetDecodeState(); // 单个数据位周期超出 3T~6T 范围,判定畸变,放弃本帧
+ return;
+ }
+
+ if (rf_isr_pending_high > duration * 2UL)
+ {
+ // 高电平明显长于低电平:逻辑 "1"
+ if (rf_isr_bit_index < 20) rf_isr_addr_acc = (rf_isr_addr_acc << 1) | 1;
+ else rf_isr_dat_acc = (rf_isr_dat_acc << 1) | 1;
+ }
+ else if (duration > rf_isr_pending_high * 2UL)
+ {
+ // 低电平明显长于高电平:逻辑 "0"
+ if (rf_isr_bit_index < 20) rf_isr_addr_acc = (rf_isr_addr_acc << 1);
+ else rf_isr_dat_acc = (rf_isr_dat_acc << 1);
+ }
+ else
+ {
+ RF_ResetDecodeState(); // 电平脉冲比值不合规,放弃本帧
+ return;
+ }
+
+ rf_isr_bit_index++;
+ if (rf_isr_bit_index >= 24)
+ {
+ // 24 位全部收齐,输出解码结果供主循环取用
+ rf_decoded_addr = rf_isr_addr_acc;
+ rf_decoded_data = rf_isr_dat_acc;
+ rf_decoded_ready = 1;
+ RF_ResetDecodeState();
+ }
+ }
+}
+
+/**
+ * @brief 半成品帧看门狗:若中断状态机卡在同步头确认/数据收集阶段太久 (信号中途被干扰打断),
+ * 超时后强制复位回空闲搜索,防止后续真实信号被误当作"半成品帧的延续"而永远解不出来。
+ */
+static void RF_CheckDecodeWatchdog(void)
+{
+ u16 elapsed;
+ if (rf_isr_state == RF_ISR_STATE_IDLE) {
+ return;
+ }
+ if (ms_tick >= rf_isr_state_enter_ms) {
+ elapsed = ms_tick - rf_isr_state_enter_ms;
+ } else {
+ elapsed = 1000 + ms_tick - rf_isr_state_enter_ms;
+ }
+ if (elapsed > RF_DECODE_TIMEOUT_MS) {
+ RF_ResetDecodeState();
+ }
+}
+
+/**
+ * @brief 阻塞式检测并解码一个合法的 EV1527 射频信号数据帧
+ * @details 改造后不再自行轮询电平做阻塞测量,真正的解码工作已经在 P3.6 边沿中断
+ * (RF_HandleEdgeInterrupt) 中增量完成;本函数只是非阻塞地检查中断解码结果,
+ * 函数签名与调用方式保持不变,RfMonitorApp/PairApp/RF_DiagnosticMode 均无需改动。
+ */
+bit EV1527_Decode(u32 *out_addr, u8 *out_data)
+{
+ bit ready;
+
+ RF_CheckDecodeWatchdog();
+
+ EA = 0; // 与中断互斥,保证 32 位地址与结果标志的原子性读取
+ ready = rf_decoded_ready;
+ if (ready) {
+ *out_addr = rf_decoded_addr;
+ *out_data = rf_decoded_data;
+ rf_decoded_ready = 0;
+ }
+ EA = 1;
+
+ return ready;
}
/**
@@ -97,16 +293,16 @@ void EV1527_TxFrame(u32 addr, u8 dat)
{
u8 i;
u32 tx_val;
-
+
// 合并 20 位地址与 4 位数据,成为 24 位完整 EV1527 帧
tx_val = (addr << 4) | (dat & 0x0F);
-
+
// 1. 同步头脉冲: 高电平 1T (350us) + 低电平 31T (10850us)
RF_TX_DAT = 1;
RF_Delay_us(350);
RF_TX_DAT = 0;
RF_Delay_us(10850);
-
+
// 2. 依次发送 24 个数据位 (MSB First)
for (i = 0; i < 24; i++)
{
@@ -137,163 +333,23 @@ void EV1527_Transmit(u32 addr, u8 dat)
u8 r;
bit ea_bak = EA; // 备份并屏蔽全局中断,确保发射高低电平周期的绝对纯净与防抖
EA = 0;
-
+
RF_SetMode(2); // 切换为发射状态,接通天线与射频模块通道
Delay_ms(5); // 稍作延时使射频通路稳定
-
+
// 连续发射 25 帧以保证接收设备顺利锁定并解码
for (r = 0; r < 25; r++)
{
EV1527_TxFrame(addr, dat);
// 去除帧与帧之间可能导致波形不连续的 Delay_ms
}
-
+
RF_TX_DAT = 0; // 结束发射,拉低数据脚
RF_SetMode(1); // 恢复为常态后台接收模式
-
+
EA = ea_bak; // 还原全局中断开关状态
}
-/**
- * @brief 获取特定电平引脚状态的持续微秒时间 (利用 Timer0 硬件测量)
- */
-u16 GetPulseDuration(u8 state, u16 timeout_us)
-{
- u16 timeout_ticks;
-
- // 强制关闭 Timer0 计数器,并将其计数值清零复位
- TR0 = 0;
- TMOD &= 0xF0; // 设置 Timer0 寄存器为模式 0 (16位自动重载)
- TL0 = 0x00;
- TH0 = 0x00;
-
- // 换算 16 位定时器所需的最大 ticks 超时值 (在 1T 模式 24MHz 下,1 Tick = 1 / 24M = 0.0416us)
- // 但此系统 Timer0 默认配置为 12T 模式 (1 Tick = 12 / 24MHz = 0.5us)
- timeout_ticks = (u16)((u32)timeout_us * (MAIN_Fosc / 1000000UL) / 12UL);
-
- TR0 = 1; // 启动 Timer0 硬件计数器
-
- // 根据要捕获的电平状态进行阻塞判断,直到电平发生翻转或计数超时
- while (RF_RX_DATA == state)
- {
- if (GetTimer0_Safe() > timeout_ticks)
- {
- TR0 = 0;
- return 0; // 判定超时,返回 0
- }
- }
- TR0 = 0; // 关断定时器
-
- return (u16)((u32)GetTimer0_Safe() * 12UL / (MAIN_Fosc / 1000000UL));
-}
-
-// 模拟射频信号全局变量定义
-volatile u32 mock_rx_addr = 0;
-volatile u8 mock_rx_data = 0;
-volatile bit mock_rf_ready = 0;
-
-/**
- * @brief 阻塞式检测并解码一个合法的 EV1527 射频信号数据帧
- */
-bit EV1527_Decode(u32 *out_addr, u8 *out_data)
-{
- u16 high_time, low_time;
- u8 i;
- u32 addr = 0;
- u8 dat = 0;
- u16 T;
- u16 idata raw_h[24];
- u16 idata raw_l[24];
-
- // 检查模拟射频触发
- if (mock_rf_ready) {
- mock_rf_ready = 0;
- *out_addr = mock_rx_addr;
- *out_data = mock_rx_data;
- return 1;
- }
-
- // ====== 1. 同步头引导脉冲捕获 ======
- {
- u16 wait_cnt = 0;
- while (RF_RX_DATA == 1)
- {
- Delay_us(5);
- wait_cnt++;
- if (wait_cnt > 2000) // 10 毫秒超时保护
- return 0;
- }
-
- wait_cnt = 0;
- while (RF_RX_DATA == 0)
- {
- Delay_us(5);
- wait_cnt++;
- if (wait_cnt > 4000) // 20 毫秒超时保护
- return 0;
- }
- }
-
- // 捕获引导头高电平时间
- high_time = GetPulseDuration(1, 2500);
- if (high_time < 50 || high_time > 2500) {
- return 0;
- }
-
- // 捕获引导头后续的 31T 低电平时间
- low_time = GetPulseDuration(0, 60000);
- if (low_time < 1500 || low_time > 60000) {
- return 0;
- }
-
- // 同步引导脉冲的基准时间 T 计算 (引导脉冲高电平宽度即为 1T)
- T = high_time;
- if (T < 100 || T > 800) return 0; // 自适应范围限幅: 100us ~ 800us
-
- // ====== 2. 依次解析 24 个数据位脉宽 ======
- for (i = 0; i < 24; i++)
- {
- raw_h[i] = GetPulseDuration(1, 2000);
- if (raw_h[i] == 0) return 0;
-
- raw_l[i] = GetPulseDuration(0, 2000);
- if (raw_l[i] == 0) return 0;
- }
-
- // ====== 3. 双端比值解调判定数据内容 ======
- for (i = 0; i < 24; i++)
- {
- u16 total = raw_h[i] + raw_l[i];
- if (total < T * 3 || total > T * 6) return 0; // 单个数据位周期必须在 3T~6T 之间
-
- if (raw_h[i] > raw_l[i] * 2)
- {
- // 高电平明显长于低电平,代表逻辑 "1"
- if (i < 20)
- addr = (addr << 1) | 1;
- else
- dat = (dat << 1) | 1;
- }
- else if (raw_l[i] > raw_h[i] * 2)
- {
- // 低电平明显长于高电平,代表逻辑 "0"
- if (i < 20)
- addr = (addr << 1);
- else
- dat = (dat << 1);
- }
- else
- {
- return 0; // 电平脉冲比值不合规,判定数据帧畸变,解码作废
- }
- }
-
- // 校验无误,输出解调数据
- *out_addr = addr;
- *out_data = dat;
- return 1;
-}
-
/**
* @brief RF 收发诊断测试功能 (支持发送与接收监听打印)
*/
@@ -315,7 +371,7 @@ void RF_DiagnosticMode(char choice)
EV1527_TxFrame(0x37A86UL, 0x01);
MOTOR = 0;
RGB_Send(0, 0, 0, 0, 0, 0);
-
+
Delay_ms(150); // 帧间隔
}
RF_TX_DAT = 0;
@@ -327,11 +383,11 @@ void RF_DiagnosticMode(char choice)
u16 wait_ms;
u32 rx_addr;
u8 rx_data;
-
+
Uart_SendString("=== RF RX Test: Listening for 5 seconds... ===\r\n");
RF_SetMode(1); // 配置进入接收模式并使能天线
Delay_ms(10);
-
+
// 5秒的非阻塞监听
for (wait_ms = 0; wait_ms < 5000; wait_ms++)
{
@@ -342,7 +398,7 @@ void RF_DiagnosticMode(char choice)
Uart_SendString(", Cmd/Data: ");
Uart_SendHex8(rx_data);
Uart_SendString("\r\n");
-
+
// 收到信号后马达震动并亮青色灯提醒
MOTOR = 1;
RGB_Send(0, 150, 150, 0, 150, 150);
@@ -364,29 +420,29 @@ void Loopback_Test(void)
{
u16 match_count = 0;
u16 i;
-
+
EAXFR = 1;
RF_SetMode(1); // 开启接收芯片
- Delay_ms(100);
-
+ Delay_ms(100);
+
Uart_SendString("Starting RF Hardware Loopback Test (P2.1 -> P3.6)...\r\n");
Uart_SendString("Please use a metal tweezers to short-circuit P2.1 (Pin 22) and P3.6 (Pin 19)!\r\n");
-
+
for (i = 0; i < 100; i++)
{
RF_TX_DAT = 1;
Delay_us(500);
if (RF_RX_DATA == 1) match_count++;
-
+
RF_TX_DAT = 0;
Delay_us(500);
if (RF_RX_DATA == 0) match_count++;
}
-
+
Uart_SendString("Loopback Match Count: ");
Uart_SendHex16(match_count);
Uart_SendString("/200\r\n");
-
+
if (match_count > 150)
{
Uart_SendString("Result: PASS! Pin P2.1 and P3.6 are electrical connected successfully!\r\n");
@@ -395,6 +451,6 @@ void Loopback_Test(void)
{
Uart_SendString("Result: FAIL! GPIO coupling check failed. Short-circuit the pins and test again.\r\n");
}
-
+
RF_SetMode(0); // 关断射频
}
diff --git a/Drivers/rf.h b/Drivers/rf.h
index 83dd502..07bde80 100644
--- a/Drivers/rf.h
+++ b/Drivers/rf.h
@@ -40,19 +40,20 @@ void EV1527_TxFrame(u32 addr, u8 dat);
void EV1527_Transmit(u32 addr, u8 dat);
/**
- * @brief 获取特定引脚电平状态的持续微秒时间
- * @param state 待检测的电平目标状态 (0-低电平, 1-高电平)
- * @param timeout_us 最大等待超时上限 (单位: 微秒)
- * @return u16 最终测得的脉冲宽度时长。如果超出 timeout_us 仍未改变电平,则直接返回超时值。
+ * @brief P3.6 (RF_RX_DATA) 边沿中断增量解码处理,由 main.c 的 Port3_Isr 在每次电平跳变时调用
+ * @details 内部维护一个三态状态机 (空闲搜索 -> 同步头确认 -> 逐位收集),用相邻两次跳变的时间差
+ * 还原刚结束的电平宽度,增量推进 EV1527 解码,凑齐 24 位后把结果写入内部标志供
+ * EV1527_Decode() 非阻塞取用。全程不做任何阻塞等待,适合在中断上下文中调用。
*/
-u16 GetPulseDuration(u8 state, u16 timeout_us);
+void RF_HandleEdgeInterrupt(void);
/**
- * @brief 阻塞式检测并解码一个合法的 EV1527 射频信号数据帧
+ * @brief 非阻塞地检查一次 EV1527 射频信号是否已解码完成
* @param out_addr 输出参数指针:若解码成功,将写入解析得到的 20 位发送地址 (即 24 位帧除去 4 位数据位后的剩余高 20 位)
* @param out_data 输出参数指针:若解码成功,将写入解析出的 4 位命令状态数据码 (例如报警/SOS等)
- * @return bit 1-成功解析出一帧有效数据;0-当前未捕获到合法脉冲或校验失败
- * @note 此函数通过对 P3.6 (RF_RX_DATA) 输入脉冲宽度进行软件精密测量,定位同步头,依次采集 24 个数据位,并进行 3 次重复性去噪校验。
+ * @return bit 1-本次调用取到了一帧新解码出的有效数据;0-当前尚无新结果
+ * @note 真正的解码工作已经在 P3.6 边沿中断 (RF_HandleEdgeInterrupt) 中增量完成,
+ * 本函数只是原子地检查并取出中断解码结果,可在主循环中高频调用而不会造成任何阻塞。
*/
bit EV1527_Decode(u32 *out_addr, u8 *out_data);
diff --git a/Drivers/timer.c b/Drivers/timer.c
new file mode 100644
index 0000000..2efa5e2
--- /dev/null
+++ b/Drivers/timer.c
@@ -0,0 +1,94 @@
+#include "timer.h"
+#include "../App/clock.h"
+
+// 外部引用的全局滴答/标志变量 (在 main.c 中定义)
+extern volatile u16 ms_tick;
+extern volatile bit alarm_flash_flag;
+extern volatile u16 key_scan_timer;
+
+/* =========================================================================
+ * Timer0:自由运行硬件时间戳基准
+ * ========================================================================= */
+
+// Timer0 溢出次数,扩展 16 位硬件计数为 32 位软件时间戳的高位部分
+static volatile u16 timer0_ovf_count = 0;
+
+void Timer0_Init(void)
+{
+ TMOD &= 0xF0; // Timer0 模式位清零 -> Mode 0 (STC 上为 16 位不停表计数)
+ TL0 = 0;
+ TH0 = 0;
+ ET0 = 1; // 开启 Timer0 溢出中断允许
+ TR0 = 1; // 启动 Timer0 计数,此后永不停止 (全局 EA 由 Timer1_Init() 统一开启)
+}
+
+/**
+ * @brief Timer0 溢出中断服务程序 (每约 32.768ms 触发一次)
+ * @details 只做计数扩展,不做任何耗时操作,保持中断尽量短。
+ */
+void Timer0_Isr(void) interrupt 1
+{
+ timer0_ovf_count++;
+}
+
+u32 Timer0_GetTimestamp(void)
+{
+ u16 ovf1, ovf2;
+ u8 h1, h2, l;
+ do {
+ ovf1 = timer0_ovf_count;
+ h1 = TH0;
+ l = TL0;
+ h2 = TH0;
+ ovf2 = timer0_ovf_count;
+ } while (h1 != h2 || ovf1 != ovf2);
+ return ((u32)ovf1 << 16) | ((u16)h1 << 8) | l;
+}
+
+/* =========================================================================
+ * Timer1:系统 1ms 基准滴答
+ * ========================================================================= */
+
+// 主循环按键扫描定时标志 (由 1ms 中断置 1,主循环响应清除),定义详见 event.h 的 extern 声明
+volatile bit key_scan_flag = 0;
+
+void Timer1_Init(void)
+{
+ AUXR |= 0x40; // 设定定时器 1 为 1T 模式 (无分频,主频直接输入)
+ TMOD &= 0x0F; // 设置定时器 1 模式寄存器,保留低四位 (定时器0配置)
+ {
+ // 按照系统主频 (MAIN_Fosc) 计算 1ms 对应的重装载值
+ u16 reload = (u16)(65536UL - (MAIN_Fosc / 1000UL));
+ TL1 = (u8)reload; // 载入低 8 位
+ TH1 = (u8)(reload >> 8);// 载入高 8 位
+ }
+ ET1 = 1; // 开启定时器 1 中断允许
+ TR1 = 1; // 启动定时器 1 计数
+ EA = 1; // 开启全局中断允许
+}
+
+/**
+ * @brief 定时器 1 中断服务程序 (每 1ms 触发一次)
+ * @details 承担系统软时钟计算,并每 10ms 置位主循环按键扫描标志,不直接在中断中进行 ADC 多次采样。
+ */
+void Timer1_Isr(void) interrupt 3
+{
+ // 1ms 递增全局滴答计数器 (0~999 循环),供各 App 的呼吸灯/闪烁/超时等周期性效果使用
+ ms_tick++;
+ if (ms_tick >= 1000) {
+ ms_tick = 0;
+ }
+
+ // 1ms 定时更新软时钟
+ Clock_IncMS();
+
+ // 报警屏幕红边框以 100ms 周期亮灭,进行颜色爆闪指示
+ alarm_flash_flag = (ms_tick / 100) % 2;
+
+ /* 物理按键定时计数 (每 10ms 置位一次标志位) */
+ key_scan_timer++;
+ if (key_scan_timer >= 10) {
+ key_scan_timer = 0; // 重置扫描分频计数器
+ key_scan_flag = 1; // 仅在中断里置起标志位,留待主循环中执行实际读取与消抖
+ }
+}
diff --git a/Drivers/timer.h b/Drivers/timer.h
new file mode 100644
index 0000000..4072e37
--- /dev/null
+++ b/Drivers/timer.h
@@ -0,0 +1,31 @@
+#ifndef __TIMER_H__
+#define __TIMER_H__
+
+#include "../App/config.h"
+
+/* ====== Timer0:自由运行硬件时间戳基准 ====== */
+
+/**
+ * @brief 初始化 Timer0 为常驻自由运行计数器
+ * @details 配置为 Mode 0 (STC 上为 16 位不停表计数),12T 模式下 24MHz 晶振对应 1 tick = 0.5us。
+ * 开机启动后永不停止,配合溢出中断 (Timer0_Isr) 把 16 位硬件计数扩展为 32 位软件时间戳,
+ * 供 RF 边沿中断解码等需要高精度时间差测量的场景使用。
+ */
+void Timer0_Init(void);
+
+/**
+ * @brief 获取以 Timer0 为基准的 32 位自由运行时间戳 (单位: 0.5us tick)
+ * @details 内部处理了读取瞬间恰逢硬件计数器溢出的读数撕裂问题。
+ */
+u32 Timer0_GetTimestamp(void);
+
+/* ====== Timer1:系统 1ms 基准滴答 ====== */
+
+/**
+ * @brief 初始化 Timer1 定时器,生成系统基准时钟 (1ms 中断)
+ * @details 配置定时器 1 为 16 位自动重装载模式,设定中断周期为 1.0 毫秒,
+ * 开启 Timer1 中断并启动计数,同时在此统一开启全局中断允许 (EA = 1)。
+ */
+void Timer1_Init(void);
+
+#endif // __TIMER_H__
diff --git a/woke-order/wo-00需要完成的功能 b/work-order/wo-00需要完成的功能
similarity index 100%
rename from woke-order/wo-00需要完成的功能
rename to work-order/wo-00需要完成的功能
diff --git a/woke-order/wo-01 b/work-order/wo-01
similarity index 100%
rename from woke-order/wo-01
rename to work-order/wo-01
diff --git a/woke-order/wo-02 b/work-order/wo-02
similarity index 100%
rename from woke-order/wo-02
rename to work-order/wo-02
diff --git a/woke-order/wo-03 b/work-order/wo-03
similarity index 100%
rename from woke-order/wo-03
rename to work-order/wo-03
diff --git a/work-order/wo-04 b/work-order/wo-04
new file mode 100644
index 0000000..9030a2c
--- /dev/null
+++ b/work-order/wo-04
@@ -0,0 +1,162 @@
+# 📋 工单 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.6 实机验证发现的问题与修复:P3.6 悬空引发中断风暴,导致开机卡死
+
+用户实测反馈:改动后开机背光闪一下就灭,按住开机键不放背光反复闪烁。排查确认根因:
+
+* 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()` 休眠前为了省电特意把这个上拉关掉了,唤醒后要重新打开)。
+
+## 六、阶段 1 实现备注
+
+* 语言字节与传感器数据库共用同一 Flash 扇区(偏移地址为 `sizeof(sensor_list)`,当前为 352,扇区剩余空间足够),复用现有 `Load_Database`/`Save_Database`,未新增独立扇区。
+* 未做二级选择子菜单:主菜单第 1 项选中"语言"时直接按■原地切换 FR/EN,立即保存并用新语言重绘当前菜单。
+* 首次开机(Flash 从未写入过语言字节,读到擦除态 `0xFF`)时 `Lang_Init` 会自动回退为 `LANG_FR`,不会出现非法枚举值。
+* 本阶段仅替换界面文案与新增语言状态管理,未改动任何按键状态机、事件队列或射频协议相关代码逻辑。
+* 尚未实机编译烧录验证,第四节验收标准仍需在真实硬件上逐项确认。
diff --git a/wristband.uvopt b/wristband.uvopt
index b0daae9..6e2fbd8 100644
--- a/wristband.uvopt
+++ b/wristband.uvopt
@@ -73,7 +73,7 @@
0
- 0
+ 1
0
1
@@ -164,7 +164,7 @@
App
- 0
+ 1
0
0
0
@@ -235,6 +235,18 @@
0
0
0
+ .\App\language.c
+ language.c
+ 0
+ 0
+
+
+ 1
+ 7
+ 1
+ 0
+ 0
+ 0
.\App\ui.c
ui.c
0
@@ -242,7 +254,7 @@
1
- 7
+ 8
1
0
0
@@ -254,7 +266,7 @@
1
- 8
+ 9
1
0
0
@@ -266,7 +278,7 @@
1
- 9
+ 10
1
0
0
@@ -278,7 +290,7 @@
1
- 10
+ 11
1
0
0
@@ -290,7 +302,7 @@
1
- 11
+ 12
1
0
0
@@ -302,7 +314,7 @@
1
- 12
+ 13
1
0
0
@@ -314,7 +326,7 @@
1
- 13
+ 14
1
0
0
@@ -326,7 +338,7 @@
1
- 14
+ 15
1
0
0
@@ -340,13 +352,13 @@
Drivers
- 0
+ 1
0
0
0
2
- 15
+ 16
1
0
0
@@ -358,7 +370,7 @@
2
- 16
+ 17
1
0
0
@@ -370,7 +382,19 @@
2
- 17
+ 18
+ 1
+ 0
+ 0
+ 0
+ .\Drivers\timer.c
+ timer.c
+ 0
+ 0
+
+
+ 2
+ 19
1
0
0
@@ -382,7 +406,7 @@
2
- 18
+ 20
1
0
0
diff --git a/wristband.uvproj b/wristband.uvproj
index 11181e2..8325f0c 100644
--- a/wristband.uvproj
+++ b/wristband.uvproj
@@ -349,6 +349,11 @@
1
.\App\database.c
+
+ language.c
+ 1
+ .\App\language.c
+
ui.c
1
@@ -409,6 +414,11 @@
1
.\Drivers\rf.c
+
+ timer.c
+ 1
+ .\Drivers\timer.c
+
adc.c
1