【eDIY】蓝牙低功耗温湿度计 02 过程贴:温湿度信息采集任务说明
SHT30 传感器驱动开发 • 完成 NXP FRDM-MCXW71 开发板与 SHT30 传感器的 I2C 物理连接 • 实现温度与湿度数据读取函数, 在串口终端打印原始数据与解析后的温湿度值,格式为:Temperature: 23.5°C, Humidity: 45.2%RH
我没有采购这类传感器,而使用盛思锐出品的I2C接口的SEN66传感器,它支持9种数据,是目前市面上集成度最高的空气质量监测模组之一。
SEN66 技术特点
高度集成:9 种传感器功能集成在一个模组中,无需多个独立传感器,节省 PCB 空间和 BOM 成本
I²C 接口:通过 I²C 总线通信,与 MCU 连接简单
低功耗:适合电池供电的便携式设备
高精度:采用 Sensirion 成熟的 SHT 温湿度传感技术和激光散射颗粒物检测技术
算法内置:VOC 和 NOx 指数通过内置算法从原始气体传感器数据计算得出
SEN66 典型应用场景
室内空气质量监测仪
智能家居环境监控
工业环境安全监测
物联网(IoT)环境监测终端
楼宇自动化系统
任务实现
此任务规划如下:
SEN66 通过I2C驱动,采集9种传感器数据并通过串口打印
OLED 显示屏初始化,显示温湿度信息
I2C 资源介绍
这两个设备都通过I2C驱动,而 FRDM-MCXW71 开发板上的I2C资源如下,在板载的 Arduino 接口和 Mikro Bus 接口上有一个共同的 I2C1_SDA, I2C1_SCL。
在 UM12063.pdf 文档中有说明,MCXW71 支持2个LPI2C 模块,分别是 LPI2C0 以及 LPI2C1,但是这个 FRDM-MCXW71 开发板只引出了一个 LPI2C1。
在 Zephyr RTOS 中,配置开发板上的外设资源,需要在设备树或者 overlay 中配置并使能。在 zephyr_rtos/boards/nxp/frdm_mcxw71/frdm_mcxw71.dts 设备树中关于 lpi2c1 的设定如下:
status="okay" 表示启用该I2C控制器;
pinctrl-0="\&pinmux_lpi2c1" 表示该I2C控制器使用的引脚复用配置,选用 pinmux_lpi2c1 那一组;
pinctrl-names="default" 为上面的 pinctrl-0 状态命名,"default" 表示这是默认引脚配置状态,系统启动时自动应用;
accelerometer 和子节点表示在该I2C总线下定义了一个子设备节点, accelerometer 是标签名,@19 是该设备的I2C从机地址。
新建 frdm_mcxw71.overlay
在工程新建 boards/frdm_mcxw71.overlay 文件,这是一个 Devicetree Overlay (设备树覆盖层)文件。 Zephyr 构建系统会将此文件与板级基础DTS (frdm_mcxw71.dts) 合并,用于追加或者覆盖硬件配置。 Overlay 文件是应用层定制硬件配置的标准方式,无需修改 SDK 中的原始板级文件。
重点介绍 overlay 中和 i2c 相关的代码。
aliases 节点下有一个 i2c-0=\&lpi2c1 是将 i2c-0 别名指向lpi2c1控制器,应用代码可以通过 DT_ALIAS(i2c_0) 获取 I2C 总线设备,方便跨板移植;
\&lpi2c1 节点,是应用 lpi2c1 而不是重新定义;
status="okay" 表示启用该i2c总线;
clock-frequency=\<400000> 设置I2C时钟频率为 400KHz;
子节点 sen66@6b 表示I2C从设备节点,@6b 表示7位I2C地址为 0x6B;
compatible = "sensirion,sen66" 声明设备为 Sensirion SEN66 环境传感器
reg = \<0x6b> 表示 I2C 从机地址为 0x6B,必须和上面的一致;
status = "okay" 表示启用该传感器;
oled: ssd1306@3c 是一个 OLED 显示屏节点,标签为oled,I2C地址为0x3C;其他属性就不一一介绍了。
/* frdm_mcxw71.overlay */
/ {
aliases {
led0 = &green_led;
i2c-0 = &lpi2c1;
};
leds {
compatible = "gpio-leds";
green_led: led_green {
label = "LED_GREEN";
gpios = <&gpioa 19 GPIO_ACTIVE_LOW>;
status = "okay";
};
};
};
&gpioa {
status = "okay";
};
&hci {
status = "okay";
};
&nbu {
status = "okay";
};
/* 引用 lpi2c1 节点进行修改 */
&lpi2c1 {
status = "okay";
clock-frequency = <400000>;
sen66@6b {
compatible = "sensirion,sen66";
reg = <0x6b>;
status = "okay";
};
/* SSD1306 OLED 显示屏,分辨率 128x64 */
oled: ssd1306@3c {
compatible = "solomon,ssd1306";
reg = <0x3c>;
width = <128>;
height = <64>;
segment-offset = <0>;
page-offset = <0>;
display-offset = <0>;
multiplex-ratio = <63>;
segment-remap;
com-invdir;
prechargep = <2>;
status = "okay";
};
};另外,还必须在项目配置文件 prj.conf 中使能 I2C 以及 OLED 相关的 CONFIG_XXX,如下图所示。
SEN66 驱动移植
SEN66 驱动代码参考官方SDK,直接拷贝到这个工程中,如下所示, sen66_i2c 就是官方SDK,移植只需要把 sen66_i2c/sample-implementations/zephyr_user_space/sensirion_i2c_hal.c 中的几个函数实现即可,即实现 I2C 底层初始化、收发字节即可。
如下图, i2c_dev = DEVICE_DT_GET(DT_ALIAS(i2c_0)) 就是通过别名访问 lpi2c1 总线。
具体来说就是实现
sensirion_i2c_hal_read() ,调用 zephyr API: i2c_read() 读取字节
sensirion_i2c_hal_write(),调用 zephyr API: i2c_write() 写入字节
SEN66 线程
在 sen66_thread.c 中新建一个线程,它的流程如下:
I2C HAL初始化,SEN66 驱动初始化,设备复位
读取并打印序列号
启动传感器的连续测量模式
每秒读取一次数据,每10行打印一次表头
关键点:i2c_bus_lock()/i2c_bus_unlock() 保护I2C总线访问;
关键代码如下
/**
* SEN66 线程入口函数
*/
void sen66_thread_entry(void *p1, void *p2, void *p3)
{
int16_t error = NO_ERROR;
/* 1. 初始化 I2C HAL 层 */
sensirion_i2c_hal_init();
/* 2. 初始化 SEN66 驱动 */
sen66_init(SEN66_I2C_ADDR_6B);
/* 3. 设备复位 */
error = sen66_device_reset();
if (error != NO_ERROR) {
LOG_ERR("sen66 device reset error: %i", error);
return;
}
/* 复位后需要等待一段时间(1.2秒) */
sensirion_hal_sleep_us(1200000);
/* 4. 获取并打印序列号 */
int8_t serial_number[32] = {0};
error = sen66_get_serial_number(serial_number, 32);
if (error != NO_ERROR) {
LOG_ERR("failed to get serial number: %i", error);
} else {
LOG_INF("SEN66 SN: %s", serial_number);
}
/* 5. 启动持续测量 */
error = sen66_start_continuous_measurement();
if (error != NO_ERROR) {
LOG_ERR("执行 start_continuous_measurement() 出错: %i", error);
return;
}
/* 数据变量 */
uint16_t pm1p0 = 0;
uint16_t pm2p5 = 0;
uint16_t pm4p0 = 0;
uint16_t pm10p0 = 0;
int16_t humidity = 0;
int16_t temperature = 0;
int16_t voc_index = 0;
int16_t nox_index = 0;
uint16_t co2 = 0;
/* 行计数器 */
uint16_t row_count = 0;
LOG_INF("SEN66 starts measuring...");
/* 初始化传感器数据模块 */
sensor_data_init();
/* 历史数据计数器 (每分钟添加一次) */
uint16_t history_counter = 0;
while (1) {
/* 获取 I2C 总线锁,保护 SEN66 整个 write+sleep+read 过程 */
i2c_bus_lock();
/* 6. 读取测量值 */
error = sen66_read_measured_values_as_integers(&pm1p0, &pm2p5, &pm4p0, &pm10p0, &humidity, &temperature,
&voc_index, &nox_index, &co2);
/* 释放 I2C 总线锁 */
i2c_bus_unlock();
if (error != NO_ERROR) {
LOG_WRN("Faild to read measurements: %i", error);
continue;
}
/* 每隔10行打印一次表头 */
if (row_count % 10 == 0) {
/* 使用固定宽度打印表头,注意这里去掉了部分空格,完全靠宽度控制 */
LOG_INF("----------------------------------------------------------------"
"----------------------");
LOG_INF("%-" COL_W "s %-" COL_W "s %-" COL_W "s %-" COL_W "s %-" COL_W "s %-" COL_W "s %-" COL_W
"s %-" COL_W "s %-" COL_W "s",
"PM1.0", "PM2.5", "PM4.0", "PM10", "Humi(%)", "Temp(℃)", "CO2", "VOC", "NOx");
}
/* 7. 输出测量结果 */
/* * 1. 统一使用 %-8 宽度
* 2. 浮点数限制宽度为 8.1f (总宽8,含1位小数)
* 3. 整数使用 %-8u 或 %-8i
*/
LOG_INF("%-" COL_W ".1f %-" COL_W ".1f %-" COL_W ".1f %-" COL_W ".1f %-" COL_W ".1f %-" COL_W ".1f %-" COL_W
"u %-" COL_W ".1f %-" COL_W ".1f",
(double)((float)pm1p0 / 10.0f), (double)((float)pm2p5 / 10.0f), (double)((float)pm4p0 / 10.0f),
(double)((float)pm10p0 / 10.0f), (double)((float)humidity / 100.0f),
(double)((float)temperature / 200.0f), co2, (double)((float)voc_index / 10.0f),
(double)((float)nox_index / 10.0f));
row_count++;
k_msleep(1000);
}
/* 理论上不会到达这里,但保留停止测量的逻辑 */
sen66_stop_measurement();
}
/* 定义并启动线程 */
K_THREAD_DEFINE(sen66_thread_id, SEN66_THREAD_STACK_SIZE, sen66_thread_entry, NULL, NULL, NULL, SEN66_THREAD_PRIORITY, 0, 0);OLED驱动
在 src/oled.c 中实现 OLED 驱动,只负责I2C通信,绘制点、线、圆、矩形等基本元素,UI层负责应用层要显示的具体界面。
函数 oled_init() 通过 i2c_dev = DEVICE_DT_GET(DT_ALIAS(i2c_0)) 获取 i2c 句柄。调用 oled_dev = DEVICE_DT_GET(DT_NODELABEL(oled)) 获取 OLED 显示句柄,后续通过这个句柄访问OLED。
UI 线程
主要流程如下:
初始化,OLED驱动初始化
欢迎界面,显示欢迎信息
数据获取,读取传感器数据
数据显示,9种传感器数据,每页显示3个,每隔5秒钟自动翻页显示
异常处理,数据无效时显示等待画面
关键代码如下
/**
* @file user_ui.c
* @brief 用户界面线程 - 应用层绘制
*
* 负责从传感器获取数据并调用OLED驱动显示
*/
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
#include <stdio.h>
#include "sensor_data.h"
#include "oled.h"
#include "user_ui.h"
LOG_MODULE_REGISTER(user_ui, LOG_LEVEL_INF);
/* UI 线程配置 */
#define UI_THREAD_STACK_SIZE 2048
#define UI_THREAD_PRIORITY 7
#define UI_UPDATE_INTERVAL_MS 2000
/**
* @brief 显示传感器数据页面1
*/
static void ui_show_page1(const struct sensor_data *data)
{
char buf[32];
snprintf(buf, sizeof(buf), "TEMP : %5.1F ^C", (double)data->temperature / 200.0);
oled_draw_string(0, 0, buf);
snprintf(buf, sizeof(buf), "HUMI : %5.1F %%", (double)data->humidity / 100.0);
oled_draw_string(0, 16, buf);
snprintf(buf, sizeof(buf), "CO2 : %5u PPM", data->co2);
oled_draw_string(0, 32, buf);
}
/**
* @brief 显示传感器数据页面2
*/
static void ui_show_page2(const struct sensor_data *data)
{
char buf[32];
snprintf(buf, sizeof(buf), "PM1.0: %5.1F UG/M3", (double)data->pm1_0 / 10.0);
oled_draw_string(0, 0, buf);
snprintf(buf, sizeof(buf), "PM2.5: %5.1F UG/M3", (double)data->pm2_5 / 10.0);
oled_draw_string(0, 16, buf);
snprintf(buf, sizeof(buf), "PM4.0: %5.1F UG/M3", (double)data->pm4_0 / 10.0);
oled_draw_string(0, 32, buf);
}
/**
* @brief 显示传感器数据页面3
*/
static void ui_show_page3(const struct sensor_data *data)
{
char buf[32];
snprintf(buf, sizeof(buf), "PM10 : %5.1F UG/M3", (double)data->pm10 / 10.0);
oled_draw_string(0, 0, buf);
snprintf(buf, sizeof(buf), "VOC : %5.1F PPB", (double)data->voc_index / 10.0);
oled_draw_string(0, 16, buf);
snprintf(buf, sizeof(buf), "NOX : %5.1F PPB", (double)data->nox_index / 10.0);
oled_draw_string(0, 32, buf);
}
/**
* @brief 显示传感器数据(自动翻页)
*/
static void ui_update_display(const struct sensor_data *data)
{
static uint8_t page_idx = 0;
static int64_t last_page_switch = 0;
int64_t now = k_uptime_get();
char buf[16];
/* 每5秒切换显示页面 */
if (now - last_page_switch >= 5000) {
page_idx = (page_idx % 3) + 1;
last_page_switch = now;
}
oled_clear();
if (page_idx == 1) {
ui_show_page1(data);
} else if (page_idx == 2) {
ui_show_page2(data);
} else if (page_idx == 3) {
ui_show_page3(data);
}
/* 页面指示器 */
snprintf(buf, sizeof(buf), "%d/3 PAGE", page_idx);
oled_draw_string(64, 48, buf);
oled_refresh();
}
/**
* @brief 显示欢迎画面
*/
static void ui_show_welcome(void)
{
oled_clear();
oled_draw_centered(0, "EEPW & ELE14");
oled_draw_centered(16, "MCXW71 BLE HT");
oled_draw_centered(32, "OLED && BLE");
oled_draw_centered(48, "WECHAT MINIPROGRAM");
oled_refresh();
}
/**
* @brief 显示等待数据画面
*/
static void ui_show_waiting(void)
{
oled_clear();
oled_draw_centered(16, "WAITING DATA...");
oled_refresh();
}
/**
* @brief UI线程入口函数
*/
void user_ui_thread_entry(void *p1, void *p2, void *p3)
{
int ret;
struct sensor_data data;
LOG_INF("UI thread started");
/* 初始化OLED驱动 */
ret = oled_init();
if (ret != 0) {
LOG_ERR("OLED init failed: %d", ret);
return;
}
/* 显示欢迎画面 */
ui_show_welcome();
k_sleep(K_MSEC(5000));
/* 主循环 */
while (1) {
ret = sensor_data_get(&data);
if (ret == 0 && data.valid) {
ui_update_display(&data);
} else {
ui_show_waiting();
}
k_sleep(K_MSEC(UI_UPDATE_INTERVAL_MS));
}
}
/* 定义UI线程 */
K_THREAD_DEFINE(user_ui_thread_id, UI_THREAD_STACK_SIZE,
user_ui_thread_entry, NULL, NULL, NULL,
UI_THREAD_PRIORITY, 0, 0);运行演示
上电后OLED上显示欢迎界面,等待5秒钟;
等待SEN66获取数据,在UI上显示,分3个页面,每个页面显示5秒钟,然后切换到下一个页面;
三个页面轮播;





