拓冰建站拓冰建站
首页 / 资讯中心 / 正文

基于ARM mbed的BLE应用开发实战与避坑指南

做BLE应用时我在ARM mbed平台和Bluetooth Smart这套组合上栽过跟头也攒了不少经验。当时客户给了一个很紧的工期要一款低功耗环境监测设备能用手机扫描到、能读取温度湿度数据、还要求CR2032纽扣电池供电运行一年以上。脑子里过了一圈方案最后选定ARM mbed平台来做原因很简单它能让我把精力放在业务逻辑上而不是从头啃蓝牙协议栈。这篇文章就把整个项目的思路、环境搭建、BLE应用实现、调试方法和踩坑记录一次性讲清楚适合刚入门的嵌入式开发者也适合从裸机开发转BLE的老手参考。1. 为什么我坚持用mbed跑BLE应用1.1 传统MCU做BLE的痛点如果你之前只用过STM32F103这类经典Cortex-M3芯片做裸机开发第一次碰BLE大概率会懵。BLE不是简单的一套外设寄存器它涉及完整的协议栈广播、扫描、连接、配对、加密、服务发现、属性读写、通知指示每个环节都有状态机和回调。自己从零写协议栈不现实绝大多数人都是拿到芯片原厂SDK后照着例程改但这个过程的陡坡依然很陡。还有一个现实问题BLE需要射频前端普通MCU不具备这个能力。要么选内置BLE的SoC比如Nordic的nRF52832、nRF52840ST的STM32WB要么外挂BLE模块。外挂模块方案省心但成本和体积上去一块用内置BLE的SoC又绕不开各家SDK的使用习惯差异。当时客户要求设备必须是单芯片方案板子尺寸控制在20mm乘30mm以内还要考虑天线布局所以直接排除了外挂模块。剩下的路就是选一颗内置BLE的SoC在它上面写应用。1.2 mbed的价值协议栈与硬件抽象ARM mbed平台当时给我的第一印象是“官方组织出来的开源生态”。它做了两件很关键的事情。第一把BLE协议栈封装成了统一API。不管底层是Nordic的SoftDevice还是ST的BLE stack在mbed OS里你面对的都是一套接口初始化BLE实例、设置广播数据、注册GATT服务、处理连接事件。这意味着你换一颗芯片应用层代码不需要重写只需要在编译时指定新的target。第二提供了事件驱动的RTOS内核。mbed OS不是裸机轮询那种模式它内置了一个精简的实时操作系统能管理线程、事件队列和定时器。BLE应用天然是事件驱动的连接断开要处理、特征值被写要处理、上报定时器到点要处理。用事件驱动模型写起来代码结构非常清晰。这不是说mbed没有缺点后面我会专门讲它坑在哪里。但单就快速把BLE应用跑起来这件事它比我用过的很多原厂SDK要省心得多。1.3 硬件选型从NRF52832到国产GD32L233mbed官方支持的开发板列表很丰富北欧和意法半导体的板子占多数。我这次项目用的是一块NRF52832_DK准确的说是nRF52832芯片Cortex-M4F内核主频64MHz512KB Flash、64KB RAMBLE 5.0。mbed对这块芯片的支持很成熟例程多社区讨论也多。后来给客户做小批量预研时还试过GD32L233这是一颗国产的Cortex-M23内核低功耗MCU带BLE 5.0。选中它的原因很现实供货稳定、价格有优势。但mbed官方并没有把它列在支持列表里需要自己移植BSP包括PinNames定义、时钟初始化、串口重定向这些。这个过程我会在避坑章节详细说。如果你刚开始学我建议先别碰非官方支持的芯片老老实实买一块官方开发板先把mbed跑通再考虑移植和量产的事。2. 开发环境搭建工具链、编译器和烧录2.1 工具链怎么选AC5、AC6还是GCCmbed平台支持的主编译工具有三个ARM Compiler 5、ARM Compiler 6和GCC ARM Embedded。很多新手一开始就卡在这里因为工具链选错了编译报一堆莫名其妙的错误。ARM Compiler 5也就是常说的AC5是老牌的armcc编译器5.06版本是它的经典收官版。很多老工程、老库对AC5的兼容性最好但ARM官方已经不再主推新芯片支持也慢慢跟不上。ARM Compiler 6则基于LLVM/Clang技术编译速度更快、对C11和C14支持更好也是armclang的前身。GCC ARM Embedded则是开源解决方案免费、无授权限制社区资料最丰富我推荐个人开发者优先用它。我用过一段时间的对比是这样的工具链优点缺点推荐场景ARM Compiler 5.06老库兼容性最好编译器停止演进维护老工程ARM Compiler 6编译快新特性支持好迁移老代码有警告新项目GCC ARM Embedded免费社区资源多代码体积可能略大个人开发、学习mbed默认的target配置里不同开发板对工具链的支持情况不同。有的板子官方只验证过AC5/AC6GCC虽然能编译但可能有细微差异。建议先查一下开发板页面上的“Tested with”信息避免无谓踩坑。2.2 本地环境三步走安装、配置、编译那时候mbed已经淘汰了纯在线编译器主流方式是用mbed CLI在本地编译。我现在给新人的建议是在Ubuntu或WSL里搭环境比Windows原生的顺滑度好很多。以下是完整步骤。第一步安装依赖sudo apt update sudo apt install -y python3 python3-pip git mercurial cmake ninja-build sudo apt install -y gcc-arm-none-eabi第二步安装mbed CLIpip3 install mbed-cli mbed --version第三步配置目标工具链路径。mbed CLI需要知道GCC ARM工具链的位置mbed config GCC_ARM_PATH /usr/bin mbed config TARGET NRF52832_DK mbed config TOOLCHAIN GCC_ARM这里有个细节mbed CLI默认会用“mbed-os.lib”文件里的版本号去拉取对应版本的mbed OS源码。版本锁定很重要否则你写的一行API调用可能因为版本升级而不见了。第四步创建工程并编译mbed new ble_demo --create-only cd ble_demo mbed add https://github.com/ARMmbed/mbed-os-example-ble mbed compile -t GCC_ARM -m NRF52832_DK编译完成后在BUILD目录下会生成一个hex文件和elf文件。用pyOCD或OpenOCD烧写到开发板即可。2.3 没有开发板用QEMU做前期验证开发板还没到货的同学可以先在PC上用QEMU模拟ARM开发板把mbed的编译链路和基础逻辑跑通。这里说的不是完全模拟BLE射频行为而是模拟Cortex-M处理器让mbed OS内核能启动、日志能输出。mbed官方没有专门为QEMU出过插件但社区有办法。你可以用qemu-system-arm配合一颗Cortex-M3的机器模型比如LM3S6965EVB编译mbed OS的cortex-m3目标固件然后在QEMU里跑。不过这种方式只能验证RTOS调度和业务逻辑BLE广播是模拟不了的最终还是要真板。我实际用它做过一次逻辑验证把一个传感器数据采集的算法在QEMU里跑到稳定再上板联调。这样至少提前暴露了内存溢出和线程栈不足的问题。2.4 烧录与调试通道mbed开发板通常自带DAPLink调试器插上USB就是一个CMSIS-DAP设备同时会虚拟一个U盘和串口。烧录固件时直接把hex文件拖进U盘即可或者用命令行更可靠pip3 install pyocd pyocd load -t nrf52 -f BUILD/NRF52832_DK/GCC_ARM/ble_demo.hex再用串口工具打开调试串口波特率一般115200能看到printf输出。这个串口输出在调试BLE连接事件时非常有用。3. BLE应用实现广播、连接、GATT服务3.1 BLE协议栈速览先来把术语捋一下。BLE协议分层实际开发中你主要跟两层打交道GAP和GATT。GAP管设备之间的“关系”谁在广播谁在扫谁做中心设备比如手机谁做外围设备比如传感器。GAP层定义了广播间隔、连接间隔、设备地址类型这些参数。广播本质上就是设备周期性地发送一段数据包告诉周围“我存在我能连”。GATT管连接后的“数据组织”。它把设备里的数据组织成服务Service和特征值Characteristic的层级结构。每个服务有唯一UUID每个特征值也有UUID。比如一个温度传感器服务里面会有一个温度特征值属性设置为“只读”或“通知”。手机连上之后通过读写特征值来交换数据。一个容易混淆的概念是“通知”和“指示”。通知是设备主动推数据给手机不需要手机回复ACK效率高但可能丢包。指示需要手机回复确认保证送达。传感器数据上报场景一般用通知就够了。BLE的广播包载荷有硬性限制最大31字节不含MAC头通过主动扫描可以额外获取31字节的扫描响应数据。设计广播数据时要注意设备名称、服务UUID、厂商自定义数据都挤在这31字节里很容易超。3.2 第一个BLE工程让设备能被手机扫到先写一个最基础的代码设备上电后周期性广播手机能扫到它但还没实现GATT服务。#include mbed.h #include ble/BLE.h #include ble/gap/Gap.h using namespace std::literals; static const char DEVICE_NAME[] EnvMon; class BLEDemo : public ble::Gap::EventHandler { public: BLEDemo(BLE ble) : _ble(ble) {} void start() { _ble.init(this, BLEDemo::on_init_complete); } private: void on_init_complete(BLE::InitializationCompleteCallbackContext *context) { if (context-error ! BLE_ERROR_NONE) { printf(BLE init failed\n); return; } start_advertising(); } void start_advertising() { ble::AdvertisingParameters adv_params( ble::advertising_type_t::CONNECTABLE_UNDIRECTED, ble::adv_interval_t(ble::millisecond_t(100)) ); _ble.gap().setAdvertisingParameters(adv_params); ble::AdvertisingDataBuilder adv_data_builder(_adv_buffer); adv_data_builder.setName(DEVICE_NAME); adv_data_builder.setFlags(ble::adv_data_flags_t::GENERAL_DISCOVERABLE | ble::adv_data_flags_t::LE_ONLY); _ble.gap().setAdvertisingPayload(adv_data_builder.getAdvertisingData()); _ble.gap().startAdvertising(ble::advertising_handle_t::CONNECTABLE); printf(Advertising started\n); } private: BLE _ble; uint8_t _adv_buffer[ble::LEGACY_ADVERTISING_MAX_SIZE]; }; int main() { BLE ble BLE::Instance(); BLEDemo demo(ble); demo.start(); while (true) { ThisThread::sleep_for(1000ms); } }这段代码做的事情很直白BLE初始化完成后设置一个可连接的广播参数广播间隔100ms广播载荷里写入设备名字。手机用nRF Connect扫描时能看到一个叫EnvMon的设备。注意广播间隔100ms意味着设备每100ms醒来一次发广播功耗会比较高。实际产品里通常要用1000ms以上甚至广播之外进入深睡眠。这个参数后面会讲怎么调。3.3 自定义GATT服务与特征值通知接下来实现正式的GATT服务。我设计了一个环境监测服务UUID为16位自定义值0xFFF0里面包含一个温度特征值0xFFF1属性为读通知和一个湿度特征值0xFFF2属性为读。在mbed OS中定义服务和特征是模板化操作。一个比较清晰的方式是直接创建GattCharacteristic对象再添加进GattServer#include ble/GattServer.h // 自定义16位UUID static const uint16_t ENV_SERVICE_UUID 0xFFF0; static const uint16_t TEMP_CHAR_UUID 0xFFF1; static const uint16_t HUMI_CHAR_UUID 0xFFF2; // 当前数据 static uint8_t temp_value 25; // 温度值单位0.1摄氏度 static uint8_t humi_value 60; // 湿度值单位0.1% // 特征值定义属性可读可通知 GattCharacteristic temp_char( TEMP_CHAR_UUID, temp_value, sizeof(temp_value), sizeof(temp_value), GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_READ | GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_NOTIFY ); GattCharacteristic humi_char( HUMI_CHAR_UUID, humi_value, sizeof(humi_value), sizeof(humi_value), GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_READ | GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_NOTIFY ); GattCharacteristic *env_chars[] { temp_char, humi_char }; GattService env_service(ENV_SERVICE_UUID, env_chars, 2);初始化后把服务添加到GattServervoid on_init_complete(BLE::InitializationCompleteCallbackContext *context) { // 错误处理略 _ble.gattServer().addService(env_service); start_advertising(); }在业务循环中传感器采集到新数据后通过GattServer的updateValue方法通知手机void update_sensor_value(uint8_t new_temp, uint8_t new_humi) { temp_value new_temp; humi_value new_humi; // 第二个参数是特征值的句柄从服务添加后返回 ble::GattServer gatt _ble.gattServer(); gatt.write(temp_char.getValueHandle(), temp_value, sizeof(temp_value)); gatt.write(humi_char.getValueHandle(), humi_value, sizeof(humi_value)); }这里我踩过一个坑直接用updateValue通知时需要检查手机是否订阅了通知也就是CCC描述符有没有被写1。mbed的GattServer会在description写了0x0001后自动允许通知但你仍然要确认连接状态否则通知发不出去也不会报错数据就悄悄丢了。3.4 连接参数与功耗策略BLE连接参数包括连接间隔、从设备延迟、超时时间。手机作为中心设备会发起连接参数协商外设可以请求改变。连接间隔短数据吞吐高但功耗也高连接间隔长数据吞吐低但省电。传感器上报场景通常设置一个折中值。我们在项目中向手机请求的连接参数如下连接间隔40ms到80ms从设备延迟0超时时间2000ms这个配置既能保证温度数据每100ms左右到达一次又不会让射频一直开着。更极端的省电模式是把连接间隔拉到1秒以上但实时性就很差了得看业务需求。功耗部分的细节我到调试章节再展开这里先记住一个结论BLE的功耗大头不在处理数据而在射频收发。广播和连接的间隔决定了射频模块的唤醒频率是功耗优化的第一优先项。4. 调试三板斧App、逻辑分析仪、SWD4.1 手机App做黑盒验证手机App是验证BLE设备最直接的工具。我用得最多的是nRF Connect免费的Android和iOS都有功能也很全。用nRF Connect能做的事情扫描并查看广播包内容包括设备名、服务UUID、厂商数据建立连接查看对端的完整GATT服务表读写特征值测试读属性和写属性订阅通知实时查看设备推送的数据修改MTU大小测试大数据传输。我调试时习惯先不看代码就用App检查广播数据是否是我预期的服务表是否逐个出现特征值读写是否一致。如果App里看起来不对那代码逻辑大概率有问题如果App里一切正常但业务数据不对就去看传感器采集和数据处理环节。4.2 用SWD读取寄存器定位HardFaultmbed开发板上的DAPLink支持SWD协议这个协议全称是Serial Wire Debug只要两根线SWCLK、SWDIO就能连接调试器Cortex-M芯片基本都支持。有次我的程序在采集传感器数据后死机终端没有任何打印代码看半天也看不出问题。后来用pyOCD连接上去halt掉CPU直接读取寄存器才发现程序卡在了HardFault_Handler里接着读取PC寄存器的值定位到我调用的一个浮点运算函数式再一查栈溢出导致函数返回地址被破坏了。具体命令是这样pyocd cmd -t nrf52进入交互式命令行后halt reg pc reg lrpc是当前执行地址lr是函数返回地址。对照编译时生成的map文件就能找到是哪一行代码出了问题。这个方法比盲改代码强得多。另外一个技巧Cortex-M的异常现场会保存在栈里可以从栈指针指向的内存往后读还原出硬错误发生前的寄存器值。这在追踪难以复现的随机死机时非常有效。4.3 功耗实测与电池寿命估算功耗不能只看数据手册必须用仪器量。我当时的设备构成是传感器、MCUnRF52832和少量外围用低功耗模式下的数据可以估算寿命。先看一组估算数据假设CR2032电池容量为220mAh以下为平均电流和预估寿命广播/连接模式平均电流CR2032预估寿命广播间隔50ms约220uA约40天广播间隔1000ms约15uA约1.6年连接间隔100ms长连接约50uA约6个月连接每天短上报10次约8uA约3年我最终采用的方式是平时不广播用RTC定时器每10秒醒一次采集温度数据存到Flash只存储一天的记录当手机通过一个GPIO按键或特定广播触发连接时才开启广播和上报。这样一个10秒唤醒周期工作模式的实测平均电流做到约12uA用CR2032理论上可以跑一年半以上。要注意这只是理论值电池自放电、低温、瞬时大电流都会让实际寿命打折。测平均电流我用的是一块记录式电流表JW5510串接在电池供电端记录48小时的数据再取平均值。测功耗时有个坑电流表量程太大低功耗电流根本测不出来要配合高分辨率的测试模式或者直接用示波器测电池电阻两端压降来换算电流。5. 避坑清单从编译器报错到连接不稳5.1 Keil与ARM Compiler 5.06的证书问题很多人是在Keil MDK里导入mbed工程编译的这里有个很经典的坑。Keil本身分成C51版和ARM版如果你电脑里之前装了Keil C51用来写51单片机再装ARM版安装路径如果不一致或者许可证文件冲突ARM编译器会被报成没有License。具体报错信息里会出现许可证错误码例如c9555e。解决办法是打开Keil的License Management检查ARM Compiler 5和ARM Compiler 6是否在同一个安装根目录下必要时卸载重装让两部分共用同一个UV4目录。另外一个思路直接用GCC工具链绕开ARM Compiler授权问题Keil MDK也支持GCC工具链只是要额外配置。我自己的习惯是mbed工程一律用GCC ARM编译Keil只用来做芯片的寄存器级调试两回事分开省得证书问题反复纠缠。5.2 广播与连接失败排查手机扫描不到设备或者连上之后一断开就再也扫不到这类问题很常见。按下面顺序排查能省很多时间检查广播参数里的flags确认设置了LE_GENERAL_DISCOVERABLE。检查广播数据长度名字太长或厂商数据太多导致超过31字节。检查设备是否真的在广播用独立手机或开发套件扫描排除手机蓝牙缓存问题。检查设备是否进入异常状态比如卡在某个while循环里没有继续跑BLE协议栈。检查电源LDO输出如果纹波过大射频性能会劣化广播距离短到只有几厘米。还有个隐蔽问题有些手机系统尤其是Android会缓存蓝牙设备名和MAC地址。你改了广播名手机扫描仍然显示旧名。此时关掉手机蓝牙再打开或者清掉App缓存就能看到新数据。5.3 电池消耗异常的几个隐藏原因如果实测电流比理论值高很多先别怀疑BLE很可能问题出在别处GPIO悬空输入导致漏电这是低功耗MCU最常见的坑。所有未使用的引脚必须设置成模拟输入或下拉输出。传感器和外设休眠策略不对SPI/I2C总线上有上拉电阻时休眠也会耗电。LED指示灯没有熄灭一个典型LED在1mA电流下足以抵消你辛苦省下的所有功耗。调试器仍然连接着开发板DAPLink本身也在耗电量产时要拔掉。有一次我把设备功耗从10uA优化到8uA折腾半天发现差异来自一个RGB LED的漏电流哭笑不得。后来我在设计里专门做了一个开关MOS管量产模式直接切断所有调试和指示电路供电。5.4 非官方开发板GD32L233等移植后的坑如果要把mbed跑在非官方支持的芯片上要有心理准备这可能消耗你两倍于应用开发的时间。我拿GD32L233举例它虽然也是Cortex-M内核但有几类差异需要处理。第一是时钟配置。mbed OS的target配置里通常有SystemClock_Config需要按照GD32的库函数重写把主频从内部RC振荡器切到外部晶振否则外设时钟不对串口和BLE都没法用。第二是PinNames定义。mbed的DigitalOut、SPI这些API需要通过PinNames表把引脚号映射到GPIO端口和引脚这个表格需要自己根据芯片数据手册建。第三是Cortex-M23和Cortex-M4的区别。M23内核没有硬件乘法除法指令没有MULS、UDIV之类如果编译时没有正确指定-march一些库函数会调用未定义的指令直接HardFault。好在GCC用-mcpucortex-m23能解决大半。我的建议是如果你只是学习mbed别碰非官方芯片如果是产品选型必须要用那先花两天时间把最小系统跑起来再谈BLE功能。6. 项目复盘mbed方案到底值不值6.1 最终成果与迭代记录这个项目两个月下来最终交付的固件包含以下模块环境数据采集温湿度传感器每10秒读一次低功耗模式下运行数据缓存内部Flash保存一天的历史记录掉电不丢失BLE服务自定义环境监测GATT服务带通知功能手机连接后可实时查看功耗管理非连接时段RTC唤醒连接时段射频按需开启OTA固件升级通过BLE无线升级应用固件使用mbed的FOTA组件二次开发。从最开始一个只会广播的demo到最后能稳定跑完老化测试的产品固件中间迭代了大概六版。mbed帮我把每一版的功能验证时间压缩到了最短。6.2 给后来者的一句话建议如果回到项目开始那一刻我会建议自己先明确一个判断标准这个项目是“验证可行性”还是“直接量产”。如果是验证可行性和原型演示mbed几乎是最好的选择它编译方便、API清晰、社区资源多能让你在很短时间内看到成果给项目各方增加信心。如果是直接量产、目标明确、功耗指标卡得很死那就别犹豫切换到芯片原厂SDK。原厂SDK对低功耗模式、射频校准、隐私特性、安全配对的支持更底层也更可控。mbed虽然也能做但它那层抽象在一些深水区场景反而会成为负担。6.3 基于mbed还能扩展什么mbed平台不止于BLE点对点通信。如果你手上还有多设备组网的需求可以继续探索两个方向一个是BLE Meshmbed OS早期实验性地支持过BLE Mesh适合灯光控制这类多节点场景另一个是把BLE和Thread双协议栈整合在一颗芯片上比如nRF52840就支持并发BLE和Threadmbed也有相应的实验性组件。对我个人来说mbed这套平台最大的价值是降低了BLE开发的门槛让一个从传统MCU转过来的工程师能够迅速上手。它的设计理念是“抽象掉底层复杂度让开发者专注业务”这个理念落到实处是很香的。但产品走深之后也要有勇气沉到寄存器层面把功耗、稳定性和安全问题自己扛起来。毕竟协议栈再方便最终跑在真实世界里的还是你手里的那块板子。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门