Funpack5-4 - 基于 MCXN236 实现环境监测
该项目使用了MCXN236,实现了环境监测的设计,它的主要功能为:时间显示调整、温湿度显示,光线紫外线显示,串口UI显示。
标签
Funpack活动
环境监测
MCXN236
I2C传感器
串口时钟显示
makeWorks
更新2026-10-20
5

Funpack5-4 - 基于 MCXN236 实现环境监测




一、项目介绍

本项目基于NXP FRDM-MCXN236开发板,配合SI1133光照/UV传感器、SHTC3温湿度传感器、DS3231高精度RTC及24C32 EEPROM,构建了一套功能完整的桌面环境监测系统。系统以1秒为周期采集环境数据,通过串口终端以TUI(文本用户界面)形式实时显示时间、温湿度、光照和UV指数,支持单按键时间调整、掉电时间保持、EEPROM读写验证等功能。

系统的核心设计目标是“在有限的硬件资源下实现信息密度高、交互友好的人机界面”。为此,项目充分利用了MCXN236的硬件I2C、GPIO中断、DWT周期计数器等外设,并在软件层面实现了无闪烁串口刷新、RTC秒驱动节拍、单按键多模式交互等机制。最终效果是在标准80列串口终端上呈现一个类似仪表盘的信息面板,操作直观,响应流畅。

选题动机很直接:这一期活动的任务 1(电子时钟设计)和任务 2(环境数据采集与显示)在硬件资源上高度重叠——都需要 RTC、都需要 I²C 传感器、都需要串口输出和 LED 状态指示。与其做成两个割裂的小 demo,不如把它们合并成一个真正能长期挂在桌面上运行的环境监测终端:时间由 DS3231 提供且掉电不丢,环境量由 SHTC3 与 SI1133 提供,人机交互由板载单按键加板载 LED 完成,数据出口是串口。这样一个系统同时满足了两个任务的功能点,也更接近实际产品的形态。

1.1 完成度对照

活动要求

本项目实现情况

任务2-1 通过 I²C 读取传感器数据

✅ 使用 LPI2C2,挂载 4 颗器件,并实现开机总线扫描

任务2-2 周期性采集环境信息

✅ 每 3 秒采集一轮温湿度 / 光照 / 紫外 / 芯片温度

任务2-3 通过串口输出采集结果

✅ LPUART4 @115200,输出带颜色与进度条的 TUI 界面

任务2-4 使用 LED 指示系统运行状态

✅ 板载红色 LED 三态状态机(运行 / 编辑 / 故障)

任务2-5 数据刷新周期不大于 1 秒

✅ 界面刷新由 RTC 秒跳变驱动,稳定 1 Hz

任务1-1 使用 RTC 获取系统时间

✅ DS3231,含振荡停止标志(OSF)检测与自动初始化

任务1-2 显示当前时分秒

✅ TUI 顶栏显示 YYYY-MM-DD Wday HH:MM:SS

任务1-3 使用按键实现时间调整

✅ 单按键:短按 +1、双击 −1、长按切换字段并保存

任务1-4 通过串口输出当前时间

✅ 与传感器数据同屏输出

任务1-5 掉电后时间能够保持

✅ DS3231 板载纽扣电池供电,MCU 掉电时间不丢

扩展-1 支持多种传感器接入

✅ 温湿度 + 光照 + 紫外 + RTC 温度 + EEPROM,共 4 类器件




二、硬件介绍

2.1 主控开发板:FRDM-MCXN236

FRDM-MCXN236 是 NXP 面向工业与消费物联网应用推出的紧凑型、可扩展开发板,核心是 MCX N236 微控制器:

  • 内核:Arm Cortex-M33,主频最高 150 MHz,支持 TrustZone 与 DSP 指令
  • 存储:1 MB 双组 Flash(支持完整 ECC),352 KB ECC RAM
  • 加速器:集成 SmartDMA,可在不占用 CPU 的情况下搬运外设数据
  • 安全:内置 EdgeLock 安全区域,是带独立安全内核、专用 ROM 与安全 RAM 的片上硬件安全子系统
  • 板载资源:MCU-Link 调试器(USB-C 一线完成供电 / SWD 调试 / VCOM 虚拟串口 / USB-to-SPI·I²C 桥接)、RGB 三色 LED、用户按键、QSPI Flash、Arduino / mikroBUS / FRDM 标准扩展接口
  • 开发环境使用MCUXpresso IDE(v25.6),基于MCUXpresso SDK进行裸机开发

本工程使用 MCUXpresso Config Tools 生成的 BOARD_InitBootClocks() 时钟配置,内核时钟为 FRO HF 48 MHz。之所以没有拉到 150 MHz,是因为本项目的瓶颈完全在 I²C(100 kHz)和串口(115200 bps)上,48 MHz 已经严重过剩,降频可以显著降低功耗——这也正好契合 MCX N236"面向超低功耗电池供电 IoT"的定位。程序启动时会调用 Startup_PrintClocks() 把实测的 Core / Bus / FRO12M / FRO_HF / I²C SCL / SysTick / DWT 频率全部打印到串口,方便随时核对时钟树是否符合预期。

2.2 板载外设与引脚分配

资源

实例

引脚

说明

调试串口

LPUART4

P1_8 (RX) / P1_9 (TX)

115200-8-N-1,时钟源 FRO12M 挂 FLEXCOMM4,经 MCU-Link 转 USB VCOM

传感器总线

LPI2C2

P4_0 (SDA) / P4_1 (SCL)

100 kHz,时钟源 FRO12M 挂 FLEXCOMM2

状态指示 LED

GPIO4

P4_18(红色)

低电平点亮,三态状态机驱动

用户按键

GPIO0

P0_6(SW3)

双边沿中断,向量 GPIO00_IRQn

2.3 I²C 传感器与外围器件

四颗器件全部挂在同一条 LPI2C2 总线上,通过 7 位从机地址区分:

① SHTC3(地址 0x70)—— 数字温湿度传感器
Sensirion 出品的超低功耗数字温湿度传感器,16 位分辨率,典型精度±2% RH和±0.2°C,工作范围-40°C至125°C,采用2×2×0.75 mm DFN封装,I2C地址0x70。SHTC3需要先发唤醒命令,然后发测量命令等待约12ms,再读取6字节数据(温度高/低/CRC、湿度高/低/CRC)。采用"命令字"式接口(而非寄存器地址式),每条命令 16 位:0xEFC8 读 ID、0x3517 唤醒、0xB098 休眠、0x805D 软复位、0x7866 触发一次"温度优先、关闭时钟拉伸"的正常模式测量。测量结果为 6 字节(温度 MSB/LSB + CRC,湿度 MSB/LSB + CRC),每字节组都带 CRC-8 校验(多项式 0x31,初值 0xFF)。典型转换时间约 12 ms,休眠电流仅数百 nA,非常适合电池供电场景。

