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

相关文章

Java中流式并行操作parallelStream的原理和使用方法

《Java中流式并行操作parallelStream的原理和使用方法》本文详细介绍了Java中的并行流(parallelStream)的原理、正确使用方法以及在实际业务中的应用案例,并指出在使用并行流... 目录Java中流式并行操作parallelStream0. 问题的产生1. 什么是parallelS

C++中unordered_set哈希集合的实现

《C++中unordered_set哈希集合的实现》std::unordered_set是C++标准库中的无序关联容器,基于哈希表实现,具有元素唯一性和无序性特点,本文就来详细的介绍一下unorder... 目录一、概述二、头文件与命名空间三、常用方法与示例1. 构造与析构2. 迭代器与遍历3. 容量相关4

Java中Redisson 的原理深度解析

《Java中Redisson的原理深度解析》Redisson是一个高性能的Redis客户端,它通过将Redis数据结构映射为Java对象和分布式对象,实现了在Java应用中方便地使用Redis,本文... 目录前言一、核心设计理念二、核心架构与通信层1. 基于 Netty 的异步非阻塞通信2. 编解码器三、

C++中悬垂引用(Dangling Reference) 的实现

《C++中悬垂引用(DanglingReference)的实现》C++中的悬垂引用指引用绑定的对象被销毁后引用仍存在的情况,会导致访问无效内存,下面就来详细的介绍一下产生的原因以及如何避免,感兴趣... 目录悬垂引用的产生原因1. 引用绑定到局部变量,变量超出作用域后销毁2. 引用绑定到动态分配的对象,对象

SpringBoot基于注解实现数据库字段回填的完整方案

《SpringBoot基于注解实现数据库字段回填的完整方案》这篇文章主要为大家详细介绍了SpringBoot如何基于注解实现数据库字段回填的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以了解... 目录数据库表pom.XMLRelationFieldRelationFieldMapping基础的一些代

Java HashMap的底层实现原理深度解析

《JavaHashMap的底层实现原理深度解析》HashMap基于数组+链表+红黑树结构,通过哈希算法和扩容机制优化性能,负载因子与树化阈值平衡效率,是Java开发必备的高效数据结构,本文给大家介绍... 目录一、概述:HashMap的宏观结构二、核心数据结构解析1. 数组(桶数组)2. 链表节点(Node

Java AOP面向切面编程的概念和实现方式

《JavaAOP面向切面编程的概念和实现方式》AOP是面向切面编程,通过动态代理将横切关注点(如日志、事务)与核心业务逻辑分离,提升代码复用性和可维护性,本文给大家介绍JavaAOP面向切面编程的概... 目录一、AOP 是什么?二、AOP 的核心概念与实现方式核心概念实现方式三、Spring AOP 的关

Python实现字典转字符串的五种方法

《Python实现字典转字符串的五种方法》本文介绍了在Python中如何将字典数据结构转换为字符串格式的多种方法,首先可以通过内置的str()函数进行简单转换;其次利用ison.dumps()函数能够... 目录1、使用json模块的dumps方法:2、使用str方法:3、使用循环和字符串拼接:4、使用字符

Redis中Hash从使用过程到原理说明

《Redis中Hash从使用过程到原理说明》RedisHash结构用于存储字段-值对,适合对象数据,支持HSET、HGET等命令,采用ziplist或hashtable编码,通过渐进式rehash优化... 目录一、开篇:Hash就像超市的货架二、Hash的基本使用1. 常用命令示例2. Java操作示例三

Redis中Set结构使用过程与原理说明

《Redis中Set结构使用过程与原理说明》本文解析了RedisSet数据结构,涵盖其基本操作(如添加、查找)、集合运算(交并差)、底层实现(intset与hashtable自动切换机制)、典型应用场... 目录开篇:从购物车到Redis Set一、Redis Set的基本操作1.1 编程常用命令1.2 集