Bootloader科普(1):认识Bootloader作用与实现原理

图片


本文约3,518字,建议收藏阅读


作者 | 吃一嵌
出品 | 汽车电子与软件

前言

回想起刚入门单片机,在学校使用单片机参加各种电子竞赛。


代码跑起来的流程可简单了。


只需要拷贝一份demo工程(比如Demo工程只实现GPIO点亮几颗LED)。


然后工程上面加应用功能逻辑、编译,烧录,最后断开仿真器,代码就能直接跑起来了。


图片


简单来说,就是仅只使用一个工程,然后在上面实现我想要的应用功能就可以了。


这样的整套流程,可是搞了好几年。


后来毕业了,在一个公司实习,首次真正见识到了Bootloader。


当时感觉公司的代码太牛了!


它一个项目下面,有2个工程,一个工程叫App、一个工程叫Boot。


Boot的main函数,它跑了一些东西之后,会执行跳转,这个跳转代码很厉害,执行它就会跳到App工程里面(当时看这跳转代码,可是好久都没理解)。


在当时,脑袋瓜子对Boot的问题可是太多了。


Boot的大概作用,我百度一下倒是能懂,可以用于升级嘛。


但是,这两个工程它们是怎么衔接起来的?跟我以前接触的不一样啊?以前都是一个工程一烧录,就跑起来了。


好像还有一级Boot、二级Boot?这个听起来更牛了!搞那么多Boot干嘛?!


听说Bootloader会初始化必要的硬件外设,啥是必要的?为啥不能在App初始化?


当时还太菜了,怎么看都看不懂。


......


于是又好几年过去。


积累了各种MCU的知识后。


终于搞清楚了这Bootloader基本的来龙去脉。




1

认识Bootloader需了解的知识


在一个代码工程里面,东西多了去了:有函数,有局部变量,有全局变量,等等。


我们所谓的刷代码刷代码,到底刷的是啥?又是刷到MCU的哪里去?


如果这些都不知道,那还谈啥boot,谈啥升级呢。


1.ROM(PFlash)、RAM概念


首先,我们需要认识的是ROM和RAM(这里我们主要是认识ROM)。


基础概念我们都知道:


ROM是断电数据不丢失的、只读的


RAM是断电后数据会丢失、可读可写的


但是,仅了解到这个层面还不够。


我们还需要深入去了解,在我们实际写的代码中,ROM对应哪些代码部分,RAM又对应哪些代码部分。


大家平时写的代码中,有函数、有局部变量、有全局变量、有常量等等,我们看看这些常见的代码都放到哪。


  1. 函数、常量


    函数是用来实现各种逻辑的代码,它是只读的。


    常量则是不可修改的变量,也是只读的。


    因此函数和常量都放在ROM里面


  2. 局部变量、全局变量


    这两种类型变量的值都是代码运行过程中可读可写的,且MCU下电后数据会丢失的。


    因此局部变量和全局变量放在RAM里面


2.MCU地址概念


在对ROM和RAM有了形象的认识后,我们就要看看它们在MCU芯片里面又是怎么放置的。


接下来我们就要去翻一翻MCU芯片手册了。


我们简单一点,把芯片理解成一个容器。


比如对于32位的MCU芯片,它的总大小就是:0x0000 0000~0xFFFF FFFF


ROM和RAM就放在这个容器(0x0000 0000 ~ 0xFFFF FFFF)里面。


于是我们翻到芯片手册关于地址的描述(以英飞凌TC275为例,Tc275它是3核芯片,但我们不用管它几个核)


然后我们找到ROM、RAM所在的位置。


ROM的地址共2个区域(大小共4M):


PFlash1:0x8000 0000 ~ 0x801F FFFF


PFlash2:0x8020 0000 ~ 0x803F FFFF


图片


RAM的地址区域就稍微分散一些(这里简单截一张图):


图片


大家不用管它叫什么PSPR、DSPR啥的,反正它就是RAM(这款芯片的RAM区域具体如下图)


图片


好了, 现在我们稍微深入一些了解ROM和RAM之后。


我们再来看看刷代码是怎么个事。


比如我们这颗TC275芯片。


①对于代码工程里面的函数、常量


所谓刷代码,就是把函数、常量这些东西刷进入了MCU的ROM所在的地址里面


画图举例如下:


图片


②对于工程里面的全局变量


所谓刷代码,就是把全局变量的地址,指定到MCU的某个RAM地址上面


比如定义一个全局变量uint16 u16TestVal,它占用的空间是2个Byte。


假设我们把这个u16TestVal变量指定到0x5000 00 00 ~ 0x5000 0001这个地址上面,那么这个地址存的值就是u16TestVal这个变量的值

画图举例如下:


图片


(我们这里只是简单举个ROM和RAM的例子,比如还有局部变量等等其它代码,就不展开讨论了)



2

  认识Bootloader

  作用与实现原理


到这里,你大概清楚了刷代码是刷了啥东西之后,你已经具备足够的知识去初步认识Bootloader的实现原理了。


第一步:


假设你手上有一个代码工程,并且只有一个main函数,先不用想这么复杂,mcu上电后,我们不管main之前还跑了别的什么东西,就假设main函数之前没有东西了。


你的代码只实现了一个功能,就是在main里面拉一下GPIO点个灯(LED1)。


图片


我们刚刚说了,函数放哪?放ROM嘛对吧。


因此,我们不妨直接把这个工程(即这个函数的起始位置)放到芯片手册里面ROM开始的地址:0x80000000(至于具体怎么放的,我们先不管)。


