前提要求
基于ESP32C3进行试验,主要使用的是网络时间作为基础,所以需要使用SNTP服务同步时间,SNTP配置参考:SNTP服务开启
本文基于已实现SNTP功能。
概述
基于 ESP32 平台的多独立闹钟系统,适配物联网智能家居场景,核心基于 ESP32 本地 RTC 实时时钟(断网续走)+ SNTP 网络校时(时间同步),支持最多 3 路独立闹钟(可灵活扩展),每路闹钟独立触发、独立停止,互不干扰。系统具备时间合法性校验、重复触发防护、独立响铃控制等特性,可直接集成到 ESP32 智能家居项目中。
核心特征
- 多路独立:支持 3 路闹钟(可通过宏定义扩展),每路闹钟时间、启停状态独立配置;
- 防重复触发:通过触发标志位,避免 1 秒内因定时器轮询导致的重复触发;
- 独立停止:每路闹钟触发后创建独立的停止任务,响铃时长可统一配置,互不影响;
- 合法性校验:对闹钟索引、设置时间做严格校验,避免越界和非法时间配置;
- 无缝集成:基于 FreeRTOS 和 ESP-IDF 原生 API,可直接集成到现有 ESP32 项目;
- 快捷操作:提供一键关闭所有闹钟的接口,方便场景化控制;
- 时间基准:复用 SNTP+RTC 本地时钟,断网后 RTC 继续走时,不影响闹钟功能。
程序设计与实现
在文件的开头,创建闹钟数据的结构,用于存放后面设置的闹钟数据,然后设定闹钟个数上限。
核心定义
#define Clock "AlarmClock" // 日志TAG,用于区分闹钟系统日志
#define ALARM_MAX_NUM 3 // 最大闹钟数量,按需修改(如改为5则支持5路闹钟)
#define BUZZER_DELAY_MS 5000 // 蜂鸣器响铃时长,单位ms,统一配置所有闹钟响铃时间包含头文件
#include <time.h>
#include "sys/time.h"
#include "esp_timer.h"创建闹钟数据结构
闹钟核心结构体
// 闹钟时间结构体:每路闹钟的独立配置和状态
typedef struct
{
uint8_t hour; // 小时(0-23)
uint8_t minute; // 分钟(0-59)
uint8_t second; // 秒(0-59)
bool enable; // 闹钟使能标志:true=开启,false=关闭
bool triggered; // 闹钟触发标志:防止一秒内重复触发
TaskHandle_t task_hdl; // 每路闹钟独立的停止任务句柄,避免多路闹钟互相干扰
} alarm_time_t;全局闹钟数组
// 全局闹钟数组,初始化所有闹钟为默认关闭状态
alarm_time_t g_alarms[ALARM_MAX_NUM] = {
{.hour = 10, .minute = 0, .second = 0, .enable = false, .triggered = false, .task_hdl = NULL}, // 闹钟0(建议用作起床闹钟)
{.hour = 12, .minute = 0, .second = 0, .enable = false, .triggered = false, .task_hdl = NULL}, // 闹钟1
{.hour = 18, .minute = 0, .second = 0, .enable = false, .triggered = false, .task_hdl = NULL} // 闹钟2
};数组索引作为闹钟唯一标识(0~ALARM_MAX_NUM-1),用于后续设置、启停闹钟;初始化默认关闭所有闹钟,避免上电误触发;
定时器句柄
// ESP_TIMER定时器句柄:单个定时器遍历所有闹钟,无需为每路闹钟创建独立定时器,节省资源
esp_timer_handle_t g_alarm_check_timer = NULL;核心函数详解
本地时间获取函数
/**
* @brief 获取当前系统本地时间(已做时区补偿,如北京时间CST-8)
* @param tm_now 输出参数:当前时间的tm结构体,包含时、分、秒、年、月、日等信息
* @note 基于系统time.h实现,复用SNTP+RTC时钟基准,无需额外初始化
*/
static void get_local_time(struct tm *tm_now)
{
time_t now;
time(&now);
//localtime_r 为线程安全函数,避免多任务下的时间错乱
localtime_r(&now, tm_now); // 转为本地时间(关键,替代gmtime_r,带时区补偿)
}这是整个系统唯一时间入口,闹钟触发、时间范围判断均基于此函数获取的时间。必须需提前完成 SNTP 网络校时(initialize_sntp())和时区设置(setenv("TZ", "CST-8", 1))SNTP服务开启。
闹钟触发动作函数
/**
* @brief 闹钟触发后的自定义执行动作(适配多闹钟,通过索引区分)
* @param alarm_idx 触发的闹钟索引(0~ALARM_MAX_NUM-1)
* @note 可在此扩展触发逻辑,如打开窗帘、点亮LED、播放语音等
*/
static void alarm_trigger_action(uint8_t alarm_idx)
{
// 索引合法性检查,防止数组越界
if (alarm_idx >= ALARM_MAX_NUM) {
ESP_LOGE(Clock, "Alarm trigger idx %d is invalid!", alarm_idx);
return;
}
// 打印触发日志,包含闹钟索引和时间,方便调试和日志排查
ESP_LOGI(Clock, "ALARM[%d] TRIGGERED! Time: %02d:%02d:%02d",
alarm_idx,
g_alarms[alarm_idx].hour,
g_alarms[alarm_idx].minute,
g_alarms[alarm_idx].second);
// 蜂鸣器响铃控制(可替换为自定义动作,如GPIO控制、外设联动等)
buzzer_control_flag = true;
}核心触发逻辑解耦,仅需修改此函数即可实现不同的触发动作,
闹钟停止动作函数
/**
* @brief 闹钟触发后的延迟停止动作(适配多闹钟,通过参数传递索引)
* @param param 传入闹钟索引(void*类型,兼容FreeRTOS任务传参规范)
* @note 作为FreeRTOS任务函数运行,每路闹钟触发后创建独立实例,互不干扰
*/
static void alarm_stop_action(void *param)
{
// 解析传入的闹钟索引(FreeRTOS任务传参为void*,需强制类型转换)
uint8_t alarm_idx = (uint8_t)(uint32_t)param;
// 索引合法性检查
if (alarm_idx >= ALARM_MAX_NUM) {
ESP_LOGE(Clock, "Alarm stop idx %d is invalid!", alarm_idx);
vTaskDelete(NULL); // 删除自身任务,释放资源
return;
}
// 延迟响铃时长(由BUZZER_DELAY_MS宏定义配置)
vTaskDelay(pdMS_TO_TICKS(BUZZER_DELAY_MS));
// 重置当前闹钟状态,防止重复触发
g_alarms[alarm_idx].triggered = false;
// 关闭蜂鸣器(与触发动作对应,可替换为自定义停止逻辑)
buzzer_control_flag = false;
ESP_LOGI(Clock, "ALARM[%d] STOPPED", alarm_idx);
// 重置任务句柄,删除自身任务,释放FreeRTOS任务资源
g_alarms[alarm_idx].task_hdl = NULL;
vTaskDelete(NULL);
}每路闹钟触发后创建独立的停止任务,避免多路闹钟同时响铃时的互相干扰;任务执行完成后主动删除自身,重置任务句柄,避免内存泄漏;
闹钟核心检查回调函数
/**
* @brief 闹钟时间检查回调函数(ESP_TIMER定时器每秒触发一次)
* @param arg 入参(未使用,兼容ESP_TIMER回调规范)
* @note 核心逻辑:遍历所有闹钟,逐个检查是否满足触发条件,是整个闹钟系统的核心
*/
static void alarm_check_timer_cb(void *arg)
{
struct tm tm_now;
// 仅获取一次当前本地时间,避免多次调用系统函数,提升效率
get_local_time(&tm_now);
ESP_LOGD(Clock, "Current local time: %02d:%02d:%02d",
tm_now.tm_hour, tm_now.tm_min, tm_now.tm_sec);
// 遍历所有闹钟,逐个检查触发条件
for (uint8_t i = 0; i < ALARM_MAX_NUM; i++)
{
// 跳过未使能的闹钟,减少无效判断,提升代码执行效率
if (!g_alarms[i].enable) {
continue;
}
// 触发条件:时间完全匹配 + 未触发 + 无正在运行的停止任务
if (tm_now.tm_hour == g_alarms[i].hour &&
tm_now.tm_min == g_alarms[i].minute &&
tm_now.tm_sec == g_alarms[i].second &&
!g_alarms[i].triggered &&
g_alarms[i].task_hdl == NULL)
{
g_alarms[i].triggered = true; // 置触发标志,防止1秒内重复触发
alarm_trigger_action(i); // 执行当前闹钟的触发动作
// 创建当前闹钟的独立停止任务
BaseType_t ret = xTaskCreate((TaskFunction_t)alarm_stop_action,
"alarm_stop_task", // 任务名可扩展为"alarm_stop_0",方便调试
2048, // 栈空间2048字节,足够满足基础逻辑
(void*)(uint32_t)i,// 传递闹钟索引给停止任务
5, // 任务优先级,与主任务持平即可
&g_alarms[i].task_hdl); // 绑定当前闹钟的任务句柄
// 任务创建失败处理:重置状态,避免系统卡死
if (ret != pdPASS)
{
ESP_LOGE(Clock, "ALARM[%d] Create stop task failed!", i);
g_alarms[i].triggered = false;
g_alarms[i].task_hdl = NULL;
}
}
}
}核心触发条件解析
==必须同时满足以下 5 个条件,闹钟才会触发==,确保系统稳定性和准确性:
- 小时匹配:当前时间小时 == 闹钟设置小时;
- 分钟匹配:当前时间分钟 == 闹钟设置分钟;
- 秒匹配:当前时间秒 == 闹钟设置秒(精确到秒触发);
- 未触发:triggered为 false,避免 1 秒内定时器轮询导致的重复触发;
- 无停止任务:task_hdl为 NULL,避免同一闹钟多次创建停止任务,导致资源泄漏。
闹钟定时器初始化函数
/**
* @brief 初始化闹钟定时器(每秒触发一次,遍历检查所有闹钟)
* @return esp_err_t ESP_OK=成功,其他=失败(参考ESP-IDF错误码)
* @note 基于ESP_TIMER实现,高精度、低功耗,适合定时轮询场景
*/
static esp_err_t alarm_timer_init(void)
{
esp_timer_create_args_t alarm_timer_args = {
.callback = &alarm_check_timer_cb, // 定时器回调函数(核心检查逻辑)
.name = "alarm_check_timer" // 定时器名称,方便系统调试和日志排查
};
// 创建定时器,做错误检查
ESP_ERROR_CHECK(esp_timer_create(&alarm_timer_args, &g_alarm_check_timer));
// 启动周期性定时器,1000000us = 1秒,每秒检查一次
return esp_timer_start_periodic(g_alarm_check_timer, 1000000);
}定时器使用 ESP-IDF 原生esp_timer,精度高(微秒级)。
闹钟时间设置函数
/**
* @brief 设置指定闹钟的触发时间
* @param alarm_idx 闹钟索引(0~ALARM_MAX_NUM-1)
* @param hour 小时(0-23)
* @param minute 分钟(0-59)
* @param second 秒(0-59)
* @note 带严格的合法性校验,支持MQTT/小程序远程调用,设置后自动重置触发标志
*/
void alarm_set_time(uint8_t alarm_idx, uint8_t hour, uint8_t minute, uint8_t second)
{
// 1. 闹钟索引合法性校验
if (alarm_idx >= ALARM_MAX_NUM) {
ESP_LOGE(Clock, "Set time idx %d is invalid! Max: %d", alarm_idx, ALARM_MAX_NUM-1);
return;
}
// 2. 时间合法性校验(小时0-23,分钟0-59,秒0-59)
if (hour > 23 || minute > 59 || second > 59)
{
ESP_LOGE(Clock, "ALARM[%d] Invalid time! %02d:%02d:%02d", alarm_idx, hour, minute, second);
return;
}
// 3. 设置闹钟时间,重置触发标志(避免设置后直接触发)
g_alarms[alarm_idx].hour = hour;
g_alarms[alarm_idx].minute = minute;
g_alarms[alarm_idx].second = second;
g_alarms[alarm_idx].triggered = false;
// 4. 打印设置成功日志,方便调试和远程控制反馈
ESP_LOGI(Clock, "ALARM[%d] Time set to: %02d:%02d:%02d", alarm_idx, hour, minute, second);
}同时校验闹钟索引和设置时间,避免非法参数导致的系统异常,设置时间后自动重置triggered标志,避免刚设置完时间就触发闹钟。
闹钟启停控制函数
/**
* @brief 开启/关闭指定的闹钟
* @param alarm_idx 闹钟索引(0~ALARM_MAX_NUM-1)
* @param enable true=开启闹钟,false=关闭闹钟
* @note 关闭闹钟时会立即停止蜂鸣器,并重置所有状态,避免残留影响
*/
void alarm_set_enable(uint8_t alarm_idx, bool enable)
{
// 闹钟索引合法性校验
if (alarm_idx >= ALARM_MAX_NUM) {
ESP_LOGE(Clock, "Set enable idx %d is invalid! Max: %d", alarm_idx, ALARM_MAX_NUM-1);
return;
}
// 设置使能状态,重置触发标志和任务句柄
g_alarms[alarm_idx].enable = enable;
g_alarms[alarm_idx].triggered = false;
g_alarms[alarm_idx].task_hdl = NULL;
if (enable)
{
ESP_LOGI(Clock, "ALARM[%d] is ENABLED", alarm_idx);
}
else
{
ESP_LOGI(Clock, "ALARM[%d] is DISABLED", alarm_idx);
// 关闭闹钟时立即停止蜂鸣器,执行紧急停止
buzzer_control_flag = false;
}
}关闭闹钟时不仅重置状态,还会立即关闭蜂鸣器,避免闹钟关闭后仍持续响铃,同时重置triggered和task_hdl,确保闹钟再次开启时处于初始状态。
一键关闭所有闹钟函数(可选)
/**
* @brief 一键关闭所有已开启的闹钟(快捷操作接口)
* @note 适用于场景化控制,如“离家模式”“睡眠模式”下快速关闭所有闹钟
*/
void alarm_disable_all(void)
{
// 遍历所有闹钟,逐个关闭
for (uint8_t i = 0; i < ALARM_MAX_NUM; i++)
{
alarm_set_enable(i, false);
}
ESP_LOGI(Clock, "All alarms are DISABLED");
}基于alarm_set_enable实现,专为智能家居场景设计,如通过语音、按键、MQTT 指令触发一键关闭。
系统初始化与使用示例
需在项目主任务中完成以下初始化,确保闹钟系统正常工作
闹钟系统初始化步骤
// 1. 初始化SNTP网络校时(必选,保证时间准确)
initialize_sntp();
// 2. 初始化闹钟定时器(必选,启动每秒检查逻辑)
if (alarm_timer_init() == ESP_OK)
{
ESP_LOGI(Clock, "Alarm timer init success");
}
// 3. 示例:设置闹钟0为7:30:00,并开启(可选,可通过MQTT/小程序远程设置)
alarm_set_time(0,7,30,0);
alarm_set_enable(0,true);
// 4. 示例:设置闹钟1为12:00:00,暂不开启(可选)
alarm_set_time(1,12,0,0);
alarm_set_enable(1,false);必须提前完成 SNTP 时间同步和时区设置,否则闹钟时间会基于 UTC 时间(与本地时间相差 8 小时),在与 MQTT 结合时,仅需在 MQTT 消息解析函数中调用alarm_set_time和alarm_set_enable即可。
// 解析小程序下发的起床时间(JSON格式)
cJSON *alarm_idx_obj = cJSON_GetObjectItem(controls_obj, "alarm_idx");
cJSON *alarm_hour_obj = cJSON_GetObjectItem(controls_obj, "alarm_hour");
cJSON *alarm_min_obj = cJSON_GetObjectItem(controls_obj, "alarm_min");
cJSON *alarm_sec_obj = cJSON_GetObjectItem(controls_obj, "alarm_sec");
cJSON *alarm_enable_obj = cJSON_GetObjectItem(controls_obj, "alarm_enable");
if (alarm_idx_obj && cJSON_IsNumber(alarm_idx_obj) &&
alarm_hour_obj && cJSON_IsNumber(alarm_hour_obj) &&
alarm_min_obj && cJSON_IsNumber(alarm_min_obj) &&
alarm_sec_obj && cJSON_IsNumber(alarm_sec_obj) &&
alarm_enable_obj && cJSON_IsBool(alarm_enable_obj))
{
uint8_t idx = (uint8_t)cJSON_GetNumberValue(alarm_idx_obj);
uint8_t h = (uint8_t)cJSON_GetNumberValue(alarm_hour_obj);
uint8_t m = (uint8_t)cJSON_GetNumberValue(alarm_min_obj);
uint8_t s = (uint8_t)cJSON_GetNumberValue(alarm_sec_obj);
bool enable = cJSON_IsTrue(alarm_enable_obj);
alarm_set_time(idx, h, m, s); // 设置远程下发的时间
alarm_set_enable(idx, enable); // 开启/关闭闹钟
mqtt_publish_feedback(); // 向小程序反馈设置结果
}
评论
暂无评论