基于 FRDM-MCXW72 + Zephyr + Web Bluetooth 的温湿度监控实战经验。
一、系统架构这套系统的数据通路是这样的:SHT40 传感器测量温湿度 → Zephyr 应用读取数据 → 通过蓝牙 NUS 服务发送 → 手机或电脑接收 → 网页实时展示。
整个链路涉及三个核心组件:
蓝牙 NUS(Nordic UART Service) 是这套方案的关键。它是 Nordic 半导体定义的一个 BLE 服务,本质上把蓝牙连接模拟成了一根串口线——应用层不需要了解 BLE 的 ATT/GATT 协议细节,只需要发送和接收字节流,对方就能收到。NUS 包含两个 Characteristic:TX(发送数据给手机)和 RX(接收手机发来的数据),配合 CCCD(Client Characteristic Configuration Descriptor)控制通知功能。
Web Bluetooth API 让我们可以直接在网页里连接 BLE 设备,无需安装任何 App 或驱动。Chrome、Edge 等主流浏览器都支持。这意味着只要有一台有蓝牙的电脑或手机,打开网页就能连接设备。
Zephyr BLE Host 层 负责处理蓝牙协议的 Host 端逻辑。在 MCXW72 这类双核芯片上,应用运行在 CM33 核心,蓝牙基带运行在另一个核心,两者通过 IPC 通信。bt_nus_send() 函数把数据放入 ATT 发送队列,由 Host 层自动处理分包、重传、流量控制。
二、硬件电路与设备树配置FRDM-MCXW72 开发板板载 SHT40 传感器,通过 I2C 总线与 MCU 通信。I2C 地址是 0x44,这是 SHT40 出厂设定的固定地址,无法修改。
在 boards/frdm_mcxw72.overlay 中声明传感器:
&lpi2c1 {
status = "okay";
sht40: sht40@44 {
compatible = "sensirion,sht4x";
reg = <0x44>;
label = "SHT40";
};
};&lpi2c1 引用了开发板默认 I2C 总线配置,status = "okay" 使能总线。传感器节点的 compatible 字符串必须与 Zephyr 现有驱动匹配,否则系统找不到对应的驱动文件。
设备树的 label 属性(这里设为 "SHT40")用于应用层获取设备句柄:
#define SHT40_NODE DT_NODELABEL(sht40) sensor_dev = DEVICE_DT_GET(SHT40_NODE);三、应用层逻辑
应用的 main 函数执行顺序很清晰:注册 NUS 回调 → 初始化蓝牙 → 初始化传感器 → 启动广播 → 主循环定时读取发送。
int main(void) {
bt_nus_cb_register(&nus_listener, NULL); // 注册透传回调
bt_enable(NULL); // 初始化蓝牙协议栈
sensor_init(); // 初始化 SHT40
bt_le_adv_start(...); // 启动广播
while (1) {
k_sleep(K_SECONDS(5));
if (current_conn && notifications_enabled) {
k_work_schedule(&sensor_work, K_NO_WAIT);
}
}
}为什么要用 k_work_schedule 而不是直接调用? 因为 bt_nus_send() 是一个异步发送函数,内部会获取蓝牙锁,如果在中断上下文(如定时器回调)中直接调用,可能导致死锁。k_work 工作项会在系统调度器安全的上下文执行,避免锁冲突。
notifications_enabled 的作用是什么? 这是个标志位,由 notif_enabled() 回调设置。当手机端成功订阅了 TX Characteristic 的 Notification,Zephyr 框架会调用这个回调,告诉我们"对方已经准备好接收数据了"。如果对方没订阅就发送数据,bt_nus_send() 会返回成功(数据进入队列),但数据实际上会被丢弃。
四、数据透传的实现细节bt_nus_send() 是透传的核心函数。它的行为需要理解清楚:
ret = bt_nus_send(current_conn, mqtt_buf, strlen(mqtt_buf));
if (ret < 0) {
printk("Failed to send NUS data (err %d)\n", ret);
} else {
printk("NUS send OK: %s\n", mqtt_buf);
}bt_nus_send() 返回成功(ret = 0)只表示数据成功放入发送队列,并不代表对方已经收到。 这是蓝牙 ATT 协议的设计:发送方把数据放进队列就立即返回,实际传输由 Host 层后台处理。如果对方没开启 Notification 订阅、或者连接已经断开,数据会在队列中等待或被丢弃,但函数本身已经返回成功了。
这与 TCP 的 send() 类似——返回成功只表示内核缓冲区接受了这个数据,不代表对方收到了。蓝牙透传是一个"尽力而为"的通道。发送的数据格式早期是 JSON:{"t":26.83,"h":73.79},但这个格式在短 payload 高频率发送场景下遇到问题。最终改为简洁的纯文本格式 T:26.7 H:72.7,更容易解析且占用带宽更少。
五、网页端连接与数据接收网页通过 Web Bluetooth API 连接设备,核心流程是:请求设备 → 连接 GATT Server → 获取 NUS Service → 获取 TX Characteristic → 开启通知。
characteristic = await service.getCharacteristic(NUS_TX_CHAR_UUID);
await characteristic.startNotifications();
// 手动使能 CCCD(部分浏览器需要)
const cccd = await characteristic.getDescriptorByUUID('2902');
await cccd.writeValue(new Uint8Array([0x01, 0x00]));startNotifications() 的作用是什么? 这个函数向蓝牙设备发送一个写请求,在 CCCD 中写入 0x0001(表示启用 Notification)。设备收到这个请求后,才会通过 Notification 方式发送数据。nRF Connect 默认会自动做这件事,但某些浏览器实现不完整,需要手动写 CCCD。
为什么数据格式带有双引号? 固件发送的是字节流 "T:26.7 H:72.7\n",但 TextDecoder.decode() 返回的是 JavaScript 字符串。经过 JSON.stringify() 打印后,引号被转义显示。实际数据处理时要去除首尾引号:text.trim().replace(/^"|"$/g, '')。网页端的解析函数会依次尝试 JSON 格式和纯文本格式:
// JSON 格式:{"t":26.7,"h":72.7}
try {
const data = JSON.parse(text);
updateData('temperature', data.t.toFixed(2));
return;
} catch (e) {}
// 纯文本格式:T:26.7 H:72.7
let clean = text.trim().replace(/^"|"$/g, '');
const t = clean.match(/T:(-?\d+\.?\d*)/);
const h = clean.match(/H:(-?\d+\.?\d*)/);六、调试过程中的坑与解决网页端连接成功但不显示数据: 控制台能看到 characteristicvaluechanged 事件触发,数据也收到了,但页面温度湿度始终是 --。排查发现是数据解析逻辑的问题——固件发送的 T:26.7 H:72.7 带双引号,JSON.parse 失败后,正则表达式没匹配到预期的格式。修复方法是在解析前去除引号。
串口调试效果:

页面数据显示效果:

使用这套系统的完整流程:
第一步:烧录固件。 用 ninja flash 将编译好的 zephyr.bin 烧录到开发板。
第二步:打开网页。 在 Chrome/Edge 中打开 ble-monitor.html,点击"搜索蓝牙设备",在弹出的设备列表中选择 MCXW72-BLE。
第三步:等待连接。 网页显示"已连接"后,等待约 5 秒,首次数据就会出现在页面上。之后每 5 秒更新一次。
第四步:查看原始数据。 页面底部的黑色区域会显示每次接收到的原始数据,方便排查问题。
如果连接成功但页面不更新,打开浏览器开发者工具(F12)→ Console,查看是否有 updateData called 日付。如果没有,说明 parseData 解析失败,页面上会显示 [PARSE FAILED]。
八、关键配置参数
我要赚赏金
