文章目录
一、前言
1.1 项目背景
实时时钟(RTC)是嵌入式系统中非常重要的外设,它能够在主电源断电后依靠备用电池继续运行,为系统提供精确的时间和日期信息。基于RTC的电子万年历与闹钟系统是学习STM32 RTC功能的经典项目,涵盖了日期时间计算、闹钟设置、低功耗管理等核心技术。
1.2 RTC功能特点
- 独立运行:主电源断电后仍可工作
- 低功耗:使用备用电池可运行数年
- 高精度:配合外部晶振精度可达±20ppm
- 丰富中断:支持闹钟、秒中断、溢出中断
- 日历功能:自动计算年月日星期
1.3 应用场景
- 智能手表:时间显示、闹钟提醒
- 智能家居:定时开关控制
- 数据记录仪:时间戳记录
- 工业控制:定时任务调度
1.4 技术栈
| 类别 | 技术/器件 | 说明 |
|---|---|---|
| 主控芯片 | STM32F103C8T6 | ARM Cortex-M3 |
| RTC晶振 | 32.768kHz | 外部低速晶振 |
| 备用电池 | CR2032 | 3V纽扣电池 |
| 显示屏 | LCD1602/OLED | 时间显示 |
| 存储 | 内部Flash | 闹钟数据存储 |
| 开发环境 | Keil MDK-ARM | 集成开发环境 |
1.5 学习目标
通过本教程,你将学会:
- ✅ STM32 RTC外设配置
- ✅ BKP备份寄存器使用
- ✅ 闰年计算与日期推算
- ✅ 闹钟中断处理
- ✅ 低功耗模式应用
二、系统架构设计
2.1 整体架构
2.2 功能模块
三、RTC原理详解
3.1 RTC时钟源
STM32 RTC支持三种时钟源:
- HSE/128:高速外部晶振分频
- LSE:低速外部晶振(32.768kHz,推荐)
- LSI:低速内部RC振荡器(约40kHz)
3.2 RTC预分频器
RTC时钟需要分频到1Hz:
- 32.768kHz / 128 / 256 = 1Hz
- 预分频器1:128
- 预分频器2:256
3.3 RTC寄存器
- RTC_CRH/CRL:控制寄存器
- RTC_PRLH/PRLL:预分频寄存器
- RTC_DIVH/DIVL:分频计数器
- RTC_CNTH/CNTL:32位计数器
- RTC_ALRH/ALRL:闹钟寄存器
四、硬件准备
4.1 硬件清单
| 序号 | 名称 | 数量 | 参考价格 | 说明 |
|---|---|---|---|---|
| 1 | STM32F103C8T6核心板 | 1 | ¥12 | 主控芯片 |
| 2 | 32.768kHz晶振 | 1 | ¥1 | RTC时钟源 |
| 3 | CR2032电池座 | 1 | ¥2 | 备用电源 |
| 4 | CR2032电池 | 1 | ¥3 | 3V备用电池 |
| 5 | LCD1602显示屏 | 1 | ¥8 | 时间显示 |
| 6 | 按键 | 4 | ¥2 | 功能按键 |
| 7 | 有源蜂鸣器 | 1 | ¥2 | 闹钟提示 |
| 8 | 电阻电容 | 若干 | ¥2 | 外围元件 |
4.2 引脚分配
| 功能 | STM32引脚 | 连接对象 | 说明 |
|---|---|---|---|
| OSC32_IN | PC14 | 晶振 | 32.768kHz输入 |
| OSC32_OUT | PC15 | 晶振 | 32.768kHz输出 |
| Vbat | Vbat | 电池 | 备用电源 |
| LCD_RS | PB0 | LCD RS | 寄存器选择 |
| LCD_RW | PB1 | LCD RW | 读写选择 |
| LCD_EN | PB2 | LCD EN | 使能信号 |
| LCD_D0-D7 | PA0-PA7 | LCD D0-D7 | 数据总线 |
| KEY_SET | PB3 | 设置键 | 功能设置 |
| KEY_UP | PB4 | 上键 | 数值增加 |
| KEY_DOWN | PB5 | 下键 | 数值减少 |
| KEY_OK | PB6 | 确认键 | 确认操作 |
| BEEP | PA8 | 蜂鸣器 | 闹钟提示 |
五、核心代码实现
5.1 RTC驱动模块
📄 创建文件:
Hardware/rtc.h
#ifndef __RTC_H
#define __RTC_H
#include "sys.h"
// 时间结构体
typedef struct {
uint16_t year;
uint8_t month;
uint8_t day;
uint8_t hour;
uint8_t minute;
uint8_t second;
uint8_t weekday;
} RTC_Time;
// 闹钟结构体
typedef struct {
uint8_t hour;
uint8_t minute;
uint8_t enable;
uint8_t repeat; // 重复模式
uint8_t snooze; // 贪睡时间(分钟)
} RTC_Alarm;
// 函数声明
void RTC_Init(void);
void RTC_SetTime(RTC_Time *time);
void RTC_GetTime(RTC_Time *time);
void RTC_SetAlarm(RTC_Alarm *alarm);
void RTC_AlarmEnable(uint8_t enable);
uint8_t RTC_GetAlarmFlag(void);
void RTC_ClearAlarmFlag(void);
uint8_t IsLeapYear(uint16_t year);
uint8_t GetWeekday(uint16_t year, uint8_t month, uint8_t day);
#endif
📄 创建文件:
Hardware/rtc.c
#include "rtc.h"
#include "stm32f10x_bkp.h"
#include "stm32f10x_pwr.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_rtc.h"
// 月份天数表(非闰年)
const uint8_t month_days[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
// 基准时间:2000年1月1日 00:00:00
#define BASE_YEAR 2000
/**
* 判断闰年
*/
uint8_t IsLeapYear(uint16_t year)
{
return ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0));
}
/**
* 获取月份天数
*/
uint8_t GetMonthDays(uint16_t year, uint8_t month)
{
if (month == 2 && IsLeapYear(year))
{
return 29;
}
return month_days[month - 1];
}
/**
* 计算星期(蔡勒公式)
*/
uint8_t GetWeekday(uint16_t year, uint8_t month, uint8_t day)
{
if (month < 3)
{
month += 12;
year--;
}
uint16_t y = year % 100;
uint16_t c = year / 100;
uint8_t w = (y + y / 4 + c / 4 - 2 * c + 26 * (month + 1) / 10 + day - 1) % 7;
// 转换为1-7(1=星期日)
return (w + 7) % 7 + 1;
}
/**
* 将日期时间转换为秒数(从2000年1月1日算起)
*/
uint32_t DateTimeToSeconds(RTC_Time *time)
{
uint32_t seconds = 0;
uint16_t year;
uint8_t month;
// 计算年份累积秒数
for (year = BASE_YEAR; year < time->year; year++)
{
seconds += (IsLeapYear(year) ? 366 : 365) * 24 * 3600;
}
// 计算月份累积秒数
for (month = 1; month < time->month; month++)
{
seconds += GetMonthDays(time->year, month) * 24 * 3600;
}
// 计算日期和时间的秒数
seconds += (time->day - 1) * 24 * 3600;
seconds += time->hour * 3600;
seconds += time->minute * 60;
seconds += time->second;
return seconds;
}
/**
* 将秒数转换为日期时间
*/
void SecondsToDateTime(uint32_t seconds, RTC_Time *time)
{
uint32_t days = seconds / (24 * 3600);
uint32_t remaining = seconds % (24 * 3600);
// 计算时间
time->hour = remaining / 3600;
remaining %= 3600;
time->minute = remaining / 60;
time->second = remaining % 60;
// 计算年月日
time->year = BASE_YEAR;
while (1)
{
uint16_t year_days = IsLeapYear(time->year) ? 366 : 365;
if (days < year_days) break;
days -= year_days;
time->year++;
}
time->month = 1;
while (1)
{
uint8_t month_days = GetMonthDays(time->year, time->month);
if (days < month_days) break;
days -= month_days;
time->month++;
}
time->day = days + 1;
// 计算星期
time->weekday = GetWeekday(time->year, time->month, time->day);
}
/**
* RTC初始化
*/
void RTC_Init(void)
{
// 使能PWR和BKP时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
// 允许访问BKP和RTC
PWR_BackupAccessCmd(ENABLE);
// 检查是否首次配置
if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5)
{
// 复位备份域
BKP_DeInit();
// 使能LSE
RCC_LSEConfig(RCC_LSE_ON);
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET);
// 选择LSE作为RTC时钟源
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
// 使能RTC时钟
RCC_RTCCLKCmd(ENABLE);
// 等待RTC寄存器同步
RTC_WaitForSynchro();
// 等待上次写操作完成
RTC_WaitForLastTask();
// 设置预分频器(32.768kHz -> 1Hz)
RTC_SetPrescaler(32767);
RTC_WaitForLastTask();
// 设置初始时间(2000-01-01 00:00:00)
RTC_SetCounter(0);
RTC_WaitForLastTask();
// 标记已配置
BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);
}
else
{
// 等待RTC寄存器同步
RTC_WaitForSynchro();
// 等待上次写操作完成
RTC_WaitForLastTask();
}
// 使能秒中断
RTC_ITConfig(RTC_IT_SEC, ENABLE);
RTC_WaitForLastTask();
}
/**
* 设置时间
*/
void RTC_SetTime(RTC_Time *time)
{
uint32_t seconds = DateTimeToSeconds(time);
// 等待上次写操作完成
RTC_WaitForLastTask();
// 设置计数器
RTC_SetCounter(seconds);
// 等待写操作完成
RTC_WaitForLastTask();
}
/**
* 获取时间
*/
void RTC_GetTime(RTC_Time *time)
{
uint32_t seconds = RTC_GetCounter();
SecondsToDateTime(seconds, time);
}
/**
* 设置闹钟
*/
void RTC_SetAlarm(RTC_Alarm *alarm)
{
RTC_Time current_time;
RTC_GetTime(¤t_time);
// 构建闹钟时间
RTC_Time alarm_time;
alarm_time.year = current_time.year;
alarm_time.month = current_time.month;
alarm_time.day = current_time.day;
alarm_time.hour = alarm->hour;
alarm_time.minute = alarm->minute;
alarm_time.second = 0;
// 如果闹钟时间已过,设置为明天
uint32_t alarm_seconds = DateTimeToSeconds(&alarm_time);
uint32_t current_seconds = RTC_GetCounter();
if (alarm_seconds <= current_seconds)
{
alarm_seconds += 24 * 3600; // 加一天
}
// 等待上次写操作完成
RTC_WaitForLastTask();
// 设置闹钟
RTC_SetAlarm(alarm_seconds);
// 等待写操作完成
RTC_WaitForLastTask();
}
/**
* 使能/禁用闹钟
*/
void RTC_AlarmEnable(uint8_t enable)
{
if (enable)
{
RTC_ITConfig(RTC_IT_ALR, ENABLE);
}
else
{
RTC_ITConfig(RTC_IT_ALR, DISABLE);
}
RTC_WaitForLastTask();
}
/**
* 获取闹钟标志
*/
uint8_t RTC_GetAlarmFlag(void)
{
return RTC_GetFlagStatus(RTC_FLAG_ALR);
}
/**
* 清除闹钟标志
*/
void RTC_ClearAlarmFlag(void)
{
RTC_ClearFlag(RTC_FLAG_ALR);
}
5.2 显示模块
📄 创建文件:
Hardware/lcd1602.h
#ifndef __LCD1602_H
#define __LCD1602_H
#include "sys.h"
// 引脚定义
#define LCD_RS_PIN GPIO_Pin_0
#define LCD_RW_PIN GPIO_Pin_1
#define LCD_EN_PIN GPIO_Pin_2
#define LCD_DATA_PORT GPIOA
#define LCD_CTRL_PORT GPIOB
// 控制宏
#define LCD_RS_HIGH() GPIO_SetBits(LCD_CTRL_PORT, LCD_RS_PIN)
#define LCD_RS_LOW() GPIO_ResetBits(LCD_CTRL_PORT, LCD_RS_PIN)
#define LCD_RW_HIGH() GPIO_SetBits(LCD_CTRL_PORT, LCD_RW_PIN)
#define LCD_RW_LOW() GPIO_ResetBits(LCD_CTRL_PORT, LCD_RW_PIN)
#define LCD_EN_HIGH() GPIO_SetBits(LCD_CTRL_PORT, LCD_EN_PIN)
#define LCD_EN_LOW() GPIO_ResetBits(LCD_CTRL_PORT, LCD_EN_PIN)
// 函数声明
void LCD1602_Init(void);
void LCD1602_Clear(void);
void LCD1602_SetCursor(uint8_t row, uint8_t col);
void LCD1602_ShowChar(uint8_t row, uint8_t col, char ch);
void LCD1602_ShowString(uint8_t row, uint8_t col, char *str);
void LCD1602_ShowNum(uint8_t row, uint8_t col, uint16_t num, uint8_t len);
#endif
📄 创建文件:
Hardware/lcd1602.c
#include "lcd1602.h"
#include "delay.h"
/**
* 写命令
*/
static void LCD_WriteCmd(uint8_t cmd)
{
LCD_RS_LOW();
LCD_RW_LOW();
GPIO_Write(LCD_DATA_PORT, cmd);
LCD_EN_HIGH();
delay_us(5);
LCD_EN_LOW();
delay_us(5);
}
/**
* 写数据
*/
static void LCD_WriteData(uint8_t data)
{
LCD_RS_HIGH();
LCD_RW_LOW();
GPIO_Write(LCD_DATA_PORT, data);
LCD_EN_HIGH();
delay_us(5);
LCD_EN_LOW();
delay_us(5);
}
/**
* LCD初始化
*/
void LCD1602_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
// 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE);
// 配置数据口
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_All;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(LCD_DATA_PORT, &GPIO_InitStructure);
// 配置控制口
GPIO_InitStructure.GPIO_Pin = LCD_RS_PIN | LCD_RW_PIN | LCD_EN_PIN;
GPIO_Init(LCD_CTRL_PORT, &GPIO_InitStructure);
// 初始化序列
delay_ms(15);
LCD_WriteCmd(0x38); // 8位数据,双行,5×7点阵
delay_ms(5);
LCD_WriteCmd(0x38);
delay_us(100);
LCD_WriteCmd(0x38);
LCD_WriteCmd(0x08); // 显示关闭
LCD_WriteCmd(0x01); // 清屏
delay_ms(2);
LCD_WriteCmd(0x06); // 地址自动+1,光标右移
LCD_WriteCmd(0x0C); // 显示开,光标关,闪烁关
}
/**
* 清屏
*/
void LCD1602_Clear(void)
{
LCD_WriteCmd(0x01);
delay_ms(2);
}
/**
* 设置光标位置
*/
void LCD1602_SetCursor(uint8_t row, uint8_t col)
{
uint8_t addr = (row == 0) ? (0x80 + col) : (0xC0 + col);
LCD_WriteCmd(addr);
}
/**
* 显示字符
*/
void LCD1602_ShowChar(uint8_t row, uint8_t col, char ch)
{
LCD1602_SetCursor(row, col);
LCD_WriteData(ch);
}
/**
* 显示字符串
*/
void LCD1602_ShowString(uint8_t row, uint8_t col, char *str)
{
LCD1602_SetCursor(row, col);
while (*str != '\0')
{
LCD_WriteData(*str++);
}
}
/**
* 显示数字
*/
void LCD1602_ShowNum(uint8_t row, uint8_t col, uint16_t num, uint8_t len)
{
LCD1602_SetCursor(row, col);
for (int i = len - 1; i >= 0; i--)
{
uint8_t digit = (num / (uint16_t)pow(10, i)) % 10;
LCD_WriteData('0' + digit);
}
}
5.3 主程序
📄 创建文件:
User/main.c
/**
* STM32 RTC电子万年历与闹钟系统
* 主程序
*/
#include "stm32f10x.h"
#include "sys.h"
#include "rtc.h"
#include "lcd1602.h"
#include "delay.h"
#include <stdio.h>
#include <string.h>
// 系统状态
typedef enum {
STATE_NORMAL = 0, // 正常显示
STATE_SET_TIME, // 设置时间
STATE_SET_ALARM, // 设置闹钟
STATE_ALARM_RING // 闹钟响铃
} SystemState;
// 全局变量
volatile SystemState system_state = STATE_NORMAL;
RTC_Time current_time;
RTC_Alarm alarm_setting = {7, 0, 1, 0, 5}; // 默认7:00
volatile uint8_t alarm_triggered = 0;
volatile uint8_t alarm_snooze_count = 0;
// 星期字符串
const char *weekday_str[8] = {
"", "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"
};
// 函数声明
void System_Init(void);
void Display_Update(void);
void Key_Scan(void);
void Alarm_Handler(void);
void Enter_LowPower(void);
int main(void)
{
System_Init();
LCD1602_ShowString(0, 0, "RTC Calendar");
LCD1602_ShowString(1, 0, "Initializing...");
delay_ms(1000);
while (1)
{
// 获取当前时间
RTC_GetTime(¤t_time);
// 更新显示
Display_Update();
// 扫描按键
Key_Scan();
// 处理闹钟
Alarm_Handler();
// 延时
delay_ms(100);
}
}
/**
* 系统初始化
*/
void System_Init(void)
{
// 时钟配置
SystemClock_Config();
// 延时初始化
delay_init();
// LCD初始化
LCD1602_Init();
// RTC初始化
RTC_Init();
// 配置RTC中断
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = RTC_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// 按键GPIO初始化
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
GPIO_Init(GPIOB, &GPIO_InitStructure);
// 蜂鸣器初始化
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_ResetBits(GPIOA, GPIO_Pin_8);
}
/**
* 更新显示
*/
void Display_Update(void)
{
char buf[17];
if (system_state == STATE_NORMAL)
{
// 第一行:时间 HH:MM:SS
sprintf(buf, "%02d:%02d:%02d %s",
current_time.hour,
current_time.minute,
current_time.second,
weekday_str[current_time.weekday]);
LCD1602_ShowString(0, 0, buf);
// 第二行:日期 YYYY-MM-DD
sprintf(buf, "%04d-%02d-%02d ",
current_time.year,
current_time.month,
current_time.day);
LCD1602_ShowString(1, 0, buf);
// 显示闹钟状态
if (alarm_setting.enable)
{
LCD1602_ShowChar(1, 14, 'A');
}
else
{
LCD1602_ShowChar(1, 14, ' ');
}
}
}
/**
* 按键扫描
*/
void Key_Scan(void)
{
static uint8_t key_state = 0;
// 读取按键状态(低电平有效)
uint8_t key_set = !GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_3);
uint8_t key_up = !GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_4);
uint8_t key_down = !GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_5);
uint8_t key_ok = !GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_6);
// 设置键
if (key_set && !(key_state & 0x01))
{
key_state |= 0x01;
if (system_state == STATE_ALARM_RING)
{
// 停止闹钟
alarm_triggered = 0;
GPIO_ResetBits(GPIOA, GPIO_Pin_8);
system_state = STATE_NORMAL;
}
else if (system_state == STATE_NORMAL)
{
system_state = STATE_SET_TIME;
}
else
{
system_state = STATE_NORMAL;
}
LCD1602_Clear();
}
else if (!key_set)
{
key_state &= ~0x01;
}
// 其他按键处理...
}
/**
* 闹钟处理
*/
void Alarm_Handler(void)
{
if (alarm_triggered)
{
// 蜂鸣器响
static uint8_t beep_state = 0;
static uint32_t beep_time = 0;
if (HAL_GetTick() - beep_time > 500)
{
beep_time = HAL_GetTick();
beep_state = !beep_state;
if (beep_state)
{
GPIO_SetBits(GPIOA, GPIO_Pin_8);
}
else
{
GPIO_ResetBits(GPIOA, GPIO_Pin_8);
}
}
// 显示闹钟界面
LCD1602_ShowString(0, 0, " ALARM!!! ");
sprintf(buf, "%02d:%02d SNOOZE:%d",
current_time.hour, current_time.minute, alarm_snooze_count);
LCD1602_ShowString(1, 0, buf);
}
}
/**
* RTC中断服务程序
*/
void RTC_IRQHandler(void)
{
if (RTC_GetITStatus(RTC_IT_SEC) != RESET)
{
RTC_ClearITPendingBit(RTC_IT_SEC);
// 秒中断处理
// 可以在这里添加秒闪等功能
}
if (RTC_GetITStatus(RTC_IT_ALR) != RESET)
{
RTC_ClearITPendingBit(RTC_IT_ALR);
// 闹钟中断
if (alarm_setting.enable)
{
alarm_triggered = 1;
system_state = STATE_ALARM_RING;
}
}
}
六、低功耗设计
6.1 进入待机模式
/**
* 进入待机模式
* 闹钟中断唤醒
*/
void Enter_Standby(void)
{
// 关闭外设
LCD1602_Clear();
// 使能WKUP引脚唤醒
PWR_WakeUpPinCmd(ENABLE);
// 进入待机模式
PWR_EnterSTANDBYMode();
}
6.2 备份域数据保持
/**
* 保存闹钟设置到备份寄存器
*/
void Save_Alarm_To_BKP(void)
{
BKP_WriteBackupRegister(BKP_DR2, alarm_setting.hour);
BKP_WriteBackupRegister(BKP_DR3, alarm_setting.minute);
BKP_WriteBackupRegister(BKP_DR4, alarm_setting.enable);
}
/**
* 从备份寄存器读取闹钟设置
*/
void Load_Alarm_From_BKP(void)
{
alarm_setting.hour = BKP_ReadBackupRegister(BKP_DR2);
alarm_setting.minute = BKP_ReadBackupRegister(BKP_DR3);
alarm_setting.enable = BKP_ReadBackupRegister(BKP_DR4);
}
七、常见问题
7.1 时间不准
原因:
- 晶振精度问题
- 温度影响
- 负载电容不匹配
解决:
- 使用高精度晶振
- 软件校准
- 调整负载电容
7.2 备用电池不工作
原因:
- 电池没电
- Vbat引脚未连接
- 电池极性错误
解决:
- 更换电池
- 检查电路连接
- 确认电池方向
八、总结
8.1 核心知识点
- RTC工作原理与配置
- 闰年与日期计算
- 闹钟中断处理
- 备份寄存器使用
- 低功耗模式应用
8.2 扩展方向
- 添加温度补偿
- 实现多时区切换
- 添加定时器功能
- 连接WiFi自动对时
如果本教程对你有帮助,欢迎点赞、收藏、关注!

6460

被折叠的 条评论
为什么被折叠?



