这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » DIY与开源设计 » 电子DIY » 【e起DIY】低功耗蓝牙温湿度计-成果贴:由浅入深实现蓝牙透传温湿度

共1条 1/1 1 跳转至

【e起DIY】低功耗蓝牙温湿度计-成果贴:由浅入深实现蓝牙透传温湿度

助工
2026-06-12 22:05:55     打赏

【e起DIY】低功耗蓝牙温湿度计-开箱及MCUXpressoIDE点灯

https://forum.eepw.com.cn/thread/400155/1


【e起DIY】低功耗蓝牙温湿度计-最简Zephyr环境搭建及点灯

https://forum.eepw.com.cn/thread/400530/1

在前面的开箱贴、过程贴中,实现了最基础的Zephyr环境搭建。

那么在本篇成果贴,就要由浅入深实现蓝牙温湿度计。

之所以要由浅入深,是因为Zephyr、蓝牙、温湿度、FRDM-MCXW72涉及的功能模块挺多的,必须脚踏实地,一步一个脚印,在实现一个功能的基础上,再实现下一个功能,最后完整实现整个功能。


我将由浅入深实现实现蓝牙温湿度计的步骤分为5步



未命名绘图.drawio (3).png


接下来就一步步实现吧!



第一步:FRDM-MCXW72驱动DHT11,串口输出温湿度数据


DHT11FRDM-MCXW72
G(GND)GND
V(VCC)VCC 3.3V
D(Data数据线)PA19(J1插座编号5)

%3CmxGraphModel%3E%3Croot%3E%3CmxCell%20id%3D%220%22%2F%3E%3CmxCell%20id%3D%221%22%20parent%3D%220%22%2F%3E%3CmxCell%20id%3D%222%22%20edge%3D%221%22%20parent%3D%221%22%20source%3D%223%22%20style%3D%22edgeStyle%3DorthogonalEdgeStyle%3Brounded%3D0%3BorthogonalLoop%3D1%3BjettySize%3Dauto%3Bhtml%3D1%3BverticalAlign%3Dmiddle%3Balign%3Dcenter%3B%22%20target%3D%225%22%20value%3D%22%22%3E%3CmxGeometry%20relative%3D%221%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3CmxCell%20id%3D%223%22%20parent%3D%221%22%20style%3D%22rounded%3D0%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3BfontSize%3D14%3BverticalAlign%3Dmiddle%3Balign%3Dcenter%3B%22%20value%3D%221.Zeohyr%E7%8E%AF%E5%A2%83%E4%B8%8B%EF%BC%8C%E5%AE%9E%E7%8E%B0FRDM-MCXW72%E9%A9%B1%E5%8A%A8DHT11%EF%BC%8C%E4%B8%B2%E5%8F%A3%E8%BE%93%E5%87%BA%E6%B8%A9%E6%B9%BF%E5%BA%A6%E6%95%B0%E6%8D%AE%22%20vertex%3D%221%22%3E%3CmxGeometry%20height%3D%2260%22%20width%3D%22196%22%20x%3D%22294%22%20y%3D%2290%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3CmxCell%20id%3D%224%22%20edge%3D%221%22%20parent%3D%221%22%20source%3D%225%22%20style%3D%22edgeStyle%3DorthogonalEdgeStyle%3Brounded%3D0%3BorthogonalLoop%3D1%3BjettySize%3Dauto%3Bhtml%3D1%3BverticalAlign%3Dmiddle%3Balign%3Dcenter%3B%22%20target%3D%227%22%20value%3D%22%22%3E%3CmxGeometry%20relative%3D%221%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3CmxCell%20id%3D%225%22%20parent%3D%221%22%20style%3D%22rounded%3D0%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3BfontSize%3D14%3BverticalAlign%3Dmiddle%3Balign%3Dcenter%3B%22%20value%3D%222.%E5%B0%86%E8%93%9D%E7%89%99%E5%9B%BA%E4%BB%B6%26lt%3Bdiv%26gt%3B%EF%BC%88%26lt%3Bspan%20style%3D%26quot%3Bbackground-color%3A%20transparent%3B%26quot%3B%26gt%3Bmcx%26lt%3B%2Fspan%26gt%3B%26lt%3Bspan%20style%3D%26quot%3Bbackground-color%3A%20transparent%3B%26quot%3B%26gt%3Bw72_nbu_ble_all_hosted.bin%26lt%3B%2Fspan%26gt%3B%EF%BC%89%E5%88%B7%E8%BF%9BFRDM-MCXW72%26lt%3B%2Fdiv%26gt%3B%22%20vertex%3D%221%22%3E%3CmxGeometry%20height%3D%2260%22%20width%3D%22236%22%20x%3D%22274%22%20y%3D%22190%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3CmxCell%20id%3D%226%22%20edge%3D%221%22%20parent%3D%221%22%20source%3D%227%22%20style%3D%22edgeStyle%3DorthogonalEdgeStyle%3Brounded%3D0%3BorthogonalLoop%3D1%3BjettySize%3Dauto%3Bhtml%3D1%3BverticalAlign%3Dmiddle%3Balign%3Dcenter%3B%22%20target%3D%229%22%20value%3D%22%22%3E%3CmxGeometry%20relative%3D%221%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3CmxCell%20id%3D%227%22%20parent%3D%221%22%20style%3D%22rounded%3D0%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3BfontSize%3D14%3BverticalAlign%3Dmiddle%3Balign%3Dcenter%3B%22%20value%3D%223.%E6%B5%8B%E8%AF%95%E8%93%9D%E7%89%99%E5%8A%9F%E8%83%BD%EF%BC%88%E5%9F%BA%E4%BA%8Eperipheral_hr%20%E5%BF%83%E7%8E%87%E4%BE%8B%E7%A8%8B%EF%BC%89%EF%BC%8C%E5%B9%B6%E5%9C%A8NXP%20IoT%20Toolbox%E6%89%8B%E6%9C%BAapp%26lt%3Bspan%20style%3D%26quot%3Bbackground-color%3A%20transparent%3B%20color%3A%20light-dark(rgb(0%2C%200%2C%200)%2C%20rgb(255%2C%20255%2C%20255))%3B%26quot%3B%26gt%3B%E4%B8%8A%E9%AA%8C%E8%AF%81%26lt%3B%2Fspan%26gt%3B%22%20vertex%3D%221%22%3E%3CmxGeometry%20height%3D%2260%22%20width%3D%22230%22%20x%3D%22277%22%20y%3D%22280%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3CmxCell%20id%3D%228%22%20edge%3D%221%22%20parent%3D%221%22%20source%3D%229%22%20style%3D%22edgeStyle%3DorthogonalEdgeStyle%3Brounded%3D0%3BorthogonalLoop%3D1%3BjettySize%3Dauto%3Bhtml%3D1%3BverticalAlign%3Dmiddle%3Balign%3Dcenter%3B%22%20target%3D%2210%22%20value%3D%22%22%3E%3CmxGeometry%20relative%3D%221%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3CmxCell%20id%3D%229%22%20parent%3D%221%22%20style%3D%22rounded%3D0%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3BverticalAlign%3Dmiddle%3Balign%3Dcenter%3B%22%20value%3D%22%26lt%3Bfont%20style%3D%26quot%3Bfont-size%3A%2014px%3B%26quot%3B%26gt%3B4.%E6%B5%8B%E8%AF%95%E8%93%9D%E7%89%99%E9%80%8F%E4%BC%A0%EF%BC%88%E5%9F%BA%E4%BA%8Eperipheral_nus%E4%BE%8B%E7%A8%8B%EF%BC%89%EF%BC%8C%E5%B9%B6%E5%9C%A8nRF%20Connect%E6%89%8B%E6%9C%BAapp%E4%B8%8A%E9%AA%8C%E8%AF%81%26lt%3B%2Ffont%26gt%3B%22%20vertex%3D%221%22%3E%3CmxGeometry%20height%3D%2260%22%20width%3D%22190%22%20x%3D%22297%22%20y%3D%22370%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3CmxCell%20id%3D%2210%22%20parent%3D%221%22%20style%3D%22rounded%3D0%3BwhiteSpace%3Dwrap%3Bhtml%3D1%3BverticalAlign%3Dmiddle%3Balign%3Dcenter%3B%22%20value%3D%225.%E5%B0%86%E7%AC%AC1%E6%AD%A5%E4%B8%8E%E7%AC%AC4%E6%AD%A5%E7%9A%84%E7%A8%8B%E5%BA%8F%E7%9B%B8%E7%BB%93%E5%90%88%EF%BC%8C%E5%AE%9E%E7%8E%B0%E8%93%9D%E7%89%99%E9%80%8F%E4%BC%A0%E6%B8%A9%E6%B9%BF%E5%BA%A6%E6%95%B0%E6%8D%AE%22%20vertex%3D%221%22%3E%3CmxGeometry%20height%3D%2260%22%20width%3D%22120%22%20x%3D%22332%22%20y%3D%22460%22%20as%3D%22geometry%22%2F%3E%3C%2FmxCell%3E%3C%2Froot%3E%3C%2FmxGraphModel%3E实物实物实物

