Funpack5-4 基于FRDM-MCXN236实现RTC电子时钟
该项目使用了NXP FRDM-MCXN236 开发板,实现了RTC电子时钟的设计,它的主要功能为:掉电保持功能,计时不中断。
标签
Funpack活动
测试
开发板
RTC
仗剑天涯
更新2026-10-19
3

一、项目介绍

这个项目是基于NXP FRDM-MCXN236 开发板完成的一个 RTC 电子时钟。核心思路很简单:利用 MCXN236 内部集成的独立实时时钟(IRTC)外设来获取精确的时间信息,通过板载的两个用户按键 SW2 和 SW3 来设置和调整时间,最终通过串口实时输出当前的年、月、日、时、分、秒。整个系统不需要外接任何显示模块,所有时间信息都通过 LPUART4 串口打印到电脑端的串口助手上,简洁直观。

为什么选这个题目?说实话,RTC 是每个 MCU 开发者迟早都要面对的外设,但很多人(包括我之前)对它的理解只停留在"调用个库函数读时间"的层面。这次借 Funpack 活动的机会,我想把 RTC 从里到外搞清楚——它的时钟源从哪来、掉电后为什么还能走、写保护是怎么回事、1Hz 中断怎么用。做完这个项目,这些问题都有了实实在在的答案。

项目完成了任务1要求的全部五项功能:使用 RTC 获取系统时间、显示当前时分秒信息、使用按键实现时间调整、通过串口输出当前时间、掉电后时间能够保持。另外我还加了星期自动计算(蔡勒公式)、RTC 有效性检测、按键消抖等细节处理,让整个时钟用起来更顺手。

二、使用到的硬件介绍

整个项目用到的硬件全部在FRDM-MCXN236 开发板上,没有外接任何模块,非常干净。

硬件

型号/参数

用途

主控MCU

MCXN236VDF,Cortex-M33,150MHz,1MB Flash,352KB RAM

系统控制核心

RTC 外设

MCXN236 内置 IRTC(独立实时时钟)

时间计数与掉电保持

按键SW2

GPIO0 Pin20(P0_20),板载用户按键

切换编辑模式(时/分/秒)

按键SW3

GPIO0 Pin6(P0_6),板载用户按键

编辑模式下数值+1

调试串口

LPUART4,P1_8(TX)/P1_9(RX),115200bps

时间信息输出

调试器

板载MCU-Link(CMSIS-DAP)

烧录、调试、串口一线通

RTC 备用电源

VBAT 引脚(板载超级电容/电池)

掉电后RTC 继续运行


表1  硬件资源一览表

FRDM-MCXN236 这块板子比我之前用的 MCXA153 强不少——Cortex-M33 内核跑到 150MHz,1MB 的双组 Flash 还支持 ECC,352KB ECC RAM,还有 SmartDMA 加速器和 EdgeLock 安全子系统。不过对这个 RTC 时钟项目来说,这些高性能资源大部分都在"休息",真正干活的就是 IRTC 这个独立外设和两个 GPIO 按键。但这也恰恰说明了 IRTC 的独立性——它有自己的电源域和时钟源,主 CPU 就算停了它也能继续走。

f454f462e542716c0877bf0b2f366eec.jpg

图1  FRDM-MCXN236 开发板实物

三、方案框图与项目设计思路 

fig2_system_block.png

图2  系统总体框图

整个系统的架构非常清晰,可以分成三个部分来看:

第一部分是时间源——MCXN236 内部的 IRTC 外设。IRTC 有自己独立的 32.768kHz 外部晶振作为时钟源,经过 32768 分频后得到精确的 1Hz 秒信号,驱动内部的秒、分、时、日、月、年计数器逐级进位。IRTC 运行在独立的 VBAT 电源域,主电源掉电后由 VBAT 供电继续计时,这就是掉电保持的硬件基础。

