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实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

使用C++实现链表元素的反转

《使用C++实现链表元素的反转》反转链表是链表操作中一个经典的问题,也是面试中常见的考题,本文将从思路到实现一步步地讲解如何实现链表的反转,帮助初学者理解这一操作,我们将使用C++代码演示具体实现,同... 目录问题定义思路分析代码实现带头节点的链表代码讲解其他实现方式时间和空间复杂度分析总结问题定义给定

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

python使用fastapi实现多语言国际化的操作指南

《python使用fastapi实现多语言国际化的操作指南》本文介绍了使用Python和FastAPI实现多语言国际化的操作指南,包括多语言架构技术栈、翻译管理、前端本地化、语言切换机制以及常见陷阱和... 目录多语言国际化实现指南项目多语言架构技术栈目录结构翻译工作流1. 翻译数据存储2. 翻译生成脚本

如何通过Python实现一个消息队列

《如何通过Python实现一个消息队列》这篇文章主要为大家详细介绍了如何通过Python实现一个简单的消息队列,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录如何通过 python 实现消息队列如何把 http 请求放在队列中执行1. 使用 queue.Queue 和 reque

Python如何实现PDF隐私信息检测

《Python如何实现PDF隐私信息检测》随着越来越多的个人信息以电子形式存储和传输,确保这些信息的安全至关重要,本文将介绍如何使用Python检测PDF文件中的隐私信息,需要的可以参考下... 目录项目背景技术栈代码解析功能说明运行结php果在当今,数据隐私保护变得尤为重要。随着越来越多的个人信息以电子形

使用 sql-research-assistant进行 SQL 数据库研究的实战指南(代码实现演示)

《使用sql-research-assistant进行SQL数据库研究的实战指南(代码实现演示)》本文介绍了sql-research-assistant工具,该工具基于LangChain框架,集... 目录技术背景介绍核心原理解析代码实现演示安装和配置项目集成LangSmith 配置(可选)启动服务应用场景

使用Python快速实现链接转word文档

《使用Python快速实现链接转word文档》这篇文章主要为大家详细介绍了如何使用Python快速实现链接转word文档功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 演示代码展示from newspaper import Articlefrom docx import

前端原生js实现拖拽排课效果实例

《前端原生js实现拖拽排课效果实例》:本文主要介绍如何实现一个简单的课程表拖拽功能,通过HTML、CSS和JavaScript的配合,我们实现了课程项的拖拽、放置和显示功能,文中通过实例代码介绍的... 目录1. 效果展示2. 效果分析2.1 关键点2.2 实现方法3. 代码实现3.1 html部分3.2