基于 Zephyr 的蓝牙温湿度计
一、项目背景介绍
1.1 选题背景
在现代智能家居、工业监测、农业大棚、冷链物流等众多场景中,温湿度始终是最基础、最常用的环境参数。传统的"有线温湿度计"存在部署不灵活、布线成本高、覆盖范围有限等痛点;而基于蓝牙低功耗(BLE)的无线温湿度计则具备部署自由、低功耗、可手机直连等天然优势。
随着 BLE 5.0+ 在智能手机上的全面普及,以及 NORDIC、TI、NXP 等主流芯片厂商不断推出高集成度、低功耗的 SoC,"无线温湿度计"已经是一项成熟可量产的方案。但在实际项目开发过程中,从"裸 MCU"到"稳定可用的产品"之间,仍然有大量的工程化工作要做:协议栈移植、传感器驱动、协议封装、低功耗优化、稳定性测试等。
本项目选择"基于 Zephyr 的蓝牙温湿度计"作为最终目标,出发点如下:
学习 BLE 协议栈的真实工程落地——区别于单纯的"协议学习",本次开发直面"从代码到烧录到手机 APP 联调"的全链路流程;
探索 Zephyr RTOS 在 BLE 物联网领域的开发模式——Zephyr 作为一款由 Linux 基金会托管、得到 NXP/Intel/Nordic 等多家支持的现代 RTOS,其模块化设计、设备树驱动模型与主流 Linux 内核思想一脉相承,是嵌入式工程师值得掌握的开源生态;
积累"传感器+BLE"的端到端经验——为后续更复杂的无线传感节点(多传感器、低功耗优化、安全加密等)打基础。
1.2 项目目标
本项目以"一个能稳定运行、可用手机读取的蓝牙温湿度计"为最终交付目标,具体分解为以下几条:
目标维度详细要求
| 功能性 | 板载传感器周期性采集环境温湿度,通过 BLE 透传至手机 APP |
| 兼容性 | 与主流 BLE 调试助手(nRF Connect / EFR Connect / 蓝牙调试宝)兼容,无需自定义手机 App |
| 实时性 | 数据更新周期 1 秒,手机端可观察到连续数据流 |
| 稳定性 | 支持手机断开后设备自动恢复广播并允许新连接 |
1.3 项目主要成果
经过方案设计、代码改造、硬件接线、固件烧录与功能验证后,本项目最终实现并验证了以下功能:
板载温度采样精度 ±0.5℃,湿度采样精度 ±3 %RH(DHT20 标称);
广播名为 Zephyr_NUS 的 BLE 外设,启动后自动可发现;
移动端调试助手(nRF Connect 等)连接后,每秒推送一条形如 T:25.32 H:60.45 的字符串;
手机端主动断开后,板载 LED 恢复闪烁、串口提示 Disconnected、设备自动重启广播,新连接后数据流立刻恢复;
完整工程文件组织清晰,包含 prj.conf / app.overlay / src/main.c / drivers/ 等模块,便于二次开发。
二、硬件介绍
2.1 主控开发板:NXP FRDM-MCXW72
FRDM-MCXW72 是 NXP 半导体(恩智浦)2024 年推出的官方评估板,搭载 MCX W72 系列无线 MCU,是 NXP 在 Matter / Thread / Zigbee / BLE 等无线连接领域的主力平台之一。
2.2 温湿度传感器:Grove DHT20
Grove DHT20 是一款基于 Aosong(奥松电子)DHT20 数字温湿度传感器的 Seeed Studio Grove 标准化模组。
2.3 辅助设备
设备用途说明
| Micro-USB 数据线 | 供电 + J-Link 调试 + 串口 | 必须为数据线,纯充电线无法识别 |
| nRF Connect 手机 APP | 蓝牙调试助手 | Android / iOS 都有,扫描、连接、NUS Notify 一应俱全 |
| 任意 Windows 10/11 PC | 编译与烧录主机 | 已装 Zephyr SDK 1.0.1 + Python 3.13 + West + CMake 3.30 |
硬件连接如下:

