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

相关文章

基于SpringBoot+Mybatis实现Mysql分表

《基于SpringBoot+Mybatis实现Mysql分表》这篇文章主要为大家详细介绍了基于SpringBoot+Mybatis实现Mysql分表的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可... 目录基本思路定义注解创建ThreadLocal创建拦截器业务处理基本思路1.根据创建时间字段按年进

SpringBoot3实现Gzip压缩优化的技术指南

《SpringBoot3实现Gzip压缩优化的技术指南》随着Web应用的用户量和数据量增加,网络带宽和页面加载速度逐渐成为瓶颈,为了减少数据传输量,提高用户体验,我们可以使用Gzip压缩HTTP响应,... 目录1、简述2、配置2.1 添加依赖2.2 配置 Gzip 压缩3、服务端应用4、前端应用4.1 N

将Mybatis升级为Mybatis-Plus的详细过程

《将Mybatis升级为Mybatis-Plus的详细过程》本文详细介绍了在若依管理系统(v3.8.8)中将MyBatis升级为MyBatis-Plus的过程,旨在提升开发效率,通过本文,开发者可实现... 目录说明流程增加依赖修改配置文件注释掉MyBATisConfig里面的Bean代码生成使用IDEA生

Java编译生成多个.class文件的原理和作用

《Java编译生成多个.class文件的原理和作用》作为一名经验丰富的开发者,在Java项目中执行编译后,可能会发现一个.java源文件有时会产生多个.class文件,从技术实现层面详细剖析这一现象... 目录一、内部类机制与.class文件生成成员内部类(常规内部类)局部内部类(方法内部类)匿名内部类二、

SpringBoot实现数据库读写分离的3种方法小结

《SpringBoot实现数据库读写分离的3种方法小结》为了提高系统的读写性能和可用性,读写分离是一种经典的数据库架构模式,在SpringBoot应用中,有多种方式可以实现数据库读写分离,本文将介绍三... 目录一、数据库读写分离概述二、方案一:基于AbstractRoutingDataSource实现动态

Python FastAPI+Celery+RabbitMQ实现分布式图片水印处理系统

《PythonFastAPI+Celery+RabbitMQ实现分布式图片水印处理系统》这篇文章主要为大家详细介绍了PythonFastAPI如何结合Celery以及RabbitMQ实现简单的分布式... 实现思路FastAPI 服务器Celery 任务队列RabbitMQ 作为消息代理定时任务处理完整

Java枚举类实现Key-Value映射的多种实现方式

《Java枚举类实现Key-Value映射的多种实现方式》在Java开发中,枚举(Enum)是一种特殊的类,本文将详细介绍Java枚举类实现key-value映射的多种方式,有需要的小伙伴可以根据需要... 目录前言一、基础实现方式1.1 为枚举添加属性和构造方法二、http://www.cppcns.co

使用Python实现快速搭建本地HTTP服务器

《使用Python实现快速搭建本地HTTP服务器》:本文主要介绍如何使用Python快速搭建本地HTTP服务器,轻松实现一键HTTP文件共享,同时结合二维码技术,让访问更简单,感兴趣的小伙伴可以了... 目录1. 概述2. 快速搭建 HTTP 文件共享服务2.1 核心思路2.2 代码实现2.3 代码解读3.

MySQL双主搭建+keepalived高可用的实现

《MySQL双主搭建+keepalived高可用的实现》本文主要介绍了MySQL双主搭建+keepalived高可用的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,... 目录一、测试环境准备二、主从搭建1.创建复制用户2.创建复制关系3.开启复制,确认复制是否成功4.同

Java实现文件图片的预览和下载功能

《Java实现文件图片的预览和下载功能》这篇文章主要为大家详细介绍了如何使用Java实现文件图片的预览和下载功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... Java实现文件(图片)的预览和下载 @ApiOperation("访问文件") @GetMapping("