Funpack5-4 - 基于zephyr制作电子时钟
基于 FRDM-MCXN236 和 Zephyr RTOS 的电子时钟,支持 RTC 走时、彩屏显示、按键调时、串口输出和掉电时间保持。
标签
Funpack活动
显示
MCU
l98405
更新2026-10-19
山东大学
3


基于 FRDM-MCXN236 的电子时钟系统

一、项目介绍

本项目基于 NXP FRDM-MCXN236 开发板,在 Zephyr RTOS 上实现了一套完整的电子时钟系统。项目的出发点很简单:把"看时间"这件事做扎实——不仅要能走时和显示,还要能在断电之后把时间找回来,并且在一块速度受限的屏幕上做到流畅刷新。

系统实现了以下五项功能:

  1. 使用 RTC 获取系统时间:使用 MCX N236 芯片内部的 IRTC(Independent Real Time Clock)模块作为走时基准,通过 Zephyr 的标准 RTC 驱动接口 rtc_get_time() / rtc_set_time() 访问。
  2. 显示当前时、分、秒:在 240×320 的 ST7789 彩屏上,用自绘的七段数码管显示 HH:MM:SS,并配有日期行和状态行。
  3. 使用按键实现时间调整:利用开发板上的两颗用户按键 SW2 / SW3,实现"切换字段 + 数值递增"的时间设置交互。
  4. 通过串口输出当前时间:每秒通过板载 MCU-Link 虚拟串口输出一次当前时间,设置、退出、恢复等关键事件也有对应的日志。
  5. 掉电后时间保持:设计了两层保持机制——IRTC 位于 VBAT 域,复位不丢时间;同时定期将时间备份到片内 Flash,冷启动时自动回灌。

整个系统除显示屏外全部使用板载资源,不需要任何额外的扩展模块。最终固件占用 Flash 约 53 KB(5.09%)、RAM 约 11.7 KB(5.94%),资源余量充足。


二、硬件介绍

本项目使用到的硬件非常精简,具体如下:

主控芯片:NXP MCX N236
FRDM-MCXN236 开发板搭载的 MCX N236 是 NXP MCX N 系列的一款高性能 MCU,基于 Arm Cortex-M33 内核,具备 1 MB 片内 Flash 和 192 KB SRAM。本项目用到了它的以下片内外设:

  • IRTC 实时时钟模块:独立实时时钟,计数器位于 VBAT 域。这是本项目走时的基础,也是"掉电保持"的第一道防线。设备树中已定义 rtc 别名,驱动兼容字符串为 nxp,irtc。
  • GPIO 端口:一部分用于驱动显示屏(模拟 SPI 时序),一部分用于读取按键。
  • LPUART4:调试串口,波特率 115200 8N1。
  • 片内 Flash(FMU 控制器):其中 storage 分区(偏移 0xF0000,容量 64 KB)用于存放时间备份。

显示模块:ST7789 彩色 LCD
240×320 分辨率,RGB565 色彩格式,SPI 接口。需要特别说明的是,本项目并未使用硬件 LPSPI,而是使用 GPIO 模拟 SPI(bitbang)驱动这块屏。占用的引脚为 SCK、SDA(MOSI)、CS、DC、RST 五根。这个选择直接决定了后续显示方案的设计,下文会详细展开。

输入:板载用户按键 SW2 / SW3
两颗按键均带上拉、低电平有效。SW2 接在 GPIO0_20,SW3 接在 GPIO0_6,在板级设备树中已经以 gpio-keys 的形式定义好,可直接由 Zephyr 的 input 子系统接管。

调试与通信:板载 MCU-Link
FRDM 系列板载的 MCU-Link 调试器同时提供 CMSIS-DAP 调试通道和 USB 虚拟串口,一根 USB 线即可完成供电、烧录和串口监视三件事,无需外接 J-Link 或 USB-TTL 模块。


三、方案框图和设计思路

3.1 系统方案框图



3.2 设计思路

项目中有两个核心的设计决策,都不是"想当然"的选择,而是被硬件条件逼出来的。

