返回首页

BLE Central 学习笔记

本笔记学习 Zephyr 的 BLE Central(中心设备)角色,实现扫描、连接和 GATT 操作。


示例路径

zephyr/samples/bluetooth/central/          - 基础 Central 示例
zephyr/samples/bluetooth/central_hr/       - 心率传感器 Central
zephyr/samples/bluetooth/central_multilink/ - 多连接 Central

核心概念

BLE 角色对比

角色说明典型设备
Central主动扫描、发起连接手机、网关
Peripheral被扫描、接受连接传感器、穿戴设备

Central 主要功能

  1. 扫描 (Scanning) - 发现周围设备
  2. 连接 (Connecting) - 建立 BLE 连接
  3. GATT 客户端 - 发现和操作远端服务
  4. 数据传输 - 接收通知、读取/写入特征值

基本流程

初始化蓝牙 → 开始扫描 → 发现设备 → 发起连接 → 
发现服务 → 订阅通知 → 数据交互 → 断开连接

代码实现

1. 初始化蓝牙

#include <zephyr/bluetooth/bluetooth.h>

int main(void)
{
    int err;
    
    err = bt_enable(NULL);
    if (err) {
        printk("蓝牙初始化失败: %d\n", err);
        return 0;
    }
    
    printk("蓝牙初始化成功\n");
    
    /* 开始扫描 */
    start_scan();
    
    return 0;
}

2. 扫描设备

#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/hci.h>

static struct bt_conn *default_conn;

/* 扫描回调 */
static void device_found(const bt_addr_le_t *addr, int8_t rssi,
                         uint8_t type, struct net_buf_simple *ad)
{
    char addr_str[BT_ADDR_LE_STR_LEN];
    bt_addr_le_to_str(addr, addr_str, sizeof(addr_str));
    
    printk("发现设备: %s (RSSI %d)\n", addr_str, rssi);
    
    /* 过滤:只连接心率传感器 */
    if (type != BT_GAP_ADV_TYPE_ADV_IND) {
        return;
    }
    
    /* 检查广播数据是否包含心率服务 UUID */
    if (ad_data_contains_hr_service(ad)) {
        /* 停止扫描,发起连接 */
        bt_le_scan_stop();
        
        struct bt_conn_le_create_param create_param = {
            .options = BT_CONN_LE_OPT_NONE,
            .interval = BT_GAP_INIT_CONN_INT_MIN,
            .window = BT_GAP_INIT_CONN_INT_MIN,
        };
        
        int err = bt_conn_le_create(addr, &create_param,
                                    BT_LE_CONN_PARAM_DEFAULT,
                                    &default_conn);
        if (err) {
            printk("连接失败: %d\n", err);
            start_scan();
        }
    }
}

/* 开始扫描 */
static void start_scan(void)
{
    struct bt_le_scan_param scan_param = {
        .type       = BT_LE_SCAN_TYPE_ACTIVE,
        .options    = BT_LE_SCAN_OPT_NONE,
        .interval   = BT_GAP_SCAN_FAST_INTERVAL,
        .window     = BT_GAP_SCAN_FAST_WINDOW,
    };
    
    int err = bt_le_scan_start(&scan_param, device_found);
    if (err) {
        printk("扫描启动失败: %d\n", err);
    } else {
        printk("扫描已启动\n");
    }
}

3. 连接管理

#include <zephyr/bluetooth/conn.h>

/* 连接回调 */
static void connected(struct bt_conn *conn, uint8_t conn_err)
{
    char addr[BT_ADDR_LE_STR_LEN];
    bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
    
    if (conn_err) {
        printk("连接失败: %s (错误 %u)\n", addr, conn_err);
        bt_conn_unref(default_conn);
        default_conn = NULL;
        start_scan();
        return;
    }
    
    printk("已连接: %s\n", addr);
    
    /* 开始 GATT 发现 */
    discover_services(conn);
}

