Compare commits

..

29 Commits

Author SHA1 Message Date
edisondeng
0ef5991d33 feat(config): add USE_PCF8563 build switch to run without the RTC chip
Some PCB variants may not populate the PCF8563. Guards RTC init/read
in HomeApp_OnStart(), the WriteDateTime() save in SetClockApp, and the
I2C/RTC diagnostic scans in Self_Test() behind #if USE_PCF8563. When
disabled, the firmware runs entirely on the Timer1-driven software
clock; date/time just won't survive a power cycle without the chip.
2026-07-31 19:54:18 +08:00
edisondeng
6022f0a409 feat(ui): extend set_clock_app to edit day/month/year, not just time
time_edit_field now cycles through 5 fields (hour/min/day/month/year)
via long-press CONFIRM. Up/down adjusts whichever field is focused,
with day clamped to Days_In_Month() when month/year changes, and
Clock_RecalcWeekday() called after every date edit so the weekday
label stays in sync. UI_UpdateSetTimeDigit() now also refreshes the
weekday abbreviation when editing day/month/year, since the computed
weekday can change independently of which digit was actually touched.
Save writes the full date+weekday via PCF8563_WriteDateTime(). Also
removes PCF8563_ReadTime()/WriteTime(), now unused since every caller
was updated to the DateTime variants directly.
2026-07-31 19:53:56 +08:00
edisondeng
6baf93c550 feat(ui): show real weekday/day/month on HOME screen instead of placeholder
UI_ShowClockPage()/UI_ShowSetTimePage() now take weekday/day/month(/year)
and build the date bar dynamically via Build_DateString() instead of
the fixed placeholder string. app_home.c reads the full date from RTC
via PCF8563_ReadDateTime() and recalculates weekday since the RTC's
stored date may differ from the software clock's. config.h exposes
current_day/month/year/weekday alongside the existing time fields so
main.c's debug commands don't need a separate include.
2026-07-31 19:43:43 +08:00
edisondeng
81ebf7e302 feat(i18n): add weekday abbreviation strings, remove static date text
Replaces the fixed placeholder STR_CLOCK_DATE ("VEN 10-07"/"FRI 10-07")
with 7 weekday abbreviation strings (FR/EN), ordered Sunday-Saturday to
align with current_weekday's 0-6 convention for direct indexing
(STR_WEEKDAY_SUN + current_weekday).
2026-07-31 19:35:21 +08:00
edisondeng
cb37b1deea feat(clock): add day/month/year rollover and Zeller weekday calc
Extends Clock_IncMS()'s existing sec->min->hour cascade to also roll
day->month->year (with leap-year Feb 29 via Days_In_Month()), and adds
Clock_RecalcWeekday() computing current_weekday via Zeller's congruence
whenever the date changes, rather than trusting PCF8563's own weekday
register (which would drift if the date is edited manually). Verified
the formula against 2026-07-31, a known Friday.
2026-07-31 19:32:44 +08:00
edisondeng
1146bade70 feat(rtc): add PCF8563 full date read/write (day/month/year/weekday)
PCF8563's second/minute/hour/day/weekday/month/year registers are
contiguous (0x02-0x08), so PCF8563_ReadDateTime() bursts all 7 bytes
in one transaction. PCF8563_ReadTime() becomes a thin wrapper that
discards the date fields. WriteTime() is kept as its own 3-byte-only
implementation (writing only sec/min/hour) rather than routed through
the new WriteDateTime(), since forcing it through a 7-byte write would
require synthesizing date values and risk corrupting the stored date;
WriteDateTime() is added as a separate full 7-byte write for callers
that actually have real date+weekday values to persist.
2026-07-31 19:30:02 +08:00
edisondeng
4a55c1f9eb docs: add wo-07 to add real year/month/day/weekday support 2026-07-31 19:26:55 +08:00
edisondeng
983b0928dd docs: mark wo-06 code-complete, pending hardware verification 2026-07-31 19:14:43 +08:00
edisondeng
dac2ea4335 fix(ui): partial-refresh time-set digit blink instead of full screen
UI_ShowSetTimePage() cleared and redrew the entire screen on every
500ms blink toggle and on every up/down value change, causing a
full-screen flash instead of just the intended hour/minute digit
blinking. Split out UI_UpdateSetTimeDigit() which only repaints the
32x32px digit region for the field being edited; the full page draw
is now only used on entry. Field-switch (long-press CONFIRM) refreshes
both digit regions to clear any stale blanked-out state.
2026-07-31 19:14:14 +08:00
edisondeng
d4b89c9336 docs: add wo-06 for time-set screen full-screen flicker fix 2026-07-31 19:10:31 +08:00
edisondeng
05f05177de chore: sync Keil uVision IDE session state 2026-07-31 19:10:00 +08:00
edisondeng
2a83079007 refactor(event): replace global 60s inactivity timer with per-second tick event
Adds KEY_EVENT_SECOND_TICK, generated once per real second from
Clock_IncMS() via a new second_tick_flag and pushed through the normal
event queue, so every foreground App's onEvent() receives it and
decides independently whether to act. Removes the old global
inactivity_timer (60s sleep from anywhere) entirely. HomeApp now
accumulates its own idle-second counter and sleeps after 10s idle
specifically while on the home screen; MenuApp's existing 10s
auto-close is converted from onRun() polling to the same tick+reset
pattern for consistency. Any non-tick key event resets each app's own
counter to 0.
2026-07-31 18:49:03 +08:00
edisondeng
cbd480eea3 docs(main): document 24MHz ISP burn requirement at top of main.c
Confirms via code inspection that the firmware isn't uniformly driven
by MAIN_Fosc: Uart1_Init()'s Timer2 baud-rate reload and rf.c/timer.c's
tick-to-microsecond conversion assume an actual 24MHz clock as
hardcoded constants, not values computed from MAIN_Fosc. Flashing at a
different STC-ISP clock setting would silently break UART baud rate
and RF timing without an obvious compile error.
2026-07-31 18:28:52 +08:00
edisondeng
f9bea08375 docs: mark wo-05 all 6 items code-complete, pending hardware test 2026-07-31 18:15:24 +08:00
edisondeng
79d604a387 fix(sleep): pin P3.6 to a fixed rising-edge trigger before Power Down
RF_HandleEdgeInterrupt()'s software ping-pong toggle can leave P3IM0
parked in either edge direction while awake; Enter_Low_Power_Sleep()
never reset it, so sleep entry could randomly land on a direction that
misses the wake edge. Per the datasheet truth table (WO-04 2.9), only
edge modes support power-down wake, not level modes, so this forces a
known rising-edge state (matching RF_Init()'s own boot default) instead
of switching to level-triggered as originally requested. wo-05 updated
to record the deviation and rationale.
2026-07-31 18:14:22 +08:00
edisondeng
ae235d3c51 feat(menu): auto-close main menu after 10s idle
menu_app now tracks the last key-activity timestamp (via current_sec,
same wraparound-safe pattern already used in pair_app) and switches
back to home_app if no key event arrives for 10 seconds, using the
AppManager's existing per-frame onRun() callback as the check point.
2026-07-31 18:11:55 +08:00
edisondeng
a771d1a970 fix(language): default to English instead of French
Both the static initializer and Lang_Init()'s erased-flash fallback
now default to LANG_EN, so first boot / never-written flash shows
English instead of French.
2026-07-31 18:10:22 +08:00
edisondeng
ec556fdd2a fix(boot): hold backlight off until first frame is drawn
SGM_CTRL was turned on at the very start of LCD_Init(), before the
ST7789V register sequence and first frame ran, showing garbage/blank
content briefly on every boot. GPIO_Init() now forces backlight off at
power-on, LCD_Init() no longer touches it, and main() only turns it on
right before entering the main loop (after HomeApp's first frame is
drawn). Wakeup_Restore()'s existing on-then-reinit sequence is
untouched since it sets the level itself before calling LCD_Init().
2026-07-31 18:09:39 +08:00
edisondeng
56b92a8ede refactor(app): App-per-screen navigation, remove menu_level entirely
Splits the ad-hoc menu_level state machine into standalone Apps with a
uniform parent/child exit convention (long-press CONFIRM -> parent):
clock_app renamed to home_app (boot-default), new set_clock_app
extracted from home_app's time-edit mode, new ipc_bind_app extracted
from menu_app's old level-2 screen, and the sensor-type selection step
merged into pair_app as its new initial state. menu_app now only holds
the 4-item top-level list (language toggle inline). Also makes both
menu_app and pair_app's up/down navigation cyclic (wraps at both ends).
2026-07-31 18:05:23 +08:00
edisondeng
655d29bbe1 docs: rename clock_app to home_app, add set_clock_app in wo-05 plan
Confirms clock_app is the boot-default app (main.c:280) and should be
renamed home_app; time editing becomes its own set_clock_app instead
of time_set_app. Drops all remaining menu_level wording, and
cross-references items 1/2 to the renamed APP_ID_HOME/home_app.
2026-07-31 17:52:42 +08:00
edisondeng
653fc85ae5 docs: redesign wo-05 item 3 as App-based menu navigation refactor
Replace the ad-hoc menu_level state machine plan with a uniform
App-per-screen model (home_app/menu_app/time_set_app/pair_app/
ipc_bind_app), each child App exiting to its own parent via long-press
CONFIRM. Folds the sensor-type selection screen into pair_app as a new
initial state, and updates item 5's cyclic-navigation notes to match.
2026-07-31 17:48:33 +08:00
edisondeng
3000780df5 docs: add wo-05 with 6 small UX/wake-reliability fixes to plan
Lists boot-flicker backlight timing, menu idle auto-close, submenu
long-press-confirm exit, default language, cyclic menu navigation, and
P3.6 sleep-wake trigger reliability, each with a brief implementation
approach based on current code.
2026-07-31 17:10:37 +08:00
edisondeng
08dbdabfcb docs(rf): confirm edge-triggered EV1527 decode verified on hardware
Multiple real transmissions (Data=08/04/02/01) decoded correctly and
stably with the ping-pong single-edge trigger fix, no interrupt storm
or crash observed. Closes out the phase-2 RF decode root-cause saga.
2026-07-31 16:52:32 +08:00
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
edisondeng
29194a4f5d debug(rf): revert to polling EV1527_Decode, ISR does pure edge counting
Diagnostic experiment: restore the commit d8df5d4 polling decode
algorithm/thresholds unchanged, while P3.6 ISR now only increments an
edge counter (no decode state machine). Successful decodes print the
edge-count delta over the decode window to compare against the
EV1527 theoretical 50-edges-per-frame, to validate/refute the
edge-interrupt glitch-noise hypothesis.
2026-07-31 15:53:15 +08:00
edisondeng
18d272aac9 chore: sync Keil uVision IDE session state
wristband.uvopt tracks debug/breakpoint/window layout state written by
the Keil IDE; no source or build behavior change.
2026-07-31 15:52:57 +08:00
edisondeng
767aa14086 fix(timer): compensate Timer0_GetTimestamp for pending TF0 overflow
TH0/TL0 torn-read protection didn't cover the case where the hardware
counter already wrapped but Timer0_Isr hasn't run yet to bump
timer0_ovf_count, making the timestamp appear to jump backward by one
overflow period (~32.768ms). Read TF0 under a brief EA guard and treat
a pending-but-unserviced overflow as ovf+1 without clearing it.
2026-07-31 15:52:50 +08:00
edisondeng
3c896ec972 fix(rf): register isr_jump.asm and redirect Port3 vector 40
App/isr_jump.asm was never added to wristband.uvproj, so the Port0/Port2/Port3
hardware vector redirects it defines were never assembled. Port3_Isr (declared
as interrupt 16) never actually ran on real P3 port events, causing the
edge-interrupt-driven RF decode to silently never fire. Adds the missing
CSEG/LJMP redirect for vector 40 and registers the file as FileType=2.
2026-07-31 15:52:43 +08:00
edisondeng
a7a2cc9465 暂存 2026-07-31 13:57:06 +08:00
42 changed files with 2194 additions and 805 deletions

View File

@@ -175,7 +175,7 @@ static void AlarmApp_onClose(void)
static EventResult AlarmApp_onEvent(SystemEvent *evt)
{
if (evt->key_event != KEY_EVENT_NONE) {
AppManager_SwitchToForeground(&clock_app); // 清除报警并切回时钟待机页
AppManager_SwitchToForeground(&home_app); // 清除报警并切回时钟待机页
return EVENT_HANDLED;
}
return EVENT_IGNORED;

View File

@@ -1,204 +0,0 @@
#include "app_manager.h"
#include "clock.h"
#include "xtell.h"
#include "ui.h"
#include "rgb.h"
#include "event.h"
#include "system.h"
#include "../Drivers/rf.h"
#include "../Drivers/lcd.h"
#include "../Drivers/pcf8563.h"
// 外部引用的全局系统状态变量
extern SystemState current_state;
extern volatile u16 ms_tick;
// 待机时钟应用内特有的编辑时间私有变量
static u8 time_edit_active = 0; // 标识当前是否正在编辑修改时间中 (1:是, 0:否)
static u8 time_edit_field = 1; // 标识当前聚焦在哪一个参数上进行微调 (1:修改小时, 2:修改分钟)
static u8 time_edit_blink_on = 1; // 时钟修改时光标闪烁周期状态位
// 声明本应用的生命周期回调函数
static void ClockApp_OnStart(void);
static void ClockApp_onRun(void);
static void ClockApp_onClose(void);
static EventResult ClockApp_onEvent(SystemEvent *evt);
// 实体化定义待机时钟应用控制块
WristbandApp clock_app = {
APP_ID_CLOCK,
"Clock",
ClockApp_OnStart,
ClockApp_onRun,
ClockApp_onClose,
ClockApp_onEvent
};
/**
* @brief 触发启动时间设置状态机,准备修改时钟
*/
void Start_Time_Edit(void)
{
time_edit_active = 1;
time_edit_field = 1;
time_edit_blink_on = 1;
current_state = STATE_SET_TIME; // 系统切入时间设置状态
// FCOB 幻彩灯条双灯珠均显示蓝色常亮 (R=0, G=0, B=180),进行模式视觉区分
RGB_Send(0, 0, 180, 0, 0, 180);
// 振动马达单次短震 50 毫秒,提供触觉反馈
MOTOR = 1;
Delay_ms(50);
MOTOR = 0;
// 渲染显示时间编辑界面,默认高亮编辑小时字段,消隐位开启
UI_ShowSetTimePage(current_hour, current_min, time_edit_field, 1);
XTELL_LOG("[ClockApp] Time edit started.\r\n");
}
/**
* @brief ClockApp 启动回调
*/
static void ClockApp_OnStart(void)
{
time_edit_active = 0;
current_state = STATE_NORMAL; // 系统切入正常待机状态
RF_SetMode(1); // 开启天线并置为接收模式,监听无线探测器
// 尝试从 PCF8563 RTC 芯片中读取硬件时间 (具备非阻塞防卡死保护)
if (PCF8563_Init()) {
PCF8563_ReadTime(&current_hour, &current_min, &current_sec);
XTELL_LOG("[ClockApp] RTC Hardware Synced OK!\r\n");
} else {
XTELL_LOG("[ClockApp] RTC Hardware Offline, using system clock.\r\n");
}
// 绘制并渲染待机时钟主页面画面
UI_ShowClockPage(current_state, current_hour, current_min);
// 关断/熄灭 FCOB 双灯条
RGB_Send(0, 0, 0, 0, 0, 0);
XTELL_LOG("[ClockApp] Started\r\n");
}
/**
* @brief ClockApp 轮询主回调
*/
static void ClockApp_onRun(void)
{
if (time_edit_active)
{
// ====== 时间设置闪烁动画 (周期 500ms) ======
u8 blink = (ms_tick / 500) % 2;
if (blink != time_edit_blink_on)
{
time_edit_blink_on = blink;
// 每次闪烁状态发生变化,重新刷写屏幕数值
UI_ShowSetTimePage(current_hour, current_min, time_edit_field, time_edit_blink_on);
}
}
else
{
// 软时钟发生分钟改变,需要通知屏幕重画时钟主页
if (clock_updated) {
clock_updated = 0;
UI_ShowClockPage(current_state, current_hour, current_min);
}
}
}
/**
* @brief ClockApp 退出清理回调
*/
static void ClockApp_onClose(void)
{
time_edit_active = 0;
RGB_Send(0, 0, 0, 0, 0, 0); // 关闭灯条
XTELL_LOG("[ClockApp] Closed\r\n");
}
/**
* @brief ClockApp 接收事件处理回调 (按键导航与布撤防交互)
*/
static EventResult ClockApp_onEvent(SystemEvent *evt)
{
if (time_edit_active)
{
// ====== 处于时间设置状态下的按键拦截处理机 ======
if (evt->key_event == KEY_EVENT_UP_CLICK)
{
// 短按 ▲ 键增加数值 (小时/分钟循环相加)
if (time_edit_field == 1) {
current_hour = (current_hour + 1) % 24;
} else {
current_min = (current_min + 1) % 60;
}
UI_ShowSetTimePage(current_hour, current_min, time_edit_field, 1);
return EVENT_HANDLED; // 吞掉并消化事件
}
if (evt->key_event == KEY_EVENT_DOWN_CLICK)
{
// 短按 ▼ 键减小数值
if (time_edit_field == 1) {
current_hour = (current_hour == 0) ? 23 : (current_hour - 1);
} else {
current_min = (current_min == 0) ? 59 : (current_min - 1);
}
UI_ShowSetTimePage(current_hour, current_min, time_edit_field, 1);
return EVENT_HANDLED;
}
if (evt->key_event == KEY_EVENT_CONFIRM_LONG)
{
// 长按 ■ 确认键切换调节字段 (在修改小时与修改分钟间跳转)
if (time_edit_field == 1) {
time_edit_field = 2;
} else {
time_edit_field = 1;
}
UI_ShowSetTimePage(current_hour, current_min, time_edit_field, 1);
return EVENT_HANDLED;
}
if (evt->key_event == KEY_EVENT_CONFIRM_CLICK)
{
// 短按 ■ 确认键保存最新时钟配置并退出编辑模式
time_edit_active = 0;
current_state = STATE_NORMAL;
RGB_Send(0, 0, 0, 0, 0, 0); // 熄灭蓝色指示灯
// 同步将最新时间 (hour, min, 0秒) 写入 PCF8563 芯片寄存器
PCF8563_WriteTime(current_hour, current_min, 0);
// 退出短振反馈
MOTOR = 1; Delay_ms(50); MOTOR = 0;
UI_ShowClockPage(current_state, current_hour, current_min);
return EVENT_HANDLED;
}
return EVENT_HANDLED; // 在时间设置模式下屏蔽拦截其他一切无关按键事件
}
// ====== 正常时钟待机状态下的按键拦截处理 ======
// 1. 长按 ■ 确认键,打开设置主菜单 MenuApp
if (evt->key_event == KEY_EVENT_CONFIRM_LONG) {
AppManager_SwitchToForeground(&menu_app);
return EVENT_HANDLED;
}
// 2. 长按 ▲ 键,一键切换系统的 布防 (ARMED) 和 撤防 (DISARMED) 状态
if (evt->key_event == KEY_EVENT_UP_LONG) {
if (current_state == STATE_ARMED) {
current_state = STATE_DISARMED;
} else {
current_state = STATE_ARMED;
}
// 布撤防状态切换成功后进行 100ms 短震确认,并重新刷写时钟待机页
MOTOR = 1; Delay_ms(100); MOTOR = 0;
UI_ShowClockPage(current_state, current_hour, current_min);
return EVENT_HANDLED;
}
return EVENT_IGNORED; // 其他未被关心的事件交由后台应用或丢弃
}

130
App/app_home.c Normal file
View File

@@ -0,0 +1,130 @@
#include "app_manager.h"
#include "clock.h"
#include "xtell.h"
#include "ui.h"
#include "rgb.h"
#include "event.h"
#include "system.h"
#include "../Drivers/rf.h"
#include "../Drivers/lcd.h"
#include "../Drivers/pcf8563.h"
// 外部引用的全局系统状态变量
extern SystemState current_state;
extern volatile u16 ms_tick;
// 待机时钟界面闲置多久自动进入低功耗休眠的门限 (秒)
#define HOME_IDLE_SLEEP_SEC 10
// 本应用私有变量:累计未按任何键的秒数 (由 KEY_EVENT_SECOND_TICK 驱动,任意按键事件清零)
static u8 home_idle_sec = 0;
// 声明本应用的生命周期回调函数
static void HomeApp_OnStart(void);
static void HomeApp_onRun(void);
static void HomeApp_onClose(void);
static EventResult HomeApp_onEvent(SystemEvent *evt);
// 实体化定义待机时钟主页应用控制块 (开机默认显示的首屏)
WristbandApp home_app = {
APP_ID_HOME,
"Home",
HomeApp_OnStart,
HomeApp_onRun,
HomeApp_onClose,
HomeApp_onEvent
};
/**
* @brief HomeApp 启动回调
*/
static void HomeApp_OnStart(void)
{
home_idle_sec = 0; // 重新进入待机主页 (开机/从菜单退回/休眠唤醒) 都清零闲置计数
current_state = STATE_NORMAL; // 系统切入正常待机状态
RF_SetMode(1); // 开启天线并置为接收模式,监听无线探测器
#if USE_PCF8563
// 尝试从 PCF8563 RTC 芯片中读取硬件时间与日期 (具备非阻塞防卡死保护)
if (PCF8563_Init()) {
PCF8563_ReadDateTime(&current_hour, &current_min, &current_sec, &current_day, &current_month, &current_year);
Clock_RecalcWeekday(); // 读回的日期可能与软时钟不同,重新算一次星期
XTELL_LOG("[HomeApp] RTC Hardware Synced OK!\r\n");
} else {
XTELL_LOG("[HomeApp] RTC Hardware Offline, using system clock.\r\n");
}
#else
XTELL_LOG("[HomeApp] USE_PCF8563=0, running on software clock only.\r\n");
#endif
// 绘制并渲染待机时钟主页面画面
UI_ShowClockPage(current_state, current_hour, current_min, current_weekday, current_day, current_month);
// 关断/熄灭 FCOB 双灯条
RGB_Send(0, 0, 0, 0, 0, 0);
XTELL_LOG("[HomeApp] Started\r\n");
}
/**
* @brief HomeApp 轮询主回调
*/
static void HomeApp_onRun(void)
{
// 软时钟发生分钟改变,需要通知屏幕重画时钟主页
if (clock_updated) {
clock_updated = 0;
UI_ShowClockPage(current_state, current_hour, current_min, current_weekday, current_day, current_month);
}
}
/**
* @brief HomeApp 退出清理回调
*/
static void HomeApp_onClose(void)
{
RGB_Send(0, 0, 0, 0, 0, 0); // 关闭灯条
XTELL_LOG("[HomeApp] Closed\r\n");
}
/**
* @brief HomeApp 接收事件处理回调 (按键导航与布撤防交互)
*/
static EventResult HomeApp_onEvent(SystemEvent *evt)
{
// 0. 整秒滴答:累计闲置秒数,达到门限就进入低功耗休眠
if (evt->key_event == KEY_EVENT_SECOND_TICK) {
home_idle_sec++;
if (home_idle_sec >= HOME_IDLE_SLEEP_SEC) {
home_idle_sec = 0;
current_state = STATE_SLEEP; // 系统置为休眠状态
Enter_Low_Power_Sleep(); // 阻塞函数MCU 在此挂起,唤醒后自动继续往下执行
AppManager_SwitchToForeground(&home_app); // 强制重新走一遍 OnStart刷新画面和背光
}
return EVENT_HANDLED;
}
// 收到任意其它按键事件,说明用户仍在活动,清零闲置计数
home_idle_sec = 0;
// 1. 长按 ■ 确认键,打开设置主菜单 MenuApp
if (evt->key_event == KEY_EVENT_CONFIRM_LONG) {
AppManager_SwitchToForeground(&menu_app);
return EVENT_HANDLED;
}
// 2. 长按 ▲ 键,一键切换系统的 布防 (ARMED) 和 撤防 (DISARMED) 状态
if (evt->key_event == KEY_EVENT_UP_LONG) {
if (current_state == STATE_ARMED) {
current_state = STATE_DISARMED;
} else {
current_state = STATE_ARMED;
}
// 布撤防状态切换成功后进行 100ms 短震确认,并重新刷写时钟待机页
MOTOR = 1; Delay_ms(100); MOTOR = 0;
UI_ShowClockPage(current_state, current_hour, current_min, current_weekday, current_day, current_month);
return EVENT_HANDLED;
}
return EVENT_IGNORED; // 其他未被关心的事件交由后台应用或丢弃
}

85
App/app_ipc_bind.c Normal file
View File

@@ -0,0 +1,85 @@
#include "app_manager.h"
#include "xtell.h"
#include "ui.h"
#include "rgb.h"
#include "event.h"
#include "system.h"
#include "../Drivers/rf.h"
// 外部引用的全局系统状态变量
extern SystemState current_state;
extern volatile u16 ms_tick;
// 本应用私有变量
static u16 last_bind_tx_ms = 0; // 记录上一次绑定发射的时间戳
// 声明本应用的生命周期回调函数
static void IpcBindApp_OnStart(void);
static void IpcBindApp_onRun(void);
static void IpcBindApp_onClose(void);
static EventResult IpcBindApp_onEvent(SystemEvent *evt);
// 实体化定义摄像头一键绑定应用控制块 (从 menu_app 的"LIAISON"选项进入)
WristbandApp ipc_bind_app = {
APP_ID_IPC_BIND,
"IpcBind",
IpcBindApp_OnStart,
IpcBindApp_onRun,
IpcBindApp_onClose,
IpcBindApp_onEvent
};
/**
* @brief IpcBindApp 启动回调
*/
static void IpcBindApp_OnStart(void)
{
last_bind_tx_ms = ms_tick; // 记录初始发射时间
current_state = STATE_IPC_BIND; // 系统设为 IPC 绑定状态
UI_ShowBindingPage(CLONED_ADDR); // 绘制相机绑定信息页
XTELL_LOG("[IpcBindApp] Started\r\n");
}
/**
* @brief IpcBindApp 轮询主回调
*/
static void IpcBindApp_onRun(void)
{
// ====== 控制青色双灯珠同步呼吸效果 (呼吸周期 1000ms) ======
u16 breath = ms_tick % 1000;
u8 val = (breath < 500) ? (breath * 2 / 5) : ((1000 - breath) * 2 / 5); // 亮度映射 0~200
RGB_Send(0, val, val, 0, val, val); // 渲染青色 (G+B)
// ====== 控制无线发射,每 500ms 广播一次配对数据 ======
if (ms_tick - last_bind_tx_ms >= 500 || ms_tick < last_bind_tx_ms)
{
last_bind_tx_ms = ms_tick; // 备份当前时间戳
RF_SetMode(2); // 暂时切换射频芯片为发射调制模式
EV1527_Transmit(CLONED_ADDR, 0x01); // 调制发射手环克隆 ID + 绑定数据码 0x01
RF_SetMode(0); // 切回关闭状态,避免产生噪声
XTELL_LOG("[IpcBindApp] Binding frame emitted\r\n");
}
}
/**
* @brief IpcBindApp 退出清理回调
*/
static void IpcBindApp_onClose(void)
{
RGB_Send(0, 0, 0, 0, 0, 0); // 熄灭 RGB
XTELL_LOG("[IpcBindApp] Closed\r\n");
}
/**
* @brief IpcBindApp 接收按键事件处理回调
*/
static EventResult IpcBindApp_onEvent(SystemEvent *evt)
{
// 长按 ■ 确认键,取消绑定,退出发射并返回主菜单 (父 App)
if (evt->key_event == KEY_EVENT_CONFIRM_LONG) {
current_state = STATE_PAIR_MENU;
AppManager_SwitchToForeground(&menu_app);
return EVENT_HANDLED;
}
return EVENT_IGNORED;
}

View File

@@ -180,9 +180,11 @@ u8 AppManager_GetActiveAppID(void)
// 供遗留代码兼容性引用的 APIStartApp 映射至 SwitchToForeground 动态方式
void AppManager_StartApp(AppID app_id)
{
if (app_id == APP_ID_CLOCK) AppManager_SwitchToForeground(&clock_app);
if (app_id == APP_ID_HOME) AppManager_SwitchToForeground(&home_app);
else if (app_id == APP_ID_MENU) AppManager_SwitchToForeground(&menu_app);
else if (app_id == APP_ID_SET_CLOCK) AppManager_SwitchToForeground(&set_clock_app);
else if (app_id == APP_ID_PAIR) AppManager_SwitchToForeground(&pair_app);
else if (app_id == APP_ID_IPC_BIND) AppManager_SwitchToForeground(&ipc_bind_app);
else if (app_id == APP_ID_ALARM) AppManager_SwitchToForeground(&alarm_app);
else if (app_id == APP_ID_SOS) AppManager_SwitchToForeground(&sos_app);
}

View File

@@ -67,9 +67,11 @@ u8 AppManager_GetActiveAppID(void);
void AppManager_StartApp(AppID app_id);
// ====== 系统中各独立 App 实例的外包声明 (各 App 内部进行实体定义) ======
extern WristbandApp clock_app;
extern WristbandApp home_app;
extern WristbandApp menu_app;
extern WristbandApp set_clock_app;
extern WristbandApp pair_app;
extern WristbandApp ipc_bind_app;
extern WristbandApp alarm_app;
extern WristbandApp sos_app;
extern WristbandApp rf_monitor_app; // 后台射频监控应用

View File

@@ -4,42 +4,31 @@
#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: 水浸探测器
};
// 外部全局变量引用
extern SystemState current_state;
extern u8 menu_select;
extern u8 captured_type;
extern volatile u16 ms_tick;
// 菜单应用私有变量
static u8 menu_level = 0; // 0-主菜单, 1-二级传感器选择菜单, 2-IPC绑定一键发射
static u16 last_bind_tx_ms = 0; // 记录上一次绑定发射的时间戳
// 菜单闲置自动关闭超时 (秒):无任何按键操作达到这个时长,自动退出菜单返回时钟主页
#define MENU_IDLE_TIMEOUT_SEC 10
// 引入外部时钟调节接口
extern void Start_Time_Edit(void);
// 本应用私有变量:累计未按任何键的秒数 (由 KEY_EVENT_SECOND_TICK 驱动,任意按键事件清零)
static u8 menu_idle_sec = 0;
// 声明本应用的生命周期回调函数
static void MenuApp_OnStart(void);
static void MenuApp_onRun(void);
static void MenuApp_onClose(void);
static EventResult MenuApp_onEvent(SystemEvent *evt);
// 实体化定义菜单应用控制块
// 实体化定义菜单应用控制块 (顶层 4 项列表:语言切换/时间设置/传感器配对/摄像头绑定)
WristbandApp menu_app = {
APP_ID_MENU,
"Menu",
MenuApp_OnStart,
MenuApp_onRun,
NULL,
MenuApp_onClose,
MenuApp_onEvent
};
@@ -49,148 +38,81 @@ WristbandApp menu_app = {
*/
static void MenuApp_OnStart(void)
{
menu_level = 0;
menu_select = 0;
menu_idle_sec = 0;
current_state = STATE_PAIR_MENU; // 系统运行状态设为对码配置菜单
RF_SetMode(0); // 关闭射频接收芯片,防止接收杂波干扰屏幕绘制
UI_ShowMainMenu(menu_select); // 初始化绘制一级主菜单
UI_ShowMainMenu(menu_select); // 绘制主菜单
RGB_Send(0, 0, 0, 0, 0, 0); // 熄灭所有灯条
XTELL_LOG("[MenuApp] Started\r\n");
}
/**
* @brief MenuApp 轮询主回调
*/
static void MenuApp_onRun(void)
{
if (menu_level == 2)
{
// ====== 控制青色双灯珠同步呼吸效果 (呼吸周期 1000ms) ======
u16 breath = ms_tick % 1000;
u8 val = (breath < 500) ? (breath * 2 / 5) : ((1000 - breath) * 2 / 5); // 亮度映射 0~200
RGB_Send(0, val, val, 0, val, val); // 渲染青色 (G+B)
// ====== 控制无线发射,每 500ms 广播一次配对数据 ======
if (ms_tick - last_bind_tx_ms >= 500 || ms_tick < last_bind_tx_ms)
{
last_bind_tx_ms = ms_tick; // 备份当前时间戳
RF_SetMode(2); // 暂时切换射频芯片为发射调制模式
EV1527_Transmit(CLONED_ADDR, 0x01); // 调制发射手环克隆 ID + 绑定数据码 0x01
RF_SetMode(0); // 切回关闭状态,避免产生噪声
XTELL_LOG("[MenuApp] IPC Binding frame emitted\r\n");
}
}
}
/**
* @brief MenuApp 退出清理回调
*/
static void MenuApp_onClose(void)
{
menu_level = 0;
RGB_Send(0, 0, 0, 0, 0, 0); // 熄灭 RGB
RF_SetMode(1); // 重新开启无线接收模式
XTELL_LOG("[MenuApp] Closed\r\n");
}
/**
* @brief MenuApp 接收按键与控制事件回调
* @brief MenuApp 接收按键与控制事件回调 (0-语言切换, 1-HORLOGE, 2-APPAIRAGE, 3-LIAISON)
*/
static EventResult MenuApp_onEvent(SystemEvent *evt)
{
if (menu_level == 0)
{
// ====== 处于一级主菜单层 (0-HORLOGE, 1-APPAIRAGE, 2-LIAISON) ======
if (evt->key_event == KEY_EVENT_UP_CLICK) {
// 短按 ▲ 键向上移动焦点
if (menu_select > 0) {
menu_select--;
UI_ShowMainMenu(menu_select);
// 0. 整秒滴答:累计闲置秒数,达到门限就自动关闭菜单退回待机主页
if (evt->key_event == KEY_EVENT_SECOND_TICK) {
menu_idle_sec++;
if (menu_idle_sec >= MENU_IDLE_TIMEOUT_SEC) {
menu_idle_sec = 0;
XTELL_LOG("[MenuApp] Idle timeout, closing.\r\n");
AppManager_SwitchToForeground(&home_app);
}
return EVENT_HANDLED;
}
if (evt->key_event == KEY_EVENT_DOWN_CLICK) {
// 短按 ▼ 键向下移动焦点
if (menu_select < 2) {
menu_select++;
// 收到任意其它按键事件,说明用户仍在操作,清零闲置计数
menu_idle_sec = 0;
if (evt->key_event == KEY_EVENT_UP_CLICK) {
// 短按 ▲ 键向上移动焦点,允许循环切换 (到顶绕到底)
menu_select = (menu_select == 0) ? 3 : (menu_select - 1);
UI_ShowMainMenu(menu_select);
return EVENT_HANDLED;
}
if (evt->key_event == KEY_EVENT_DOWN_CLICK) {
// 短按 ▼ 键向下移动焦点,允许循环切换 (到底绕到顶)
menu_select = (menu_select == 3) ? 0 : (menu_select + 1);
UI_ShowMainMenu(menu_select);
return EVENT_HANDLED;
}
if (evt->key_event == KEY_EVENT_CONFIRM_CLICK) {
// 短按 ■ 确认键,进入选定功能的子页面
if (menu_select == 0) {
// 功能 1设置时间。启动待机时钟应用并调用微调编辑入口
AppManager_SwitchToForeground(&clock_app);
Start_Time_Edit();
// 功能 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) {
// 功能 2传感器学习。进入二级传感器类别选择菜单
menu_level = 1;
menu_select = 0;
UI_ShowPairMenuPage(menu_select); // 绘制二级设备分类页
// 功能 1设置时间。切换到独立的时间设置子 App
AppManager_SwitchToForeground(&set_clock_app);
} else if (menu_select == 2) {
// 功能 3相机配对。进入摄像机一键绑定发射状态
menu_level = 2;
last_bind_tx_ms = ms_tick; // 记录初始发射时间
current_state = STATE_IPC_BIND; // 系统设为 IPC 绑定状态
UI_ShowBindingPage(CLONED_ADDR); // 绘制相机绑定信息页
// 功能 2传感器学习。切换到独立的对码子 App (类型选择是它的第一个状态)
AppManager_SwitchToForeground(&pair_app);
} else if (menu_select == 3) {
// 功能 3相机配对。切换到独立的摄像头绑定子 App
AppManager_SwitchToForeground(&ipc_bind_app);
}
return EVENT_HANDLED;
}
if (evt->key_event == KEY_EVENT_CONFIRM_LONG) {
// 长按 ■ 确认键,退出设置菜单直接返回待机时钟
AppManager_SwitchToForeground(&clock_app);
// 长按 ■ 确认键,退出设置菜单直接返回待机主页
AppManager_SwitchToForeground(&home_app);
return EVENT_HANDLED;
}
}
else if (menu_level == 1)
{
// ====== 处于二级传感器对码类型菜单层 (0-门磁, 1-PIR, 2-烟感, 3-气感, 4-水浸) ======
if (evt->key_event == KEY_EVENT_UP_CLICK) {
// 上移焦点
if (menu_select > 0) {
menu_select--;
UI_ShowPairMenuPage(menu_select);
}
return EVENT_HANDLED;
}
if (evt->key_event == KEY_EVENT_DOWN_CLICK) {
// 下移焦点
if (menu_select < 4) {
menu_select++;
UI_ShowPairMenuPage(menu_select);
}
return EVENT_HANDLED;
}
if (evt->key_event == KEY_EVENT_CONFIRM_CLICK) {
// 确认选择:缓存用户选好的传感器类别,短震反馈并加载对码应用 (PairApp)
captured_type = menu_select;
MOTOR = 1;
Delay_ms(100);
MOTOR = 0;
AppManager_SwitchToForeground(&pair_app); // 切换至 PairApp 对码
return EVENT_HANDLED;
}
if (evt->key_event == KEY_EVENT_DOWN_LONG) {
// 长按 ▼ 键,从二级菜单退回到一级主菜单
menu_level = 0;
menu_select = 1; // 回到 APPAIRAGE 选项上
UI_ShowMainMenu(menu_select);
return EVENT_HANDLED;
}
}
else if (menu_level == 2)
{
// ====== 处于摄像机配对一键绑定发送层 ======
// 长按 ■ 确认键 或 长按 ▼ 键,均表示取消绑定,退出发射并返回一级主菜单
if (evt->key_event == KEY_EVENT_CONFIRM_LONG || evt->key_event == KEY_EVENT_DOWN_LONG) {
menu_level = 0;
menu_select = 2; // 回到 LIAISON 选项上
current_state = STATE_PAIR_MENU;
RGB_Send(0, 0, 0, 0, 0, 0); // 关闭呼吸灯
UI_ShowMainMenu(menu_select);
return EVENT_HANDLED;
}
}
return EVENT_IGNORED;
}

View File

@@ -12,10 +12,21 @@
extern SystemState current_state;
extern u8 captured_type;
extern u32 captured_addr;
extern u8 menu_select;
extern volatile u16 ms_tick;
// 二级传感器对码菜单中,各类传感器显示名称的字符串 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: 水浸探测器
};
// 对码状态机定义
enum {
PAIR_STATE_SELECT_TYPE, // 选择要学习的传感器类型 (门磁/PIR/烟感/气感/水浸)
PAIR_STATE_WAIT, // 等待射频探测器触发 (扩散雷达页面)
PAIR_STATE_CONFIRM, // 已捕获,进入分配防区确认页面
PAIR_STATE_SUCCESS, // 对码绑定成功界面 (SUCCESS)
@@ -48,13 +59,10 @@ WristbandApp pair_app = {
};
/**
* @brief PairApp 启动回调
* @brief 进入雷达搜索等待阶段 (选定传感器类型之后)
*/
static void PairApp_OnStart(void)
static void PairApp_EnterWait(void)
{
// 1. 为了防止后台射频监听与对码射频接收冲突,将后台射频监听应用暂时卸载
AppManager_DetachFromBackground(&rf_monitor_app);
pair_state = PAIR_STATE_WAIT;
last_radar_sec = 99; // 强制刷写
current_state = STATE_PAIR_WAIT;
@@ -69,7 +77,25 @@ static void PairApp_OnStart(void)
// 开启白色幻彩双灯珠常亮 (R=150, G=150, B=150),作为对码状态视觉指示
RGB_Send(150, 150, 150, 150, 150, 150);
XTELL_LOG("[PairApp] Started (WAIT)\r\n");
XTELL_LOG("[PairApp] Entering WAIT\r\n");
}
/**
* @brief PairApp 启动回调
*/
static void PairApp_OnStart(void)
{
// 为了防止后台射频监听与对码射频接收冲突,将后台射频监听应用暂时卸载
AppManager_DetachFromBackground(&rf_monitor_app);
pair_state = PAIR_STATE_SELECT_TYPE;
menu_select = 0;
current_state = STATE_PAIR_MENU;
RF_SetMode(0); // 选类型阶段暂不需要天线,防止接收杂波干扰屏幕绘制
UI_ShowPairMenuPage(menu_select); // 绘制传感器类型选择页
RGB_Send(0, 0, 0, 0, 0, 0);
XTELL_LOG("[PairApp] Started (SELECT_TYPE)\r\n");
}
/**
@@ -142,14 +168,14 @@ static void PairApp_onRun(void)
else RGB_Send(0, 0, 0, 0, 0, 0);
}
else if (pair_state == PAIR_STATE_SUCCESS) {
// ====== 成功展示状态:延时 2 秒自动返回二级设置菜单 ======
// ====== 成功展示状态:延时 2 秒自动返回菜单 ======
u8 elapsed = (current_sec >= success_start_sec) ? (current_sec - success_start_sec) : (60 + current_sec - success_start_sec);
if (elapsed >= 2) {
AppManager_SwitchToForeground(&menu_app);
}
}
else if (pair_state == PAIR_STATE_FAIL) {
// ====== 失败展示状态:延时 5 秒自动返回二级设置菜单 ======
// ====== 失败展示状态:延时 5 秒自动返回菜单 ======
u8 elapsed = (current_sec >= fail_start_sec) ? (current_sec - fail_start_sec) : (60 + current_sec - fail_start_sec);
if (elapsed >= 5) {
AppManager_SwitchToForeground(&menu_app);
@@ -177,6 +203,30 @@ static void PairApp_onClose(void)
*/
static EventResult PairApp_onEvent(SystemEvent *evt)
{
if (pair_state == PAIR_STATE_SELECT_TYPE) {
if (evt->key_event == KEY_EVENT_UP_CLICK) {
// 短按 ▲ 键上移焦点,允许循环切换 (到顶绕到底)
menu_select = (menu_select == 0) ? 4 : (menu_select - 1);
UI_ShowPairMenuPage(menu_select);
return EVENT_HANDLED;
}
if (evt->key_event == KEY_EVENT_DOWN_CLICK) {
// 短按 ▼ 键下移焦点,允许循环切换 (到底绕到顶)
menu_select = (menu_select == 4) ? 0 : (menu_select + 1);
UI_ShowPairMenuPage(menu_select);
return EVENT_HANDLED;
}
if (evt->key_event == KEY_EVENT_CONFIRM_CLICK) {
// 确认选择:缓存用户选好的传感器类别,短震反馈并进入雷达搜索阶段
captured_type = menu_select;
MOTOR = 1;
Delay_ms(100);
MOTOR = 0;
PairApp_EnterWait();
return EVENT_HANDLED;
}
}
if (pair_state == PAIR_STATE_CONFIRM) {
if (evt->key_event == KEY_EVENT_UP_CLICK) {
// 短按 ▲ 键增加目标分配防区号 (上限 99)
@@ -212,16 +262,10 @@ static EventResult PairApp_onEvent(SystemEvent *evt)
XTELL_LOG("[PairApp] Sensor saved successfully.\r\n");
return EVENT_HANDLED;
}
if (evt->key_event == KEY_EVENT_CONFIRM_LONG) {
// 长按 ■ 确认键放弃本次捕获,直接返回二级主菜单
RGB_Send(0, 0, 0, 0, 0, 0);
AppManager_SwitchToForeground(&menu_app);
return EVENT_HANDLED;
}
}
// 在对码搜寻或确认防区期间,长按 ▼ 键代表强行取消对码直接退回菜单
if (evt->key_event == KEY_EVENT_DOWN_LONG) {
// 在任何阶段,长按 ■ 确认键或长按 ▼ 键代表放弃/取消对码,统一退回菜单 (父 App)
if (evt->key_event == KEY_EVENT_CONFIRM_LONG || evt->key_event == KEY_EVENT_DOWN_LONG) {
RGB_Send(0, 0, 0, 0, 0, 0);
AppManager_SwitchToForeground(&menu_app);
return EVENT_HANDLED;

174
App/app_set_clock.c Normal file
View File

@@ -0,0 +1,174 @@
#include "app_manager.h"
#include "clock.h"
#include "xtell.h"
#include "ui.h"
#include "rgb.h"
#include "event.h"
#include "system.h"
#include "../Drivers/pcf8563.h"
// 外部引用的全局系统状态变量
extern SystemState current_state;
extern volatile u16 ms_tick;
// 本应用私有的时间编辑状态
static u8 time_edit_field = 1; // 当前聚焦调节的字段 (1:小时, 2:分钟, 3:日, 4:月, 5:年)
static u8 time_edit_blink_on = 1; // 时钟修改时光标闪烁周期状态位
// 声明本应用的生命周期回调函数
static void SetClockApp_OnStart(void);
static void SetClockApp_onRun(void);
static void SetClockApp_onClose(void);
static EventResult SetClockApp_onEvent(SystemEvent *evt);
// 实体化定义时间设置应用控制块 (从 menu_app 的"HORLOGE"选项进入)
WristbandApp set_clock_app = {
APP_ID_SET_CLOCK,
"SetClock",
SetClockApp_OnStart,
SetClockApp_onRun,
SetClockApp_onClose,
SetClockApp_onEvent
};
/**
* @brief SetClockApp 启动回调
*/
static void SetClockApp_OnStart(void)
{
time_edit_field = 1;
time_edit_blink_on = 1;
current_state = STATE_SET_TIME; // 系统切入时间设置状态
// FCOB 幻彩灯条双灯珠均显示蓝色常亮 (R=0, G=0, B=180),进行模式视觉区分
RGB_Send(0, 0, 180, 0, 0, 180);
// 振动马达单次短震 50 毫秒,提供触觉反馈
MOTOR = 1;
Delay_ms(50);
MOTOR = 0;
// 渲染显示时间编辑界面,默认高亮编辑小时字段,消隐位开启
UI_ShowSetTimePage(current_hour, current_min, current_day, current_month, current_year, current_weekday, time_edit_field, 1);
XTELL_LOG("[SetClockApp] Started\r\n");
}
/**
* @brief SetClockApp 轮询主回调
*/
static void SetClockApp_onRun(void)
{
// ====== 时间设置闪烁动画 (周期 500ms) ======
u8 blink = (ms_tick / 500) % 2;
if (blink != time_edit_blink_on)
{
time_edit_blink_on = blink;
// 每次闪烁状态发生变化,只局部刷新正在调节的那个字段,不整屏重绘
UI_UpdateSetTimeDigit(current_hour, current_min, current_day, current_month, current_year, current_weekday, time_edit_field, time_edit_blink_on);
}
}
/**
* @brief SetClockApp 退出清理回调
*/
static void SetClockApp_onClose(void)
{
RGB_Send(0, 0, 0, 0, 0, 0); // 关闭灯条
XTELL_LOG("[SetClockApp] Closed\r\n");
}
/**
* @brief SetClockApp 接收按键事件处理回调
*/
static EventResult SetClockApp_onEvent(SystemEvent *evt)
{
if (evt->key_event == KEY_EVENT_UP_CLICK)
{
// 短按 ▲ 键增加当前聚焦字段的数值 (各字段独立循环)
switch (time_edit_field) {
case 1: current_hour = (current_hour + 1) % 24; break;
case 2: current_min = (current_min + 1) % 60; break;
case 3:
current_day = (current_day >= Days_In_Month(current_year, current_month)) ? 1 : (current_day + 1);
Clock_RecalcWeekday();
break;
case 4:
current_month = (current_month >= 12) ? 1 : (current_month + 1);
// 月份变化后,若当前日超出新月份的最大天数,截断到最大天数
if (current_day > Days_In_Month(current_year, current_month)) {
current_day = Days_In_Month(current_year, current_month);
}
Clock_RecalcWeekday();
break;
default: // 5: 年
current_year = (current_year >= 99) ? 0 : (current_year + 1);
if (current_day > Days_In_Month(current_year, current_month)) {
current_day = Days_In_Month(current_year, current_month);
}
Clock_RecalcWeekday();
break;
}
time_edit_blink_on = 1; // 调值后强制显示 (不处于消隐周期)
UI_UpdateSetTimeDigit(current_hour, current_min, current_day, current_month, current_year, current_weekday, time_edit_field, 1);
return EVENT_HANDLED;
}
if (evt->key_event == KEY_EVENT_DOWN_CLICK)
{
// 短按 ▼ 键减小当前聚焦字段的数值 (各字段独立循环)
switch (time_edit_field) {
case 1: current_hour = (current_hour == 0) ? 23 : (current_hour - 1); break;
case 2: current_min = (current_min == 0) ? 59 : (current_min - 1); break;
case 3:
current_day = (current_day <= 1) ? Days_In_Month(current_year, current_month) : (current_day - 1);
Clock_RecalcWeekday();
break;
case 4:
current_month = (current_month <= 1) ? 12 : (current_month - 1);
if (current_day > Days_In_Month(current_year, current_month)) {
current_day = Days_In_Month(current_year, current_month);
}
Clock_RecalcWeekday();
break;
default: // 5: 年
current_year = (current_year == 0) ? 99 : (current_year - 1);
if (current_day > Days_In_Month(current_year, current_month)) {
current_day = Days_In_Month(current_year, current_month);
}
Clock_RecalcWeekday();
break;
}
time_edit_blink_on = 1; // 调值后强制显示 (不处于消隐周期)
UI_UpdateSetTimeDigit(current_hour, current_min, current_day, current_month, current_year, current_weekday, time_edit_field, 1);
return EVENT_HANDLED;
}
if (evt->key_event == KEY_EVENT_CONFIRM_LONG)
{
// 长按 ■ 确认键切换调节字段 (小时 -> 分钟 -> 日 -> 月 -> 年 -> 循环回小时)
time_edit_field = (time_edit_field >= 5) ? 1 : (time_edit_field + 1);
time_edit_blink_on = 1; // 切换焦点后所有字段都应处于可见状态
// 全部字段各局部刷新一次,避免切换前正处于消隐周期的那个字段停留在涂黑状态
UI_UpdateSetTimeDigit(current_hour, current_min, current_day, current_month, current_year, current_weekday, 1, 1);
UI_UpdateSetTimeDigit(current_hour, current_min, current_day, current_month, current_year, current_weekday, 2, 1);
UI_UpdateSetTimeDigit(current_hour, current_min, current_day, current_month, current_year, current_weekday, 3, 1);
UI_UpdateSetTimeDigit(current_hour, current_min, current_day, current_month, current_year, current_weekday, 4, 1);
UI_UpdateSetTimeDigit(current_hour, current_min, current_day, current_month, current_year, current_weekday, 5, 1);
return EVENT_HANDLED;
}
if (evt->key_event == KEY_EVENT_CONFIRM_CLICK)
{
// 短按 ■ 确认键保存最新时间与日期配置,退出到主菜单 (父 App)
current_state = STATE_PAIR_MENU;
#if USE_PCF8563
// 同步将最新时间与日期 (含算好的星期) 一次性写入 PCF8563 芯片寄存器
PCF8563_WriteDateTime(current_hour, current_min, 0, current_day, current_month, current_year, current_weekday);
#endif
// 退出短振反馈
MOTOR = 1; Delay_ms(50); MOTOR = 0;
AppManager_SwitchToForeground(&menu_app);
return EVENT_HANDLED;
}
return EVENT_HANDLED; // 在时间设置模式下屏蔽拦截其他一切无关按键事件
}

View File

@@ -127,7 +127,7 @@ static void SOSApp_onClose(void)
static EventResult SOSApp_onEvent(SystemEvent *evt)
{
if (evt->key_event != KEY_EVENT_NONE) {
AppManager_SwitchToForeground(&clock_app); // 退出 SOS返回时钟
AppManager_SwitchToForeground(&home_app); // 退出 SOS返回时钟
return EVENT_HANDLED;
}
return EVENT_IGNORED;

View File

@@ -5,14 +5,62 @@
u8 current_hour = 10; // 系统软时钟:小时 (默认初始化为 10 点)
u8 current_min = 35; // 系统软时钟:分钟 (默认初始化为 35 分)
u8 current_sec = 0; // 系统软时钟:秒
u8 current_day = 1; // 系统软时钟:日 (默认初始化为 1 号)
u8 current_month = 1; // 系统软时钟:月 (默认初始化为 1 月)
u8 current_year = 25; // 系统软时钟:年 (默认初始化为 2025 年)
u8 current_weekday = 0; // 系统软时钟:星期 (0=周日, 由 Clock_RecalcWeekday 现算)
volatile u8 clock_updated = 0; // 分钟跳变标志位,置位后用于驱动 UI 刷新时间显示
volatile bit second_tick_flag = 0; // 整秒滴答标志位,每次秒进位置 1由主循环转换为 KEY_EVENT_SECOND_TICK 事件
// 毫秒计数器 (Timer1 每 1ms 累加)
static u16 ms_cnt = 0;
// 每月天数表 (2 月固定按 28 天,闰年在 Days_In_Month 里单独加 1 天处理)
static code u8 days_in_month_table[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
u8 Days_In_Month(u8 year, u8 month)
{
if (month < 1 || month > 12) {
return 31; // 防御性兜底,不应该发生
}
if (month == 2) {
// 闰年判断2000~2099 区间内,能被 4 整除即为闰年 (期间没有能被 100 整除的年份需要排除)
if ((year % 4) == 0) {
return 29;
}
}
return days_in_month_table[month - 1];
}
void Clock_RecalcWeekday(void)
{
// Zeller 公式h = (q + [13(m+1)/5] + K + [K/4] + [J/4] - 2J) mod 7
// h: 0=周六,1=周日,2=周一,...,6=周五;用 "+5J" 代替 "-2J" 避免无符号数下溢 (-2 ≡ 5 (mod 7))
// 1、2 月按上一年的 13、14 月处理
u8 m = current_month;
u8 y = current_year;
u8 j = 20; // 世纪数 (2000~2099 固定为 20)
u16 h;
if (m < 3) {
m += 12;
if (y == 0) {
y = 99; // 2000 年 1/2 月,按 1999 年处理的边界情况
j = 19;
} else {
y -= 1;
}
}
h = (current_day + (13UL * (m + 1)) / 5 + y + y / 4 + j / 4 + 5UL * j) % 7;
// 把 Zeller 原始约定 (0=周六) 转换成常用的 0=周日, 1=周一, ..., 6=周六
current_weekday = (u8)((h + 6) % 7);
}
/**
* @brief 系统软时钟毫秒递增更新 (在 1ms 中断中调用)
* @details 负责由定时器 1ms 驱动递增软时钟,并在分钟发生跳变时将 clock_updated 置位
* @details 负责由定时器 1ms 驱动递增软时钟,并在分钟/日期发生跳变时做相应处理
*/
void Clock_IncMS(void)
{
@@ -20,6 +68,7 @@ void Clock_IncMS(void)
if (ms_cnt >= 1000) {
ms_cnt = 0; // 1秒时间到
current_sec++;
second_tick_flag = 1; // 整秒滴答,通知主循环生成 KEY_EVENT_SECOND_TICK 事件
if (current_sec >= 60) {
current_sec = 0; // 1分钟时间到
current_min++;
@@ -27,7 +76,20 @@ void Clock_IncMS(void)
current_min = 0; // 1小时时间到
current_hour++;
if (current_hour >= 24) {
current_hour = 0; // 小时循环
current_hour = 0; // 小时循环,日期进位
current_day++;
if (current_day > Days_In_Month(current_year, current_month)) {
current_day = 1; // 月份进位
current_month++;
if (current_month > 12) {
current_month = 1; // 年份进位 (不处理世纪)
current_year++;
if (current_year > 99) {
current_year = 0;
}
}
}
Clock_RecalcWeekday(); // 日期变化,重新计算星期
}
}
clock_updated = 1; // 分钟跳变,标志位置位,前台待机 App 会捕获以刷新 UI

View File

@@ -7,11 +7,30 @@
extern u8 current_hour;
extern u8 current_min;
extern u8 current_sec;
extern u8 current_day; // 日 (1~31)
extern u8 current_month; // 月 (1~12)
extern u8 current_year; // 年 (0~99代表 2000~2099不处理世纪)
extern u8 current_weekday; // 星期 (0=周日, 1=周一, ..., 6=周六)
extern volatile u8 clock_updated;
extern volatile bit second_tick_flag; // 整秒滴答标志位,每次 current_sec 递增时置 1供主循环转换为事件
/**
* @brief 系统软时钟毫秒递增更新 (在 1ms 中断中调用)
*/
void Clock_IncMS(void);
/**
* @brief 返回指定年月的天数上限 (处理闰年 2 月)
* @param year 年 (0~99代表 2000~2099)
* @param month 月 (1~12)
*/
u8 Days_In_Month(u8 year, u8 month);
/**
* @brief 根据年月日重新计算星期,并写回 current_weekday
* @details 采用 Zeller 公式现算,不依赖 RTC 自身的星期寄存器,
* 避免手动改日期后星期跟着错乱;日期发生变化(自然进位或手动调节)时都要调用一次。
*/
void Clock_RecalcWeekday(void);
#endif // __CLOCK_H__

View File

@@ -81,7 +81,8 @@ typedef enum {
KEY_EVENT_UP_DOWN_COMB, // ▲ 与 ▼ 双键组合按下事件
KEY_EVENT_CONFIRM_CLICK, // ■ 确认键短按事件 (保存设置或进入子菜单)
KEY_EVENT_CONFIRM_LONG, // ■ 确认键长按事件 (打开主菜单)
KEY_EVENT_POWER_OFF // 长按 KEY_DOWN 开关机触发事件
KEY_EVENT_POWER_OFF, // 长按 KEY_DOWN 开关机触发事件
KEY_EVENT_SECOND_TICK // 整秒滴答事件,每秒派发一次,各 App 自行决定是否用它累计闲置时长
} KeyEvent;
/* ====== 事件优先级枚举 ====== */
@@ -108,9 +109,11 @@ typedef enum {
/* ====== 应用程序 ID 枚举 ====== */
typedef enum {
APP_ID_CLOCK, // 待机时钟应用程序 (首屏)
APP_ID_MENU, // 系统设置与操作主菜单应用程序
APP_ID_PAIR, // 无线传感器对码学习应用程序
APP_ID_HOME, // 待机时钟主页应用程序 (开机默认首屏)
APP_ID_MENU, // 系统设置主菜单应用程序 (顶层 4 项列表)
APP_ID_SET_CLOCK, // 时间设置应用程序 (menu_app 子应用)
APP_ID_PAIR, // 无线传感器对码学习应用程序 (menu_app 子应用)
APP_ID_IPC_BIND, // 摄像头一键绑定应用程序 (menu_app 子应用)
APP_ID_ALARM, // 报警接收与波形反馈应用程序
APP_ID_SOS, // 紧急呼救与无线发射应用程序
APP_ID_MAX // 应用程序最大数量上限,用于数组大小
@@ -138,7 +141,12 @@ extern SystemState current_state; // 系统当前整体运行状态
extern u8 current_hour; // 当前系统时间:小时
extern u8 current_min; // 当前系统时间:分钟
extern u8 current_sec; // 当前系统时间:秒
extern u8 current_day; // 当前系统日期:日
extern u8 current_month; // 当前系统日期:月
extern u8 current_year; // 当前系统日期:年 (0~99代表 2000~2099)
extern u8 current_weekday; // 当前系统日期:星期 (0=周日, ..., 6=周六)
extern volatile u8 clock_updated; // 时钟分钟更新标志 (1表示需要刷新待机页时间)
extern volatile bit second_tick_flag; // 整秒滴答标志位,每秒置 1由主循环转换为 KEY_EVENT_SECOND_TICK 事件
extern u8 menu_select; // 当前菜单高亮的选中项索引
extern u32 captured_addr; // 对码或报警时,当前捕获到的传感器 24 位射频地址
@@ -146,11 +154,9 @@ 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循环)
extern volatile u16 inactivity_timer; // 用户闲置计时器 (用作无操作自动休眠单位10ms)
extern volatile u32 pair_timeout_ms; // 对码等待搜寻超时毫秒计数器
extern volatile u16 alarm_timer_ms; // 报警振动与闪烁定时计数器
extern volatile u16 motor_timer_ms; // 马达单次震动定时计时器
@@ -168,4 +174,9 @@ extern volatile u16 comb_hold; // 组合键持续按下计时器
#define MAIN_Fosc 24000000UL // STC32G 主频工作频率定义 (24.0 MHz)
#define CLONED_ADDR 0x37A86UL // 手环自身克隆发射的默认 24 位射频出厂 ID 地址
// 硬件模块使能开关:某些 PCB 版本可能不焊接 PCF8563 RTC 芯片,置 0 后完全跳过
// I2C 探测/读写与开机诊断扫描,只依赖软件时钟 (Timer1 驱动的 current_hour/min/sec 等)
// 不写入/不写回 RTC断电后时间不保留但整机功能不受影响。
#define USE_PCF8563 1
#endif // __CONFIG_H__

View File

@@ -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 为擦除态 0xFFLang_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; // 记录对应的防区号
// 生成该槽位传感器的法语名称
// 生成该槽位传感器的名称 (取当前界面语言下的简称)
{
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] = sensor_names_fr[type][i];
if (sensor_names_fr[type][i] == '\0') break; // 遇零终止
// 从当前语言名称字符串中复制对应名字的字符
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'; // 尾部安全截断,防止字符越界

View File

@@ -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);

View File

@@ -2,7 +2,6 @@
#include "app_manager.h"
#include "database.h"
#include "system.h"
#include "clock.h"
#include "xtell.h"
#include "../Drivers/adc.h"
@@ -118,12 +117,12 @@ void Event_Dispatcher_Loop(void)
return;
}
inactivity_timer = 0; // 捕获到有效事件,清零休眠计时器,重置闲置超时
// 串口打印调试信息
// 串口打印调试信息 (整秒滴答事件每秒都会触发一次,不打印以免刷屏)
if (evt.key_event != KEY_EVENT_SECOND_TICK) {
XTELL_LOG("[EVENT] key=");
XTELL_LOG_HEX8((u8)evt.key_event);
XTELL_LOG("\r\n");
}
// SOS 按键紧急处理:若当前前台不为 SOS 应用,接收到 SOS 键动作,立即强行切入到 SOS_APP 并执行求救
if ((evt.key_event == KEY_EVENT_SOS_CLICK || evt.key_event == KEY_EVENT_SOS_LONG) &&
@@ -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 检查并执行,完全在主线程中运行,绝无中断重入冲突。
@@ -223,16 +181,6 @@ void Event_KeyScan_Poll(void)
}
}
// 检测到有任意按键处于下压状态,立即将无操作闲置休眠计时器归零
if (raw_up || raw_down || raw_confirm || raw_sos) {
inactivity_timer = 0;
} else {
// 如果在常态下(非休眠、非报警、非紧急求救),累加闲置时间
if (current_state != STATE_SLEEP && current_state != STATE_ALARM && current_state != STATE_SOS_EMITTED) {
inactivity_timer++;
}
}
// 默认将派发事件重置为空
evt.key_event = KEY_EVENT_NONE;
evt.priority = EVENT_PRIORITY_NORMAL;

View File

@@ -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;
/**

View File

@@ -4,11 +4,17 @@
; 【技术原理分析】:
; 1. Keil C251 编译器为了节省硬件资源,默认的 `interrupt` 关键字只支持 0 ~ 31 范围的软件中断向量号。
; 2. 然而STC32G 单片机拥有大量的扩展硬件外设,其 Port0 端口中断物理向量号为 37Port2 端口中断物理向量号为 39
; 超出了编译器的直接定义范畴。如果直接在 C 语言中声明 `interrupt 37`,编译器会报错。
; Port3 端口中断物理向量号为 40 (定义见 Drivers/stc32g.h 的 P0INT_VECTOR/P2INT_VECTOR/P3INT_VECTOR)
; 均超出了编译器的直接定义范畴。如果直接在 C 语言中声明 `interrupt 37`,编译器会报错。
; 3. 为此,本汇编模块采用“绝对地址跳转重定向”技术:
; - 直接在硬件物理中断向量入口地址Port0: 0x00012B, Port2: 0x00013B处放置一条长跳转 `LJMP` 汇编指令。
; - 将触发的物理中断强行重定向引导至 Keil C251 所能合法编译生成的较小编译向量入口地址(例如 13 号和 15 号中断服务程序入口)。
; - 由此,我们在 C 语言中声明 `interrupt 13``interrupt 15`,就能够成功被硬件 Port0 和 Port2 端口中断触发调用。
; - 直接在硬件物理中断向量入口地址Port0: 0x00012B, Port2: 0x00013B, Port3: 0x000143)处放置一条长跳转 `LJMP` 汇编指令。
; - 将触发的物理中断强行重定向引导至 Keil C251 所能合法编译生成的较小编译向量入口地址(例如 13、15、16 号中断服务程序入口)。
; - 由此,我们在 C 语言中声明 `interrupt 13``interrupt 15`、`interrupt 16`,就能够成功被硬件
; Port0、Port2、Port3 端口中断触发调用。
; 【重要修复记录】Port3 这条重定向此前一直缺失——C 代码里 `Port3_Isr` 声明的是 `interrupt 16`
; (对应真实向量 16 号 INT4与 P3 端口无关),而 P3 端口真正的硬件向量号是 40此前没有任何代码
; 把向量 40 重定向过去,导致 P3.6 硬件中断真正触发时CPU 会跳转到 0x000143 这个从未写入过任何
; 指令的地址,造成运行跑飞/复位。现已补上对应的跳转条目。
; =========================================================================
NAME ISR_JUMP ; 定义当前汇编模块的名字为 ISR_JUMP
@@ -31,4 +37,13 @@ CSEG AT 00013BH
; 0x00007B 即为 C 语言声明的 `interrupt 15` 所对应的中断服务函数入口地址
LJMP 00007BH
; -------------------------------------------------------------------------
; 端口 3 掉电唤醒/常态边沿中断跳转配置 (P3.6 / RF_RX_DATA)
; -------------------------------------------------------------------------
; 硬件 Port3 中断向量号 40 (P3INT_VECTOR) 对应的绝对闪存入口地址为 0x000143
CSEG AT 000143H
; 当硬件触发 Port3 中断时,程序计数器 (PC) 定位到 000143H此处 LJMP 指令强行将 PC 指向 000083H
; 0x000083 即为 C 语言声明的 `interrupt 16` 所对应的中断服务函数入口地址 (Port3_Isr)
LJMP 000083H
END ; 汇编文件结束标志

48
App/language.c Normal file
View File

@@ -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_EN;
void Lang_Init(Language lang)
{
current_lang = (lang < LANG_COUNT) ? lang : LANG_EN;
}
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;
}

41
App/language.h Normal file
View File

@@ -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__

View File

@@ -7,6 +7,31 @@
#include "system.h"
#include "../Drivers/rf.h"
#include "../Drivers/lcd.h"
#include "../Drivers/timer.h"
/* =========================================================================
* 【烧录必读】STC-ISP 下载时时钟频率必须选择 24MHz且不能只改这一处
* =========================================================================
* 本项目代码里多处对"芯片实际运行在 24MHz"这件事做了硬编码假设,而不是每处都
* 从 MAIN_Fosc 动态换算;如果烧录时在 STC-ISP 里选了别的 IRC 频率(或换成外部
* 晶振但频率不是 24MHz),下面这些地方会全部跟着出错,不是改一下 config.h 里的
* MAIN_Fosc 常量就能自动生效的:
* 1. App/config.h:169 MAIN_Fosc = 24000000UL —— 大部分模块的换算基准。
* 2. Drivers/timer.c Timer1_Init() 里 1ms 定时重装值由 MAIN_Fosc 动态算出,
* 跟着 MAIN_Fosc 改没问题。
* 3. App/database.c IAP_TPS 由 MAIN_Fosc 动态算出 (Flash 读写/擦除等待周期)
* 跟着 MAIN_Fosc 改没问题,但算错会导致 Flash 读写异常甚至数据损坏。
* 4. App/system.c Uart1_Init() 里 Timer2 波特率重装值 (T2L=0xCC, T2H=0xFF)
* 是按 24MHz/115200 手工算好直接硬编码的数字,***不会跟着 MAIN_Fosc 变化***
* 实际主频不是 24MHz 时串口波特率会直接算错,调试串口输出全部变乱码。
* 5. Drivers/rf.c/timer.c 里所有 EV1527 时序阈值和 RF_Delay_us() 都基于
* "Timer0 工作在标准 12T 模式、1 个计数节拍 = 12/Fosc 秒"这个换算关系,
* 代码里到处写的 "0.5us/tick"、"* 2UL" 都是 12/24MHz = 0.5us 这个具体
* 数字,***同样不会跟着 MAIN_Fosc 变化***,主频不对会导致 RF 收发全部解码/
* 发射失败。
* 结论:必须保证 STC-ISP 烧录时选择的时钟频率就是 24MHz代码里各处的计时也
* 都是按实际运行在 24MHz 主频来写的,两边必须一致。
* ========================================================================= */
/* =========================================================================
* 系统全局变量定义
@@ -14,7 +39,7 @@
SystemState current_state = STATE_NORMAL; // 系统整体的当前运行状态,默认为正常时钟待机页
u8 menu_select = 0; // 主菜单当前选中的行高亮索引 (0~2)
u8 menu_select = 0; // 当前菜单高亮的选中项索引 (menu_app 0~3pair_app 传感器类型选择 0~4)
u32 captured_addr = 0; // 配对或报警时,捕获到的传感器 24 位 EV1527 射频物理地址
u8 captured_type = 0; // 正在对码或触发警报的传感器设备类型 (0:门磁, 1:PIR...)
u8 alarm_sensor_slot = 0; // 触发警报的传感器在 sensor_list 数组中的索引槽位号
@@ -24,7 +49,6 @@ Sensor_Slot xdata sensor_list[16];
// 全局滴答与定时中断计数变量,位于 volatile 防止被编译器优化
volatile u16 ms_tick = 0; // 定时中断 1ms 基准滴答累加器
volatile u16 inactivity_timer = 0; // 用户闲置计时器,用于无操作判定(单位: 10ms
volatile u32 pair_timeout_ms = 0; // 传感器配对对码超时毫秒计时器 (上限 30000ms 即 30s)
volatile u16 alarm_timer_ms = 0; // 入侵报警马达震动时序计数器
volatile u16 motor_timer_ms = 0; // 独立马达震动时长计数器
@@ -54,6 +78,7 @@ volatile u16 comb_hold = 0; // ▲ + ▼ 组合键按下时长累
* - '1'/'2'/'3': 模拟系统的正常、已布防、已撤防状态切换。
* - 'z'/'Z': 模拟超时,强行使闲置计数器置 6000触发自动深度休眠。
* - 't'/'T': 触发屏幕与无线控制引脚测试。
* - 'e'/'E': DEBUG打印 RF 边沿中断解码排查计数器。
*/
/**
* @brief 串口调试专属RGB 物理颜色通道排查测试
@@ -94,7 +119,6 @@ void RGB_Diagnostic_Test(void)
void Debug_ProcessCommand(char cmd)
{
SystemEvent debug_evt;
inactivity_timer = 0; // 收到指令,立即重置用户的无操作闲置计时器
debug_evt.extra_data = 0;
debug_evt.extra_size = 0;
@@ -160,28 +184,31 @@ void Debug_ProcessCommand(char cmd)
}
} else if (cmd == '1') {
// 模拟切换至 正常撤防 (NORMAL) 状态并刷新时钟
if (AppManager_GetActiveAppID() == APP_ID_CLOCK) {
if (AppManager_GetActiveAppID() == APP_ID_HOME) {
current_state = STATE_NORMAL;
UI_ShowClockPage(current_state, current_hour, current_min);
UI_ShowClockPage(current_state, current_hour, current_min, current_weekday, current_day, current_month);
}
Uart_SendString("[UART] State: NORMAL\r\n");
} else if (cmd == '2') {
// 模拟切换至 已布防 (ARMED) 状态,普通门磁可触发警报
if (AppManager_GetActiveAppID() == APP_ID_CLOCK) {
if (AppManager_GetActiveAppID() == APP_ID_HOME) {
current_state = STATE_ARMED;
UI_ShowClockPage(current_state, current_hour, current_min);
UI_ShowClockPage(current_state, current_hour, current_min, current_weekday, current_day, current_month);
}
Uart_SendString("[UART] State: ARMED\r\n");
} else if (cmd == '3') {
// 模拟切换至 已撤防 (DISARMED) 状态,忽略普通报警
if (AppManager_GetActiveAppID() == APP_ID_CLOCK) {
if (AppManager_GetActiveAppID() == APP_ID_HOME) {
current_state = STATE_DISARMED;
UI_ShowClockPage(current_state, current_hour, current_min);
UI_ShowClockPage(current_state, current_hour, current_min, current_weekday, current_day, current_month);
}
Uart_SendString("[UART] State: DISARMED\r\n");
} else if (cmd == 'z' || cmd == 'Z') {
inactivity_timer = 6000; // 模拟闲置 60 秒超时,强制触发主循环深度休眠
// 直接强制进入低功耗休眠,测试用 (原来靠伪造全局闲置计时器触发,现改为直接调用)
Uart_SendString("[UART] Force Entering Sleep mode!\r\n");
current_state = STATE_SLEEP;
Enter_Low_Power_Sleep();
AppManager_SwitchToForeground(&home_app); // 唤醒后强制重绘时钟画面
} else if (cmd == 'q' || cmd == 'Q') {
RGB_Diagnostic_Test(); // 启动 RGB 物理颜色通道排查测试
} else if (cmd == 't' || cmd == 'T') {
@@ -190,12 +217,56 @@ void Debug_ProcessCommand(char cmd)
RF_DiagnosticMode('r'); // 触发 5 秒射频接收监听诊断
} else if (cmd == 'l' || cmd == 'L') {
Loopback_Test(); // 触发物理电气回环测试 (短路 P2.1 和 P3.6 自检)
} else if (cmd == 'e' || cmd == 'E') {
RF_PrintDebugCounters(); // DEBUG: 打印 RF 边沿中断解码排查计数器
}
}
// ====== 调试用最小化启动开关 ======
// 置 1 时 main() 只做点亮屏幕这一件事RF/自检/数据库/事件框架/Timer1 全部跳过,
// 用于定位"开机反复重启"问题的范围。
// 根因(isr_jump.asm 缺少 Port3 中断向量重定向、且该文件从未加入工程编译)已定位并修复,
// 现改回 0恢复完整正常启动流程测试整机端到端是否稳定。
#define DEBUG_MINIMAL_BOOT 0
/* =========================================================================
* 主函数 (入口)
* ========================================================================= */
#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 溢出中断开始工作
// ====== 步骤 C真正的病根 (isr_jump.asm 缺少 Port3 重定向、且该文件从未被加入工程编译) 已修复,
// P3.6 中断与 RF_HandleEdgeInterrupt 都已恢复为正常逻辑,现在测试真实功能是否稳定 ======
RF_SetMode(1);
LCD_ShowStringCentered(100, "STEP C OK", COLOR_WHITE, COLOR_BLACK);
LCD_ShowStringCentered(130, "ISR fixed", COLOR_GRAY, COLOR_BLACK);
while (1) {
Delay_ms(1);
}
}
#else
void main(void)
{
char cmd;
@@ -211,7 +282,7 @@ void main(void)
RGB_Send(0, 0, 0, 0, 0, 0); // 【首要修正】:上电第一时间强制发送 0 码关闭灯条,熄灭上电暂态引起的随机白光!
RF_Init(); // 初始化射频芯片各引脚控制
Delay_ms(500); // 软启动延时,等待供电电容稳定
LCD_Init(); // 初始化 LHS114TC-IF03 (ST7789V) 屏幕控制寄存器序列并开启背光
LCD_Init(); // 初始化 LHS114TC-IF03 (ST7789V) 屏幕控制寄存器序列 (背光保持关闭,避免闪烁)
Uart1_Init(); // 初始化串口 1 波特率 115200 供调试日志
Uart_SendString("Wristband System Initialized!\r\n");
@@ -221,7 +292,7 @@ void main(void)
/* 步骤 1: 初始化事件环形缓冲区队列 */
EventQueue_Init();
/* 步骤 2+5: 初始化应用管理器并登记所有前台应用,默认加载 ClockApp */
/* 步骤 2+5: 初始化应用管理器并登记所有前台应用,默认加载 HomeApp */
AppManager_Init();
/* 启动并挂载后台射频监控应用,开始侦听无线传感器信号 */
@@ -231,10 +302,13 @@ void main(void)
Timer1_Init();
/* 开机自动加载并运行待机时钟前台活跃应用 */
AppManager_StartApp(APP_ID_CLOCK);
AppManager_StartApp(APP_ID_HOME);
Uart_SendString("[SYS] Event-driven framework ready\r\n");
// 所有初始化与首帧时钟界面绘制均已完成,此刻才点亮背光,避免开机过程中的中间画面被看到而产生闪烁
SGM_CTRL = 0;
// ====== 主循环 ======
while (1) {
/* 0. 检查主循环按键定时标志 (由 1ms 中断置位,主循环响应清除并处理) */
@@ -243,6 +317,21 @@ void main(void)
Event_KeyScan_Poll();
}
/* 0.5 检查整秒滴答标志 (由 Clock_IncMS 每秒置位一次),转换为事件入队分发;
* 各 App 自行决定要不要用它累计自己的闲置时长 (收到任何按键事件就清零)
* 不再由主循环维护一个全局的休眠倒计时。 */
if (second_tick_flag) {
second_tick_flag = 0;
{
SystemEvent tick_evt;
tick_evt.key_event = KEY_EVENT_SECOND_TICK;
tick_evt.priority = EVENT_PRIORITY_LOW;
tick_evt.extra_data = 0;
tick_evt.extra_size = 0;
EventQueue_Push(tick_evt);
}
}
/* 1. 串口非阻塞指令捕获 -> 装配为事件方式入队 */
cmd = Uart_RxChar();
if (cmd != '\0') {
@@ -256,18 +345,11 @@ void main(void)
/* 3. 应用调度中心:运行当前处于前台活跃状态的应用(集成了 CPU 运行时间超时挂起保护) */
AppManager_RunActiveApp();
/* 4. 自动休眠判定:常态无操作闲置达到 60 秒 (6000 * 10ms = 60s) 时,切入低功耗停机休眠 */
if (inactivity_timer >= 6000) {
inactivity_timer = 0;
current_state = STATE_SLEEP; // 系统置为休眠状态
Enter_Low_Power_Sleep(); // 该函数是阻塞的MCU 将在此挂起。唤醒后将自动从其后继续执行
current_state = STATE_NORMAL; // 唤醒后重新置为正常待机状态
AppManager_StartApp(APP_ID_CLOCK); // 重新开启时钟应用以刷新画面和背光
}
}
}
#endif // DEBUG_MINIMAL_BOOT
// 唤醒源事件标志,由对应的外部中断 ISR 写入
volatile u8 p0_wakeup_flag = 0; // P0 端口按键唤醒标志
volatile u8 p2_wakeup_flag = 0; // P2 端口按键唤醒标志
@@ -304,12 +386,17 @@ 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否则第 1 项唤醒功能会在第一次边沿后失效。
*/
void Port3_Isr(void) interrupt 16
{
EAXFR = 1; // 使能访问扩展寄存器
p3_wakeup_flag = P3INTF | 0x01;// 读取并备份 P3 中断标志寄存器值 (0x01作安全掩码)
P3INTF = 0x00; // 清除 P3 端口中断悬挂标志位
P3INTE = 0x00; // 立即关闭 P3 中断通道,防止反复重入
RF_HandleEdgeInterrupt(); // 驱动 EV1527 边沿增量解码状态机
}

73
App/strings.h Normal file
View File

@@ -0,0 +1,73 @@
#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") \
/* ---- 星期缩写 (顺序必须是周日~周六,与 current_weekday 的 0~6 对齐,取用时按 STR_WEEKDAY_SUN + current_weekday 直接索引,顺序不能打乱) ---- */ \
DEFINE_STRING(WEEKDAY_SUN, "DIM", "SUN") \
DEFINE_STRING(WEEKDAY_MON, "LUN", "MON") \
DEFINE_STRING(WEEKDAY_TUE, "MAR", "TUE") \
DEFINE_STRING(WEEKDAY_WED, "MER", "WED") \
DEFINE_STRING(WEEKDAY_THU, "JEU", "THU") \
DEFINE_STRING(WEEKDAY_FRI, "VEN", "FRI") \
DEFINE_STRING(WEEKDAY_SAT, "SAM", "SAT")
typedef enum {
WRISTBAND_STRING_LIST
STR_COUNT
} StringID;
#endif // __STRINGS_H__

View File

@@ -60,6 +60,9 @@ void GPIO_Init(void)
// SGM_CTRL (P1.7), LCD 控制管脚 (P1.0, P1.1, P1.4, P1.5, P1.6) 置为推挽输出
P1M1 &= ~((1 << 7) | (1 << 0) | (1 << 1) | (1 << 4) | (1 << 5) | (1 << 6));
P1M0 |= ((1 << 7) | (1 << 0) | (1 << 1) | (1 << 4) | (1 << 5) | (1 << 6));
SGM_CTRL = 1; // 背光从上电第一刻起就确定关闭 (LEDA=1),避免推挽模式生效前电平不确定造成瞬间闪烁
// SHUT (P3.5) 射频/外设开启使能脚置为推挽输出
P3M1 &= ~(1 << 5);
@@ -165,6 +168,7 @@ void Self_Test(void)
Uart_SendString("\r\n========================================\r\n");
Uart_SendString("[SELF-TEST] System Power-On Self Test...\r\n");
#if USE_PCF8563
// 执行 I2C 总线器件扫描,查找 RTC 是否物理响应
PCF8563_ScanDiagnostic();
@@ -195,6 +199,9 @@ void Self_Test(void)
// 0.2 执行 RTC 底层单独试读测试
PCF8563_TryReadDiagnostic();
#else
Uart_SendString("[SELF-TEST] USE_PCF8563=0, skipping RTC/I2C diagnostics.\r\n");
#endif
// 1. 验证 PWR_HOLD 锁存状态
if (PWR_HOLD == 1) {
@@ -342,11 +349,18 @@ 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)
// RF_HandleEdgeInterrupt() 用软件乒乓单边沿切换模拟双边沿触发,运行期间 P3IM0 会在
// 上升沿/下降沿之间动态翻转;如果休眠前不重新固定它,进入 Power Down 那一刻 P3IM0
// 具体停在哪个方向是不确定的可能导致唤醒不了或唤醒延迟。根据《STC32G 技术手册》
// 15.1.3 节真值表PxIM1/PxIM0 的四种模式里只有下降沿(0,0)和上升沿(0,1)两种边沿模式
// 标注支持掉电唤醒,高/低电平两种模式都不支持——所以这里没有改成电平触发,而是强制
// 固定为上升沿 (与 RF_Init() 开机时的初始配置一致),确保休眠那一刻处于一个明确的、
// 手册标注支持唤醒的状态;唤醒后 P3IM0 就停在上升沿,与开机状态相同,解码状态机从
// 一个已知状态重新开始,不需要额外恢复。
P3IM1 &= ~0x40;
P3IM0 |= 0x40; // (0,1) 上升沿触发
P3INTF = 0x00; // 清除挂起标志
P3INTE |= 0x40; // 开启 P3.6 中断允许
P3WKUE |= 0x40; // 使能 P3.6 掉电唤醒
// 确保引脚上拉和输入使能,防止因浮空产生的功耗泄漏
@@ -365,7 +379,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 +400,14 @@ 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;
// P3.6 的悬空钳位已改用内部下拉 (P3PD在 RF_Init() 里一次性永久开启Power Down 期间
// SFR 状态保持不变,无需在这里重新配置);此处不再恢复上拉,避免上拉与下拉同时使能相互打架
P0WKUE = 0x00;
P2WKUE = 0x00;
P3WKUE = 0x00;
@@ -398,8 +419,9 @@ void Wakeup_Restore(void)
WKTCH = 0x00;
WKTCL = 0x00;
// 3. 开启全局中断并重启系统滴答定时器
// 3. 开启全局中断并重启系统滴答定时器 Timer1 与 RF 时间戳基准 Timer0
EA = 1;
ET0 = 1;
ET1 = 1;
// 4. 恢复射频芯片工作模式
@@ -425,9 +447,6 @@ void Wakeup_Restore(void)
// 6. 延时 200ms 等待液晶稳压板上电稳定后,重新对 LCD 寄存器初始化
Delay_ms(200);
LCD_Init();
// 7. 充零闲置计时器
inactivity_timer = 0;
}
/**

253
App/ui.c
View File

@@ -1,23 +1,83 @@
#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];
// 底部日期栏固定用 8x16 小字体、LCD_ShowStringCentered 居中绘制在 y=195
// 编辑日期时对 日/月/年 做局部刷新需要精确知道每个字段的像素 X 坐标,这里按
// LCD_ShowStringCentered 的居中公式 (LCD_WIDTH - 字符数*8) / 2 手工算好,
// 前提是字符串格式固定为 "WWW DD-MM-YY" (12 个字符),不能随意改动格式。
#define DATE_BAR_Y 195
#define DATE_BAR_START_X 19 // (135 - 12*8) / 2 = 19
#define DATE_BAR_DAY_X (DATE_BAR_START_X + 4 * 8) // 第 4 个字符起 = 51
#define DATE_BAR_MONTH_X (DATE_BAR_START_X + 7 * 8) // 第 7 个字符起 = 75
#define DATE_BAR_YEAR_X (DATE_BAR_START_X + 10 * 8) // 第 10 个字符起 = 99
#define DATE_BAR_FIELD_W 16 // 2 位数字8px/字符
/**
* @brief 拼出"星期缩写 日-月"格式的日期字符串 (如 "VEN 10-07"),供 HOME 页时钟显示
* @param out 输出缓冲区,调用方需保证至少 10 字节 (3字母+空格+2位+'-'+2位+'\0')
*/
static void Build_DateString(char *out, u8 weekday, u8 day, u8 month)
{
char *wd = Lang_Get((StringID)(STR_WEEKDAY_SUN + weekday));
u8 i = 0;
while (*wd) {
out[i++] = *wd++;
}
out[i++] = ' ';
out[i++] = '0' + (day / 10);
out[i++] = '0' + (day % 10);
out[i++] = '-';
out[i++] = '0' + (month / 10);
out[i++] = '0' + (month % 10);
out[i] = '\0';
}
/**
* @brief 拼出"星期缩写 日-月-年"格式的日期字符串 (如 "VEN 10-07-25"),供时间设置页编辑日期时显示
* @param out 输出缓冲区,调用方需保证至少 13 字节 (3字母+空格+2位+'-'+2位+'-'+2位+'\0')
*/
static void Build_DateStringWithYear(char *out, u8 weekday, u8 day, u8 month, u8 year)
{
char *wd = Lang_Get((StringID)(STR_WEEKDAY_SUN + weekday));
u8 i = 0;
while (*wd) {
out[i++] = *wd++;
}
out[i++] = ' ';
out[i++] = '0' + (day / 10);
out[i++] = '0' + (day % 10);
out[i++] = '-';
out[i++] = '0' + (month / 10);
out[i++] = '0' + (month % 10);
out[i++] = '-';
out[i++] = '0' + (year / 10);
out[i++] = '0' + (year % 10);
out[i] = '\0';
}
/**
* @brief 渲染并显示系统待机时钟主页面
* @param state 当前的系统布防状态 (STATE_ARMED: 已布防, 否则为正常或撤防)
* @param hour 当前待显示的小时数 (0~23)
* @param min 当前待显示的分钟数 (0~59)
* @param weekday 当前星期 (0=周日, ..., 6=周六)
* @param day 当前日 (1~31)
* @param month 当前月 (1~12)
* @details 该页面被划分为三大功能视觉区:
* 1. 顶部状态栏:左侧绘制布撤防指示锁,右侧显示真实剩余电量百分比以及电池单色位图。
* 2. 中部核心栏:垂直居中以 16x32 大号粗字体绘制格式化时间 "HH:MM",在其下方标注 AM/PM 标识。
* 3. 底部信息栏:使用灰色小字模绘制法语星期日期 "VEN 10-07" (星期五 7月10日)。
* 3. 底部信息栏:使用灰色小字模绘制真实星期日期 "VEN 10-07" (星期五 7月10日)。
*/
void UI_ShowClockPage(SystemState state, u8 hour, u8 min)
void UI_ShowClockPage(SystemState state, u8 hour, u8 min, u8 weekday, u8 day, u8 month)
{
char time_str[6];
char date_str[10];
char batt_str[7];
u8 batt;
@@ -70,10 +130,11 @@ 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);
// ====== 绘制日期星期栏 (真实日期,不再是写死的占位文案) ======
Build_DateString(date_str, weekday, day, month);
LCD_ShowStringCentered(DATE_BAR_Y, date_str, COLOR_GRAY, COLOR_BLACK);
}
/**
@@ -87,21 +148,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 +176,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 +187,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 +201,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 +214,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 +224,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 +235,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 +248,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 +285,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 +295,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 +321,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 +343,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,9 +372,9 @@ 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)
{
@@ -326,27 +387,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 宽度下居中) ======
@@ -356,15 +423,27 @@ void UI_ShowMainMenu(u8 selected_index)
}
/**
* @brief 渲染修改时间闪烁调节页面 (调节小时或分钟)
* @brief 渲染修改时间/日期闪烁调节页面
* @param hour 修改状态中的小时
* @param min 修改状态中的分钟
* @param selection 当前正在编辑聚焦的属性位 (1: 修改小时, 2: 修改分钟)
* @param day 修改状态中的日
* @param month 修改状态中的月
* @param year 修改状态中的年 (0~99代表 2000~2099)
* @param weekday 根据当前日期算出的星期 (0=周日, ..., 6=周六)
* @param selection 当前正在编辑聚焦的属性位 (1:小时, 2:分钟, 3:日, 4:月, 5:年)
* @param blink_on 当前 500ms 控制隐现的闪烁周期标志位 (1-绘制正常字0-以空格消隐该数值字符)
*/
void UI_ShowSetTimePage(u8 hour, u8 min, u8 selection, u8 blink_on)
// 小时/分钟数字在屏幕上的固定坐标 (16x32 字体,两位数字宽度为 32px)
#define SET_TIME_DIGIT_Y 100
#define SET_TIME_DIGIT_W 32
#define SET_TIME_DIGIT_H 32
#define SET_TIME_HOUR_X 23
#define SET_TIME_MIN_X 75
void UI_ShowSetTimePage(u8 hour, u8 min, u8 day, u8 month, u8 year, u8 weekday, u8 selection, u8 blink_on)
{
char time_str[6];
char date_str[13];
LCD_Clear(COLOR_BLACK);
// 绘制绿锁头等背景静态信息
@@ -377,40 +456,72 @@ void UI_ShowSetTimePage(u8 hour, u8 min, u8 selection, u8 blink_on)
LCD_ShowString(77, 24, "85%", COLOR_GRAY, COLOR_BLACK);
LCD_DrawMonoBitmap(107, 27, 20, 10, bmp_battery_20x10, COLOR_GRAY, COLOR_BLACK);
// ====== 处理调节项一:小时的消隐/显示逻辑 ======
if (selection == 1 && !blink_on)
{
// 聚焦在小时且处于灭周期,用空格隐藏数值,形成数值跳变闪烁调节感
time_str[0] = ' ';
time_str[1] = ' ';
}
else
{
// 整页绘制时,各数字一律完整显示 (消隐闪烁效果交给 UI_UpdateSetTimeDigit 局部刷新处理)
time_str[0] = '0' + (hour / 10);
time_str[1] = '0' + (hour % 10);
}
time_str[2] = '\0';
LCD_ShowString16x32(23, 100, time_str, COLOR_CYAN, COLOR_BLACK); // 采用青色特大字描绘小时 (在 135px 下整体居中 X = 23)
LCD_ShowString16x32(SET_TIME_HOUR_X, SET_TIME_DIGIT_Y, time_str, COLOR_CYAN, COLOR_BLACK); // 采用青色特大字描绘小时 (在 135px 下整体居中 X = 23)
// 绘制中间冒号 (在 135px 下整体居中 X = 59)
LCD_ShowString16x32(59, 98, ":", COLOR_WHITE, COLOR_BLACK);
// ====== 处理调节项二:分钟的消隐/显示逻辑 ======
if (selection == 2 && !blink_on)
{
// 聚焦在分钟且处于灭周期,用空格隐藏数值
time_str[0] = ' ';
time_str[1] = ' ';
}
else
{
time_str[0] = '0' + (min / 10);
time_str[1] = '0' + (min % 10);
}
time_str[2] = '\0';
LCD_ShowString16x32(75, 100, time_str, COLOR_CYAN, COLOR_BLACK); // 采用青色特大字描绘分钟 (在 135px 下整体居中 X = 75)
LCD_ShowString16x32(SET_TIME_MIN_X, SET_TIME_DIGIT_Y, 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);
// 编辑日期时,日期栏额外带上年份 ("WWW DD-MM-YY"),方便看清正在改的年份数值
Build_DateStringWithYear(date_str, weekday, day, month, year);
LCD_ShowStringCentered(DATE_BAR_Y, date_str, COLOR_GRAY, COLOR_BLACK);
}
/**
* @brief 局部刷新时间设置界面里"正在调节的那一个"字段 (小时/分钟/日/月/年)
* @param weekday 根据当前日期算出的星期 (0=周日, ..., 6=周六),调节日/月/年时顺带刷新星期缩写
* @param selection 1:小时, 2:分钟, 3:日, 4:月, 5:年
* @param blink_on 1:显示数值, 0:消隐(涂黑),用于 500ms 周期闪烁效果
* @details 小时/分钟用 32x32px 大字体区域;日/月/年用日期栏里对应的 16x16px 小字体区域,
* 都只重画自己那一小块,不触碰锁头/电池/冒号/AM-PM/星期文字等其它内容,
* 避免每次闪烁或调值都整屏 LCD_Clear() 造成全屏闪烁。
*/
void UI_UpdateSetTimeDigit(u8 hour, u8 min, u8 day, u8 month, u8 year, u8 weekday, u8 selection, u8 blink_on)
{
char buf[3];
u16 x;
u16 y;
u16 w;
u16 h;
u8 value;
switch (selection) {
case 1: x = SET_TIME_HOUR_X; y = SET_TIME_DIGIT_Y; w = SET_TIME_DIGIT_W; h = SET_TIME_DIGIT_H; value = hour; break;
case 2: x = SET_TIME_MIN_X; y = SET_TIME_DIGIT_Y; w = SET_TIME_DIGIT_W; h = SET_TIME_DIGIT_H; value = min; break;
case 3: x = DATE_BAR_DAY_X; y = DATE_BAR_Y; w = DATE_BAR_FIELD_W; h = 16; value = day; break;
case 4: x = DATE_BAR_MONTH_X; y = DATE_BAR_Y; w = DATE_BAR_FIELD_W; h = 16; value = month; break;
default: x = DATE_BAR_YEAR_X; y = DATE_BAR_Y; w = DATE_BAR_FIELD_W; h = 16; value = year; break;
}
// 调节日/月/年时,星期缩写可能因为日期变化而改变,顺带刷新一下 (不参与闪烁,始终显示)
if (selection >= 3) {
LCD_FillRect(DATE_BAR_START_X, DATE_BAR_Y, 24, 16, COLOR_BLACK);
LCD_ShowString(DATE_BAR_START_X, DATE_BAR_Y, Lang_Get((StringID)(STR_WEEKDAY_SUN + weekday)), COLOR_GRAY, COLOR_BLACK);
}
if (!blink_on) {
// 灭周期:直接把这一小块涂黑,形成消隐效果
LCD_FillRect(x, y, w, h, COLOR_BLACK);
return;
}
buf[0] = '0' + (value / 10);
buf[1] = '0' + (value % 10);
buf[2] = '\0';
if (selection <= 2) {
LCD_ShowString16x32(x, y, buf, COLOR_CYAN, COLOR_BLACK);
} else {
LCD_ShowString(x, y, buf, COLOR_GRAY, COLOR_BLACK);
}
}

View File

@@ -10,10 +10,13 @@
* @param state 当前的系统运行状态 (STATE_NORMAL/STATE_ARMED/STATE_DISARMED)
* @param hour 当前待显示的小时
* @param min 当前待显示的分钟
* @param weekday 当前星期 (0=周日, ..., 6=周六)
* @param day 当前日 (1~31)
* @param month 当前月 (1~12)
* @details 负责在 120x240 大小的 AMOLED 屏幕上居中绘制巨大的时钟字样,并在顶部
* 绘制对应的状态图标 (如一把打开的锁代表已撤防、闭合的锁代表已布防等)。
*/
void UI_ShowClockPage(SystemState state, u8 hour, u8 min);
void UI_ShowClockPage(SystemState state, u8 hour, u8 min, u8 weekday, u8 day, u8 month);
/**
* @brief 渲染显示传感器对码的二级设备选择菜单页面
@@ -74,12 +77,24 @@ void UI_ShowBindingPage(u32 addr);
void UI_ShowMainMenu(u8 selected_index);
/**
* @brief 渲染显示修改时间页面 (时钟设置编辑状态)
* @brief 渲染显示修改时间/日期页面 (时钟设置编辑状态)
* @param hour 待修改的小时
* @param min 待修改的分钟
* @param selection 当前选中的修改项 (1:修改小时, 2:修改分钟)
* @param day 待修改的日
* @param month 待修改的月
* @param year 待修改的年 (0~99代表 2000~2099)
* @param weekday 根据当前日期算出的星期 (0=周日, ..., 6=周六)
* @param selection 当前选中的修改项 (1:小时, 2:分钟, 3:日, 4:月, 5:年)
* @param blink_on 控制被选中数值以 500ms 周期进行隐现闪烁的周期标志位 (1-显示, 0-消隐)
*/
void UI_ShowSetTimePage(u8 hour, u8 min, u8 selection, u8 blink_on);
void UI_ShowSetTimePage(u8 hour, u8 min, u8 day, u8 month, u8 year, u8 weekday, u8 selection, u8 blink_on);
/**
* @brief 局部刷新时间设置界面里正在调节的那一个字段 (小时/分钟/日/月/年),用于闪烁/调值时避免整屏重绘
* @param weekday 根据当前日期算出的星期 (0=周日, ..., 6=周六),调节日/月/年时顺带刷新星期缩写
* @param selection 1:小时, 2:分钟, 3:日, 4:月, 5:年
* @param blink_on 1:显示数值, 0:消隐(涂黑)
*/
void UI_UpdateSetTimeDigit(u8 hour, u8 min, u8 day, u8 month, u8 year, u8 weekday, u8 selection, u8 blink_on);
#endif // __UI_H__

View File

@@ -85,8 +85,9 @@ void LCD_Init(void)
P1M1 &= ~((1<<0) | (1<<1) | (1<<4) | (1<<5) | (1<<6) | (1<<7));
P1M0 |= ((1<<0) | (1<<1) | (1<<4) | (1<<5) | (1<<6) | (1<<7));
// 设置各控制引脚初始默认电平
SGM_CTRL = 0; // PMOS低电平点亮背光 (LEDA = 0)
// 设置各控制引脚初始默认电平 (LEDA 背光电平不在这里设置——开机路径由 GPIO_Init() 提前
// 关闭、main() 即将进入主循环前才点亮;休眠唤醒路径由 Wakeup_Restore() 在调用本函数前
// 先行点亮两条路径对背光的时序要求不同LCD_Init() 本身不做任何背光电平的假设)
LCD_CS = 1;
LCD_SCL = 1;
LCD_RST = 1;

View File

@@ -256,11 +256,11 @@ void PCF8563_ScanDiagnostic(void)
}
/**
* @brief 从 PCF8563 读取当前时间
* @brief 从 PCF8563 连续读取时间与日期 (秒/分/时/日/星期/月/年,寄存器 0x02~0x08 地址连续)
*/
bit PCF8563_ReadTime(u8 *hour, u8 *min, u8 *sec)
bit PCF8563_ReadDateTime(u8 *hour, u8 *min, u8 *sec, u8 *day, u8 *month, u8 *year)
{
u8 raw_sec, raw_min, raw_hour;
u8 raw_sec, raw_min, raw_hour, raw_day, raw_weekday, raw_month, raw_year;
I2C_Start();
I2C_SendByte(PCF8563_ADDR_WRITE);
@@ -276,12 +276,25 @@ bit PCF8563_ReadTime(u8 *hour, u8 *min, u8 *sec)
if (!I2C_WaitAck()) return 0;
raw_sec = I2C_ReadByte();
I2C_SendAck(1); // 发送 ACK
I2C_SendAck(1);
raw_min = I2C_ReadByte();
I2C_SendAck(1); // 发送 ACK
I2C_SendAck(1);
raw_hour = I2C_ReadByte();
I2C_SendAck(1);
raw_day = I2C_ReadByte();
I2C_SendAck(1);
raw_weekday = I2C_ReadByte(); // 星期寄存器不使用,芯片自身的星期在改日期后可能与实际不符
I2C_SendAck(1);
(void)raw_weekday;
raw_month = I2C_ReadByte();
I2C_SendAck(1);
raw_year = I2C_ReadByte();
I2C_SendAck(0); // 发送 NACK 结束连续读取
I2C_Stop();
@@ -290,35 +303,46 @@ bit PCF8563_ReadTime(u8 *hour, u8 *min, u8 *sec)
*sec = BCD2DEC(raw_sec & 0x7F);
*min = BCD2DEC(raw_min & 0x7F);
*hour = BCD2DEC(raw_hour & 0x3F);
*day = BCD2DEC(raw_day & 0x3F);
*month = BCD2DEC(raw_month & 0x1F); // 忽略 bit7 世纪位,固定按 2000~2099 处理
*year = BCD2DEC(raw_year);
return 1;
}
/**
* @brief 将时间精准写入 PCF8563 RTC 芯片
* @brief 将时间与日期一次性连续写入 PCF8563 RTC 芯片 (寄存器 0x02~0x08 共 7 字节连续写入)
*/
bit PCF8563_WriteTime(u8 hour, u8 min, u8 sec)
bit PCF8563_WriteDateTime(u8 hour, u8 min, u8 sec, u8 day, u8 month, u8 year, u8 weekday)
{
I2C_Start();
I2C_SendByte(PCF8563_ADDR_WRITE);
if (!I2C_WaitAck()) return 0;
// 定位写入起始寄存器 0x02
I2C_SendByte(REG_VL_SECONDS);
if (!I2C_WaitAck()) return 0;
// 写入秒
I2C_SendByte(DEC2BCD(sec & 0x7F));
if (!I2C_WaitAck()) return 0;
// 写入分
I2C_SendByte(DEC2BCD(min & 0x7F));
if (!I2C_WaitAck()) return 0;
// 写入时
I2C_SendByte(DEC2BCD(hour & 0x3F));
if (!I2C_WaitAck()) return 0;
I2C_SendByte(DEC2BCD(day & 0x3F));
if (!I2C_WaitAck()) return 0;
I2C_SendByte(weekday & 0x07); // 星期不是 BCD直接写 0~6
if (!I2C_WaitAck()) return 0;
I2C_SendByte(DEC2BCD(month & 0x1F)); // bit7 世纪位固定写 0不处理跨世纪
if (!I2C_WaitAck()) return 0;
I2C_SendByte(DEC2BCD(year));
if (!I2C_WaitAck()) return 0;
I2C_Stop();
return 1;

View File

@@ -25,22 +25,29 @@
bit PCF8563_Init(void);
/**
* @brief 从 PCF8563 芯片读取当前时间 (时、分、秒)
* @brief 从 PCF8563 芯片连续读取时间与日期 (时/分/秒/日/月/年)
* @param hour 小时指针 (0~23)
* @param min 分钟指针 (0~59)
* @param sec 秒钟指针 (0~59)
* @param day 日指针 (1~31)
* @param month 月指针 (1~12)
* @param year 年指针 (0~99代表 2000~2099不处理世纪位)
* @return bit 1: 读取成功, 0: 通讯失败
*/
bit PCF8563_ReadTime(u8 *hour, u8 *min, u8 *sec);
bit PCF8563_ReadDateTime(u8 *hour, u8 *min, u8 *sec, u8 *day, u8 *month, u8 *year);
/**
* @brief 将指定的时间 (时、分、秒) 写入 PCF8563 芯片
* @brief 将指定的时间与日期一次性连续写入 PCF8563 芯片
* @param hour 小时 (0~23)
* @param min 分钟 (0~59)
* @param sec 秒钟 (0~59)
* @param day 日 (1~31)
* @param month 月 (1~12)
* @param year 年 (0~99代表 2000~2099)
* @param weekday 星期 (0~6需与软件侧约定的星期数值一致芯片本身不做校验)
* @return bit 1: 写入成功, 0: 通讯失败
*/
bit PCF8563_WriteTime(u8 hour, u8 min, u8 sec);
bit PCF8563_WriteDateTime(u8 hour, u8 min, u8 sec, u8 day, u8 month, u8 year, u8 weekday);
/**
* @brief I2C 总线器件扫描诊断函数

View File

@@ -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,13 @@ 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 +46,30 @@ void RF_Init(void)
P3M0 &= ~(1 << 6);
P3M0 |= ((1 << 5) | (1 << 7));
// P3.6 内部下拉SHUT=1(芯片关断)期间 DATA 脚高阻悬空,下拉把悬空状态钳在低电平。
// 之前误用上拉,配合当年错误的"高电平中断"配置,悬空高电平会导致中断风暴——
// 现在确认端口中断硬件只有下降沿/上升沿/低电平/高电平四选一,没有真正的"双边沿"模式,
// 已改为下面的软件乒乓单边沿切换方案,配下拉才是让悬空态落在"不触发"一侧的正确选择。
P3PD |= 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) 中断常态开启。硬件 PxIM1/PxIM0 只支持下降沿/上升沿/低电平/高电平
// 四选一,没有"双边沿都触发"的模式;(IM1=1,IM0=1) 实际是"高电平中断",会在整个高电平
// 期间不断重复触发,这正是之前边沿计数异常偏高(远超协议理论值)的真正原因。
// 现改为初始配置成"上升沿触发",配合 RF_HandleEdgeInterrupt() 里的软件乒乓切换
// (每次触发后立即翻转到相反边沿) 来模拟真正的双边沿触发,每次真实翻转只精确响应一次。
P3IM1 &= ~0x40; // IM1=0
P3IM0 |= 0x40; // IM0=1 => (0,1) 上升沿触发,先等待同步头/数据位高电平的起始
P3INTF = 0x00; // 清除挂起的中断标志
P3INTE |= 0x40; // 开启 P3.6 中断允许 (全局 EA 由 Timer1_Init() 统一开启)
}
/**
@@ -76,18 +99,263 @@ 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 边沿中断增量解码状态机 (真正的单边沿触发版本)
* @details 硬件 PxIM1/PxIM0 只有下降沿/上升沿/低电平/高电平四选一,没有"双边沿都触发"
* 的模式,之前 (IM1=1,IM0=1) 其实是"高电平中断",会在一整段高电平期间反复
* 重触发——这才是边沿计数远超协议理论值 50 的真正原因,与信号噪声无关(示波器已
* 确认信号干净)。现在改为 RF_Init() 里先配置成单一边沿(上升沿)触发,每次触发后
* 在本函数里立即软件翻转到相反边沿(乒乓切换),从而让每一次真实的电平翻转都精确
* 触发一次中断,不多不少。由于翻转前的 P3IM0 状态本身就明确告诉了我们"刚刚发生的
* 是哪种边沿",不再需要像旧版那样读取电平猜测方向,也不需要毛刺过滤器。
* ========================================================================= */
// 计时阈值 (单位: tick0.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 (1200UL * 2UL) // 1200us
#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;
// 简易运行计数器:解码成功帧数 / 因校验不通过而放弃的候选帧数,供 'e' 调试指令查看
static volatile u32 dbg_frame_count = 0;
static volatile u32 dbg_abort_count = 0;
/**
* @brief DEBUG: 打印边沿解码状态机的简易运行计数器
*/
void RF_PrintDebugCounters(void)
{
Uart_SendString("[RF-DBG] state=");
Uart_SendHex8(rf_isr_state);
Uart_SendString(" bit_idx=");
Uart_SendHex8(rf_isr_bit_index);
Uart_SendString(" frames=");
Uart_SendHex32(dbg_frame_count);
Uart_SendString(" aborts=");
Uart_SendHex32(dbg_abort_count);
Uart_SendString("\r\n");
}
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 处理"刚刚结束的一段高电平"(即将开始新的低电平)
* @param duration 刚结束的高电平宽度 (tick)
*/
static void RF_HandleHighSegmentEnd(u32 duration)
{
if (rf_isr_state == RF_ISR_STATE_IDLE)
{
// 空闲搜索阶段:只关心刚结束的高电平脉宽是否落在同步头候选区间
if (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_COLLECTING)
{
// 本位的高电平段先暂存,等对应的低电平段结束后一起判定这一位
rf_isr_pending_high = duration;
return;
}
// SYNC_SEEN 阶段理应只等待低电平段 (乒乓切换保证了 高/低 严格交替出现)
// 不会在这里收到高电平结束事件;防御性地放弃当前候选,避免状态卡死
RF_ResetDecodeState();
dbg_abort_count++;
}
/**
* @brief 处理"刚刚结束的一段低电平"(即将开始新的高电平)
* @param duration 刚结束的低电平宽度 (tick)
*/
static void RF_HandleLowSegmentEnd(u32 duration)
{
if (rf_isr_state == RF_ISR_STATE_IDLE)
{
return; // 同步头候选还没确立前,空闲期低电平时长任意,不关心
}
if (rf_isr_state == RF_ISR_STATE_SYNC_SEEN)
{
// 期望这段低电平是同步头的 31T 低电平
if (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(); // 不满足同步条件,放弃候选,回到空闲搜索
dbg_abort_count++;
}
return;
}
if (rf_isr_state == RF_ISR_STATE_COLLECTING)
{
u32 total = rf_isr_pending_high + duration;
if (total < rf_isr_bit_T * 3UL || total > rf_isr_bit_T * 6UL)
{
RF_ResetDecodeState(); // 单个数据位周期超出 3T~6T 范围,判定畸变,放弃本帧
dbg_abort_count++;
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(); // 电平脉冲比值不合规,放弃本帧
dbg_abort_count++;
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;
dbg_frame_count++;
RF_ResetDecodeState();
}
}
}
/**
* @brief P3.6 (RF_RX_DATA) 边沿中断处理 (由 main.c 的 Port3_Isr 调用)
* @details 每次触发时P3IM0 的当前值就是"触发前配置的是哪种边沿"——因为软件每次都会
* 在处理完当前事件后立即翻转到相反边沿,所以这个值精确对应"刚刚发生的是上升沿
* 还是下降沿",不需要额外读取电平判断方向。翻转完成后再把 (方向, 距上次边沿的
* 时间差) 喂给增量解码状态机。
*/
void RF_HandleEdgeInterrupt(void)
{
u32 now;
u32 duration;
bit was_rising;
now = Timer0_GetTimestamp();
was_rising = (P3IM0 & 0x40) ? 1 : 0;
duration = now - rf_isr_last_edge_ts;
rf_isr_last_edge_ts = now;
// 乒乓切换触发边沿,为下一次真实翻转做准备 (模拟双边沿触发)
if (was_rising) {
P3IM0 &= ~0x40; // 现在已进入高电平,下一次要抓它结束 (改为下降沿触发)
} else {
P3IM0 |= 0x40; // 现在已进入低电平,下一次要抓它结束 (改为上升沿触发)
}
if (was_rising) {
RF_HandleLowSegmentEnd(duration); // duration = 刚结束的低电平宽度
} else {
RF_HandleHighSegmentEnd(duration); // duration = 刚结束的高电平宽度
}
}
/**
* @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) 中增量完成;
* 本函数只是原子地检查并取出中断解码结果,可在主循环中高频调用而不会造成阻塞。
*/
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;
}
/**
@@ -154,146 +422,6 @@ void EV1527_Transmit(u32 addr, u8 dat)
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 收发诊断测试功能 (支持发送与接收监听打印)
*/

View File

@@ -40,19 +40,21 @@ 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 硬件 PxIM1/PxIM0 只支持下降沿/上升沿/低电平/高电平四选一,没有真正的"双边沿"模式;
* 本函数在每次触发后都会立即把 P3IM0 翻转到相反边沿(乒乓切换),模拟出双边沿触发的效果,
* 并据此驱动内部的三态增量解码状态机 (空闲搜索 -> 同步头确认 -> 逐位收集),凑齐 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);
@@ -66,4 +68,9 @@ void RF_DiagnosticMode(char choice);
*/
void Loopback_Test(void);
/**
* @brief DEBUG: 打印边沿解码状态机的简易运行计数器 (当前状态/位索引/成功帧数/放弃次数)
*/
void RF_PrintDebugCounters(void);
#endif // __RF_H__

111
Drivers/timer.c Normal file
View File

@@ -0,0 +1,111 @@
#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;
bit tf, ea_bak;
do {
ovf1 = timer0_ovf_count;
h1 = TH0;
l = TL0;
h2 = TH0;
ovf2 = timer0_ovf_count;
} while (h1 != h2 || ovf1 != ovf2);
// 上面的撕裂读保护只处理了 TH0/TL0 两字节配对读取时的撕裂,但还有另一种更隐蔽的竞态:
// 硬件计数器已经物理溢出归零 (TH0/TL0 已经是溢出后的小数值),但 Timer0_Isr 因为中断延迟
// 还没来得及把 timer0_ovf_count 加一,导致算出来的时间戳比真实值凭空小了一整个溢出周期
// (65536 tick ≈ 32.768ms),看起来像是"时间倒流"。这里读一次硬件溢出标志位 TF0 做补偿:
// 若 TF0 已经置位但还未被服务,本次读数临时按 ovf+1 计算,不清 TF0留给真正的
// Timer0_Isr 之后正常执行、更新持久计数,不影响后续调用。
ea_bak = EA;
EA = 0;
tf = TF0;
EA = ea_bak;
if (tf) {
ovf1++;
}
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; // 仅在中断里置起标志位,留待主循环中执行实际读取与消抖
}
}

31
Drivers/timer.h Normal file
View File

@@ -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__

222
work-order/wo-04 Normal file
View File

@@ -0,0 +1,222 @@
# 📋 工单 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()` 移除上拉恢复)。
状态:**实机验证通过**。连续多次真实发射(Data=08/04/02/01对应遥控器不同按键)均被稳定解码,`[RF_Monitor] Decoded Addr=00037A86, Data=xx` 持续正确输出,未见中断风暴或崩溃。至此阶段 2(RF 接收中断化改造)的核心解码问题已定位并修复完成。
## 六、阶段 1 实现备注
* 语言字节与传感器数据库共用同一 Flash 扇区(偏移地址为 `sizeof(sensor_list)`,当前为 352,扇区剩余空间足够),复用现有 `Load_Database`/`Save_Database`,未新增独立扇区。
* 未做二级选择子菜单:主菜单第 1 项选中"语言"时直接按■原地切换 FR/EN,立即保存并用新语言重绘当前菜单。
* 首次开机(Flash 从未写入过语言字节,读到擦除态 `0xFF`)时 `Lang_Init` 会自动回退为 `LANG_FR`,不会出现非法枚举值。
* 本阶段仅替换界面文案与新增语言状态管理,未改动任何按键状态机、事件队列或射频协议相关代码逻辑。
* 尚未实机编译烧录验证,第四节验收标准仍需在真实硬件上逐项确认。

82
work-order/wo-05 Normal file
View File

@@ -0,0 +1,82 @@
# 工单 WO-05细节问题修复清单
**优先级**:中
**状态**:代码已完成 (6 项全部实现),待实机烧录逐项验证
---
## 问题列表
1. 开机启动瞬间背光应保持关闭,等所有初始化都完成、即将进入 `while(1)` 主循环前才点亮,避免开机闪烁。
2. 长按 KEY_CONFIRM 打开主菜单后10 秒内无任何按键操作,自动关闭菜单返回时钟界面。
3. 按 App 重新梳理菜单导航结构menu_app 只保留 4 个入口(语言开关内置于 menu_app其余 3 项各自跳转到独立的 App),每个子 App 统一用长按 CONFIRM 退出、回到自己的父 App而不是像现在这样有的退回上一级、有的直接跳回主菜单/首页,规则不统一。
4. 默认菜单语言改为英文(目前首次开机/Flash 擦除态默认是法语)。
5. 主菜单上下键切换菜单项允许循环切换(到顶/底后可以绕到另一端)。
6. P3.6 进入休眠前改成电平触发方式作为唤醒源,现在经常唤醒不了处理器。
---
## 实现思路
### 1. 背光延迟点亮
- `Drivers/lcd.c` 的 `LCD_Init()` 第 89 行 `SGM_CTRL = 0;`(点亮背光)在整个 ST7789V 寄存器初始化序列**最开头**就执行了,这就是闪烁的直接原因——去掉这一行(或改成 `SGM_CTRL = 1` 熄灭)。
- `App/system.c` 的 `GPIO_Init()` 只把 P1.7 配置成推挽输出,没有写初始电平,上电瞬间电平不确定;补一行 `SGM_CTRL = 1;`(熄灭),让背光从上电第一刻就确定是关的。
- `App/main.c` 的 `main()` 里,在 `AppManager_StartApp(APP_ID_HOME)`(画出第一帧时钟界面,若第 3 条改名重构先落地)之后、`while(1)` 之前,加一行 `SGM_CTRL = 0;` 点亮背光。
### 2. 菜单 10 秒无操作自动关闭
- 现有的 `inactivity_timer`(`App/main.c`)是全局 60 秒深度休眠计时,粒度和用途都不一样,不能直接复用。
- 在 `App/app_menu.c` 里新增一个 `static u16 menu_idle_enter_ms`,仿照 `Drivers/rf.c` 的 `RF_CheckDecodeWatchdog()`/`app_menu.c` 里已有的 `last_bind_tx_ms` 那种 `ms_tick` 环绕安全减法模式(`ms_tick` 是 0~999 循环的 1ms 计时)。
- `MenuApp_onEvent()` 每收到一次按键事件就刷新这个时间戳。
- 需要一个周期性检查点:目前 AppManager 框架没有 `OnTick`/`OnUpdate` 回调,需要确认要不要新增一个,或者简单地在 `main.c` 的 `while(1)` 里,当前台 App 是 `menu_app` 时调用一个新增的 `MenuApp_CheckIdleTimeout()`。超时后调用 `AppManager_SwitchToForeground(&home_app)`(若第 3 条改名重构先落地,否则仍是 `&clock_app`)。
### 3. 按 App 重新梳理菜单导航结构
**现状问题**`app_menu.c` 用一个 `menu_level`(0/1/2)在单个 App 内部模拟出三层菜单,其中 `menu_level=1` 短按 CONFIRM 会切换到独立的 `pair_app``pair_app` 退出时又统一调用 `AppManager_SwitchToForeground(&menu_app)`,而 `MenuApp_OnStart()`/`MenuApp_onClose()` 都会无条件把 `menu_level` 清零——导致不管从哪个子菜单退出,最后都被强制拉回顶层主菜单,退出规则不统一,也不符合"退出到上一级"的预期。时间设置(`Start_Time_Edit()`)则是另一套模式:混在 `clock_app` 内部用 `time_edit_active` 标志位实现,进入时还要先 `AppManager_SwitchToForeground(&clock_app)` 闪一下正常时钟画面再叠加编辑界面,也是一处潜在的画面闪烁点。
**改造方向 —— 完全去掉 `menu_level` 这个概念,统一按 App 组织,父子关系一致、退出手势一致**`clock_app` 已经确认是开机默认显示的 App(`App/main.c:280` 开机调用 `AppManager_StartApp(APP_ID_CLOCK)`),直接更名为 `home_app`;从中新建一个专门的 `set_clock_app` 承载时间设置。
| App | 现状 | 改造后 |
|---|---|---|
| `home_app`(时钟主页) | 即现有 `clock_app`(`App/app_clock.c`),兼管时间编辑(`time_edit_active`) | 整体更名:文件 `App/app_clock.c`→`App/app_home.c`,结构体实例 `clock_app`→`home_app``APP_ID_CLOCK`→`APP_ID_HOME`,函数前缀 `ClockApp_*`→`HomeApp_*`;剥离时间编辑逻辑,只保留待机时钟显示与布撤防 |
| `menu_app`(主菜单) | 内部 `menu_level` 0/1/2 三态,语言开关 + 传感器子菜单 + 摄像头绑定全挤在一个 App 里 | 不再有 `menu_level`,只保留顶层 4 项列表(`menu_select` 0~3语言开关内置处理不跳转);其余 3 项短按 CONFIRM 各自切到独立 App |
| `set_clock_app`(时间设置) | 不是独立 App是 `clock_app` 内的一个模式标志(`time_edit_active`) | 新建独立 App(`App/app_set_clock.c`),从 `app_clock.c` 抽出 `Start_Time_Edit`/`time_edit_*` 相关代码 |
| `pair_app`(传感器配对) | 已经是独立 App但传感器类型选择这一步(门磁/PIR/烟感/气感/水浸)现在留在 `menu_app` 的 `menu_level=1` 里,选完才切到 `pair_app` 开始搜索 | 把类型选择合并成 `pair_app` 的新初始状态(比如 `PAIR_STATE_SELECT_TYPE`,排在现有 `PAIR_STATE_WAIT` 之前)`menu_app` 上"传感器配对"这一项短按 CONFIRM 直接整体切到 `pair_app``pair_app` 不管在哪个状态退出,都统一回到 `menu_app` |
| `ipc_bind_app`(摄像头绑定) | 不是独立 App是 `menu_app` 内 `menu_level=2` 的一段代码(呼吸灯+定时发射绑定帧) | 新建独立 App(`App/app_ipc_bind.c`),仿照 `pair_app` 的结构抽出来 |
**统一导航规则**
- `home_app` 长按 CONFIRM → 进入 `menu_app`。
- `menu_app` 长按 CONFIRM → 回到 `home_app`(现有行为,不变,只是改了名字)。
- `menu_app` 顶层短按 CONFIRM 选中某一项 → 进入对应子 App(`set_clock_app`/`pair_app`/`ipc_bind_app`)。
- 三个子 App 无论是长按 CONFIRM 主动退出、还是完成/超时自动退出(`pair_app` 的成功/失败/取消),都统一 → 回到 `menu_app`(它们共同的父 App),不再有"直接跳回首页"或"退到某个具体层级"的特殊情况。
**需要新增/修改的文件**
- `App/app_clock.c` → 更名为 `App/app_home.c``clock_app`→`home_app`、`APP_ID_CLOCK`→`APP_ID_HOME`、`ClockApp_*`→`HomeApp_*` 函数全部改名;去掉时间编辑相关代码(`time_edit_active`/`time_edit_field`/`time_edit_blink_on`/`Start_Time_Edit`),长按 CONFIRM 改为切到 `menu_app`(不变)。
- 新建 `App/app_set_clock.c`:承接从 `app_clock.c` 抽出来的时间编辑逻辑,退出(短按 CONFIRM 保存 或 长按退出)统一改为 `AppManager_SwitchToForeground(&menu_app)`。
- 新建 `App/app_ipc_bind.c`:把 `app_menu.c` 里 `menu_level==2` 的呼吸灯效果、`EV1527_Transmit` 绑定广播、`last_bind_tx_ms` 计时都搬过去,退出统一改为 `AppManager_SwitchToForeground(&menu_app)`。
- 精简 `App/app_menu.c`:彻底去掉 `menu_level` 变量和三态分支,只保留 `menu_select` 这一个 0~3 的顶层索引;短按 CONFIRM 按 `menu_select` 分支跳转到三个子 App 或原地切语言。
- `App/app_manager.h`(或对应头文件)`APP_ID_CLOCK`→`APP_ID_HOME`,新增 `APP_ID_SET_CLOCK`/`APP_ID_IPC_BIND``extern WristbandApp clock_app` 等声明一并改名/新增。
- 全仓库搜索 `clock_app`/`APP_ID_CLOCK` 的引用点(至少 `App/main.c`、`App/app_menu.c`、`App/app_pair.c`、`App/event.c` 中出现过)统一改名,避免漏改。
- `wristband.uvproj`:文件改名(`app_clock.c`→`app_home.c`)与两个新文件(`app_set_clock.c`、`app_ipc_bind.c`)都要同步维护工程文件列表,不能像之前 `isr_jump.asm` 那样漏加。
**待确认的行为变化**:现在"时间设置"短按 CONFIRM 保存后,是直接回到时钟主页;改造后按统一规则会回到 `menu_app`(主菜单)而不是直接回时钟——这是一个和现状不一样的地方,需要确认是否符合预期,还是希望 `set_clock_app` 作为特例、保存后也直接回 `home_app`。
### 4. 默认语言改英文
- `App/language.c``static Language current_lang = LANG_FR;` 改成 `LANG_EN`。
- 同文件 `Lang_Init()` 里的越界回退分支 `(lang < LANG_COUNT) ? lang : LANG_FR` 改成 `: LANG_EN`。
- 两处都要改,否则开机静态初始值和 Flash 擦除态回退值会不一致。
### 5. 菜单上下循环切换
- 涉及两处 UP/DOWN 边界判断(若第 3 条的 App 化重构先落地,第二处会搬到 `pair_app` 里,但改法一样)
- `App/app_menu.c` 顶层菜单(现第 106-121 行4 项,索引 0~3)
- 传感器类型选择(现在是 `app_menu.c` 的 `menu_level==1`,第 158-172 行;重构后是 `pair_app` 的 `PAIR_STATE_SELECT_TYPE`5 项,索引 0~4)
- 现在是 `if (menu_select > 0) menu_select--;` / `if (menu_select < MAX) menu_select++;`,到边界就不动;改成到边界时绕到另一端,例如 UP 在 `menu_select==0` 时跳到 `MAX`DOWN 在 `menu_select==MAX` 时跳回 `0`。
### 6. P3.6 休眠前唤醒触发方式修复
- 原因:`RF_HandleEdgeInterrupt()` 用软件乒乓单边沿切换模拟双边沿触发,`P3IM0` 会在上升沿/下降沿之间动态翻转;`Enter_Low_Power_Sleep()` 进入休眠前完全没有重新设置 `P3IM0`/`P3IM1`,导致进入 Power Down 那一刻 `P3IM0` 具体停在哪个方向是不确定的,如果停在"等下降沿"而实际先来的是上升沿,就可能唤醒不了或唤醒延迟。
- **已实现方案与最初需求不同,供确认**:根据 WO-04 §2.9 查到的《STC32G 技术手册》15.1.3 节真值表,`PxIM1/PxIM0` 的"高电平中断"和"低电平中断"两种模式**都标注为不支持掉电唤醒**,只有下降沿/上升沿两种边沿模式支持。既然手册明确说电平模式不支持掉电唤醒,改成电平触发大概率解决不了"唤醒不了"的问题,所以最终没有采用"改成电平触发",而是实现了手册认可的方案:`App/system.c` 的 `Enter_Low_Power_Sleep()` 里,`P3WKUE |= 0x40;` 之前,把 `P3IM1`/`P3IM0` 强制固定为上升沿 (`P3IM1&=~0x40; P3IM0|=0x40;`,与 `RF_Init()` 开机初始配置一致),不再依赖乒乓切换留下的随机状态。`Wakeup_Restore()` 不需要改动,因为固定后的值就是开机时的初始值,唤醒后解码状态机自然从已知状态开始。
- 状态:代码已完成,待实机验证休眠后 P3.6 (RF 信号) 能否稳定唤醒;如果实测仍不能唤醒,再考虑真的去试电平触发(即便手册说不支持)。

21
work-order/wo-06 Normal file
View File

@@ -0,0 +1,21 @@
# 工单 WO-06时间设置界面整屏闪烁改成局部刷新
**优先级**:中
**状态**:代码已完成,待实机验证只有数字局部闪烁、其它元素不受影响
---
## 问题列表
1. 时间设置界面(`set_clock_app`)调整小时/分钟时的闪烁效果,现在是每次闪烁状态翻转就整屏 `LCD_Clear()` 再全部重画,视觉上是整屏黑一下亮一下,而不是只有正在调节的那个数字在闪。
---
## 实现思路
- 根因:`App/ui.c` 的 `UI_ShowSetTimePage()` 函数开头无条件 `LCD_Clear(COLOR_BLACK)`然后重画锁头图标、电池、小时、冒号、分钟、AM/PM、日期全部元素而 `App/app_set_clock.c` 的 `SetClockApp_onRun()` 每 500ms 闪烁状态翻转一次就整个调用这个函数。
- 改法:拆成两个函数——
- 一个"整页绘制"函数,只在真正进入时间设置界面(`SetClockApp_OnStart()`)或调节字段切换(长按 CONFIRM 切换 小时/分钟 焦点)时调用一次负责画锁头、电池、冒号、AM/PM、日期这些不变的静态内容以及当前的小时、分钟数字。
- 一个"局部刷新"函数,只负责重画正在调节的那个数字所在的矩形区域(先 `LCD_FillRect` 成黑色,再按闪烁状态决定是否写回数字)`SetClockApp_onRun()` 里每次闪烁翻转只调用这个局部刷新函数,不再动其它区域。
- 需要确定小时/分钟数字在屏幕上的准确坐标和尺寸(参照现有 `LCD_ShowString16x32(23, 100, ...)` / `LCD_ShowString16x32(75, 100, ...)` 调用16x32 字体两位数字宽度为 32px),局部刷新时精确擦除这一块,不多不少。
- 涉及文件:`App/ui.c`(拆分 `UI_ShowSetTimePage`)、`App/ui.h`(新增局部刷新函数声明)、`App/app_set_clock.c`(`OnStart`/字段切换调用整页绘制,`onRun` 调用局部刷新)。

50
work-order/wo-07 Normal file
View File

@@ -0,0 +1,50 @@
# 工单 WO-07完善日期功能 (年/月/日/星期)
**优先级**:中
**状态**:待处理
---
## 问题列表
1. 软时钟(`App/clock.c`)只有时/分/秒,没有年、月、日、星期。
2. `PCF8563_ReadTime`/`PCF8563_WriteTime` 只读写时分秒三个寄存器,芯片本身支持的日/星期/月/年寄存器(0x05~0x08)从没被用过。
3. `set_clock_app` 只能调小时和分钟,没有日期可调。
4. HOME APP 和时间设置界面上显示的"星期 日-月"是写死的占位字符串 `"VEN 10-07"`/`"FRI 10-07"`,不是真实日期。
---
## 实现思路
### 1. 驱动层:`Drivers/pcf8563.c`/`.h`
PCF8563 的秒/分/时/日/星期/月/年寄存器地址是连续的(0x02~0x08共 7 字节),可以一次 I2C 连续读写全部拿到/写入,不用分开多次收发:
- 新增 `PCF8563_ReadDateTime(u8 *hour, u8 *min, u8 *sec, u8 *day, u8 *month, u8 *year)`:从 0x02 开始连续读 7 字节,`day` 掩码 `0x3F`(BCD)`month` 掩码 `0x1F`(BCD忽略 bit7 世纪位)`year` 直接 BCD 转十进制(0~99代表 2000~2099)。
- 新增 `PCF8563_WriteDateTime(u8 hour, u8 min, u8 sec, u8 day, u8 month, u8 year, u8 weekday)`:从 0x02 开始连续写 7 字节,包含星期(0x06取值 0~6不需要 BCD 转换,直接写数值)。
- 旧的 `PCF8563_ReadTime`/`PCF8563_WriteTime` 保留,内部改成调用新函数并丢弃日期部分,避免改动现有调用点(`app_home.c`/`app_set_clock.c`)。
- 世纪位(月寄存器 bit7)不处理,固定按 2000~2099 年处理,这个产品的使用年限内不用考虑跨世纪。
### 2. 软时钟层:`App/clock.c`/`.h`
- 新增全局变量 `u8 current_day`、`u8 current_month`、`u8 current_year`(0~99代表 2000~2099)、`u8 current_weekday`(0~6周日=0 或周一=0需要定一个约定建议跟 Zeller 公式的定义保持一致)。
- `Clock_IncMS()` 现有的时/分/小时进位链条(秒→分→时)后面继续接上日/月/年进位:小时满 24 归零时,`current_day++`;根据"每月天数表"(`{31, 28或29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}`2 月按闰年规则(能被 4 整除且(不能被 100 整除或能被 400 整除)2000~2099 区间内简化成"能被 4 整除"即可)判断是否要月进位、年进位(99 → 0不处理世纪)。
- 日期发生变化(不管是自然进位还是手动调节)时,用 Zeller 公式或类似算法重新计算 `current_weekday`,不依赖 PCF8563 自己的星期寄存器自增(避免手动改日期后星期跟着错乱);写入 RTC 时把算出来的星期一起写进去,保持芯片内部数据一致。
- `HomeApp_OnStart()`/相关读取 RTC 的地方,从 `PCF8563_ReadTime` 换成 `PCF8563_ReadDateTime`,一并读回日期。
### 3. 界面层:`App/ui.c`/`.h`
- 新增星期缩写字符串表(法语/英语),参照现有占位文案风格:`LUN/MAR/MER/JEU/VEN/SAM/DIM` 对应 `MON/TUE/WED/THU/FRI/SAT/SUN`,放进 `App/strings.h` 的 `WRISTBAND_STRING_LIST`。
- `UI_ShowClockPage()`/`UI_ShowSetTimePage()` 里原来直接 `Lang_Get(STR_CLOCK_DATE)` 显示死文案的地方,改成拼接"星期缩写 + 日-月两位数字"的动态字符串,用真实的 `current_weekday`/`current_day`/`current_month` 现算现拼。
### 4. 时间设置 App`App/app_set_clock.c`
- `time_edit_field` 从 1~2(时/分)扩展到 1~5(时/分/日/月/年),长按 CONFIRM 依次循环切换。
- 上下键调节日/月/年时要做合理的范围限制和循环(日 1~当月最大天数、月 1~12、年 0~99),月份变化时如果当前日超过新月份的最大天数要做截断处理(比如从 31 号切到 2 月要变成 28/29 号)。
- 局部刷新沿用 WO-06 刚做的模式,不要整屏重绘:日/月/年每个字段各自对应屏幕上一小块数字区域,只刷新当前正在调节的那一块。
- 保存(短按 CONFIRM)时用新的 `PCF8563_WriteDateTime()`,同时把算好的 `current_weekday` 一起写入。
### 5. 需要确认的点
- 星期数值约定(0=周日 还是 0=周一)以及对应的 Zeller 公式实现需要仔细算一遍避免算错,建议实现后用几个已知日期(比如今天)手动核对星期是否正确。
- 日期显示格式沿用现有"星期 日-月"(如 "MON 03-08")这种顺序,不显示年份(屏幕位置有限);如果需要年份显示,要另外找位置或者做成可选切换。

View File

@@ -73,7 +73,7 @@
<LExpSel>0</LExpSel>
</OPTXL>
<OPTFL>
<tvExp>0</tvExp>
<tvExp>1</tvExp>
<tvExpOptDlg>0</tvExpOptDlg>
<IsCurrentTarget>1</IsCurrentTarget>
</OPTFL>
@@ -164,7 +164,7 @@
<Group>
<GroupName>App</GroupName>
<tvExp>0</tvExp>
<tvExp>1</tvExp>
<tvExpOptDlg>0</tvExpOptDlg>
<cbSel>0</cbSel>
<RteFlg>0</RteFlg>
@@ -183,6 +183,18 @@
<File>
<GroupNumber>1</GroupNumber>
<FileNumber>2</FileNumber>
<FileType>2</FileType>
<tvExp>0</tvExp>
<tvExpOptDlg>0</tvExpOptDlg>
<bDave2>0</bDave2>
<PathWithFileName>.\App\isr_jump.asm</PathWithFileName>
<FilenameWithoutPath>isr_jump.asm</FilenameWithoutPath>
<RteFlg>0</RteFlg>
<bShared>0</bShared>
</File>
<File>
<GroupNumber>1</GroupNumber>
<FileNumber>3</FileNumber>
<FileType>1</FileType>
<tvExp>0</tvExp>
<tvExpOptDlg>0</tvExpOptDlg>
@@ -194,7 +206,7 @@
</File>
<File>
<GroupNumber>1</GroupNumber>
<FileNumber>3</FileNumber>
<FileNumber>4</FileNumber>
<FileType>1</FileType>
<tvExp>0</tvExp>
<tvExpOptDlg>0</tvExpOptDlg>
@@ -206,7 +218,7 @@
</File>
<File>
<GroupNumber>1</GroupNumber>
<FileNumber>4</FileNumber>
<FileNumber>5</FileNumber>
<FileType>1</FileType>
<tvExp>0</tvExp>
<tvExpOptDlg>0</tvExpOptDlg>
@@ -218,7 +230,7 @@
</File>
<File>
<GroupNumber>1</GroupNumber>
<FileNumber>5</FileNumber>
<FileNumber>6</FileNumber>
<FileType>1</FileType>
<tvExp>0</tvExp>
<tvExpOptDlg>0</tvExpOptDlg>
@@ -230,7 +242,19 @@
</File>
<File>
<GroupNumber>1</GroupNumber>
<FileNumber>6</FileNumber>
<FileNumber>7</FileNumber>
<FileType>1</FileType>
<tvExp>0</tvExp>
<tvExpOptDlg>0</tvExpOptDlg>
<bDave2>0</bDave2>
<PathWithFileName>.\App\language.c</PathWithFileName>
<FilenameWithoutPath>language.c</FilenameWithoutPath>
<RteFlg>0</RteFlg>
<bShared>0</bShared>
</File>
<File>
<GroupNumber>1</GroupNumber>
<FileNumber>8</FileNumber>
<FileType>1</FileType>
<tvExp>0</tvExp>
<tvExpOptDlg>0</tvExpOptDlg>
@@ -242,7 +266,7 @@
</File>
<File>
<GroupNumber>1</GroupNumber>
<FileNumber>7</FileNumber>
<FileNumber>9</FileNumber>
<FileType>1</FileType>
<tvExp>0</tvExp>
<tvExpOptDlg>0</tvExpOptDlg>
@@ -254,7 +278,7 @@
</File>
<File>
<GroupNumber>1</GroupNumber>
<FileNumber>8</FileNumber>
<FileNumber>10</FileNumber>
<FileType>1</FileType>
<tvExp>0</tvExp>
<tvExpOptDlg>0</tvExpOptDlg>
@@ -266,19 +290,31 @@
</File>
<File>
<GroupNumber>1</GroupNumber>
<FileNumber>9</FileNumber>
<FileNumber>11</FileNumber>
<FileType>1</FileType>
<tvExp>0</tvExp>
<tvExpOptDlg>0</tvExpOptDlg>
<bDave2>0</bDave2>
<PathWithFileName>.\App\app_clock.c</PathWithFileName>
<FilenameWithoutPath>app_clock.c</FilenameWithoutPath>
<PathWithFileName>.\App\app_home.c</PathWithFileName>
<FilenameWithoutPath>app_home.c</FilenameWithoutPath>
<RteFlg>0</RteFlg>
<bShared>0</bShared>
</File>
<File>
<GroupNumber>1</GroupNumber>
<FileNumber>10</FileNumber>
<FileNumber>12</FileNumber>
<FileType>1</FileType>
<tvExp>0</tvExp>
<tvExpOptDlg>0</tvExpOptDlg>
<bDave2>0</bDave2>
<PathWithFileName>.\App\app_set_clock.c</PathWithFileName>
<FilenameWithoutPath>app_set_clock.c</FilenameWithoutPath>
<RteFlg>0</RteFlg>
<bShared>0</bShared>
</File>
<File>
<GroupNumber>1</GroupNumber>
<FileNumber>13</FileNumber>
<FileType>1</FileType>
<tvExp>0</tvExp>
<tvExpOptDlg>0</tvExpOptDlg>
@@ -290,7 +326,7 @@
</File>
<File>
<GroupNumber>1</GroupNumber>
<FileNumber>11</FileNumber>
<FileNumber>14</FileNumber>
<FileType>1</FileType>
<tvExp>0</tvExp>
<tvExpOptDlg>0</tvExpOptDlg>
@@ -302,7 +338,19 @@
</File>
<File>
<GroupNumber>1</GroupNumber>
<FileNumber>12</FileNumber>
<FileNumber>15</FileNumber>
<FileType>1</FileType>
<tvExp>0</tvExp>
<tvExpOptDlg>0</tvExpOptDlg>
<bDave2>0</bDave2>
<PathWithFileName>.\App\app_ipc_bind.c</PathWithFileName>
<FilenameWithoutPath>app_ipc_bind.c</FilenameWithoutPath>
<RteFlg>0</RteFlg>
<bShared>0</bShared>
</File>
<File>
<GroupNumber>1</GroupNumber>
<FileNumber>16</FileNumber>
<FileType>1</FileType>
<tvExp>0</tvExp>
<tvExpOptDlg>0</tvExpOptDlg>
@@ -314,7 +362,7 @@
</File>
<File>
<GroupNumber>1</GroupNumber>
<FileNumber>13</FileNumber>
<FileNumber>17</FileNumber>
<FileType>1</FileType>
<tvExp>0</tvExp>
<tvExpOptDlg>0</tvExpOptDlg>
@@ -326,7 +374,7 @@
</File>
<File>
<GroupNumber>1</GroupNumber>
<FileNumber>14</FileNumber>
<FileNumber>18</FileNumber>
<FileType>1</FileType>
<tvExp>0</tvExp>
<tvExpOptDlg>0</tvExpOptDlg>
@@ -340,13 +388,13 @@
<Group>
<GroupName>Drivers</GroupName>
<tvExp>0</tvExp>
<tvExp>1</tvExp>
<tvExpOptDlg>0</tvExpOptDlg>
<cbSel>0</cbSel>
<RteFlg>0</RteFlg>
<File>
<GroupNumber>2</GroupNumber>
<FileNumber>15</FileNumber>
<FileNumber>19</FileNumber>
<FileType>1</FileType>
<tvExp>0</tvExp>
<tvExpOptDlg>0</tvExpOptDlg>
@@ -358,7 +406,7 @@
</File>
<File>
<GroupNumber>2</GroupNumber>
<FileNumber>16</FileNumber>
<FileNumber>20</FileNumber>
<FileType>1</FileType>
<tvExp>0</tvExp>
<tvExpOptDlg>0</tvExpOptDlg>
@@ -370,7 +418,19 @@
</File>
<File>
<GroupNumber>2</GroupNumber>
<FileNumber>17</FileNumber>
<FileNumber>21</FileNumber>
<FileType>1</FileType>
<tvExp>0</tvExp>
<tvExpOptDlg>0</tvExpOptDlg>
<bDave2>0</bDave2>
<PathWithFileName>.\Drivers\timer.c</PathWithFileName>
<FilenameWithoutPath>timer.c</FilenameWithoutPath>
<RteFlg>0</RteFlg>
<bShared>0</bShared>
</File>
<File>
<GroupNumber>2</GroupNumber>
<FileNumber>22</FileNumber>
<FileType>1</FileType>
<tvExp>0</tvExp>
<tvExpOptDlg>0</tvExpOptDlg>
@@ -382,7 +442,7 @@
</File>
<File>
<GroupNumber>2</GroupNumber>
<FileNumber>18</FileNumber>
<FileNumber>23</FileNumber>
<FileType>1</FileType>
<tvExp>0</tvExp>
<tvExpOptDlg>0</tvExpOptDlg>

View File

@@ -329,6 +329,11 @@
<FileType>1</FileType>
<FilePath>.\App\main.c</FilePath>
</File>
<File>
<FileName>isr_jump.asm</FileName>
<FileType>2</FileType>
<FilePath>.\App\isr_jump.asm</FilePath>
</File>
<File>
<FileName>rgb.c</FileName>
<FileType>1</FileType>
@@ -349,6 +354,11 @@
<FileType>1</FileType>
<FilePath>.\App\database.c</FilePath>
</File>
<File>
<FileName>language.c</FileName>
<FileType>1</FileType>
<FilePath>.\App\language.c</FilePath>
</File>
<File>
<FileName>ui.c</FileName>
<FileType>1</FileType>
@@ -365,9 +375,14 @@
<FilePath>.\App\clock.c</FilePath>
</File>
<File>
<FileName>app_clock.c</FileName>
<FileName>app_home.c</FileName>
<FileType>1</FileType>
<FilePath>.\App\app_clock.c</FilePath>
<FilePath>.\App\app_home.c</FilePath>
</File>
<File>
<FileName>app_set_clock.c</FileName>
<FileType>1</FileType>
<FilePath>.\App\app_set_clock.c</FilePath>
</File>
<File>
<FileName>app_menu.c</FileName>
@@ -379,6 +394,11 @@
<FileType>1</FileType>
<FilePath>.\App\app_pair.c</FilePath>
</File>
<File>
<FileName>app_ipc_bind.c</FileName>
<FileType>1</FileType>
<FilePath>.\App\app_ipc_bind.c</FilePath>
</File>
<File>
<FileName>app_alarm.c</FileName>
<FileType>1</FileType>
@@ -409,6 +429,11 @@
<FileType>1</FileType>
<FilePath>.\Drivers\rf.c</FilePath>
</File>
<File>
<FileName>timer.c</FileName>
<FileType>1</FileType>
<FilePath>.\Drivers\timer.c</FilePath>
</File>
<File>
<FileName>adc.c</FileName>
<FileType>1</FileType>