微信图片_20260612201911_102_65.jpg

实物图见以上。

接下来导入官方例程zephyr/samples/sensor/dht_polling


屏幕截图 2026-06-12 202657.png


接下来修改2个地方

屏幕截图 2026-06-12 202745.png


  1. 在Project Files——boards下新建一个frdm_mcxw72.overlay文件

    在其中添加DHT对应的PA19

/*
 * Copyright (c) 2023 Ian Morris
 *
 * SPDX-License-Identifier: Apache-2.0
 */

/ {  
  aliases {
    /* 别名 dht0 指向 dht11_node,和代码里 DHT_ALIAS(dht0) 对应 */
    dht0 = &dht11_node;
  };

  /* 定义 DHT11 设备节点 */
  dht11_node: dht11 {    
    compatible = "aosong,dht";    /* 匹配 Zephyr 官方DHT驱动兼容字符串 */
    dio-gpios = <&gpioa 19 GPIO_ACTIVE_LOW>;  /* 数据引脚:PA19,低电平有效 */
    status = "okay";  /* 启用该设备 */
  };   
};  

/* 使能 GPIOA 外设 */
&gpioa {    
  status = "okay";    
};


2.    修改main.c

#include <stdio.h>
#include <stdlib.h>

// Zephyr 设备模型头文件
#include <zephyr/device.h>
// Zephyr 通用工具宏定义
#include <zephyr/sys/util_macro.h>
// Zephyr 内核基础头文件
#include <zephyr/kernel.h>
// Zephyr 传感器驱动框架
#include <zephyr/drivers/sensor.h>
// 传感器数据类型定义
#include <zephyr/drivers/sensor_data_types.h>
// RTIO 异步IO框架(Zephyr 高性能异步IO)
#include <zephyr/rtio/rtio.h>
// Q31 定点数打印格式化宏
#include <zephyr/dsp/print_format.h>

/**
 * @brief 宏:拼接别名 dht0 / dht1 ... 对应设备树别名
 * @param i 传感器编号
 */
#define DHT_ALIAS(i) DT_ALIAS(_CONCAT(dht, i))

/**
 * @brief 宏:根据设备树是否存在对应DHT节点,生成设备指针
 * 配合 LISTIFY 批量生成多个传感器设备
 * @param i 传感器编号
 * @param _ 占位参数(LISTIFY 固定传参)
 */
#define DHT_DEVICE(i, _)                                                          \
	IF_ENABLED(DT_NODE_EXISTS(DHT_ALIAS(i)), (DEVICE_DT_GET(DHT_ALIAS(i)),))

/*
 * 定义传感器设备数组
 * LISTIFY(10, ...) 最多支持 10 路 DHT 传感器
 * 设备树中别名 dht0 ~ dht9 会被自动遍历载入
 */
static const struct device *const sensors[] = {LISTIFY(10, DHT_DEVICE, ())};

/**
 * @brief 宏:批量创建传感器RTIO IO设备实例
 * 绑定对应DHT设备,并指定需要读取的通道:温度 + 湿度
 * @param i 传感器编号
 * @param _ 占位参数
 */
