Skip to content

概念先知道

  • Central(主机):主动扫描、发起连接的一方,相当于“客户端”。
  • 扫描与过滤:例程用 bt_le_scan_start 扫描,回调里解析广播包中的设备名,与 CONFIG_BT_DEVICE_NAME(默认 BTBLE-DEV)匹配才发起连接。
  • 服务发现(GATT Discovery):连接后按“主服务 → 特征 → 描述符”逐级枚举对端的数据结构,拿到特征句柄后才能读写/订阅。
  • 订阅通知:对支持 NOTIFY 的特征写 CCC 描述符(bt_gatt_subscribe),对端一推送就能在回调里收到。

例程功能简介

本页对应博流官方 SDK 的 central 例程(examples/btble/central),演示 BLE **主机(Central)**完整流程:

  • 扫描(bt_le_scan_start)→ 按设备名 BTBLE-DEV 匹配 → 连接(bt_conn_create_le)→ 发现主服务/特征/描述符(bt_gatt_discover)→ 订阅 NOTIFY(bt_gatt_subscribe)→ 每秒写入数据(bt_gatt_write_without_response),断开后自动重新扫描。
  • 配套:与 BLE 从机(peripheral 例程)配对使用效果最佳。

操作步骤

1
进入例程目录

在终端进入 SDK 的 BLE central(主机)例程目录(前提:环境已按快速开始(Linux)Windows搭好):

cd examples/btble/central
2
编译工程

Ai-M61 与 Ai-M62 统一填写 bl616

make CHIP=bl616 BOARD=bl616dk
3
烧录固件

按住 BOOT 键不放、短按 EN/RST 进入下载模式后烧录:

make flash CHIP=bl616 COMX=/dev/ttyUSB0
4
准备一台从机

本页例程会扫描设备名为 BTBLE-DEVCONFIG_BT_DEVICE_NAME)的外设,并连接其测试服务(UUID 07af27a5-...)。在另一块开发板烧录 BLE 从机 例程,或准备一台同名且可连接的外设。

5
运行验证

串口助手波特率 2000000。日志依次打印 Start scan successfully[DEVICE FOUND] ...Connection startingConnected: ... → 服务/特征发现;随后模块每秒向写特征写入 244 字节测试数据,打印 Write success

代码执行流程

例程调用的 API 介绍

bt_le_scan_start(&scan_param, device_found)

开始 BLE 扫描;每次发现设备回调 device_found

参数

  • scan_parambt_le_scan_param(主动扫描/间隔/窗口)
  • device_found:发现回调

返回值:成功返回 0;失败返回负值

bt_conn_create_le(addr, &param)

向指定地址发起 LE 连接。

参数

  • addr:目标 bt_addr_le_t
  • param:连接参数(间隔/延迟/超时)

返回值bt_conn * 连接对象;失败返回 NULL

bt_gatt_discover(conn, &discover_params)

发现对端 GATT 服务/特征/描述符,结果逐项回调。

参数

  • conn:连接对象
  • params:发现参数(类型/句柄范围/回调)

返回值:成功返回 0;失败返回负值

bt_gatt_subscribe(conn, &subscribe_params)

订阅特征的 NOTIFY/INDICATE(写 CCC 描述符),推送数据走 notify 回调。

参数

  • conn:连接对象
  • params:订阅参数(ccc 句柄/值/回调)

返回值:成功返回 0;失败返回负值

bt_gatt_write_without_response(conn, handle, data, len, false)

无应答写:向特征句柄写入数据(Write Without Response),不需要对端确认。

参数

  • conn:连接对象
  • handle:特征值句柄
  • data:数据
  • len:长度(≤MTU-3)
  • false:无需应答

返回值:成功返回 0;失败返回负值

bt_conn_cb_register(&cb)

注册连接状态回调(connected/disconnected)。

参数

  • cbbt_conn_cb 回调集

返回值:无

完整代码

以下为 central 例程完整源码,与官方示例一致,默认折叠,点击展开:

📜 点击展开 central/main.c 完整代码
c
#include "shell.h"
#include <FreeRTOS.h>
#include "task.h"
#include "board.h"

#include "bluetooth.h"
#include "conn.h"
#include "conn_internal.h"
#if defined(BL702) || defined(BL602)
#include "ble_lib_api.h"
#else
#include "btble_lib_api.h"
#include "rfparam_adapter.h"
#endif

#if defined(BL616)
#include "bl616_glb.h"
#elif defined(BL618DG)
#include "bl618dg_glb.h"
#elif defined(BL616CL)
#include "bl616cl_glb.h"
#endif

#include "hci_driver.h"
#include "hci_core.h"
#include "gatt.h"
#include "bt_log.h"

#include "bflb_mtd.h"
#include "easyflash.h"