/* 断开连接回调 */
static void disconnected(struct bt_conn *conn, uint8_t reason)
{
    char addr[BT_ADDR_LE_STR_LEN];
    bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
    
    printk("已断开: %s (原因 0x%02x)\n", addr, reason);
    
    if (default_conn == conn) {
        bt_conn_unref(default_conn);
        default_conn = NULL;
    }
    
    /* 重新开始扫描 */
    start_scan();
}

/* 注册连接回调 */
BT_CONN_CB_DEFINE(conn_callbacks) = {
    .connected = connected,
    .disconnected = disconnected,
};

4. GATT 服务发现

#include <zephyr/bluetooth/uuid.h>
#include <zephyr/bluetooth/gatt.h>

static struct bt_uuid_16 discover_uuid = BT_UUID_INIT_16(0);
static struct bt_gatt_discover_params discover_params;
static struct bt_gatt_subscribe_params subscribe_params;

/* 发现回调 */
static uint8_t discover_func(struct bt_conn *conn,
                             const struct bt_gatt_attr *attr,
                             struct bt_gatt_discover_params *params)
{
    int err;
    
    if (!attr) {
        printk("发现完成\n");
        return BT_GATT_ITER_STOP;
    }
    
    printk("[属性] handle %u\n", attr->handle);
    
    /* 发现心率服务 */
    if (!bt_uuid_cmp(discover_params.uuid, BT_UUID_HRS)) {
        /* 下一步:发现心率测量特征 */
        memcpy(&discover_uuid, BT_UUID_HRS_MEASUREMENT, sizeof(discover_uuid));
        discover_params.uuid = &discover_uuid.uuid;
        discover_params.start_handle = attr->handle + 1;
        discover_params.type = BT_GATT_DISCOVER_CHARACTERISTIC;
        
        err = bt_gatt_discover(conn, &discover_params);
        if (err) {
            printk("特征发现失败: %d\n", err);
        }
    }
    /* 发现心率测量特征 */
    else if (!bt_uuid_cmp(discover_params.uuid, BT_UUID_HRS_MEASUREMENT)) {
        /* 记录值句柄 */
        subscribe_params.value_handle = bt_gatt_attr_value_handle(attr);
        
        /* 下一步:发现 CCC 描述符 */
        memcpy(&discover_uuid, BT_UUID_GATT_CCC, sizeof(discover_uuid));
        discover_params.uuid = &discover_uuid.uuid;
        discover_params.start_handle = attr->handle + 2;
        discover_params.type = BT_GATT_DISCOVER_DESCRIPTOR;
        
        err = bt_gatt_discover(conn, &discover_params);
        if (err) {
            printk("描述符发现失败: %d\n", err);
        }
    }
    /* 发现 CCC 描述符 */
    else {
        /* 订阅通知 */
        subscribe_params.ccc_handle = attr->handle;
        subscribe_params.notify = notify_func;
        subscribe_params.value = BT_GATT_CCC_NOTIFY;
        
        err = bt_gatt_subscribe(conn, &subscribe_params);
        if (err && err != -EALREADY) {
            printk("订阅失败: %d\n", err);
        } else {
            printk("[已订阅]\n");
        }
    }
    
    return BT_GATT_ITER_STOP;
}

/* 开始服务发现 */
static void discover_services(struct bt_conn *conn)
{
    memcpy(&discover_uuid, BT_UUID_HRS, sizeof(discover_uuid));
    discover_params.uuid = &discover_uuid.uuid;
    discover_params.func = discover_func;
    discover_params.start_handle = BT_ATT_FIRST_ATTRIBUTE_HANDLE;
    discover_params.end_handle = BT_ATT_LAST_ATTRIBUTE_HANDLE;
    discover_params.type = BT_GATT_DISCOVER_PRIMARY;
    
    int err = bt_gatt_discover(conn, &discover_params);
    if (err) {
        printk("服务发现失败: %d\n", err);
    }
}

5. 订阅通知

