背景知识

整个蓝牙协议栈内容太多了,以后如果有时间单独写几篇相关文章梳理一下,本文将直接上手写程序。

利用 Bluetooth LE 点亮 LED 灯

最终目标:利用智能手机远程控制(开启及关闭) EFR32BG22 IO 口连接的 LED 灯。

上述 EFR32BG22 将运行在 Standalone mode。

预先准备

手机上需要安装蓝牙调试程序,常用的调试软件有:

  1. nRF Connect
  2. LightBlue
  3. EFR Connect

bluetooth tools

三个软件的功能完全重合。每个都试了一下,感觉在 iOS 上做的最完善的还是 Nordic Semiconductor 的 nRF Connect。

这何尝不是一种 NTR?

创建 Project

首先,基于 Bluetooth - SoC Empty 模板创建一个空蓝牙项目,命名为 bluetooth_led_blink:

new project wizard

随后,安装 Services - I/O: stream USART 并创建 instance;安装 Application - Utility: Log,卸载 Platform - RAIL Utility, PTI,完成后就可以写应用相关代码了。

为什么要卸载 PTI Components?

Packet Trace Interface 是一个用于调试 network activity 的组件,我们暂时用不到这个功能,当然也可以不卸载,但不卸载的话会,编译后在 Simplicity IDE 底部的 Problems 中出现一条报错消息:


Errors (1 item): 
ERROR: DOUT is not defined - DOUT is required when PTI enabled, please select pin for DOUT

pti problems

虽然不影响最后生成 .hex 文件,但是显然让人看着心烦,不如直接删了。

修改 GATT

在正式编写程序之前,还需要修改 Generic Attribute Profile (GATT),

打开 bluetooth_led_blink.slcp,切换到 Configuration Tools Tab,随后打开 Bluetooth GATT Configurator:

configuration tools

修改 Device Name 为 LED Controller,后面我们使用手机搜索蓝牙设备时,看到的名称就是这里填写的 Device Name。

Bluetooth GATT Configurator

Device Information 里都是一些“无关紧要的”设备信息,包括制造商名称,产品序列号,版本号等,这里就不修改了,不影响点灯。

接下来就是最重要的部分,在 GATT 中增加一个 Service,该 Service 包含一个控制 LED 开关的 Characteristics 字段:

led status

这里 Characteristic Name 其实无所谓,重要的是 ID (将影响我们的程序中的变量名),Value Settings(将决定手机和芯片通信时传输的数据类型) 和 Properties (将决定其他设备是否有权限读取和修改该字段,换句话说,手机是否有权限控制灯的打开和关闭)。另外,记下这里的 UUID。

完成修改后保存该文件。

修改程序

最后就是程序部分了,先来简单过一遍初始文件模板。

// app.c

#include "em_common.h"
#include "app_assert.h"
#include "sl_bluetooth.h"
#include "app.h"

// The advertising set handle allocated from Bluetooth stack.
static uint8_t advertising_set_handle = 0xff;

/**************************************************************************//**
 * Application Init.
 *****************************************************************************/
SL_WEAK void app_init(void)
{
  /////////////////////////////////////////////////////////////////////////////
  // Put your additional application init code here!                         //
  // This is called once during start-up.                                    //
  /////////////////////////////////////////////////////////////////////////////
}

/**************************************************************************//**
 * Application Process Action.
 *****************************************************************************/
SL_WEAK void app_process_action(void)
{
  /////////////////////////////////////////////////////////////////////////////
  // Put your additional application code here!                              //
  // This is called infinitely.                                              //
  // Do not call blocking functions from here!                               //
  /////////////////////////////////////////////////////////////////////////////
}

/**************************************************************************//**
 * Bluetooth stack event handler.
 * This overrides the dummy weak implementation.
 *
 * @param[in] evt Event coming from the Bluetooth stack.
 *****************************************************************************/