第二部分是人机交互——两个板载按键。SW2 负责模式切换,在"运行→编辑时→编辑分→编辑秒→运行"之间循环;SW3 负责在编辑模式下对当前选中的时间位做 +1 操作(时%24、分%60、秒%60)。两个按键都配置为内部上拉输入,低电平有效,软件做了 50ms 消抖处理。

第三部分是信息输出——LPUART4 串口。系统初始化时打印开机信息和按键说明,之后在运行模式下每秒(由 IRTC 的 1Hz 中断触发)读取一次 RTC 时间并打印到串口。进入编辑模式后,每次按键操作也会立即打印当前状态,方便用户确认调整结果。串口波特率 115200,8 位数据位,无校验,1 位停止位。

设计思路上,我刻意把时间获取和时间显示解耦:IRTC 硬件负责精确计时(1Hz 中断只设一个标志位,不在中断里做任何耗时操作),主循环负责轮询标志位、读 RTC、打印时间。这样即使串口打印慢一点,也不会影响 RTC 的计时精度。按键处理也放在主循环里轮询,用状态机的方式管理编辑模式,逻辑清晰且不容易出 bug。

四、RTC(IRTC)工作原理详解 

fig3_rtc_principle.png

图3  RTC (IRTC) 工作原理框图

要把这个时钟项目做好,得先搞懂IRTC 到底是怎么工作的。MCXN236 的 IRTC(Independent Real-Time Clock)是一个完全独立的实时时钟外设,所谓"独立"体现在三个方面:独立的时钟源、独立的电源域、独立的寄存器接口。

4.1时钟源与计时原理

IRTC 的时钟源是外部 32.768kHz 晶振。为什么是 32.768kHz 这个奇怪的数字?因为 32768 = 2^15,经过 15 级二分频后正好得到 1Hz 的秒信号,用二进制计数器实现起来非常方便,不需要复杂的除法电路。晶振输出的 32.768kHz 方波进入 IRTC 内部的预分频器,分频得到 1Hz,然后驱动秒计数器从 0 数到 59,溢出后向分计数器进 1,分计数器溢出向时计数器进 1,以此类推,时(0-23)、日(1-31,自动处理大小月和闰年)、月(1-12)、年(可到 2239 年)逐级进位。整个过程完全由硬件自动完成,CPU 不需要干预。

4.2掉电保持原理(VBAT电源域) 

fig4_vbat_power.png

图4  RTC 掉电保持原理(VBAT 独立电源域)

RTC 最神奇的地方就是掉电后时间还能继续走,这靠的就是 VBAT 电源域。MCXN236 内部有一个独立的 VBAT 电源域,IRTC 的所有寄存器和计数器都在这个域里。正常工作时,VBAT 由主电源 VDD 供电(同时给板载的超级电容或纽扣电池充电);当主电源掉电后,VBAT 自动切换到备用电源供电,IRTC 继续运行,而 CPU 和其他外设则全部断电停止工作。

需要注意的是,VBAT 电源域的功耗非常低(微安级甚至纳安级),一颗小小的纽扣电池就能让 RTC 跑好几年。但这个电源域只能给 IRTC 供电,不能给 CPU 或其他外设供电,所以掉电后串口肯定是没输出的,但时间一直在走,重新上电后读出来的时间就是准确的。

4.3写保护与有效性检测

IRTC 有一个写保护机制,默认情况下时间寄存器是只读的,防止误写导致时间错乱。要设置时间,必须先调用 IRTC_SetWriteProtection(RTC, false) 关闭写保护,设置完成后硬件会自动重新开启写保护。这个机制在我的 APP_ApplyDatetime() 函数里体现得很清楚——每次设置时间前都先关写保护。

另外,IRTC 有一个 Invalid Flag(无效标志)。当 RTC 第一次上电、VBAT 完全掉电(备用电池也没电了)或者检测到时钟故障时,这个标志会被置位,表示当前时间不可信。我的代码在启动时会检查这个标志和年份范围(2020-2239),如果时间无效就加载默认时间 2026/8/22 17:00:00 并写入 RTC;如果时间有效就直接读取并继续运行,这就是掉电恢复的逻辑。

