Skip to content

概述

从机(Slave) 是 BLE 通信中被动的一方:一直广播(像大喇叭喊话:「我在这里,快来连我!」)等待主机或手机来连接。本教程让 Ai-WB2 扮演从机角色,注册一个名为 UUID1 的服务(功能模块),手机或 BLE 主机 连接上来后,双方可以互相收发数据,实现「串口 ⇄ 蓝牙」双向透传。这是手环、传感器、智能门锁等设备最常见的形态。

用大白话讲:从机就像站在柜台后的店员,一直举着「欢迎光临」的招牌(广播)等顾客(主机/手机)上门。顾客来了(连接成功),双方就按GATT 规矩(蓝牙设备通信的协议:服务里装着特征值,数据都从特征值走)递纸条:顾客把纸条放进「收件箱」(特征值),店员从串口看到;店员把数据塞进「发件箱」,顾客那边能收到通知。本教程的「收件箱/发件箱」就是官方工程注册的 UUID1 服务。

本教程基于安信可官方 SDKAi-Thinker-Open/Ai-Thinker-WB2,版本 release_bl_iot_sdk_1.6.40)的官方示例 applications/bluetooth/ble_slave 编写,代码可在本地 SDK 中直接找到。

🎯本页目标让 Ai-WB2 作为 BLE 从机注册 UUID1 透传服务,手机 App 或 BLE 主机可连接并双向收发数据。
🧰前置条件① Ai-WB2 开发板一块 ② 已按 [SDK 安装](../sdk/sdk_intro) 完成开发环境搭建 ③ 手机(装 nRF Connect 等蓝牙调试 App)或另一块烧了 [BLE 主机](./ble_master) 示例的开发板。
🔗相关章节GATT 服务/特征值概念见 [BLE 简介](./ble_intro);主动连接它的主机示例见 [BLE 主机](./ble_master)。

进入示例工程

打开终端,进入官方 ble_slave 示例工程目录:

cd ~/Ai-Thinker-WB2/applications/bluetooth/ble_slave

说明:cd 是「进入目录」命令,这里进入从机示例工程目录;后续的 make 编译、make flash 烧录命令都必须先在这个目录里执行。

📌 注意:该工程由多个源文件组成main.cble_interface.c),GATT 服务的注册、特征值的读写回调都在 ble_interface.c 里实现。本文只展示 main.c,其余文件见官方工程。

工程目录结构说明:

文件 作用
ble_slave/main.c 主程序:启动从机、串口转发逻辑,本教程主要看的文件
ble_slave/ble_interface.c/.h 从机服务实现:注册 UUID1 服务、UUID1_SendNotify 通知接口、apps_ble_start 启动入口
认识从机的工作方式

打开 ble_slave/main.c,本示例无需修改任何参数。它只做了两件事:

  1. apps_ble_start():启动从机——注册 UUID1 服务(收件箱/发件箱)并开始广播(在 ble_interface.c 中实现)。
  2. TaskUart 任务:不停轮询串口,串口收到什么字符,就通过 UUID1_SendNotify 原样发给已连接的客户端。

💡 想知道「服务/特征值」具体长什么样?用手机 nRF Connect 连上本设备后,进入 GATT 页面就能看到 UUID1 服务下的特征值——那就是蓝牙世界的「规矩」在设备上的实体。

编写代码

打开 ble_slave/main.c,本步完整代码已移至文末,见:

📜 完整代码 — 位于本页「完整代码」章节,默认折叠,点击展开,与官方示例(applications/bluetooth/ble_slave/ble_slave/main.c)完全一致。

代码要点:

API 作用
apps_ble_start() 启动从机:注册 UUID1 服务并开广播(实现在 ble_interface.c),不调用手机就搜不到本设备
UUID1_SendNotify(len, data) 把串口数据以「通知」方式发给已连接的客户端,App 需开启通知才能收到
hosal_uart_receive(&uart_dev_log, ...) 轮询读串口,收到数据就转发到蓝牙,这是「串口→蓝牙」方向
xTaskCreate(TaskUart, ...) 让串口监听单独跑一个任务,不建任务就没人帮你盯着串口输入
uart_dev_log 结构体 串口参数(uart0、TX=16、RX=7、波特率 115200),串口助手波特率要和它一致
编译工程

在工程目录执行编译:

make -j8