void sl_bt_on_event(sl_bt_msg_t *evt)
{
  sl_status_t sc;

  switch (SL_BT_MSG_ID(evt->header)) {
    // -------------------------------
    // This event indicates the device has started and the radio is ready.
    // Do not call any stack command before receiving this boot event!
    case sl_bt_evt_system_boot_id:
      // Create an advertising set.
      sc = sl_bt_advertiser_create_set(&advertising_set_handle);
      app_assert_status(sc);

      // Generate data for advertising
      sc = sl_bt_legacy_advertiser_generate_data(advertising_set_handle,
                                                 sl_bt_advertiser_general_discoverable);
      app_assert_status(sc);

      // Set advertising interval to 100ms.
      sc = sl_bt_advertiser_set_timing(
        advertising_set_handle,
        160, // min. adv. interval (milliseconds * 1.6)
        160, // max. adv. interval (milliseconds * 1.6)
        0,   // adv. duration
        0);  // max. num. adv. events
      app_assert_status(sc);
      // Start advertising and enable connections.
      sc = sl_bt_legacy_advertiser_start(advertising_set_handle,
                                         sl_bt_advertiser_connectable_scannable);
      app_assert_status(sc);
      break;

    // -------------------------------
    // This event indicates that a new connection was opened.
    case sl_bt_evt_connection_opened_id:
      break;

    // -------------------------------
    // This event indicates that a connection was closed.
    case sl_bt_evt_connection_closed_id:
      // Generate data for advertising
      sc = sl_bt_legacy_advertiser_generate_data(advertising_set_handle,
                                                 sl_bt_advertiser_general_discoverable);
      app_assert_status(sc);

      // Restart advertising after client has disconnected.
      sc = sl_bt_legacy_advertiser_start(advertising_set_handle,
                                         sl_bt_advertiser_connectable_scannable);
      app_assert_status(sc);
      break;

    ///////////////////////////////////////////////////////////////////////////
    // Add additional event handlers here as your application requires!      //
    ///////////////////////////////////////////////////////////////////////////

    // -------------------------------
    // Default event handler.
    default:
      break;
  }
}

其实文件里就是三个函数:

SL_WEAK void app_init(void)

SL_WEAK void app_process_action(void)

void sl_bt_on_event(sl_bt_msg_t *evt)

app_init() 和 app_process_action() 都是老熟人了,至于它们为什么被 SL_WEAK 关键字修饰,这里暂且先不用管,只要把我们应用代码放到这两个函数里就行。重点是最后一个函数 sl_bt_on_event(),我们删掉一部分内容,简单看一下这个函数的结构:

/**************************************************************************//**
 * Bluetooth stack event handler.
 * This overrides the dummy weak implementation.
 *
 * @param[in] evt Event coming from the Bluetooth stack.
 *****************************************************************************/
