一、项目介绍
本项目以 NXP FRDM-MCXN236 开发板为核心平台,设计并实现了一个简易的电子时钟系统。系统采用 MCX N236 微控制器内置的独立实时时钟 IRTC作为计时来源,通过软件模拟 I2C 总线驱动一块 0.96 英寸、分辨率为 128×64 的 SSD1306 OLED 单色显示屏,实时显示当前时、分、秒;同时利用开发板上的两个用户按键实现时间的设置与调整,并通过 LPUART4 以 115200 波特率把“模式 + 时间”输出到上位机串口终端,便于观察与调试。

系统的主要功能包括:
- 时钟显示:OLED 以 16×32 大号字体居中显示 “HH:MM:SS”,每秒刷新一次;
- 掉电保持:由于 IRTC 由 VBAT 域供电,只要 VDD_VBAT 有后备电源,整机断电后时间不丢失,重新上电后继续走时;冷启动(从未设置或彻底掉电)时默认从 00:00:00 开始;
- 按键校时:SW3(P0_6)用于循环选择“时/分/秒”设置位,SW2(P0_20)用于调整选中位的数值,短按 ±1,长按切换 “(+) / (-)” 增减方向;
- 状态提示:顶部一行显示 “NORMAL MODE” 或 “SET MODE (+) / SET MODE (-)”,设置状态下被选中的字段以闪烁方式提示;
- 串口输出:每秒向上位机输出一行 “模式 时间”,例如 “SET MODE (+) 12:12:12”。
该项目覆盖了 GPIO、软件 I2C、外设驱动移植、实时时钟(RTC)、按键状态机、电源域管理以及上位机调试等多个嵌入式开发的关键知识点,是一个较为完整的综合性实战项目。
二、硬件介绍
1. FRDM-MCXN236 开发板
FRDM-MCXN236 是 NXP 推出的低成本评估开发板。板载主控为 MCX N236,基于 ARM Cortex-M33 内核(带 TrustZone 安全特性),最高主频 150 MHz,内含 1 MB Flash;板上还集成了外部 QSPI Flash、加速度计、CAN 收发器、RGB LED 以及一块基于 LPC55S69 的 MCU-Link 调试探针。本项目中主要用到 MCU 本体、32.768 kHz 晶振、用户按键、调试串口与 MCU-Link。

2. 主控与实时时钟(IRTC)
MCX N236 的实时时钟模块在 SDK 中由 fsl_irtc 驱动(外设 RTC0),位于芯片的 VBAT(常电)域,配合板载 32.768 kHz 晶振(Y2,接到 XTAL32K / EXTAL32K 引脚)实现精确计时。IRTC 提供年月日时分秒的日历寄存器,可支持日历、闹钟、秒中断等功能,并具备电池供电下的时间保持能力。
3. 0.96 英寸 OLED 显示屏(SSD1306)
显示屏为 4 针模块(VCC、GND、SCL、SDA),主控芯片为 SSD1306,分辨率 128×64,单色。模块无独立复位脚(RES),依靠上电内部复位。模块从机地址为 0x78(7 位地址 0x3C)。本项目中采用软件模拟 I2C 方式驱动,接线为:SCL 接 P1_0,SDA 接 P1_2。

4. 按键、串口与调试器
器件/接口 | 引脚 | 作用 |
SW2(Wake up) | P0_20 | 调整选中时间位(短按±1,长按切换方向) |
SW3(ISP) | P0_6 | 循环切换设置状态(正常/时/分/秒) |
LPUART4 | P1_8/P1_9 | 调试串口输出,波特率115200 |
MCU-Link | USB-C | 程序下载、在线调试、虚拟串口(VCOM) |
32.768kHz 晶振 Y2 | XTAL32K/EXTAL32K | RTC 计时时钟源 |
三、方案框图与设计思路

