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 Reset
                AREA    RESET, DATA, READONLY
                EXPORT  __Vectors
                EXPORT  __Vectors_End
                EXPORT  __Vectors_Size

__Vectors       DCD     __initial_sp ; Top of Stack
                DCD     Reset_Handler ; Reset Handler
                DCD     COMMON_IRQHANDLER ; NMI Handler
                DCD     COMMON_IRQHANDLER ; Hard Fault Handler
                DCD     COMMON_IRQHANDLER ; Reserved
                DCD     COMMON_IRQHANDLER ; Reserved
                DCD     COMMON_IRQHANDLER ; Reserved
                DCD     COMMON_IRQHANDLER ; Reserved
                DCD     COMMON_IRQHANDLER ; Reserved
                DCD     COMMON_IRQHANDLER ; Reserved
                DCD     COMMON_IRQHANDLER ; Reserved
                DCD     COMMON_IRQHANDLER ; SVCall Handler
                DCD     COMMON_IRQHANDLER ; Reserved
                DCD     COMMON_IRQHANDLER ; Reserved
                DCD     COMMON_IRQHANDLER ; PendSV Handler
                DCD     COMMON_IRQHANDLER ; SysTick Handler

                ; External Interrupts
                DCD     COMMON_IRQHANDLER ; Window Watchdog
                DCD     COMMON_IRQHANDLER ; PVD through EXTI Line detect
                DCD     COMMON_IRQHANDLER ; RTC through EXTI Line
                DCD     COMMON_IRQHANDLER ; FLASH
                DCD     COMMON_IRQHANDLER ; RCC
                DCD     COMMON_IRQHANDLER ; EXTI Line 0 and 1 ; chizhiling 2021.8.13
                DCD     COMMON_IRQHANDLER ; EXTI Line 2 and 3 ; chizhiling 2021.8.13
                DCD     COMMON_IRQHANDLER ; EXTI Line 4 to 15 ; chizhiling 2021.8.13
                DCD     COMMON_IRQHANDLER ; Reserved
                DCD     COMMON_IRQHANDLER ; DMA1 Channel 1
                DCD     COMMON_IRQHANDLER ; DMA1 Channel 2 and Channel 3
                DCD     COMMON_IRQHANDLER ; DMA1 Channel 4, Channel 5, Channel 6 and Channel 7
                DCD     COMMON_IRQHANDLER ; ADC1, COMP1 and COMP2 
                DCD     COMMON_IRQHANDLER ; LPTIM1
                DCD     COMMON_IRQHANDLER ; USART4 and USART5
                DCD     COMMON_IRQHANDLER ; TIM2
                DCD     COMMON_IRQHANDLER ; TIM3
                DCD     COMMON_IRQHANDLER ; TIM6
                DCD     COMMON_IRQHANDLER ; TIM7
                DCD     COMMON_IRQHANDLER ; Reserved
                DCD     COMMON_IRQHANDLER ; TIM21
                DCD     COMMON_IRQHANDLER ; I2C3
                DCD     COMMON_IRQHANDLER ; TIM22
                DCD     COMMON_IRQHANDLER ; I2C1
                DCD     COMMON_IRQHANDLER ; I2C2
                DCD     COMMON_IRQHANDLER ; SPI1
                DCD     COMMON_IRQHANDLER ; SPI2
                DCD     COMMON_IRQHANDLER ; USART1
                DCD     COMMON_IRQHANDLER ; USART2
                DCD     COMMON_IRQHANDLER ; LPUART1
                DCD     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  0x08001400

isp_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 -ex

IMAGE_BIN=out/h076_mcu.bin
APP_HEX=./Project/MDK/Out/APM32F072/CEC_Controler.hex

rm -r out/
mkdir out/

# flash 64k
dd if=/dev/zero of=$IMAGE_BIN bs=1k count=64

objcopy -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)。

  • 17
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值