4.4 1Hz中断

IRTC 支持 1Hz 中断,每秒钟触发一次。这个中断的处理函数 RTC_IRQHandler() 非常简单——只做一件事:把全局变量 s_tick1Hz 设为 true,然后清除中断标志。不在中断里做任何耗时操作(比如串口打印),这是嵌入式开发的基本原则。主循环里轮询这个标志位,发现为 true 就去读 RTC 时间并打印,然后把标志位清掉。这样既保证了每秒一次的精确触发,又不会因为中断里做太多事而影响系统响应。

五、调试软件介绍、软件流程图及关键代码

5.1开发与调试环境

开发环境用的是NXP 官方的 MCUXpresso IDE(基于 Eclipse),免费下载使用。SDK 直接在 IDE 里导入 FRDM-MCXN236 的 board package,时钟配置、引脚复用、外设初始化都可以用 Config Tools 图形化生成。系统时钟配置为 PLL0 150MHz(BOARD_BootClockPLL150M),由 FIRC 48MHz 经过 PLL0 倍频得到,AHB 总线时钟也是 150MHz。调试用板载 MCU-Link,一根 USB-C 线搞定供电、烧录和串口调试,非常方便。

5.2软件流程图 

fig5_software_flow.png

图5  软件主程序流程图

主程序的流程如上图所示:上电后依次初始化硬件(时钟、串口、引脚)、按键(SW2/SW3 上拉输入)、IRTC(关闭写保护、清除状态标志)。然后检查 RTC 时间是否有效——无效则加载默认时间并写入,有效则直接读取。接着使能 1Hz 中断并打印开机信息,进入主循环。主循环里做两件事:轮询处理按键(SW2 切模式、SW3 调数值),检查 1Hz 中断标志并在运行模式下打印时间。

5.3按键状态机 

fig6_button_fsm.png

图6  按键状态机(SW2 切换模式 / SW3 调整数值)

按键处理用了一个简单的状态机。SW2 按下时在四个状态之间循环切换:运行(Run) → 编辑时(Hour) → 编辑分(Minute) → 编辑秒(Second) → 回到运行(Run)。从编辑秒回到运行时,会把调整后的时间写入 RTC 并保存。在编辑模式下,SW3 按下对当前选中的位 +1(时对 24 取模,分和秒对 60 取模),并立即写入 RTC。运行模式下 SW3 无效。每次按键都做了 50ms 软件消抖,用的是 SDK 提供的 SDK_DelayAtLeastUs() 函数。

5.4关键代码片段

RTC 初始化与 1Hz 中断:

static void APP_InitRtc(void)
{
    irtc_config_t irtcConfig;
    uint32_t flags;
 
    IRTC_GetDefaultConfig(&irtcConfig);
    if (IRTC_Init(RTC, &irtcConfig) == kStatus_Fail)
    {
        for (;;) { }  /* 初始化失败,死循环 */
    }
 
    /* 关闭写保护,清除状态标志 */
    (void)IRTC_SetWriteProtection(RTC, false);
    IRTC_DisableInterrupts(RTC, kIRTC_AlarmInterruptEnable);
    flags = IRTC_GetStatusFlags(RTC);
    if (0U != flags)
    {
        IRTC_ClearStatusFlags(RTC, flags);
    }
}
 
/* 1Hz 中断处理函数:只设标志位,不做耗时操作 */
void RTC_IRQHandler(void)
{
    uint32_t flags = IRTC_GetStatusFlags(RTC);
    if (0U != (flags & kIRTC_1hzFlag))
    {
        s_tick1Hz = true;
    }
    if (0U != flags)
    {
        (void)IRTC_SetWriteProtection(RTC, false);
        IRTC_ClearStatusFlags(RTC, flags);
    }
    SDK_ISR_EXIT_BARRIER;
}

RTC 时间有效性检测与默认时间加载:

