BLE 和经典蓝牙不是一回事

很多人一听到蓝牙就想到耳机、音箱,那是经典蓝牙(BR/EDR),要连续传音频,功耗高。BLE(Bluetooth Low Energy,现在叫 Bluetooth Smart)完全是另一种东西:它按"突发小数据包"工作,平时深度睡眠,醒过来几百毫秒发一帧就走。一颗纽扣电池能跑一两年的传感器、手环、Beacon、信标,全靠它。

BLE 的设计目标就是"占空比极低",不是"带宽高"。把它当 WiFi 用是错的,把它当"每隔几秒发一次小报文的无线串口"才是对的。
BLE 配图:中央芯片向四周辐射蓝牙信号波纹,连接手机与小设备
BLE 用极低的占空比,把纽扣电池的寿命拉到年级别

这篇从协议栈分层讲到 GATT 设计、再到省电和配对,把 BLE 真正做产品要踩的坑走一遍。

协议栈分层与角色

BLE 协议栈从下到上分:PHY(2.4G 射频)、Link Layer(链路层,负责广播与连接)、Host(逻辑链路控制、属性协议 ATT)、GAP(广播/扫描的角色定义)、GATT(服务与特征的应用层模型)。开发板上你通常只跟 Host 以上打交道,PHY 和 Link Layer 由 Nordic nRF52、PHY6222、CH582 这类芯片的协议栈固件替你做了。

常见 BLE 芯片
  • nRF52832/52840Nordic 生态最成熟,资料最多。
  • PHY6222国产、便宜、配蓝牙 mesh 不错。
  • CH582沁恒国产,RISC-V 内核,性价比高。
  • ESP32-C3/C6带 WiFi+BLE,做网关最合适。

角色上,设备要么是 Peripheral(从设备,被手机连),要么是 Central(中心设备,去连别人)。绝大多数传感器是 Peripheral。

广播与扫描:让手机找到你

Peripheral 平时不建连接,只是周期性地在 37、38、39 三个广播信道上"喊":我叫 SHT30-001,我有温度数据。手机 App 在周围扫描,听到广播包,再发起连接。

// 典型广播数据结构(按 ADV 结构组装)
// Flags: 0x06  (LE General Disc, 不支持经典)
// Complete Local Name: "SHT30-001"
// Manufacturer Specific: 0x0059 + 设备 MAC + 电池电压
// 广播间隔 100ms,发射功率 0dBm
adv_interval = MSEC_TO_UNITS(100, UNIT_0_625_MS);
sd_ble_gap_adv_data_set(adv_data, sizeof(adv_data),
                        scan_rsp, sizeof(scan_rsp));
sd_ble_gap_adv_start(&adv_params);

广播包只有 31 字节,所以通常把"名字"放广播里、复杂数据放扫描响应里,或者干脆连上之后通过 GATT 读。

GATT:服务、特征与 Notify

连上之后,所有数据交换都走 GATT。GATT 是一棵树:Profile → Service → Characteristic → Descriptor。每个 Characteristic 有一个 UUID,可以读、写、Notify。

// 自定义 Service UUID: 0000A000-0000-1000-8000-00805F9B34FB
//   Char1 0xA001 温度值 (Read + Notify)
//   Char2 0xA002 采样间隔 (Read + Write)
//   Char3 0xA003 设备名字 (Read + Write)

// 温度变化时主动 Notify 给手机
uint16_t len = 2;
uint8_t temp[2] = { temp_lo, temp_hi };
sd_ble_gatts_hvx(conn_handle, &(ble_gatts_hvx_params_t){
    .type = BLE_GATT_HVX_NOTIFICATION,
    .handle = char_temp_handle,
    .p_len = &len, .p_data = temp
});

Notify 是 BLE 省电的关键:不用手机轮询,设备温度一变化就推一帧。手机端订阅 Notify 之后,数据是自动上来的。