基于ARM Cortex-M0+内核的bootloader程序升级原理及代码解析

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 基于ARM Cortex-M0+内核的bootloader程序升级原理及代码解析

本文主要讲述BootLoader程序升级原理及一些代码的解析,力图用通俗易懂的语言描述清楚BootLoader升级的主要关键点。

BootLoader 升级原理概述

首次接触这一块时,有一个概念叫IAP(在应用编程),通俗一点讲便是通过一段已有的程序(我们称之为BootLoader程序)去升级另外的一段程序(用户程序)。升级的方式多种多样,可以通过串口、USB、SPI等等多种接口去升级。实际上,我们是把我们需要升级的芯片里面分为两个区域,暂且称之为A区域和B区域。
A区域主要存放BootLoader程序,B区域主要存放用户程序,也就是我们希望升级或修改的程序。
一般情况下,为了升级流程的方便,我们会把A区域布置在芯片flash(有人喜欢称ROM,就是存放代码的区域)的起始位置,也就是0x0开始的位置,至于A区域在哪里结束,这需要看你的BootLoader程序有多大了,它能占用多少的代码量了。比如你的BootLoader程序编译完后有2.5KB左右的大小,那么你可以计算一下:
2.5K = 2.5 X 1024 = 2560B = A00(H)
也就是说,你的这段代码如果从零地址开始存放的话,他会在0xA00的位置结束,0xA00之后的区域你便可以用来存放需要升级的用户代码了。但有时我们并不会紧接着0xA01的位置开始放置用户代码,而是会留出一定的空间,比如从0xB00处开始存放代码,这主要是因为BootLoader程序在flash中存放时不一定会紧挨着存放,有时代码段之间会有空闲区域;然后,我们通过在BootLoader程序中设置相关参数(应用程序起始位置等),使应用程序升级时按照我们设置的位置存放在B区域,从而完成升级。这一点我会在后面的代码详解中介绍。

我们可以通过下面的图解来理解bootloader程序升级时的区域占用情况。
image.png

需要了解的关键点

进行BootLoader程序编写之前,我们需要了解并熟悉以下几个关键点:

 1. 文件传输协议。因为升级时需要上位机软件配合下位机的BootLoader程序进行应用程序代码的传输,因此文件传输协议至关重要,笔者推荐使用Xmodem 1K协议。 这个协议的好处便是上位机可以自动打包数据,每一包数据含有1k字节的代码,传输效率很高,传输时间很短。
 2. 芯片空间map。 做BootLoader升级相关的项目,肯定离不开对芯片空间的了解,需要对自己所用芯片的RAM、ROM以及向量表(如果有的话)等占用情况有比较深入的了解。
 3. 跳转函数。这个是程序从BootLoader程序跳转到应用程序运行的关键。笔者在做项目时曾经在这一块浪费了不少的时间。文末笔者会提供实用的跳转函数。

升级代码解析

其实前面说的再多,脱离了代码都是纸上谈兵。下面通过一个实际的BootLoader升级例子,结合笔者自己编写的BootLoader代码,对这一过程进行解析。
代码主要包括
main.c
BootLoader.c
xmodem-1k.c
跳转函数

其中,
main.c主要是升级执行初始化及升级完成后初始化升级环境及跳转代码实现的部分。
BootLoader.c部分主要是升级流程的代码控制。
xmodem-1k.c主要是文件传输协议的代码实现。

至于其他部分的代码,比如串口相关以及时钟相关的代码,每种芯片的编程方式都不尽相同,因此笔者不详细介绍这部分,该部分代码大家可以从需要升级的应用程序中直接移植即可。

main.c

int main (void)
{
    SystemCoreClockUpdate();  // 时钟初始化 
    WatchDog_Initial();       // 看门狗初始化
    vBootLoader(&vScene_Init,&vScene_Renew);  // BootLoader主程序
}

vBootloader()这个函数中用到了两个函数指针,分别指向初始化函数vScene_Init()和环境重置函数vScene_Renew()。初始化函数很好理解,在运行程序之前,先对芯片时钟、管脚等初始化,或者有些参数需要初始化,这个根据自己的代码情况进行选择。