整体设计采用“分层 + 模块化”的思路:
- 底层驱动层:软件 I2C + SSD1306 显示驱动(oled.c)、IRTC 时钟驱动封装(rtc_clock.c)、按键扫描与去抖(buttons.c);
- 应用逻辑层:主程序(gpio_led_output.c)负责初始化、按键状态机、时间设置与显示/串口刷新;
- 数据流:RTC 提供时间 → 应用读取(正常运行读实时时间,设置状态读取冻结值)→ 分别送往 OLED 显示与串口输出;按键输入反过来修改设置值并在退出设置时写回 RTC。
关键设计决策:
- 计时源选用 IRTC 而非软件计数:软件定时器受中断与负载影响,误差大且无法掉电保持;IRTC 在 VBAT 域持续计时,可实现真正的掉电保持。
- 显示采用软件 I2C:移植厂商标称例程方便,引脚占用少、接线简单,且便于理解 I2C 时序。
- 设置时“冻结”时间:由于 IRTC 没有单独的“停止/启动计时”位,进入设置模式时把当前时间读入一个编辑变量并停止更新,退出设置时再一并写回 RTC,从而达到“设置期间时间暂停”的观感。
四、调试软件介绍
本项目在 Windows 环境下开发,主要使用的软件与工具如下:
软件/工具 | 用途说明 |
MCUXpresso for VS Code / MCUXpresso IDE | 官方集成开发环境,提供工程创建、配置工具、编译、下载与调试 |
CMake + Ninja | 工程的构建系统,通过 CMakePresets 管理 debug/release 配置 |
Arm GNU Toolchain (armgcc 14.2) | arm-none-eabi-gcc 交叉编译工具链 |
MCUXpresso SDK (west 工作区) | 提供 IRTC、GPIO、LPUART、时钟等外设驱动与示例 |
LinkServer / J-Link | 固件下载与在线调试(SWD) |
串口终端(串口助手/SSCOM 等) | 查看 115200 波特率下的运行输出 |
开发板文档 (UM12041/原理图等) | 查询引脚、电源域、按键与晶振等硬件信息 |
调试流程一般为:在 VS Code 中通过 CMake 配置并编译 → 用 LinkServer 下载到开发板 → 打开串口终端观察打印、并通过 OLED 显示判断运行状态。工程中还通过 .vscode 配置了编译任务与调试配置,实现了“一键编译 + 下载 + 调试”。
五、软件流程图及关键代码

1. RTC 初始化(rtc_clock.c)
初始化 IRTC 的关键点在于:必须在访问任何 RTC 寄存器之前先为它提供功能时钟,否则总线访问会被挂起。同时利用 VBAT 域的时间保持能力判断是否需要载入默认时间。
void RTC_CLOCK_Init(void)
{
CLOCK_EnableClock(kCLOCK_Rtc0); /* 使能 RTC 总线时钟 */
(void)CLOCK_SetupClk16KClocking(kCLOCK_Clk16KToVbat | kCLOCK_Clk16KToMain);
month = RTC0->YEARMON & RTC_YEARMON_MON_CNT_MASK;
if ((month < 1U) || (month > 12U)) /* 日历无效 = 冷启动 */
{ IRTC_GetDefaultConfig(&rtcConfig); /* clockSelect = 16K */
if (kStatus_Success == IRTC_Init(RTC0, &rtcConfig))
{
rtc_load_default(&dt);
(void)IRTC_SetDatetime(RTC0, &dt);
}
}
(void)IRTC_SetWriteProtection(RTC0, false);
}
2. 直接读日历寄存器
SDK 的 IRTC_GetDatetime() 内部会对 INVAL 状态位做无上限忙等,一旦 RTC 未正常运行就会永久阻塞。为此改为直接读取日历寄存器,并做两次比对以消除跨秒撕裂。
static void rtc_read_regs(irtc_datetime_t *dt)
{
uint16_t seconds = RTC0->SECONDS;
uint16_t hourMin = RTC0->HOURMIN;
uint16_t days = RTC0->DAYS;
uint16_t yearMon = RTC0->YEARMON;
dt->second = seconds & RTC_SECONDS_SEC_CNT_MASK;
dt->hour = (hourMin & RTC_HOURMIN_HOUR_CNT_MASK) >> RTC_HOURMIN_HOUR_CNT_SHIFT;
dt->minute = hourMin & RTC_HOURMIN_MIN_CNT_MASK;
/* ... 日/月/年同理 ... */
}
3. 按键去抖与长短按(buttons.c)
btn_event_t BUTTON_Scan(button_t *b, uint8_t raw) /* 每 1ms 调用 */
{
if (raw != b->stable) { /* 电平变化 -> 去抖计时 */
if (++b->debounce >= BTN_DEBOUNCE_MS) {
b->stable = raw;
if (raw) { if (!b->long_done) ev = BTN_EVENT_SHORT; }
else { b->hold = 0; b->long_done = 0; }
}
} else if (b->stable == 0) { /* 按住 -> 长按检测 */
if (++b->hold >= BTN_LONG_MS && !b->long_done) { b->long_done=1; ev=BTN_EVENT_LONG; }
}
return ev;
}
4. 应用状态机与串口输出(gpio_led_output.c)
/* SW3: 切换模式并重置为 (+) */
if (e_sel == BTN_EVENT_SHORT) {
if (s_mode == UI_NORMAL) RTC_CLOCK_Read(&s_edit); /* 进入设置: 冻结时间 */
s_mode = (s_mode + 1) % 4; s_decrement = false;
if (s_mode == UI_NORMAL) RTC_CLOCK_Set(&s_edit); /* 退出设置: 写回 */
}
/* SW2: 仅设置态有效;短按±1,长按切换方向 */
if (s_mode != UI_NORMAL) {
if (e_adj == BTN_EVENT_LONG) s_decrement = !s_decrement;
else if (e_adj == BTN_EVENT_SHORT) edit_adjust(s_decrement ? -1 : +1);
}
/* 串口: "模式 时间"(两个空格) */
PRINTF("%s %s\r\n", mode_string(), tbuf);
六、功能展示图及说明