② SI1133(地址 0x55)—— 紫外指数 / 环境光传感器
Silicon Labs 的光学传感器,集成双23位ADC和高灵敏度紫外/可见光/红外光电二极管阵列,环境光分辨率优于100 mlx,动态范围可达128 klx,支持I2C接口,工作电压1.62–3.6 V。它不是简单读寄存器就能拿到物理量,而是内部有 4 个 ADC 通道,需要配置通道列表与每个通道的 ADCCONFIG / ADCSENS / ADCPOST 参数,触发 FORCE_CH 命令后等待转换完成,再读回 4 组 24 位有符号原始值,最后用 Silicon Labs 官方给出的多项式系数做定点运算换算成 lux 和 UVI。本项目的通道分配为:CH0 = UV、CH1 = 可见光高增益、CH2 = 红外、CH3 = 可见光低增益。

③ DS3231(地址 0x68)—— 高精度实时时钟
Maxim(现 ADI)的温补晶振 RTC,精度 ±2 ppm(0 ℃~40 ℃),远优于普通 RC 振荡方案。板上带纽扣电池,主电源断开后时钟继续走,这是任务 1"掉电后时间保持"要求的关键。片内还集成了一个温度传感器,可用于读取芯片结温。寄存器采用 BCD 编码,状态寄存器的 OSF(Oscillator Stop Flag,0x80)位用于指示振荡器是否曾停振——如果电池耗尽或首次上电,OSF 会被置位,此时读出的时间是无效值,必须重新设置。

④ AT24C32(地址 0x57)—— I²C EEPROM
32 Kbit(4 KB)串行 EEPROM,32 字节页写。用于存储历史环境数据与配置参数。写操作有内部写周期(约 5 ms),期间器件不应答,需要等待;跨页写入必须由软件拆分,否则会回卷覆盖页首数据。与DS3231位于同一模块上,I2C地址由A0/A1/A2引脚决定,本模块默认为0x57。

板载 FXLS 加速度计(0x18)也在同一条总线上,本项目的开机总线扫描会把它一并列出,但当前功能未使用。




三、方案框图与设计思路

3.1 系统框图

┌─────────────────────────────────────────────────────┐
│                   FRDM-MCXN236                     │
│                                                     │
│ ┌────────────────┐   ┌────────────────┐         │
│ │ LPI2C5 主机   │   │ GPIO (SW/LED) │         │
│ └───────┬────────┘   └────────┬───────┘         │
│         │                     │                   │
│     ┌────┴───────┬─────────┬────┴────┐             │
│     │           │         │         │             │
│     ▼           ▼         ▼         ▼             │
│   SI1133     SHTC3     DS3231   24C32           │
│   0x55       0x70     0x68     0x57             │
│   (光照/UV)   (温湿度)   (RTC)     (EEPROM)         │
│                                                     │
│ ┌──────────────────────────────┐                   │
│ │ LPUART/FLEXCOMM4 (调试串口) │                   │
│ └──────────────┬───────────────┘                   │
└─────────────────┼───────────────────────────────────┘
                │
            USB虚拟串口
                │
                ▼
          PC (MobaXterm)
            串口TUI显示

3.2 软件分层

┌────────────────────────────────────────────────┐
│ 应用层 main.c:TUI 绘制 / 状态机 / 交互调度   │
├────────────────────────────────────────────────┤
│ 驱动层 shtc3.c si1133.c ds3231.c           │
│         eeprom24c32.c                         │
│         —— 统一 handle 结构 + 函数指针回调     │
├────────────────────────────────────────────────┤
│ 板级层 board.c / hardware_init.c / pin_mux.c │
│         clock_config.c / peripherals.c         │
├────────────────────────────────────────────────┤
│ SDK 层 fsl_lpi2c fsl_gpio fsl_clock         │
│         fsl_debug_console fsl_lpflexcomm     │
└────────────────────────────────────────────────┘

为什么用串口TUI而不是LCD? 项目目标是“环境监测”,信息展示是核心需求。串口终端零额外硬件成本,且通过ANSI转义码可以实现光标定位、颜色和局部刷新,信息密度远高于小尺寸LCD。代价是需要处理闪烁和残留字符问题,这恰好是软件设计的重点。

统一I2C回调接口。 四个传感器的寄存器访问协议差异很大:DS3231用1字节寄存器地址,24C32用2字节,SHTC3需要先发16位命令再读,SI1133有命令-响应状态机。如果每个驱动都直接操作LPI2C寄存器,代码会非常冗长。项目采用回调函数指针方案,每个驱动只依赖两个函数指针I2C_SendFunc和I2C_ReceiveFunc,都指向BOARD_LPI2C_Send/Receive,极大简化了驱动移植和总线复用。

typedef struct {
   shtc3_i2c_send_t  I2C_SendFunc;
   shtc3_i2c_recv_t  I2C_ReceiveFunc;
   uint8_t           slaveAddress;   /* 7-bit address, default 0x70 */
} shtc3_handle_t;

RTC秒驱动节拍。 最初用 SDK_DelayAtLeastUs(1000000U) 实现1秒刷新,但传感器读取耗时导致实际周期波动在1.25秒左右,秒数有时跳1有时跳2。改为每20ms轮询一次RTC秒寄存器,只有秒值变化时才执行刷新,实现了与RTC严格同步的节拍。

单按键多模式交互。 板载只有一个按钮,需要完成“进入编辑、加/减数值、切换字段、保存退出”四种操作。方案是:短击(<1s)= +1,双击(<350ms内两次短击)= -1,长按(≥1s)= 下一字段。长按在按住满1秒时立即触发,无需松手,用户感知更灵敏。