决策一:放弃 GUI 框架,自绘七段数码管

最初的想法是直接上 LVGL。但实测发现,由于显示屏走的是 GPIO 模拟 SPI,速度极慢:整屏 240×320 的 RGB565 数据量是 153.6 KB,刷一次需要数秒。而时钟的秒位每秒都要更新一次——如果每次都整屏重绘,画面会一直处于"正在刷新"的状态,完全不可用。

因此显示方案改为自绘 + 最小重绘:

  • 时间部分用七段数码管渲染。每一位数字宽 28 px、高 52 px,由 7 个矩形段拼成,段的亮灭由一个 7 bit 的段码决定。
  • 日期和状态行用一套 5×7 点阵字库渲染,覆盖 ASCII 0x20–0x5A(空格到大写 Z),每个字形 5 个字节。
  • 核心优化是段级脏区重绘:为每一位数字缓存上一帧的值,刷新时把新旧段码做异或,得到"状态发生变化的段",只重画这些段。正常走时下,秒的个位每秒只有 2~4 个段变化,一次实际发送的数据量只有几百字节,比整屏刷新减少了三个数量级。
  • 日期行和状态行同理,采用逐字符 diff,内容不变就一个字节都不发。

决策二:掉电保持做成两层

IRTC 的计数器在 VBAT 域,所以复位、重新烧录、甚至断开调试器,时间都不会丢——这一点在实机上已经验证:复位前串口输出 00:00:46,复位后接着输出 00:00:48,时间是连续的。

但 FRDM-MCXN236 出厂并未安装纽扣电池,VBAT 实际是跟随 VDD 的。一旦真的拔掉 USB,IRTC 会被冷复位,年份寄存器回到 2112(IRTC 的年份基准是 2112 而非 1900)。

因此设计了第二层:定期把时间备份到片内 Flash,上电时检查 IRTC 的内容是否可信,不可信就用备份回灌。备份周期设为 300 秒,这是"掉电后最大误差"和"Flash 寿命"之间的权衡:片内 Flash 一次写入要占用一整个 128 字节页,4 个 8 KB 扇区约 250 次写入擦除一轮,300 秒一次相当于每天 4~5 次扇区擦除,按 10000 次擦写寿命计算可以使用数年。


四、调试软件、软件流程图与关键代码

4.1 开发与调试环境

  • 操作系统 / 框架:Zephyr RTOS(v4.4.0 开发分支),使用其设备树(Devicetree)+ Kconfig 的配置体系。
  • 构建工具:west 命令行工具,构建命令为
    west build -b frdm_mcxn236 ./my_app/nxp -d build/nxp_clock -p always
  • 烧录工具:pyocd,通过板载 MCU-Link 的 CMSIS-DAP 接口烧录,命令为 west flash --runner pyocd。
  • 串口监视:直接读取 /dev/ttyACM0(MCU-Link 虚拟串口),115200 8N1。
  • 代码编辑:VS Code,配合 compile_commands.json 实现跳转与补全。

工程代码组织如下:

文件

职责

src/main.c

主循环、按键状态机、串口输出、上电时间恢复

src/ui.c / ui.h

显示层:矩形填充、七段数码管、点阵文字

src/font5x7.h

5×7 点阵字库

src/time_store.c / .h

Flash 掉电备份

boards/frdm_mcxn236.overlay

设备树覆盖:bitbang SPI + ST7789 节点

prj.conf

Kconfig 配置

4.2 主程序流程图

4.3 按键设置状态机

4.4 上电时间恢复流程

4.5 关键代码介绍

(1)段级脏区重绘——整个显示方案的核心