那么环境重置函数什么意思呢?这主要是为了和需要升级的应用程序的运行想配合,因为我们的bootloader程序的相关配置有时候并不一定会和应用程序的配置完全一致,如果运行完BootLoader之后,没有把BootLoader程序的相关配置关闭掉或者恢复到默认值,运行到应用程序之后,还可能会执行BootLoader程序的配置,这样会出现问题。举个栗子,在BootLoader程序中用中断喂狗,跳转到应用程序之前,没有关闭喂狗中断,如果在应用程序中没有配置相关喂狗中断的程序,那么应用程序仍然会按照bootloader的配置执行中断喂狗,这样会导致应用程序中的喂狗失效,因为中断喂狗是很准时的,往往起不到喂狗的效果,有时会影响程序的复位操作。因此,环境重置函数说白了,就是把bootloader用到的配置关掉。笔者建议把用到的所有的东西全部关闭(包括但不限于串口、时钟、看门狗、IO等),因为在应用程序中会根据自己的应用程序配置相关的代码。

BootLoader.c

/**********************************************************
*  BootLoader流程控制函数
** 参 数: pfunSenceInitCallBack   初始化芯片指针函数
**       pfunSenceRenewCallBack  重新初始化代码环境指针函数
** 返回值: 无
**********************************************************/
void vBootLoader(void(* pfunSenceInitCallBack)(void), void (* pfunSenceRenewCallBack)(void))
{
    uint8_t ucMessage = 0;
    
    unsigned int sp;
    unsigned int pc;
    uint16_t bootflag_read;

    sp = APP_START_Flash;
    pc = sp + 4;
    
    pfunSenceInitCallBack();  //初始化函数指针,具体函数怎么写这里不再赘述

    while(1)
    {
        wdt_feed();
        do{
            ucMessage = u8UpdateMode();  // 此函数为升级主函数
            if (UPDATE_OK == ucMessage)        /* 升级成功 */
            {
                memcpy(erase_pg_buf, bootflag_OK,    sizeof(bootflag_OK));
                update_bootflag();
                break;
            } 
            else if (UPDATE_NO == ucMessage)   /* 没有升级 */
            {
                break;
            }
            else                              /* 升级错误 */
            {
                memcpy(erase_pg_buf, bootflag_ERROR, sizeof(bootflag_ERROR));
                update_bootflag();
                break;
            }        
        } while (1);

        bootflag_read = *( volatile uint16_t *)(BOOT_FLAG_ADDR);   /* 读取存放在bootflag地址的值 */
        
        if (u8UserCodeEffect() == USERCODE_OK) // 代码判断
        {
            if (bootflag_read == 0xAA55)
            {
                pfunSenceRenewCallBack();  // 环境重置函数
                vControlSwitch(sp,pc); // 跳转函数
            }
        }
    }
}

上面是bootloader程序擦写完flash之后判断是否升级成功以及执行跳转函数的代码。流程主要是,升级主函数u8UpdateMode()(下面是详细代码)中进行数据接收校验以及flash擦写工作,如果擦写成功,该函数返回0(UPDATE-OK),擦写失败,该函数返回1(UPDATE-ERROR),没有擦写操作,该函数返回2(UPDATE-NO)。在这个函数中根据相关返回标志进行处理。处理完去读取flash中存放BootLoader标志的地方的数据,如果使我们希望的数据,我们就执行跳转函数,让程序从BootLoader跳转到应用程序中,如果标志不正确,说明升级过程出了问题,我们就不跳转,一直运行在BootLoader程序中。当然,在跳转之前需要执行我们之前提到的环境重置函数pfunSenceRenewCallBack()

// 升级主函数
static uint8_t u8UpdateMode(void)
{
   uint8_t ret;
   
   if (iap_prepare_sector(APP_START_SECTOR, APP_END_SECTOR) == CMD_SUCCESS) // 准备扇区
    {
      if (iap_erase_sector(APP_START_SECTOR, APP_END_SECTOR) == CMD_SUCCESS) // 擦除扇区
          {
            ret = u8Xmodem1kClient(ProgramFlash, (uint16_t)BOOT_DELAYTIME_C, (uint16_t)BOOT_WAITTIME_UPDATE);// 编程指针+X-Modem协议识别

            if (0 == ret) 
            {
                return UPDATE_OK;// 返回标志
            }
            if (2 == ret)
            {
                return UPDATE_NO;
            }
            
        }
   }
   return UPDATE_ERROR;
}

主函数代码不难理解,进来之后先准备相应的扇区,然后擦除(FLash内部值置全FF),然后启动X-Modem协议接收数据,数据接收完成启动写flash函数ProgramFlash()进行代码烧写,这一部分在下一节X-Modem-1K.c中讲。不同的片子的烧写流程不同,这个得看芯片手册,有的需要准备扇区,有的不需要,但是大多数流程都保留了先擦除后烧写的内容。