TUI无闪烁刷新。 每行动态内容先构建到缓冲区,再用单次PRINTF配合\033[row;1H光标定位发送。设置每行diff缓存,如果内容与上一帧完全相同则跳过发送,静止的数字不产生任何串口流量。

3.3 数据流与调度策略

整个系统是单线程轮询 + 中断打标记的裸机架构,没有上 RTOS,理由是本项目的实时性要求非常宽松(最快 1 Hz 刷新),引入 RTOS 反而增加复杂度和 RAM 开销。

调度上做了三级时间分层:

  1. 20 ms 软 tick(主循环末尾 SDK_DelayAtLeastUs(20000U, ...))—— 驱动按键长按识别、LED 状态机、编辑态闪烁
  2. 1 Hz 界面刷新(由 RTC 秒值变化触发)—— 保证任务 2"刷新周期不大于 1 秒"
  3. 3 s 传感器采集(由 cur_sec % 3 == 0 触发)—— 与 SI1133 单次测量约 200 ms 的耗时相匹配,避免刷新卡顿

这里有个容易踩的坑:SI1133 的一次完整测量需要 200 ms 以上(force_measurement 后先 Delay_Ms(200),再轮询 IRQ_STATUS)。如果每 1 秒都去读光照,界面就会有明显的顿挫感。把传感器采集降到 3 秒一次、界面刷新保持 1 秒,时间的秒针依旧平滑走动,观感上就完全没有卡顿——这是把"显示"和"采集"两个周期解耦后得到的效果。

此外还设计了RTC 失效降级路径:一旦 DS3231 读取失败(okRtc 清零),刷新触发源自动切换为主循环计数(每 50 次循环刷新一次),LED 转入 5 Hz 快闪,界面顶栏显示红色 RTC not available,时间字段显示 ----。系统不会因为一颗器件故障而整机停摆。




四、调试软件介绍

工具

版本 / 说明

用途

MCUXpresso IDE

25.6.136

主开发环境,基于 Eclipse,负责工程管理、编译、下载、在线调试

MCUXpresso SDK

工程内 mcuxsdk_version.h

提供 fsl_lpi2c / fsl_gpio / fsl_clock / fsl_debug_console 等外设驱动

MCUXpresso Config Tools

IDE 内置插件

图形化生成 pin_mux.c、clock_config.c、peripherals.c;工程内保留了 .bak 副本以便对比

MCU-Link 调试器

板载,CMSIS-DAP

通过 USB-C 一线完成 SWD 烧录调试 + VCOM 虚拟串口,无需外接调试器和 USB-TTL

串口终端

115200-8-N-1

需支持 ANSI 转义序列(推荐 MobaXterm / Windows Terminal / PuTTY),用于显示 TUI 界面

调试手段上,本项目大量使用了串口打印做运行时诊断,因为很多问题(I²C 不应答、时钟配错、器件 ID 不符)用断点很难复现。为此在启动阶段专门写了两个自检函数:

  • Startup_PrintClocks():运行时读取并打印全部时钟节点的实际频率,一次性确认时钟树配置是否正确
  • Startup_ScanI2C():扫描 0x08~0x77 全部地址,对每个应答的地址尝试识别器件名并打印
static void Startup_ScanI2C(void)
{
   PRINTF("\r\nI2C bus scan:\r\n");
   int found = 0;
   for (uint8_t a = 0x08; a < 0x78; a++) {
       uint8_t dummy = 0;
       if (BOARD_LPI2C_Receive(BOARD_ACCEL_I2C_BASEADDR,
                               a, 0, 0, &dummy, 1) == kStatus_Success) {
           PRINTF(" 0x%02X | %s\r\n", a, DeviceName(a));
           found++;
      }
  }
   PRINTF("Total %d I2C device(s) found\r\n", found);
}

这个扫描函数在调试初期帮了大忙——接错线、地址跳线不对、上拉缺失,都会在扫描结果里立刻暴露出来,比单步调试快得多。




五、软件流程图与关键代码

5.1 总体流程

        ┌──────────────┐
      │ 上电复位     │
      └──────┬───────┘
              ▼
  ┌───────────────────────┐
  │ BOARD_InitHardware() │ 引脚/时钟/调试串口
  │ BOARD_Accel_I2C_Init()│ LPI2C2 @100kHz
  │ dwt_init()           │ DWT CYCCNT 毫秒时基
  │ SW3 双边沿中断使能     │
  │ LED 引脚初始化         │
  └───────────┬───────────┘
              ▼
  ┌───────────────────────┐
  │ 阶段一:启动自检(文本) │
  │ · 打印时钟树         │
  │ · 扫描 I²C 总线       │
  │ · SI1133_Init       │
  │ · SHTC3_Init         │
  │ · DS3231_Init       │
  │   └ OSF置位?→用编译时间初始化 │
  │ · EEPROM24C32_Test   │
  │   └ 页内写+跨页写回读 │
  └───────────┬───────────┘
              ▼
  ┌───────────────────────┐
  │ 阶段二:进入 TUI       │
  │ 隐藏光标 / 清屏       │
  │ 绘制静态边框         │
  │ ※ 此后禁止任意 PRINTF │
  └───────────┬───────────┘
              ▼
  ┌───────────────────────────────────────┐
  │           阶段三:主循环 (20ms)       │
  │                                       │
  │ 1. 长按识别(按下≥1000ms)           │
  │ 2. 消费按键事件 → 单击/双击/长按     │
  │ 3. 350ms 窗口结算 pending click       │
  │ 4. 编辑态 60s 无操作 → 自动保存退出   │
  │ 5. LED 状态机更新                     │
  │ 6. 读 DS3231 时间                     │
  │ 7. 编辑字段闪烁 tick(400ms 周期)   │
  │ 8. 判断是否刷新(秒变化 or 强制)     │
  │ 9. 若刷新:                           │
  │       · uptime_s++ / cycle++           │
  │       · 若 sec%3==0 → 采集 4 路传感器 │
  │       · 重绘 header / 4 行数据 / 状态栏│
  │ 10. 延时 20ms                         │
  └───────────────────────────────────────┘

5.2 关键代码一:毫秒级时间基准(DWT CYCCNT)

按键长按识别、双击窗口、超时退出都需要毫秒计时,但项目没有用 SysTick(SDK_DelayAtLeastUs 已占用),改用 Cortex-M33 内核自带的 DWT 周期计数器:

static void dwt_init(void)
{
   CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
#if defined(DWT_LAR)
   DWT->LAR = 0xC5ACCE55U;      /* 解锁 DWT,部分内核需要 */
#endif
   DWT->CYCCNT = 0;
   DWT->CTRL  |= DWT_CTRL_CYCCNTENA_Msk;
}
​
static inline uint32_t ms_now(void)
{
   return DWT->CYCCNT / (SystemCoreClock / 1000U);
}

零硬件开销、零中断,一行除法就得到上电以来的毫秒数。在 48 MHz 下 CYCCNT 约 89 小时回绕一次,对按键时序判断影响可忽略;若需要连续运行数月,可改用 LPTMR 或加溢出计数。

5.3 关键代码二:按键中断 + 软件消抖 + 单击/双击/长按识别

只有一个按键,却要实现"短按 +1、双击 −1、长按切字段"三种语义,做法是中断只记录边沿时间戳,语义判断全部放到主循环:

void BOARD_SW_IRQ_HANDLER(void)
{
   GPIO_GpioClearInterruptFlags(BOARD_SW_GPIO, 1U << BOARD_SW_GPIO_PIN);
​
   static uint32_t last_edge_ms = 0;
   uint32_t now = ms_now();
​
   if (now - last_edge_ms < 30U) {   /* 30ms 软件消抖 */
       SDK_ISR_EXIT_BARRIER;
       return;
  }
   last_edge_ms = now;
​
   if (GPIO_PinRead(BOARD_SW_GPIO, BOARD_SW_GPIO_PIN) == 0U) {
       g_btn_press_ms   = now;       /* 按下:记录时刻 */
       g_btn_pressed    = true;
       g_btn_long_fired = false;
  } else {
       if (g_btn_pressed) {          /* 释放:结算 */
           uint32_t held = now - g_btn_press_ms;
           g_btn_pressed = false;
           if (!g_btn_long_fired && held >= 30U) {
               g_btn_event    = EVT_CLICK;
               g_btn_event_ms = now;
          }
           g_btn_long_fired = false;
      }
  }
   SDK_ISR_EXIT_BARRIER;
}

主循环里再区分单击与双击——单击不能立即响应,必须先等 350 ms 看有没有第二击:

} else if (e == EVT_CLICK) {
   if (pending_click && (t - pending_click_ms < 350U)) {
       pending_click = false;
       on_double_click(&ds3231Handle);   /* 350ms 内第二击 → 双击 */
       force_refresh = 1;
  } else {
       pending_click    = true;          /* 挂起,等待窗口结束 */
       pending_click_ms = t;
  }
}
...
/* 窗口超时,确认为单击 */
if (pending_click && (ms_now() - pending_click_ms >= 350U)) {
   pending_click = false;
   on_short_click(&ds3231Handle);
   force_refresh = 1;
}