问题与解决
问题1: hci_core.c 504 Controller unresponsive
问题
解决办法,参考 zephyr 官网 https://docs\.zephyrproject\.org/latest/boards/nxp/frdm\_mcxw71/doc/index\.html 这里有针对 FRDM-MCXW71 开发板的资源介绍、
问题1:解决,获取无线固件并烧写
为支持蓝牙功能, frdm_mcxw71 需要获取二进制固件blob,即运行命令行 west blobs fetch hal_nxp 进行下载。
命令行烧写 NBU 固件
烧写成功
问题2:烧写 NBU 固件之后的运行错误
报错如下,还是 hci_core.c:504 Controller unresponsive. command opcode 0x0c03 timeout with err -11
可能的原因:配置文件中缺少 MCXW71 BLE HCI 控制器的驱动配置
问题2:解决,修改 prj.conf 和 overlay 文件
修改点1:prj.conf 新增 BLE HCI IPC 控制器配置
修改点2: 更新 frdm_mcxw71.overlay 添加 BLE HCI 和 NBU 设备节点并启用
问题3:OLED device not ready
问题3:解决,修改 overlay 文件
修改overlay文件,即 boards/frdm_mcxw71.overlay 文件,在 lpi2c1 节点下新增 oled 标签,设置 ssd1306 设备属性,兼容性为 solomon,ssd1306fb;宽度、高度等属性,如下所示:
问题4:运行一段时间后传感器数据异常
如下图所示,SEN66传感器只有一两次数据正常,其他读数都是异常的。
经过分析,异常的数据都是当前数据类型的最大值,例如
温度 32760 ≈ 32767(INT16_MAX),PM 65335 ≈ 65535(UINT16_MAX)。这些都是 SEN66 传感器返回的无效/错误标记值。
由于 sen66 是通过I2C通信,挂载 i2c0,并且 oled 也是挂载 i2c0 上,并且两个线程的优先级相近,可能导致 sen66_thread.c 现成在读取传感器数据时被打断。
问题4:解决,给 i2c0 访问添加互斥锁
第一步:添加 i2c_bus_clock.c/h
实现3个API,即针对 i2c 的 mutex 初始化、上锁、解锁。
第二步:互斥锁初始化,针对 i2c0 访问上锁、解锁
在 main.c 中调用 i2c_bus_lock_init() 初始化互斥锁;
在 sen66_thread.c 中读取SEN66传感器的函数前后上锁、解锁;
在 oled.c 中刷新 OLED 屏幕上锁、解锁;
解决之后运行50分钟再没有出现数值为最大值的异常情况,如下日志所示:
[00:50:35.643,035] <inf> sen66_thread: -------------------------------------------------------------------------------------- [00:50:35.643,127] <inf> sen66_thread: PM1.0 PM2.5 PM4.0 PM10 Humi(%) Temp(℃) CO2 VOC NOx [00:50:35.643,157] <inf> sen66_thread: 497.7 518.3 518.3 518.3 49.3 26.2 787 126.0 1.0 [00:50:36.676,269] <inf> sen66_thread: 746.4 777.2 777.2 777.2 49.3 26.2 787 125.0 1.0 [00:50:37.709,442] <inf> sen66_thread: 717.4 747.0 747.0 747.0 49.4 26.2 787 126.0 1.0 [00:50:38.743,621] <inf> sen66_thread: 593.9 618.4 618.4 618.4 49.4 26.2 787 126.0 1.0 [00:50:39.777,801] <inf> sen66_thread: 456.3 475.2 475.2 475.2 49.4 26.2 787 126.0 1.0 [00:50:40.812,011] <inf> sen66_thread: 516.6 537.9 537.9 537.9 49.4 26.2 787 129.0 1.0 [00:50:41.846,160] <inf> sen66_thread: 689.5 718.0 718.0 718.0 49.4 26.2 787 129.0 1.0 [00:50:42.880,371] <inf> sen66_thread: 642.4 668.9 668.9 668.9 49.4 26.2 787 129.0 1.0 [00:50:43.913,543] <inf> sen66_thread: 522.7 544.2 544.2 544.2 49.4 26.2 787 129.0 1.0 [00:50:44.947,753] <inf> sen66_thread: 396.0 412.3 412.3 412.3 49.4 26.2 787 137.0 1.0 [00:50:45.981,903] <inf> sen66_thread: -------------------------------------------------------------------------------------- [00:50:45.981,994] <inf> sen66_thread: PM1.0 PM2.5 PM4.0 PM10 Humi(%) Temp(℃) CO2 VOC NOx [00:50:45.982,025] <inf> sen66_thread: 579.5 603.4 603.4 603.4 49.4 26.2 786 136.0 1.0 [00:50:47.015,136] <inf> sen66_thread: 724.5 754.4 754.4 754.4 49.4 26.2 786 135.0 1.0 [00:50:48.049,285] <inf> sen66_thread: 661.7 689.0 689.0 689.0 49.4 26.2 785 135.0 1.0 [00:50:49.083,465] <inf> sen66_thread: 533.3 555.4 555.4 555.4 49.4 26.2 785 135.0 1.0 [00:50:50.116,668] <inf> sen66_thread: 401.7 418.3 418.3 418.3 49.4 26.2 785 134.0 1.0 [00:50:51.150,848] <inf> sen66_thread: 653.1 680.0 680.0 680.0 49.4 26.2 784 134.0 1.0 [00:50:52.185,058] <inf> sen66_thread: 701.4 730.4 730.4 730.4 49.4 26.2 784 134.0 1.0 [00:50:53.231,903] <inf> sen66_thread: 606.6 631.6 631.6 631.6 49.4 26.2 784 134.0 1.0 [00:50:54.266,113] <inf> sen66_thread: 477.2 496.9 496.9 496.9 49.4 26.2 784 134.0 1.0 [00:50:55.299,285] <inf> sen66_thread: 426.8 444.5 444.5 444.5 49.4 26.2 784 134.0 1.0 [00:50:56.333,465] <inf> sen66_thread: -------------------------------------------------------------------------------------- [00:50:56.333,557] <inf> sen66_thread: PM1.0 PM2.5 PM4.0 PM10 Humi(%) Temp(℃) CO2 VOC NOx [00:50:56.333,587] <inf> sen66_thread: 758.6 789.9 789.9 789.9 49.4 26.2 784 134.0 1.0 [00:50:57.366,699] <inf> sen66_thread: 756.7 787.9 787.9 787.9 49.4 26.2 784 134.0 1.0 [00:50:58.400,848] <inf> sen66_thread: 635.8 662.0 662.0 662.0 49.4 26.2 785 134.0 1.0 [00:50:59.468,231] <inf> sen66_thread: 490.6 510.9 510.9 510.9 49.4 26.2 785 134.0 1.0 [00:51:00.502,441] <inf> sen66_thread: 493.0 513.3 513.3 513.3 49.4 26.2 786 134.0 1.0 [00:51:01.536,621] <inf> sen66_thread: 693.6 722.2 722.2 722.2 49.4 26.2 786 134.0 1.0 [00:51:02.570,770] <inf> sen66_thread: 572.2 595.8 595.8 595.8 49.4 26.2 786 134.0 1.0 [00:51:03.604,980] <inf> sen66_thread: 438.7 456.8 456.8 456.8 49.4 26.2 786 134.0 1.0 [00:51:04.638,153] <inf> sen66_thread: 565.2 588.6 588.6 588.6 49.4 26.2 785 134.0 1.0 [00:51:05.672,332] <inf> sen66_thread: 723.1 753.0 753.0 753.0 49.4 26.2 785 134.0 1.0
我要赚赏金