#define DHT_IODEV(i, _)                                                            \
	IF_ENABLED(DT_NODE_EXISTS(DHT_ALIAS(i)),                                       \
	(SENSOR_DT_READ_IODEV(_CONCAT(dht_iodev, i), DHT_ALIAS(i),                     \
	{SENSOR_CHAN_AMBIENT_TEMP, 0},                                                \
	{SENSOR_CHAN_HUMIDITY, 0})))

// 批量定义 dht_iodev0 ~ dht_iodev9 共10个RTIO IO设备对象
LISTIFY(10, DHT_IODEV, (;));

/**
 * @brief 宏:获取对应RTIO IO设备地址,无设备则返回NULL
 * @param i 传感器编号
 * @param _ 占位参数
 */
#define DHT_IODEV_REF(i, _)                                                      \
	COND_CODE_1(DT_NODE_EXISTS(DHT_ALIAS(i)), (CONCAT(&dht_iodev, i)), (NULL))

/*
 * RTIO IO设备指针数组
 * 和 sensors 数组一一对应,空设备位置填 NULL
 */
static struct rtio_iodev *dht_iodev[] = { LISTIFY(10, DHT_IODEV_REF, (,)) };

/**
 * 定义 RTIO 上下文实例
 * 参数:名称、提交队列深度、完成队列深度
 */
RTIO_DEFINE(dht_ctx, 1, 1);

int main(void)
{
	int rc;

	/* 遍历所有DHT传感器,检查设备是否就绪 */
	for (size_t i = 0; i < ARRAY_SIZE(sensors); i++) {
		if (!device_is_ready(sensors[i])) {
			printk("sensor: device %s not ready.\n", sensors[i]->name);
			return 0;
		}
	}

	/* 主循环:周期性读取所有DHT温湿度 */
	while (1) {
		for (size_t i = 0; i < ARRAY_SIZE(sensors); i++) {
			struct device *dev = (struct device *) sensors[i];

			// 传感器原始数据接收缓冲区
			uint8_t buf[128];

			/*
			 * 通过 RTIO 异步接口读取传感器数据
			 * 参数:IO设备、RTIO上下文、数据缓冲区、缓冲区大小
			 */
			rc = sensor_read(dht_iodev[i], &dht_ctx, buf, 128);

			if (rc != 0) {
				printk("%s: sensor_read() failed: %d\n", dev->name, rc);
				return rc;
			}

			// 获取传感器解码接口实例
			const struct sensor_decoder_api *decoder;
			rc = sensor_get_decoder(dev, &decoder);
			if (rc != 0) {
				printk("%s: sensor_get_decode() failed: %d\n", dev->name, rc);
				return rc;
			}

			uint32_t temp_fit = 0;                // 温度数据有效计数
			struct sensor_q31_data temp_data = {0}; // 温度Q31定点数数据结构

			// 解码温度通道数据
			decoder->decode(buf,
				(struct sensor_chan_spec) {SENSOR_CHAN_AMBIENT_TEMP, 0},
				&temp_fit, 1, &temp_data);

			uint32_t hum_fit = 0;                 // 湿度数据有效计数
			struct sensor_q31_data hum_data = {0};  // 湿度Q31定点数数据结构

			// 解码湿度通道数据
			decoder->decode(buf,
				(struct sensor_chan_spec) {SENSOR_CHAN_HUMIDITY, 0},
				&hum_fit, 1, &hum_data);

			/*
			 * 格式化打印温湿度
			 * PRIq_arg:Q31定点数格式化宏,保留2位小数
			 */
			printk("%16s: temp is %s%d.%d °C humidity is %s%d.%d %%RH\n", dev->name,
				PRIq_arg(temp_data.readings[0].temperature, 2, temp_data.shift),
				PRIq_arg(hum_data.readings[0].humidity, 2, hum_data.shift));
		}

		// 延时1秒,每秒采集一次
		k_msleep(1000);
	}

	return 0;
}


编译下载后,打开串口调试助手,就可以看到温湿度数据输出了。

屏幕截图 2026-06-11 193248.png


到这一步说明FRDM-MCXW72驱动DHT11成功,后续我们需要解决将数据通过蓝牙发送到手机app的问题。


第二步:将蓝牙固件(mcxw72_nbu_ble_all_hosted.bin)刷进FRDM-MCXW72

我们知道MCXW72是双核结构,除了常规应用核心,还有一个无线子系统Arm Cortex-M33专用内核。

默认情况下这个核心是没有程序的,因此要实现蓝牙功能的第一步,就是给这个核心刷入蓝牙固件:mcxw72_nbu_ble_all_hosted.bin


这部分内容,在官方的教程中描述的很清楚:

FRDM-MCXW72_Zephyr_Lab.pdf


固件放在哪?

<zephyr_repo>/modules/hal/nxp/zephyr/blobs/mcxw72/mcxw72_nbu_ble_all_hosted.bin

用什么工具刷写固件?

官方例程是用LinkFlash ,我电脑上版本是v26.3.123

但打开后,无法识别开发板,只好换个方法。

屏幕截图 2026-06-12 211032.png


我选择使用MCUXpresso IDE v25.6.136中的内嵌工具:GUI Flash Tool(如图,一个黑色芯片图标)

屏幕截图 2026-06-12 211750.png


选择固件所在位置(我电脑上是这个):D:\ZephyrProject\zephyr\modules\hal\nxp\zephyr\blobs\mcxw72\mcxw72_nbu_ble_all_hosted.bin

烧录地址:0x48800000

屏幕截图 2026-06-12 211858.png

点击烧写即可。

这步只是烧写成功,如何验证固件运行成功呢?继续下一步。



第三步:测试蓝牙功能(基于peripheral_hr 心率例程),并在NXP IoT Toolbox手机app上验证

导入例程:zephyr/samples/blutooth/peripheral_hr 

屏幕截图 2026-06-12 212722.png

其main.c源代码不用修改直接编译:

/* main.c - 应用程序入口文件 */

/*
 * Copyright (c) 2024 Nordic Semiconductor ASA
 * Copyright (c) 2015-2016 Intel Corporation
 *
 * SPDX-License-Identifier: Apache-2.0
 */