void sl_bt_on_event(sl_bt_msg_t *evt)
{
  sl_status_t sc;

  switch (SL_BT_MSG_ID(evt->header)) {
    // -------------------------------
    // This event indicates the device has started and the radio is ready.
    // Do not call any stack command before receiving this boot event!
    case sl_bt_evt_system_boot_id:
      break;

    // -------------------------------
    // This event indicates that a new connection was opened.
    case sl_bt_evt_connection_opened_id:
      break;

    // -------------------------------
    // This event indicates that a connection was closed.
    case sl_bt_evt_connection_closed_id:
      break;

    ///////////////////////////////////////////////////////////////////////////
    // Add additional event handlers here as your application requires!      //
    ///////////////////////////////////////////////////////////////////////////

    // -------------------------------
    // Default event handler.
    default:
      break;
  }

该函数内部就是一个大的 switch...case... 循环,具体执行哪些代码分支,将由传入的参数 evt 决定。当 EFR32 的蓝牙状态发生改变时(如有新的蓝牙设备连接到 EFR32,或者已连接的设备尝试读取或写入数据),sl_bt_on_event() 函数将被自动调用,用户可以在这里增加一个 switch...case... 分支,通过传入的 evt 参数来判断到底是哪一个 bluetooth event 触发了本次调用,并在对应的 case 语句里进行相应的处理。

其实就是 UI 编程里非常常用的事件驱动模型嘛。

看到这里自然会产生疑问,我怎么知道有哪些 event 会触发该函数的调用?实际上官方文档里写的非常详细,具体可以查阅文档的 API Reference 部分。

回到我们的目的上来,在 app.c 中进行如下修改:

首先,增加对以下几个头文件的引用:

// 为了控制 GPIO:
#include "em_cmu.h"
#include "em_gpio.h"

// 为了引用我们在 GATT 中创建的所有内容:
#include "gatt_db.h"

// 为了更方便的打印日志,当然也可以用 printf() 代替:
#include "app_log.h"

配置 GPIO 初始化:

SL_WEAK void app_init(void)
{
  CMU_ClockEnable(cmuClock_GPIO, true);

  // LED 灯连接在 PB02 上, 高电平点亮
  GPIO_PinModeSet(gpioPortB, 2, gpioModePushPull, 0);
  GPIO_PinOutSet(gpioPortB, 2);
}

蓝牙事件处理,这里主要针对三个事件进行处理:

  • sl_bt_evt_connection_opened_id, 蓝牙连接打开事件
  • sl_bt_evt_connection_closed_id, 蓝牙连接关闭事件
  • sl_bt_evt_gatt_server_attribute_value_id, GATT 修改事件,在这里切换 LED 状态
void sl_bt_on_event(sl_bt_msg_t *evt)
{
  sl_status_t sc;

  switch (SL_BT_MSG_ID(evt->header)) {
    // -------------------------------
    // This event indicates the device has started and the radio is ready.
    // Do not call any stack command before receiving this boot event!
    case sl_bt_evt_system_boot_id:
      // Create an advertising set.
      sc = sl_bt_advertiser_create_set(&advertising_set_handle);
      app_assert_status(sc);

      // Generate data for advertising
      sc = sl_bt_legacy_advertiser_generate_data(advertising_set_handle,
                                                 sl_bt_advertiser_general_discoverable);
      app_assert_status(sc);

      // Set advertising interval to 100ms.
      sc = sl_bt_advertiser_set_timing(
        advertising_set_handle,
        160, // min. adv. interval (milliseconds * 1.6)
        160, // max. adv. interval (milliseconds * 1.6)
        0,   // adv. duration
        0);  // max. num. adv. events
      app_assert_status(sc);
      // Start advertising and enable connections.
      sc = sl_bt_legacy_advertiser_start(advertising_set_handle,
                                         sl_bt_advertiser_connectable_scannable);
      app_assert_status(sc);
      break;

    // -------------------------------
    // This event indicates that a new connection was opened.
    case sl_bt_evt_connection_opened_id:
      app_log_info("Connection opened.\n");
      break;

    // -------------------------------
    // This event indicates that a connection was closed.
    case sl_bt_evt_connection_closed_id:
      app_log_info("Connection closed.\n");

      // Generate data for advertising
      sc = sl_bt_legacy_advertiser_generate_data(advertising_set_handle,
                                                 sl_bt_advertiser_general_discoverable);
      app_assert_status(sc);

      // Restart advertising after client has disconnected.
      sc = sl_bt_legacy_advertiser_start(advertising_set_handle,
                                         sl_bt_advertiser_connectable_scannable);
      app_assert_status(sc);
      break;

    // -------------------------------
    // This event indicates that the value of an attribute in the local GATT
    // database was changed by a remote GATT client.
    case sl_bt_evt_gatt_server_attribute_value_id:
      // The value of the gattdb_led_status characteristic was changed.
      if (gattdb_led_status == evt->data.evt_gatt_server_attribute_value.attribute) {
        uint8_t data_recv;
        size_t data_recv_len;

        // Read characteristic value.
        sc = sl_bt_gatt_server_read_attribute_value(gattdb_led_status,
                                                    0,
                                                    sizeof(data_recv),
                                                    &data_recv_len,
                                                    &data_recv);
        (void)data_recv_len;
        app_log_status_error(sc);

        if (sc != SL_STATUS_OK) {
          break;
        }

        // Toggle LED.
        if (data_recv == 0x00) {
          GPIO_PinOutClear(gpioPortB, 2);;
          app_log_info("LED off.\n");
        } else if (data_recv == 0x01) {
          GPIO_PinOutSet(gpioPortB, 2);;
          app_log_info("LED on.\n");
        } else {
          app_log_error("Invalid attribute value: 0x%02x\n", (int)data_recv);
        }
      }
      break;

    ///////////////////////////////////////////////////////////////////////////
    // Add additional event handlers here as your application requires!      //
    ///////////////////////////////////////////////////////////////////////////

    // -------------------------------
    // Default event handler.
    default:
      break;
  }

到这里编译并烧录其实就可以了,但是我们注意 build console 中的提示信息,会发现存在两条警告:

../sl_gatt_service_device_information.c: In function 'sl_gatt_service_device_information_on_event':
../sl_gatt_service_device_information.c:63:2: warning: #warning "Could not set Model Number String." [-Wcpp]
   63 | #warning "Could not set Model Number String."
      |  ^~~~~~~
../sl_gatt_service_device_information.c:77:2: warning: #warning "Could not set Hardware Revision String." [-Wcpp]
   77 | #warning "Could not set Hardware Revision String."
      |  ^~~~~~~

为此,打开 sl_gatt_service_device_information.c 文件,定位到报错的 line 63 和 77,可以看到:

#if defined(gattdb_model_number_string) && defined(SL_BOARD_NAME)
      sc = sl_bt_gatt_server_write_attribute_value(gattdb_model_number_string,
                                                   0,
                                                   GATTDB_MODEL_NUMBER_STRING_LEN,
                                                   (uint8_t *)SL_BOARD_NAME);
      app_assert_status(sc);
#else
#warning "Could not set Model Number String."
// Check the presence of this characteristic and the ID reference in the GATT
// Configurator. If using a custom board, remove this section and use the
// GATT Configurator to set the value manually.
#endif

      // Hardware Revision String
#if defined(gattdb_hardware_revision_string) && defined(SL_BOARD_REV)
      sc = sl_bt_gatt_server_write_attribute_value(gattdb_hardware_revision_string,
                                                   0,
                                                   GATTDB_HARDWARE_REVISION_STRING_LEN,
                                                   (uint8_t *)SL_BOARD_REV);
      app_assert_status(sc);
#else
#warning "Could not set Hardware Revision String."
// Check the presence of this characteristic and the ID reference in the GATT
// Configurator. If using a custom board, remove this section and use the
// GATT Configurator to set the value manually.
#endif

注释里写的很清楚,如果我们用的不是官方开发板,把这两条 #warning 直接注释掉就行。

注释后重新编译:

Build Finished. 0 errors, 0 warnings. (took 8s.351ms)

0 error 0 warning

2025.06.25 简单说几句,烧录程序时需要特别注意起始地址:

直接烧录,会发现 MCU 仿佛被冻结,代码没有被正确运行,这是因为默认从 Flash 基地址 0x0000 下载程序,没有启用蓝牙时确实是对的

启用蓝牙后,由于 bootloader 的原因,Flash 前面几段地址被另作他用,因此用户程序的烧录起始地址需要修改,没记错的话是 0x6000

测试

PC 端启动串口调试程序。手机端启动 nRF Connect,并搜索附近的设备:

nRF Connect Scanner

连接后,切换到 Client Tab,根据前面记下的 UUID,在 Attribute Table 中找到对应的 Characteristics,并点击向上的箭头(其实在我们的程序里,只有这个 Characteristics 开启了 Write 权限,所以只有它有上箭头,看不看 UUID 意义不大):

led controller

尝试改变写入的值,就可以看到 LED 灯随着我们的操作被点亮和关闭。