# 前言, 仅记录,初步简单实现串口方式下的json固件包的解析与数据写入,跳转
# 实际上还未做到OTA, 只是预留接口(远程调试还未走通,上位机串口方式走通了)
项目源码:
https://gitee.com/feiniao33/ch32_-ota
一、Flash分区与执行
工程生成.bin文件前提下,观察.bin文件大小即可简单估算空间,合理分配即可。
分区:
app程序起始地址为iap程序跳转地址。注意,MRS下.ld文件:
提供的闪存与ram分配地址起址是逻辑地址,实际执行的地址不是如此的。对于IAP应用,Flash写起始地址一定要注意是物理地址。具体怎么映射查资料即可,不多赘述。
执行:
APP程序跳转到IAP程序,一份实际工程里用到的代码,作为示例:
void OTA::jump_to_bootloader(uint32_t bootloader_addr) {
// 1. 关闭所有外设时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, DISABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, DISABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, DISABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, DISABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, DISABLE);
// 2. 禁用所有中断
__disable_irq();
SysTick->CTLR = 0; // 关闭SysTick
// 3. 清除挂起的中断(使用正确的PFIC寄存器)
NVIC->IPRR[0] = 0xFFFFFFFF; // 清除所有中断挂起状态
// 4. RISC-V架构下的跳转实现
__asm volatile (
"mv a0, %[addr]\n" // 将变量addr的值移动到寄存器a0
"jr a0" // 跳转到a0寄存器中的地址
: // 输出操作数列表(此处为空)
: [addr] "r" (bootloader_addr) // 输入操作数列表
: "a0" // 被破坏的寄存器列表
);
// 5. 确保跳转完成
while(1);
}
二、协议帧简单构建
一)协议:
可触发更新,可通过按下按键 和 发送指令实现。
按下按键,设备会跳到bootloader程序。这个阶段有两个按键可选择,一个取消,一个确认。若取消,则回到原APP.
这里只记录按键触发情形、指令触发也是类似,就不赘述。
设备请求升级:
若确认升级,设备会从e2prom读取信息,然后会发出一帧数据出去:
{
"type": "request",
"device_id": "CH32_001",
"current_fw": "1.0.0",
"trigger": "button"
}
服务器起始帧:
然后服务器会返回一个起始帧并等待设备确认:
{
"type": "start",
"device_id": "CH32_001" // 目标设备ID
"version": "1.0.1",
"file_size": 32768, // 固件总大小
"block_size": 256, // 每帧数据块大小
}
设备应答帧:
设备会确认device_id是否正确,并记录更新信息(如总共多少帧数据),返回应答帧:
这里1就表示下一帧应该传输数据帧的第一帧。
服务器数据帧:
{
"type": "ack",
"device_id": "CH32_001",
"status": "ok",
"next_seq": 1
}
接着服务器开始发出数据帧:
{
"type": "data",
"device_id": "CH32_001",
"seq": 1, // 帧序号(从1开始)
"offset": 0, // 当前帧在固件中的偏移量
"data": Base64 编码的256字节的bin文件块, // 固件数据
"check_code": ""0x2E" // 本帧数据的校验值
}
设备会计算校验和(只用了简单的加法校验),解析并写入固件数据。处理完这一帧后返回应答帧:
{
"type": "ack",
"device_id": "CH32_001",
"seq":1, // 确认的帧序号
"status": "ok", // 或 "error"(需重传)
"next_seq": 2 // 期望下一帧的序号
}
服务器结束帧:
这样,直到最后一帧传输结束, 服务器发送:
{
"type": "end",
"device_id": "CH32_001",
"received_frames": x, // 实际接收的帧数
"error_msg": "" // 失败时的错误信息
}
固件传输结束。
设备固件信息更新:
固件传输完成后,设备通过i2c更新状态信息到e2prom, 格式:
{
"device_id": "CH32_001",
“current_fw": "1.0.0",
“fw_size”: 32468
}
二)E2PROM分区:
0 - 100, 出场信息:
{
"device_id": "DEVICE_123",
"current_fw": "1.0.0",
“fw_size”: x
}
100 - 150,升级请求信息:
{"UpgradeNeed":"yes"}
用于标记用户是否点击了升级按键,升级或者取消之后,置为:
{"UpgradeNeed":"no"}
这是决定程序复位后能不能执行app的关键,更新完固件或者取消更新,都会置状态为:{"UpgradeNeed":"no"},方便程序复位就执行app程序。
如果程序要执行iap程序,app程序设计了相应的触发逻辑(按键、指令),会将状态置为{"UpgradeNeed":"yes"},这样iap程序就会进入iap流程而非返回app程序
三、上位机设计(即服务端设计)
一)固件打包:
按照之前的协议标准,将.bin文件处理成一帧一帧的json, 通过上位机传输至MCU。
注意,数据部分应该base64编码,避免传输错误。同样,MCU端base64解码。
python文件与字典、列表操作,不多赘述
本案例生成一个json数据文件,在Qt程序,只需一行行发送至MCU即可。
二)Qt程序:
主要用到 QFileDialog, pyserial, 和其余基础组件(基于PyQT)。串口读写的方式有多种,定时器,或者多线程均能实现。
重点在于与MCU的对接,以及异常处理。
考虑时间问题,暂时只用的顺序处理
四、设备IAP程序设计
概述
基于裸机的C语言程序程序设计。
使用DMA收发, 空闲中断检测收完,考虑到开发周期,数据解析与烧写目前在串口中断做的。超时机制使用TIM做时钟,base64编码借助开源库,网络资源丰富,不赘述。
解析数据帧用到cJSON,提取校验码除了用到cJSON的提取,还用到C语言strtoul函数将字符串转为数:
对于写入flash, 先解锁,再擦除,再写入,再上锁:
均是"ch32v30x_flash.h"库函数
到APP的跳转:
使用了软中断:
__attribute__((noinline))
void jump_APP(uint32_t addr)
{
__asm("jr a0");
while(1);
}
void SW_Handler(void) __attribute__((interrupt("WCH-Interrupt-fast")));
void SW_Handler() {
jump_APP(APP_ADDR);
}
通过调用:
NVIC_EnableIRQ(Software_IRQn);
NVIC_SetPendingIRQ(Software_IRQn);
执行。
程序架构:
五、测试
测试成功