1. STM32WB55 BLE OTA实现原理与架构解析
STM32WB55是STMicroelectronics推出的双核无线微控制器,内置Cortex-M4应用处理器和Cortex-M0+无线处理器,支持蓝牙5.0协议栈。其OTA(Over-The-Air)功能允许通过蓝牙无线更新设备固件,是物联网设备的关键特性。
1.1 OTA整体工作流程
STM32WB55的OTA更新涉及三个核心组件协同工作:
- Bootloader:驻留在Flash起始位置的小型程序,负责验证新固件并跳转到应用程序
- Application:用户主程序,包含BLE协议栈和OTA服务处理逻辑
- FUS(Firmware Upgrade Service):无线固件更新服务,管理无线协议栈更新
典型OTA流程如下:
- 移动端APP通过BLE连接设备并发送固件数据
- 设备接收数据并写入Flash预留区域
- 数据传输完成后,设备校验固件完整性
- 重启后Bootloader验证新固件并完成切换
1.2 关键存储区域划分
STM32WB55的Flash存储通常按如下方式分区:
| 地址范围 | 大小 | 用途 |
|---|---|---|
| 0x08000000-0x0800FFFF | 64KB | Bootloader区域 |
| 0x08010000-0x0803FFFF | 192KB | 无线协议栈(FUS/Stack) |
| 0x08040000-0x0807FFFF | 256KB | 用户应用程序区域 |
| 0x08080000-0x080FFFFF | 512KB | OTA下载缓存区 |
这种分区设计确保了即使OTA过程中断,原有固件仍保持完整,提供故障恢复能力。
2. BLE OTA服务实现细节
2.1 GATT服务设计
STM32WB55的OTA功能通过自定义GATT服务实现,包含三个关键特征:
-
OTA_BASE_ADDR:用于设置固件写入的起始地址
- 属性:WRITE_WITHOUT_RESPONSE
- 大小:4字节(32位地址)
-
OTA_RAW_DATA:传输固件原始数据
- 属性:WRITE_WITHOUT_RESPONSE
- 大小:247字节(ATT_MTU最大值)
-
OTA_CONF:确认和状态通知
- 属性:INDICATE
- 大小:1字节(状态码)
服务UUID通常定义为128位自定义UUID,例如:
8A7F1168-48AF-4EFB-83B5-679E9A5EA0F3
2.2 服务初始化代码分析
c复制void OTAS_STM_Init(void)
{
// 注册服务事件处理器
SVCCTL_RegisterSvcHandler(OTAS_Event_Handler);
// 添加主服务
aci_gatt_add_service(OTA_UUID_LENGTH,
(Service_UUID_t *)OTAS_SVC_UUID,
PRIMARY_SERVICE,
8, // 总属性数
&(OTAS_Context.OTAS_SvcHdle));
// 添加基础地址特征
aci_gatt_add_char(OTAS_Context.OTAS_SvcHdle,
OTA_UUID_LENGTH,
(Char_UUID_t *)OTA_BASE_ADR_CHAR_UUID,
OTA_BASE_ADR_CHAR_SIZE,
CHAR_PROP_WRITE_WITHOUT_RESP,
ATTR_PERMISSION_NONE,
GATT_NOTIFY_ATTRIBUTE_WRITE,
10, // 加密密钥大小
0, // 固定长度
&(OTAS_Context.OTAS_Base_Addr_CharHdle));
// 添加确认特征
aci_gatt_add_char(OTAS_Context.OTAS_SvcHdle,
OTA_UUID_LENGTH,
(Char_UUID_t *)OTA_CONF_CHAR_UUID,
OTA_CONF_CHAR_SIZE,
CHAR_PROP_INDICATE,
ATTR_PERMISSION_NONE,
GATT_DONT_NOTIFY_EVENTS,
10,
0,
&(OTAS_Context.OTAS_Conf_CharHdle));
// 添加原始数据特征
aci_gatt_add_char(OTAS_Context.OTAS_SvcHdle,
OTA_UUID_LENGTH,
(Char_UUID_t *)OTA_RAW_DATA_CHAR_UUID,
OTA_RAW_DATA_CHAR_SIZE,
CHAR_PROP_WRITE_WITHOUT_RESP,
ATTR_PERMISSION_NONE,
GATT_NOTIFY_ATTRIBUTE_WRITE | GATT_NOTIFY_WRITE_REQ_AND_WAIT_FOR_APPL_RESP,
10,
1, // 可变长度
&(OTAS_Context.OTAS_Raw_Data_CharHdle));
}
2.3 数据接收处理流程
当客户端写入特征值时,触发以下事件处理链:
- BLE协议栈接收数据并生成
ACI_GATT_ATTRIBUTE_MODIFIED_VSEVT_CODE事件 OTAS_Event_Handler函数根据特征句柄分发事件- 对于
OTA_RAW_DATA写入,调用OTAS_STM_Notification处理数据 - 数据被写入Flash缓存区,采用双字(8字节)对齐写入
c复制static SVCCTL_EvtAckStatus_t OTAS_Event_Handler(void *Event)
{
hci_event_pckt *event_pckt = (hci_event_pckt *)(((hci_uart_pckt*)Event)->data);
if(event_pckt->evt == HCI_VENDOR_SPECIFIC_DEBUG_EVT_CODE) {
evt_blecore_aci *blecore_evt = (evt_blecore_aci*)event_pckt->data;
switch(blecore_evt->ecode) {
case ACI_GATT_ATTRIBUTE_MODIFIED_VSEVT_CODE: {
aci_gatt_attribute_modified_event_rp0 *attribute_modified =
(aci_gatt_attribute_modified_event_rp0*)blecore_evt->data;
if(attribute_modified->Attr_Handle == (OTAS_Context.OTAS_Raw_Data_CharHdle + 1)) {
// 处理原始数据写入
OTA_STM_Notification_t notification;
notification.ChardId = OTAS_STM_RAW_DATA_ID;
notification.pPayload = (uint8_t*)&attribute_modified->Attr_Data[0];
notification.ValueLength = attribute_modified->Attr_Data_Length;
OTAS_STM_Notification(¬ification);
return_value = SVCCTL_EvtAckFlowEnable;
}
}
break;
}
}
return return_value;
}
3. Flash操作与固件更新实现
3.1 Flash写入关键逻辑
STM32WB55的Flash编程需要遵循特定规则:
- 必须以双字(8字节)为单位写入
- 写入前必须擦除相应扇区
- 操作期间需要禁用中断
c复制void OTAS_STM_Notification(OTA_STM_Notification_t *p_notification)
{
switch(p_notification->ChardId) {
case OTAS_STM_RAW_DATA_ID: {
uint32_t count = 0;
uint32_t size_left = p_notification->ValueLength;
// 处理完整双字数据块
while(size_left >= (8 - OTAS_APP_Context.write_value_index)) {
memcpy((uint8_t*)&OTAS_APP_Context.write_value + OTAS_APP_Context.write_value_index,
p_notification->pPayload + count,
8 - OTAS_APP_Context.write_value_index);
// 写入Flash
uint32_t NbrOfDataToBeWritten = 1;
while(NbrOfDataToBeWritten > 0) {
NbrOfDataToBeWritten = FD_WriteData(OTAS_APP_Context.base_address,
&(OTAS_APP_Context.write_value),
1);
}
// 验证写入
if(*(uint64_t*)(OTAS_APP_Context.base_address) == OTAS_APP_Context.write_value) {
OTAS_APP_Context.base_address += 8;
size_left -= (8 - OTAS_APP_Context.write_value_index);
count += (8 - OTAS_APP_Context.write_value_index);
OTAS_APP_Context.write_value_index = 0;
}
}
// 处理剩余不足8字节的数据
if(size_left != 0) {
memcpy((uint8_t*)&OTAS_APP_Context.write_value + OTAS_APP_Context.write_value_index,
p_notification->pPayload + count,
size_left);
OTAS_APP_Context.write_value_index += size_left;
}
}
break;
}
}
3.2 固件校验机制
为确保固件完整性,通常采用CRC16校验:
c复制uint16_t crc16_crc16(uint8_t *address, uint32_t length)
{
uint16_t crc = 0xFFFF;
for(uint32_t i=0; i<length; i++) {
crc ^= address[i];
for(uint8_t j=0; j<8; j++) {
if(crc & 0x01)
crc = (crc >> 1)^0xA001;
else
crc = (crc >> 1);
}
}
// 字节序转换
return (crc << 8) | (crc >> 8);
}
在校验阶段,比较接收到的CRC值与计算值:
c复制case OTAS_STM_UPLOAD_FINISHED:
{
uint16_t crc16_match = p_notification->pPayload[1] <<8 |
p_notification->pPayload[2];
uint32_t ota_length = p_notification->pPayload[3] <<24 |
p_notification->pPayload[4] <<16 |
p_notification->pPayload[5] <<8 |
p_notification->pPayload[6];
uint16_t crc16_ret = crc16_crc16((uint8_t *)FLASH_APP_ADDRESS, ota_length);
uint8_t temp_buff[16] = {0};
if(crc16_match == crc16_ret) {
temp_buff[0] = OTAS_STM_REBOOT_CONFIRMED;
} else {
temp_buff[0] = OTAS_STM_REBOOT_ERR;
OTAS_APP_Context.file_type = Fw_App_ERR;
}
OTAS_STM_UpdateChar(OTAS_STM_CONF_ID, temp_buff);
}
break;
4. 启动流程与固件切换
4.1 启动文件分析
STM32的启动流程由启动文件(如startup_stm32wb55xx_cm4.s)控制,关键部分如下:
assembly复制MODULE ?cstartup
SECTION CSTACK:DATA:NOROOT(3)
SECTION .intvec:CODE:NOROOT(2)
EXTERN __iar_program_start
EXTERN SystemInit
PUBLIC __vector_table
DATA
__vector_table
DCD sfe(CSTACK) ; 初始栈指针
DCD Reset_Handler ; 复位向量
; ... 其他中断向量 ...
THUMB
PUBWEAK Reset_Handler
SECTION .text:CODE:REORDER(2)
Reset_Handler
LDR R0, =SystemInit
BLX R0
LDR R0, =__iar_program_start
BX R0
4.2 固件跳转实现
Bootloader完成验证后,通过以下代码跳转到新固件:
c复制void JumpToApp(void)
{
typedef void (*pFunction)(void);
pFunction AppEntry;
// 设置VTOR寄存器指向新向量表
SCB->VTOR = FLASH_APP_ADDRESS;
// 初始化主堆栈指针
__set_MSP(*(uint32_t*)FLASH_APP_ADDRESS);
// 获取复位处理函数地址
AppEntry = (pFunction)(*(uint32_t*)(FLASH_APP_ADDRESS + 4));
// 跳转到应用程序
AppEntry();
}
关键点:
VTOR寄存器重定向到新固件的向量表- 从新向量表加载初始栈指针到MSP
- 获取新固件的
Reset_Handler地址并跳转
5. 实战经验与问题排查
5.1 常见问题及解决方案
-
OTA过程中断导致设备变砖
- 现象:传输中途断开后设备无法启动
- 解决方案:
- 实现双Bank Flash架构,保持旧固件完整
- 添加看门狗定时器检测超时
- 在Bootloader中实现恢复模式
-
Flash写入速度慢
- 现象:传输大固件耗时过长
- 优化方案:
- 增大MTU至247字节
- 使用无响应写入(Write Without Response)
- 实现数据压缩(如LZSS)
-
CRC校验失败
- 现象:固件传输完成但校验不通过
- 排查步骤:
- 检查Flash写入地址是否正确
- 验证CRC算法实现
- 检查Flash编程对齐要求
- 确认无线信号质量(RSSI > -80dBm)
5.2 性能优化技巧
- Flash写入加速
c复制void Flash_WriteFast(uint32_t address, uint8_t *data, uint32_t length)
{
HAL_FLASH_Unlock();
// 预取指和缓存配置
__HAL_FLASH_PREFETCH_BUFFER_ENABLE();
__HAL_FLASH_DATA_CACHE_ENABLE();
// 使用64位写入
for(uint32_t i = 0; i < length; i += 8) {
while(__HAL_FLASH_GET_FLAG(FLASH_FLAG_BSY));
if(HAL_FLASH_Program(FLASH_TYPEPROGRAM_DOUBLEWORD,
address + i,
*(uint64_t*)(data + i)) != HAL_OK) {
break;
}
}
HAL_FLASH_Lock();
}
- BLE传输优化配置
c复制void BLE_Optimize_OTA(void)
{
// 增大MTU
aci_gatt_exchange_config(connection_handle, 247);
// 启用数据长度扩展
aci_l2cap_connection_parameter_update_req(connection_handle,
251, 251, 0, 0xFFFF);
// 设置PHY为2Mbps
aci_hal_set_phy(0, 0, 1, 1);
}
5.3 安全增强措施
- 固件签名验证
c复制bool Verify_Firmware_Signature(uint32_t address, uint32_t length)
{
// 从固件尾部提取签名
ECDSA_Signature *signature = (ECDSA_Signature*)(address + length - sizeof(ECDSA_Signature));
// 计算固件哈希(SHA-256)
uint8_t hash[32];
SHA256_Calculate(address, length - sizeof(ECDSA_Signature), hash);
// 使用公钥验证签名
return ECDSA_Verify(&public_key, hash, signature);
}
- 安全启动实现
assembly复制Reset_Handler:
// 检查安全启动标志
LDR R0, =SECURITY_FLAG_ADDR
LDR R1, [R0]
CMP R1, #SECURE_BOOT_MAGIC
BNE Enter_Recovery_Mode
// 验证主固件签名
BL Verify_Main_Firmware
CMP R0, #0
BEQ Enter_Recovery_Mode
// 正常启动流程
LDR R0, =SystemInit
BLX R0
LDR R0, =__iar_program_start
BX R0
Enter_Recovery_Mode:
// 跳转到恢复模式
LDR R0, =Recovery_Handler
BX R0
6. 高级主题:双Bank切换与故障恢复
STM32WB55支持双Bank Flash操作,可实现无缝OTA更新:
- Bank布局配置
| Bank | 地址范围 | 用途 |
|---|---|---|
| Bank1 | 0x08000000-0x0807FFFF | 主固件 |
| Bank2 | 0x08080000-0x080FFFFF | OTA更新区 |
- Bank切换流程
c复制void Switch_Banks(void)
{
// 解锁Flash控制寄存器
HAL_FLASH_Unlock();
// 配置双Bank模式
FLASH_OBProgramInitTypeDef OBInit;
HAL_FLASHEx_OBGetConfig(&OBInit);
OBInit.OptionType = OPTIONBYTE_BANK;
OBInit.Banks = FLASH_BANK_2;
HAL_FLASHEx_OBProgram(&OBInit);
// 执行系统复位
HAL_FLASH_Lock();
NVIC_SystemReset();
}
- 故障检测与恢复
c复制void Check_And_Recover(void)
{
uint32_t *magic = (uint32_t*)0x20000000;
// 检查"魔法数"判断是否更新中断
if(*magic == OTA_MAGIC_NUMBER) {
// 恢复更新流程
Continue_OTA_Update();
} else {
// 验证当前Bank固件完整性
if(!Verify_Firmware(FLASH_BASE, FIRMWARE_SIZE)) {
// 切换到备用Bank
Switch_To_Backup_Bank();
}
}
}