Files
stc32g128k/App/system.c

438 lines
15 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

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

#include "system.h"
#include "rgb.h"
#include "ui.h"
#include "../Drivers/lcd.h"
#include "../Drivers/rf.h"
#include "app_manager.h"
#include "database.h"
// 外部声明的中断唤醒标志 (在 main.c 中定义,用于在中断中记录是谁唤醒了系统)
extern volatile u8 p0_wakeup_flag;
extern volatile u8 p2_wakeup_flag;
extern volatile u8 rf_wakeup_flag;
/**
* @brief 初始化 STC32G 单片机的 GPIO 管脚模式及初始电平
* @details 按照电路原理图对各个端口的输入输出模式、内部上拉、外设扩展功能进行配置。
*/
void GPIO_Init(void)
{
IE = 0x00; // 禁用 CPU 全局中断及常规中断
IE2 = 0x00; // 禁用扩展中断
TCON = 0x00; // 清零定时器控制寄存器,关闭 Timer0/1 的常规控制
TMOD &= 0xF0; // 清零 Timer0 工作模式
TR0 = 0; // 停止 Timer0 运行
ET0 = 0; // 禁用 Timer0 中断允许
AUXR = 0x00; // 辅助寄存器清零,时钟分频恢复默认
INTCLKO = 0x00; // 禁用时钟输出与固定外部中断触发
P_SW1 = 0x00; // 重置外设映射 1 (串口1、SPI、I2C等)
P_SW2 = 0x80; // 开启 EAXFR (Extended Access Flag Register),允许访问 P0PU/P2PU/P0INTE/P2INTE 等扩展 SFR 寄存器
P_SW3 = 0x00; // 重置外设映射 3
// 1. 配置 P0.0 (BAT_ADC) 电池电量采集管脚为高阻输入模式 (M1=1, M0=0)
P0M1 |= (1 << 0);
P0M0 &= ~(1 << 0);
// 2. 配置 P0.1 (KEY_UP), P0.2 (KEY_CONFIRM), P0.3 (KEY_DOWN) 按键引脚为准双向口模式并置 1 (M1=0, M0=0)
P0M1 &= ~((1 << 1) | (1 << 2) | (1 << 3));
P0M0 &= ~((1 << 1) | (1 << 2) | (1 << 3));
KEY_UP = 1;
KEY_CONFIRM = 1;
KEY_DOWN = 1;
// 3. 配置 P2.3 (RGB_DIN) 幻彩灯条控制线为推挽输出模式 (M1=0, M0=1)
P2M1 &= ~(1 << 3);
P2M0 |= (1 << 3);
RGB_DIN = 0; // 默认拉低,保持复位时序状态
// 4. 配置 P2.5 (MOTOR) 振动马达控制端为推挽输出模式 (M1=0, M0=1)
P2M1 &= ~(1 << 5);
P2M0 |= (1 << 5);
MOTOR = 0; // 默认不震动
// 5. 配置 P2.6 (KEY_SOS) 物理求救按键引脚为准双向口模式并置 1 (M1=0, M0=0)
P2M1 &= ~(1 << 6);
P2M0 &= ~(1 << 6);
KEY_SOS = 1;
// 6. 配置 P2.2 (SHUT) 射频芯片使能引脚为推挽输出模式 (M1=0, M0=1)
P2M1 &= ~(1 << 2);
P2M0 |= (1 << 2);
SHUT = 1; // 默认拉高使能开启,允许无线芯片运行
// 7. 配置 P0.1, P0.2, P0.3 和 P2.6 的内部上拉电阻,避免引脚因浮空抖动误唤醒系统
P0PU |= 0x0E; // 开启 P0.1/P0.2/P0.3 上拉
P2PU |= 0x40; // 开启 P2.6 上拉
// 8. 配置 P2.7 (DET) 充电检测脚为高阻输入模式 (M1=1, M0=0)
P2M1 |= (1 << 7);
P2M0 &= ~(1 << 7);
// 9. 配置 P3.0 (UART1_RXD - 输入)、P3.1 (UART1_TXD - 输出) 以及 P3.4 (SGM_CTRL - 屏幕电源控制,输出)
P3M1 &= ~((1 << 0) | (1 << 1) | (1 << 4)); // 清除 M1 相应位
P3M0 &= ~(1 << 0); // 准双向 / 输入模式
P3M0 |= ((1 << 1) | (1 << 4)); // 推挽输出模式 (TXD 和 SGM_CTRL)
}
/**
* @brief 阻塞式毫秒级延时函数 (基于 CPU 频率估算环路次数)
* @param ms 延迟毫秒数
*/
void Delay_ms(u16 ms)
{
u16 i, j;
for (i = 0; i < ms; i++)
// 24MHz 晶振下,双重循环实现约 1ms 延迟
for (j = 12000; j > 0; j--);
}
/**
* @brief 精确的 10 微秒延时函数 (利用 8051 指令循环时间)
*/
void Delay10us(void)
{
unsigned char data i;
_nop_();
i = 30; // 循环常数
while (--i);
}
/**
* @brief 规避 16 位寄存器在读取瞬间发生进位导致的读错问题
* @details 采用双重读取比对法,读取 TH0 -> TL0 -> 再读一次 TH0如果两次高 8 位一致,代表没有发生高低字节间的溢出进位,是安全的。
* @return u16 当前 Timer0 的 16 位计数值
*/
u16 GetTimer0_Safe(void)
{
u8 h1, l, h2;
do {
h1 = TH0;
l = TL0;
h2 = TH0;
} while (h1 != h2); // 当两次 TH0 值相同时说明读取安全,退出循环
return ((u16)h1 << 8) | l;
}
/**
* @brief 基于 Timer0 的精确微秒延时函数
* @param us 延时微秒数
*/
void Delay_us(u16 us)
{
// 根据系统时钟频率及 T0 运行在 12T 模式下计算时钟 Tick 计数量
u16 ticks = (u16)((u32)us * (MAIN_Fosc / 1000000UL) / 12UL);
TL0 = 0; // 清零 Timer0 低字节
TH0 = 0; // 清零 Timer0 高字节
TR0 = 1; // 启动 Timer0 计数
while (GetTimer0_Safe() < ticks); // 等待计数达到目标值
TR0 = 0; // 停止 Timer0
}
/**
* @brief 初始化串口 1 (UART1) 为 115200 波特率,主要用于调试输出
*/
void Uart1_Init(void)
{
// 计算 115200 波特率在定时器 2 (1T模式) 的重装载值
u16 reload = (u16)(65536UL - (MAIN_Fosc / 4 / 115200UL));
SCON = 0x50; // 8位数据位允许串口接收 (REN=1)
AUXR |= 0x01; // 串口 1 波特率发生器选择定时器 2
AUXR |= 0x04; // 定时器 2 工作在 1T 模式 (无分频)
T2L = (u8)reload; // 载入重装载低字节
T2H = (u8)(reload >> 8);// 载入重装载高字节
AUXR |= 0x10; // 启动定时器 2
TI = 0; // 清除串口发送标志位
}
/**
* @brief 串口 1 发送单字节数据,并在发送时屏蔽接收以防自收自发杂波干扰
* @param dat 待发送的数据字节
*/
void Uart_SendByte(u8 dat)
{
REN = 0; // 临时关闭串口接收,避免发送杂波反灌被接收中断误认
SBUF = dat; // 送入串口数据寄存器启动发送
while (!TI);// 阻塞等待 TI (Transmit Interrupt) 变为 1表示发送完毕
TI = 0; // 清除发送中断标志位
RI = 0; // 顺便清零接收标志,确保没有误动作
REN = 1; // 重新开启接收允许
}
/**
* @brief 串口发送字符串,直到遇到 '\0' 结束
* @param s 字符串首地址
*/
void Uart_SendString(char *s)
{
REN = 0; // 临时关闭接收
while (*s) {
SBUF = *s++;
while (!TI); // 等待发送结束
TI = 0;
}
RI = 0;
REN = 1; // 重新开启接收
}
/**
* @brief 非阻塞接收串口字符
* @return char 接收到的字符,若无则返回 0
*/
char Uart_RxChar(void)
{
if (RI) {
char c = SBUF; // 读取接收数据
RI = 0; // 手动清零接收标志位
return c; // 返回字符
}
return 0;
}
/**
* @brief 串口输出 4 位十六进制数 (半字节0x0~0xF)
*/
void Uart_SendHex4(u8 val)
{
val &= 0x0F;
if (val < 10)
Uart_SendByte((u8)('0' + val));
else
Uart_SendByte((u8)('A' + (val - 10)));
}
/**
* @brief 串口输出 8 位十六进制数 (单字节0x00~0xFF)
*/
void Uart_SendHex8(u8 val)
{
Uart_SendHex4(val >> 4); // 输出高四位
Uart_SendHex4(val); // 输出低四位
}
/**
* @brief 串口输出 20 位十六进制数 (射频 24 位地址的有效 20 位表达)
*/
void Uart_SendHex20(u32 val)
{
Uart_SendHex4((u8)(val >> 16));
Uart_SendHex8((u8)(val >> 8));
Uart_SendHex8((u8)val);
}
/**
* @brief 串口输出 32 位十六进制数
*/
void Uart_SendHex32(u32 val)
{
Uart_SendHex8((u8)(val >> 24));
Uart_SendHex8((u8)(val >> 16));
Uart_SendHex8((u8)(val >> 8));
Uart_SendHex8((u8)val);
}
/**
* @brief 串口输出 16 位十六进制数
*/
void Uart_SendHex16(u16 val)
{
Uart_SendHex8((u8)(val >> 8));
Uart_SendHex8((u8)val);
}
/**
* @brief 将数值格式化为 8 位十六进制形式的 "ADDR: 0xXXXXX" 字符串格式并清零尾部
* @param val 十六进制数值
* @param buf 输出接收缓冲区
*/
void FormatHex(u32 val, char *buf)
{
u8 i;
// 拷贝固定前缀 "ADDR: 0x"
buf[0] = 'A'; buf[1] = 'D'; buf[2] = 'D'; buf[3] = 'R'; buf[4] = ':';
buf[5] = ' '; buf[6] = '0'; buf[7] = 'x';
// 循环提取 20 位射频地址对应的 5 个半字节字符
for (i = 0; i < 5; i++) {
u8 nibble = (val >> (4 * (4 - i))) & 0x0F;
if (nibble < 10)
buf[8 + i] = '0' + nibble;
else
buf[8 + i] = 'A' + (nibble - 10);
}
buf[13] = '\0'; // 尾部安全零字符
}
/**
* @brief 系统进入低功耗深度休眠 (Power-Down) 核心逻辑流程
* @details 实施对屏幕、电源芯片、以及各个总线管脚的低功耗关断,配置相应的端口外部唤醒中断,
* 然后将 MCU 的置入 PCON 的 PD 模式,直至被外部硬件动作唤醒。
*/
void Enter_Low_Power_Sleep(void)
{
Uart_SendString("[SYS] Entering low power sleep...\r\n");
// 1. 关闭/熄灭 FCOB 灯条
RGB_Send(0, 0, 0, 0, 0, 0);
// 2. 向 AMOLED 屏幕控制芯片 RM69310 写入 Display OFF 指令 (0x28),关闭屏幕发光
WriteComm(0x28);
Delay_ms(20);
// 3. 屏幕芯片写入 Sleep In 指令 (0x10),使其内部数字逻辑单元进入停机低损耗模式
WriteComm(0x10);
Delay_ms(20);
// 4. 将 P3.4 (SGM_CTRL) 拉低,切断 SGM3833 升压/负压电源芯片的使能,屏幕完全断电
SGM_CTRL = 0;
// 5. 【屏幕接口防漏电配置】:
// 当屏幕控制电源 (SGM3833) 切断后,屏幕内部处于无电状态。
// 如果单片机引脚 (SCL, SDA 等) 依然保持高电平,电流将通过屏幕保护二极管倒灌漏电,不仅会引发数毫安漏电,还会导致屏幕芯片损坏。
// - CS (片选) 必须拉高 (1) 以释放屏幕片选并禁用其输入缓冲器;
// - RST (复位) 必须保持高电平 (1),防止屏幕芯片被持续按在复位状态产生 ~130uA 的数字偏置电流;
// - 时钟 SCL、数据 SDI、命令/数据选择 DCX 拉低 (0) 以防浮空输入。
LCD_CS = 1;
LCD_RST = 1;
LCD_DCX = 0;
LCD_SCL = 0;
LCD_SDI = 0;
// 6. 休眠模式下关闭射频发射、接通接收 (SHUT=0, RF_RX=1),用来在休眠中监听射频警报。
// 此时屏幕和升压负压芯片已被切断,系统内部不存在高频开关纹波的噪底干扰,射频接收灵敏度达到最佳状态。
RF_SetMode(0);
// 开启扩展 SFR 访问权限 (EAXFR = 1)
P_SW2 |= 0x80;
// 7. 配置 P0.1, P0.2, P0.3 (KEY_UP/CONFIRM/DOWN) 端口下降沿低电平触发唤醒源
P0IM1 |= 0x0E;
P0IM0 &= ~0x0E;
P0INTE |= 0x0E; // 开启 P0 端口的中断允许
// 8. 配置 P2.6 (KEY_SOS) 为低电平触发P2.1 (RF_RX_DATA) 为双沿跳变触发唤醒源
// P2.6: IM1=1, IM0=0 (低电平)
// P2.1: IM1=1, IM0=1 (双沿跳变触发,保障任何射频脉冲上升或下降沿都能立刻叫醒 MCU)
P2IM1 |= 0x42;
P2IM0 &= ~0x40;
P2IM0 |= 0x02;
P2INTE |= 0x42; // 开启 P2 端口中断允许
// 9. 【掉电唤醒核心扩展配置】:
P0WKUE |= 0x0E; // 使能 P0.1/P0.2/P0.3 的掉电唤醒 (WakeUp Enable)
P2WKUE |= 0x42; // 使能 P2.6 和 P2.1 的掉电唤醒
// 【漏电控制】P2.1 (RF_RX_DATA) 接入的是射频芯片的数据引脚。在休眠中,
// 若开启了 P2.1 的上拉电阻,当射频芯片拉低引脚时,会产生高达 ~660uA 的强倒灌漏电流。
// 因此这里特地仅开启 P2.6 (SOS) 上拉,而彻底关闭 P2.1 上拉以实现极致微安级待机。
P2PU |= 0x40;
P2IE |= 0x42; // 强制使能 P2.6/P2.1 的内部数字输入通道,确保电平跳变能被硬件捕捉
// 10. 禁用无用的外部中断 2 (因为 STC32G 硬件 INT2 绑定在 P3.6,该物理引脚没有连接射频数据端,我们改由更灵活的 Port2 端口中断处理唤醒)
EX2 = 0;
// 11. 清除 P0 和 P2 的全部中断标志,防止进入休眠瞬间直接触发唤醒
P0INTF = 0x00;
P2INTF = 0x00;
// 清零内存暂存的唤醒源事件标志
p0_wakeup_flag = 0;
p2_wakeup_flag = 0;
rf_wakeup_flag = 0;
// 12. 关闭 Timer1 (系统滴答) 中断。全局中断保持开启 (EA=1) 用作掉电唤醒条件
ET1 = 0;
EA = 1;
// 13. 向电源控制寄存器 PCON 写入 0x02使 MCU 核心进入 Power-Down 模式CPU 停止运转
Uart_SendString("[SYS] Entering Power-Down mode...\r\n");
PCON |= 0x02; // 设定 PD 位,硬件立刻休眠挂起
// 唤醒后必须跟 _nop_() 空操作以占位和刷新指令流水线
_nop_();
_nop_();
_nop_();
_nop_();
// 14. 当有按键按下或无线信号到来时CPU 被唤醒进入对应的 ISR (在 main.c 中执行中断标志保存)
// 中断处理完返回后CPU 接着从本行执行,调用 Wakeup_Restore() 唤醒恢复流程。
Wakeup_Restore();
}
/**
* @brief 系统被中断唤醒后的运行环境恢复逻辑
* @details 负责重新开启屏幕供电、配置 GPIO、重新初始化 LCD 芯片寄存器、重置时间,最终回到 Clock 主界面。
*/
void Wakeup_Restore(void)
{
EAXFR = 1; // 确保允许访问扩展寄存区
// 1. 立即关断所有的唤醒源和端口中断,防止应用运行期间反复因电平抖动误触发中断服务程序
P0INTE = 0x00;
P2INTE = 0x00;
P0WKUE = 0x00; // 清除掉电唤醒源使能
P2WKUE = 0x00;
P0INTF = 0x00; // 清空标志位
P2INTF = 0x00;
EX2 = 0; // 禁用外部 INT2 中断
// 2. 清零掉电专用定时器 (防止定时唤醒)
WKTCH = 0x00;
WKTCL = 0x00;
// 3. 恢复 Timer1 定时器中断允许,重新开启系统基准时基轮询
EA = 1;
ET1 = 1;
// 4. 重置射频工作模式:进入模式 1 (接收模式) 并开启天线,恢复监听外部射频
RF_SetMode(1);
// 打印日志,判断并告知调试串口具体是哪个物理源将手环唤醒的
if ((p0_wakeup_flag & 0x0E) || (p2_wakeup_flag & 0x40))
{
Uart_SendString("[WR] Woken by KEY (P0=");
Uart_SendHex8(p0_wakeup_flag);
Uart_SendString(", P2=");
Uart_SendHex8(p2_wakeup_flag);
Uart_SendString(")\r\n");
}
else if ((p2_wakeup_flag & 0x02) || rf_wakeup_flag)
{
rf_wakeup_flag = 1; // 确认标记为射频解调唤醒
Uart_SendString("[WR] Woken by RF (P2.1)!\r\n");
}
else
{
Uart_SendString("[WR] Woken by TIMER/Other\r\n");
}
// 5. 【屏幕供电软启动】:
// 重新拉高 P3.4 (SGM_CTRL),并为负压电源芯片发送 27 个脉冲将输出稳压开启。
SGM_CTRL = 1;
SGM_SendPulse(27);
// 6. 阻塞延时 200ms。
// 这期间用于避开电源电磁浪涌冲击,等待屏幕正负电压电轨彻底稳定,防止屏幕在初始化时画面出现撕裂或逻辑卡死。
Delay_ms(200);
// 7. 重新调用 LCD_Init() 初始化序列,写入控制偏置、伽马等命令,唤醒 Truly OLED 屏幕控制 IC RM69310
LCD_Init();
// 8. 重置用户的无操作闲置计时器为零
inactivity_timer = 0;
// 9. 更新运行状态为普通时钟,并强制调用时钟界面函数刷屏,返回工作状态
current_state = STATE_NORMAL;
UI_ShowClockPage(current_state, current_hour, current_min);
Uart_SendString("[SYS] Wakeup restored!\r\n");
}
/**
* @brief 掉电休眠用外部中断 2 中断服务子程序
* @details 当配置为 INT2 (P3.6) 跳变触发唤醒时,本函数将捕获唤醒标志,随后迅速关闭中断,
* 防止运行时产生的噪底高频跳变引发中断堆栈溢出。
*/
void INT2_Isr(void) interrupt 10
{
rf_wakeup_flag = 1; // 中断唤醒标志置位,标明由无线射频触发唤醒
EX2 = 0; // 保护机制:进入 ISR 后立即切断中断通道
}