长按则在按住状态下满 1000 ms 直接触发(不用等松手),触发后置 g_btn_long_fired,避免松手时再补一个单击事件:

if (g_btn_pressed && !g_btn_long_fired &&
(ms_now() - g_btn_press_ms >= 1000U)) {
g_btn_long_fired = true;
g_btn_event = EVT_LONG;
g_btn_event_ms = ms_now();
}

5.4 关键代码三:SHTC3 温湿度采集(含 CRC 校验与重试)

SHTC3 走"命令字"接口,16 位命令被当作 2 字节子地址发送,测量结果必须逐字节做 CRC-8 校验,否则总线上的偶发干扰会被当成真实数据:

static int SHTC3_Measure(shtc3_handle_t *h, float *temp, float *humi)
{
uint8_t buf[6] = {0};
uint16_t rawT, rawH;
uint8_t crcT, crcH;

if (SHTC3_WakeUp(h) != 0) return 1;

/* 触发一次"温度优先、关闭时钟拉伸"的测量 */
if (SHTC3_WriteCmd(h, SHTC3_NOR_READ_TEMP_FIR_DIS_CMD) != 0) return 1;
Delay_Ms(15); /* 典型转换时间约 12ms */

if (SHTC3_ReadBytes(h, buf, 6) != 0) return 1;

Delay_Ms(1);
SHTC3_Sleep(h); /* 测完立即休眠,省电 */

rawT = (uint16_t)((buf[0] << 8) | buf[1]); crcT = buf[2];
rawH = (uint16_t)((buf[3] << 8) | buf[4]); crcH = buf[5];

if (SHTC3_CRC_CHECK(rawT, crcT) == 0 &&
SHTC3_CRC_CHECK(rawH, crcH) == 0) {
*temp = 175.0f * (float)rawT / 65535.0f - 45.0f; /* ℃ */
*humi = 100.0f * (float)rawH / 65535.0f; /* %RH */
return 0;
}
return 1; /* CRC 不过 → 判为坏帧 */
}

外层再包一层重试与量程兜底,保证交给界面的一定是物理上合理的值:

int SHTC3_GetTempAndHumi(shtc3_handle_t *h, float *temp, float *humi)
{
float t = 0.0f, hu = 0.0f;
int ret = 1;

for (int i = 0; i < 3; i++) { /* 最多重试 3 次 */
if (SHTC3_Measure(h, &t, &hu) == 0) { ret = 0; break; }
}
if (ret != 0) { *temp = 0.0f; *humi = 0.0f; return 1; }

if (t > 70.0f || t < -40.0f) t = 25.0f; /* 量程合理性检查 */
if (hu > 100.0f || hu < 0.0f) hu = 50.0f;

*temp = t; *humi = hu;
return 0;
}

5.5 关键代码四:SI1133 光照与紫外换算(定点多项式)

SI1133 是本项目最难啃的一颗器件。它的 4 路 ADC 输出的是 24 位有符号原始码,必须经过 Silicon Labs 官方给定的多项式系数做定点运算才能得到 lux 与 UVI。系数以 {info, mag} 结构存储,info 里编码了 x 阶数、y 阶数、符号与移位量:

static const si1133_lux_coeff_t s_lk = {
{ { 0, 209 }, { 1665, 93 }, { 2064, 65 }, { -2671, 234 } },
{ { 0, 0 }, { 1921, 29053 }, { -1022, 36363 }, { 2320, 20789 },
{ -367, 57909 }, { -1774, 38240 }, { -608, 46775 }, { -1503, 51831 },
{ -1886, 58928 } }
};
static const si1133_coeff_t s_uk[2] = {
{ 1281, 30902 }, { -638, 46301 }
};

照度计算还有一个高/低增益分支切换:当可见光高增益通道或红外通道的原始值超过阈值 16000 时,说明光强较大,改用 4 系数的高幅值多项式;否则用 9 系数的低幅值多项式,以保证弱光下的分辨率:

static int32_t get_lux(int32_t vis_high, int32_t vis_low, int32_t ir)
{
if (vis_high > ADC_THRESHOLD || ir > ADC_THRESHOLD) {
return calc_poly(vis_high, ir, INPUT_FRACTION_HIGH, LUX_OUTPUT_FRACTION,
NUMCOEFF_HIGH, &s_lk.coeff_high[0]);
}
return calc_poly(vis_low, ir, INPUT_FRACTION_LOW, LUX_OUTPUT_FRACTION,
NUMCOEFF_LOW, &s_lk.coeff_low[0]);
}

完整的一次测量流程是:发 FORCE_CH 命令 → 延时 200 ms → 轮询 IRQ_STATUS 直到等于 0x0F(四通道全部完成,最多重试 400 次防死循环)→ 从 IRQ_STATUS 起突发读 13 字节(1 字节状态 + 4 通道 × 3 字节)→ 符号扩展 → 多项式换算 → 右移 12 位去掉定标:

retval = force_measurement(h);
if (retval != SI1133_OK) return retval;

Delay_Ms(200);

