做嵌入式这几年,蓝牙方案前前后后摸了不少,从HC-05这类经典串口透传模块,到nRF52832,再到ESP32,都踩过不少坑。这次项目要做的是一个小型环境监测节点,体积和功耗卡得都比较死,设备端打算直接用纽扣电池供电,蓝牙通信得稳定,还要方便手机端联调。选型比对了一圈,最后把方案定在了Silicon Labs的EFR32BG22上。
这篇东西不是官方文档翻译,是我从Simplicity Studio安装到第一个BLE透传服务真正跑通的完整记录。里面包括芯片选型时对比过的方案、开发环境安装时卡住我大半天的各种问题、GATT服务配置的细节、以及用手机调试时的各种翻车现场。如果你正准备用EFR32BG22做低功耗蓝牙产品,或者还在犹豫选哪颗芯片,这篇应该能帮你少走很多弯路。内容覆盖从环境搭建到透传实现全流程,面向的是刚接触BLE开发、手里可能只有一块开发板的工程师,也适合从HC-05这类经典蓝牙模块转过来的朋友。
1. 项目选型:为什么在众多蓝牙方案里选了EFR32BG22
1.1 选型前我对比过哪些方案
项目需求其实不算复杂:设备端采集传感器数据,通过蓝牙把数据推给手机App,手机也能下发一些控制指令。真正卡人的是几条硬指标——峰值电流要低、休眠电流尽量小、封装要小、射频一致性要好。
先看的自然是nRF52832,这颗芯片在BLE圈子里口碑很好,资料多、社区活跃,软硬件方案满天飞。但我手里这颗项目的目标功耗比较苛刻,nRF52832的rx峰值电流在5.8mA左右,而同级别产品里EFR32BG22标称能做到3.6mA级别。不要小看这2mA的差距,在纽扣电池供电的场景里,这直接决定了设备是能跑三个月还是半年。功耗数据只是选型的一方面,我拿到的EFR32BG22样品和BGM220P模块体积也确实更小,适合做小型化产品。
ESP32-C3也认真考虑过。这颗芯片功能强、价格便宜,Wi-Fi加蓝牙双模在物联网场景很有吸引力。但对这个项目来说,Wi-Fi是多余的,而且ESP32-C3在低功耗蓝牙模式下的功耗控制不如专用BLE SoC。再加上这个项目不需要Wi-Fi,没必要为用不上的功能付出额外的功耗和设计复杂度。
HC-05这类经典蓝牙串口模块也顺带提一下。很多人是从HC-05起步的,它确实简单,但那是经典蓝牙(BR/EDR)时代的产物,配对方式、数据通道、功耗模型跟BLE完全是两套逻辑。这个项目从一开始就排除在BLE方案之外。
1.2 EFR32BG22的参数底牌
确定方向后我仔细翻了一遍EFR32BG22的datasheet。它用的是ARM Cortex-M33内核,最高主频76.8MHz,Flash最大352KB,RAM 32KB。BLE方面支持蓝牙5.2,包含2M PHY、编码长距离PHY、广告扩展等特性。发射功率最高+6dBm,接收灵敏度在1Mbit/s GFSK下能做到-98.5dBm左右,这个灵敏度级别在同类产品里算相当能打的。
低功耗性能是这个芯片的看家本领。EM2深度睡眠模式下电流能到1.4μA左右(具体数值跟RTC、GPIO唤醒配置有关),配合BLE协议栈的唤醒事件机制,整个系统的平均功耗可以压得很低。另外它还集成了一些模拟外设,比如12位ADC、模拟比较器,以及支持PDM的数字麦克风接口,对做传感器节点来说很实用。
封装方面也很灵活,QFN32、QFN40这些都有,甚至有更小的WLCSP封装可以选。对于做小体积产品来说,这个灵活度很关键。实际打样的时候我用的还是官方模块BGM220P,省去了天线设计和射频调试的麻烦,先把功能验证跑通再说。
1.3 软件生态凭什么让我倾向它
芯片硬件再好,开发环境跟不上也白搭。EFR32BG22对应的开发环境是Simplicity Studio 5,配合Gecko SDK(GSDK)。这套工具链的核心理念是图形化配置加自动生成代码,尤其是Bluetooth GATT Configurator,配置服务和特征值基本可以不用手写一行属性表代码。
官方还提供了大量示例工程,包括蓝牙SoC Empty、Bluetooth Beacon、Health Thermometer等,可以直接基于这些工程改。这对我这种不太喜欢从寄存器层面折腾的人来说非常友好。虽然实际使用中Simplicity Studio也有一些令人抓狂的问题,但整体完成度在MCU厂家的IDE里算是第一梯队,后面我会细说踩过的坑。
2. Simplicity Studio安装与工程环境搭建:第一道坎往往最耗时间
2.1 安装前的准备工作和License问题
先说结论:Simplicity Studio 5支持Windows、Linux、macOS三个平台,我是在Windows上开发的。安装包直接在Silicon Labs官网下载,需要注册一个账号才能下载,这个账号同时也用于后续SDK包管理。注意这里注册的是Silicon Labs的账号,不是某些第三方"蓝牙官网"的会员,网上有些资料把这两个概念混在一起,容易误导人。
安装本身不复杂,一路Next就行。但有几个细节直接影响后面是否顺利。第一,安装路径不要有中文和空格,别问我是怎么知道的。第二,如果电脑上有杀毒软件,建议先把整个安装目录加白名单,Simplicity Studio首次启动要解压和缓存大量文件,杀毒软件实时扫描会导致后续操作异常变慢。第三,记得保证磁盘至少有5GB左右的可用空间,SDK包加工具链占用的空间比想象中大。
还要提醒一句,安装过程中它会自动安装一些依赖组件,可能包括Java运行时、各种编译器工具链,这些是Simplicity Studio正常运行的前提。如果安装过程中报错,直接重新运行安装程序选择修复即可,一般都能解决。
2.2 GSDK固件包下载:最耗时间的环节
Simplicity Studio装好只是开始,真正的考验是首次启动时下载GSDK。
首次启动会让你选择SDK包版本,它会列出可用的Gecko SDK版本。这里我建议直接选择最新的稳定版,不要选pre-release版本。选定之后是漫长的下载过程,这个环节受网络环境影响比较大,有时候进度条半天不动,有时候下到一半报错退出。
结合我自己的经验,尽量避免在下载过程中切换网络或者休眠电脑。下载中断后重新开始,它理论上支持断点续传,但实际体验并不稳定。如果总卡在某一个进度点,可以试试清理一下本地的SDK缓存,然后重新启动Studio再下载。
GSDK下载完成后,里面的结构大概是这样的:核心SDK代码在gecko_sdk目录下,包含platform、protocol、app等子目录;示例工程放在app/example目录;蓝牙协议栈相关的核心代码主要在protocol/bluetooth目录。熟悉这个结构对后面排查问题很有帮助,因为有时候编译报错会直接指向SDK里的某个源文件。
2.3 驱动安装和调试器识别:开发板没反应怎么办
SDK装好后插上开发板,这里就是很多人卡住的第二个地方——系统里根本识别不到设备。
EFR32BG22的开发板,比如BGM220 Explorer Kit或Thunderboard BG22,板载的调试器是Silicon Labs自家的。第一次插入电脑时,Simplicity Studio会提示安装驱动,需要手动确认允许它安装。如果驱动装不上或者设备管理器里出现黄色感叹号,可以到Silicon Labs官网下载对应的驱动包手动安装。
我在这个环节就遇到过开发板完全没反应的情况。排查下来发现是USB线的问题,手头有几根线只能供电不能传数据。所以如果插上开发板后没有任何枚举反应,先换一根确定能传输数据的USB线试试。
驱动正常后,Simplicity Studio的Launcher视角里应该能看到连接的设备,显示芯片型号和调试器固件版本。如果设备显示为unknown或者需要更新固件,直接点击更新按钮等它完成即可。注意更新过程中不要拔USB线,否则有可能把板载调试器刷成砖。
3. 第一个BLE透传工程:从模板到能广播、能被手机发现
3.1 新建工程:选对模板能省一半事
在Simplicity Studio里新建工程,操作用的是Project Wizard。流程是File -> New -> Silicon Labs Project Wizard,然后选择你的开发板型号,下一步会让选示例工程。
这个环节我强烈建议选Bluetooth - SoC Empty这个模板。有些教程会让你选相对复杂的Bluetooth - SoC BLE Beacon或者Health Thermometer,但对做透传来说,Empty模板更干净,没有多余的业务逻辑,从零开始加自己的服务和特征值,反而更好理解整个协议栈的工作方式。
选完模板之后,配置工程名和保存路径。路径要求跟安装路径一样,别有中文和空格。Toolchain选默认的GNU ARM Embedded Toolchain就行,不用折腾IAR或者Keil,免费且官方支持完善。
工程生成之后,左侧项目文件列表里会出现一堆文件。最重要的几个是:
app.c:应用层主逻辑,BLE事件回调主要在这里处理gatt_configuration.btconf:GATT数据库配置,用图形化GATT Configurator打开main.c:程序入口,大部分情况下不用动sl_bluetooth.c/h:协议栈初始化和事件派发,一般也不用直接改
3.2 用GATT Configurator配置透传服务
双击gatt_configuration.btconf,会打开GATT Configurator图形界面。这就是我前面说的省力功能,不用手写GATT表,所有属性都可以可视化编辑。
透传服务一般需要定义两个特征值:一个是手机写数据给设备,一个是设备主动推送数据给手机。我在工程里自定义了一个128位UUID的服务,命名叫Custom Service,然后在里面加两个Characteristic:
RX:属性设置为Write和Write No Response,用于接收手机下发的数据TX:属性设置为Read和Notify,用于设备向手机推送数据
这里有一个新手容易忽略的地方:如果希望设备能主动推数据给手机,Notify属性必须勾上,并且需要添加一个Client Characteristic Configuration Descriptor(CCCD)。这个描述符是0x2902,作用是为每一台连接的手机保存"是否开启通知"的状态。没有CCCD,手机端想收notify都不知道去哪读开关状态。
UUID建议自己生成一个固定的128位UUID,不要用默认的占位符。Silicon Labs的工具里可以直接填入你想要的UUID值,直接替换成自己定义的那一串即可。我用的UUID是随手用在线工具生成的,只要确保全局唯一就行。
3.3 编译烧录:第一个里程碑
配置完成后按F7编译。第一次编译速度会慢一些,因为要编译协议栈库和所有引用到的SDK模块。如果编译报错,大部分情况是SDK包和Studio版本不匹配,或者工程引用了缺失的组件。这时候检查一下工程在Project Attributes里有没有正确引用GSDK,以及安装的SDK版本和工程要求的版本是否一致。
编译通过后会生成.s37或者.hex文件,然后点击Build菜单里的Debug或者直接按F8,工具会自动调用板载调试器把固件烧进EFR32BG22。烧录完成后开发板会自动复位运行。
验证固件是否正常工作,最简单的方法是打开手机上的蓝牙调试App。官方推荐的是EFR Connect,但国内用nRF Connect的更普遍,功能都差不多。扫描之后应该能看到一个广播名为SoC Empty的设备。设备名是在GATT Configurator里的Advertising配置中设置的,默认就是工程名。
到这里,你的EFR32BG22已经能作为一个BLE外设被手机发现了。但此时设备上只有Generic Access、Generic Attribute这些基础服务,透传功能还八字没一撇,继续往下看。
4. 透传服务实战:把数据通道真正打通
4.1 透传的两种数据方向:Write和Notify各干各的活
开始写代码之前,先把透传的机制彻底说明白。BLE的GATT层数据交互模型跟UART这种串口透传完全不同,不是建立一条虚拟串口然后双向随便写。它是基于"服务-特征值"的属性模型,每个特征值有固定的读、写、通知权限。
我们说的透传,本质上是这样两条独立通路:
- 手机到设备:手机向RX特征值发起Write请求,EFR32BG22在GATT事件回调里收到数据。
- 设备到手机:EFR32BG22调用协议栈API主动向TX特征值发起Notification或Indication,手机端的回调里就能读到数据。
很多人分不清Notification和Indication的区别,实际上区别在于有没有应用层确认。Notification发出去对方不一定知道数据丢了,Indication则要求手机给一个确认。从可靠性角度讲Indication更稳,但数据吞吐效率低,因为每发一个包都要等确认。做透传我一般默认用Notification,如果项目对数据完整性要求特别严格,再换Indication。
还有一个新手容易踩坑的地方:Read属性只是让手机主动来拉数据,它不能作为设备主动推数据的通道。所以不要把透传的"设备到手机"方向做成Read,那样手机要不停轮询,功耗和实时性都比较差。
4.2 事件回调框架:协议栈怎么把数据交给你
EFR32BG22的蓝牙协议栈是事件驱动的。GSDK的应用代码核心就是一个sl_bt_on_event回调函数,协议栈把各种事件(连接建立、断开、收到写请求、对端订阅通知等)源源不断派发到这个函数里。
透传涉及的核心事件主要有这几个:
sl_bt_evt_system_boot_id:系统启动完成后触发。在这个事件里,你要调用协议栈的API开始广播,或者设置一些初始状态。如果不启动广播,手机端永远扫描不到设备。
sl_bt_evt_gatt_server_attribute_value_id:当手机向某个特征值写入数据时触发。事件参数里有attribute编号和value数据和长度,你需要在自己定义的RX特征值编号上做匹配,然后处理收到的数据。
sl_bt_evt_gatt_server_characteristic_status_id:当手机端通过CCCD开启或关闭通知时触发。如果做的是透传,一般不需要在这个事件里做太多逻辑,但要知道有这个东西存在,排查时能帮上忙。
4.3 初始化广播和GATT服务的代码骨架
整个透传功能的实现思路,我直接整理成代码骨架,你可以对照自己的工程来理解。
#include "sl_bluetooth.h" #include "sl_bt_api.h" #include "gatt_db.h" // 假设你已经通过GATT Configurator定义了这两个特征值 // gattdb_rx: 可写的RX特征值 // gattdb_tx: 可通知的TX特征值 static uint8_t rx_buffer[512]; static uint16_t rx_len = 0; static void start_advertising(void) { sl_status_t sc; sc = sl_bt_legacy_advertiser_start(0, 0, 1); if (sc != SL_STATUS_OK) { // 广播启动失败时,可以通过log打印错误码 } } void sl_bt_on_event(sl_bt_msg_t *evt) { switch (SL_BT_MSG_ID(evt->header)) { case sl_bt_evt_system_boot_id: // 系统启动,开始广播 start_advertising(); break; case sl_bt_evt_gatt_server_attribute_value_id: if (evt->data.evt_gatt_server_attribute_value.attribute == gattdb_rx) { // 收到手机发来的数据 uint8_t len = evt->data.evt_gatt_server_attribute_value.value.len; uint8_t *data = evt->data.evt_gatt_server_attribute_value.value.data; memcpy(rx_buffer, data, len); rx_len = len; // 在这里解析并处理收到的数据 } break; case sl_bt_evt_gatt_server_characteristic_status_id: // 手机端修改CCCD,打开或关闭通知时触发 break; default: break; } }这段代码的核心逻辑很简单:启动时开始广播,收到写请求时把数据拷贝到buffer里,然后交给你的业务逻辑去处理。
4.4 数据上报:怎么把传感器数据主动发给手机
设备主动向手机推数据,用的是sl_bt_gatt_server_send_notification这个API。调用前必须确认一件事:手机端已经通过CCCD订阅了通知,否则协议栈会返回错误。
代码长这样:
static void send_data_to_phone(uint8_t *data, uint16_t len) { sl_status_t sc; // 向TX特征值发送通知 sc = sl_bt_gatt_server_send_notification( 0, // connection handle,这里用0表示第一路连接 gattdb_tx, // 目标特征值 len, // 数据长度 data // 数据指针 ); if (sc != SL_STATUS_OK) { // 发送失败,通常是连接已经断开或对方没有开启通知 } }这里有个细节:发送通知前,最好自己维护一个标志位记录手机有没有开启通知。开关逻辑放在characteristic_status事件里,当手机端写入CCCD的值为0x0001时置true,写0时清false。这样既能避免无效调用,也为后续功耗优化打下基础。
调用这个函数前,数据要先放到本地buffer里。如果你是采集完传感器数据立刻发送,我建议用队列或者简单的状态变量来同步发送时机,避免在中断上下文里直接调用协议栈API。GSDK的蓝牙协议栈虽然可以在中断里调用,但从应用层架构来讲,更好的是在任务或主循环里处理,省得调试时满世界找问题。
4.5 MTU、连接间隔和真实吞吐量
把通道打通只算完成了一半,实际传数据时你会发现速率可能远低于预期。这里有几个参数直接影响吞吐量。
首先是MTU。BLE 4.2/5.x的默认ATT MTU是23字节,扣掉ATT头,实际每包能装的有效数据只有20字节。通过MTU协商,这个值可以提高到最多247字节,也就是说每包能传244字节有效数据。在GSDK里,调用sl_bt_gatt_server_send_notification之前,可以蓝牙协议栈组件里打开MTU自动协商,或者调用API显式请求更大MTU。
其次是连接间隔。BLE连接里设备是按连接间隔跟主机同步的,每个连接间隔能交换的数据包数量跟连接事件长度有关。连接间隔越短,理论吞吐越高,但功耗也越高。EFR32BG22上,我实测下来比较均衡的参数是连接间隔30ms左右,配合2M PHY,实际吞吐能达到80KB/s以上,这个速度做普通透传完全够用。
对于功耗敏感项目,我不建议盲目追求高吞吐。要根据实际业务数据量去反推需要的连接参数,够用就行,否则电池续航会很难看。
5. 高频问题排查与避坑记录
5.1 编译构建时报错:别急着怀疑SDK坏了
我在调试过程中碰到好几次编译报错,一度以为是SDK下载不完整,卸载重装了两次才慢慢摸清楚规律。这里把我遇到的典型情况整理成表格。
| 报错现象 | 常见原因 | 解决办法 |
|---|---|---|
cannot find bluetooth.h | 工程没有正确引用GSDK组件 | 在Project Attributes里重新添加GSDK的bluetooth组件 |
链接时报undefined reference to sl_bt_* | 没有链接协议栈库 | 检查组件是否启用了Bluetooth时,确保开启了Bluetooth stack组件 |
| 编译后烧录进去没有任何广播 | 没有调用sl_bt_legacy_advertiser_start | 在system_boot事件里显式启动广播 |
| 修改GATT后重新编译,手机还是看到旧服务 | 手机端App缓存了旧的GATT表 | 在调试App里清除缓存,或者关闭蓝牙再重新打开 |
此外,如果使用了中文路径或者用户名带特殊字符,编译也可能出现莫名奇妙的错误。这个跟GNU工具链的处理方式有关,尽量不要在带空格的路径下建工程。
5.2 手机搜不到设备或者连不上
这个问题的出现频率非常高。首先要区分是广播没出来,还是广播出来了连接不上。
广播没出来,排查方向是:代码里到底有没有调用启动广播的API;有没有在system_boot事件里做;如果启用了静默广播,还要检查广播参数。还有一个非常隐蔽的坑:开发板本身处于异常状态,比如刚被烧录了异常固件进入死循环。这种情况下最简单粗暴的办法是按住开发板复位键,或者拔插USB重新上电。
广播能看到但连接不上,问题多半出在配对和安全设置上。如果设备端配置了配对和加密要求,而手机App没有正确处理配对流程,就会出现连接初期就断开的现象。可以先在GATT配置里暂时关闭安全要求,等透传功能全部跑通再加上配对。
如果用的是Windows电脑的蓝牙去连,有时候会显示能看到但连不上,这种情况通常是Windows的蓝牙协议栈和BLE设备兼容性问题,换一个手机App验证是最快的定位方法。很多人从HC-05转过来,习惯用电脑蓝牙连模块,但BLE和经典蓝牙在两套完全不同的协议框架里,别用经典蓝牙的思路去套BLE。
5.3 通知发不出去:总是返回错误码
sl_bt_gatt_server_send_notification返回错误码,最常见的两个原因:一是连接句柄不对,二是在手机端没有开启CCCD通知。
连接句柄问题多发于多连接场景,单连接时连接句柄一般是0,但不要写死。在sl_bt_evt_connection_opened_id事件里把传进来的连接句柄保存到全局变量,后面发送时引用这个变量,可以避免很多莫名其妙的错误。
CCCD没开启问题,前面已经提过,我在第一次调透传时就在这里卡了很久。现象是手机端nRF Connect里明明能看到TX特征值,但发送通知总是报错。后来发现,在nRF Connect里需要先点开TX特征值的"通知"开关,向设备端写入CCCD,然后设备端才能真正发送通知。这一步需要在App里手动操作,很多刚开始做Ble的人不知道这个细节。
5.4 功耗表现不如预期
最后说一个大家容易在项目后期才关注的问题:功耗。
用EFR32BG22做低功耗产品,如果发现电流始终降不下来,先别急着怀疑芯片参数。我有一次发现设备明明在深度睡眠模式,电流仍有几百微安,排查了很久发现是开发板上一个LED的限流电阻在偷偷耗电。开发板上集成的调试器、LED、电平转换芯片都会贡献额外功耗,产品化时如果用模块方案,要确认模块上的外围器件是否可以在软件上关断。
另一个功耗陷阱是广播参数。如果广播间隔设得太短,比如20ms,设备会一直处于发射状态,平均电流自然高。我的习惯是:需要被发现时用快广播,比如间隔100ms;设备已经连接的情况下把连接参数调到适合业务功耗的水平。这些都是透传功能做完之后应该顺手做掉的优化。
5.5 不同芯片平台间的迁移成本
最后分享一点前瞻性的体会。如果你之前用的是nRF52系列,转到EFR32BG22初期会感觉API风格差异挺大。nRF的SDK更多是静态库加注册回调,GSDK则更依赖事件回调加组件配置。但一旦适应了GSDK者的配置方式,你会发现修改GATT表、调试服务属性比手写C代码维护属性表要省心很多。
如果后续项目要做Zigbee或者多协议,EFR32系列还有很多可玩空间。GSDK当中同时集成了蓝牙、Zigbee、Thread等协议栈,选型时可以提前考虑未来的产品规划,省得再次换平台。
根据我个人这段时间的经验,最想提醒刚接触EFR32BG22的同学是:Simplicity Studio这个工具虽然大,但它配置GATT的方式非常值得花一天时间去熟悉。用熟了之后,你修改一个特征值属性、加一个服务,基本上就是点几下鼠标的事。而一旦跳过这个环节直接手写GATT表,后面的调试成本反而会高很多。
如果真的在某个环节卡住了,也建议打开工程目录里的project.log日志看两眼,里面往往比报错弹窗藏了更多有效信息。做BLE开发,耐心和系统性的排查方法比端着代码猜来猜去靠谱得多。