static bool APP_IsRtcTimeValid(void)
{
    irtc_datetime_t datetime;
    /* 检查无效标志 */
    if (0U != (IRTC_GetStatusFlags(RTC) & kIRTC_InvalidFlag))
    {
        return false;
    }
    IRTC_GetDatetime(RTC, &datetime);
    /* 检查年份范围是否合理 */
    return (datetime.year >= 2020U) && (datetime.year <= 2239U);
}
 
static void APP_SetDefaultDatetime(irtc_datetime_t *datetime)
{
    datetime->year    = 2026U;
    datetime->month   = 8U;
    datetime->day     = 22U;
    datetime->hour    = 17U;
    datetime->minute  = 0U;
    datetime->second  = 0U;
    /* 蔡勒公式计算星期 */
    datetime->weekDay = APP_CalcWeekDay(datetime->year,
                                          datetime->month, datetime->day);
}

按键处理(SW2 切模式 + SW3 +1):

static void APP_HandleButtons(irtc_datetime_t *datetime,
                                 app_edit_mode_t *mode)
{
    static bool sw2LastState = false;
    static bool sw3LastState = false;
 
    /* SW2:切换编辑模式 */
    if (APP_ButtonJustPressed(BOARD_SW2_GPIO, BOARD_SW2_GPIO_PIN,
                               &sw2LastState))
    {
        SDK_DelayAtLeastUs(50000U, CLOCK_GetCoreSysClkFreq());  /* 消抖50ms */
        switch (*mode)
        {
            case kAppEditMode_Run:
                *mode = kAppEditMode_Hour; break;
            case kAppEditMode_Hour:
                *mode = kAppEditMode_Minute; break;
            case kAppEditMode_Minute:
                *mode = kAppEditMode_Second; break;
            default:
                *mode = kAppEditMode_Run;
                APP_ApplyDatetime(datetime);  /* 保存时间到RTC */
                break;
        }
        IRTC_GetDatetime(RTC, datetime);
        APP_PrintStatusLine("[TIME]", datetime, *mode);
    }
 
    /* SW3:编辑模式下数值 +1 */
    if ((*mode != kAppEditMode_Run) &&
        APP_ButtonJustPressed(BOARD_SW3_GPIO, BOARD_SW3_GPIO_PIN,
                               &sw3LastState))
    {
        SDK_DelayAtLeastUs(50000U, CLOCK_GetCoreSysClkFreq());
        switch (*mode)
        {
            case kAppEditMode_Hour:
                datetime->hour = (datetime->hour + 1U) % 24U; break;
            case kAppEditMode_Minute:
                datetime->minute = (datetime->minute + 1U) % 60U; break;
            case kAppEditMode_Second:
                datetime->second = (datetime->second + 1U) % 60U; break;
            default: break;
        }
        APP_ApplyDatetime(datetime);  /* 立即写入RTC */
        PRINTF("[SW3] %s +1 -> ", APP_GetEditModeName(*mode));
        APP_PrintClock(datetime);
        PRINTF("\r\n");
    }
}

主循环:

int main(void)
{
    irtc_datetime_t datetime;
    app_edit_mode_t editMode = kAppEditMode_Run;
    bool firstBoot = false;
 
    BOARD_InitHardware();
    APP_InitButtons();
    APP_InitRtc();
 
    /* 打印开机信息 */
    PRINTF("\r\n========================================\r\n");
    PRINTF("  FRDM-MCXN236 Electronic Clock\r\n");
    PRINTF("========================================\r\n");
    PRINTF("SW2: switch hour/minute/second\r\n");
    PRINTF("SW3: +1 while editing\r\n");
    PRINTF("RESET: simulate power loss, RTC keeps time\r\n");
    PRINTF("----------------------------------------\r\n");
 
    /* 检查RTC时间有效性,无效则加载默认时间 */
    if (!APP_IsRtcTimeValid())
    {
        APP_SetDefaultDatetime(&datetime);
        APP_ApplyDatetime(&datetime);
        firstBoot = true;
        PRINTF("[BOOT] RTC not ready, default time loaded\r\n");
    }
    else
    {
        IRTC_GetDatetime(RTC, &datetime);
        PRINTF("[BOOT] RTC time restored after reset\r\n");
    }
 
    APP_PrintStatusLine(firstBoot ? "[START]" : "[RESUME]",
                         &datetime, editMode);
    PRINTF("----------------------------------------\r\n");
 
    /* 使能1Hz中断 */
    IRTC_EnableInterrupts(RTC, kIRTC_1hzInterruptEnable);
    EnableIRQ(RTC_IRQn);
 
    while (1)
    {
        APP_HandleButtons(&datetime, &editMode);
 
        /* 每秒触发一次,运行模式下打印时间 */
        if (s_tick1Hz && (editMode == kAppEditMode_Run))
        {
            s_tick1Hz = false;
            IRTC_GetDatetime(RTC, &datetime);
            PRINTF("[TICK] ");
            APP_PrintClock(&datetime);
            PRINTF("\r\n");
        }
        else if (s_tick1Hz)
        {
            s_tick1Hz = false;  /* 编辑模式下只清标志,不打印 */
        }
    }
}

