mcu loader升级固件原理与实现

2024-09-01 21:28

本文主要是介绍mcu loader升级固件原理与实现,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

1 mcu loader升级固件原理

        mcu 固件有两部分,如下图所示,一部分是 loader.bin,一部分是 app.bin,将两部分的固件合并在一起烧录进 mcu 的 flash 当中。mcu 上电进入loader 模式执行 loader.bin 部分的程序,然后读取 flash 某个地址的值,判断是否进入 app 模式执行app.bin 部分的程序。

        用户需要升级 mcu 固件时,soc 通过 i2c 发送一个信号给 mcu,mcu 进入 loader 模式,然后通过 i2c 通信接收 soc 发送过来的固件数据,更新 flash 中的 app.bin 这一块区域,更新完毕后,跳转到 app 模式中执行程序。

2 mcu进入loader模式的方法

        如下图所示, mcu 固件有一个中断向量表,在固件的头部,loader.bin 和 app.bin 的头部都会有一个中断向量表。 mcu 上电时会从 flash 的首地址地区(0x08000000)某个区块作为中断向量表,所以 loader 和 app 模式下发生中断响应实际寻找是 loader 下中断向量表从而跳转到相应的中断服务程序中执行。

        根据这个原理,可以通过触发一个中断进入 loader 模式。代码如下所示,是通过汇编执行一个"svc #13" 指令从而触发一个 scv 中断进入 loader 模式。#13 是一个参数,执行 svc 必须带一个参数,根据这个参数执行不同的svc处理函数,本次代码不对该参数作区分处理。

void RebootToLoader(void){
printf("switch to loader\n\r");
__asm volatile ("svc #13");
}

3 loader部分代码重写中断向量操作

3.1 重写中断向量表

        由于 app 模式下响应的中断,会去 loader 下寻找中断向量,从而进入相应的中断服务程序中执行, 因此 loader 下对中断向量表进行了重写,代码如下所示,除了栈顶(__initial_sp)和复位中断(Reset_Handler)没变化,其它中断向量都改成了一个通用的中断向量(COMMON_IRQHANDLER)。这样修改之后,app 模式下响应的中断,会跳转到 COMMON_IRQHANDLER 中断向量下执行程序。

; Vector Table Mapped to Address 0 at ResetAREA    RESET, DATA, READONLYEXPORT  __VectorsEXPORT  __Vectors_EndEXPORT  __Vectors_Size__Vectors       DCD     __initial_sp ; Top of StackDCD     Reset_Handler ; Reset HandlerDCD     COMMON_IRQHANDLER ; NMI HandlerDCD     COMMON_IRQHANDLER ; Hard Fault HandlerDCD     COMMON_IRQHANDLER ; ReservedDCD     COMMON_IRQHANDLER ; ReservedDCD     COMMON_IRQHANDLER ; ReservedDCD     COMMON_IRQHANDLER ; ReservedDCD     COMMON_IRQHANDLER ; ReservedDCD     COMMON_IRQHANDLER ; ReservedDCD     COMMON_IRQHANDLER ; ReservedDCD     COMMON_IRQHANDLER ; SVCall HandlerDCD     COMMON_IRQHANDLER ; ReservedDCD     COMMON_IRQHANDLER ; ReservedDCD     COMMON_IRQHANDLER ; PendSV HandlerDCD     COMMON_IRQHANDLER ; SysTick Handler; External InterruptsDCD     COMMON_IRQHANDLER ; Window WatchdogDCD     COMMON_IRQHANDLER ; PVD through EXTI Line detectDCD     COMMON_IRQHANDLER ; RTC through EXTI LineDCD     COMMON_IRQHANDLER ; FLASHDCD     COMMON_IRQHANDLER ; RCCDCD     COMMON_IRQHANDLER ; EXTI Line 0 and 1 ; chizhiling 2021.8.13DCD     COMMON_IRQHANDLER ; EXTI Line 2 and 3 ; chizhiling 2021.8.13DCD     COMMON_IRQHANDLER ; EXTI Line 4 to 15 ; chizhiling 2021.8.13DCD     COMMON_IRQHANDLER ; ReservedDCD     COMMON_IRQHANDLER ; DMA1 Channel 1DCD     COMMON_IRQHANDLER ; DMA1 Channel 2 and Channel 3DCD     COMMON_IRQHANDLER ; DMA1 Channel 4, Channel 5, Channel 6 and Channel 7DCD     COMMON_IRQHANDLER ; ADC1, COMP1 and COMP2 DCD     COMMON_IRQHANDLER ; LPTIM1DCD     COMMON_IRQHANDLER ; USART4 and USART5DCD     COMMON_IRQHANDLER ; TIM2DCD     COMMON_IRQHANDLER ; TIM3DCD     COMMON_IRQHANDLER ; TIM6DCD     COMMON_IRQHANDLER ; TIM7DCD     COMMON_IRQHANDLER ; ReservedDCD     COMMON_IRQHANDLER ; TIM21DCD     COMMON_IRQHANDLER ; I2C3DCD     COMMON_IRQHANDLER ; TIM22DCD     COMMON_IRQHANDLER ; I2C1DCD     COMMON_IRQHANDLER ; I2C2DCD     COMMON_IRQHANDLER ; SPI1DCD     COMMON_IRQHANDLER ; SPI2DCD     COMMON_IRQHANDLER ; USART1DCD     COMMON_IRQHANDLER ; USART2DCD     COMMON_IRQHANDLER ; LPUART1DCD     COMMON_IRQHANDLER ; CEC__Vectors_End__Vectors_Size  EQU  __Vectors_End - __Vectors