说明:make 是「编译」命令,把代码变成开发板能运行的固件(烧进开发板的程序);-j8 表示用 8 个 CPU 核并行编译,更快。

编译成功后生成固件 build_out/ble_slave.bin

⚠️ 若提示 riscv64-unknown-elf-gcc: command not found,说明工具链权限未配置,先执行 cd toolchain/riscv/Linux && . chmod755.sh 再重新编译。

烧录固件

开发板保持 USB 连接,确认串口设备号后执行烧录:

make flash p=/dev/ttyUSB0 b=921600

说明:make flash 是「烧录」命令,把编译好的固件写进开发板芯片。p= 后面是串口设备号(要改成你电脑上实际的串口,可用 ls /dev/ttyUSB* 查看),b=921600 是烧录波特率(串口传数据的速度),保持默认即可。

⏳ 烧录过程中按提示长按开发板 EN 键进入下载模式,等待进度条完成即烧录成功。

运行验证

烧录完成后开发板自动重启运行,打开手机 nRF Connect 扫描并连接本设备(也可用另一块烧了 BLE 主机 示例的开发板连接)。

先看串口日志——注意!本示例把串口配置为波特率 115200,串口助手要选 115200:

[OS] proc_main_entry task...

用手机验证双向透传:

  1. 手机 → 开发板:nRF Connect 连接设备后进入 GATT 页面,找到 UUID1 服务的特征值,向它写入任意字符(如 hello),开发板串口应打印出来。
  2. 开发板 → 手机:在开发板串口输入字符,手机 App 应收到该数据的通知(在特征值上开启「Notify」订阅后才会收到)。

串口打印 [OS] proc_main_entry task... 且手机能连接并双向收发数据即为成功;如果手机搜不到设备或连不上,见文末 FAQ;如果开发板→手机的方向收不到数据,多半是没在 App 里开启通知订阅。

💡 进阶验证:配合 BLE 主机 教程,一块板烧主机、一块板烧从机,串口互发字符即完成「板对板透传」,这是很多产品的真实工作方式。

代码执行流程

例程从启动到运行的完整流程如下(图中的循环箭头表示反复执行):


本文 API 汇总

说明:apps_ble_startUUID1_SendNotify该官方工程自定义的接口(实现在 ble_interface.c);hosal_uart_*bl_sys_init 为 SDK 平台层接口。

bl_sys_init

初始化系统时钟、外设等基础资源,使用 BLE 前必须先调用(本示例在 main() 里调用)。

返回值:成功返回 0;失败返回负值错误码

hosal_uart_init(uart)

按结构体配置初始化串口(本例 uart_id=0、TX=GPIO16、RX=GPIO7、波特率 115200)。

参数

  • uarthosal_uart_dev_t 结构体指针,必填,含串口号、引脚、波特率等配置

返回值:成功返回 0;失败返回负值错误码

hosal_uart_receive(uart, data, expect_size)

轮询接收串口数据(本例循环调用,作为「串口 → 蓝牙通知」的入口)。

参数

  • uarthosal_uart_dev_t 结构体指针
  • data:接收缓冲区指针,必填
  • expect_size:期望接收的字节数(缓冲区大小)

返回值:成功返回实际接收的字节数(大于 0);失败返回负值错误码

apps_ble_start

启动从机:注册 UUID1 服务(含特征值)并开始广播(工程自定义 API,实现在 ble_interface.c)。

返回值:无

UUID1_SendNotify(len, data)

把数据通过「通知」发送给已连接的客户端(工程自定义 API,实现在 ble_interface.c)。

参数

  • len:数据长度(uint16_t),必填
  • data:数据指针,必填

返回值:无

vTaskDelay(ms)

让当前任务挂起指定毫秒数,期间让出 CPU 给其他任务。

参数

  • ms:延时毫秒数,本示例 50(降低串口轮询频率)

返回值:无

xTaskCreate(task, name, stack, param, prio, handle)

创建任务并加入就绪队列,由调度器按优先级调度执行。

参数

  • task:任务入口函数指针,形如 void task(void *arg),必填
  • name:任务名称字符串(调试用),本示例 "TaskUart" / "main_entry"
  • stack:任务栈大小(单位:字),本示例 2048(串口任务)/ 1024(主任务)
  • param:传给入口函数的参数指针,无参传 NULL
  • prio:任务优先级,可选值:0(最低)~19(最高,SDK 配置),本示例 15
  • handle:任务句柄输出指针,不需要可传 NULL