六、功能展示图及说明 

image.png

图7  串口输出示例(含开机/计时/按键设置全过程)

上面这张图是实际运行时串口输出的完整截图,可以看到整个时钟的工作流程:

  1. 开机后先打印分隔线和项目标题 "FRDM-MCXN236 Electronic Clock",然后是按键说明(SW2 切换模式、SW3 调整数值、RESET 模拟掉电)。
  2. 接着打印 "[BOOT] RTC time restored after reset",表示 RTC 时间有效,从掉电前的状态恢复。如果是第一次上电或者 VBAT 完全掉电,会打印 "[BOOT] RTC not ready, default time loaded" 并加载默认时间。
  3. 然后打印当前时间 "[RESUME] 2026/08/22 17:00:05",进入正常运行模式。
  4. 之后每秒打印一行 "[TICK] HH:MM:SS",时间精确到秒。
  5. 按下 SW2 进入编辑模式,打印 "[SW2] Enter time setup, now editing hour" 和当前时间状态行,状态行末尾会提示 "<< editing hour, press SW3 to +1 >>"。
  6. 按下 SW3,小时 +1,打印 "[SW3] hour +1 -> 18:00:08"。
  7. 再按 SW2 切换到编辑分,继续按 SW3 调整,最后按 SW2 回到运行模式,打印 "[SW2] Time setup done, clock resumes",时钟继续从调整后的时间开始走。


掉电保持是这个项目最核心的功能之一。测试方法很简单:让时钟正常运行一段时间,记住当前时间,然后长按RESET复位键(模拟掉电),因为没有电池。等十几秒后再松开复位键,重新打开串口,会看到打印的"[BOOT] RTC time restored after reset" 和恢复后的时间——这个时间正好等于断电前的时间加上断电的时长,说明 RTC 在掉电期间一直在精确计时。这个功能在实际产品中非常重要,比如电表、水表、温控器等设备,断电后时间不能乱,靠的就是 RTC 的 VBAT 掉电保持。

七、项目中遇到的难题及解决方法

7.1 RTC初始化失败的问题

一开始写IRTC_Init() 的时候,偶尔会返回 kStatus_Fail,程序进入死循环。查了参考手册和 SDK 代码才发现,IRTC_Init() 内部会检查 Invalid Flag,如果这个标志被置位(比如第一次上电、VBAT 掉电后),初始化就会失败。解决方法是在初始化失败时不要直接死循环,而是先清除 Invalid Flag,然后重新初始化,或者像我最终的代码那样,在 APP_InitRtc() 之后单独检查有效性并加载默认时间。实际上 SDK 的 IRTC_Init() 在某些版本里会自动处理 Invalid Flag,但为了保险起见,我在应用层又做了一次有效性检测。

7.2写保护导致时间设置不生效