正常运行模式

增加设置模式

减小设置模式

串口助手接收数据界面
功能说明:
- 正常运行模式(NORMAL MODE):OLED 顶部显示 “NORMAL MODE”,下方以 16×32 大号字体显示 “12:12:12”,时间每秒自动递增。
- 设置模式(SET MODE):OLED 顶部显示 “SET MODE (+)” 或 “SET MODE (-)”,被选中的字段(时/分/秒)以约 0.4 秒周期闪烁提示;此时时间暂停走动。
- 按键操作:SW3 循环切换 正常→时→分→秒→正常,每次切换都会把调整方向重置为 “(+)”;SW2 短按对选中位 ±1,长按约 1 秒切换 “(+)/(-)”。在正常模式下 SW2 不产生任何调整。
- 串口输出:每秒(以及每次状态变化时)输出一行 “模式 时间”,如 “SET MODE (+) 12:12:12”,方便在不看屏幕时确认状态。
七、项目中遇到的难题及解决方法
难题一:屏幕移植后完全黑屏
现象:将厂商 51 单片机例程移植到 MCX N236 后,OLED 不显示。原因有两方面:一是原例程使用了 Keil 专用的 code 关键字与 8051 位操作,需要改写为基于 SDK GPIO 的写法;二是驱动软件的 I2C 时序与硬件无关,但引脚定义需要重新对应(原例程用 P1_2 作 RES,而本项目 4 针模块没有 RES,把 P1_2 用作 SDA)。解决:重写软件 I2C 的位操作,去除 RES 相关代码,改为上电延时等待内部复位,并正确配置 P1_0/P1_2 的复用与时钟。
难题二:RTC 初始化时程序卡死
现象:加入 RTC 后屏幕停在初始化阶段。逐句加标记定位后发现卡在读取 RTC 寄存器 RTC0->YEARMON 处。原因是 MCX N236 的 RTC 位于 VBAT 域,在它获得功能时钟之前访问其寄存器会挂起总线。解决:把 CLOCK_SetupClk16KClocking()(提供 VBAT 域 FRO16K 作为 RTC 功能时钟)提到任何 RTC 寄存器访问之前,与官方 irtc 示例保持一致。
难题三:使用了会“永久等待”的 SDK API
现象:CLOCK_SetupOsc32KClocking() 内部对 32.768 kHz 晶振就绪状态做无上限 while 等待,若晶振未就绪即死循环;IRTC_GetDatetime() 内部对 INVAL 状态位同样无上限忙等。解决:改用无阻塞的 FRO16K 时钟路径(CLOCK_SetupClk16KClocking),并将读时间改为直接读日历寄存器、配合带超时的写法,从根本上消除死等风险。
难题四:软件 I2C 与中断竞争
现象:启用 RTC 秒中断后显示偶发异常。原因是软件 I2C 是逐位翻转引脚实现的,事务进行中被中断打断会破坏时序。解决:在 OLED 单帧写入函数 OLED_WR_Byte() 前后用 DisableGlobalIRQ()/EnableGlobalIRQ() 做临界区保护,保证一帧 I2C 的原子性。
八、心得体会
通过本次项目,我对嵌入式系统的开发流程有了更完整的认识。首先是“先定位、再修改”的调试方法:当屏幕黑屏时,通过在 OLED 上分阶段显示标记,迅速把问题锁定到具体的某一行语句,避免了盲目改动。这种“可观测性优先”的思路在嵌入式开发中非常关键。
其次是理解外设的“使用前提”。RTC 卡死的根源并不在于代码逻辑,而在于访问它之前必须先提供功能时钟——这是数据手册与官方示例中都强调、却容易被忽视的细节。这提醒我,使用任何外设前都要弄清楚它的时钟、电源与复位依赖。
第三是对“阻塞式 API”的警惕。SDK 中一些函数内部存在无超时的等待,在异常情况下会导致程序永久卡死。工程代码应当尽量使用可预测、可超时的实现,或对关键等待加以保护。
此外,模块化设计(把显示、RTC、按键拆分为独立文件与接口)让代码更清晰、更易维护与复用;而软件 I2C 的中断竞争问题,则让我体会到“临界区保护”在底层时序代码中的重要性。
总而言之,本项目从硬件理解、驱动移植、外设配置到应用状态机与调试排错,形成了一条完整的实践链路,加深了我对 MCX N236 平台与嵌入式实时时钟应用的理解,也为后续更复杂的项目积累了宝贵经验。最后感谢电子森林与得捷电子联合推出的 《Funpack》 系列活动,对于我来说是个很好的学习机会,理论结合实践。