注意:擦除和烧写之前需要看技术手册搞明白芯片是支持区块擦除还是支持页擦除。

X-modem-1k.c

关于X-Modem协议是什么,大家可以自行去百度,这里也不再赘述。
先看代码,比较长:

/**********************************************************
** Xmodem1k协议传输程序
** 参  数:     pfunPktHandle,: Xmodem1k协议传输所需函数结构体指针
**          u16ShortDly:    轮询发送C字符的时间间隔
**          u8LongDly:      等待传输开始超时时限
** 返回值: 传输结果: 0--成功,1--升级失败(错误或取消升级),2--没有升级
**********************************************************/
uint8_t u8Xmodem1kClient(pFunPKTHAND pfunPktHandle, uint16_t  u16ShortDly, uint16_t u8LongDly)
{
    uint32_t u32ByteCnt   = 0;                                          /* 位计数器  计数一包的第几个字节数据   */
    uint8_t  u8TimeoutCnt = 0;                                          /* 超时次数                             */
    uint8_t  u8DataerrCnt = 0;                                          /* 数据错误次数                         */
    uint8_t  u8PktIndex   = 1;                                          /* 包序号期望值                         */

    uint8_t  u8STATE = STAT_IDLE_C;                                     /* 状态变量                             */
    uint8_t  u8Data;                                                    /* 存放接收数据及发送命令               */
    volatile uint16_t u16PktLen;                                        /* 包中有效数据的长度                   */
    uint8_t  u8Message;
    
    sysTimerClr(1);
    
    while (1)
    {
        wdt_feed();
      
        switch (u8STATE)
        {
        case STAT_IDLE_C:                                               /* 轮询发C状态                  */
                if (sysTimerGet(1) >= u8LongDly )
             {
                u8STATE = STAT_TIMEOUT_C;                               /* 等待开始超时,跳到结束状态   */
             } 
             else 
             {
                u8Data = POLL;
                do {
                        u8Message = UART_SendByte(u8Data);        
                      } while (u8Message == UART_NO_SPACE);
                sysTimerClr(0);
                u8STATE = STAT_IDLE_DATA;                               /* 跳到轮询读数状态             */
                }
             break;
                    
        case STAT_IDLE_DATA:                                            /* 轮询读数状态                 */
             if (UART_RecvByte(&u8Data) == UART_SUCCESS)
             {
                u8STATE = STAT_CONNECT;                                 /* 接收到数据,跳到数据链接状态 */
                sysTimerClr(0);
             } 
             else
             {
                if (sysTimerGet(0) >= (u16ShortDly * SECOND_PER_TICK))                   
                {
                    u8STATE = STAT_IDLE_C;                              /* 轮询读数超时,跳回轮询发C    */
                }
             }
             break;

        case STAT_CONNECT:                                              
             if ((u8Data == SOH) || (u8Data == STX))                    /* 数据连接状态   SOH--CRC128字节协议  STX--1k协议   */
             {
                u16PktLen = (u8Data == SOH)? SHORTPKT_LEN : LONGPKT_LEN;
                ((uint8_t *)ptHead)[u32ByteCnt] = u8Data;
                u32ByteCnt++;
                u8STATE = STAT_RECEIVE;                                 /* 连接成功,跳到数据接收状态   */
                sysTimerClr(2);
             } 
             else
             {
                u8STATE = STAT_IDLE_C;                                  /* 起始控制字符错,跳回轮询发C  */
             }
             break;

        case STAT_RECEIVE:                                               /* 数据接收状态                 */
             if (UART_RecvByte(&u8Data) == UART_SUCCESS) 
             {            
                if (u32ByteCnt < PKT_HEAD_LEN) 
                {
                    ((uint8_t *)ptHead)[u32ByteCnt] = u8Data;           /* 控制字符、序号、序号补码     */
                    if (ptHead->u8Ctrl == EOT)
                    {
                        u8STATE = STAT_ACK;
                        break;
                    }
                } 
                else 
                {
                    ((uint8_t *)puData)[u32ByteCnt - 3] = u8Data;       /* 数据段部分(数据、CRC值)      */
                }
                u32ByteCnt++;
                
                if (u32ByteCnt >= u16PktLen + PKT_HEAD_LEN + 2)
                {
                    u8STATE = STAT_HANDLE;                              /* 包接收结束,跳到数据处理状态 */
                }
                u8TimeoutCnt = 0;
                sysTimerClr(0);
            } 
            else
            {                                                          /* 未收到数据,判断超时         */
               /* 包间隔最大为1s,字符间隔最大为20ms, 根据包内部和包之间的不同选择不同的超时间隔 */
               if (sysTimerGet(0) >= ((u32ByteCnt == 0) ? PKT_TIMEOUT_MS : CHAR_TIMEOUT_MS)) 
               {
                    sysTimerClr(0);
                    u8TimeoutCnt++;
                    u8STATE = STAT_NAK;
                   }
            }
            break;    

        case STAT_HANDLE:                                               /* 数据处理状态                 */
            {
            uint16_t u16CRCTemp;
            
            if (ptHead->u8Ctrl != ((u16PktLen == SHORTPKT_LEN) ? SOH : STX))  /* 检查控制字符是否一致  */
            {                                                         
                u8DataerrCnt++;
                u8STATE = STAT_NAK;
                break;
            }
            if (ptHead->u8Index + ptHead->u8Patch != 0xFF)                    /* 检查序号、序号补码是否完整   */
            {          
                u8DataerrCnt++;
                u8STATE = STAT_NAK;
                break;
            }
            if ((ptHead->u8Index) == (u8PktIndex - 1))                 /* 检查序号是否为上一包序号, 数据重发的时候,检测是否为上一包序号 */
            {
                u8STATE = STAT_ACK;
                break;
            }
            if (ptHead->u8Index != u8PktIndex)                         /* 检查序号是否为期望的包序号   */
            {
                u8DataerrCnt++;
                u8STATE = STAT_NAK;
                break;
            }
            u16CRCTemp = ((uint16_t)(*((uint8_t *)puData + u16PktLen)) << 8) | (*((uint8_t *)puData + u16PktLen + 1));
            if (u16CRCVerify((uint8_t *)puData, u16PktLen, 0) != u16CRCTemp)
            {
                u8DataerrCnt++;
                u8STATE = STAT_NAK;                                     /* CRC检查                      */
                break;
            }                
            if (!pfunPktHandle((uint8_t *)puData, u16PktLen))   // 烧写flash的函数指针,具体怎么烧写查看芯片手册
            {
                u8PktIndex++;
                u8STATE = STAT_ACK;                                     /* 数据处理                     */
                break;
            }
            u8DataerrCnt++;
            u8STATE = STAT_NAK;
            break;
               }

        case STAT_ACK:                                                  /* 正常响应状态(ACK)            */
             u8Data = ACK;
             do {
                   u8Message = UART_SendByte(u8Data);
                } while (u8Message == UART_NO_SPACE);
             
             if (ptHead->u8Ctrl == EOT)                                 /* 结束控制符时进入ACK状态情况  */
             {                               
                u8STATE = STAT_END;                                     /* 发送方发送EOT结束传输        */
                break;
             }
             u8DataerrCnt = 0;
             u32ByteCnt = 0;
             u8STATE = STAT_RECEIVE;                                    /* 正常响应发送ACK后跳到数据接收*/
             break;
            
        case STAT_NAK:                                                  /* 非正常响应状态(NAK)          */
             if ((u8DataerrCnt >= 5) || (u8TimeoutCnt >= 5))            /* 发送错误次数或接收超时次数超过5次*/
             {
                 u8STATE = STAT_CAN;
                break;
             }
             u8Data = NAK;
             do {
                   u8Message = UART_SendByte(u8Data);
                } while (u8Message == UART_NO_SPACE);
             u32ByteCnt = 0;
             u8STATE = STAT_RECEIVE;
             break;
        
        case STAT_CAN:                                                  /* 强制结束状态(CAN)            */
             u8Data = CAN;
             do {
                  u8Message = UART_SendByte(u8Data);
                } while (u8Message == UART_NO_SPACE);
             return 1;
            
        case STAT_END:                                                  /* 传输结束状态(CAN)            */
             return 0;

        case STAT_TIMEOUT_C:
             return 2;

        default:
             break;
        }
    }
}