返回值:成功返回 pdPASS;失败返回 pdFAIL(如内存不足)


完整代码

以下为 ble_slave/main.c 完整源码,与官方示例(applications/bluetooth/ble_slave/ble_slave/main.c)完全一致。该工程由 main.cble_interface.c 组成,本文只展示 main.c,GATT 服务注册与回调逻辑见官方工程的 ble_interface.c

📜 点击展开 main.c 完整代码
c

#include <FreeRTOS.h>
#include <task.h>
#include <timers.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <aos/kernel.h>
#include <aos/yloop.h>
#include <event_device.h>
#include <bl_sys.h>
#include "ble_lib_api.h"
#include <hosal_uart.h>
#include <blog.h>
#include <hosal_uart.h>
#include <hosal_timer.h>
#include "gatt.h"
#include "bluetooth.h"
#include "ble_interface.h"

hosal_uart_dev_t uart_dev_log = {
    .config = {
        .uart_id = 0,
        .tx_pin = 16, // TXD GPIO
        .rx_pin = 7,  // RXD GPIO
        .cts_pin = 255,
        .rts_pin = 255,
        .baud_rate = 115200,
        .data_width = HOSAL_DATA_WIDTH_8BIT,
        .parity = HOSAL_NO_PARITY,
        .stop_bits = HOSAL_STOP_BITS_1,
        .mode = HOSAL_UART_MODE_POLL,
    },
};

void TaskUart(void *param)
{
    uint8_t data[32] = {0};
    int ret;

    hosal_uart_init(&uart_dev_log);

    while (1)
    {
        /* Uart receive poll */
        ret = hosal_uart_receive(&uart_dev_log, data, sizeof(data));
        if (ret > 0)
        {
            UUID1_SendNotify(strlen((char *)data), data);
        }
        vTaskDelay(50);
    }
}

static void proc_main_entry(void *pvParameters)
{
    apps_ble_start();
    vTaskDelete(NULL);
}

void main()
{
    bl_sys_init();
    puts("[OS] proc_main_entry task...\r\n");
    xTaskCreate(TaskUart, "TaskUart", 2048, NULL, 15, NULL);
    xTaskCreate(proc_main_entry, (char *)"main_entry", 1024, NULL, 15, NULL);
}

常见问题与踩坑提示

⚠️ 手机搜不到设备 / 连接不上
原因:广播没启动、距离太远,或手机蓝牙/定位权限没开
解决:确认串口打印了 [OS] proc_main_entry task...;Android 手机扫描 BLE 必须开启「定位权限」;手机靠近开发板(1 米内)再扫;App 有缓存就先杀掉重开

⚠️ 手机连上了,写入数据开发板串口却没反应
原因:写入的特征值不对,或串口助手波特率选错(本示例是 115200)
解决:在 nRF Connect 的 GATT 页面确认写的是 UUID1 服务下的特征值;把串口助手波特率改成 115200;确认写入后特征值处出现「写入成功」提示

⚠️ 开发板串口发数据,手机收不到
原因:没有开启特征值的「通知(Notify)」订阅——通知不是默认打开的
解决:在 nRF Connect 特征值页面点击「开启通知/订阅」(Enable Notifications)后再从串口发数据

⚠️ 编译报错找不到 ble_interface.h 或其它头文件
原因:在错误的目录执行了编译命令,或工程文件不完整
解决:确认在 ~/Ai-Thinker-WB2/applications/bluetooth/ble_slave 目录内执行 make -j8;工程需要的 ble_interface.c/.h 都在该目录下,不要只拷贝 main.c

⚠️ 烧录一直卡住等待,进度条不动
原因:未进入下载模式,或数据线只能充电不能传数据
解决:烧录时按提示长按 EN 键进入下载模式;换一根能传数据的 Type-C 数据线后重试

⚠️ 串口找不到设备 / 打不开
原因:USB 转串口驱动未装、权限不足,或设备号不对
解决:Linux 用 lsusb/dmesg 查看设备,权限不足执行 sudo usermod -aG dialout $USER 后重新登录;Windows 到设备管理器查看 COM 口

运行自检

串口打印 [OS] proc_main_entry task...;手机 nRF Connect 能搜到并连接本设备,向 UUID1 服务特征值写入字符后开发板串口打印出来,开启「通知」后串口输入的字符能在 App 收到,即从机透传验证通过。

遇到问题?

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

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