【Funpack5-4】基于MCX N236的温湿度数据采集和显示
该项目使用了FRDM-MCXN236,实现了温湿度数据采集和显示的设计,它的主要功能为:温湿度采集、OLED显示。
标签
嵌入式系统
Funpack活动
MCU
I2C
RTOS
码匠许师傅
更新2026-10-19
10


【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,设备名 i2c2

读取 MikroBUS 接口上的 SHT3x 传感器

FlexCOMM0(I2C)

P0_16/P0_17 复用为 FC0 I2C,设备名 i2c0

驱动 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 系统节拍

线程调度和 rt_thread_mdelay 延时

简单来说,两个模块各挂在一条独立的 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

i2c2

0x44

0.96 寸 OLED 模块

J4 扩展排针(7/9 脚信号,1/3 脚供电)

P0_16

P0_17

3V3 / GND

i2c0

0x3C

开发环境:

软件基于RT-Thread系统,以及sht3x、ssd1306软件包。

开发环境RT-Thread Env工具,使用scons命令进行构建、menuconfig命令进行图形化配置、pkgs命令进行软件包下载。

使用 pyocd命令实现将RT-Thread固件下载到开发板。

本项目实际使用的工具链和软件版本:

项目

版本/说明

RT-Thread

5.3.1(主线 BSP bsp/nxp/mcx/mcxn/frdm-mcxn236 裁剪而来)

编译工具链

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 完成两方面的配置:

  1. 片上外设配置
  • 打开I2C、I2C0、I2C2配置
  1. 软件包配置
  • 勾选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 挂在 i2c2(FlexCOMM2,P4_0/P4_1),地址 0x44,单次测量模式读取

周期性采集环境信息

采集线程每 500 ms 采样一次,周期误差用毫秒时戳扣除,读数异常自动重试

串口输出采集结果

MCU-Link 虚拟串口 115200 输出启动信息和采集状态日志(读取状态变化时打印 ok/failed),并支持 ps、free、i2c_scan 等 MSH 命令交互

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 mcxn236

3.如何用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 做统计分析。


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