3.2 通用中断服务程序

        通用中断( COMMON_IRQHANDLER)程序代码如下所示,作用是:把 LR 寄存器的值保存到 R1 寄存器,判断当前 app 模式下使用的栈寄存器是 PSP 还是 MSP,并把当前栈复制给 R0 寄存器,把 R0、R1、R2 和 R3 寄存器压到栈中,并跳转到common_irqhandler 函数下执行程序,执行common_irqhandler 完毕后后对 R0、R1、R2 和 R3 寄存器执行出栈操作,跳转到原地址执行。

void COMMON_IRQHANDLER(void)
{__asm("MOVS    r0, #4");__asm("MOV     r1, LR");__asm("TST     r0, r1");__asm("BEQ     Stack_Use_MSP");__asm("MRS     R0, PSP");       // stack use PSP__asm("B       Get_LR_and_Branch");__asm("Stack_Use_MSP:");__asm("MRS     R0, MSP");       // stack use MSP__asm("Get_LR_and_Branch:");__asm("MOV     R1, LR");        // LR current value__asm("PUSH    {r0, r1, r2, r3}");      // save r0-r3__asm("BL      common_irqhandler");__asm("POP     {r0, r1, r2, r3}");      // restore r0-r3__asm("BX      r1");
}

        common_irqhandler 函数如下所示,功能是:获取当前触发中断的中断向量编号保存到index 变量当中,由 app 模式下触发的中断app_ready 为 1,由 loader 模式下触发的中断app_ready 为 0。device_irq_check 函数检测该中断是不是SVC 中断,是的话就进入 loader 模式,不是的话就跳转到 app 下的对应中断中执行。loader 模式下只开了 i2c 中断,用于接收 soc 发送过来的数据,从而更新 flash 中 app 部分的固件,触发了 i2c 中断,会在device_irq_handler 函数检测然后执行 i2c 中断函数i2c_handler。

        举两个例子:

        ①app 模式下触发了 I2C1 中断, I2C1 的中断向量编号为 39(0x27),所以index 的值为 39(0x27),在函数device_irq_check中判断中断编号(11-16)不等于 SVC_IRQn(-5),APROM_START 等于 0x08001800,handler = 0x08001800 + 0x27*4 =0x800189c,0x800189c 地址刚好存储的是 app 代码中的 I2C1 的中断服务程序地址,从而跳转到 app 程序中的I2C1 的中断服务程序中执行。

        ②app 模式下触发了 SVC 中断,I2C1 的中断向量编号为 11(0x0b),所以index 的值为 11(0x0b),在函数device_irq_check中判断中断编号(11-16)等于 SVC_IRQn(-5),所以status 等于 1,对 flash 某个位置写 2,标志 app 固件未更新好,将 loader 的起始位置复制给 PC 寄存器,程序将重新执行 loader 程序。

void common_irqhandler(u32 stack[], u32 LR)
{void (**handler)(void);u32 index = __get_xpsr();u32 status = 0;if (app_ready) {handler = (void *)APROM_START;index = index & 0xff;handler += index;status = device_irq_check(index);if (status == 1) {/* Entry loader mode */app_info_set(0x00000002);stack[pc] = (u32)enter_loader;} else {/* Exec app handler */(*handler)();}} else {device_irq_handler(index & 0xff);}
}

        common_irqhandler 函数中device_irq_check 函数如下所示,该函数用于检测 app 模式下产生的中断是是否是 svc 中断。

