先说结论
BLE固件开发最实用的架构是"前后台+事件驱动":后台用中断处理紧急事务(串口接收、定时器、BLE事件),前台主循环处理状态机逻辑。 别一上来就学RTOS,小项目用裸机够了,把省下的复杂度用在优化功耗和调试协议上。
固件开发这行有个规律:代码写得越"聪明",维护的人越痛苦。我见过新人用RTOS+多线程写一个BLE透传模块,结果出了bug根本不知道从哪查。这篇文章用最接地气的方式,帮你搭一个稳定、易调试的BLE固件框架。
固件开发的前置知识
BLE协议栈分层
BLE 从下到上分为这几层:
| 层级 | 负责内容 | 和开发者的关系 |
|---|---|---|
| 物理层(PHY) | 射频调制解调,2.4GHz载波 | 几乎不用管,芯片搞定 |
| 基带层(BB) | 跳频、链路管理 | 有API可以调整连接参数 |
| LL层(Link Layer) | 广播、连接、加密 | 固件控制广播/连接 |
| ATT层 | 属性协议,数据读/写/通知 | 主要打交道的地方 |
| GATT层 | 属性配置框架,Service/Characteristic | 主要打交道的地方 |
| GAP层 | 连接模式、广播策略 | 决定怎么被发现和连接 |
| 应用层 | 你的业务逻辑 | 你写的代码 |
开发者最常接触的是 GATT 层——你需要定义 Service(服务)和 Characteristic(特征值),决定数据怎么组织和传输。
芯片原厂协议栈 vs 自研协议栈
| 方案 | 代表 | 优点 | 缺点 |
|---|---|---|---|
| 原厂SoftDevice/Stack | Nordic、TI | 稳定、认证通过、FAE支持 | 占用ROM/RAM,有学习成本 |
| 第三方RTOS+BLE协议栈 | Zephyr、BLE5-Stack | 开源、可定制 | 门槛高,bug要自己修 |
| 完全自研 | 少数大厂 | 完全可控 | 工作量大,不推荐 |
选型建议:绝大多数项目用原厂协议栈就够了。Nordic 的 SoftDevice、TI 的 BLE-Stack、Dialog 的 SmartSnippets,都是成熟方案,别自己造轮子。
实用架构:前后台 + 事件驱动
为什么选这个架构?
BLE应用的特点是:事件驱动,实时性要求不高,但并发多。
- 串口数据来了要收
- 手机连接/断开要处理
- 定时器要更新状态
- ADC采样要定期读
这些事互相独立,用一个好的事件机制串起来就够了,不需要多线程。
系统架构图
┌─────────────────────────────────────────────────┐
│ 主循环(前台) │
│ │
│ while(1) { │
│ handle_events(); // 处理事件队列 │
│ process_data(); // 处理串口/BLE数据 │
│ update_state(); // 更新状态机 │
│ go_to_sleep(); // 没事情就睡觉 │
│ } │
└─────────────────────────────────────────────────┘
▲
│ 事件
▼
┌─────────────────────────────────────────────────┐
│ 中断处理程序(后台) │
│ │
│ UART_IRQ: 串口接收中断,收一字节进缓冲区 │
│ TIM_IRQ: 定时器中断,设置事件标志 │
│ BLE_IRQ: BLE协议栈事件中断(厂商提供) │
│ GPIO_IRQ: 按键/传感器中断 │
└─────────────────────────────────────────────────┘
核心设计原则:中断只做最少的事——接收数据、设置标志位,不做复杂逻辑。
代码实现
第一步:定义事件类型
// events.h
typedef enum {
EVT_NONE = 0,
EVT_UART_DATA, // 串口收到完整数据包
EVT_BLE_CONNECT, // BLE连接建立
EVT_BLE_DISCONNECT, // BLE断开连接
EVT_BLE_WRITE, // 手机写数据过来
EVT_TIMER_TICK, // 定时器触发
EVT_ADC_READY, // ADC采样完成
} event_type_t;
typedef struct {
event_type_t type;
uint8_t len;
uint8_t data[32]; // 根据实际调整大小
} event_t;
第二步:环形缓冲区(串口数据)
// buffer.h
#define UART_BUF_SIZE 256
typedef struct {
uint8_t buf[UART_BUF_SIZE];
volatile uint16_t head; // 写指针
volatile uint16_t tail; // 读指针
} ringbuf_t;
void ringbuf_push(ringbuf_t *rb, uint8_t byte);
uint8_t ringbuf_pop(ringbuf_t *rb, uint8_t *out);
uint16_t ringbuf_available(ringbuf_t *rb);
第三步:中断服务程序(ISR)
// isr.c
extern ringbuf_t uart_rx_buf;
extern event_queue_t g_event_queue;
// UART接收中断——只做一件事:收字节进缓冲区
void UART1_IRQHandler(void) {
if (UART_GetITStatus(UART1, UART_IT_RXIEN) != RESET) {
uint8_t byte = UART_ReceiveData(UART1);
ringbuf_push(&uart_rx_buf, byte);
UART_ClearITPendingBit(UART1, UART_IT_RXIEN);
}
}
// BLE事件由厂商提供的中断处理,通常是一个回调
void ble_stack_event_callback(ble_evt_t *p_ble_evt) {
switch (p_ble_evt->header.evt_id) {
case BLE_GAP_EVT_CONNECTED:
event_queue_push(&g_event_queue, EVT_BLE_CONNECT, NULL, 0);
break;
case BLE_GAP_EVT_DISCONNECTED:
event_queue_push(&g_event_queue, EVT_BLE_DISCONNECT, NULL, 0);
break;
}
}
第四步:主循环事件处理
// main.c
int main(void) {
// 硬件初始化
SystemInit();
UART1_Init(115200);
TIM2_Init(1000); // 1ms中断
ble_stack_init(); // 原厂API
// BLE GATT服务定义
ble_service_t *p_uart_service = ble_service_create("FFE0");
ble_char_add(p_uart_service, "FFE1", CHAR_PROP_READ |
CHAR_PROP_NOTIFY | CHAR_PROP_WRITE, 32);
ble_advertising_start(); // 开始广播
while (1) {
// 处理所有待处理事件
event_t evt;
while (event_queue_pop(&g_event_queue, &evt)) {
handle_event(&evt);
}
// 处理串口数据
process_uart_data();
// 没事件就进睡眠,等中断唤醒
go_to_sleep();
}
}
GATT服务设计(BLE数据交互的核心)
什么是 Service 和 Characteristic?
BLE设备的数据结构是一个树:
Device
└── Service: UART Service (UUID: FFE0)
├── Characteristic: TX (UUID: FFE1, 读写+通知) ← 模块发送给手机
└── Characteristic: RX (UUID: FFE2, 写) ← 手机发送给模块
UUID FFE0/FFE1 是民间约定俗成的透传UUID,几乎所有BLE透传模块都用这个。
功耗优化实战
睡眠模式选择
| 睡眠模式 | 功耗 | 唤醒方式 | 适用场景 |
|---|---|---|---|
| Active | 3~15mA | 一直工作 | 传输中 |
| Idle | 0.5~2mA | 定时器/IO | 等待数据 |
| Sleep | 1~10μA | 定时器/IO/BLE | 电池设备默认 |
| Deep Sleep | 0.1~1μA | IO边沿/特定引脚 | 超长待机 |
Nordic nRF52 实测(nRF52832)
| 模式 | 电流 | 说明 |
|---|---|---|
| System OFF | 0.4μA | 最高级别睡眠,需要外部触发唤醒 |
| Idle + RTC | 1.2μA | RTC运行维持时间,支持BLE唤醒 |
| BLE连接(1s间隔) | 8~12μA | 平均功耗,含射频开销 |
开发调试工具
必装工具
| 工具 | 用途 | 价格 |
|---|---|---|
| nRF Connect (手机APP) | BLE调试,发包收包 | 免费 |
| BLE Scanner (手机APP) | BLE调试,设备扫描 | 免费 |
| J-Link / ST-Link | 固件烧录+在线调试 | 30~200元 |
| J-Link RTT Viewer | 实时日志输出,不占串口 | 免费 |
| Wireshark + BTVS | BLE协议抓包分析 | 免费(需要nRF Dongle) |
推荐调试神器:nRF Connect + J-Link RTT
传统做法是用串口输出日志,但串口本身会影响实时性和功耗。J-Link RTT 通过调试器直接读写内存,速度快且不影响正常程序运行。
常见问题Q&A
Q1:BLE固件需要RTOS吗?
简单项目不需要。BLE固件的核心是事件驱动,裸机+中断+状态机完全够用。上了RTOS反而引入复杂性(任务调度、优先级、锁),除非你有多个实时性要求差异大的任务(比如音频+BLE双任务)。
Q2:BLE连接参数怎么设置最合理?
取决于应用场景。传感器数采:连接间隔 500ms~1s,从机延迟 5~10,省电。实时控制场景:连接间隔 20ms~50ms,响应快但功耗高。
Q3:BLE_MTU是什么?
MTU(Maximum Transmission Unit)是单次传输的最大数据量。BLE 4.2 默认 23 字节,BLE 5.0 可协商到 512 字节。如果传输大包,需要在连接建立后协商 MTU。
Q4:BLE固件怎么调试最有效?
优先级:RTT日志 > 串口日志 > 断点调试。RTT日志速度快且不影响程序运行,是BLE固件调试的首选。遇到蓝牙协议层的问题,再用 Wireshark + nRF Dongle 抓包分析。
Q5:BLE和BLE 5.0的区别大吗?
主要三个区别:① 2M PHY(速率翻倍);② Long Range(远距离模式);③ 广播扩展。对大多数数传应用,BLE 4.2 够用,追求远距离才上 BLE 5.0 Long Range。