// Zephyr 设备驱动基础头文件
#include <zephyr/device.h>
// 设备树相关接口
#include <zephyr/devicetree.h>

// 蓝牙协议栈总头文件
#include <zephyr/bluetooth/bluetooth.h>
// 蓝牙HCI层(主机控制器接口)
#include <zephyr/bluetooth/hci.h>
// 蓝牙连接管理
#include <zephyr/bluetooth/conn.h>
// 蓝牙UUID定义
#include <zephyr/bluetooth/uuid.h>
// 蓝牙GATT通用属性协议
#include <zephyr/bluetooth/gatt.h>
// 电池服务 BAS
#include <zephyr/bluetooth/services/bas.h>
// 心率服务 HRS
#include <zephyr/bluetooth/services/hrs.h>

// 心率服务通知使能标志
static bool hrf_ntf_enabled;

// 广播数据结构体:广播包内容
static const struct bt_data ad[] = {
    // 广播标志:通用可发现、不支持传统BR/EDR蓝牙
    BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
    // 广播16位服务UUID:心率、电池、设备信息服务
    BT_DATA_BYTES(BT_DATA_UUID16_ALL,
              BT_UUID_16_ENCODE(BT_UUID_HRS_VAL),
              BT_UUID_16_ENCODE(BT_UUID_BAS_VAL),
              BT_UUID_16_ENCODE(BT_UUID_DIS_VAL)),
// 开启扩展广播时,设备名称放入主广播数据
#if defined(CONFIG_BT_EXT_ADV)
    BT_DATA(BT_DATA_NAME_COMPLETE, CONFIG_BT_DEVICE_NAME, sizeof(CONFIG_BT_DEVICE_NAME) - 1),
#endif /* CONFIG_BT_EXT_ADV */
};

// 传统广播:扫描响应数据(存放设备名称)
#if !defined(CONFIG_BT_EXT_ADV)
static const struct bt_data sd[] = {
    BT_DATA(BT_DATA_NAME_COMPLETE, CONFIG_BT_DEVICE_NAME, sizeof(CONFIG_BT_DEVICE_NAME) - 1),
};
#endif /* !CONFIG_BT_EXT_ADV */

// 连接状态枚举
enum {
    STATE_CONNECTED,    // 已连接状态
    STATE_DISCONNECTED, // 已断开状态

    STATE_BITS,         // 状态位总数
};

// 原子变量:安全记录连接状态(多上下文访问防冲突)
static ATOMIC_DEFINE(state, STATE_BITS);

/**
 * @brief 蓝牙连接成功/失败回调函数
 * @param conn 连接句柄
 * @param err 错误码,0表示连接成功
 */
static void connected(struct bt_conn *conn, uint8_t err)
{
    if (err) {
        // 连接失败,打印错误码与错误描述
        printk("连接失败, 错误码 0x%02x %s\n", err, bt_hci_err_to_str(err));
    } else {
        printk("蓝牙已连接\n");
        // 置位:标记为已连接状态
        (void)atomic_set_bit(state, STATE_CONNECTED);
    }
}

/**
 * @brief 蓝牙断开连接回调
 * @param conn 连接句柄
 * @param reason 断开原因码
 */
static void disconnected(struct bt_conn *conn, uint8_t reason)
{
    printk("蓝牙已断开, 原因码 0x%02x %s\n", reason, bt_hci_err_to_str(reason));
    // 置位:标记为断开状态
    (void)atomic_set_bit(state, STATE_DISCONNECTED);
}

// 注册蓝牙连接事件回调
BT_CONN_CB_DEFINE(conn_callbacks) = {
    .connected = connected,
    .disconnected = disconnected,
};

/**
 * @brief 心率服务通知开关状态变更回调
 * @param enabled true=开启通知,false=关闭通知
 */
static void hrs_ntf_changed(bool enabled)
{
    hrf_ntf_enabled = enabled;

    printk("心率通知状态变更: %s\n",
           enabled ? "已开启" : "已关闭");
}

// 心率服务回调结构体绑定
static struct bt_hrs_cb hrs_cb = {
    .ntf_changed = hrs_ntf_changed,
};

/**
 * @brief 配对取消回调
 * @param conn 连接句柄
 */
static void auth_cancel(struct bt_conn *conn)
{
    char addr[BT_ADDR_LE_STR_LEN];

    // 将蓝牙地址转为字符串格式
    bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));

    printk("配对已取消: %s\n", addr);
}

// 注册蓝牙认证/配对回调
static struct bt_conn_auth_cb auth_cb_display = {
    .cancel = auth_cancel,
};

/**
 * @brief 模拟电池电量并发送通知(每秒递减)
 */
static void bas_notify(void)
{
    // 获取当前电池电量
    uint8_t battery_level = bt_bas_get_battery_level();

    // 电量减1
    battery_level--;

    // 电量为0时重置为100%
    if (!battery_level) {
        battery_level = 100U;
    }

    // 更新并上报电池电量
    bt_bas_set_battery_level(battery_level);
}

/**
 * @brief 模拟心率数据,开启通知时向上位机发送
 */
static void hrs_notify(void)
{
    // 初始心率值 90
    static uint8_t heartrate = 90U;

    // 模拟心率变化:自增,到160后重置为90
    heartrate++;
    if (heartrate == 160U) {
        heartrate = 90U;
    }

    // 通知开启时,发送心率数据
    if (hrf_ntf_enabled) {
        bt_hrs_notify(heartrate);
    }
}

// 启用GPIO驱动时编译LED相关代码
#if defined(CONFIG_GPIO)
// 设备树别名:led0 对应的节点
#define LED0_NODE DT_ALIAS(led0)

// 设备树中LED节点状态正常,则启用LED功能
#if DT_NODE_HAS_STATUS_OKAY(LED0_NODE)
#include <zephyr/drivers/gpio.h>
#define HAS_LED     1
// 从设备树获取LED引脚配置
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);
// LED闪烁间隔 500ms
#define BLINK_ONOFF K_MSEC(500)