u32 device_irq_check(u32 vector_index){int irq = vector_index - 16;switch (irq) {case HardFault_IRQn:break;case SVC_IRQn:/* entry loader mode */return 1;default:break;}return 0;
}

        common_irqhandler 函数中device_irq_handler函数如下所示,该函数用于检测 loader 模式下产生的中断是是否是 I2C1 中断并执行 I2C1 中断的处理函数i2c_handler。

void device_irq_handler(u32 vector_index){int irq = vector_index - 16;switch (irq) {case HardFault_IRQn:break;case SysTick_IRQn:break;case I2C1_IRQn:i2c_handler();default:NVIC_ClearPendingIRQ(irq);break;}return ;
}

4 mcu进入app模式的方法

        当 mcu 烧录固件后首次上电或者在 loader 模式下更新完 mcu 的 app 固件后,mcu 会从 loader 模式下跳转到 app 模式,跳转代码如下所示,使用isp_exec 函数传入 app 在 flash 中的起始地址进行跳转。

        mcu 固件的第一个 4 字节的地址存储的是栈顶指针,第二个 4 字节的地址存储的是复位中断指针,__jump 函数中 r0(参数 new_sp 的值)拷贝到msp 寄存器(栈寄存器)中,然后跳转到 r1(参数 new_pc 的值),也就是复位中断指针,从而执行 app 模式下的程序。

#define APROM_START  0x08001400isp_exec(APROM_START);void isp_exec(u32 image_address)
{u32 *vector = (u32 *)image_address;u32 initial_sp;u32 reset_handler;initial_sp = vector[0];reset_handler = vector[1];/* Inital Stack Pointer and Jump to reset handler */__jump(initial_sp, reset_handler);
}void __jump(u32 new_sp, u32 new_pc) {__asm volatile ("MSR msp, r0");	//new_sp__asm volatile ("bx r1");	//
}

5 loader更新mcu的app固件过程

5.1 mcu从app进入loader模式

        在系统上通过发送一个 i2 命令“i2cset -y -f 3 0x15 0x60 0x01 b”,3 表示 i2c 总线 3,0x15 表示 mcu 的 i2c 地址,0x60 表示 mcu 的寄存器地址,0x01 表示要设置的值。mcu 接收到要设置的寄存器为 0x60(固件更新寄存器) 后会去执行"svc #13"汇编指令来触发一个 svc 中断从而进入 loader 模式。

5.2 mcu更新app固件

        更新 mcu 的 app 固件使用mcu_upgrade_tool 工具,在系统下执行命令“./mcu_upgrade_tool /dev/i2c-3 h076_mcu_app.bin ”即可,更新过程如下图所示。原理是系统发送 i2c 命令和数据给 mcu ,读出 mcu 的 loader 版本号,检查当前 flash 上 app 固件和要更新的固h076_mcu_app.bin 的 checksum 是否相同,不同 loader 将擦除 flash 中 app 区块的数据,然后读取h076_mcu_app.bin 数据更新到 flash 中。

        loader 代码中负责更新 mcu 的 app 固件的函数是isp_handler,代码如下所示,通过switch 来区分不同的命令参数执行对应的功能:

        CMD_GET_LD_VERSION :将 loader 的版本数据发送给 soc。

        CMD_UPDATE_APROM :通过写 flash (app 区域)来更新 mcu 的 app 固件。

        CMD_ERASE_APROM: 擦除 flash(app 区域)。

        CMD_RUN_APROM :写 flash 某个地址来作为固件更新完成标志,跳转到 app 模式下执行程序。

        CMD_RUN_LDROM: 跳转到 loader 模式下执行程序。

