1. 项目概述
在嵌入式设备开发中,固件升级一直是个让人头疼的问题。想象一下,当你的设备已经部署在几百公里外的现场,突然发现一个需要紧急修复的BUG,难道要派人一个个去拆机烧录?这就是OTA(Over-The-Air)技术大显身手的时候了。今天我要分享的是基于STM32F103C8T6的OTA实现方案,这个方案已经在我们的工业设备上稳定运行了两年多。
这个方案的核心特点包括:
- 采用双区存储设计(Bootloader+APP),确保升级失败时能回退
- 通过4G模组ML307A实现远程通信
- 使用W25Q64 Flash扩展存储空间
- 自定义二进制协议保证传输可靠性
- 支持断点续传和超时重试机制
2. 硬件架构设计
2.1 主控芯片选型
选择STM32F103C8T6主要基于以下考虑:
- 性价比高:作为经典的Cortex-M3内核MCU,价格约10元/片
- 资源充足:64KB Flash+20KB RAM,满足基础OTA需求
- 生态完善:有丰富的开发资料和社区支持
注意:实际项目中建议选择Flash更大的型号(如STM32F103RET6),因为64KB在划分双区后剩余空间有限。
2.2 外设连接方案
硬件连接采用模块化设计,关键外设接口如下:
| 外设 | 引脚配置 | 功能说明 |
|---|---|---|
| USART1 | PA9, PA10 | 调试输出和日志打印 |
| USART3 | PB10, PB11 | 与4G模组通信(115200bps) |
| SPI1 | PA4-PA7 | 连接W25Q64 Flash(Quad SPI) |
| LED指示灯 | PC13 | 升级状态指示 |
特别说明SPI Flash的电路设计要点:
- 上拉电阻:SCK、MOSI、MISO需接10K上拉
- 去耦电容:每个电源引脚就近放置0.1μF电容
- 布线原则:信号线长度不超过10cm,避免平行走线
3. 软件架构实现
3.1 存储分区规划
Flash空间分配是OTA系统的核心,我们的分区方案如下:
| 地址范围 | 大小 | 用途 |
|---|---|---|
| 0x08000000-0x08002FFF | 12KB | Bootloader区域 |
| 0x08003000-0x0800EFFF | 48KB | 应用程序区(APP) |
| 0x0800F000-0x0800FFFF | 4KB | 系统参数存储区 |
这个设计的考虑因素:
- Bootloader需要包含完整的通信协议栈,12KB是经过实测的最小需求
- 参数区单独划分,避免频繁擦写影响主程序
- 保留最后1页(2KB)作为备份区,用于存储升级状态标志
3.2 Bootloader设计要点
Bootloader的主要工作流程:
c复制void Bootloader_Main(void)
{
// 1. 初始化硬件
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART3_UART_Init();
// 2. 检查升级标志
if(Check_Update_Flag() == UPDATE_REQUIRED)
{
// 3. 进入升级模式
Start_OTA_Update();
// 4. 验证固件完整性
if(Verify_Firmware() == SUCCESS)
{
// 5. 更新启动标志
Set_Boot_Flag(APP_VALID);
}
}
// 6. 跳转到APP
JumpToApp();
}
关键实现技巧:
- 中断向量重映射:SCB->VTOR = FLASH_BASE | APP_OFFSET
- 堆栈指针检查:确保APP的初始SP值合法
- 超时机制:每个步骤都设置超时限制(通常3-5秒)
3.3 应用程序设计
APP端需要特别注意以下几点:
- 中断向量偏移设置:
c复制// 在main()最开始处添加
SCB->VTOR = FLASH_BASE | 0x3000;
- 通信协议处理优化:
- 使用DMA+空闲中断提高接收效率
- 采用环形缓冲区避免数据丢失
- 实现数据分包重组处理
- 升级触发机制:
c复制void Check_Upgrade_Command(void)
{
if(received_upgrade_cmd)
{
// 保存必要参数到备份寄存器
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR0, UPGRADE_FLAG);
// 软复位
NVIC_SystemReset();
}
}
4. 通信协议详解
4.1 协议栈结构
我们采用双层协议设计:
- 传输层:固定10字节头+变长数据
- 业务层:自定义指令格式
帧结构示例:
code复制传输层头(10B) + [业务头(5B) + 业务数据(N) + 校验(1B)]
4.2 关键指令解析
4.2.1 注册指令(0x01)
code复制68 01 03 00 08 08 08 08 08 08 08 08 88
字段说明:
- 0x68:起始符
- 0x01:注册指令
- 0x03:上行方向
- 0x0008:数据长度8字节
- 后8字节:设备唯一ID
- 0x88:校验和
4.2.2 升级指令(0x15)
服务器下发:
code复制68 15 04 00 02 00 02 85
- 0x0002:版本号
- 0x85:校验和
终端响应:
code复制68 15 14 00 00 91
- 0x14:ACK响应
- 空数据域
4.3 数据分包传输
大文件传输采用滑动窗口协议:
- 终端发送请求包(带当前包序号)
- 服务器返回指定序号的数据包(1KB/包)
- 终端校验后回复ACK
- 失败时自动重试(最多3次)
实测建议:在4G网络环境下,包大小设为512字节时传输效率最佳
5. 服务器端实现
5.1 Python服务端核心逻辑
python复制class OTAServer:
def __init__(self):
self.clients = {}
def handle_client(self, conn, addr):
while True:
data = conn.recv(1024)
if not data:
break
# 解析帧头
header = data[:10]
if header[:6] != b'\x00\x01\x00\x19\x00\x19':
continue
# 处理业务帧
cmd = data[10]
if cmd == 0x01: # 注册
self.handle_register(conn, data[10:])
elif cmd == 0x15: # 升级
self.handle_upgrade(conn, data[10:])
def handle_upgrade(self, conn, data):
# 检查版本号
current_ver = struct.unpack('>H', data[4:6])[0]
if current_ver < NEW_VERSION:
# 发送升级指令
resp = b'\x68\x15\x04\x00\x02' + struct.pack('>H', NEW_VERSION)
resp += bytes([sum(resp) & 0xFF]) # 校验和
conn.send(resp)
# 启动文件传输
self.send_firmware(conn)
5.2 内网穿透配置
使用花生壳的配置要点:
- 申请免费域名
- 配置TCP端口映射(如外网端口12345→内网8000)
- 在路由器设置DMZ主机或端口转发
6. 实战经验分享
6.1 常见问题排查
- 跳转APP后死机
- 检查VTOR设置是否正确
- 确认APP的bin文件烧录地址偏移
- 测量供电电压是否稳定(≥3.3V)
- 数据包丢失
- 增加DMA缓冲区大小(建议≥2KB)
- 添加重传机制
- 检查4G模块天线信号强度(RSSI应≥-85dBm)
- Flash写入失败
- 确保擦除操作在先
- 检查写保护位是否清除
- 验证SPI时序(用逻辑分析仪抓取波形)
6.2 性能优化技巧
- 压缩传输:
- 在服务器端使用LZ77算法压缩bin文件
- MCU端实现简易解压算法
- 实测可减少40%传输量
- 差分升级:
- 使用bsdiff生成差分包
- MCU端实现bspatch
- 适合小版本更新(节省90%流量)
- 安全加固:
- 添加SHA-256校验
- 实现简单的AES加密
- 每个设备单独分配密钥
7. 扩展思考
这个方案在实际部署后,我们还做了以下改进:
- 心跳监测:
- 每5分钟上报运行状态
- 离线自动重连
- 流量统计功能
- 多版本回滚:
- 在SPI Flash保存最近3个版本
- 通过RTC备份寄存器记录版本号
- 支持命令行切换版本
- 远程诊断:
- 通过4G通道实时查看日志
- 支持内存dump分析
- 可远程触发复位
这个项目给我的最大启示是:OTA不是简单的文件传输,而是一套完整的设备生命周期管理系统。从最初的单纯升级功能,到现在已经演变成了我们产品的核心竞争优势之一。