【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步

接下来就一步步实现吧!
第一步:FRDM-MCXW72驱动DHT11,串口输出温湿度数据
| DHT11 | FRDM-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实物实物实物

实物图见以上。
接下来导入官方例程zephyr/samples/sensor/dht_polling

接下来修改2个地方

在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;
}编译下载后,打开串口调试助手,就可以看到温湿度数据输出了。

到这一步说明FRDM-MCXW72驱动DHT11成功,后续我们需要解决将数据通过蓝牙发送到手机app的问题。
第二步:将蓝牙固件(mcxw72_nbu_ble_all_hosted.bin)刷进FRDM-MCXW72
我们知道MCXW72是双核结构,除了常规应用核心,还有一个无线子系统Arm Cortex-M33专用内核。
默认情况下这个核心是没有程序的,因此要实现蓝牙功能的第一步,就是给这个核心刷入蓝牙固件:mcxw72_nbu_ble_all_hosted.bin
这部分内容,在官方的教程中描述的很清楚:
固件放在哪?
<zephyr_repo>/modules/hal/nxp/zephyr/blobs/mcxw72/mcxw72_nbu_ble_all_hosted.bin
用什么工具刷写固件?
官方例程是用LinkFlash ,我电脑上版本是v26.3.123
但打开后,无法识别开发板,只好换个方法。

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

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

点击烧写即可。
这步只是烧写成功,如何验证固件运行成功呢?继续下一步。
第三步:测试蓝牙功能(基于peripheral_hr 心率例程),并在NXP IoT Toolbox手机app上验证
导入例程:zephyr/samples/blutooth/peripheral_hr

其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;
}编译下载运行后。
电脑串口可以看到蓝牙启动成功:

在手机上打开NXP IoT Toolbox APP。
依次点击:Heart Rate——Zephyr Heartrate Sensor就可以看到心率数据及对应曲线了。
当然这个心率数据是模拟出来的。

证明蓝牙固件运行成功后,就可以进入下一步,最关键的一环:蓝牙透传(或者简单理解为蓝牙串口)
第四步:测试蓝牙透传(基于peripheral_nus例程),并在nRF Connect手机app上验证
导入例程:zephyr/samples/blutooth/peripheral_nus

蓝牙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

打开手机app:nRF Connect
依次点击Zephyer——Nordic UART Service——TX Characteristic右边的3个箭头图标,
就可以看到下方的Value:Hello World!
这正是本程序的主功能:向蓝牙客户端发送字符:Hello World!

既然发送字符能成功,说明发送温湿度近在咫尺!
只不过是换个数据类型,一个是字符串,一个是数字而已。
第五步:将第1步(读取温湿度)与第4步(蓝牙透传)的程序相结合,实现蓝牙透传温湿度数据
我这里是将peripheral_nus的功能加到dht_polling中,主要需要修改2个地方:
1.在dht_polling的prj.conf中添加打开蓝牙及打开蓝牙透传的语句
CONFIG_BT=y CONFIG_BT_PERIPHERAL=y CONFIG_BT_ZEPHYR_NUS=y

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));
编译运行后,电脑端串口显示如下:


手机端已经可以正常显示温湿度了。
本次的分享就到这里,非常感谢EEPW举办的这次活动,让我对Zephyr有了全新的连接,也初步算是步入蓝牙的大门,后面还有很多值得学习的地方,再持续进行了!
我要赚赏金