// 延时工作队列(用于LED定时翻转)
static struct k_work_delayable blink_work;
static bool                  led_is_on; // LED当前状态

/**
 * @brief LED定时翻转回调函数
 */
static void blink_timeout(struct k_work *work)
{
    // 翻转LED状态
    led_is_on = !led_is_on;
    gpio_pin_set(led.port, led.pin, (int)led_is_on);

    // 重新调度下一次翻转
    k_work_schedule(&blink_work, BLINK_ONOFF);
}

/**
 * @brief LED硬件初始化
 * @return 0成功,非0失败
 */
static int blink_setup(void)
{
    int err;

    printk("检测LED设备...");
    // 检查GPIO设备是否就绪
    if (!gpio_is_ready_dt(&led)) {
        printk("失败.\n");
        return -EIO;
    }
    printk("完成.\n");

    printk("配置GPIO引脚...");
    // 配置引脚为输出模式
    err = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
    if (err) {
        printk("失败.\n");
        return -EIO;
    }
    printk("完成.\n");

    // 初始化延时工作项
    k_work_init_delayable(&blink_work, blink_timeout);

    return 0;
}

/**
 * @brief 启动LED闪烁
 */
static void blink_start(void)
{
    printk("开始LED闪烁\n");
    led_is_on = false;
    gpio_pin_set(led.port, led.pin, (int)led_is_on);
    // 启动定时任务
    k_work_schedule(&blink_work, BLINK_ONOFF);
}

/**
 * @brief 停止LED闪烁,保持常亮
 */
static void blink_stop(void)
{
    struct k_work_sync work_sync;

    printk("停止LED闪烁\n");
    // 同步取消延时工作
    k_work_cancel_delayable_sync(&blink_work, &work_sync);

    // LED保持常亮
    led_is_on = true;
    gpio_pin_set(led.port, led.pin, (int)led_is_on);
}
#endif /* LED0_NODE */
#endif /* CONFIG_GPIO */

/**
 * @brief 主函数,程序入口
 */
int main(void)
{
    int err;

    // 1. 初始化并开启蓝牙协议栈
    err = bt_enable(NULL);
    if (err) {
        printk("蓝牙初始化失败 (错误码 %d)\n", err);
        return 0;
    }

    printk("蓝牙初始化完成\n");

    // 注册蓝牙配对认证回调
    bt_conn_auth_cb_register(&auth_cb_display);

    // 注册心率服务事件回调
    bt_hrs_cb_register(&hrs_cb);

    // 传统蓝牙广播(非扩展广播)
#if !defined(CONFIG_BT_EXT_ADV)
    printk("启动传统广播(可连接+可扫描)\n");
    // 启动快速可连接广播
    err = bt_le_adv_start(BT_LE_ADV_CONN_FAST_1, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd));
    if (err) {
        printk("广播启动失败 (错误码 %d)\n", err);
        return 0;
    }

// 扩展广播(BLE 5.0+ 扩展广播)
#else /* CONFIG_BT_EXT_ADV */
    struct bt_le_adv_param adv_param = {
        .id = BT_ID_DEFAULT,          // 使用默认蓝牙实例
        .sid = 0U,                    // 广播集ID
        .secondary_max_skip = 0U,
        // 开启扩展广播、可连接、编码PHY(远距离模式)
        .options = (BT_LE_ADV_OPT_EXT_ADV | BT_LE_ADV_OPT_CONN | BT_LE_ADV_OPT_CODED),
        .interval_min = BT_GAP_ADV_FAST_INT_MIN_2, // 最小广播间隔
        .interval_max = BT_GAP_ADV_FAST_INT_MAX_2, // 最大广播间隔
        .peer = NULL,
    };
    struct bt_le_ext_adv *adv; // 扩展广播句柄

    printk("创建编码PHY远距离可连接广播集\n");
    err = bt_le_ext_adv_create(&adv_param, NULL, &adv);
    if (err) {
        printk("创建编码PHY广播集失败 (错误码 %d)\n", err);

        // 编码PHY失败则关闭编码模式,使用普通PHY重试
        printk("改用普通PHY创建扩展广播集\n");
        adv_param.options &= ~BT_LE_ADV_OPT_CODED;
        err = bt_le_ext_adv_create(&adv_param, NULL, &adv);
        if (err) {
            printk("创建扩展广播集失败 (错误码 %d)\n", err);
            return 0;
        }
    }

    printk("设置扩展广播数据\n");
    err = bt_le_ext_adv_set_data(adv, ad, ARRAY_SIZE(ad), NULL, 0);
    if (err) {
        printk("设置广播数据失败 (错误码 %d)\n", err);
        return 0;
    }

    printk("启动扩展广播(可连接、不可扫描)\n");
    err = bt_le_ext_adv_start(adv, BT_LE_EXT_ADV_START_DEFAULT);
    if (err) {
        printk("启动扩展广播失败 (错误码 %d)\n", err);
        return 0;
    }
#endif /* CONFIG_BT_EXT_ADV */

    printk("广播启动成功\n");

    // 初始化并启动LED闪烁
#if defined(HAS_LED)
    err = blink_setup();
    if (err) {
        return 0;
    }

    blink_start();
#endif /* HAS_LED */

    // 主循环:周期性上报心率、电池数据
    while (1) {
        // 休眠1秒
        k_sleep(K_SECONDS(1));

        // 模拟并发送心率数据
        hrs_notify();

        // 模拟并发送电池电量数据
        bas_notify();

        // 检测:收到【已连接】状态标记
        if (atomic_test_and_clear_bit(state, STATE_CONNECTED)) {
#if defined(HAS_LED)
            blink_stop(); // 连接成功,停止闪烁,LED常亮
#endif /* HAS_LED */
        }
        // 检测:收到【已断开】状态标记
        else if (atomic_test_and_clear_bit(state, STATE_DISCONNECTED)) {
            // 断开后重新开启广播
#if !defined(CONFIG_BT_EXT_ADV)
            printk("重启传统广播(可连接+可扫描)\n");
            err = bt_le_adv_start(BT_LE_ADV_CONN_FAST_1, ad, ARRAY_SIZE(ad), sd,
                          ARRAY_SIZE(sd));
            if (err) {
                printk("广播重启失败 (错误码 %d)\n", err);
                return 0;
            }

#else /* CONFIG_BT_EXT_ADV */
            printk("重启扩展广播(可连接、不可扫描)\n");
            err = bt_le_ext_adv_start(adv, BT_LE_EXT_ADV_START_DEFAULT);
            if (err) {
                printk("重启扩展广播失败 (错误码 %d)\n", err);
                return 0;
            }
#endif /* CONFIG_BT_EXT_ADV */

#if defined(HAS_LED)
            blink_start(); // 断开连接,恢复LED闪烁
#endif /* HAS_LED */
        }
    }

    return 0;
}