while (1) { /* 轮询四通道完成标志 */
retval = SI1133_ReadReg(h, SI1133_REG_IRQ_STATUS, &response);
if (response == 0x0FU) break;
if (++retry_cnt >= 400U) return retval;
Delay_Ms(5);
}

if (measure(h, &samples) != SI1133_OK) return SI1133_ERROR_I2C_TRANSACTION_FAILED;

*lux = (float)get_lux(samples.ch1, samples.ch3, samples.ch2)
/ (float)(1 << LUX_OUTPUT_FRACTION);
*uvi = (float)get_uv(samples.ch0)
/ (float)(1 << UV_OUTPUT_FRACTION);

芯片初始化时按参考驱动配置四通道(CH0=UV、CH1=可见光高、CH2=IR、CH3=可见光低),并在读 PART_ID 时做了 5 次重试——因为 SI1133 上电后至少需要 25 ms 才能访问,过早读取会拿到无效值:

retval += set_parameter(h, SI1133_PARAM_CH_LIST,    0x0F);
retval += set_parameter(h, SI1133_PARAM_ADCCONFIG0, 0x78); /* UV */
retval += set_parameter(h, SI1133_PARAM_ADCSENS0, 0x71);
retval += set_parameter(h, SI1133_PARAM_ADCPOST0, 0x40);
retval += set_parameter(h, SI1133_PARAM_ADCCONFIG1, 0x4D); /* 可见光高增益 */
retval += set_parameter(h, SI1133_PARAM_ADCSENS1, 0xE1);
retval += set_parameter(h, SI1133_PARAM_ADCPOST1, 0x40);
retval += set_parameter(h, SI1133_PARAM_ADCCONFIG2, 0x41); /* 红外 */
retval += set_parameter(h, SI1133_PARAM_ADCSENS2, 0xE1);
retval += set_parameter(h, SI1133_PARAM_ADCPOST2, 0x50);
retval += set_parameter(h, SI1133_PARAM_ADCCONFIG3, 0x4D); /* 可见光低增益 */
retval += set_parameter(h, SI1133_PARAM_ADCSENS3, 0x87);
retval += set_parameter(h, SI1133_PARAM_ADCPOST3, 0x40);

另外,SI1133 的参数写入不是普通寄存器写,而是要向 HOSTIN0 连写 2 字节:第 1 字节是参数值,第 2 字节是 0x80 | 参数地址,第 2 字节正好落在 COMMAND 寄存器上从而触发 PARAM_SET。写完后还要轮询 RESPONSE0 的命令计数器发生变化,才算真正生效——这是它和普通 I²C 器件最大的不同。

5.6 关键代码五:DS3231 掉电检测与自动初始化

任务 1 要求"掉电后时间能够保持"。DS3231 靠纽扣电池维持走时,但如果电池被取走过或首次上电,时间就是无效的。正确做法是读 OSF 标志位判断,而不是无条件相信 RTC:

if (DS3231_Init(&ds3231Handle) == DS3231_OK) {
g_data.okRtc = 1;
uint8_t osf = 0;
DS3231_IsOscillatorStopped(&ds3231Handle, &osf);
if (osf) {
/* 振荡器曾停振 → 时间无效,用固件编译时间初始化 */
static const char month_str[12][4] = {
"Jan","Feb","Mar","Apr","May","Jun",
"Jul","Aug","Sep","Oct","Nov","Dec"
};
char mon[4] = {0};
int day = 0, year = 0, hh = 0, mm = 0, ss = 0;
struct tm tm_set;
memset(&tm_set, 0, sizeof(tm_set));
sscanf(__DATE__, "%3s %d %d", mon, &day, &year);
sscanf(__TIME__, "%d:%d:%d", &hh, &mm, &ss);
for (int i = 0; i < 12; i++) {
if (strncmp(mon, month_str[i], 3) == 0) { tm_set.tm_mon = i; break; }
}
tm_set.tm_year = year - 1900;
tm_set.tm_mday = day;
tm_set.tm_hour = hh; tm_set.tm_min = mm; tm_set.tm_sec = ss;
DS3231_SetTm(&ds3231Handle, &tm_set);
DS3231_ClearOscillatorFlag(&ds3231Handle);
PRINTF("RTC initialized to build time\r\n");
}
}

__DATE__ / __TIME__ 是编译器注入的宏,展开成固件编译那一刻的时间字符串。这样即使 RTC 完全失效,系统也能给出一个"大致正确"的时间,用户再用按键校准即可,不至于显示 2000-01-01。

为了让上层代码可以直接用 C 标准库的 struct tm,DS3231 驱动做了一层 BCD ↔ 二进制封装,DS3231_GetTm() 还会自动推算 tm_wday(星期)和 tm_yday(年内第几天),并自带不依赖时区的 ds3231_timegm() / ds3231_gmtime()。

5.7 关键代码六:无闪烁串口 TUI(行缓冲差分刷新)

这是本项目在交互体验上花的最大功夫。直接用 PRINTF 刷屏会有两个问题:一是整屏重绘导致明显闪烁,二是光标乱跳。解决方案是自己实现一套行缓冲 + 差分刷新:

static char g_line[LB_SIZE];              /* 当前行缓冲 */
static char g_line_prev[NUM_ROWS][LB_SIZE]; /* 每行上一次内容的缓存 */

static void lb_flush(int row)
{
if (row <= 0 || row >= NUM_ROWS) return;
if (strcmp(g_line, g_line_prev[row]) == 0) return; /* 内容没变 → 跳过 */
strncpy(g_line_prev[row], g_line, LB_SIZE - 1);
g_line_prev[row][LB_SIZE - 1] = '\0';
PRINTF("\033[%d;1H%s", row, g_line); /* 绝对定位到该行行首后输出 */
}

每次重绘时,只有内容真正发生变化的行才会被写到串口。实测下来,稳定运行时每秒通常只有 2~3 行需要更新(顶栏秒数、状态栏计数),串口负载极低,画面完全无闪烁。

带颜色的输出还带来一个额外麻烦:ANSI 转义序列和 UTF-8 多字节字符都不占可见列宽,直接用 strlen 补齐会把边框撑歪。所以专门写了可见宽度计算:

static int lb_visible_len(void)
{
int vis = 0;
const char *p = g_line;
while (*p) {
if ((unsigned char)p[0] == 0x1B && p[1] == '[') {
p += 2;
while (*p && (*p < '@' || *p > '~')) p++; /* 跳过 ANSI 序列 */
if (*p) p++;
} else if ((unsigned char)*p >= 0x80) {
p++;
while (*p && ((unsigned char)*p & 0xC0) == 0x80) p++; /* 跳过 UTF-8 续字节 */
vis++;
} else {
vis++;
p++;
}
}
return vis;
}