刚开始做按键设置时间的时候,按下SW3 调整数值后,LCD(哦不对,这个项目没有 LCD,是串口)——串口打印的时间确实变了,但下一秒 1Hz 中断读出来的时间又变回原来的值了。查了半天发现是写保护的问题:IRTC 默认开启写保护,直接调用 IRTC_SetDatetime() 是写不进去的。必须先调用 IRTC_SetWriteProtection(RTC, false) 关闭写保护,设置完成后硬件会自动重新开启。我在 APP_ApplyDatetime() 函数里加了这一步,问题就解决了。这个写保护机制虽然有点麻烦,但确实能防止误写导致时间错乱,设计得很合理。

7.3 1Hz中断里打印导致时间不准

最开始图省事,直接在RTC_IRQHandler() 里调用 PRINTF() 打印时间。结果发现时间偶尔会跳秒或者延迟——因为串口打印是个耗时操作(115200 波特率下打印一行十几个字符要几毫秒),在中断里做这件事会影响中断响应时间,甚至可能导致下一个 1Hz 中断被延迟。后来改成了"中断只设标志位,主循环轮询打印"的标准做法,时间就精确了。这也再次印证了嵌入式开发的一条基本原则:中断服务函数里只做最必要、最快速的操作,耗时的事情放到主循环里去做。

7.4按键消抖

按键刚做好的时候,按一次SW2 经常会跳两三个模式——这是典型的按键机械抖动问题。机械按键在按下和松开的瞬间会有几毫秒的抖动,导致 GPIO 读到多次电平变化。解决方法是软件消抖:检测到按键按下后延时 50ms 再读取一次电平,如果仍然是按下状态才认为是有效按键。我用的是 SDK 提供的 SDK_DelayAtLeastUs(50000, ...) 函数,消抖 50ms 后按键就非常稳定了。另外用 APP_ButtonJustPressed() 函数做了边沿检测(只在按下的瞬间触发一次,按住不放不会重复触发),这也是按键处理的标准做法。

八、心得体会

做完这个RTC 电子时钟项目,最大的感受是:看似简单的东西,真正做透了并不简单。RTC 这个外设,以前在别的项目里也用过,无非就是调个库函数读时间,但这次从原理层面把它搞清楚之后,对很多细节有了全新的认识。

比如32.768kHz 晶振为什么是这个频率——以前只知道是 RTC 常用的晶振,现在明白了 32768 = 2^15,15 级二分频正好得到 1Hz,用二进制计数器实现起来零误差。再比如 VBAT 电源域——以前只知道 RTC 掉电能走,现在明白了它有独立的电源域和时钟源,CPU 断电不影响它,而且功耗极低,一颗纽扣电池能用好几年。还 有写保护机制——以前觉得这东西多余,现在理解了它是防止误写的重要保障,时间寄存器如果被误写,整个时钟就乱了。

另一个收获是对"中断里做什么"这个问题有了更深刻的理解。以前写中断服务函数,有时候图省事就把打印、计算都放进去了,结果各种奇怪的问题。这次 1Hz 中断里打印导致时间不准的问题,让我真正体会到了"中断要短、要快"这条原则的实际意义。现在再写中断,第一反应就是"只设标志位,剩下的交给主循环"。

FRDM-MCXN236 这块板子也给我留下了不错的印象。150MHz 的 Cortex-M33、1MB Flash、352KB RAM,资源非常充裕,板载 MCU-Link 调试器一线通非常方便,MCUXpresso IDE 和 Config Tools 的配合也很流畅。虽然这个项目只用了它很小一部分资源(IRTC + 两个 GPIO + 一个串口),但通过这个项目熟悉了板子的开发环境和基本流程,后面做更复杂的项目(比如任务2的环境数据采集)就有基础了。

最后,Funpack 这种"给块板子、定个任务、自己折腾"的活动形式真的很适合嵌入式学习者。有明确的任务目标,但实现方式完全自由,做完之后还有返还激励,比自己瞎学效率高多了。这个项目虽然难度只有两颗星,但把 RTC 这个外设从原理到实践彻底搞清楚了值了。

附件下载
frdmmcxn236_irtc_electronic_clock.zip
源代码
团队介绍
个人
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号