蓝牙BLE固件开发指南:程序架构设计与代码实战

发布时间:2026-03-27 · 阅读时长:22分钟

先说结论

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/StackNordic、TI稳定、认证通过、FAE支持占用ROM/RAM,有学习成本
第三方RTOS+BLE协议栈Zephyr、BLE5-Stack开源、可定制门槛高,bug要自己修
完全自研少数大厂完全可控工作量大,不推荐

选型建议:绝大多数项目用原厂协议栈就够了。Nordic 的 SoftDevice、TI 的 BLE-Stack、Dialog 的 SmartSnippets,都是成熟方案,别自己造轮子。

实用架构:前后台 + 事件驱动

为什么选这个架构?

BLE应用的特点是:事件驱动,实时性要求不高,但并发多。

这些事互相独立,用一个好的事件机制串起来就够了,不需要多线程。

系统架构图

┌─────────────────────────────────────────────────┐
│                   主循环(前台)                   │
│                                                 │
│  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透传模块都用这个。

功耗优化实战

睡眠模式选择

睡眠模式功耗唤醒方式适用场景
Active3~15mA一直工作传输中
Idle0.5~2mA定时器/IO等待数据
Sleep1~10μA定时器/IO/BLE电池设备默认
Deep Sleep0.1~1μAIO边沿/特定引脚超长待机

Nordic nRF52 实测(nRF52832)

模式电流说明
System OFF0.4μA最高级别睡眠,需要外部触发唤醒
Idle + RTC1.2μARTC运行维持时间,支持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 + BTVSBLE协议抓包分析免费(需要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。

技术问题?查看更多 开发教程文章