三、方案框图和项目设计思路
3.1 系统总体框图
┌─────────────────┐ I2C (SDA=PTB4, SCL=PTB5, 100/400kHz) ┌────────────────────────────┐ │ DHT20 传感器 │◄───────────────────────────────────────►│ FRDM-MCXW72 开发板 │ │ (地址 0x38) │ │ ┌──────────────────────┐ │ │ │ │ │ MCXW727CMFTA │ │ │ VCC=3V3 │ │ │ ┌────────────────┐ │ │ │ GND │ │ │ │ Cortex-M33 CPU │ │ │ └────────────────┘ │ │ └───────┬────────┘ │ │ │ │ │ │ │ │ │ ┌───────▼────────┐ │ │ │ │ │ LPI2C1 控制器 │──┼──┘ │ │ └────────────────┘ │ │ │ │ │ │ ┌────────────────┐ │ │ │ │ 2.4G 射频模块 │──┼──► 板载天线 ──► 空中 BLE 5.3 │ │ └────────────────┘ │ │ │ │ │ │ ┌────────────────┐ │ │ │ │ LPUART1 串口 │──┼──► J-Link CDC ──► USB ──► PC 串口 │ │ └────────────────┘ │ │ │ │ │ │ ┌────────────────┐ │ │ │ │ GPIO 控制器 │──┼──► 蓝色 LED (PTB1) / 用户按键 (PTC6) │ │ └────────────────┘ │ │ └──────────────────────┘ │ │ │ │ 板载 J-Link 调试器 │ │ (USB <--> SWD + UART) │ └──────────┬─────────────────┘ │ USB ┌──────────▼─────────────────┐ │ PC 编译/烧录 + 手机 APP │ │ - west build / west flash │ │ - 串口监视 (COM83) │ │ - nRF Connect (扫描/连接) │ └────────────────────────────┘
3.2 软件架构
Zephyr 工程的代码分层非常清晰,从底层到上层依次为:
┌───────────────────────────────────────────────────────┐ │ 应用层 (本项目的 src/main.c) │ │ ├─ 广播管理 (bt_le_adv_start) │ │ ├─ 连接管理 (BT_CONN_CB_DEFINE) │ │ ├─ NUS 服务 (bt_nus_send / bt_nus_cb_register) │ │ └─ 传感器读取 (sensor_sample_fetch / channel_get) │ ├───────────────────────────────────────────────────────┤ │ Zephyr 服务层 (subsys/) │ │ ├─ bluetooth/services/nus (NUS GATT 实现) │ │ ├─ bluetooth/host (GATT/ATT/GAP 协议栈) │ │ └─ sensor (Sensor Framework) │ ├───────────────────────────────────────────────────────┤ │ Zephyr 驱动层 (drivers/) │ │ ├─ sensor/aosong/dht20 (DHT20 I2C 驱动) │ │ ├─ i2c/nxp_lpi2c (LPI2C1 控制器驱动) │ │ ├─ bluetooth/nxp (MCXW72 BLE 控制器) │ │ ├─ gpio/... (LED GPIO 驱动) │ │ └─ uart/... (J-Link 串口) │ ├───────────────────────────────────────────────────────┤ │ 硬件抽象 (HAL, 由 NXP MCUXpresso SDK 提供) │ │ ├─ nxp,mcxw7 clock / pinctrl / peripheral │ │ └─ 直接寄存器操作 │ ├───────────────────────────────────────────────────────┤ │ ARM Cortex-M33 内核 + Zephyr 内核 │ └───────────────────────────────────────────────────────┘
3.3 关键设计
在动手编码前,有几个关键决策点需要明确。这些选择直接影响后续的开发难度和最终效果。
为什么选 Zephyr?
原生支持 BLE 5.3 协议栈——Zephyr 的 Bluetooth 子系统是经过 Bluetooth SIG 认证的完整 Host 层;
统一驱动模型——所有传感器、外设都通过标准 API(sensor_*、i2c_*)访问,方便在不同 MCU 上移植;
设备树驱动——硬件描述(I2C 总线、传感器节点、引脚复用)全部写在 .dts/.overlay 中,代码与硬件解耦;
维护活跃——NXP 官方支持,官方文档齐全,issue 响应快。
为什么选 NUS 作为传输协议?
手机调试助手原生支持——nRF Connect 等 APP 在列出 GATT 服务时,会自动识别 NUS 并把 TX/RX 字符画成 "UART" 视图,免去自己开发 APP;
协议简单——NUS 仅 2 个 characteristic(TX Notify / RX Write),是 BLE 上最简洁的"流式通道"之一;
官方有现成模块——CONFIG_BT_ZEPHYR_NUS=y 直接打开,bt_nus_send() 即可发送,不用手写 GATT 表。
为什么以 peripheral_hr 为起点?
Zephyr 自带的蓝牙示例大致分为两类:
通用外设(peripheral、peripheral_gap_svc):只有一个空的 GATT 表,需要自己加服务
标准服务(peripheral_hr、peripheral_bas、peripheral_dis):内置标准 profile,但不能直接用来传输应用数据
本项目先用 peripheral_hr 学习 Zephyr BLE 工程的完整骨架(初始化、连接回调、状态机、主循环),再改造成 NUS 透传。这是初学者最平滑的上手路径——比直接看 peripheral_nus 那种"已经做完的"例子学得更多。
为什么把 DHT20 驱动放在工程目录下而不是 SDK?
学习目的——把驱动放在 drivers/sensor/aosong/dht20/ 下作为参考,未来想修改驱动逻辑(如增加 CRC 校验、改采样流程)时非常方便;
解耦产品工程——业务代码与驱动代码分目录管理,符合实际产品工程的代码组织规范;
最终选择 SDK 版本——Zephyr SDK 中已经包含完全相同的 DHT20 驱动,并且会被设备树节点自动启用;本地副本仅作为参考、备份存在,不参与编译。这样可以避免符号冲突的麻烦
四、调试软件、流程图和关键代码
开发环境
工具版本用途
| Zephyr RTOS | v3.7+ | RTOS + BLE 协议栈 + 驱动 |
| Zephyr SDK | 1.0.1 (GCC 14.3.0) | 交叉编译工具链 |
| MCUXpresso Tools | v25.x | IDE + 烧录辅助 |
| Visual Studio Code | 最新版 | 代码编辑(推荐) |
| CMake | 3.30+ | 构建系统 |
| Ninja | 1.10+ | 构建执行器 |
| Python | 3.13 | west 工具链 |
| West | 1.5+ | Zephyr 多仓管理 + 构建命令 |
| J-Link | V8.94 | 烧录 + 调试 |
| nRF Connect | 任意版本 | 手机端 BLE 调试 APP |
软件流程图
主循环流程
┌──────────────────┐
│ 上电 / 复位 │
└────────┬─────────┘
▼
┌────────────────────────────────┐
│ Zephyr 内核初始化 │
│ (drivers / subsystems / main) │
└────────────────┬───────────────┘
▼
┌────────────────────────────────┐
│ DHT20 init: │
│ - 检查 I2C 总线是否 ready │
│ - 等待 100ms 上电稳定 │
│ - 读取并校正 status 寄存器 │
│ (init 失败: device 不 ready) │
└────────────────┬───────────────┘
▼
┌────────────────┐
│ main() │
└────────┬───────┘
▼
┌────────────────────────────────┐
│ bt_enable(NULL) │
│ - 初始化 HCI 传输层 │
│ - 初始化 Host 层协议栈 │
│ - 设置本地 BLE 地址 │
└────────────────┬───────────────┘
▼
┌────────────────────────────────┐
│ bt_nus_cb_register(&nus_cb) │
│ - 注册 NUS 通知/接收回调 │
└────────────────┬───────────────┘
▼
┌────────────────────────────────┐
│ bt_le_adv_start(...) │
│ - 启动 Legacy 可连接广播 │
│ - 间隔 30~60ms │
│ - 广播数据: flags + NUS UUID │
└────────────────┬───────────────┘
▼
┌────────────────────────────────┐
│ while (1) 主循环 │◄──────────┐
│ k_sleep(K_SECONDS(1)) │ │
│ if (current_conn): │ │
│ - read DHT20 │ │
│ - bt_nus_send("T:xx.x H:xx") │ │
│ if (DISCONNECTED 位): │ │
│ - restart advertising │ │
└────────────────┬───────────────┘ │
│ │
┌────────────────────┴───────────────────┐ │
▼ ▼ │
┌──────────────────┐ ┌──────────────────┐ │
│ 手机连上 │ │ 手机断开 │ │
│ connected() │ │ disconnected() │ │
│ - bt_conn_ref() │ │ - bt_conn_unref │ │
│ - 置 CONNECTED │ │ - 置 DISCONNECT │──┘
│ - LED 常亮 │ │ - LED 闪烁 │
└──────────────────┘ └──────────────────┘DHT20 单次读取流程
┌──────────────────────────────────┐ │ sensor_sample_fetch(dev) │ │ 触发测量: 写 [0xAC, 0x33, 0x00] │ └────────────┬─────────────────────┘ ▼ ┌──────────────────────────────────┐ │ k_msleep(80) │ │ 等待 DHT20 完成测量 │ └────────────┬─────────────────────┘ ▼ ┌──────────────────────────────────┐ │ i2c_read_dt(bus, buf, 7) │ │ 读取 7 字节: │ │ buf[0]: 状态 │ │ buf[1..3]: 湿度原始值 (20 bit) │ │ buf[3..5]: 温度原始值 (20 bit) │ │ buf[6]: CRC │ └────────────┬─────────────────────┘ ▼ ┌──────────────────────────────────┐ │ 校验 buf[0] 的 bit7: │ │ - 1: busy, 报错 │ │ - 0: 测量完成 │ └────────────┬─────────────────────┘ ▼ ┌──────────────────────────────────┐ │ 计算公式: │ │ T = (raw/2^20)*200 - 50 │ │ H = (raw/2^20)*100 │ │ 结果存为 struct sensor_value │ │ val1: 整数部分 │ │ val2: 小数部分 (×1e6) │ └────────────┬─────────────────────┘ ▼ 返回到应用层
关键代码详解
设备树 Overlay(app.overlay)
设备树是 Zephyr 描述硬件的核心方式。下面的 overlay 文件把 DHT20 挂载到 lpi2c1 总线:
&lpi2c1 {
status = "okay";
pinctrl-0 = <&pinmux_lpi2c1>;
pinctrl-names = "default";
dht20: dht20@38 {
compatible = "aosong,dht20";
reg = <0x38>;
status = "okay";
};
};&lpi2c1 —— 引用板级 dts 中的 lpi2c1 节点
pinctrl-0 = <&pinmux_lpi2c1> —— 引用板级 pinmux 配置(PTB4/PTB5)
compatible = "aosong,dht20" —— 与 Zephyr SDK 中 DHT20 驱动匹配
reg = <0x38> —— I2C 设备地址
Kconfig 配置(prj.conf)
# Bluetooth CONFIG_BT=y CONFIG_LOG=y CONFIG_BT_PERIPHERAL=y CONFIG_BT_ZEPHYR_NUS=y CONFIG_BT_DEVICE_NAME="Zephyr_NUS" # I2C bus CONFIG_I2C=y # Sensors CONFIG_SENSOR=y CONFIG_DHT20=y
CONFIG_BT_ZEPHYR_NUS=y —— 关键,使能 NUS 服务模块
CONFIG_I2C=y —— 使能 I2C 子系统
CONFIG_SENSOR=y —— 使能 Sensor Framework
CONFIG_DHT20=y —— 选择 aosong_dht20 驱动(实际由设备树触发自动选中)
主循环核心代码
int main(void)
{
int err;
static const char hello_msg[] = "hello\n"; // 早期调试用
err = bt_enable(NULL); /* (1) 初始化 BLE 协议栈 */
if (err) { return 0; }
bt_nus_cb_register(&nus_cb, NULL); /* (2) 注册 NUS 回调 */
start_advertising(); /* (3) 启动广播 */
while (1) {
k_sleep(K_SECONDS(1));
if (current_conn) { /* (4) 已连接则发数据 */
struct sensor_value temp, hum;
char buf[64];
int len = snprintf(buf, sizeof(buf),
"T:%d.%02d H:%d.%02d\n",
temp.val1, abs(temp.val2) / 10000,
hum.val1, abs(hum.val2) / 10000);
bt_nus_send(current_conn, buf, len);
}
if (STATE_DISCONNECTED 位) { /* (5) 断线重启广播 */
start_advertising();
}
}
}5 个关键步骤:
bt_enable(NULL) —— 同步等待协议栈初始化完成
bt_nus_cb_register() —— 注册 NUS 通知状态/接收回调
start_advertising() —— 启动可发现广播
bt_nus_send() —— 只有连接后才发数据(避免无连接时返回 ENOTCONN 刷屏)
断线后重启广播 —— 这是"支持断线重连"的关键
传感器读取函数
static int read_dht20(struct sensor_value *temp, struct sensor_value *hum)
{
const struct device *dev = DEVICE_DT_GET(DT_INST(0, aosong_dht20));
if (!device_is_ready(dev)) { /* 检查驱动 init 是否成功 */
printk("DHT20 device not ready\n");
return -ENODEV;
}
if (sensor_sample_fetch(dev) < 0) return -EIO; /* 触发测量 + 读回 */
sensor_channel_get(dev, SENSOR_CHAN_AMBIENT_TEMP, temp);
sensor_channel_get(dev, SENSOR_CHAN_HUMIDITY, hum);
return 0;
}DT_INST(0, aosong_dht20) —— 取得设备树中第 0 个 aosong,dht20 节点
DEVICE_DT_GET() —— 把节点转换为 struct device *
device_is_ready() —— 关键防御,驱动 init 失败时返回 false(这是调试时排查故障的入口)
sensor_sample_fetch() —— 触发一次完整测量(~80ms)
sensor_channel_get() —— 分别取得温湿度通道值
五、硬件功能展示
串口输出信息:

手机app接收打印展示:

我要赚赏金