代码虽长,但其实比较好理解。
用了一个while(1)循环,先默认向上位机发C确认需要升级,上位机发送来升级数据后根据xmodem协议判断是否正确,然后执行不同的case。直到全部升级完成或者升级失败或者没有升级,也就是之前所说的3中状态,也对应了这个程序中的三个出口(分别为return 0/1/2)。

看到这,基本上整个升级流程就完成了,包括:

  1. 初始化
  2. 进入升级主函数
  3. 协议判断(烧写flash等)
  4. 3种出口,对应三种处理方式
  5. 查询BootLoader标志
  6. 是否跳转

最后一步很关键的是跳转函数。

跳转函数

跳转函数的基本思想是将芯片的pc指针指向应用程序烧写的起始地址APP_START_Flash,然后sp调到APP_START_Flash + 4的位置也就是复位向量所在的地方,然后开始执行。

下面是跳转函数的代码,该方法对于M0+核的芯片均适用,前提是spPC要正确。

static void vControlSwitch(unsigned int sp,unsigned int pc)
{
  asm("ldr   r0, [r0]");
  asm("mov   sp, r0");
  asm("ldr   r0, [r1]");
  asm("bx    r0");
}

如果你是从头看到这的话,我相信你应该会对升级的流程有了大致的理解,以上的代码均可直接运行。相信你能写出更好的更精湛更有效的升级代码来。

