Skip to content

概述

主机(Master) 是 BLE 通信中主动的一方:负责扫描(寻找)周围广播的设备,发现目标后发起连接,连上后双方即可互相传数据。本教程让 Ai-WB2 扮演主机角色,按 MAC 地址(蓝牙设备的「门牌号」)定向连接一台从机,并把串口(UART,电脑和开发板之间逐位传数据的通道)收到的数据通过 BLE 透传给从机,实现「串口 ⇄ 蓝牙」双向透传。适合做「手机/网关读取传感器」这类应用。

用大白话讲:主机就像拿着手机「找朋友」的你:先竖起耳朵听周围谁在广播(喊话:「我在这里!」),听到目标后主动上前「加好友」(连接),加完好友就能互相递纸条(传数据)。本教程里的开发板就是那个「找朋友」的人,它按门牌号(MAC 地址)精确找到约定的从机,加完好友后,你在电脑上往它串口里打的字,会原样递到从机手上。

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

🎯本页目标让 Ai-WB2 作为 BLE 主机,按 MAC 地址自动连接从机并开启自动重连,实现串口与蓝牙的双向透传。
🧰前置条件① 两块 Ai-WB2 开发板(本机做主机,另一块烧 [BLE 从机](./ble_slave) 示例当从机)② Type-C 数据线 ×2 ③ 已按 [SDK 安装](../sdk/sdk_intro) 完成开发环境搭建。
🔗相关章节主机/从机角色概念见 [BLE 简介](./ble_intro);从机示例见 [BLE 从机](./ble_slave)。

进入示例工程

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

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

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

📌 注意:该工程由多个源文件组成main.cble_interface.cble_common_api.cble_central_api.c),BLE 扫描/连接的实现分散在 ble_common_api.cble_central_api.c 中。本文只展示 main.c,其余文件见官方工程。

工程目录结构说明:

