前提要求

基于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 个条件,闹钟才会触发==,确保系统稳定性和准确性:

  1. 小时匹配:当前时间小时 == 闹钟设置小时;
  2. 分钟匹配:当前时间分钟 == 闹钟设置分钟;
  3. 秒匹配:当前时间秒 == 闹钟设置秒(精确到秒触发);
  4. 未触发:triggered为 false,避免 1 秒内定时器轮询导致的重复触发;
  5. 无停止任务: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();       // 向小程序反馈设置结果
}