这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » DIY与开源设计 » 电子DIY » 【e起DIY】低功耗蓝牙温湿度计:把DHT20装进BLE透传,实现蓝牙温湿度计

共1条 1/1 1 跳转至

【e起DIY】低功耗蓝牙温湿度计:把DHT20装进BLE透传,实现蓝牙温湿度计

高工
2026-06-09 23:26:11     打赏

        这是我们这阶段最后的成果展示。从一块裸的FRDM-MCXW72开发板开始,一步步把它做成了一个"打开手机/浏览器就能看温湿度"的蓝牙小设备。先说最终的样子:

  • 一块FRDM-MCXW72板子,接了Grove-TH Sensor V2.0(DHT20)

  • 板子通过BLE广播,名字叫Zephyr TH Sensor

  • 一台手机/电脑上的浏览器打开配套的HTML页面

  • 点一下"扫描连接",浏览器和板子蓝牙握手

  • 之后每隔2秒,板子把"温度 24.32°C、湿度 58.06%RH"以一行文本的形式推过去

  • 页面顶部两张大卡片显示当前温湿度,底部黑窗口是滚动日志

            形态上,就是把以前USB串口接到电脑上用printk打印日志的方式,搬到了无线链路上。逻辑上,板子干的事和之前一模一样——读传感器、格式化、往外吐数据——只是"往外吐"这一步从UART换成了 BLE NUS。

为什么选 NUS

        蓝牙BLE上能跑的东西很多(心率、电池、按键……),我们要做"无线串口",最直接的方案是 NUS(Nordic UART Service)

        NUS是Nordic半导体的私有GATT服务,它在GATT之上模拟了一个UART通道

  • TX characteristic:设备往客户端发(用 notify 主动推)

  • RX characteristic:客户端往设备发(用 write)

  • 128-bit UUID:6E400001-... (Service) / 6E400002 (RX) / 6E400003 (TX)

        为什么选它:

  1. 零成本:Zephyr官方就内置了,注册几个回调就能用

  2. 协议简单:和串口完全一样,吐纯文本、收纯文本,没有二进制编解码

  3. Web Bluetooth 友好:Chrome / Edge 的Web Bluetooth API允许访问NUS(虽然128-bit私有UUID 普遍被禁,但NUS在白名单里)

        UUID 这点特别提一下:绝大多数128-bit自定义UUID浏览器都会拒绝。NUS是为数不多被 Web Bluetooth显式允许的私有服务之一。如果我们自己造一个UUID出来,前端连不上。

Zephyr固件侧用NUS多简单

#include <zephyr/bluetooth/services/nus.h> static void notif_enabled(bool enabled, void *ctx) { /* 客户端订阅/取消订阅 */ } static void nus_received(struct bt_conn *conn, const void *data,                         uint16_t len, void *ctx) {    /* 客户端发过来的数据回这里 */ } struct bt_nus_cb nus_listener = {    .notif_enabled = notif_enabled,    .received       = nus_received, }; /* 主流程 */ bt_nus_cb_register(&nus_listener, NULL); bt_enable(NULL); bt_le_adv_start(BT_LE_ADV_CONN_FAST_1, ad, ARRAY_SIZE(ad),                sd, ARRAY_SIZE(sd)); /* 要发数据时 */ char line[64]; int len = snprintk(line, sizeof(line), "DHT20: 24.32 degC, 58.06 %RH\n"); bt_nus_send(NULL, line, len);   /* NULL = 广播给所有订阅了通知的连接 */

        整个GATT服务声明、UUID注册、notify subscription管理都让Zephyr做掉了,我们只关心 send/receive 两个动作

把 hello_world 升级成 peripheral_th

        工程在 g:/peripheral_th。基线是Zephyr自带的 samples/bluetooth/peripheral_nus,相当于它把 NUS的样板代码已经写好,我们把DHT20那部分加进去。

设备树overlay(沿用上一阶段)

&lpi2c1 {    dht20: dht20@38 {        compatible = "aosong,dht20";        reg = <0x38>;        status = "okay";    }; };

        lpi2c1是板子上Arduino接口里 SDA(D14)/SCL(D15) 对应的那条I2C总线,已经在板级DTS里使能了,所以这里只挂设备。注意DHT20默认I2C地址是0x38,和板上加速度计0x19互不冲突,可以共享总线。