限于笔者的能力及精力的限制,以上有可能会有疏漏,如发现有不妥之处,欢迎留言交流。

相关文章
|
1月前
|
存储 物联网 调度
操作系统的心脏:内核深度解析
在数字世界的构建中,操作系统扮演着基石的角色,而其核心—内核,则是这一复杂系统的灵魂。本文将深入探讨操作系统内核的工作原理,揭示它是如何管理硬件资源、运行程序以及提供系统服务的。通过理解内核的结构和功能,我们可以更好地把握计算机系统的运作机制,进而优化和创新我们的技术实践。
|
2月前
|
存储 算法 Java
解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用
在Java中,Set接口以其独特的“无重复”特性脱颖而出。本文通过解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用。
50 3
|
28天前
|
存储 人工智能 安全
操作系统的心脏——内核深度解析
【10月更文挑战第29天】 本文深入探讨了操作系统的核心组件——内核,包括其定义、功能、架构以及在现代计算中的重要性。通过对比不同操作系统内核的设计哲学和技术实现,揭示了内核如何影响系统性能、稳定性和安全性。此外,文章还讨论了未来内核技术的潜在发展方向,为读者提供了一个全面了解内核工作原理的平台。
|
26天前
|
存储 消息中间件 算法
深入探索操作系统的心脏——内核机制解析
本文旨在揭示操作系统核心——内核的工作原理,通过剖析其关键组件与机制,为读者提供一个清晰的内核结构图景。不同于常规摘要的概述性内容,本文摘要将直接聚焦于内核的核心概念、主要功能以及其在系统管理中扮演的角色,旨在激发读者对操作系统深层次运作原理的兴趣与理解。
|
1月前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
69 4
|
1月前
|
存储 安全 Java
系统安全架构的深度解析与实践:Java代码实现
【11月更文挑战第1天】系统安全架构是保护信息系统免受各种威胁和攻击的关键。作为系统架构师,设计一套完善的系统安全架构不仅需要对各种安全威胁有深入理解,还需要熟练掌握各种安全技术和工具。
106 10
|
1月前
|
缓存 负载均衡 算法
Linux内核中的进程调度算法解析####
本文深入探讨了Linux操作系统核心组件之一——进程调度器,着重分析了其采用的CFS(完全公平调度器)算法。不同于传统摘要对研究背景、方法、结果和结论的概述,本文摘要将直接揭示CFS算法的核心优势及其在现代多核处理器环境下如何实现高效、公平的资源分配,同时简要提及该算法如何优化系统响应时间和吞吐量,为读者快速构建对Linux进程调度机制的认知框架。 ####
|
1月前
|
算法 Java 数据库连接
Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性
本文详细介绍了Java连接池技术,从基础概念出发,解析了连接池的工作原理及其重要性。连接池通过复用数据库连接,显著提升了应用的性能和稳定性。文章还展示了使用HikariCP连接池的示例代码,帮助读者更好地理解和应用这一技术。
46 1
|
16天前
|
JavaScript 前端开发 API
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
Vue.js响应式原理深度解析:从Vue 2到Vue 3的演进
45 0
|
1月前
|
前端开发 JavaScript 开发者
揭秘前端高手的秘密武器:深度解析递归组件与动态组件的奥妙,让你代码效率翻倍!
【10月更文挑战第23天】在Web开发中,组件化已成为主流。本文深入探讨了递归组件与动态组件的概念、应用及实现方式。递归组件通过在组件内部调用自身,适用于处理层级结构数据,如菜单和树形控件。动态组件则根据数据变化动态切换组件显示,适用于不同业务逻辑下的组件展示。通过示例,展示了这两种组件的实现方法及其在实际开发中的应用价值。
36 1

推荐镜像

更多