项目简介
这次使用 NXP FRDM-MCXN236 开发板、Sensirion SEN66 环境传感器和一块 1.8 英寸 ST7735 LCD,制作了一台桌面空气质量监测仪。
设备可以同时读取并显示:
- PM1.0、PM2.5、PM4.0、PM10
- 相对湿度和温度
- VOC 指数和 NOx 指数
- CO2 浓度
LCD 包含两个界面:
- 总览界面:显示运行计时和九项实时测量值;
- 曲线界面:显示最近 106 个有效采样点的温度、湿度变化。
按下开发板上的 SW2 可以在两个界面之间切换。全部测量值还会通过板载虚拟串口输出,便于记录和后续分析。
硬件组成与连线
NXP FRDM-MCXN236开发板
FRDM-MCXN236是恩智浦(NXP)推出的一款紧凑型、可扩展的高性价比开发板。该开发板的核心是一颗 MCX N236 微控制器,搭载 Arm Cortex-M33 内核,主频最高可达 150 MHz,能够高效地将工作负载分配到模拟与数字外设上。其接口分配如下。

Sensirion SEN66 环境传感器
这是 Sensirion 的一款环境传感器,可以检测 PH 值、温湿度、氮氧化合物、CO2 等信息

其控制方式采用的 IIC,接口引脚定义如数据手册中所示

因此其与开发板的连接方式如下表所示
SEN66 端子 | FRDM-MCXN236 | MCXN236 功能 | 说明 |
|---|---|---|---|
VDD | 3V3 | — | — |
GND | GND | — | 必须与开发板和 LCD 共地 |
SDA | P1_16 | LPI2C5 / FC5_P0 | I2C 数据线 |
SCL | P1_17 | LPI2C5 / FC5_P1 | I2C 时钟线 |
1.8 寸 LCD 屏幕
该 1.8寸 LCD 屏幕使用的驱动芯片为 ST7735S,采用的通信方式为 SPI,由于 ST7735S 只接收显示数据,因此本项目不使用 MISO。LCD 驱动采用 RGB565 色彩格式,设置 LPSPI3 时钟为 12 MHz。LCD 屏幕与开发板连接方式如下表所示。
ST7735 端子 | FRDM-MCXN236 | 软件定义 | 作用 |
|---|---|---|---|
VCC | 3V3 | — | LCD 逻辑与显示供电 |
GND | GND | — | 公共地 |
SCL | P1_0 | LPSPI3 时钟 | SPI 时钟 |
SDA | P1_1 | LPSPI3 数据输出 | MCU 向 LCD 发送数据 |
CS | P1_3 | GPIO1_3 | 片选,低电平有效 |
RST | P4_13 | GPIO4_13 | LCD 硬件复位 |
DC | P5_7 | GPIO5_7 | 命令/数据选择 |
BLK | 背光控制端 | — | 背光,默认连接3V3,因此不接引脚 |
串口与按键
串口用于记录当前获取到的环境参数,串口参数设置为 115200 8N1,电脑连接开发板 USB 口之后,可以通过虚拟串口查看数据;按键用于实现页面切换
功能 | 引脚/通道 | 说明 |
|---|---|---|
调试串口 | LPUART4,P1_8/P1_9 | FRDM 板载调试器已连接到 USB 虚拟串口,通常无需再接 USB-UART |
页面切换 | SW2,P0_20 | 板载按键,内部上拉,按下时为低电平 |
最终硬件连接如下图所示

系统结构
硬件连接与数据流
连接方式和数据传递如下图所示

软件启动流程

主循环与页面切换