/* 通知回调 */
static uint8_t notify_func(struct bt_conn *conn,
                           struct bt_gatt_subscribe_params *params,
                           const void *data, uint16_t length)
{
    if (!data) {
        printk("[取消订阅]\n");
        params->value_handle = 0U;
        return BT_GATT_ITER_STOP;
    }
    
    /* 解析心率数据 */
    const uint8_t *hr_data = data;
    uint8_t flags = hr_data[0];
    uint16_t hr_value;
    
    if (flags & 0x01) {
        /* 16-bit 心率值 */
        hr_value = sys_get_le16(&hr_data[1]);
    } else {
        /* 8-bit 心率值 */
        hr_value = hr_data[1];
    }
    
    printk("[心率] %d bpm\n", hr_value);
    
    return BT_GATT_ITER_CONTINUE;
}

读取和写入特征值

读取特征值

static uint8_t read_func(struct bt_conn *conn, uint8_t err,
                         struct bt_gatt_read_params *params,
                         const void *data, uint16_t length)
{
    if (err) {
        printk("读取失败: %d\n", err);
        return BT_GATT_ITER_STOP;
    }
    
    if (data) {
        printk("读取数据 (%d bytes): ", length);
        for (int i = 0; i < length; i++) {
            printk("%02x ", ((uint8_t *)data)[i]);
        }
        printk("\n");
    }
    
    return BT_GATT_ITER_CONTINUE;
}

static struct bt_gatt_read_params read_params;

void read_characteristic(struct bt_conn *conn, uint16_t handle)
{
    read_params.func = read_func;
    read_params.handle_count = 1;
    read_params.single.handle = handle;
    read_params.single.offset = 0;
    
    bt_gatt_read(conn, &read_params);
}

写入特征值

static void write_func(struct bt_conn *conn, uint8_t err,
                       struct bt_gatt_write_params *params)
{
    if (err) {
        printk("写入失败: %d\n", err);
    } else {
        printk("写入成功\n");
    }
}

static struct bt_gatt_write_params write_params;

void write_characteristic(struct bt_conn *conn, uint16_t handle,
                          const uint8_t *data, uint16_t len)
{
    write_params.func = write_func;
    write_params.handle = handle;
    write_params.offset = 0;
    write_params.data = data;
    write_params.length = len;
    
    bt_gatt_write(conn, &write_params);
}

/* 无响应写入(更快,无确认) */
void write_without_response(struct bt_conn *conn, uint16_t handle,
                            const uint8_t *data, uint16_t len)
{
    bt_gatt_write_without_response(conn, handle, data, len, false);
}

多设备连接

#define MAX_CONNECTIONS 4
static struct bt_conn *connections[MAX_CONNECTIONS];

static int get_free_connection_slot(void)
{
    for (int i = 0; i < MAX_CONNECTIONS; i++) {
        if (connections[i] == NULL) {
            return i;
        }
    }
    return -1;
}

static void connected(struct bt_conn *conn, uint8_t conn_err)
{
    int slot = get_free_connection_slot();
    if (slot >= 0) {
        connections[slot] = bt_conn_ref(conn);
        printk("设备 %d 已连接\n", slot);
    }
}

static void disconnected(struct bt_conn *conn, uint8_t reason)
{
    for (int i = 0; i < MAX_CONNECTIONS; i++) {
        if (connections[i] == conn) {
            bt_conn_unref(connections[i]);
            connections[i] = NULL;
            printk("设备 %d 已断开\n", i);
            break;
        }
    }
}

连接参数更新

static void update_connection_params(struct bt_conn *conn)
{
    struct bt_le_conn_param param = {
        .interval_min = 30,   /* 30 * 1.25ms = 37.5ms */
        .interval_max = 50,   /* 50 * 1.25ms = 62.5ms */
        .latency = 0,
        .timeout = 400,       /* 400 * 10ms = 4s */
    };
    
    int err = bt_conn_le_param_update(conn, &param);
    if (err) {
        printk("参数更新失败: %d\n", err);
    }
}