第二步:


你把刚刚那个工程拷贝一份,然后改一下main函数的实现内容(甚至你不改也行)。


图片


这个工程就改成点亮另一个LED灯吧(LED2)。


同样的,函数放ROM里面嘛。


对于这个工程2的起始地址(即工程2的main函数的起始地址),放到第1个工程的后面,我们给第1个工程一些预留位置(因为不可能一个工程就点个灯吧,后续要加功能的)


工程2起始地址就放到0x80200000吧。


好了。经过了第一步、第二步的操作。


你现在MCU的ROM里面成功放了2个工程进去。


图片


同时,我们把MCU上电后跑起来的起始地址设为0x80000000,即工程1的main起始地址。


那么问题又来了,这两个工程怎么衔接起来?


这就需要靠下面这句跳转代码了:


((void (*)(void))(0x80200000))();


我们把这段代码放到工程1点亮LED1代码的后面。


图片


于是,MCU上电,代码跑起来后,工程1在点完LED1就会直接跳到工程2的main函数的起始地址,然后跑工程2的代码去点亮LED2了。


图片


说明:这里得再说一下,实际工程的起始地址肯定不是main的,main的前面还会跑别的东西。我们这里只是简化理解。


但是,main前面的东西也一样是放在ROM里,所以我们现在不需要纠结main前面到底有啥。


到这里,很关键的一步已经完成了,你的MCU已经能跑2个工程了,已经有点Bootloader的雏形了。


接下来,我们再进一步。


比如我们简简单单,实现一个应用功能,比如两轮平衡小车的功能。


主要的逻辑功能放哪呢?


由于工程2的起始地址比工程1的靠后。


所以我们放到工程2里面,工程1我们后续有大用。


于是,在工程2里面就有了各种应用功能逻辑代码。


比如读取陀螺仪的方向转角代码啦,PID算法代码啦,等等。


图片


你好好地搞着平衡车的功能,突然有一天,天塌了。


烧录器被人偷了!没法烧录代码了!平衡小车功能还没优化完呢!


好在很久以前,你从网上ctrl-c、ctrl-v,移植了下面这样功能的代码到工程1里面,并且在烧录器没被偷之前就刷写进MCU了。


你移植到工程1的这代码干的事情也不多,主要是下面这些:


  1. 擦除指定ROM地址数据


    数据写入指定ROM地址之前,我们需要先把MCU这段ROM地址上的数据擦除


    比如我们现在需要刷写工程2到MCU里面,那么我们就需要从ROM地址0x80200000开始擦除,至于擦除到哪里结束,则取决于这个工程2使用的ROM大小了。


    又至于它是怎么擦除的,实际代码就是调用一个函数接口,这个函数是MCU厂家的软件包就有这个接口,因此不需要担心。


  2. 接收升级包


    比如你编译工程2之后,会生成一个.hex文件。

    这个.hex文件就是升级包。


    然后,我们电脑会有一个上位机(类似于串口工具那样的上位机),它可以载入这个.hex文件,然后通过UART(或者CAN)发送给MCU

    至于MCU如何接收整个数据包的过程,我们现在先不管。


  3. 把升级包写入指定ROM


    在擦除完成之后,就MCU就把接收到的升级包刷写至指定的ROM里面。


    至于它是怎么写入的,也是类似于擦除那样,MCU厂家的软件包就有这个函数调用接口,直接调用就行,只需要把数据本身、数据大小和写入地址传进函数里面,它就会去写入了。


图片


到这里,我们整理一下:


工程2,实现各种想要的应用功能代码,用来搞功能逻辑的,比如你的平衡小车功能。


工程1,不搞应用逻辑功能,作用是通过UART(或CAN)直接把工程2的代码刷写进MCU。


好了,MCU里面有了工程1在,你的烧录器就算被偷了,在工程2需要升级的时候,工程1执行以下这3个关键步骤:


  1. 擦除工程2在MCU中所在的ROM地址


  2. 接收工程2的hex文件


  3. 把接收到的工程2的hex文件数据写入指定ROM地址


就能通过UART(或者CAN)直接把工程2的代码刷写进去了。


朋友们,这个工程1,不就是所以谓的Bootloader了嘛。


有了工程1在,我们的MCU不就具备升级功能了嘛。


我们这里接收升级包的方式是通过UART(或者CAN)。


但完全可以改成别的方式。


比如这个工程1可以通过云端远程接收数据包,然后对工程2进行升级。


这不就是所谓的OTA(Over The Air)了,你的手机、车、电脑不就是用这种方式升级的嘛!



3

结 语


朋友们,这里我们为了更清晰去理解Bootloader的作用和实现流程。


其中很多细节的东西我们都暂且忽略了(因此会有很多不严谨的地方,比如升级功能是Bootloader和App相互协作的,App也需要有跳转Bootloader的相关代码)。


后续就是一点点填坑,把这些细节一点点补上。


比如:


main前面还跑了什么东西,干啥用的?


具体要怎么指定代码工程的ROM到我们想要放到的MCU的ROM地址上(编译链接文件)?


Bootloader工程和App工程,他们使用的RAM又是怎么样去分配的?


Bootloader跑起来的时候,Bootloader它怎么知道当前是上电流程直接跑到App去还是执行升级流程升级App?


一级Boot、二级Boot又是怎么回事?


升级包接收的流程具体是怎么样的?


hex和bin有啥关系,好像两者都能用于升级?


等等......


真正要完整把Bootloader实现起来,细节可太多了,我们后续慢慢讲解。