【Funpack5-4】基于MCX N236的温湿度数据采集和显示
一、项目简介
本项目是Funpack5-4活动项目之一,主要完成了活动任务2:环境数据采集与显示的功能。题目要求如下:
任务2:环境数据采集与显示【难度:⭐️⭐️】
>
基础要求:
>
1. 通过 I2C 读取传感器数据
2. 周期性采集环境信息
3. 串口输出采集结果
4. LED 指示运行状态
5. 数据刷新周期不大于 1 秒
>
可选加分项:
>
* 支持多种传感器接入
* 实现数据滤波
* 阈值报警
* 数据存储与统计分析
本项目基于 FRDM-MCXN236 开发板,使用 SHT3x 温湿度传感器采集环境温湿度,用 0.96 寸 OLED 屏(SSD1306)实时显示,板上红色 LED 指示测量状态,串口输出运行日志。软件上没有从零造轮子,而是基于 RT-Thread 实时操作系统(5.3.1 版本)和官方软件包(sht3x、ssd1306)搭建:用两个线程分别负责传感器采集和结果显示,中间用一个消息队列传递数据,实现"采集"与"显示"的解耦。整个实现过程覆盖了 RT-Thread Env 环境搭建、BSP 裁剪、menuconfig 图形化配置、软件包下载、scons 编译、pyOCD 下载固件和实际调试验证等环节。
二、硬件简介
FRDM-MCXN236开发板:
FRDM-MCXN236 是 NXP 推出的低成本评估与开发板,主控为 MCX N236 微控制器(MCXN236VDF),基于 Arm Cortex-M33 内核,主频最高 150 MHz,片内集成 1 MB 双区 Flash 和 352 KB ECC RAM,内置 EdgeLock 安全区域和 SmartDMA 加速器。板卡集成了 MCU-Link 调试器(支持 SWD 调试和 USB 虚拟串口)、RGB LED、加速度计、DMIC 传感器、CAN 收发器和外部 W25Q64 Flash,并引出 Arduino、mikroBUS、FRDM、Pmod、FlexIO/LCD、SmartDMA/摄像头等丰富的扩展接口。
除开发板外,本项目使用了两个外接模块:
- SHT3x 温湿度传感器模块:Sensirion 的 I2C 数字温湿度传感器,典型精度 ±0.2 °C / ±2 %RH。模块的 ADDR 引脚默认下拉,I2C 地址为 0x44。
- 0.96 寸 OLED 显示模块:SSD1306 驱动,128×64 分辨率,I2C 接口,地址 0x3C。
两个模块都接 3.3 V 供电,和 MCU 引脚电平直接兼容,不需要电平转换。
使用到的硬件资源及连接关系如下:
硬件资源 | 连接关系 | 项目中的作用 |
|---|---|---|
SHT3x 温湿度模块 | MikroBUS 扩展接口的 I2C 信号,P4_0=SDA、P4_1=SCL,3V3/GND 供电 | 采集环境温度和相对湿度 |
0.96 寸 OLED 模块 | J4 扩展排针,第 7 脚 P0_16=SDA、第 9 脚 P0_17=SCL,第 1 脚 3V3、第 3 脚 GND | 实时显示温湿度数值 |
RGB LED(D13) | 红色接 MCU 的 P4_18 引脚(低电平点亮) | 点亮表示正在测量传感器数据 |
USB 调试下载接口 | 连接 PC 与开发板(板载 MCU-Link) | 用于 pyOCD 下载固件、串口日志输出和供电 |
MCXN236VDF MCU | 开发板主控芯片 | 负责 I2C 通信、数据处理和显示控制 |
对应的 MCU 片上外设如下:
MCU 片上外设 | 使用方式 | 说明 |
|---|---|---|
FlexCOMM2(I2C) | P4_0/P4_1 复用为 FC2 I2C,设备名 | 读取 MikroBUS 接口上的 SHT3x 传感器 |
FlexCOMM0(I2C) | P0_16/P0_17 复用为 FC0 I2C,设备名 | 驱动 J4 排针上的 SSD1306 OLED |
GPIO | P4_18/P4_19/P4_17 作为普通输出控制 RGB LED | 红色 LED 指示测量窗口,低电平点亮 |
LPUART4 | 接板载 MCU-Link 虚拟串口,115200-8-N-1 | RT-Thread 控制台,输出运行日志 |
SysTick | RT-Thread 系统节拍 | 线程调度和 |
简单来说,两个模块各挂在一条独立的 I2C 总线上:SHT3x 使用 MikroBUS 接口的 I2C(FlexCOMM2 → i2c2),OLED 使用 J4 排针引出的 I2C(FlexCOMM0 → i2c0)。这样接线最简单,两条总线互不干扰,也不用考虑两个设备并联时的地址冲突和总线负载问题。
三、方案设计
用SHT3x温湿度传感器实现温湿度采集,OLED屏实现温湿度显示。
系统整体结构如下:采集线程作为生产者,每 500 ms 通过 i2c2 读一次 SHT3x,把结果打包成消息发进队列;显示线程作为消费者,阻塞等待队列,收到消息后通过 i2c0 刷新 OLED;主线程只负责初始化和创建队列、线程。红色 LED 在测量窗口点亮用于指示运行状态,日志通过 MCU-Link 虚拟串口输出。