static void draw_digit(int idx, int val, uint16_t color)
{
uint8_t now = seg_of_digit[val];
uint8_t before = (prev_val[idx] < 0) ? 0 : seg_of_digit[prev_val[idx]];
/* 颜色变了就整位重画,否则只动亮灭状态变化的段 */
uint8_t dirty = (prev_val[idx] < 0 || color != prev_col[idx])
? 0x7F : (now ^ before);

for (int s = 0; s < 7; s++) {
if ((dirty & BIT(s)) == 0) {
continue;
}
fill_rect(digit_x[idx] + seg_geom[s].dx, TIME_Y + seg_geom[s].dy,
seg_geom[s].w, seg_geom[s].h,
(now & BIT(s)) ? color : COL_BG);
}

prev_val[idx] = val;
prev_col[idx] = color;
}

now ^ before 这一行是整段代码的关键:异或的结果正好是"亮灭状态发生了变化"的那些段,只有它们需要重新发送数据。

(2)按键处理——中断回调只做入队

static void key_cb(struct input_event *evt, void *user_data)
{
uint8_t e;

/* 只在按下沿动作,松开(value==0)忽略 */
if (evt->type != INPUT_EV_KEY || evt->value == 0) {
return;
}

if (evt->code == KEY_UP_CODE) {
e = KEY_EVENT_UP;
} else if (evt->code == KEY_MODE_CODE) {
e = KEY_EVENT_MODE;
} else {
return;
}

/* 队列满了就丢掉,时钟不该因为按键堆积而卡住 */
(void)k_msgq_put(&key_q, &e, K_NO_WAIT);
}
INPUT_CALLBACK_DEFINE(NULL, key_cb, NULL);

按键使用 Zephyr 的 input 子系统 + gpio-keys 驱动,消抖由框架完成,不需要自己写延时判断。回调中只把事件放进消息队列,真正的处理放在主循环里做,避免在回调上下文中执行耗时的显示刷新。

键值也没有写死,而是从设备树读取:

#define KEY_UP_CODE   DT_PROP(DT_ALIAS(sw0), zephyr_code) /* SW2 */
#define KEY_MODE_CODE DT_PROP(DT_ALIAS(sw1), zephyr_code) /* SW3 */

(3)时间调整

switch (field) {
case UI_FIELD_HOUR: t.tm_hour = (t.tm_hour + 1) % 24; break;
case UI_FIELD_MIN: t.tm_min = (t.tm_min + 1) % 60; break;
case UI_FIELD_SEC: t.tm_sec = (t.tm_sec + 1) % 60; break;
}
/* IRTC 的 DOW 字段这里没维护,置 -1 让驱动跳过它 */
t.tm_wday = -1;
t.tm_yday = -1;
t.tm_isdst = -1;

rtc_set_time(rtc_dev, &t);

(4)上电时间恢复判定

rtc_ok  = (rtc_get_time(rtc_dev, &from_rtc) == 0) && time_is_sane(&from_rtc);
bkup_ok = (time_store_load(&from_bkup) == 0) && time_is_sane(&from_bkup);

/*
* VBAT 没断时 IRTC 一直在走,它的值必然比 flash 里那份旧备份新,用它。
* 只有 IRTC 不可信、或者它比备份还旧(说明被冷复位了),才回灌备份。
*/
if (rtc_ok && (!bkup_ok ||
time_store_key(&from_rtc) >= time_store_key(&from_bkup))) {
return "RUNNING";
}

其中 time_is_sane() 用"年份是否落在 2020–2099 之间"作为可信性判据——IRTC 冷复位后年份会变成 2112,正好落在区间之外。


五、功能展示

5.1 屏幕显示效果

屏幕自上而下分为四个区域:

  • 标题行:蓝色 RTC CLOCK,5×7 点阵放大 2 倍绘制。
  • 时间区:青绿色七段数码管,格式为 HH:MM:SS,两个冒号为静态绘制。
  • 指示条 / 日期行:进入设置模式后,当前编辑的字段下方会出现琥珀色下划线指示条;日期行显示 YYYY-MM-DD。
  • 状态行:显示当前状态,正常走时为 RUNNING,设置模式下依次为 SET HOUR / SET MINUTE / SET SECOND,上电恢复时可能显示 RESTORED 或 COLD START。

进入设置模式后,被编辑的两位数字会以琥珀色每 400 ms 明暗闪烁一次,配合下划线指示条,可以非常直观地看出当前正在调整哪一项。

