Flipper Zero 固件中的 BLE LLD:基于 BLE 射频硬件的专有协议无线电抽象层解析
【免费下载链接】flipperzero-firmwareFlipper Zero firmware source code项目地址: https://gitcode.com/GitHub_Trending/fl/flipperzero-firmware
本指南以 lib/stm32wb_copro/wpan/ble_lld/README.md 为骨架,结合lib/stm32wb_copro/wpan/ble_lld/目录下的头文件与实现源码,讲解 BLE LLD 的架构设计、Action Packet 链接机制、双核(appli core / radio core)协作方式,以及 HAL/LLD 两套 API 的配置与通信流程。读完本文,你将理解如何利用 BLE 射频硬件实现自定义专有无线协议,并掌握从初始化、事件处理到发射/接收动作链配置的完整调用链。
什么是 BLE LLD
BLE LLD(BLE Low Level Driver)是 STM32WB 平台上的一层专有射频通信抽象层:它依赖 BLE 射频硬件,但本身并不是一个 BLE 协议栈。正如 README 开篇所述,它的定位是"light and simple layer to develop proprietary protocols and applications"——即为开发者提供一套轻量、简单的接口,用来在 2.4GHz BLE 射频硬件之上构建自定义的专有协议,而不是实现 BLE 标准通信。
BLE LLD 对外提供两层接口:
- LLD(全功能层):暴露射频核心支持的全部特性,API 较为复杂,支持自定义动作包(Action Packet)链式调度;
- HAL(简单 API 层):建立在 LLD 之上,把常见通信场景封装成简单函数,适合不需要自定义动作链的场合。
对应源码分布在 lib/stm32wb_copro/wpan/ble_lld/ 下,其中lld/ble_lld.h、lld/ble_lld.c是 LLD 实现,hal/ble_hal.h、hal/ble_hal.c是 HAL 封装,lld/ipBLE_lld_public.h定义了动作包、事件枚举等公共数据结构,lld/ble_lld_transport.h则定义了两核之间传输层使用的命令码与参数结构。
双核架构与 IPCC 通信
appli core 与 radio core 的分工
BLE LLD 专为双核硬件设计(见 README "Dual core" 一节):
- Application core(应用核):运行用户代码;
- Radio core(射频核):运行专用于射频管理的私有代码。
两核之间的软件通信层称为IPCC(Inter-Processor Communication Controller)。README 特别强调:IPCC 这一传输层与 BLE LLD 本身解耦,也就是说 BLE LLD 不关心底层消息如何送达射频核,具体如何"接线"由应用层负责。在仓库中,与传输层相关的基础设施位于 lib/stm32wb_copro/wpan/interface/patterns/(如tl/目录下的 hci_tl、shci_tl 等模块)。
README 中的架构示意图清晰地展示了两个核上的软件栈:
appli core: HAL -> LLD -> LLD proxy radio core: LLD proxy -> LLD -> BLE radio 中间以 IPCC 分隔- appli core上:Application 是使用自定义射频协议的用户程序;HAL 是基于 LLD 的简单通信封装;LLD 是全功能通信层;LLD proxy 负责把数据和命令打包/解包,与射频核往来。
- radio core上:LLD proxy 与 appli core 侧对应打包/解包;LLD 提供射频抽象;BLE radio 是 RF 硬件。
双核架构带来的约束
这一架构带来两个重要约束:
- 射频核上不运行任何应用代码;
- 射频事件发生后,应用代码的响应耗时较长(因为要跨 IPCC 往返)。
为了在约束下依然能实现快速的射频操作序列,射频核支持将action packets(动作包)链接(chain)起来执行,而链接方式由应用侧配置。这两个约束会直接影响协议设计以及 LLD/HAL 的选择——如果需要极低延迟的连续收发,就需要用 LLD 精心编排动作链;如果时序要求不苛刻,用 HAL 即可。
Action Packet:射频核执行 Tx/Rx 的最小单位
Action packet 是射频核用来控制无线电数据包**发射(Tx)与接收(Rx)**的结构(README "Action Packet" 一节)。在源码中,它的定义见 lib/stm32wb_copro/wpan/ble_lld/lld/ble_lld.h 的ActionPacket结构体:
typedef struct ActPac_s { uint8_t StateMachineNo; /* 状态机编号 (0 - 7) */ uint8_t ActionTag; /* 动作包配置位域: PLL_TRIG, TXRX, TIMER_WAKEUP, TIMESTAMP_POSITION 等 */ uint32_t WakeupTime; /* 执行动作包前的唤醒时间 (us),仅 TIMER_WAKEUP 置位时有效 */ uint32_t ReceiveWindowLength; /* Rx 窗口大小 (us),仅 Rx 有效 */ void *data; /* 待发送的载荷,仅 Tx 有效 */ uint8_t dataSize; /* 载荷大小,仅 Tx 有效 */ uint32_t status; /* 来自硬件的中断状态寄存器 */ int32_t rssi; /* 收到数据包的 RSSI,仅 Rx 有效 */ uint8_t nextTrue; /* 操作成功时执行的下一个动作包 */ uint8_t nextFalse; /* 操作失败时执行的下一个动作包 */ uint8_t actionPacketNb; /* 动作包编号 (0 - 7) */ void (*callback)(radioEventType, struct ActPac_s *, void *, uint8_t); /* 动作包结束时运行,可为 NULL */ } ActionPacket;动作链:成功/失败双分支
动作包可以链式执行以完成复杂射频序列。链接通过两个字段配置(见 README):
nextTrue:本次操作(Tx 或 Rx)成功时接下来执行的动作包;nextFalse:操作失败时接下来执行的动作包。
射频核会根据动作包配置与操作结果自动沿分支跳转。仓库中ipBLE_lld_public.h还定义了每个动作包编号的取值范围:APACKET_0~APACKET_7共 8 个(ACTION_PACKET_NB 8),以及用于终止链的APACKET_STOP=0xFF("to be placed at the end",放在链末尾用于停止)。同时每个动作包还挂在一个状态机上,StateMachine_t枚举定义了STATE_MACHINE_0~STATE_MACHINE_7共 8 个状态机,LLD 的BLE_LLD_SetChannel()、BLE_LLD_SetTxAttributes()等配置函数都以StateMachineNo为参数——不同状态机可以持有各自独立的信道、网络 ID、PHY 等配置。
Back-to-back 与 wake-up 两种模式
动作包之间(或第一个动作包之前)的延迟可用两种模式配置(README "Back-to-back vs wake-up" 一节):
- Back-to-back 模式:射频全程保持供电,动作包间延迟最低。该时间是全局参数,通过
BLE_LLD_SetBackToBackTime()配置,不能为单个动作包单独设置。源码注释补充了更精确的信息:该模式对应TIMER_WAKEUP位为 0 时使用的 back-to-back 定时器,是全局定时器、所有动作包共用同一个值,且"can be used for low values (down to ~150us)",即最低可到约 150 微秒。 - Wake-up 模式:射频在等待期间进入睡眠,因此动作包间延迟不可能像 back-to-back 那样短。唤醒时间是每个动作包单独配置的(对应
ActionPacket.WakeupTime字段)。源码注释同样给出了量级参考:wakeup 是本地定时器、每个动作包可有不同值,但"cannot be used for low values (minimum is ~700us)",即最小约 700 微秒。
序列中的第一个动作包必须使用 wake-up 模式(README 明确说明),因为此时射频处于空闲未运行状态,需要先被唤醒。
ActionTag位域在 lib/stm32wb_copro/wpan/ble_lld/lld/ipBLE_lld_public.h 中给出了完整的位掩码定义:
| 位掩码 | 宏名 | 含义 |
|---|---|---|
0x01 | PLL_TRIG | 使能射频 PLL 校准(0 关闭 / 1 开启) |
0x02 | TXRX | 动作类型:1 = Tx,0 = Rx |
0x04 | TIMER_WAKEUP | 定时器选择:0 = back-to-back 全局定时器,1 = wake-up 本地定时器 |
0x08 | NS_EN | 自动 NS(Network Select)使能 |
0x10 | INC_CHAN | 自动信道递增(0 不递增 / 1 自动递增) |
0x80 | TIMESTAMP_POSITION | 时间戳采样位置(仅 Rx):0 = 包尾,1 = 包头 |
无线数据包的细节
README "Radio packet details" 一节给出了数据包的三个关键属性:
- 地址匹配:每个数据包包含一个地址,接收时必须与接收方配置的地址匹配才会被接受;
- 最大载荷 255 字节:
ipBLE_lld_public.h中的IPBLE_LLDANT_MAX_PAYLOAD_SIZE 255与之对应,且"less if using encryption"——启用加密后载荷上限会减小; - CRC 校验:数据包包含 CRC,接收时会被检查错误。
从源码的ipBLE_lld_txrxdata_Type结构(见ipBLE_lld_public.h)可以看到实际的数据包内存布局:
typedef struct { uint8_t header; // 被硬件按 BLE flags 解释,LLD 中保留,默认安全值 LLD_HEADER = 0x55 uint8_t length; // 实际载荷大小 uint8_t payload[IPBLE_LLDANT_MAX_PAYLOAD_SIZE]; // 用户数据 } ipBLE_lld_txrxdata_Type;注意两点实现细节:
- header 字段不能随意改动。
ble_lld.c中定义了#define LLD_HEADER 0x55,并注明"Header field is interpreted by hardware and has an impact on some flags, so user should not change it",因此封装数据包时应使用默认安全值。 - 加密时尾部预留 MIC。启用加密后,
payload[length]起始的 4 字节(MIC_SIZE 4)会保留给 MIC(消息完整性校验码)。BLE_LLD_packetPrepareCopy()在encrypt为真时会把actual_size = size + MIC_SIZE写入length。加密相关参数(AES_KEY_SIZE 16、AES_IV_SIZE 8、AES_BLOCK_SIZE 16)同样定义在该头文件中。
数据包的准备与解析有四个配套函数(ble_lld.h):
BLE_LLD_packetPrepareCopy()/BLE_LLD_packetPrepareInPlace():将用户数据写入 Tx 缓冲(Copy 版带拷贝,InPlace 版就地在共享内存缓冲中构建);BLE_LLD_packetExtractCopy()/BLE_LLD_packetExtractInPlace():从 Rx 缓冲提取载荷;BLE_LLD_packetGetSize():查询包大小(encrypt 参数决定是否包含 MIC)。
使用指南:从初始化到事件处理
阻塞式 API 语义
README "Blocking functions" 一节指出:所有 API 函数都是阻塞的——它们会等待射频核完成处理后才返回(但不等候实际的数据包发射/接收完成)。换句话说,调用返回只代表命令已送达射频核并被处理,真正的空中收发结果要通过事件回调获知。
Radio proxy 配置(无论 HAL/LLD 都必须先做)
由于双核架构,用户无法直接访问射频核,必须通过proxy控制射频。而 BLE LLD 与两核间的通信层解耦,因此需要应用侧把 proxy "接上线"——这些接线函数统一以BLE_LLD_PRX_为前缀(README "Radio proxy configuration" 一节):
BLE_LLD_PRX_Init():必须第一个调用,用于配置射频核 proxy。函数签名(见ble_lld.h)为:void BLE_LLD_PRX_Init(param_BLE_LLD_t *parameters, ipBLE_lld_txrxdata_Type *transmitBuffer, ipBLE_lld_txrxdata_Type *receiveBuffer, uint8_t (*callbackSend)(BLE_LLD_Code_t bleCmd));它需要传入命令参数联合体
param_BLE_LLD_t、Tx/Rx 共享内存缓冲,以及一个发送回调callbackSend——这个回调把BLE_LLD_Code_t命令码实际投递到两核通信层,正是"BLE LLD 与传输层解耦、由应用接线"的体现。BLE_LLD_PRX_EventProcessInter():在射频事件中断内部调用,负责记录事件数据(见下文事件机制);BLE_LLD_PRX_EventProcessTask():在中断之后的某个任务上下文中调用,负责执行用户回调。
README 特别提醒:无论使用 LLD 还是 HAL API,proxy 配置都是必需的。
射频事件与回调机制
用户在启动/配置一次射频操作时可以注册回调函数(README "Radio events" 一节)。当射频核发生事件(如发送成功、接收失败等),BLE LLD proxy 被通知,进而运行针对该事件注册的回调,从而让用户应用对射频事件做出反应。
事件类型在ipBLE_lld_public.h中完整定义,共 12 种,命名规律为[TX|RX]_[OK|FAIL|TIMEOUT|CRC_KO]_[BUSY|READY]:
TX_OK_BUSY / TX_OK_READY /* 发送成功,radio busy / ready */ TX_FAIL_BUSY / TX_FAIL_READY /* 发送失败,radio busy / ready */ RX_OK_BUSY / RX_OK_READY /* 接收成功,radio busy / ready */ RX_TIMEOUT_BUSY / RX_TIMEOUT_READY /* 接收超时,radio busy / ready */ RX_CRC_KO_BUSY / RX_CRC_KO_READY /* 接收 CRC 错误,radio busy / ready */ RX_FAIL_BUSY / RX_FAIL_READY /* 接收失败(其他原因),radio busy / ready */radioEventType枚举从 1 开始(0 被保留),且最多 32 个事件(与中断过滤实现相关)。ble_lld.c实现了两阶段事件处理:
- 中断阶段
BLE_LLD_PRX_EventProcessInter():根据params->reply.actionPacketNb找到对应的ActionPacket,保存事件与状态,并在RADIO_IS_RX_OK(RX_OK_BUSY或RX_OK_READY)时记录 RSSI; - 任务阶段
BLE_LLD_PRX_EventProcessTask():若该动作包注册了回调,则在RADIO_IS_RX_OK时先从 Rx 缓冲就地提取数据,然后调用radioEventAp->callback(radioEvent, radioEventAp, data, size)。
调试时可使用eventToString()(ble_lld.h/ble_lld.c)将事件枚举转为可读字符串。
HAL 接口:简单通信的正确打开方式
HAL 是 LLD 之上的一层封装,目标是让简单通信更省事。README 明确说明:HAL 提供特性受限的简单 API,适用于不需要自定义动作包链接的场景;底层它通过调用 LLD 来配置若干动作包。
配置流程(README "Configuration"):先用HAL_BLE_LLD_Init()初始化,再用HAL_BLE_LLD_Configure()配置。完整函数签名见 lib/stm32wb_copro/wpan/ble_lld/hal/ble_hal.h:
uint8_t HAL_BLE_LLD_Init(uint16_t hsStartupTime, bool lsOscInternal); uint8_t HAL_BLE_LLD_Configure(txPower_t txPower, uint8_t channel, bool phy2mbps, uint32_t b2bTimeUs, uint32_t networkId);通信接口分为两组(README "Communication"):
- 无 ACK:
HAL_BLE_LLD_SendPacket()/HAL_BLE_LLD_ReceivePacket()——射频只发射/接收一个数据包; - 带 ACK:
HAL_BLE_LLD_SendPacketWithAck()/HAL_BLE_LLD_ReceivePacketWithAck()——射频发射/接收一个数据包后,另一个数据包向相反方向发送。
"带 ACK"的函数可用于检测丢包,因此可以在此基础上实现带重传的可靠通信信道(README 原话)。ACK 方向的具体实现由 README 中"ReceivePacketWithAck 动作链图"给出,见下文 LLD 部分。
LLD 接口:全功能与自定义动作链
LLD 暴露射频核心支持的全部特性,API 更复杂,用于实现自定义动作包链接(README "LLD interface" 一节)。
配置流程:先用BLE_LLD_Init()初始化,再用下列函数配置:
BLE_LLD_SetChannel(StateMachineNo, channel)—— 设置状态机对应的射频信道;BLE_LLD_SetTxAttributes(StateMachineNo, NetworkID)—— 设置网络 ID(即包地址匹配依据,对应 README 中"address must match configured address of the recipient");BLE_LLD_SetTxPower(powerLevel)—— 设置发射功率,txPower_t枚举覆盖TX_POW_MIN_40_DB(0)到TX_POW_PLUS_6_DB(31)共 32 档,例如TX_POW_MIN_20_85_DB表示约 -20.85 dB;BLE_LLD_SetTx_Rx_Phy(StateMachineNo, txPhy, rxPhy)—— 设置收发 PHY,宏定义RX_PHY_1MBPS 0x00、RX_PHY_2MBPS 0x10、TX_PHY_1MBPS 0x00、TX_PHY_2MBPS 0x01。
BLE_LLD_Init()的签名是:
void BLE_LLD_Init(uint16_t hsStartupTime, uint8_t lowSpeedOsc, FunctionalState whitening);它对应传输层命令BLE_LLD_INIT_CMDCODE的参数param_BLE_LLD_init_t(startupTime、lowSpeedOsc、whitening),whitening 即数据白化开关。
通信流程:使用 LLD API 时,每个动作包都要由用户负责配置(README 原话):
- 为期望的动作设置
ActionPacket的全部必需字段(部分字段仅 Tx 有效,部分仅 Rx 有效); - 调用
BLE_LLD_SetReservedArea()把动作包下发到射频核; - 对链首动作包调用
BLE_LLD_MakeActionPacketPending()启动执行——射频核将依据各动作包的配置与操作结果自动链接后续动作包; - 每个动作包结束时,若注册了回调,会向应用发送事件。
中断动作链:BLE_LLD_StopActivity()可随时终止动作包链接。README 强调:该调用会"杀掉"射频,之后任何其他操作前都必须重新初始化。源码注释同样说明BLE_LLD_StopActivity()对应BLE_LLD_STOPACTIVITY_CMDCODE。
状态码:ipBLE_lld_public.h定义了一组命令返回码:SUCCESS_0、INVALID_PARAMETER_C0(无效参数)、WAKEUP_NOTSET_C2(唤醒未设置)、RADIO_BUSY_C4(射频忙)、COMMAND_DISALLOWED(命令不允许),可作为MakeActionPacketPending等函数的返回判断依据。
一个完整的动作链示例:带 ACK 的接收
README 用图示展示了HAL_BLE_LLD_ReceivePacketWithAck()底层的动作包编排(三个动作包组成的链):
START → Action Packet 2 (reception, wake-up, timeout) ├─ SUCCESS → Action Packet 3 (transmission, back-to-back) │ ├─ SUCCESS → STOP │ └─ FAILURE → STOP └─ FAILURE → STOP执行逻辑(README 原话转述):
- Action Packet 2 首先执行,配置数据包的接收;
- 若数据正确收到(CRC OK),则执行Action Packet 3,配置 ACK 包的发射;
- 之后射频停止;
- 若任一动作包失败,射频停止。
可以看到,nextTrue/nextFalse的双分支字段正是这个链的控制骨架:动作包 2 的失败分支直接指向停止,成功分支指向动作包 3;动作包 3 的成功/失败分支都指向停止。这正是"自定义协议可靠通信"(如自动应答)的最小范式。
附加工具:Tone 生成(测试用)
为了测试目的,BLE LLD 提供单音发射功能(README "Tone generation" 一节):
BLE_LLD_StartTone(rfChannel, powerLevel):开始发射单音,参数为射频信道(0-39)与输出功率等级(0-31),对应传输层param_BLE_LLD_toneStart_t;BLE_LLD_StopTone():停止单音。
源码注释补充了两个要点:这两个函数专用于测试,且会销毁上下文与多状态配置("destroys context and multistate"),因此单音发射后、进行任何其他操作之前,射频必须重新初始化(与StopActivity后的要求一致)。
源码结构速览
若要在 Flipper Zero 固件仓库中进一步深入 BLE LLD,可沿以下路径阅读:
| 关注点 | 文件 |
|---|---|
| 官方 README(本文骨架) | lib/stm32wb_copro/wpan/ble_lld/README.md |
| LLD 头文件:ActionPacket、全部 API 声明 | lib/stm32wb_copro/wpan/ble_lld/lld/ble_lld.h |
| LLD 实现:proxy 事件处理、packet 准备/提取 | lib/stm32wb_copro/wpan/ble_lld/lld/ble_lld.c |
| 公共定义:ActionTag 位域、事件枚举、功率/PHY 枚举 | lib/stm32wb_copro/wpan/ble_lld/lld/ipBLE_lld_public.h |
| 传输层命令码与参数结构 | lib/stm32wb_copro/wpan/ble_lld/lld/ble_lld_transport.h |
| HAL 封装头文件 | lib/stm32wb_copro/wpan/ble_lld/hal/ble_hal.h |
| 两核通信传输层参考实现 | lib/stm32wb_copro/wpan/interface/patterns/ble_thread/tl/ |
小结:BLE LLD 的价值在于把 STM32WB 的 BLE 射频硬件抽象成可编程的、支持动作包链接的专有协议引擎。理解ActionPacket的nextTrue/nextFalse分支与TIMER_WAKEUP定时器模式,是设计低延迟自定义射频协议的关键;而BLE_LLD_PRX_系列接线函数则体现了它"与 IPCC 传输层解耦"的架构取舍——接入不同通信层时,只需替换BLE_LLD_PRX_Init()传入的callbackSend实现。
【免费下载链接】flipperzero-firmwareFlipper Zero firmware source code项目地址: https://gitcode.com/GitHub_Trending/fl/flipperzero-firmware
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考