硬件连接:
SHT3x传感器、OLED显示屏和主控MCU的连接关系如下:
- SHT3x传感器:MikroBUS扩展接口,参考QSG文档,P4_0是SDA、P4_1是SCL(FlexCOMM2)。
- OLED显示屏:J4扩展接口,参考QSG文档,P0_16是SDA、P0_17是SCL(FlexCOMM0)。
汇总成表格:
模块 | 开发板接口 | SDA | SCL | 供电 | RT-Thread 设备名 | I2C 地址 |
|---|---|---|---|---|---|---|
SHT3x 温湿度模块 | MikroBUS 扩展接口 | P4_0 | P4_1 | 3V3 / GND | | 0x44 |
0.96 寸 OLED 模块 | J4 扩展排针(7/9 脚信号,1/3 脚供电) | P0_16 | P0_17 | 3V3 / GND | | 0x3C |
开发环境:
软件基于RT-Thread系统,以及sht3x、ssd1306软件包。
开发环境RT-Thread Env工具,使用scons命令进行构建、menuconfig命令进行图形化配置、pkgs命令进行软件包下载。
使用 pyocd命令实现将RT-Thread固件下载到开发板。
本项目实际使用的工具链和软件版本:
项目 | 版本/说明 |
|---|---|
RT-Thread | 5.3.1(主线 BSP |
编译工具链 | arm-none-eabi-gcc(Env 自带环境) |
软件包 | sht3x v1.0.0、ssd1306(latest)、NXP MCX CMSIS/系列驱动包 |
固件下载 | pyOCD + 板载 MCU-Link(目标芯片 pack:MCXN236VDF) |
代码管理 | 项目已独立建仓(rtt-n236),用 git 管理 |
四、软件设计
使用两个线程分别实现传感器数据采集,以及结果显示工作,使用一个队列传递传感器结果数据。主线程中创建采集线程、显示线程,以及结果队列。
这是一个典型的"生产者-消费者"模型:采集线程只管按固定周期采样并把结果发进队列,显示线程只管从队列取数据刷屏,两边通过消息队列解耦。即使某次 I2C 读取变慢或者失败,也只是队列里多一条用 NAN 填充的失败结果,两个线程的节奏互不影响。

配置要点
在 Env 环境中用 menuconfig 完成两方面的配置:
- 片上外设配置
- 打开I2C、I2C0、I2C2配置
- 软件包配置
- 勾选sht3x软件包
- 勾选ssd1306软件包,配置总线i2c0、地址0x3C
对应的rtconfig.h变更内容为:
diff --git a/rtconfig.h b/rtconfig.h
index 3d73724..e9b14e4 100644
--- a/rtconfig.h
+++ b/rtconfig.h
@@ -171,6 +171,8 @@
#define RT_CANSND_MSG_TIMEOUT 100
#define RT_CAN_NB_TX_FIFO_SIZE 256
#define RT_USING_CLOCK_TIME
+#define RT_USING_I2C
+#define RT_USING_I2C_BITOPS
#define RT_USING_WDT
#define RT_USING_PIN
/* end of Device Drivers */
@@ -363,11 +365,19 @@
/* sensors drivers */
+#define PKG_USING_SHT3X
+#define PKG_USING_SHT3X_V100
/* end of sensors drivers */
/* touch drivers */
/* end of touch drivers */
+#define PKG_USING_SSD1306
+#define PKG_USING_SSD1306_DEBUG
+#define PKG_USING_SSD1306_I2C_ADDRESS 0x3C
+#define PKG_USING_SSD1306_I2C_BUS_NAME "i2c0"
+#define PKG_USING_SSD1306_SAMPLE
+#define PKG_USING_SSD1306_LATEST_VERSION
/* end of peripheral libraries and drivers */
/* AI packages */
@@ -451,7 +461,10 @@
#define BSP_USING_UART
#define BSP_USING_UART4
#define BSP_USING_UART5
-#define BSP_USING_UART2
+#define BSP_USING_I2C
+#define BSP_USING_I2C0
+#define BSP_USING_I2C1
+#define BSP_USING_I2C2
#define BSP_USING_WDT
#define BSP_USING_CLOCK_TIMER
/* end of On-chip Peripheral Drivers */
配置完成后用 pkgs --update 下载软件包,scons 编译,scons -j8 可以加速。
主线程:创建队列和线程
main() 中先初始化 RGB LED 和按键,然后按"先建队列、再建两端线程"的顺序创建 IPC 对象和工作线程:
int main(void)
{
/* RGB 三色全部熄灭;红灯只在传感器测量窗口点亮 */
rt_pin_mode(LED_RED, PIN_MODE_OUTPUT);
rt_pin_mode(LED_GREEN, PIN_MODE_OUTPUT);
rt_pin_mode(LED_BLUE, PIN_MODE_OUTPUT);
rt_pin_write(LED_RED, LED_OFF);
rt_pin_write(LED_GREEN, LED_OFF);
rt_pin_write(LED_BLUE, LED_OFF);
#if defined(PKG_USING_SSD1306) && defined(PKG_USING_SHT3X)
/* 先建队列,再启动队列两端的线程 */
sensor_mq = rt_mq_create("senmq", sizeof(struct sensor_msg),
SENSOR_MQ_LEN, RT_IPC_FLAG_FIFO);
if (sensor_mq != RT_NULL)
{
rt_thread_t meas = rt_thread_create("sens", temp_meas_entry, RT_NULL,
MEAS_THREAD_STACK_SIZE,
MEAS_THREAD_PRIORITY, 5);
rt_thread_t disp = rt_thread_create("disp", oled_disp_entry, RT_NULL,
DISP_THREAD_STACK_SIZE,
DISP_THREAD_PRIORITY, 5);
if (meas != RT_NULL) rt_thread_startup(meas);
if (disp != RT_NULL) rt_thread_startup(disp);
}
#endif
/* 采集和显示都在各自的线程里运行,main 没有剩余工作 */
return 0;
}
线程之间传递的数据就是一个小结构体,读取失败时两个字段都填 NAN,显示线程用 isnan() 判断这一轮是否有效:
/* 采集线程交给显示线程的一次测量结果;
* 读取失败时用 NAN 标记两个字段 */
struct sensor_msg
{
float temp;
float humi;
};
采集线程:500 ms 周期采样
采集线程的核心逻辑是:初始化传感器,然后按固定节拍采样。每轮采样用红色 LED 点亮来标记测量窗口(对应任务要求的"LED 指示运行状态"),并用 rt_tick_get_millisecond() 记录起始时刻,扣除本轮耗时后再延时,保证采样周期稳定在 500 ms:
static void temp_meas_entry(void *parameter)
{
sht = sht3x_init(SHT3X_I2C_BUS, SHT3X_I2C_ADDR); /* "i2c2", 0x44 */
if (sht == RT_NULL) { /* 初始化失败:发一条 NAN 消息告知显示线程后退出 */ }
sht3x_softreset(sht);
rt_thread_mdelay(2); /* 软复位时间典型 1.5 ms */
while (1)
{
rt_bool_t ok;
start_ms = rt_tick_get_millisecond();
rt_pin_write(LED_RED, LED_ON); /* 红灯标记测量窗口 */
ok = sht3x_read_valid(sht);
rt_pin_write(LED_RED, LED_OFF);
if (ok)
{
msg.temp = sht->temperature;
msg.humi = sht->humidity;
}
else
{
msg.temp = NAN; /* NAN 标记本轮读取失败 */
msg.humi = NAN;
}
if (rt_mq_send(sensor_mq, (const void *)&msg, sizeof(msg)) == -RT_EFULL)
{
/* 队列满:丢掉最旧的一帧,最新的数据最重要 */
rt_mq_recv(sensor_mq, &oldest, sizeof(oldest), RT_WAITING_NO);
rt_mq_send(sensor_mq, (const void *)&msg, sizeof(msg));
}
/* 扣除本轮耗时,保持稳定的采样周期 */
elapsed = rt_tick_get_millisecond() - start_ms;
if (elapsed < MEAS_PERIOD_MS)
{
rt_thread_mdelay(MEAS_PERIOD_MS - elapsed);
}
}
}
其中 sht3x_read_valid() 是在软件包 API 之上加的一层保护:调试中发现 i2c2 偶尔会读到全零的原始数据,SHT3x 驱动会把它换算成 T = -45.0 °C / RH = 0 %。这里就以这两个值为"无效读数"的特征,最多重试 3 次:
static rt_bool_t sht3x_read_valid(sht3x_device_t dev)
{
rt_uint8_t retry;
for (retry = 0; retry < SHT3X_READ_RETRY; retry++)
{
if (sht3x_read_singleshot(dev) == RT_EOK &&
dev->humidity > 0.0f && dev->temperature > -45.0f)
{
return RT_TRUE;
}
rt_thread_mdelay(20);
}
return RT_FALSE;
}
串口日志也做了降噪处理:不是每个周期都打印,只在读取状态发生变化(ok 变 failed 或反过来)时输出一行 sens: sht3x read ok/failed,正常稳态运行时串口保持安静。
显示线程:阻塞收数据、按本轮结果刷新
显示线程 rt_mq_recv() 永久阻塞等队列,收到消息后刷屏。has_data 直接由本轮消息是否为 NAN 决定:读取成功就显示本轮的温湿度;读取失败(包括初始化失败)则显示 --.- 占位,明确提示当前没有有效数据:
static void oled_disp_entry(void *parameter)
{
struct sensor_msg msg;
float temp = 0.0f, humi = 0.0f;
rt_bool_t has_data = RT_FALSE;
ssd1306_Init();
while (1)
{
rt_mq_recv(sensor_mq, &msg, sizeof(msg), RT_WAITING_FOREVER);
has_data = !isnan(msg.temp); /* NAN 表示本轮读取失败 */
if (has_data)
{
temp = msg.temp;
humi = msg.humi;
}
oled_show(temp, humi, has_data);
}
}
oled_show() 负责 OLED 排版:标题行 "Temp/Humid" 加一条分隔线,温度、湿度各占一行大字体(Font_11x18)。浮点数没有用 %f 打印,而是乘 10 拆成整数部分和小数部分用 %d.%d 拼出来,避免在 MCU 上引入浮点格式化开销:
t = (int)(temp * 10);
h = (int)(humi * 10);
rt_sprintf(line, "T: %s%d.%d C", (t < 0) ? "-" : "", abs(t) / 10, abs(t) % 10);
ssd1306_SetCursor(0, 17);
ssd1306_WriteString(line, Font_11x18, White);
rt_sprintf(line, "H: %d.%d %%", abs(h) / 10, abs(h) % 10);
ssd1306_SetCursor(0, 40);
ssd1306_WriteString(line, Font_11x18, White);
调试辅助命令
应用层还保留了一个 i2c_scan MSH 命令,用来扫描任意 I2C 总线上的设备地址(范围 0x08–0x77),接线排查时非常有用:
msh> i2c_scan i2c2
scanning i2c2 ...
0x18 ack # 板载加速度计 FXLS8974
0x44 ack # SHT3x 温湿度传感器
msh> i2c_scan i2c0
scanning i2c0 ...
0x3C ack # SSD1306 OLED
五、功能展示
最终实现的功能与任务 2 基础要求的对照:
任务要求 | 实现情况 |
|---|---|
通过 I2C 读取传感器数据 | SHT3x 挂在 |
周期性采集环境信息 | 采集线程每 500 ms 采样一次,周期误差用毫秒时戳扣除,读数异常自动重试 |
串口输出采集结果 | MCU-Link 虚拟串口 115200 输出启动信息和采集状态日志(读取状态变化时打印 ok/failed),并支持 |
LED 指示运行状态 | 红色 LED(P4_18)在每个测量窗口点亮、测量结束熄灭,肉眼可见约 0.5 s 的心跳节律 |
数据刷新周期不大于 1 秒 | 采集和显示周期均为 500 ms,满足要求 |
实际运行现象:
- 上电后 OLED 显示 "Temp/Humid" 标题栏和分隔线,随后温湿度数值每 0.5 秒刷新一次。
- 传感器读取失败(例如拔掉 SHT3x 模块)时,OLED 显示
--.-占位,串口打印一次sens: sht3x read failed;重新接好后自动恢复显示实际数值。 - 红色 LED 随每个测量窗口规律闪烁,直观指示系统正在周期性采集。
效果展示:

六、问题解决
1. RT-Thread主线代码有很多其他厂商的代码,怎么精简?
RT-Thread主线代码支持通过scons命令创建项目目录,项目目录中只包含一个具体板卡的bsp和一个芯片厂商的驱动代码,可以大幅精简代码。
具体操作是,在RT-Thread Env环境,使用scons命令:
scons --dist --target=vsc --project-name=rtt-n236 --project-path=e:/DIY/NXP/FRDM-N236/rtt-n236/
其中,几个关键命令行参数的作用是:
- project-name用于指定项目名称;
- project-path 用于指定项目目录,生成的代码会位于这里;
- target用于指定目标项目的类型,目前支持这些:mdk/mdk4/mdk5/iar/vs/vsc/ua/cdk/ses/makefile/eclipse/codelite/cmake/vsc_workspace
执行效果截图:

2. pyocd如何支持MCX N236?
查找支持包:
pyocd pack find mcxn236安装支持包:
pyocd pack install mcxn2363.如何用pyocd下载rtthread.bin到FRDM-MCXN236开发板?
pyocd -t MCXN236VDF -f 2000000 rtthread.bin
实际项目中把下载命令写成了 download.sh 脚本,直接下载 elf 文件方便调试对齐符号:
pyocd load -t MCXN236VDF -f 8000000 rtthread.elf
4. 怎么确认 I2C 接线和地址没有问题?
问题描述:两个模块分属两条总线,接线完成后需要先确认每条总线上设备是否应答、地址是否正确,再谈驱动。
解决方法:在应用层写了一个 i2c_scan MSH 命令,对指定总线逐地址发起 1 字节读传输,应答即认为设备存在。扫描 i2c2 能看到板载加速度计 FXLS8974(0x18)和 SHT3x(0x44),扫描 i2c0 能看到 OLED(0x3C),与 QSG 文档和模块资料完全对上。
5. SHT3x 偶尔读到全零数据怎么办?
问题描述:调试中发现在 i2c2 上偶尔读到全零原始数据,驱动会换算成 T = -45.0 °C / RH = 0 % 并返回成功,屏幕上偶尔闪现异常值。
解决方法:在应用层加 sht3x_read_valid() 判断——读取成功且湿度大于 0、温度大于 -45 才认为有效,否则延时 20 ms 重试,最多 3 次;连续失败则通过消息队列发一条 NAN 消息,显示线程据此在屏幕上显示 --.- 占位。
6. ssd1306 软件包怎么知道挂在哪条总线?
问题描述:ssd1306 软件包默认的 I2C 总线名和地址不一定和自己的接线一致。
解决方法:在 menuconfig 中配置 PKG_USING_SSD1306_I2C_BUS_NAME 为 i2c0、地址为 0x3C,与 J4 排针的 FC0 实际接线保持一致;SHT3x 软件包则在 sht3x_init() 时直接传入总线名 i2c2 和地址 0x44。
7. MCU 上怎么显示浮点数?
问题描述:温度、湿度是 float,直接用 %f 格式化会引入浮点 printf 支持,增加代码体积。
解决方法:把数值乘 10 取整,用 %d.%d 分别拼出整数和小数部分,负温单独处理符号位,OLED 和串口日志统一使用这个技巧。
8. 消息队列满了怎么处理?
问题描述:如果显示线程被阻塞(比如 OLED 刷新变慢),队列可能被填满,采集线程发送失败。
解决方法:发送返回 -RT_EFULL 时,先用非阻塞方式取出并丢弃最旧一条,再重新发送最新数据,保证队列里永远是最近的数据。
9. 使能 i2c2 后总线还是不通,P4_0/P4_1 去哪了?
问题描述:menuconfig 里打开了 I2C,但 MikroBUS 上的 SHT3x 一直无应答。排查发现 BSP 默认把 FlexCOMM2 配成了 UART2,P4_0/P4_1 被 UART2 占用,FC2 根本没有工作在 I2C 方式。
解决方法:在 menuconfig 中关闭 UART2、使能 I2C2;同时在 board/Kconfig 里给 BSP 补充了 BSP_USING_I2C2 选项(depends on !BSP_USING_UART2),从配置层面避免两个功能同时打开。
七、心得体会
这次项目的软件基于 RT-Thread 系统构建,和 Funpack5-2 的裸机开发完全不同。一方面,体会到了RTOS和纯裸机软件结构的显著差异。另一方面,感受到了RT-Thread软件包生态带来的开发便利。
裸机开发时,所有逻辑挤在一个大 while 循环里,节奏不同的任务只能靠状态机硬拆;RTOS 上采集和显示是两个独立线程,中间用消息队列衔接,各自逻辑变简单,改一边也不牵动另一边。
sht3x 和 ssd1306 两个软件包把传感器换算、OLED 绘图都封装好了,应用层只需关注"怎么读、怎么显示",几行代码就能把功能组合起来。当然,总线名、地址要和接线配置一致,读数也要做有效性判断,这些都是很小的代价。
整体来看,这个项目把多线程、消息队列、I2C 和软件包开发串了一遍,是一次比较完整的练习。后续可以加滑动平均滤波、阈值报警,或把历史数据存进板载 W25Q64 Flash 做统计分析。