SEN66 通信原理
I2C 地址与命令
SEN66 的 7 位 I2C 地址为 0x6B,本项目使用 100kbit/s 标准模式。
程序使用的主要命令如下:
命令 | 数值 | 作用 |
|---|---|---|
Start Measurement |
| 启动连续测量 |
Data Ready |
| 查询新数据是否就绪 |
Read Measurements |
| 读取全部测量结果 |
发送命令时先发送高字节,再发送低字节:
static status_t SEN66_WriteCommand(uint16_t command)
{
uint8_t commandBytes[2] = {
(uint8_t)(command >> 8U),
(uint8_t)command
};
return SEN66_Transfer(kLPI2C_Write,
commandBytes,
sizeof(commandBytes));
}
返回数据与 CRC-8
一次完整测量返回 27 字节,对应 9 个测量量。SEN66 每两个数据字节附带一个 CRC:
MSB | LSB | CRC | MSB | LSB | CRC | ...
CRC-8 的初始值为 0xFF,多项式为 0x31。驱动逐个检查 3 字节数据组,任何一组校验失败都会放弃本次数据:
static uint8_t SEN66_CalculateCrc(const uint8_t *data)
{
uint8_t crc = 0xFFU;
for (uint8_t byte = 0U; byte < 2U; byte++)
{
crc ^= data[byte];
for (uint8_t bit = 0U; bit < 8U; bit++)
{
crc = (crc & 0x80U) != 0U ?
(uint8_t)((crc << 1U) ^ 0x31U) :
(uint8_t)(crc << 1U);
}
}
return crc;
}
数据解析与单位换算
驱动把每个大端序数据字还原为 16 位整数:
static uint16_t SEN66_GetWord(const uint8_t *data,
uint32_t wordIndex)
{
uint32_t offset = wordIndex * 3U;
return (uint16_t)(((uint16_t)data[offset] << 8U) |
data[offset + 1U]);
}
原始值的换算关系为:
数据 | 换算方法 | 显示精度 |
|---|---|---|
PM1.0/2.5/4.0/10 | 原始值 ÷ 10 | 1 位小数,µg/m³ |
湿度 | 原始值 ÷ 100 | 2 位小数,%RH |
温度 | 原始值 ÷ 200 | 2 位小数,°C |
VOC/NOx 指数 | 原始值 ÷ 10 | 1 位小数 |
CO2 | 不缩放 | ppm |
代码没有使用浮点格式化,而是通过整数除法和余数生成小数文本。这样可以减小嵌入式程序体积,也避免引入浮点 printf。
传感器用 0xFFFF 或 0x7FFF 表示未知值,界面与串口统一输出 ---,避免把无效值误认为真实测量结果。
I2C 总线恢复与故障诊断
I2C 从设备如果在 MCU 复位期间仍处于发送状态,可能持续拉低 SDA,导致控制器一直认为总线忙。
初始化 LPI2C5 之前,程序暂时把 SDA/SCL 切换为 GPIO:
- 释放 SDA 和 SCL;
- 当 SDA 为低时,最多输出 9 个 SCL 脉冲;
- 生成一次 STOP 条件;
- 恢复 FC5 引脚复用并初始化 LPI2C5。
for (uint32_t pulse = 0U;
pulse < 9U && GPIO_PinRead(GPIO1, SEN66_SDA_PIN) == 0U;
pulse++)
{
SEN66_DrivePinLow(SEN66_SCL_PIN);
SEN66_BitDelay();
SEN66_ReleasePin(SEN66_SCL_PIN);
SEN66_BitDelay();
}
启动失败时,程序还会扫描 0x08 到 0x77,并通过 GPIO 模拟地址阶段再次检查 0x6B 的 ACK。
ST7735 显示原理与刷新优化
基本写屏流程
ST7735 使用 RGB565,每个像素占 2 字节。写入矩形区域时依次发送:
- CASET (0x2A):设置列范围;
- RASET (0x2B):设置行范围;
- RAMWR (0x2C):写入像素数据。
字符块传输
字符串传输使用的方法是先在 RAM 中生成一个字符的 RGB565 像素块,再一次性发送:
ST7735_SetAddressWindow(x, y, width, height);
ST7735_WriteData(pixels, pixelIndex);
这样每个字符由多次小传输变为一次块传输,界面刷新速度明显提升。
只刷新数据区域
如果每秒调用 FillScreen() 再重画所有内容,人眼会看到明显闪烁。当前策略是:
- 切换页面时才绘制整页静态框架;
- 总览页的标签只绘制一次;
- 时间和测量值使用等宽背景字符直接覆盖原位置;
- 曲线页每次只绘制“上一个点到最新点”的两条线段;
- 106 点满后只清除曲线内部,不清除标题和坐标轴。
固定宽度数值区域的实现如下:
static void DrawValue(uint16_t y, const char *value)
{
char paddedValue[14];
snprintf(paddedValue, sizeof(paddedValue), "%-13s", value);
ST7735_DrawString(44U, y, paddedValue,
ST7735_COLOR_WHITE,
ST7735_COLOR_BLACK,
1U);
}
双页面与温湿度曲线
SW2 消抖
SW2 接在 P0_20,生成的 pin mux 已将其配置为上拉输入,因此按下时读到低电平。
程序每 100 ms 轮询一次,并要求电平稳定 30 ms 才确认状态变化。函数只在“稳定松开 → 稳定按下”时返回一次事件,长按不会连续切页。
if ((rawPressed != stablePressed) &&
((now - changedAt) >= 30U))
{
stablePressed = rawPressed;
return stablePressed;
}
历史数据缓存
程序使用两个 106 点数组保存原始温度和湿度:
static int16_t g_temperatureHistory[106];
static int16_t g_humidityHistory[106];
坐标映射
曲线区域温度范围设置为 -10~50 °C,湿度范围设置为 0~100 %RH。
屏幕 Y 坐标向下增大,因此换算公式需要反向:
screenY = bottom -
(value - minimum) * (bottom - top) /
(maximum - minimum);
温度使用绿色,湿度使用青色。相邻采样点通过 Bresenham 直线算法连接,避免浮点计算。
运行计时与主循环
顶部 HH:MM:SS 是设备本次上电后的运行计时,模拟 RTC 的功能。
SysTick 每 1 ms 进入一次中断:
void SysTick_Handler(void)
{
g_rtcMilliseconds++;
}
显示时将毫秒数换算为时、分、秒。只有秒值变化时才重画时间字符。
主循环的逻辑为:
检查 SW2 并切换页面
↓
查询 SEN66 Data Ready
↓
读取 27 字节并校验 CRC
↓
换算并更新当前页面
↓
通过 UART 输出格式化数值
实现效果
数据显示和运行时间显示界面

温湿度变化曲线界面

心得体会
本次活动让我对 NXP 系列的 MCU 的开发有了更深的理解,包括工具配置、SDK的查看和调用,进一步熟练了对类似项目的开发流程。