1. CH585/CH592蓝牙OTA升级方案概述
作为一名嵌入式开发工程师,我最近在项目中使用了沁恒微电子的CH592蓝牙芯片,并深入研究了其OTA(空中升级)功能。在实际开发中,OTA功能对于物联网设备来说至关重要,它允许我们通过无线方式更新设备固件,而无需物理接触设备。
CH592芯片提供了两种OTA升级方案:
- 备份无线升级(带库升级)
- 固定库无线升级(不带库升级)
今天我要详细介绍的是第一种方案——备份无线升级。这种方案特别适合Flash资源相对充裕的应用场景,它的最大优势是能够同时升级应用程序(APP)和蓝牙协议栈(LIB),但代价是需要将Flash存储空间对半划分使用。
2. 备份无线升级的架构设计
2.1 Flash分区规划
在备份无线升级方案中,整个Code Flash被划分为四个关键区域:
-
Jump IAP区(4KB):
- 地址范围:0x0000-0x1000
- 功能:系统上电后的第一跳转点,负责跳转到IAP程序
-
APP1区(Image A)(216KB):
- 地址范围:0x1000-0x37000
- 功能:当前运行的用户程序,包含完整的蓝牙协议栈
-
APP2区(Image B)(216KB):
- 地址范围:0x37000-0x6D000
- 功能:OTA暂存区,用于存放待升级固件
-
IAP区(12KB):
- 地址范围:0x6D000-0x70000
- 功能:固件搬运程序,负责将Image B的内容复制到Image A
提示:这种分区设计确保了在升级过程中,即使出现意外断电等情况,原有程序也不会被破坏,因为新固件是先写入备份区(Image B)的。
2.2 三个关键程序组件
整个OTA系统由三个独立的程序组成:
-
BackupUpgrade_IAP:
- 功能:检测当前代码标志,判断是否需要搬运备份区代码到用户区
- 大小:约12KB
- 位置:Flash高地址区域(0x6D000)
-
BackupUpgrade_JumpIAP:
- 功能:放在代码起始地址,负责无条件跳转到IAP程序
- 大小:4KB
- 位置:Flash起始地址(0x0000)
-
BackupUpgrade_OTA:
- 功能:在蓝牙从机例程基础上添加OTA功能,可将升级固件保存到备份区
- 大小:最多216KB
- 位置:Image A区域(0x1000)
3. JumpIAP程序详解
3.1 程序功能分析
JumpIAP是整个OTA系统的"第一道门",它的功能极其简单但至关重要:
assembly复制; startup_CH59x.s中的关键代码
j 0x6D000 ; 无条件跳转到IAP程序起始地址
这段汇编代码的作用就是在上电复位后,立即跳转到IAP程序的入口地址(0x6D000)。这种设计确保了无论系统处于什么状态,上电后都会先执行IAP程序,由IAP程序来决定后续的执行流程。
3.2 关键实现细节
-
地址配置:
- 跳转地址必须在编译时确定,并写入启动文件(startup_CH59x.s)
- 如果修改IAP程序的存放地址,必须同步修改JumpIAP中的跳转地址
-
空间占用:
- 整个JumpIAP程序仅占用4KB空间
- 通过链接脚本(Link.ld)严格控制其大小
-
执行流程:
- 芯片上电后,CPU从0x0000开始执行
- 执行唯一的跳转指令,跳转到IAP区域
- 不会执行任何其他功能
经验分享:在实际项目中,我曾遇到过因跳转地址配置错误导致系统无法启动的情况。建议在修改IAP地址后,务必使用仿真器单步调试,确认跳转是否正确执行。
4. IAP程序深入解析
4.1 IAP程序的核心职责
IAP(In-Application Programming)程序是整个OTA系统的"搬运工",它主要负责两个关键任务:
- 状态判断:检查DataFlash中的标志位,判断是否有新固件需要升级
- 固件搬运:如果需要升级,将新固件从Image B复制到Image A
4.2 Flash分区与地址定义
在ota.h文件中,定义了关键的地址常量:
c复制#define IMAGE_A_START_ADD 0x00001000 // Image A起始地址
#define IMAGE_A_SIZE 0x00036000 // Image A大小(216KB)
#define IMAGE_B_START_ADD 0x00037000 // Image B起始地址
#define IMAGE_IAP_START_ADD 0x0006D000 // IAP程序起始地址
#define OTA_DATAFLASH_ADD 0x0007F000 // DataFlash地址(存放标志位)
4.3 关键函数实现
4.3.1 jump_APP函数
这是IAP程序的核心逻辑所在:
c复制void jump_APP(void)
{
if(CurrImageFlag == IMAGE_IAP_FLAG) {
// 需要升级的情况
uint8_t buffer[1024];
// 第一步:擦除旧的APP区域(Image A)
FLASH_ROM_ERASE(IMAGE_A_START_ADD, IMAGE_A_SIZE);
// 第二步:从Image B搬运程序到Image A
for(uint32_t i=0; i<IMAGE_A_SIZE; i+=1024) {
FLASH_ROM_READ(IMAGE_B_START_ADD+i, buffer, 1024);
FLASH_ROM_WRITE(IMAGE_A_START_ADD+i, buffer, 1024);
}
// 第三步:修改标志位
SwitchImageFlag(IMAGE_A_FLAG);
// 第四步:擦除Image B区域
FLASH_ROM_ERASE(IMAGE_B_START_ADD, IMAGE_A_SIZE);
}
// 跳转到APP执行
jumpApp();
}
4.3.2 ReadImageFlag函数
该函数用于读取DataFlash中的标志位,决定系统行为:
c复制void ReadImageFlag(void)
{
OTADataFlashInfo_t p_image_flash;
FLASH_ROM_READ(OTA_DATAFLASH_ADD, (uint32_t *)&p_image_flash, 4);
// 检查读取到的标志位是否有效
if(p_image_flash.ImageFlag != IMAGE_A_FLAG &&
p_image_flash.ImageFlag != IMAGE_B_FLAG &&
p_image_flash.ImageFlag != IMAGE_IAP_FLAG) {
// 无效标志位(可能是首次使用),默认设为IMAGE_A_FLAG
CurrImageFlag = IMAGE_A_FLAG;
} else {
CurrImageFlag = p_image_flash.ImageFlag;
}
printf("Current Image Flag: 0x%02X\n", CurrImageFlag);
}
4.4 IAP程序执行流程
- 上电后通过JumpIAP跳转到IAP程序
- 读取DataFlash中的标志位
- 根据标志位决定行为:
- IMAGE_A_FLAG(0x01):直接跳转到APP执行
- IMAGE_IAP_FLAG(0x03):执行固件搬运流程
- 搬运完成后跳转到APP执行
注意事项:IAP程序在搬运固件时,务必先擦除目标区域再写入。Flash编程的一个基本原则是"先擦后写",直接写入未擦除的区域会导致写入失败。
5. APP程序(BackupUpgrade_OTA)实现
5.1 程序结构与初始化
APP程序除了实现正常的蓝牙功能外,还需要处理OTA相关逻辑。在peripheral_main.c中,关键的初始化流程如下:
c复制void Peripheral_Main(void)
{
// 读取当前Image标志位
ReadImageFlag();
// 初始化蓝牙协议栈
GAPRole_PeripheralInit();
// 添加OTA服务
OTAProfile_AddService();
// 注册OTA回调函数
OTAProfile_RegisterAppCBs(&Peripheral_OTA_IAPProfileCBs);
// 其他外设初始化...
}
5.2 OTA服务注册
蓝牙OTA服务的注册是通过以下代码完成的:
c复制// 定义OTA服务的UUID
#define OTA_SERVICE_UUID 0xFEE0
#define OTA_CHARACTERISTIC_UUID 0xFEE1
bStatus_t OTAProfile_AddService(void)
{
// 创建服务
attServiceUUID_t service;
service.len = ATT_BT_UUID_SIZE;
service.uuid = OTA_SERVICE_UUID;
// 添加特征值
attCharUUID_t charUUID;
charUUID.len = ATT_BT_UUID_SIZE;
charUUID.uuid = OTA_CHARACTERISTIC_UUID;
// 注册服务到蓝牙协议栈
return GATT_AddService(&service, &charUUID);
}
5.3 OTA数据接收与处理
当手机通过蓝牙发送OTA数据时,会触发以下回调函数:
c复制void OTA_IAPWriteData(unsigned char index, unsigned char *p_data, unsigned char w_len)
{
// 将接收到的数据拷贝��缓冲区
tmos_memcpy((unsigned char *)&iap_rec_data, p_data, w_len);
// 处理OTA数据
Rec_OTA_IAP_DataDeal();
}
5.4 OTA命令解析与处理
Rec_OTA_IAP_DataDeal函数是OTA功能的核心,它解析手机发送的各种命令:
c复制void Rec_OTA_IAP_DataDeal(void)
{
switch(iap_rec_data.other.buf[0]) {
case CMD_IAP_PROM: // 编程命令
handleProgramCommand();
break;
case CMD_IAP_ERASE: // 擦除命令
handleEraseCommand();
break;
case CMD_IAP_VERIFY: // 校验命令
handleVerifyCommand();
break;
case CMD_IAP_END: // 结束命令
handleEndCommand();
break;
case CMD_IAP_INFO: // 信息查询
handleInfoCommand();
break;
default: // 未知命令
OTA_IAP_CMDErrDeal();
break;
}
}
6. OTA升级流程详解
6.1 完整升级流程
-
握手阶段:
- 手机连接设备后发送CMD_IAP_INFO命令
- 设备回复当前状态和Flash信息
-
准备阶段:
- 手机发送CMD_IAP_ERASE命令
- 设备擦除Image B区域
-
数据传输阶段:
- 手机分多次发送CMD_IAP_PROM命令
- 设备将数据写入Image B区域
-
校验阶段:
- 手机发送CMD_IAP_VERIFY命令
- 设备读取Image B数据与手机发送的数据进行比对
-
结束阶段:
- 手机发送CMD_IAP_END命令
- 设备修改标志位并重启
6.2 关键命令处理
6.2.1 编程命令(CMD_IAP_PROM)处理
c复制void handleProgramCommand(void)
{
// 解析地址和长度
OpParaDataLen = iap_rec_data.program.len;
OpAdd = (iap_rec_data.program.addr[0] | (iap_rec_data.program.addr[1] << 8)) * 16;
// 地址重定向到Image B
OpAdd += IMAGE_A_SIZE;
// 写入Flash
uint8_t status = FLASH_ROM_WRITE(OpAdd, iap_rec_data.program.buf, OpParaDataLen);
// 回复状态
OTA_IAP_SendCMDDealSta(status);
}
6.2.2 结束命令(CMD_IAP_END)处理
c复制void handleEndCommand(void)
{
// 关闭所有中断
DisableAllIRQ();
// 修改标志位为IAP模式
SwitchImageFlag(IMAGE_IAP_FLAG);
// 短暂延时确保串口打印完成
mDelaymS(10);
// 系统复位
SYS_ResetExecute();
}
7. 实际操作指南
7.1 固件准备
-
使用WCH_AssemblingFileTool工具将三个HEX文件合并:
- JumpIAP.hex
- IAP.hex
- OTA_APP.hex
-
生成最终的BIN文件并烧录到芯片中
7.2 手机端操作
- 准备要升级的新固件(HEX文件)
- 打开手机OTA升级工具
- 搜索并连接目标设备
- 选择要升级的固件文件
- 开始升级并等待完成提示
7.3 验证升级结果
- 升级完成后设备会自动重启
- 使用BLE调试工具检查设备名称等特征是否已更新
- 验证新功能是否正常工作
经验分享:在实际项目中,建议在APP程序中添加版本号查询功能,方便确认当前运行的固件版本。同时,可以在升级过程中添加LED指示灯或蜂鸣器提示,提升用户体验。
8. 常见问题与解决方案
8.1 升级失败常见原因
-
Flash空间不足:
- 检查编译生成的固件大小是否超过Image A/B区域容量
- 优化代码或调整分区大小
-
数据传输错误:
- 确保蓝牙连接稳定
- 实现完善的校验机制(如CRC校验)
-
标志位错误:
- 检查DataFlash操作是否正确
- 添加默认值处理逻辑
8.2 调试技巧
-
串口日志:
- 在各个关键步骤添加调试打印
- 记录Flash操作结果和状态变化
-
仿真器调试:
- 使用JTAG/SWD接口单步调试
- 检查关键变量和内存内容
-
边界测试:
- 测试最小和最大尺寸的固件
- 模拟断电等异常情况
9. 性能优化建议
-
Flash操作优化:
- 合理设置擦除块大小
- 使用双缓冲提高写入效率
-
蓝牙传输优化:
- 调整MTU大小提高吞吐量
- 实现流量控制避免数据丢失
-
电源管理:
- 升级过程中保持稳定供电
- 添加低电量检测机制
10. 安全考虑
-
固件校验:
- 实现数字签名验证
- 添加CRC校验防止数据损坏
-
回滚机制:
- 保留旧版本固件
- 检测新固件异常时自动回退
-
访问控制:
- 实现配对加密
- 添加权限验证
在实际项目中,我曾遇到过因Flash操作不当导致系统锁死的情况。经过多次实践,我总结出一个重要经验:在进行关键Flash操作前,务必先验证目标地址是否在允许范围内,并确保操作不会破坏正在运行的代码。此外,完善的错误处理和恢复机制是保证OTA可靠性的关键。