typedef struct{
    TaskHandle_t handle;
    uint32_t wr_hdl;
}wr_arg_t;

const char* discovery_str[] = {
    "primary",
    "secondary",
    "include",
    "characteristic",
    "descriptor",
    "attribute",
};

static u16_t gatt_mtu_size;
static wr_arg_t wr_arg;
static struct bflb_device_s *uart0;
static struct bt_conn *default_conn;
static struct bt_gatt_discover_params discover_params;
static struct bt_gatt_subscribe_params subscribe_params;
static void ble_start_scan(void);
static void start_discovery(u8_t type);

extern void shell_init_with_task(struct bflb_device_s *shell);

static void ble_write_task(void *arg)
{
    wr_arg_t* args = (wr_arg_t*)arg;
    uint8_t data[244] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09};

    for(int i = 0; i < sizeof(data); ++i){
        data[i] = i & 0xff;
    }

    while(args->handle){
        int err = bt_gatt_write_without_response(default_conn, (uint32_t)arg & 0xffff, data,
            (gatt_mtu_size - 3) > sizeof(data) ? sizeof(data) : (gatt_mtu_size - 3), false);
        if(err){
            printf("Write without response (err %d)\r\n", err);
        }
        printf("Write success\r\n");
        vTaskDelay(pdMS_TO_TICKS(1000));
    }

    printf("Deleted blewrite task\r\n");
    vTaskDelete(NULL);
}

void ble_write_task_set(bool enable, uint16_t handle)
{
    wr_arg.wr_hdl = handle + 0;
    if(!enable){
        if(wr_arg.handle){
            wr_arg.handle = NULL;
        }
        return;
    }

    if(xTaskCreate(ble_write_task, (char*)"blewrite", 256, (void*)&wr_arg, configMAX_PRIORITIES - 5, &wr_arg.handle) == pdPASS){
        printf("Create ble_write_task success, start write\r\n");
    }
    else{
        printf("Create ble_write_task failed\r\n");
    }
}

static u8_t notify_func(struct bt_conn *conn,
			struct bt_gatt_subscribe_params *params,
			const void *data, u16_t length)
{
    if (!params->value) {
        printf("Unsubscribed\r\n");
        params->value_handle = 0U;
        return BT_GATT_ITER_STOP;
    }
    printf("Subscribed\r\n");
    return BT_GATT_ITER_CONTINUE;
}

static void do_subscribe(uint16_t ccc_handle, uint16_t val)
{
    subscribe_params.ccc_handle = ccc_handle;
    subscribe_params.value = val;
    subscribe_params.notify = notify_func;

    int err = bt_gatt_subscribe(default_conn, &subscribe_params);
    if (err) {
        printf("Subscribe failed (err %d)\r\n", err);
    } else {
        printf("Subscribed starting\r\n");
    }
}

static void print_chrc_props(u8_t properties)
{
    printf("Properties: ");

    if (properties & BT_GATT_CHRC_BROADCAST) {
        printf("[bcast]\r\n");
    }
    if (properties & BT_GATT_CHRC_READ) {
        printf("[read]\r\n");
    }
    if (properties & BT_GATT_CHRC_WRITE) {
        printf("[write]\r\n");
    }
    if (properties & BT_GATT_CHRC_WRITE_WITHOUT_RESP) {
        printf("[write w/w rsp]\r\n");
    }
    if (properties & BT_GATT_CHRC_NOTIFY) {
        printf("[notify]\r\n");
    }
    if (properties & BT_GATT_CHRC_INDICATE) {
        printf("[indicate]\r\n");
    }
    if (properties & BT_GATT_CHRC_AUTH) {
        printf("[auth]\r\n");
    }
    if (properties & BT_GATT_CHRC_EXT_PROP) {
        printf("[ext prop]\r\n");
    }
}