文件 作用
ble_master/main.c 主程序:初始化、扫描连接、串口透传逻辑,本教程主要看的文件
ble_master/ble_interface.c/.h 工程自定义接口层(供 main.c 调用的 BLE 函数声明)
ble_master/ble_common_api.c 通用 BLE API 实现(含 axk_HalBleInit
ble_master/ble_central_api.c 主机角色 API 实现(扫描/连接/发送,含连接状态变量)
修改从机 MAC 地址

打开 ble_master/main.c,找到开头的 MAC 地址数组(占位符 = 代码里先写好的示例内容,需要换成你自己的真实值):

/*填写从机mac地址*/
static uint8_t slave_mac[6] = {0x88, 0x88, 0x88, 0x88, 0x88, 0x88};

{0x88, 0x88, 0x88, 0x88, 0x88, 0x88} 改成你的从机开发板的实际 MAC 地址。怎么查从机的 MAC:用手机蓝牙 App(如 nRF Connect)扫描,从机广播的设备信息里就能看到它的 MAC(形如 88:88:88:88:88:88)。

💡 第 3 个参数 BLE_MASTER_AUTOCONN_ENABLE 表示自动重连:连接断开后主机会自动再去找从机。MAC 填错或从机没开机,就会一直打印 no ble connect!

编写代码

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

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

代码要点:

API 作用
axk_HalBleInit() 初始化 BLE 主机功能(定义在 ble_common_api.c),不初始化后面扫描/连接都用不了
axk_HalBleCentralStartScan() 开始扫描周围广播的设备,不扫描就发现不了从机
axk_HalBleCentralConnect(slave_mac, NULL, 1) 按 MAC 地址定向连接从机并开自动重连,MAC 填错永远连不上
bleuart_connect_status == 1 连接状态标志(1=已连上),没连上时发数据会打印 no ble connect!
axk_HalBleCentralTTWrite(ret, data) 把串口收到的数据经 BLE 发给从机,返回负值对应不同错误原因
hosal_uart_receive(&ble_uart_dev, ...) 轮询读串口,就是你往串口助手打字时数据进来的入口
编译工程

在工程目录执行编译:

make -j8

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

编译成功后生成固件 build_out/ble_master.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 键进入下载模式,等待进度条完成即烧录成功。两块开发板记得分别接线、分别烧录。

运行验证

主机与从机两块板都上电运行(从机请先烧好 BLE 从机 示例)。给主机接两个串口助手窗口:一个看日志,一个发数据。

打开串口助手(波特率 115200——本示例在 hosal_uart_dev_t 配置里把串口设成了 115200),主机开机打印:

MY BLE MASTER

连接成功后,在主机串口输入任意字符,从机串口应原样收到;反过来从机串口发字符,主机串口也能收到——这就是「双向透传」。

主机串口打印 MY BLE MASTER 且与从机建立连接(不再打印 no ble connect!)即为成功;如果一直打印 no ble connect!,说明没连上从机(MAC 地址/从机是否开机/距离问题),见文末 FAQ。

💡 进阶验证:连接状态下拔掉从机电源再恢复,主机会自动重连(BLE_MASTER_AUTOCONN_ENABLE 生效),重连后透传自动恢复,无需重启主机。

代码执行流程

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


本文 API 汇总

说明:axk_HalBle* 系列是该官方工程自定义的接口(定义在 ble_common_api.c / ble_central_api.c,通过 extern 声明后供 main.c 调用),不属于 SDK 平台层;hosal_uart_*bl_sys_init 为 SDK 平台层接口。

bl_sys_init

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

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

hosal_uart_init(uart)

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

参数

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

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

hosal_uart_send(uart, txbuf, size)

通过串口发送一段数据(本例用它打印日志和错误提示)。

参数

  • uarthosal_uart_dev_t 结构体指针
  • txbuf:待发送数据指针,必填
  • size:发送字节数

返回值:成功返回发送的字节数(大于 0);失败返回负值错误码

hosal_uart_receive(uart, data, expect_size)

轮询接收串口数据(本例循环调用,作为「电脑输入 → 蓝牙透传」的入口)。

参数

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

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

axk_HalBleInit

初始化 BLE 主机功能(工程自定义 API,定义在 ble_common_api.c),所有主机操作的第一步。

返回值uint8_t,成功返回 0(工程内约定);失败返回非 0

axk_HalBleCentralStartScan

开始扫描周围广播的蓝牙设备(工程自定义 API,定义在 ble_central_api.c)。

返回值uint8_t,成功返回 0;失败返回非 0

axk_HalBleCentralConnect(mac, uuid, autoConnect)

按 MAC 地址(或 UUID)定向连接从机(工程自定义 API,定义在 ble_central_api.c)。

参数

  • mac:目标从机 MAC 地址数组(6 字节),必填(本示例 slave_mac
  • uuid:目标服务 UUID,本示例传 NULL(按 MAC 精确匹配)
  • autoConnect:是否自动重连,可选值:BLE_MASTER_AUTOCONN_ENABLE(1,启用)/ 0(关闭)

返回值uint8_t,成功返回 0;失败返回非 0

axk_HalBleCentralTTWrite(len, data)

把数据通过 BLE 发送给已连接的从机(工程自定义 API,定义在 ble_central_api.c)。

参数

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

返回值int>= 0 发送成功;-1 连接状态异常、-2 数据长度错误、-3 数据为空、其他负值为发送失败

vTaskDelay(ms)

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

参数

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

返回值:无

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

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

参数

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

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


完整代码

以下为 ble_master/main.c 完整源码,与官方示例(applications/bluetooth/ble_master/ble_master/main.c)完全一致。该工程由多个源文件组成(main.cble_interface.cble_common_api.cble_central_api.c),本文只展示 main.c,其余文件见官方工程:

📜 点击展开 main.c 完整代码
c
/*
 * Copyright (c) 2020 Bouffalolab.
 *
 * This file is part of
 *     *** Bouffalolab Software Dev Kit ***
 *      (see www.bouffalolab.com).
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *   1. Redistributions of source code must retain the above copyright notice,
 *      this list of conditions and the following disclaimer.
 *   2. Redistributions in binary form must reproduce the above copyright notice,
 *      this list of conditions and the following disclaimer in the documentation
 *      and/or other materials provided with the distribution.
 *   3. Neither the name of Bouffalo Lab nor the names of its contributors
 *      may be used to endorse or promote products derived from this software
 *      without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
#include <FreeRTOS.h>
#include <task.h>
#include <timers.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>

#include <vfs.h>
#include <aos/kernel.h>
#include <aos/yloop.h>
#include <event_device.h>
#include <cli.h>

#include <lwip/tcpip.h>
#include <lwip/sockets.h>
#include <lwip/netdb.h>
#include <lwip/tcp.h>
#include <lwip/err.h>
#include <netutils/netutils.h>

#include <bl602_glb.h>
#include <bl602_hbn.h>

#include <bl_uart.h>
#include <bl_chip.h>
#include <bl_sec.h>
#include <bl_cks.h>
#include <bl_irq.h>
#include <bl_dma.h>
#include <bl_adc.h>
#include <bl_timer.h>
#include <bl_gpio_cli.h>
#include <bl_wdt_cli.h>
// #include <hal_uart.h>
#include <hal_sys.h>
#include <hal_gpio.h>
#include <hal_boot2.h>
#include <hal_board.h>
#include <looprt.h>
#include <loopset.h>
#include <bl_sys_time.h>
#include <bl_sys_ota.h>
#include <bl_romfs.h>
#include <fdt.h>
#include <bl_sys.h>
#include <bl_timer.h>
#include <easyflash.h>
#include <bl60x_fw_api.h>
#include <utils_log.h>
#include <libfdt.h>
#include <blog.h>
// #include <ble_cli_cmds.h>
#include <hosal_uart.h>
#include "ble_interface.h"

/*填写从机mac地址*/
static uint8_t slave_mac[6] = {0x88, 0x88, 0x88, 0x88, 0x88, 0x88};

#define OS_CMP(s1, s2) (strcmp(s1, s2) == 0)
extern unsigned char bleuart_connect_status;
extern uint8_t axk_HalBleInit();
extern uint8_t axk_HalBleCentralStartScan(void);
extern uint8_t axk_HalBleCentralConnect(uint8_t *mac, uint8_t *uuid, uint8_t autoConnect);
extern int axk_HalBleCentralTTWrite(uint16_t len, uint8_t *data);
hosal_uart_dev_t ble_uart_dev = {
    .config = {
        .uart_id = 1,
        .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 bleuart_printf(char *buf)
{
  hosal_uart_send(&ble_uart_dev, buf, strlen(buf));
}

void ble_user_init(void)
{
  axk_HalBleInit();
  axk_HalBleCentralStartScan();                                          // 扫描周围的蓝牙设备
  axk_HalBleCentralConnect(slave_mac, NULL, BLE_MASTER_AUTOCONN_ENABLE); // 通过MAC地址和UUID指定连接从机,开启自动重连
}

int str2hex(char *pbuf, int len)
{
  int i = 0;
  for (i = 0; i < len; i++)
  {
    if (((pbuf[i] >= '0') && (pbuf[i] <= '9')) || ((pbuf[i] >= 'A') && (pbuf[i] <= 'F')) || ((pbuf[i] >= 'a') && (pbuf[i] <= 'f')))
    {
      if ((pbuf[i] >= '0') && (pbuf[i] <= '9'))
      {
        pbuf[i] -= '0';
      }
      else if (((pbuf[i] >= 'A') && (pbuf[i] <= 'F')))
      {
        pbuf[i] -= 'A';
        pbuf[i] += 0x0A;
      }
      else
      {
        pbuf[i] -= 'a';
        pbuf[i] += 0x0A;
      }

      if (i % 2)
      {
        pbuf[i / 2] = (pbuf[i - 1] << 4) | pbuf[i];
      }
    }
    else
    {
      return -1;
    }
  }

  return 0;
}

static void ble_loop_proc(void *pvParameters)
{
  char data[250];
  int ret, rep;
  ble_user_init();
  while (1)
  {
    /* Uart receive poll */
    ret = hosal_uart_receive(&ble_uart_dev, data, sizeof(data));
    if (ret > 0)
    { /* Uart send poll */
      // hosal_uart_send(&ble_uart_dev, data, ret);
      if (bleuart_connect_status == 1)
      {
        rep = axk_HalBleCentralTTWrite(ret, (uint8_t *)data);
        if (rep >= 0)
        {
          // hosal_uart_send(&ble_uart_dev, data, ret);//
        }
        else if (rep == -1)
        {
          bleuart_printf("ble status error!\r\n");
        }
        else if (rep == -2)
        {
          bleuart_printf("ble data len error!\r\n");
        }
        else if (rep == -3)
        {
          bleuart_printf("ble data null!\r\n");
        }
        else
        {
          bleuart_printf("ble send fail!\r\n");
        }
      }
      else
        bleuart_printf("no ble connect!\r\n");
    }
    vTaskDelay(100);
  }
  vTaskDelete(NULL);
}

static void uart_init(void)
{
  hosal_uart_init(&ble_uart_dev);
}

void main()
{
  /*Init UART In the first place*/
  // bl_uart_init(0, 16, 7, 255, 255, 115200);//2 * 1000 * 1000
  uart_init();
  bleuart_printf("MY BLE MASTER\r\n");
  bl_sys_init(); // if use ble ,must init
  xTaskCreate(ble_loop_proc, "ble master", 1024, NULL, 15, NULL);
}

常见问题与踩坑提示

⚠️ 一直打印 no ble connect!
原因:MAC 地址没改成从机实际的、从机没开机/没在广播、或两块板离得太远
解决:用手机蓝牙 App 查看从机实际 MAC,替换 slave_mac 后重新编译烧录;确认从机已烧录并上电;两块板放近一些(1 米内)测试

⚠️ 串口看不到 MY BLE MASTER 日志
原因:串口助手波特率选错——本示例串口配置为 115200,不是常用的 921600
解决:把串口助手波特率改成 115200;确认串口号选对(ls /dev/ttyUSB* 查看)

⚠️ 连上了,但主机发数据从机收不到
原因:从机烧的不是 ble_slave 示例、或从机服务 UUID 不匹配
解决:确认从机板烧录的是 BLE 从机 示例(串口打印 [OS] proc_main_entry task...);两台设备距离太远时数据可能丢包,靠近测试

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

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

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

运行自检

主机串口打印 MY BLE MASTER,与从机建立连接后不再打印 no ble connect!,且主机串口输入的字符能在从机串口收到、从机串口输入的字符能在主机串口收到,即双向透传验证通过。

遇到问题?

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

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