5.2 串口输出

以下是实机运行时通过串口捕获到的完整启动日志:

*** Booting Zephyr OS build v4.4.0-8295-g0bd807ceeb75 ***

=== FRDM-MCXN236 RTC CLOCK ===
SW2 = +1 on the selected field, SW3 = next field
<inf> fs_zms: 4 Sectors of 8192 bytes
<inf> fs_zms: alloc wra: 0, 1e00
<inf> fs_zms: data wra: 0, 0
<inf> time_store: ZMS mounted at 0xf0000, sector 8192 x 4
[RTC ] 2026-01-01 00:03:44
[BOOT] no valid flash backup
[TIME] 2026-01-01 00:03:44
[TIME] 2026-01-01 00:03:45
[TIME] 2026-01-01 00:03:46

日志的含义依次是:备份存储挂载成功(4 个 8 KB 扇区,起始地址 0xF0000);读到 IRTC 中的时间为 00:03:44 且判定为可信;此时尚无有效的 Flash 备份;随后进入主循环,每秒输出一次当前时间。

按键操作时的输出格式为 [SET] ...(调整了某一项)、[DONE] ...(退出设置并落盘)、[NOW ] ...(正常走时下手动查询)。

5.3 复位保持验证

在实机上进行了复位测试:复位前串口最后一行为 [TIME] 2026-01-01 00:00:46,按下复位键后系统重启,启动日志中读到的 IRTC 值为 00:00:48——时间是连续的,证明 IRTC 在复位过程中持续走时,未被清零。


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

难题一:显示刷新太慢,GUI 方案完全走不通

现象:由于显示屏走 GPIO 模拟 SPI,整屏刷新一次需要数秒,秒位根本来不及更新。

解决:放弃 GUI 框架,改为自绘七段数码管 + 段级脏区重绘。通过异或运算精确定位"变化的段",把每秒的数据发送量从 153.6 KB 降到几百字节。日期行和状态行同样采用逐字符 diff 策略。

这个问题的启发是:不要脱离硬件条件去选技术栈。同样一块屏,接硬件 SPI 和接 bitbang SPI,能采用的软件方案完全不同。

难题二:NVS 挂载失败 —— Unsupported write block size

这是本项目中最花时间、也最有收获的一个问题。

现象:掉电备份最初选用 Zephyr 的 NVS(Non-Volatile Storage)文件系统,代码编译通过、烧录正常,但一上电串口就报错:

<err> fs_nvs: Unsupported write block size
<err> time_store: nvs_mount: -22

排查过程:-22 即 -EINVAL。顺着报错信息定位到 Zephyr 源码 subsys/kvss/nvs/nvs.c 中的这段检查:

write_block_size = flash_get_write_block_size(fs->flash_device);
if (write_block_size > NVS_BLOCK_SIZE || write_block_size == 0) {
LOG_ERR("Unsupported write block size");
return -EINVAL;
}

而 NVS_BLOCK_SIZE 定义为 32。再去查 MCX N236 的设备树,找到片内 Flash 节点的定义:

flash: flash@0 {
compatible = "soc-nv-flash";
reg = <0 DT_SIZE_M(1)>;
erase-block-size = <DT_SIZE_K(8)>;
/* MCXN23x ROM Flash API supports writing of 128B pages. */
write-block-size = <128>;
};

根本原因:MCX N23x 的 ROM Flash API 一次最少写入一个 128 字节的页,而 NVS 内部缓冲区上限只有 32 字节,二者从架构上就不兼容——这不是配置问题,NVS 在这块芯片上根本用不了。

解决方法:改用 Zephyr 较新的 ZMS(Zephyr Memory Storage)。ZMS 是为兼容 RRAM、MRAM 等新型存储介质而设计的,其内部缓冲区大小可以通过 Kconfig 调整:

CONFIG_ZMS=y
CONFIG_ZMS_CUSTOMIZE_BLOCK_SIZE=y
CONFIG_ZMS_CUSTOM_BLOCK_SIZE=128