编译下载运行后。

电脑串口可以看到蓝牙启动成功:

屏幕截图 2026-06-12 213115.png


在手机上打开NXP  IoT Toolbox APP。

依次点击:Heart Rate——Zephyr Heartrate Sensor就可以看到心率数据及对应曲线了。

当然这个心率数据是模拟出来的。

微信图片_20260612213253_103_65.jpg


证明蓝牙固件运行成功后,就可以进入下一步,最关键的一环:蓝牙透传(或者简单理解为蓝牙串口)



第四步:测试蓝牙透传(基于peripheral_nus例程),并在nRF Connect手机app上验证

导入例程:zephyr/samples/blutooth/peripheral_nus


屏幕截图 2026-06-12 213708.png

蓝牙NUS服务(Nordic UART Service)

蓝牙NUS服务是由Nordic Semiconductor公司定义的一个自定义服务,专门用于通过BLE实现类似UART的串行通信。


NUS服务:NUS服务是一个自定义的GATT服务,有自己的UUID,通常由Nordic定义和使用。

NUS特征:NUS服务包含两个主要的特征:

RX特征:用于接收从中央设备发送到外围设备的数据。

TX特征:用于从外围设备向中央设备发送数据(通过通知机制)。


导入后main.c不用修改直接编译,程序非常简洁:

/*
 * Copyright (c) 2024 Croxel, Inc.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

// Zephyr 内核基础头文件
#include <zephyr/kernel.h>
// 蓝牙协议栈基础头文件
#include <zephyr/bluetooth/bluetooth.h>
// 蓝牙 NUS 串口透传服务头文件
#include <zephyr/bluetooth/services/nus.h>

// 蓝牙设备名称,取自工程配置项
#define DEVICE_NAME     CONFIG_BT_DEVICE_NAME
// 设备名称长度(去除字符串末尾 '\0')
#define DEVICE_NAME_LEN     (sizeof(DEVICE_NAME) - 1)

/**
 * @brief 蓝牙广播数据(AD 包)
 * 包含蓝牙标志位 + 完整设备名
 */
static const struct bt_data ad[] = {
    // 蓝牙LE标志:通用可发现模式、不支持经典蓝牙(BR/EDR)
    BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
    // 广播携带完整设备名称
    BT_DATA(BT_DATA_NAME_COMPLETE, DEVICE_NAME, DEVICE_NAME_LEN),
};

/**
 * @brief 扫描响应数据(SD 包)
 * 对外广播 NUS 服务 128位 UUID,让扫描端识别串口透传服务
 */
static const struct bt_data sd[] = {
    BT_DATA_BYTES(BT_DATA_UUID128_ALL, BT_UUID_NUS_SRV_VAL),
};

/**
 * @brief NUS 通知开关状态回调函数
 * @param enabled  true: 客户端开启通知  false: 客户端关闭通知
 * @param ctx     自定义上下文(本例未使用)
 */
static void notif_enabled(bool enabled, void *ctx)
{
    // 消除未使用参数警告
    ARG_UNUSED(ctx);

    // 打印通知状态
    printk("%s() - %s\n", __func__, (enabled ? "通知已开启" : "通知已关闭"));
}

/**
 * @brief NUS 数据接收回调函数
 * 客户端向设备发送数据时,此函数被触发
 * @param conn   蓝牙连接句柄
 * @param data   接收到的数据缓冲区
 * @param len    数据长度
 * @param ctx    自定义上下文(本例未使用)
 */
static void received(struct bt_conn *conn, const void *data, uint16_t len, void *ctx)
{
    // 消除未使用参数警告
    ARG_UNUSED(conn);
    ARG_UNUSED(ctx);

    // 打印接收长度 + 原始字符串数据
    printk("%s() - 数据长度: %d, 接收内容: %.*s\n", __func__, len, len, (char *)data);
}

// 定义 NUS 服务回调结构体,绑定上面两个回调函数
struct bt_nus_cb nus_listener = {
    .notif_enabled = notif_enabled,  // 通知状态回调
    .received      = received,       // 数据接收回调
};

/**
 * @brief 主函数入口
 * 功能:初始化蓝牙 + NUS服务 + 开启广播 + 循环定时发送串口透传数据
 */
int main(void)
{
    int err;  // 错误码存储变量

    printk("示例 - 蓝牙从机 NUS 串口透传\n");

    // 注册 NUS 服务回调函数
    err = bt_nus_cb_register(&nus_listener, NULL);
    if (err) {
        printk("注册 NUS 回调失败: %d\n", err);
        return err;
    }

    // 使能蓝牙协议栈
    err = bt_enable(NULL);
    if (err) {
        printk("蓝牙初始化失败: %d\n", err);
        return err;
    }

    // 开启蓝牙广播:快速可连接广播,配置广播数据和扫描响应数据
    err = bt_le_adv_start(BT_LE_ADV_CONN_FAST_1, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd));
    if (err) {
        printk("启动蓝牙广播失败: %d\n", err);
        return err;
    }

    printk("设备初始化完成\n");

    // 主循环
    while (true) {
        // 待发送的字符串
        const char *hello_world = "Hello World!\n";

        // 休眠 3 秒
        k_sleep(K_SECONDS(3));

        // NUS 发送数据,NULL 表示向**所有已连接客户端**发送
        err = bt_nus_send(NULL, hello_world, strlen(hello_world));
        printk("数据发送 - 结果码: %d\n", err);

        // 非临时错误、非未连接状态,则直接退出程序
        if (err < 0 && (err != -EAGAIN) && (err != -ENOTCONN)) {
            return err;
        }
    }

    return 0;
}