还有一条纪律:进入 TUI 之后严禁再调用任何 PRINTF,所有状态一律通过顶栏的 [ LIVE ] / [ EDIT ] 徽章和状态栏的 OK/-- 标记来表达。任何一句随手加的调试打印都会把光标位置打乱,导致整个画面错位——这一点在代码注释里也专门写明了。

界面布局固定为 80 列 × 13 行。

+==============================================================================+
| ENV MONITOR 2026-09-23 Wed 10:47:05 [ LIVE ] |
+==============================================================================+
| |
| Temperature 27.92 C |
| Humidity 76.13 % [############### ] |
| Light 510.62 lx [########## ] |
| UV Index 0.08 [ ] |
| |
+==============================================================================+
| RTC OK SHTC3 OK SI1133 OK 24C32 OK Cycle 045186 Up 12:33:06 |
+==============================================================================+

其中行号由 R_TOP ~ R_BOT 一组宏固定,四个数据行分别是 R_TEMP / R_HUMI / R_LIGHT / R_UV,进度条宽度 BAR_WIDTH 为 20 个字符,按 value / max_val 比例填充(湿度与照度上限 100 / 1000,UVI 上限 11)。

TUI_TEMPLATE 宏可以在编译期切换三种边框风格(ASCII +== / Unicode ║═ / 极简分隔线),方便适配不同的终端字体。

5.8 关键代码七:LED 三态状态机

板载红色 LED 为低电平点亮,用一个 20 ms tick 驱动的状态机表达系统状态(下面代码块中的中文注释为报告添加,源码中无):

static void led_update(void)
{
g_led_tick++;
int on = 0;
switch (g_led_state) {
case LED_EDIT: /* 编辑时间:常亮 */
on = 1;
break;
case LED_ERROR: /* RTC 故障:快速闪烁 */
on = ((g_led_tick / 2U) & 1U) ? 0 : 1;
break;
case LED_RUN: /* 正常运行:心跳 */
default:
on = (g_led_tick % 50U) < 5U; /* 每 1s 亮 100ms */
break;
}
led_set(on);
}

LED_RUN 的 % 50 对应 20 ms × 50 = 1 s 周期、亮 100 ms,是一个舒缓的心跳;LED_ERROR 用 /2 & 1 得到 40 ms 翻转的快速闪烁,一眼就能看出系统出问题了。状态优先级为:编辑态 > 故障态 > 运行态。

5.9 关键代码八:AT24C32 开机自检

EEPROM 的可靠性直接决定后续数据记录功能能不能用,所以在启动阶段就做了完整的读写自检,覆盖"页内写"和"跨页写"两种最容易出错的情况:

for (int i = 0; i < 16; i++) wbuf[i] = (uint8_t)(0xA0 + i);
PRINTF("[TEST] In-page write @ 0x0000 ...\r\n");
if (EEPROM24C32_Write(h, 0x0000, wbuf, 16) != EEPROM24C32_OK) return 0;
memset(rbuf, 0, sizeof(rbuf));
if (EEPROM24C32_Read(h, 0x0000, rbuf, 16) != EEPROM24C32_OK) return 0;
for (int i = 0; i < 16; i++) if (wbuf[i] != rbuf[i]) { pass = 0; break; }

for (int i = 0; i < 16; i++) wbuf[i] = (uint8_t)(0xB0 + i);
PRINTF("[TEST] Cross-page write @ 0x001E ...\r\n"); /* 0x001E+16 跨越 0x0020 页边界 */
if (EEPROM24C32_Write(h, 0x001E, wbuf, 16) != EEPROM24C32_OK) return 0;

跨页测试特意选在地址 0x001E 写 16 字节,让数据跨过 0x0020 的页边界——这是 EEPROM 驱动最经典的 bug 来源。驱动的 EEPROM24C32_Write() 内部已实现自动分页拆分与写周期等待。




六、功能展示

6.1 开机自检输出

上电后串口依次输出时钟树、I²C 扫描结果、各器件初始化状态与 EEPROM 自检结论,随后清屏进入 TUI:

Clock tree:
Core : 48000000 Hz (48 MHz)
Bus : 48000000 Hz (48 MHz)
FRO12M : 12000000 Hz (12 MHz)
FRO_HF : 48000000 Hz (48 MHz)
I2C SCL : 12000000 Hz (target)
SysTick : 48000000 Hz (SystemCoreClock)
DWT CYCCNT : 48000000 Hz (ms timing)

其中 I2C SCL 一行打印的是 BOARD_ACCEL_I2C_CLOCK_FREQ(12 MHz),它是LPI2C2 的功能时钟源(FRO12M 挂 FLEXCOMM2),不是 SCL 线上的实际波特率。SCL 的真实速率由 LPI2C_MasterGetDefaultConfig() 里的 baudRate_Hz = 100000U 决定,即标准模式 100 kHz。

I2C bus scan:
0x18 | FXLS accelerometer (on-board)
0x55 | SI1133 light / UV
0x57 | 24C32 EEPROM (A2A1A0=111)
0x68 | DS3231 RTC
Total 4 I2C device(s) found
SI1133 init OK
SHTC3 init OK
DS3231 init OK

24C32 EEPROM test:
[OK] 24C32 is online (0x57)
[TEST] In-page write @ 0x0000 ...
result: [PASS] in-page OK
[TEST] Cross-page write @ 0x001E ...
result: [PASS] cross-page OK

Entering TUI mode (template 1)...
Button: click=+1 double-click=-1 long-press(>=1s)=next field/save
Edit mode auto-exits after 60 s of inactivity
LED : RUN=1Hz heartbeat EDIT=solid ERROR=5Hz blink

6.2 实时监测界面

进入 TUI 后,界面稳定 1 Hz 刷新,秒针平滑走动;每 3 秒更新一次温湿度、照度、紫外指数。数值右侧的 [#### ] 进度条按量程比例填充(湿度 0~100 %、照度 0~1000 lx、UVI 0~11),无需读数字就能一眼看出趋势。状态栏实时显示四颗器件的在线状态、累计刷新次数与开机时长。

6.3 按键校时操作

单击任意处进入编辑模式,顶栏徽章由绿色 [ LIVE ] 变为黄色 [ EDIT ],红色 LED 转为常亮,小时字段开始以黄色闪烁提示(g_blink_tick 每 20 个主循环 tick 翻转一次,即亮/灭各约 400 ms)。字段切换顺序为 时 → 分 → 年 → 月 → 日,长按(≥1 s)跳到下一字段,走完"日"字段即自动写回 DS3231 并退出编辑。编辑期间传感器采集与秒针显示不会中断(代码里 MODE_RUN 之外只更新 tm_sec,其余字段用编辑中的缓存值),60 秒无操作则自动保存退出,防止用户走开后系统卡在编辑态。




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

难题一:SI1133 上电即读,PART_ID 始终读不到 0x33

现象:SI1133_Init() 反复报 SI1133 not found, id=0x00,但 I²C 总线扫描能扫到 0x55 有应答。

分析:器件在线但读不出正确 ID,说明问题不在物理连接,而在时序。查手册发现 SI1133 明确要求上电后至少 25 ms 才允许访问,而我的初始化紧跟在 BOARD_InitHardware() 之后,间隔远小于这个值。

解决:初始化开头补 Delay_Ms(30),并把读 PART_ID 改成最多 5 次重试、每次间隔 20 ms:

Delay_Ms(30);
for (retry = 0; retry < 5; retry++) {
if (SI1133_ReadReg(handle, SI1133_REG_PART_ID, &part_id) == SI1133_OK) break;
Delay_Ms(20);
}
if (part_id != 0x33U) { /* 报错并置初始化失败标志 */ }

同时给驱动加了一个 s_si1133_init_status 标志位,初始化失败后 SI1133_GetLightAndUV() 会直接返回 −1 并把输出清零,不会再去访问一颗没配好的芯片而阻塞主循环。

难题二:SI1133 参数写入"看似成功",实际没生效

现象:通道参数配完,FORCE_CH 后 IRQ_STATUS 一直是 0x00,四通道从不完成转换。

分析:SI1133 的参数不是普通寄存器,不能直接写。它的机制是:先把参数值写入 HOSTIN0(0x0A),再向 COMMAND(0x0B)写 0x80 | 参数地址 来触发 PARAM_SET。如果只写第一个字节,参数根本不会进芯片。而且写完后必须等 RESPONSE0 里的命令计数器发生变化,才代表命令真正执行完。

解决:实现 set_parameter(),用一次 2 字节连续写同时覆盖 HOSTIN0 与 COMMAND,并加入计数器轮询与超时保护:

buffer[0] = value;
buffer[1] = 0x80U + ((uint8_t)addr & 0x3FU);
retval = SI1133_WriteRegBlock(h, SI1133_REG_HOSTIN0, 2, buffer);
/* 轮询 RESPONSE0 的命令计数器变化,最多 5 次 */

同样地,send_cmd() 在发命令前先用 wait_until_sleep() 确认芯片空闲、记录计数器,发完命令再等计数器跳变,形成完整的"握手"流程。

难题三:串口 TUI 刷屏严重闪烁、边框错位

现象:最初版本每 20 ms 整屏重绘,画面疯狂闪烁,肉眼几乎无法读数;加了颜色后边框更是歪七扭八。

分析:两个独立原因。闪烁来自整屏重绘——即使内容没变也把整屏字节重发一遍。错位来自宽度计算——strlen() 把 \033[91m 这样的 ANSI 转义序列和 UTF-8 多字节字符也算成了可见列宽。

解决:① 引入每行内容缓存 g_line_prev[NUM_ROWS][LB_SIZE],lb_flush() 里先 strcmp 比对,完全相同的行直接跳过不发;② 实现 lb_visible_len(),跳过 ESC 序列与 UTF-8 续字节后再计数;③ 用 \033[%d;1H 绝对定位到行首输出,不依赖光标当前位置;④ 进入 TUI 前隐藏光标、清屏,之后严格禁止任何 PRINTF。改完后画面完全稳定,串口流量也降了一个量级。

难题四:单按键要承载三种语义,单击响应有明显延迟感

现象:为了区分单击和双击,单击必须等 350 ms 窗口结束才响应,用户按下去感觉"没反应"。

分析:这是单键多义的固有代价——不等待就无法判断是不是双击。但可以优化"等待期"的观感,以及避免长按被误判成单击。

解决:① 长按在按住状态下满 1 秒立即触发,不等松手,g_btn_long_fired 标志阻止松手时补发单击;② 编辑字段用 400 ms 黄色闪烁给出明确的视觉反馈,用户知道当前在编辑哪个字段;③ 任何有效操作都置 force_refresh,界面立刻重绘,不等下一个秒跳变;④ 中断里做 30 ms 软件消抖(双边沿都会触发中断,抖动会造成重复计数)。实际使用中,短按的 350 ms 延迟配合闪烁反馈,主观感受是可接受的。

难题五:DS3231 首次上电显示 2000-01-01,误以为驱动有 bug

现象:新板子上电,时间显示为一个明显错误的历史日期。

分析:不是驱动问题。DS3231 出厂或纽扣电池未装时,振荡器从未运行,寄存器里是默认值。状态寄存器的 OSF(Oscillator Stop Flag)位就是专门用来标识这种情况的——只要振荡器停振过,OSF 就被置位,此时读出的时间不可信。

解决:初始化后主动查询 OSF,若置位则用编译器注入的 __DATE__ / __TIME__ 宏解析出固件编译时刻并写入 RTC,随后清除 OSF 标志。这样即便完全断电换电池,也能得到一个接近真实的时间起点,用户再用按键精调即可。

难题六:LED 快闪频率与注释标称值不一致

现象:代码注释写 ERROR = 5 Hz fast blink,实测闪烁明显快于 5 Hz。

分析:on = ((g_led_tick / 2U) & 1U) 是每 2 个 tick 翻转一次,而 tick 周期是 20 ms,所以翻转周期 40 ms、完整闪烁周期 80 ms,实际约 12.5 Hz,而不是注释里写的 5 Hz。这是注释与实现脱节。

解决:确认实际效果(快速闪烁,与 RUN 态的 1 Hz 心跳区分度足够)后保留实现,但把注释里的频率标称值改掉。这件事给我的教训是:凡是写进注释的时序数值,都应该能从 tick 周期直接算出来并被验证,否则注释本身就是下一个 bug 的来源。 若确实需要精确 5 Hz,应改为 (g_led_tick / 5U) & 1U(周期 200 ms)。




八、当前实现的边界与后续扩展

这一章专门说明"还没做完的部分",避免报告夸大项目完成度。

8.1 数据滤波(扩展项 2)⚠️

现状:目前的"数据质量保障"由三层构成——SHTC3 每帧做 CRC-8 校验、校验失败最多重试 3 次、通过后再做量程合理性检查(温度超出 −40~70 ℃、湿度超出 0~100 % 则回退到默认值)。这保证了不会出现明显的坏值,但它不是数字滤波:连续采样之间的随机抖动没有被平滑。

补全方案:在应用层为每个物理量维护一个 8 点环形缓冲,采集后写入并输出算术平均;对温湿度这类慢变量,可进一步用一阶低通 y = y + α(x − y)(α 取 0.2~0.3)替代均值,既省 RAM 又能抑制突变。SI1133 的 lux 建议在多项式换算之后再滤波,避免在原始码域上做非线性运算。

8.2 阈值报警(扩展项 3)⚠️

现状:只有器件级故障告警——DS3231 读取失败时 LED 转快闪、状态栏变红。没有数据级的环境越限报警。

补全方案:定义一组可配置阈值(如温度上限 35 ℃、湿度上限 70 %RH、UVI 上限 6),在每次采集后比较;越限时把对应数据行改为红色高亮,并让 LED 进入区别于 ERROR 态的报警闪烁模式。阈值本身可以存到 AT24C32,配合按键在编辑模式下调整,实现"断电也记住报警设置"。

8.3 数据存储与统计(扩展项 4)⚠️

现状:AT24C32 的驱动已经完整实现并通过开机自检——支持跨页自动拆分的块写、块读、单字节读写、整片填充,页内写和跨页写都已实测回读一致。但主循环里还没有把传感器数据周期性写进去,EEPROM 目前只跑在自检阶段。

补全方案:4 KB 空间按 16 字节一条记录规划(4 字节时间戳 + 2 字节温度定点 + 2 字节湿度定点 + 4 字节照度定点 + 2 字节 UVI 定点 + 2 字节 CRC),可存约 250 条;写入指针与记录计数放在 EEPROM 头部固定区域,采用环形覆盖策略。上位机侧可以用串口命令导出全部记录,做日均值、极值与超标时长统计。DS3231 自带的两个闹钟与 32 kHz 输出,也可以用来做"每 10 分钟自动唤醒记录一次"的低功耗方案。

8.4 其他可继续挖掘的板载能力

FRDM-MCXN236 上还有不少资源本项目没有用到,都是可以继续做的方向:SmartDMA 可以把 I²C 突发读取搬到后台,彻底消除 SI1133 那 200 ms 的阻塞;板载 FXLS 加速度计(I²C 扫描已识别到 0x18)可以加入"倾斜切换显示页"的交互;MCX N236 的超低功耗特性配合 WUU 唤醒,可以把整机做成纽扣电池供电的独立记录仪;EdgeLock 安全区域则可用于给导出的历史数据做签名校验。




九、心得体会

这是我第一次完整接触 NXP 的 MCX 系列和 MCUXpresso 生态。之前在 STM32 上养成的习惯是"直接翻参考手册配寄存器",到了 MCX 上发现完全行不通——这颗芯片的外设命名(LP_FLEXCOMM、LPI2C、SCG、SPC)和时钟结构都是另一套体系。真正让我跑起来的是 MCUXpresso Config Tools:在图形界面里点几下引脚和时钟,pin_mux.c / clock_config.c / peripherals.c 就自动生成好了,配合 SDK 里现成的 fsl_lpi2c 驱动,第一次点亮串口只花了很短时间。这也让我意识到,现代 MCU 开发的重点正在从"会配寄存器"转向"会用工具链 + 会写分层良好的驱动"。

技术上收获最大的两点:

一是"驱动通过函数指针注入 I²C 收发"这个设计。 四颗器件共用一条总线、共用一套底层实现,驱动本身却不依赖任何具体的 I²C 实例。写的时候只觉得干净,等后来调试 SI1133 需要临时把它挪到另一个 LPI2C 通道验证时,才发现只改 main.c 里两行回调绑定就够了,驱动一行没动。这种解耦在真实项目里能省下大量返工。

二是"把显示周期和采集周期解耦"。 一开始我按直觉让两者都是 1 秒,结果 SI1133 那 200 ms 的测量阻塞让秒针明显卡顿。把采集改成 3 秒一次之后,秒针立刻平滑了,而环境量本来就是慢变量,3 秒的间隔对使用者毫无影响。这个教训可以推广:用户感知的流畅度取决于最高频的那个反馈通道,不能让它被低频的重量级操作拖住。

踩坑最多的还是 SI1133。它完全不像一颗"读寄存器就有数据"的普通传感器——参数要通过 HOSTIN0+COMMAND 的两字节连写来注入,命令要靠 RESPONSE0 的命令计数器来握手,测量完成要看 IRQ_STATUS 的位图,原始值还要过一遍官方给的定点多项式。整个过程我基本是照着 Silicon Labs 参考驱动的逻辑一行行理解过来的,中间因为漏了"上电 25 ms"这个要求卡了很久。但也正是这颗芯片让我第一次体会到:读懂一颗器件的数据手册,比会写 I²C 收发代码重要得多。

另外,做串口 TUI 这段经历挺有意思。一开始只是想"能把数据打出来就行",后来发现整屏刷屏根本没法看,才被迫去实现行差分刷新、ANSI 转义跳过、UTF-8 宽度计算这些东西。这些看似"跟嵌入式无关"的字符处理细节,最后反而成了整个项目里最影响使用体验的部分。也让我重新认识到:嵌入式项目的评价标准不只是功能是否跑通,还包括人机交互的实际观感。

最后要说的是,本项目没有刻意追求把扩展加分项全部堆满。滤波、阈值报警、数据存储这三项我确实还没做完,与其写一段没实测过的代码糊弄过去,不如在报告里如实标注现状与补全方案。AT24C32 的驱动我已经写完并通过了跨页读写自检,剩下的接入工作是明确的、可验证的。我倾向于交一份"每一行都能演示"的项目,而不是一份"看起来功能很多但经不起追问"的项目。

感谢硬禾科技与 DigiKey 联合推出的 Funpack 系列活动。"玩成功就全额退"这个机制其实是在逼人把项目真正做通——因为交不出能演示的东西就拿不到返还,这种约束比任何教程都有效。下期活动再见!




十、参考资料

  • FRDM-MCXN236 开发板官方页面:https://www.nxp.com.cn/FRDM-MCXN236
  • FRDM-MCXN236 Board User Manual(NXP)
  • MCX N23x 数据手册与参考手册(NXP Semiconductors)
  • Funpack 第五季第四期活动页:https://www.eetree.cn/page/activity/funpack-5-4
  • FRDM-MCXN236 板卡详情(电子森林):https://www.eetree.cn/project/5282
  • MCUXpresso IDE / SDK / Config Tools 用户指南(NXP)
  • Silicon Labs Si1133 数据手册与 UV Index / Ambient Light 应用笔记(多项式系数来源)
  • Sensirion SHTC3 数据手册(命令集、CRC-8 算法、物理量换算公式)
  • Maxim DS3231 数据手册(BCD 时间寄存器、OSF 标志、温补特性)
  • Atmel AT24C32 数据手册(32 字节页写、写周期时序)





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