void isp_handler(void)
{u32 temp = 0;struct isp_package package = {0};struct isp_package respond = {0};if(! isp_data.need_handle) {return ;}__memcpy(&package, isp_data.rx_buf, sizeof(struct isp_package));isp_data.need_handle = 0;respond.command = package.command;respond.length += 1;switch(package.command) {case CMD_GET_LD_VERSION:__memcpy(respond.byte, version_info, sizeof(version_info));/* Read version of loader */respond.length += sizeof(version_info);break;case CMD_UPDATE_APROM:if (package.flash_length > 16) {break;}/* Update APROM */flash_write_safe(package.flash_address,package.flash_length, package.flash_data);break;case CMD_ERASE_APROM:flash_erase_safe(package.flash_address, package.flash_length);break;case CMD_RUN_APROM:app_info_set(APP_INFO_STATUS_OK);/* Jump to APROM *///isp_exec(APROM_START);device_reset();break;case CMD_RUN_LDROM:device_reset();break;case CMD_GET_AP_CHECKSUM:/*Caculate checksum of aprom */respond.flash_address = package.flash_address;respond.flash_length = package.flash_length;temp = Checksum((u8 *)respond.flash_address, respond.flash_length);respond.flash_data[0] = ((temp >> 0) & 0xff);respond.flash_data[1] = ((temp >> 8) & 0xff);respond.flash_data[2] = ((temp >> 16) & 0xff);respond.flash_data[3] = ((temp >> 24) & 0xff);respond.length += (4 + 4 + 4);break;case CMD_READ_APROM:/* Read APROM */if (package.flash_length > 16) {break;}respond.flash_address = package.flash_address;respond.flash_length = package.flash_length;respond.length += (4 + 4 + respond.flash_length);flash_read_safe(respond.flash_address,respond.flash_length, respond.flash_data);break;case CMD_DEVICE_INFO:respond.flash_address = APROM_START;respond.flash_length = APROM_SIZE;respond.length += (4 + 4);break;default:break;}isp_send((void *)&respond, sizeof(struct isp_package));
}

        device_reset 代码如下所示,功能是:关闭所有中断,然后跳转到 loader 起始位置重新执行程序。

void device_reset(void)
{/* Disable all interrupts */NVIC->ICER[0] = (0xffffffffUL);SysTick->CTRL = (0x00000000UL);/* This MCU control the power of all system *//* GPIO state can not reset */isp_exec(LDROM_START);
}

6 mcu固件合并的方法

        通过脚本mkimg.sh 将 loader 和 app 的固件合并在一起,脚本如下所示。

#!/bin/sh -exIMAGE_BIN=out/h076_mcu.bin
APP_HEX=./Project/MDK/Out/APM32F072/CEC_Controler.hexrm -r out/
mkdir out/# flash 64k
dd if=/dev/zero of=$IMAGE_BIN bs=1k count=64objcopy -I ihex -O binary $APP_HEX out/h076_mcu_app.bin# loader: [0-6k]
dd if=./loader.bin of=$IMAGE_BIN conv=notrunc bs=1k seek=0
dd if=out/h076_mcu_app.bin of=$IMAGE_BIN conv=notrunc bs=1k seek=6#objcopy -I binary -O ihex $IMAGE_BIN out/mcu.hex --set-start=0x08000000
objcopy --change-address=0x08000000 -I binary -O ihex $IMAGE_BIN out/h076_mcu.hex# build app_info.bin #
#
# dd if=/dev/zero of=app_info.bin bs=1 count=128
# vim -b app_info.bin
# :%!xxd
# :%!xxd -r
# :wq

        loader.bin 和h076_mcu_app.bin 合并成h076_mcu.bin,h076_mcu.bin 一共 64k(根据 MCU 的 flash 大小来调整,如果 flash 的大小为 32k,h076_mcu.bin 应设置为 32k), 其中loader.bin 在h076_mcu.bin 的前 6k 位置中,后 58 k 为h076_mcu_app.bin,如图所示。

7 keil 工程配置

7.1 loader代码工程的配置

        由于 keil 编译生成的固件文件(如*.bin 或*.hex 文件)的头部存储的是栈顶指针和中断向量,因此需要配置 flash 的起始位置,这样在 common_irqhandler 函数(详细介绍请看 3.2 部分)中才能正确进入相应的中断服务程序中执行。

        下图是 loader 代码的配置,因为 flash 在 mcu 眼中的首地址是 0x08000000,loader.bin 的大小为 6k,也就是 0x1800。

        下面是 loader.bin 的头部文件,可以看到第二个四字节数据位 0x080000D1,这就是 loader 模式的复位中断入口地址。第三个字节数据以及后面那一大块数据都是 0x08000179,也就是COMMON_IRQHANDLER 中断入口地址,发生除了Reset_Handler 的大部分中断,都会跳到 0x08000179 这个地址去执行COMMON_IRQHANDLER 中断程序。

7.2 app代码工程的配置

        下图是 app 代码的配置,因为 0x08000000 + 0x1800 =0x08001800,0x1800 是 loader.bin 的大小, flash 的大小为 0x10000,0x10000 - 0x1800 = 0xE800。

        下面是 app.bin 的头部文件,可以看到第二个四字节数据为 0x080018D5,这就是 app 模式的复位中断入口地址。如果 app 模式下发生 I2C1 中断,中断会在 0x0800009C 处取出 I2C1 的中断入口地址,也就是 0x08000179(COMMON_IRQHANDLER),执行COMMON_IRQHANDLER 中断服务函数,然后跳到 common_irqhandler 函数中执行,根据中断号计算出 APP 模式下的 I2C1 中断入口地址存储在 0x800189C 地址处,从该地址中取值为 0x08002A41,然后跳到该地址下执行 app 模式下 I2C1 中断程序(I2C1_IRQHandler)。

这篇关于mcu loader升级固件原理与实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1128085

相关文章

使用Python实现IP地址和端口状态检测与监控

《使用Python实现IP地址和端口状态检测与监控》在网络运维和服务器管理中,IP地址和端口的可用性监控是保障业务连续性的基础需求,本文将带你用Python从零打造一个高可用IP监控系统,感兴趣的小伙... 目录概述:为什么需要IP监控系统使用步骤说明1. 环境准备2. 系统部署3. 核心功能配置系统效果展

Python实现微信自动锁定工具

《Python实现微信自动锁定工具》在数字化办公时代,微信已成为职场沟通的重要工具,但临时离开时忘记锁屏可能导致敏感信息泄露,下面我们就来看看如何使用Python打造一个微信自动锁定工具吧... 目录引言:当微信隐私遇到自动化守护效果展示核心功能全景图技术亮点深度解析1. 无操作检测引擎2. 微信路径智能获

redis中使用lua脚本的原理与基本使用详解

《redis中使用lua脚本的原理与基本使用详解》在Redis中使用Lua脚本可以实现原子性操作、减少网络开销以及提高执行效率,下面小编就来和大家详细介绍一下在redis中使用lua脚本的原理... 目录Redis 执行 Lua 脚本的原理基本使用方法使用EVAL命令执行 Lua 脚本使用EVALSHA命令

Python中pywin32 常用窗口操作的实现

《Python中pywin32常用窗口操作的实现》本文主要介绍了Python中pywin32常用窗口操作的实现,pywin32主要的作用是供Python开发者快速调用WindowsAPI的一个... 目录获取窗口句柄获取最前端窗口句柄获取指定坐标处的窗口根据窗口的完整标题匹配获取句柄根据窗口的类别匹配获取句

在 Spring Boot 中实现异常处理最佳实践

《在SpringBoot中实现异常处理最佳实践》本文介绍如何在SpringBoot中实现异常处理,涵盖核心概念、实现方法、与先前查询的集成、性能分析、常见问题和最佳实践,感兴趣的朋友一起看看吧... 目录一、Spring Boot 异常处理的背景与核心概念1.1 为什么需要异常处理?1.2 Spring B

Python位移操作和位运算的实现示例

《Python位移操作和位运算的实现示例》本文主要介绍了Python位移操作和位运算的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一... 目录1. 位移操作1.1 左移操作 (<<)1.2 右移操作 (>>)注意事项:2. 位运算2.1

如何在 Spring Boot 中实现 FreeMarker 模板

《如何在SpringBoot中实现FreeMarker模板》FreeMarker是一种功能强大、轻量级的模板引擎,用于在Java应用中生成动态文本输出(如HTML、XML、邮件内容等),本文... 目录什么是 FreeMarker 模板?在 Spring Boot 中实现 FreeMarker 模板1. 环

Qt实现网络数据解析的方法总结

《Qt实现网络数据解析的方法总结》在Qt中解析网络数据通常涉及接收原始字节流,并将其转换为有意义的应用层数据,这篇文章为大家介绍了详细步骤和示例,感兴趣的小伙伴可以了解下... 目录1. 网络数据接收2. 缓冲区管理(处理粘包/拆包)3. 常见数据格式解析3.1 jsON解析3.2 XML解析3.3 自定义

SpringMVC 通过ajax 前后端数据交互的实现方法

《SpringMVC通过ajax前后端数据交互的实现方法》:本文主要介绍SpringMVC通过ajax前后端数据交互的实现方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价... 在前端的开发过程中,经常在html页面通过AJAX进行前后端数据的交互,SpringMVC的controll

Spring Security自定义身份认证的实现方法

《SpringSecurity自定义身份认证的实现方法》:本文主要介绍SpringSecurity自定义身份认证的实现方法,下面对SpringSecurity的这三种自定义身份认证进行详细讲解,... 目录1.内存身份认证(1)创建配置类(2)验证内存身份认证2.JDBC身份认证(1)数据准备 (2)配置依