编译运行后,

电脑端串口透传成功:

结果码-128是未连接手机端蓝牙app:nRF Connect

结果码0是已连接手机端蓝牙app

屏幕截图 2026-06-12 214333.png


打开手机app:nRF Connect

依次点击Zephyer——Nordic UART Service——TX Characteristic右边的3个箭头图标,

就可以看到下方的Value:Hello World!

这正是本程序的主功能:向蓝牙客户端发送字符:Hello World!

微信图片_20260612214630_104_65.jpg


既然发送字符能成功,说明发送温湿度近在咫尺!

只不过是换个数据类型,一个是字符串,一个是数字而已。


第五步:将第1步(读取温湿度)与第4步(蓝牙透传)的程序相结合,实现蓝牙透传温湿度数据

我这里是将peripheral_nus的功能加到dht_polling中,主要需要修改2个地方:

1.在dht_polling的prj.conf中添加打开蓝牙及打开蓝牙透传的语句

CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_ZEPHYR_NUS=y


屏幕截图 2026-06-12 215321.png


2.将main.c修改,添加蓝牙透传功能,并将温湿度数据发送出去。


#include <stdio.h>
#include <stdlib.h>

// Zephyr 设备模型头文件
#include <zephyr/device.h>
// Zephyr 通用工具宏定义
#include <zephyr/sys/util_macro.h>
// Zephyr 内核基础头文件
#include <zephyr/kernel.h>
// Zephyr 传感器驱动框架
#include <zephyr/drivers/sensor.h>
// 传感器数据类型定义
#include <zephyr/drivers/sensor_data_types.h>
// RTIO 异步IO框架(Zephyr 高性能异步IO)
#include <zephyr/rtio/rtio.h>
// Q31 定点数打印格式化宏
#include <zephyr/dsp/print_format.h>

// 蓝牙协议栈基础头文件
#include <zephyr/bluetooth/bluetooth.h>
// 蓝牙 NUS 串口透传服务头文件
#include <zephyr/bluetooth/services/nus.h>

// 蓝牙设备名称,取自工程配置项
#define DEVICE_NAME     CONFIG_BT_DEVICE_NAME
// 设备名称长度(去除字符串末尾 '\0')
#define DEVICE_NAME_LEN     (sizeof(DEVICE_NAME) - 1)

/**
 * @brief 蓝牙广播数据(AD 包)
 * 包含蓝牙标志位 + 完整设备名
 */
static const struct bt_data ad[] = {
    // 蓝牙LE标志:通用可发现模式、不支持经典蓝牙(BR/EDR)
    BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
    // 广播携带完整设备名称
    BT_DATA(BT_DATA_NAME_COMPLETE, DEVICE_NAME, DEVICE_NAME_LEN),
};

/**
 * @brief 扫描响应数据(SD 包)
 * 对外广播 NUS 服务 128位 UUID,让扫描端识别串口透传服务
 */
static const struct bt_data sd[] = {
    BT_DATA_BYTES(BT_DATA_UUID128_ALL, BT_UUID_NUS_SRV_VAL),
};

/**
 * @brief NUS 通知开关状态回调函数
 * @param enabled  true: 客户端开启通知  false: 客户端关闭通知
 * @param ctx     自定义上下文(本例未使用)
 */
static void notif_enabled(bool enabled, void *ctx)
{
    // 消除未使用参数警告
    ARG_UNUSED(ctx);

    // 打印通知状态
    printk("%s() - %s\n", __func__, (enabled ? "通知已开启" : "通知已关闭"));
}

/**
 * @brief NUS 数据接收回调函数
 * 客户端向设备发送数据时,此函数被触发
 * @param conn   蓝牙连接句柄
 * @param data   接收到的数据缓冲区
 * @param len    数据长度
 * @param ctx    自定义上下文(本例未使用)
 */
static void received(struct bt_conn *conn, const void *data, uint16_t len, void *ctx)
{
    // 消除未使用参数警告
    ARG_UNUSED(conn);
    ARG_UNUSED(ctx);

    // 打印接收长度 + 原始字符串数据
    printk("%s() - 数据长度: %d, 接收内容: %.*s\n", __func__, len, len, (char *)data);
}

// 定义 NUS 服务回调结构体,绑定上面两个回调函数
struct bt_nus_cb nus_listener = {
    .notif_enabled = notif_enabled,  // 通知状态回调
    .received      = received,       // 数据接收回调
};


/**
 * @brief 宏:拼接别名 dht0 / dht1 ... 对应设备树别名
 * @param i 传感器编号
 */
#define DHT_ALIAS(i) DT_ALIAS(_CONCAT(dht, i))

/**
 * @brief 宏:根据设备树是否存在对应DHT节点,生成设备指针
 * 配合 LISTIFY 批量生成多个传感器设备
 * @param i 传感器编号
 * @param _ 占位参数(LISTIFY 固定传参)
 */
#define DHT_DEVICE(i, _)                                                          \
    IF_ENABLED(DT_NODE_EXISTS(DHT_ALIAS(i)), (DEVICE_DT_GET(DHT_ALIAS(i)),))

/*
 * 定义传感器设备数组
 * LISTIFY(10, ...) 最多支持 10 路 DHT 传感器
 * 设备树中别名 dht0 ~ dht9 会被自动遍历载入
 */
static const struct device *const sensors[] = {LISTIFY(10, DHT_DEVICE, ())};

/**
 * @brief 宏:批量创建传感器RTIO IO设备实例
 * 绑定对应DHT设备,并指定需要读取的通道:温度 + 湿度
 * @param i 传感器编号
 * @param _ 占位参数
 */
#define DHT_IODEV(i, _)                                                            \
    IF_ENABLED(DT_NODE_EXISTS(DHT_ALIAS(i)),                                       \
    (SENSOR_DT_READ_IODEV(_CONCAT(dht_iodev, i), DHT_ALIAS(i),                     \
    {SENSOR_CHAN_AMBIENT_TEMP, 0},                                                \
    {SENSOR_CHAN_HUMIDITY, 0})))