配置选项

# prj.conf

# 启用蓝牙
CONFIG_BT=y

# 启用 Central 角色
CONFIG_BT_CENTRAL=y

# 启用 GATT 客户端
CONFIG_BT_GATT_CLIENT=y

# 扫描配置
CONFIG_BT_SCAN=y

# 连接数量
CONFIG_BT_MAX_CONN=4

# 日志
CONFIG_BT_DEBUG_LOG=y

nRF54L15 BLE 特性

特性说明
BLE 5.4支持最新蓝牙规范
Long RangeCoded PHY 长距离通信
2Mbps高速物理层
多连接支持 8+ 同时连接
低功耗优化连接功耗

常见 UUID

服务UUID
通用访问0x1800
通用属性0x1801
心率0x180D
血压0x1810
健康温度计0x1809
设备信息0x180A
电池服务0x180F
NUS (Nordic UART)6E400001-B5A3-F393-E0A9-E50E24DCCA9E

错误处理

错误码含义
BT_HCI_ERR_UNKNOWN_CONN_ID连接不存在
BT_HCI_ERR_REMOTE_USER_TERM_CONN远程用户断开
BT_HCI_ERR_LOCALHOST_TERM_CONN本地断开
BT_HCI_ERR_CONN_TIMEOUT连接超时

完整示例:Nordic UART Service 客户端

#include <zephyr/kernel.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/uuid.h>
#include <zephyr/bluetooth/gatt.h>

/* Nordic UART Service UUID */
#define BT_UUID_NUS_VAL \
    BT_UUID_128_ENCODE(0x6E400001, 0xB5A3, 0xF393, 0xE0A9, 0xE50E24DCCA9E)
#define BT_UUID_NUS_RX_VAL \
    BT_UUID_128_ENCODE(0x6E400002, 0xB5A3, 0xF393, 0xE0A9, 0xE50E24DCCA9E)
#define BT_UUID_NUS_TX_VAL \
    BT_UUID_128_ENCODE(0x6E400003, 0xB5A3, 0xF393, 0xE0A9, 0xE50E24DCCA9E)

static struct bt_uuid_128 nus_uuid = BT_UUID_INIT_128(BT_UUID_NUS_VAL);
static struct bt_uuid_128 nus_tx_uuid = BT_UUID_INIT_128(BT_UUID_NUS_TX_VAL);
static struct bt_uuid_128 nus_rx_uuid = BT_UUID_INIT_128(BT_UUID_NUS_RX_VAL);

static uint16_t nus_tx_handle;
static uint16_t nus_rx_handle;
static uint16_t nus_ccc_handle;

/* NUS 数据回调 */
static uint8_t nus_notify_func(struct bt_conn *conn,
                               struct bt_gatt_subscribe_params *params,
                               const void *data, uint16_t length)
{
    if (!data) {
        printk("[NUS] 取消订阅\n");
        return BT_GATT_ITER_STOP;
    }
    
    printk("[NUS] 收到: %.*s\n", length, (char *)data);
    return BT_GATT_ITER_CONTINUE;
}

/* 发送数据到 NUS */
void nus_send(struct bt_conn *conn, const char *data)
{
    bt_gatt_write_without_response(conn, nus_rx_handle,
                                   data, strlen(data), false);
}

int main(void)
{
    bt_enable(NULL);
    start_scan();
    
    /* 主循环 */
    while (1) {
        k_sleep(K_FOREVER);
    }
    
    return 0;
}

相关 API 总结

函数说明
bt_enable()初始化蓝牙
bt_le_scan_start()开始扫描
bt_le_scan_stop()停止扫描
bt_conn_le_create()发起连接
bt_conn_unref()释放连接引用
bt_gatt_discover()发现服务/特征
bt_gatt_subscribe()订阅通知
bt_gatt_read()读取特征值
bt_gatt_write()写入特征值

*小白 🤖 - 2026-03-17*