DK45
IAP-in application programming,就是在应用中编程的意思,在产品发布以后,不管是增加功能啊,或者对bug修复啊,都可以对原来固件进行更新升级。
基于stm32做的iap大多数的思路都是先设计一个bootloader,如果需要升级呢就跳转到bootloader用来更新后面的应用程序。应用程序的空闲呢可以是一个或者两个。如下图所示,两个app肯定就要限定每个app的大小,但是它相比一个app空间会更安全,因为它随时都会存在一个可以工作的app。
这种方式用的比较多,但是也会存在一些问题:
而stm32L0系列呢(部分型号)提供了一套双BANK机制可以用来做双存储区在线升级。基于该双bank方式就可以抛弃bootloader,维护起来更方便。不过要理解双bank还是稍微有那么一点复杂,好的结果是用来却很方便。
我们先从宏观上理解一下双bank的原理:
如图所示,它的flash被平均分为两块,一块儿为bank1,一块儿为bank2。当在bank1中运行app时就可以把新更新的固件写入到bank2,写完以后就切换到从bank2启动运行新的app。如果当前在bank2运行就把新固件写入bank1,写完以后切换从bank1启动。
原理就是这样很简单,但是这里面有个关键的问题点,在没有bootloader的情况下如何实现从bank1或者bank2启动。这就是stm32L071cb自带的一个主要特性(其他型号是否有请自查手册,目前只有stm32L0、stm32L4、stm32G4中的某些型号有这个特性)。下面以stm32L071cb为例来分析如何实现切换。
这是存在于SYSCFG寄存器中的一个位,它有两个值:0或者1,功能是:
用过stm32的同学可能思维里面有一个固定的概念:stm32的flash都是从0x8000000开始的。换句话说,我从0x8000000读取出来的数据肯定都是同一片flash区域。然而,在stm32L071CB上面并不是。你从0x8000000读出来一个数据可能是BANK1开头的数据也可能是BANK2开头的数据。到底是哪个取决于UFB这个位当前的值是0还是1。
这个UFB先记住,等待后面综合起来理解。
这个可能一下抖出来的内容有点多,请打起精神。
这又要从ARM的启动方式说起了,可以看我上一篇文章有详细的总结:STM32在线升级中断向量重定向深度剖析。要记住关键的一点就是ARM是从0x00000000取的第一条指令。而STM32正常情况下之所以从用户flash开始运行,是因为用户flash被映射到了0x00000000地址上。0x8000000和0x00000000都能访问到flash同一个区域,所以才让看起来貌似是从0x8000000运行起来的一样。
当从bank1启动时,实际上就是普通的启动模式。上电后用户flash最底部(BANK1区域)被映射到0x00000000地址,然后CPU直接从这里取指令开始执行。
那重点就是从BANK2启动的流程是怎样的,这里要给大家再介绍个新的配置选项:BFB2,存在于option bytes里面的一个位。这个位呢就可以用来选择从bank2启动。因为在option bytes里面,所以掉电是不会丢失的。
当boot0=0的时候,stm32默认就会从用户flash启动。也就是用户flash被映射到0x0地址。但是当boot0=0并且BFB2=1的时候,系统flash会被映射到0x0地址,系统flash也就是stm32内置的bootloader。
这时候进内置bootloader以后呢就会去检查BANK2有没有有效代码(当在bank的第一个数据所指向的地址是有效的(指向栈顶地址),则认为代码就是有效的),如果有就会把UFB设置为1(UFB前面介绍过,忘了往上翻再看一遍),bank2被映射到0x8000000地址,然后跳转到BANK2开始运行。所以从BANK2启动和从BANK1启动是不一样的,也比从bank1启动流程复杂一些。bank2启动流程如下:
所以我这里画了一张stm32L071cb上电到从bank1或者bank2开始运行的流程图:
仔细再看上面的流程图,如果从bank1启动,bank1是被映射到0x0地址的,而ARM内核默认也是从0x0处读取中断向量表。所以不用做任何设置都可以正常的运行。
但是从BANK2启动就没那么简单了,假如BFB2=1时,CPU并不是直接跳转到bank2运行,而是先进入了系统flash区域(内置bootloader)。这时候实际上是stm32内置bootloader被映射到了0x0地址。之后流程只是把bank2映射到了0x8000000,然后就从BANK2启动了。
这样如果不管中断向量表位置会产生什么现象呢?当中断发生了,ARM内核会跳转到0x0地址处(跳回了内置bootloader)找到中断向量。那这样程序岂不乱套了,我们自己编写的中断服务函数将永远不会被执行到。
所以进入到BANK2以后一定要做中断向量表的重定位。这里我列出来三种是被我在stm32L071cb上面验证过的思路:(关于更多如何定位中断向量表还是看我上一篇文章:STM32在线升级中断向量重定向深度剖析)
最终我觉得最简单的是第一种,就是上电以后就修改VTOR到0x8000000。当然还有另一个好处就是不管从bank1或者bank2启动都可以执行这一条。虽然对bank1启动来说这个设置不是必须的,但是执行了也无害。这样就能做到bank1和bank2的app代码处理流程尽量统一。
另外还有就是实际上如果你使用的是STM32 HAL库,在SystemInit()中实际上已经帮我们重新设置VTOR到0x8000000。所以我们就无需再次添加修改VTOR的代码了。
对于IAP把代码新固件写到另一片bank之后,就要切换到从另一个bank启动。尽管前面流程原理有点复杂,但是实现它的也比较简单,主要执行以下步骤:
后来经过各方排查,定位到ARM内核的一个寄存器:PRIMASK。这个寄存器从stm32参考手册上是查不到的,如果了解详情可以去看arm-cortex M0+编程手册。
在切换到从bank2启动以后,中断向量表偏移我也重新设置,但是还是出现了中断无法响应的问题。systick中断进不去,这样就导致HAL库运行会一直等待systick计时时间到的地方。
说这个寄存器你可能不熟悉,但是__enable_irq()和__disable_irq()实际上操作的就是这个寄存器的值。默认这个值是0,是不屏蔽任何中断的。但是从bank2启动时候发现这个值变成了1。这样就能解释为什么中断进不去了,它为1的时候除了不可屏蔽中断,其他的中断都一律屏蔽不响应。
而为什么从bank2启动才变成1,从bank1启动就正常。再看下前面bank2启动的流程是先从内部bootloader跳转过来的,所以肯定是内部bootloader把PRIMASK给配置成1了。
所以最终解决起来就简单了,就是从bank2启动以后,就调用一条__enable_irq()重新修改PRIMASK值变成0就可以了。