灵动微MM32 芯片串口升级OTA功能开发6. APP里实现串口指令进入OTA
arm m0芯片 ota 实现
灵动微MM32 芯片串口升级OTA功能开发6. APP里实现串口指令进入OTA

一、SYSCFG重映射
背景
- Bootloader 位于 Flash 起始地址
0x08000000(映射到0x00000000) - 应用程序 位于
0x08004000(Bootloader 占用前 16KB) - 当应用程序运行时,如果发生中断,CPU 仍然会从
0x00000000(即 Bootloader 的向量表)读取中断处理函数地址 - 这导致应用程序的中断无法正常工作
在 Cortex-M0 架构的微控制器上实现 Bootloader 时,由于Cortex-M0 不支持 VTOR(向量表偏移寄存器)。这意味着向量表必须固定在 0x00000000 地址,无法通过软件动态更改。
一般有两方式处理应用程序的中断映射问题:
- 硬件重映射(芯片带 SYSCFG 或类似 remap 寄存器)
跳转前先把 APP 向量表(通常 48×4 字节)整个 memcpy 到 SRAM 起始 0x2000 0000
调用库函数 HAL_SYSCFG_SetRemapMemory(SYSCFG_BOOT_SRAM) 把 SRAM 映射到 0x0000 0000
再把 MSP、PC 设成 APP 的栈顶与 Reset_Handler,完成跳转
→ 此后任何中断 CPU 都会从 SRAM 里的新表取地址,自然进入 APP 的 ISR - 软件转发(芯片无 remap 功能,或不想占 SRAM)
Bootloader 的向量表保留在 Flash(0x0800 0000 起),但所有中断向量统一填同一个“转发函数” VectorRedirect()
进入 APP 前把 APP 向量表基地址(如 0x08005000)记到全局变量 userAppStart
VectorRedirect() 里用 SCB->ICSR & 0x1FF 拿到当前 IRQ 号,按 (userAppStart + 164 + irq4) 取出 APP 真正的 ISR 地址,再 BX 过去
→ 中断先跑到 Bootloader,但立即跳到 APP 的 ISR,对应用完全透明
本文使用方案1.
工作原理
- 向量表复制:将应用程序的向量表从 Flash(
0x08004000)复制到 RAM 起始地址(0x20000000) - 硬件重映射:通过 SYSCFG 的
MEM_MODE位设置为0b11,将 RAM 映射到0x00000000 - 中断处理:当发生中断时,CPU 从
0x00000000(现在映射到 RAM)读取向量表,从而使用应用程序的中断处理函数
实现细节
1. 向量表重映射函数
/**
-
@brief 跳转到主程序
-
@note 跳转前清理所有中断和外设状态,确保app启动时环境干净
*/
void BootJump_ToApplication(void)
{
uint32_t app_stack_ptr = *((volatile uint32_t *)APPLICATION_START_ADDR);
uint32_t app_reset_vector = *((volatile uint32_t *)(APPLICATION_START_ADDR + 4));/* 主程序复位处理函数指针 */
void (app_reset_handler)(void) = (void ()(void))(app_reset_vector);/* 关闭所有中断(必须在清理外设之前) */
__disable_irq();/* ========== 关键:清理中断相关残留状态 ========== /
/ 按照最佳实践,在跳转前清理所有使用过的外设和中断状态 *//* 1. 清理UART1(如果bootloader使用了串口) /
/ 1.1 直接操作外设寄存器关闭中断使能位 /
UART1->IER &= ~(UART_IER_RX | UART_IER_TX); / 禁用UART1接收和发送中断 *//* 1.2 清除NVIC的中断挂起位 */
NVIC_ClearPendingIRQ(UART1_IRQn);/* 1.3 禁用NVIC中的对应中断 */
NVIC_DisableIRQ(UART1_IRQn);/* 1.4 复位UART1外设(推荐,确保外设回到默认状态) /
RCC->APB2RSTR |= RCC_APB2RSTR_UART1;
volatile uint32_t delay = 100;
while(delay–); / 等待复位完成 /
RCC->APB2RSTR &= ~RCC_APB2RSTR_UART1;
delay = 100;
while(delay–); / 等待复位释放 *//* 1.5 关闭UART1外设时钟(彻底清理,app会重新初始化) /
RCC->APB2ENR &= ~RCC_APB2ENR_UART1; / 关闭UART1时钟 *//* 注意:不复位GPIOB,因为app会重新配置GPIO,复位可能导致时序问题 /
/ 也不关闭GPIOB时钟,因为app会立即使用,避免时钟开关的时序问题 *//* 2. 清理其他可能使用的外设(如定时器、DMA等) /
/ 如果bootloader使用了其他外设,在这里添加清理代码 /
/ 例如:- TIM2->DIER &= ~TIM_DIER_UIE; // 禁用定时器中断
- NVIC_DisableIRQ(TIM2_IRQn);
- NVIC_ClearPendingIRQ(TIM2_IRQn);
- RCC->APB1RSTR |= RCC_APB1RSTR_TIM2; // 复位定时器
- …
*/
/* 2. 清理SysTick(如果bootloader使用了SysTick) /
/ 禁用SysTick /
SYSTICK_CTRL = 0;
/ 清除SysTick计数器和重载值 /
SYSTICK_LOAD = 0;
SYSTICK_VAL = 0;
/ 清除SysTick中断pending状态 */
SCB_ICSR_ADDR |= SCB_ICSR_PENDSTCLR;/* 3. 关键:清除所有NVIC中断pending状态(确保没有残留的中断请求) /
/ Cortex-M0的NVIC只有ICER[0]和ICPR[0],支持32个中断 /
/ 清除所有可能的中断pending状态 /
NVIC->ICPR[0] = 0xFFFFFFFF; / 清除前32个中断的pending状态 *//* 4. 禁用所有NVIC中断(确保app启动时中断系统干净) /
NVIC->ICER[0] = 0xFFFFFFFF; / 禁用前32个中断 *//* 5. 注意:MM32SPIN0280是Cortex-M0,不支持VTOR (向量表偏移寄存器)
- 向量表始终在 0x00000000 (映射到 Flash 0x08000000)
- 应用程序会在SystemInit()中使用SYSCFG硬件重映射将向量表复制到RAM
- 并设置MEM_MODE使0x00000000映射到RAM,从而使用应用程序的中断处理函数
- 因此这里不需要设置VTOR(Cortex-M0不支持VTOR)
*/
/* 6. 确保内存屏障,保证所有清理操作完成后再跳转 /
__DSB(); / 数据同步屏障 /
__ISB(); / 指令同步屏障 *//* 设置主堆栈指针 */
__set_MSP(app_stack_ptr);/* 跳转到主程序 */
app_reset_handler();/* 不应该执行到这里 */
while (1) {
}
}
2. 在 SystemInit() 中调用
void SystemInit(void)
{
// 注意:从bootloader跳转过来时,时钟已经配置好了,不需要重置
// SetSysClockToDefaultHSI(); // 禁用,避免重置bootloader配置的时钟
// SetSysClock(); // 禁用,使用bootloader配置的时钟
/* 向量表重映射到RAM(用于Bootloader场景)
* 必须在启用中断之前完成
*/
AppVectorTable_RemapToRAM();
}
3. 链接脚本配置
需要确保链接器不会在 0x20000000-0x20000100 范围内放置数据,为向量表预留空间:
LR_IROM1 0x08004000 0x0001C000 { ; Flash区域
ER_IROM1 0x08004000 0x0001C000 {
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
*.o (CHECKSUM, +Last)
}
; RAM数据从0x20000100开始,预留0x20000000-0x20000100给向量表
RW_IRAM1 0x20000100 0x00001F00 { ; 0x2000 0100 – 0x2000 1FFF
.ANY (+RW +ZI)
}
; 注意:0x20000000-0x20000100 (256字节) 预留给向量表
; 向量表通过AppVectorTable_RemapToRAM()函数从Flash复制到这里
}
4. MEM_MODE 设置方式
正确写法:
SYSCFG->CFGR = (SYSCFG->CFGR & ~SYSCFG_CFGR_MEM_MODE) /* 清除2位 /
| (0x3 << 0); / 写入0b11 */
5. 向量表地址
重要:向量表必须放在 RAM 起始地址(0x20000000),因为 SYSCFG 的 MEM_MODE 设置为 RAM 映射后,0x00000000 映射到 RAM 起始地址,而不是 RAM 的任意地址。
工作流程
- Bootloader 跳转:Bootloader 跳转到应用程序的 Reset_Handler
- SystemInit() 执行:调用
AppVectorTable_RemapToRAM()- 复制向量表到 RAM(
0x20000000) - 设置 SYSCFG MEM_MODE =
0b11
- 复制向量表到 RAM(
- main() 执行:初始化外设,启用中断
- 中断发生:CPU 从
0x00000000(映射到 RAM)读取向量表,跳转到应用程序的中断处理函数
二、应用程序串口指令
1. 指令格式
- 编码:ASCII 字符串
- 大小写:不敏感(自动转换为小写处理)
- 结束符:
\r\n或\n - 最大长度:64 字节(包括结束符)
- 空格处理:自动去除首尾空格
2. 响应格式
成功响应
OK=[数据]\r\n
OK=:固定前缀[数据]:可选的数据内容(某些命令会返回数据)\r\n:换行符
错误响应
ERR=<错误码>[:错误描述]\r\n
ERR=:固定前缀<错误码>:单个数字(0-9)[:错误描述]:可选的错误描述(以冒号分隔)
3. 支持的指令
1. 版本查询指令
指令格式
ver
或
version
功能说明
查询应用程序的版本号信息。
响应示例
OK=v1.0.0\r\n
版本号格式:v<主版本号>.<次版本号>.<修订号>
实现细节
- 版本号定义在
app_cmd.h中:#define APP_VERSION_MAJOR 1 #define APP_VERSION_MINOR 0 #define APP_VERSION_PATCH 0
2. 进入升级模式指令
指令格式
ota
功能说明
进入 OTA(Over-The-Air)升级模式。
注意:当前版本中此指令已定义但功能未完全实现(标记为 TODO)。
响应示例
OK=\r\n
使用场景
用于触发固件升级流程,通常需要配合 Bootloader 使用。
3. 进入 Bootloader 模式指令
指令格式
boot
功能说明
设置 Boot Magic 值并重启系统,使系统在下次启动时进入 Bootloader 模式。
工作流程
- 写入 Boot Magic 值(
0x5A5A)到备份寄存器(BKP->DR1) - 发送成功响应
- 延时确保响应发送完成
- 执行系统复位(
NVIC_SystemReset())
响应示例
OK=\r\n
(响应发送后系统会立即重启)
错误响应
如果写入 Boot Magic 失败:
ERR6:Write magic failed\r\n
实现细节
- Boot Magic 存储在备份寄存器
BKP->DR1 - Magic 值:
0x5A5A(表示需要进入 Bootloader) - 系统重启后,Bootloader 会检查此值,如果匹配则进入升级模式
4. 退出升级模式指令
指令格式
exit
功能说明
清除 Boot Magic 值并重启系统,使系统在下次启动时正常运行应用程序。
工作流程
- 清除 Boot Magic 值(写入
0x0000到备份寄存器) - 发送成功响应
- 延时确保响应发送完成
- 执行系统复位(
NVIC_SystemReset())
响应示例
OK=\r\n
(响应发送后系统会立即重启)
错误响应
如果清除 Boot Magic 失败:
ERR=7:Clear magic failed\r\n
使用场景
- 在 Bootloader 升级模式下,完成固件升级后使用此指令退出升级模式
- 确保系统下次启动时正常运行应用程序
错误码说明
| 错误码 | 描述 | 说明 |
|---|---|---|
| 4 | Invalid command | 无效的命令 |
| 6 | Write magic failed | 写入 Boot Magic 失败 |
| 7 | Clear magic failed | 清除 Boot Magic 失败 |
命令处理流程
接收流程
- 字符接收:通过
AppUART_ReceiveChar()从串口接收缓冲区读取字符 - 缓冲区管理:使用 64 字节的命令缓冲区存储接收到的字符
- 命令识别:检测到换行符(
\r或\n)时,标记命令接收完成 - 溢出处理:如果缓冲区溢出,清空缓冲区并丢弃当前命令
处理流程
- 预处理:
- 去除首尾空格(
trim_string()) - 转换为小写(
to_lowercase())
- 去除首尾空格(
- 命令匹配:使用
strcmp()匹配命令字符串 - 执行处理:调用对应的处理函数
- 发送响应:根据处理结果发送成功或错误响应
代码示例
void AppCMD_Process(void)
{
char ch;
/* 接收串口数据 */
while (AppUART_ReceiveChar(&ch)) {
if (ch == '\n' || ch == '\r') {
if (cmd_index > 0) {
cmd_buffer[cmd_index] = '\0';
cmd_ready = 1;
}
}
else {
if (cmd_index < (CMD_BUFFER_SIZE - 1)) {
cmd_buffer[cmd_index++] = ch;
}
else {
/* 缓冲区溢出,清空缓冲区 */
cmd_index = 0;
cmd_ready = 0;
}
}
}
/* 处理已接收完成的命令 */
if (cmd_ready) {
execute_command(cmd_buffer);
cmd_index = 0;
cmd_ready = 0;
memset(cmd_buffer, 0, sizeof(cmd_buffer));
}
}
三、Boot Magic 机制
1. BKP寄存器说明:
BKP 寄存器属于备份域(Backup Domain),这是一个独立的电源域,具有以下特性:
- 独立电源:备份域通常有独立的电源供应(VBAT),即使主电源断电,只要 VBAT 有电,备份域的数据就不会丢失
- 独立复位:备份域有独立的复位控制,普通系统复位不会影响备份域
2. Magic 值:
0x5A5A:进入 Bootloader 模式0x0000:正常运行应用程序- 检查时机:系统复位后,Bootloader 启动时检查
3. 写入代码
u8 BootMagic_Write(u16 value)
{
/* 确保备份域写保护已解除 */
if ((PWR->CR & PWR_CR_DBP) == 0) {
/* 如果写保护未解除,先解除 */
PWR->CR |= PWR_CR_DBP;
/* 等待写保护解除 */
volatile u32 i;
for (i = 0; i < 10; i++);
}
/* 直接写入BKP寄存器
* BKP寄存器在系统复位后会保持值,这正是Boot Magic机制所需要的特性
*/
BKP->DR1 = (u32)value;
/* 确保写入完成(内存屏障) */
__DSB();
__ISB();
/* 等待至少5个APB1总线周期(手册要求)
* APB1通常运行在较低频率,这里使用循环延时确保时序要求
*/
{
volatile u32 i;
for (i = 0; i < 20; i++); /* 确保超过5个周期 */
}
/* 验证写入是否成功 */
if ((BKP->DR1 & 0xFFFF) != value) {
return 0; /* 写入失败 */
}
return 1;
}
4. bootloader实现magic值判断和跳转等待握手
1. 启动时检查 Magic 值
Bootloader 启动后首先初始化 BKP 模块,读取备份寄存器中的 Magic 值:
/* 初始化boot magic模块 */
BootMagic_Init();
/* 读取并检查magic值 */
if (!BootMagic_CheckBootMode()) {
/* Magic值不匹配,跳转到应用程序 */
if (BootJump_IsApplicationValid()) {
BootJump_ToApplication();
}
}
2. 未收到握手时清除 BKP并跳转应用
/* 10秒内未收到握手命令,清除BKP并跳转到应用程序 */
BSP_LOG_Print("Handshake timeout, clearing BKP and jumping to application...");
/* 清除BKP Magic值 */
BootMagic_Clear();
运行效果:
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐

所有评论(0)