概述
从机(Slave) 是 BLE 通信中被动的一方:一直广播(像大喇叭喊话:「我在这里,快来连我!」)等待主机或手机来连接。本教程让 Ai-WB2 扮演从机角色,注册一个名为 UUID1 的服务(功能模块),手机或 BLE 主机 连接上来后,双方可以互相收发数据,实现「串口 ⇄ 蓝牙」双向透传。这是手环、传感器、智能门锁等设备最常见的形态。
用大白话讲:从机就像站在柜台后的店员,一直举着「欢迎光临」的招牌(广播)等顾客(主机/手机)上门。顾客来了(连接成功),双方就按GATT 规矩(蓝牙设备通信的协议:服务里装着特征值,数据都从特征值走)递纸条:顾客把纸条放进「收件箱」(特征值),店员从串口看到;店员把数据塞进「发件箱」,顾客那边能收到通知。本教程的「收件箱/发件箱」就是官方工程注册的 UUID1 服务。
本教程基于安信可官方 SDK(Ai-Thinker-Open/Ai-Thinker-WB2,版本
release_bl_iot_sdk_1.6.40)的官方示例applications/bluetooth/ble_slave编写,代码可在本地 SDK 中直接找到。
打开终端,进入官方 ble_slave 示例工程目录:
cd ~/Ai-Thinker-WB2/applications/bluetooth/ble_slave
说明:
cd是「进入目录」命令,这里进入从机示例工程目录;后续的make编译、make flash烧录命令都必须先在这个目录里执行。
📌 注意:该工程由多个源文件组成(
main.c和ble_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,本示例无需修改任何参数。它只做了两件事:
apps_ble_start():启动从机——注册 UUID1 服务(收件箱/发件箱)并开始广播(在ble_interface.c中实现)。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...
用手机验证双向透传:
- 手机 → 开发板:nRF Connect 连接设备后进入 GATT 页面,找到 UUID1 服务的特征值,向它写入任意字符(如
hello),开发板串口应打印出来。 - 开发板 → 手机:在开发板串口输入字符,手机 App 应收到该数据的通知(在特征值上开启「Notify」订阅后才会收到)。
串口打印 [OS] proc_main_entry task... 且手机能连接并双向收发数据即为成功;如果手机搜不到设备或连不上,见文末 FAQ;如果开发板→手机的方向收不到数据,多半是没在 App 里开启通知订阅。
💡 进阶验证:配合 BLE 主机 教程,一块板烧主机、一块板烧从机,串口互发字符即完成「板对板透传」,这是很多产品的真实工作方式。
代码执行流程
例程从启动到运行的完整流程如下(图中的循环箭头表示反复执行):
本文 API 汇总
说明:
apps_ble_start、UUID1_SendNotify是该官方工程自定义的接口(实现在ble_interface.c);hosal_uart_*、bl_sys_init为 SDK 平台层接口。
hosal_uart_init(uart)
按结构体配置初始化串口(本例 uart_id=0、TX=GPIO16、RX=GPIO7、波特率 115200)。
参数:
uart:hosal_uart_dev_t结构体指针,必填,含串口号、引脚、波特率等配置
返回值:成功返回 0;失败返回负值错误码
hosal_uart_receive(uart, data, expect_size)
轮询接收串口数据(本例循环调用,作为「串口 → 蓝牙通知」的入口)。
参数:
uart:hosal_uart_dev_t结构体指针data:接收缓冲区指针,必填expect_size:期望接收的字节数(缓冲区大小)
返回值:成功返回实际接收的字节数(大于 0);失败返回负值错误码
UUID1_SendNotify(len, data)
把数据通过「通知」发送给已连接的客户端(工程自定义 API,实现在 ble_interface.c)。
参数:
len:数据长度(uint16_t),必填data:数据指针,必填
返回值:无
xTaskCreate(task, name, stack, param, prio, handle)
创建任务并加入就绪队列,由调度器按优先级调度执行。
参数:
task:任务入口函数指针,形如void task(void *arg),必填name:任务名称字符串(调试用),本示例"TaskUart"/"main_entry"stack:任务栈大小(单位:字),本示例2048(串口任务)/1024(主任务)param:传给入口函数的参数指针,无参传NULLprio:任务优先级,可选值:0(最低)~19(最高,SDK 配置),本示例15handle:任务句柄输出指针,不需要可传NULL
返回值:成功返回 pdPASS;失败返回 pdFAIL(如内存不足)
完整代码
以下为 ble_slave/main.c 完整源码,与官方示例(applications/bluetooth/ble_slave/ble_slave/main.c)完全一致。该工程由 main.c 和 ble_interface.c 组成,本文只展示 main.c,GATT 服务注册与回调逻辑见官方工程的 ble_interface.c:
📜 点击展开 main.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