static u8_t discover_func(struct bt_conn *conn, const struct bt_gatt_attr *attr, struct bt_gatt_discover_params *params)
{
    struct bt_gatt_service_val *gatt_service;
    struct bt_gatt_chrc *gatt_chrc;
    struct bt_gatt_include *gatt_include;
    char str[37];

    if (!attr) {
        printf( "Discover %s complete\r\n", discovery_str[discover_params.type]);
        if(discover_params.type == BT_GATT_DISCOVER_PRIMARY){
            start_discovery(BT_GATT_DISCOVER_CHARACTERISTIC);
        }
        else if(discover_params.type == BT_GATT_DISCOVER_CHARACTERISTIC){
            start_discovery(BT_GATT_DISCOVER_DESCRIPTOR);
        }
        else{
            memset(params, 0, sizeof(*params));
        }
        return BT_GATT_ITER_STOP;
    }

    switch (params->type) {
    case BT_GATT_DISCOVER_SECONDARY:
    case BT_GATT_DISCOVER_PRIMARY:
        gatt_service = attr->user_data;
        bt_uuid_to_str(gatt_service->uuid, str, sizeof(str));
        printf("Service %s found: start handle %x, end_handle %x\r\n", str, attr->handle, gatt_service->end_handle);
        break;
    case BT_GATT_DISCOVER_CHARACTERISTIC:
        gatt_chrc = attr->user_data;
        bt_uuid_to_str(gatt_chrc->uuid, str, sizeof(str));
        printf("Characteristic %s found: attr->handle %x  chrc->handle %x \r\n", str, attr->handle,gatt_chrc->value_handle);
        print_chrc_props(gatt_chrc->properties);
        if(gatt_chrc->properties & BT_GATT_CHRC_NOTIFY){
            do_subscribe(attr->handle + 2, BT_GATT_CCC_NOTIFY);
        }
        if(!wr_arg.handle){
            ble_write_task_set(true, gatt_chrc->value_handle);
        }
        break;
    case BT_GATT_DISCOVER_INCLUDE:
        gatt_include = attr->user_data;
        bt_uuid_to_str(gatt_include->uuid, str, sizeof(str));
        printf("Include %s found: handle %x, start %x, end %x\r\n", str, attr->handle,
            gatt_include->start_handle, gatt_include->end_handle);
        break;
    default:
        bt_uuid_to_str(attr->uuid, str, sizeof(str));
        printf("Descriptor %s found: handle %x\r\n", str, attr->handle);
        break;
    }

    return BT_GATT_ITER_CONTINUE;
}

static void start_discovery(u8_t type)
{
    int err;

    discover_params.func = discover_func;
    discover_params.start_handle = 0x0001;
    discover_params.end_handle = 0xffff;
    discover_params.type = type;
    discover_params.uuid = NULL;

    err = bt_gatt_discover(default_conn, &discover_params);
    if (err) {
        printf("Discover %s failed (err %d)\r\n", discovery_str[discover_params.type], err);
    } 
}

static void ble_connected(struct bt_conn *conn, u8_t err)
{
    char addr[BT_ADDR_LE_STR_LEN];

    if(conn->type != BT_CONN_TYPE_LE) {
        return;
    }

    bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
    if (err || conn->type != BT_CONN_TYPE_LE) {
        printf("Failed to connect to %s (%u) \r\n", addr, err);
        return;
    }

    printf("Connected: %s \r\n", addr);

    if (!default_conn) {
        default_conn = conn;
    }

    gatt_mtu_size = bt_gatt_get_mtu(default_conn);

    start_discovery(BT_GATT_DISCOVER_PRIMARY);
}

static void ble_disconnected(struct bt_conn *conn, u8_t reason)
{
    char addr[BT_ADDR_LE_STR_LEN];

    if(conn->type != BT_CONN_TYPE_LE){
        return;
    }

    bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
    printf("Disconnected: %s (reason %u) \r\n", addr, reason);

    if (default_conn == conn) {
        default_conn = NULL;
    }

    // restart scan
    ble_start_scan();

    // stop write task
    ble_write_task_set(false, 0);
}

static struct bt_conn_cb ble_conn_callbacks = {
    .connected	=   ble_connected,
    .disconnected	=   ble_disconnected,
};

static bool data_cb(struct bt_data *data, void *user_data)
{
    char *name = user_data;
    u8_t len;

    switch (data->type) {
    case BT_DATA_NAME_SHORTENED:
    case BT_DATA_NAME_COMPLETE:
        len = (data->data_len > 30 - 1)?(30 - 1):(data->data_len);
        memcpy(name, data->data, len);
        return false;		
    default:
        return true;
    }
}
static void device_found(const bt_addr_le_t *addr, s8_t rssi, u8_t evtype,
			 struct net_buf_simple *buf)
{
    char le_addr[BT_ADDR_LE_STR_LEN];
    char name[30];
    int ret;
    struct bt_conn* conn = NULL;
    struct bt_le_conn_param param = {
        .interval_min =  BT_GAP_INIT_CONN_INT_MIN,
        .interval_max =  BT_GAP_INIT_CONN_INT_MAX,
        .latency = 0,
        .timeout = 400,
    };

    (void)memset(name, 0, sizeof(name));
    bt_data_parse(buf, data_cb, name);

    if(!strcmp(name,  CONFIG_BT_DEVICE_NAME)){
        bt_addr_le_to_str(addr, le_addr, sizeof(le_addr));
        printf("[DEVICE FOUND]: %s, AD evt type %u, RSSI %i %s \r\n",le_addr, evtype, rssi, name);
        // Stop scan 
        ret = bt_le_scan_stop();
        if(ret){
            printf("Stop scan fail err = %d\r\n", ret);
            return;
        }

        // Do connect
        conn = bt_conn_create_le(addr, /*BT_LE_CONN_PARAM_DEFAULT*/&param);

        if(!conn){
            printf("Connection failed\r\n");
        }else{
            printf("Connection starting\r\n");
        }
    }
}