prj.conf关键开关

# 蓝牙 CONFIG_BT=y CONFIG_BT_PERIPHERAL=y CONFIG_BT_DEVICE_NAME="Zephyr TH Sensor" # 关键:把 NUS 打开(Zephyr 自带的服务) CONFIG_BT_ZEPHYR_NUS=y CONFIG_BT_ZEPHYR_NUS_DEFAULT_INSTANCE=y # MTU 拉满,一行 DHT20 数据不会分片 CONFIG_BT_L2CAP_TX_MTU=247 # 传感器 CONFIG_I2C=y CONFIG_SENSOR=y CONFIG_DHT20=y

main.c 的核心循环

        整个固件的核心其实就一个周期任务:每2秒读一次DHT20,把结果同时printk到串口 + 通过 NUS 发出去。

static volatile bool nus_notif_on;   /* 是否有人在订阅通知 */ static void sample_work_handler(struct k_work *work) {    if (!nus_notif_on) {        schedule_next_sample();        return;    }    char line[64];    int len = read_dht20(line, sizeof(line));  /* "DHT20: 24.32 degC, 58.06 %RH\n" */    if (len < 0) { /* I2C 错误处理 */ return; }    printk("%s", line);            /* 串口也打一份,方便调试 */    bt_nus_send(NULL, line, len);  /* 蓝牙推一份 */    schedule_next_sample(); }

两个值得展开的设计点:

(1) 用 K_WORK_DELAYABLE_DEFINE + k_work_schedule 做周期任务

  • 不用 k_timer,因为 handler 里要做 printk(很慢,不能在 ISR 上下文)

  • handler 自己递归 schedule 自己,实现周期

  • 周期 2s 由 SAMPLE_PERIOD = K_SECONDS(2) 控制

(2) nus_notif_on 闸门

  • 只在客户端订阅了 NUS notify 的时候才采样 + 发送

  • 没人连接时整个链路静态,省电、省 I2C 总线

  • 这个标志位由 notif_enabled 回调更新(客户端 subscribe/unsubscribe CCC 时触发)

烧录跑起来后,串口长这样(手机没连时只显示广播开始):

edd4634f-86ad-4c38-ba52-8be44a219da7.png

手机/浏览器连上、订阅 NUS 之后变成:

73b4a597-4101-4a5b-a501-c107114ad834.png

网页侧:纯前端 + Web Bluetooth API

        测试代码:
// 1) 扫描并让用户选设备 const device = await navigator.bluetooth.requestDevice({    filters: [{ services: ['6e400001-...'] }]   // 只列 NUS 设备 }); // 2) 连接 + 发现服务 + 拿 TX characteristic const server = await device.gatt.connect(); const service = await server.getPrimaryService('6e400001-...'); const tx = await service.getCharacteristic('6e400003-...'); // 3) 订阅通知 await tx.startNotifications(); tx.addEventListener('characteristicvaluechanged', (event) => {    const text = new TextDecoder('utf-8').decode(event.target.value);    // 拼行、解析、显示 });

        打开就是这样的布局:


07735170-56e1-47db-b841-9a5a978b9aac.png

        简洁够用,没花哨。数据每 2 秒循环一次:

  1. k_work 周期触发

  2. sensor_sample_fetch 读 DHT20(I2C)

  3. sensor_channel_get 取温度/湿度

  4. snprintk 格式化成字符串

  5. 串口 printk 打印

  6. bt_nus_send 走 BLE notify

  7. 浏览器 characteristicvaluechanged 收到

  8. TextDecoder 解码 + 解析 + 刷新 UI

        阶段成果:

  • 固件:g:/peripheral_th,FLASH 97KB / RAM 35KB

  • Web 页面:g:/peripheral_th/web/index.html

  • 一行命令 west flash(或 JLink 脚本)烧录即用

        到这一步,串口能打的内容,浏览器也能看了——这一句话就是这阶段全部的成果。





关键词: 透传     湿度计     蓝牙     DHT20    

共1条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]