// 批量定义 dht_iodev0 ~ dht_iodev9 共10个RTIO IO设备对象
LISTIFY(10, DHT_IODEV, (;));

/**
 * @brief 宏:获取对应RTIO IO设备地址,无设备则返回NULL
 * @param i 传感器编号
 * @param _ 占位参数
 */
#define DHT_IODEV_REF(i, _)                                                      \
    COND_CODE_1(DT_NODE_EXISTS(DHT_ALIAS(i)), (CONCAT(&dht_iodev, i)), (NULL))

/*
 * RTIO IO设备指针数组
 * 和 sensors 数组一一对应,空设备位置填 NULL
 */
static struct rtio_iodev *dht_iodev[] = { LISTIFY(10, DHT_IODEV_REF, (,)) };

/**
 * 定义 RTIO 上下文实例
 * 参数:名称、提交队列深度、完成队列深度
 */
RTIO_DEFINE(dht_ctx, 1, 1);

int main(void)
{
    int err;  // 错误码存储变量

    printk("示例 - 蓝牙从机 NUS 串口透传\n");

    // 注册 NUS 服务回调函数
    err = bt_nus_cb_register(&nus_listener, NULL);
    if (err) {
        printk("注册 NUS 回调失败: %d\n", err);
        return err;
    }

    // 使能蓝牙协议栈
    err = bt_enable(NULL);
    if (err) {
        printk("蓝牙初始化失败: %d\n", err);
        return err;
    }

    // 开启蓝牙广播:快速可连接广播,配置广播数据和扫描响应数据
    err = bt_le_adv_start(BT_LE_ADV_CONN_FAST_1, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd));
    if (err) {
        printk("启动蓝牙广播失败: %d\n", err);
        return err;
    }

    printk("设备初始化完成\n");


    int rc;

    /* 遍历所有DHT传感器,检查设备是否就绪 */
    for (size_t i = 0; i < ARRAY_SIZE(sensors); i++) {
        if (!device_is_ready(sensors[i])) {
            printk("sensor: device %s not ready.\n", sensors[i]->name);
            return 0;
        }
    }

    // 传感器原始数据接收缓冲区
    uint8_t buf[128];

    // 定义足够长度的数组存放格式化字符串,根据实际长度调整大小
    uint8_t buf1[20];


    /* 主循环:周期性读取所有DHT温湿度 */
    while (1) {

        // 休眠 3 秒
        k_sleep(K_SECONDS(3));


        for (size_t i = 0; i < ARRAY_SIZE(sensors); i++) {
            struct device *dev = (struct device *) sensors[i];



            /*
             * 通过 RTIO 异步接口读取传感器数据
             * 参数:IO设备、RTIO上下文、数据缓冲区、缓冲区大小
             */
            rc = sensor_read(dht_iodev[i], &dht_ctx, buf, 128);

            if (rc != 0) {
                printk("%s: sensor_read() failed: %d\n", dev->name, rc);
                return rc;
            }

            // 获取传感器解码接口实例
            const struct sensor_decoder_api *decoder;
            rc = sensor_get_decoder(dev, &decoder);
            if (rc != 0) {
                printk("%s: sensor_get_decode() failed: %d\n", dev->name, rc);
                return rc;
            }

            uint32_t temp_fit = 0;                // 温度数据有效计数
            struct sensor_q31_data temp_data = {0}; // 温度Q31定点数数据结构

            // 解码温度通道数据
            decoder->decode(buf,
                (struct sensor_chan_spec) {SENSOR_CHAN_AMBIENT_TEMP, 0},
                &temp_fit, 1, &temp_data);

            uint32_t hum_fit = 0;                 // 湿度数据有效计数
            struct sensor_q31_data hum_data = {0};  // 湿度Q31定点数数据结构

            // 解码湿度通道数据
            decoder->decode(buf,
                (struct sensor_chan_spec) {SENSOR_CHAN_HUMIDITY, 0},
                &hum_fit, 1, &hum_data);

            /*
             * 格式化打印温湿度
             * PRIq_arg:Q31定点数格式化宏,保留2位小数
             */
            sprintf(buf1,"%s%d.%d °C,%s%d.%d %%RH\n",
                PRIq_arg(temp_data.readings[0].temperature, 2, temp_data.shift),
                PRIq_arg(hum_data.readings[0].humidity, 2, hum_data.shift));

             
            printk("%s", buf1);

        }



        // NUS 发送数据,NULL 表示向**所有已连接客户端**发送
        err = bt_nus_send(NULL, buf1, strlen(buf1));
        printk("数据发送 - 结果码: %d\n", err);

        if (err == -ENOMEM) {
            // 缓冲区满,延迟重试
            printk("发送缓冲区满,延迟重试\n");
            k_sleep(K_MSEC(100));
            continue;
        }
        // 非临时错误、非未连接状态,则直接退出程序
        else if (err < 0 && (err != -EAGAIN) && (err != -ENOTCONN)) {
            return err;
        }


    }

    return 0;
}


其中最关键的就是如下几句:

// 定义足够长度的数组存放格式化字符串,根据实际长度调整大小
uint8_t buf1[20];

//格式化打印温湿度 PRIq_arg:Q31定点数格式化宏,保留2位小数

sprintf(buf1,"%s%d.%d °C,%s%d.%d %%RH\n", 
PRIq_arg(temp_data.readings[0].temperature, 2, temp_data.shift),
PRIq_arg(hum_data.readings[0].humidity, 2, hum_data.shift));	

// NUS 发送数据,NULL 表示向**所有已连接客户端**发送
err = bt_nus_send(NULL, buf1, strlen(buf1));

编译运行后,电脑端串口显示如下:

屏幕截图 2026-06-12 215956.png

微信图片_2026-06-12_220229_109.png

手机端已经可以正常显示温湿度了。

本次的分享就到这里,非常感谢EEPW举办的这次活动,让我对Zephyr有了全新的连接,也初步算是步入蓝牙的大门,后面还有很多值得学习的地方,再持续进行了!





关键词: Zephyr    

共1条 1/1 1 跳转至

回复

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