static void ble_start_scan(void)
{
    struct bt_le_scan_param scan_param;
    int err;

    memset(&scan_param, 0, sizeof(scan_param));

    scan_param.type = BT_HCI_LE_SCAN_ACTIVE;
    scan_param.filter_dup = BT_HCI_LE_SCAN_FILTER_DUP_DISABLE;
    scan_param.interval = BT_GAP_SCAN_FAST_INTERVAL;
    scan_param.window = BT_GAP_SCAN_FAST_WINDOW;

    err = bt_le_scan_start(&scan_param, device_found);

    if(err){
        printf("Failed to start scan (err %d) \r\n", err);
    }else{
        printf("Start scan successfully \r\n");
    }
}

static void bt_gatt_mtu_changed_cb(struct bt_conn *conn, int mtu)
{
    if(conn == default_conn){
        gatt_mtu_size = mtu;
    }
}

static void ble_notification_all_cb_t(struct bt_conn *conn, u16_t handle,const void *data, u16_t length)
{
    printf("handle %x, %s\r\n", handle, bt_hex(data, length));
}

void bt_enable_cb(int err)
{
    if (!err) {
        bt_addr_le_t bt_addr;
        bt_get_local_public_address(&bt_addr);
        printf("BD_ADDR:(MSB)%02x:%02x:%02x:%02x:%02x:%02x(LSB) \r\n",
            bt_addr.a.val[5], bt_addr.a.val[4], bt_addr.a.val[3], bt_addr.a.val[2], bt_addr.a.val[1], bt_addr.a.val[0]);

        bt_conn_cb_register(&ble_conn_callbacks);
        #if defined(BFLB_BLE_MTU_CHANGE_CB)
        bt_gatt_register_mtu_callback(bt_gatt_mtu_changed_cb);
        #endif

        // start advertising
        ble_start_scan();

        // Register notification callback
        bt_gatt_register_notification_callback(ble_notification_all_cb_t);
    }
}

static TaskHandle_t app_start_handle;

static void app_start_task(void *pvParameters)
{
    // Initialize BLE controller
    #if defined(BL702) || defined(BL602)
    ble_controller_init(configMAX_PRIORITIES - 1);
    #else
    btble_controller_init(configMAX_PRIORITIES - 1);
    #endif
    // Initialize BLE Host stack
    hci_driver_init();
    bt_enable(bt_enable_cb);

    vTaskDelete(NULL);
}

int main(void)
{
    board_init();

    configASSERT((configMAX_PRIORITIES > 4));

    uart0 = bflb_device_get_by_name("uart0");
    shell_init_with_task(uart0);

    bflb_mtd_init();
    /* ble stack need easyflash kv */
    easyflash_init();

    /* Init rf */
    if (0 != rfparam_init(0, NULL, 0)) {
        printf("PHY RF init failed!\r\n");
        return 0;
    }

    #if defined(BL618DG)
    #if defined(CONFIG_BTBLE_USE_STANDALONE_PATH)
    printf("cmd_set_btble_standalone\r\n");
    extern void cmd_set_btble_standalone(int argc, char **argv);
    cmd_set_btble_standalone(0, 0);
    #else
    printf("cmd_set_btble_combo\r\n");
    extern void cmd_set_btble_combo(int argc, char **argv);
    cmd_set_btble_combo(0, 0);
    #endif
    #endif

    xTaskCreate(app_start_task, (char *)"app_start", 1024, NULL, configMAX_PRIORITIES - 2, &app_start_handle);

    vTaskStartScheduler();

    while (1) {
    }
}

FAQ

一直扫描但连不上?

确认从机烧录的是 peripheral 例程且设备名匹配:central 通过 CONFIG_BT_DEVICE_NAME 匹配(默认 BTBLE-DEV),可在 defconfig 修改保持一致;距离放近一点。

连接上但没有服务输出?

bt_gatt_discover 需要连接建立后立刻执行;若从机没有注册任何 GATT 服务,会发现不到特征。从机例程的 ble_tp_svc 提供了读写/通知特征。

`Write without response` 失败?

数据长度不能超过 MTU-3(默认 20 字节);例程已按 gatt_mtu_size 裁剪。若从机特征不支持 WRITE_WITHOUT_RESP,改用 bt_gatt_write

断开后会自动重连吗?

例程断开后自动重新扫描(ble_start_scan),发现设备后再次连接,形成“断开→扫描→连接”循环。

遇到问题?

如有其他问题,请到统一的提问与讨论区:Ai-Thinker Discussions

Released under the MIT License. Build Time 2026-09-11 14:52:23