改用 ZMS 后挂载成功,串口输出 ZMS mounted at 0xf0000, sector 8192 x 4。API 层面 ZMS 与 NVS 高度相似(zms_mount / zms_write / zms_read),业务代码几乎不用改动。

收获:遇到框架层的报错,直接去读框架源码里那句报错的上下文,往往比在网上搜关键词更快。同时也认识到,通用中间件并不总是适配所有芯片,选型时必须核对底层参数。

难题三:如何判断 IRTC 里的时间"可不可信"

问题:冷启动时 IRTC 里也有值,怎么知道这个值是真实的时间,还是复位后的垃圾值?

解决:查阅 Zephyr 的 IRTC 驱动源码,发现其中定义了

/* The IRTC Offset is 2112 instead of 1900 [2112 - 1900] -> [212] */
#define RTC_NXP_IRTC_YEAR_OFFSET 212

即 IRTC 的年份基准是 2112。冷复位后年份寄存器为 0,换算出来的年份就是 2112。因此只要判断"年份是否落在 2020–2099 之间",就能可靠地区分有效值和冷复位后的初值。

此外还增加了一层保险:即使 IRTC 的值看起来合法,也要和 Flash 备份比较——如果 IRTC 反而比备份更旧,说明它被冷复位过,仍然以备份为准。这样避免了"用旧备份覆盖正在正常走时的 IRTC"和"用垃圾值覆盖有效备份"两个方向的错误。


七、心得体会

做完这个项目,最大的感受是:一个看起来很简单的需求,真正做扎实之后,涉及的东西远比想象中多。

"做一个电子时钟"听起来像是入门练习,但实际动手后会发现,每一条需求背后都有需要权衡的地方。显示时间——屏幕刷新速度跟不上怎么办?按键调时——消抖怎么做、状态机怎么设计才不容易误操作?掉电保持——芯片到底能不能保持、保持不了的话备份多久写一次才不会磨坏 Flash?这些问题都没有标准答案,只能结合具体硬件去测、去算。

第一点体会是要善用 RTOS 的抽象,但也要能穿透它。 Zephyr 的设备树 + Kconfig 体系确实大幅降低了驱动适配的工作量:按键的消抖、RTC 的寄存器操作、Flash 的分区管理,都有现成的框架接管,业务代码可以写得很干净。但当 NVS 挂载失败时,光看应用层代码是永远找不到原因的——必须能读懂框架源码,能查设备树里那一行 write-block-size = <128>。框架是用来提高效率的,不是用来当黑盒的。

第二点体会是性能优化要打在真正的瓶颈上。 段级脏区重绘的思路本身并不复杂,核心就是一行异或运算,但它带来的效果是数量级的。前提是先搞清楚瓶颈在哪——如果没有意识到"整屏 153.6 KB 在 bitbang SPI 上要几秒"这个事实,可能会去优化别的地方,做很多无用功。先测量,再优化,这句话在嵌入式开发里尤其重要。

第三点体会是要对系统的边界条件保持诚实。 掉电保持这个功能,如果只做到"IRTC 在 VBAT 域,所以断电不丢"就交差,在有纽扣电池的板子上没问题,但在这块没装电池的开发板上就是错的。正是因为老老实实去确认了"VBAT 实际跟随 VDD"这个事实,才有了第二层 Flash 备份的设计。同样,备份周期定为 300 秒也意味着断电恢复后最大可能有 5 分钟的误差——这是一个真实存在的取舍,写在代码注释里比假装它不存在要负责任得多。

最后,这个项目让我对 NXP 的 MCX N 系列和 Zephyr 生态都熟悉了不少。MCX N236 的片内资源相当丰富,IRTC、FMU、LPUART 这些外设在 Zephyr 中都有成熟的驱动支持;而 Zephyr 的设备树机制让"换一块板子"这件事从改一堆宏定义变成了改一个 overlay 文件。这套工程方法上的收获,可能比这个时钟本身更有价值。


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