前言

之前提到的项目产品开发日志:合宙Air724UG与云端服务器进行TCP直连,最近客户要求增加一个远程升级功能,毕竟项目安装比较分散,万一后续软件升级一个个手工升太麻烦,所以提出了这个需求。这次也算是边学边用了,记录一下防止下次忘记。

一般更改芯片程序都是通过芯片厂家提供的烧录工具完成的,而芯片厂家的烧录协议往往不会开放。那么就需要为芯片加入Bootloader功能,让芯片可以通过串口通讯的方式来实现对芯片的升级。我们这里就用到了合宙的Air724UG进行串口通讯来升级程序。

原理

要实现Bootloader的功能,必须使用允许在应用中编程的单片机,即具有IAP功能的单片机,为了方便更新数据的传输,通常还需要通讯串口,如UART。在实现Bootloader功能时,需要把单片机的存储空间划成两大块,Bootloader代码实现区域LDROM和用户代码区域APROM。LDROM用于存放Bootloader实现的一些相关代码,如当前工作状态检查、程序跳转条件判断、通讯程序、烧写程序等,这部分的程序是用于完成对APROM更新的程序,在应用中不可对其改写。APROM就是用户的应用程序,该部分代码可通过LDROM里的Bootloader程序进行更新。本项目中我使用的单片机品牌是中微,都支持这个功能,可以很方便的实现升级。

IAP介绍

所谓IAP就是In-Application Programming。即允许用户在程序运行过程中对部分程序区域进行烧写,目的是为了在产品发布后可以方便地通过预留的通信口对产品中的固件程序进行更新升级。详细的IAP操作方法见对应芯片规格书描述。IAP命令指令结合LDROM专用存储区域即可实现ISP升级功能。

空间结构

单片机的flash空间需要分为三部分:1. Bootloader程序区;2. 用户代码区;3. 待升级的代码区域。注意,第1点的程序和第2,3点的程序虽然是储存在一个芯片的flash中,但是实际写代码的时候其实是分成两个工程文件来写的

flash空间分配
flash空间分配

Bootloader流程

为了实现Bootloader功能,需要对Cortex-M0+内核的运行机制做一些了解。ARM Cortex-M0+内核的复位启动过程也被称为复位序列(Reset sequence)。ARM Cortex-M0+内核的复位启动过程与其他大部分CPU不同。大部分CPU复位后都是从0x00000000处取得第一条指令开始运行的,然而在 ARM Cortex-M0+内核中并不是这样的。其复位序列为:

  1. 从地址0x0000_0000处取出MSP的初始值;
  2. 从地址0x0000_0004处取出PC的初始值,然后从这个值对应的地址处取指。事实上,地址0x00000004开始存放的就是默认中断向量表。
  3. 当一次中断发生时,Cortex-M0+的单片机会发生如下动作:

    • 入栈:把8个寄存器的值压入栈;
    • 取向量:从向量表中找出对应的服务程序入口地址
    • 选择堆栈指针 MSP/PSP,更新堆栈指针SP,更新连接寄存器LR,更新程序计数器PC;

由于 Bootloader 和 APP 通常都要使用到中断资源,因此中断向量表需要根据运行区域进行变化,既Bootloader运行时使用Bootloader的中断向量表,APP运行的时候使用APP的中断向量表,因此取向量的动作需要通过程序控制,Cortex-M0+内核对此做了专门的设计,可以改变中断向量获取的位置,详情见软件部分设计章节。

流程图
流程图

软件设计

工程文件设置

一般情况使用的都是Keil MDK,前面提到的空间规划好之后,就需要在工程文件里把相应的起始地址填入。这里计算很简单,bootloader程序从0x0地址开始,按照该芯片每一页512Byte大小,一共使用16页,也就是0x2000个Byte。剩下的空间为0x20000-0x2000=0x1E000,分成两份也就是0xF000个Byte。

bootloader工程文件1
bootloader工程文件1
bootloader工程文件2
bootloader工程文件2
App工程文件1
App工程文件1
App工程文件2
App工程文件2

实现从BOOTLOADER引导程序跳转到用户程序

依据Cortex-M0+的内核特点,在进行程序跳转前需关闭全局中断,再将 MSP 值修改为 APP 代码的 MSP值。在跳转到用户APP程序前需要重映射中断向量表。另外为了保证升级的完整,在接收完待升级代码后需要在特定地址写入一个用于识别完整的值。为了使这个引导程序更加的通用,接收升级程序的代码在App工程中实现。这样可以灵活的匹配客户通讯协议。

下面这段是主体结构,先在指定内存地址读取flag,正确表示有升级就将缓存区数据全部复制到程序运行区,之后将flag擦除,跳转App地址。

int main(void)
{
    /* Start user code. Do not edit comment generated here */
    //    system_tick_init();
    //BootInit();
    while (1U)
    {
        if ((IAP_ReadOneByte(DATA_ADDR, 0xAA) == 0xAA) && (IAP_ReadOneByte(DATA_ADDR+1, 0xAA) == 0x55) && (IAP_ReadOneByte(DATA_ADDR+2, 0xAA) == 0x55) && (IAP_ReadOneByte(DATA_ADDR+3, 0xAA) == 0xAA))
        {
            IAP_Remap();                          // 将代码缓存区的内容加载到程序运行区
            IAP_FlagWrite(1);                     // 设置为App可运行状态flag
            IAP_Erase_512B(DATA_ADDR, DATA_AREA); // 将升级完成flag擦除
            IAP_Reset();                          // 跳转App运行地址
        }
        else
        {
            IAP_Reset();                         // 跳转App运行地址
        }
    }
}

需要注意的是下面这段是设置App运行地址,涉及到一些底层操作。其他flash复制的就是看每个芯片的指导手册了。

void IAP_Reset()
{    
    #ifdef ENCRYPT_UID_ENABLE        
    if(!CheckUID())
    {
        return;
    }
    #endif
    SCI0->ST0   = _0002_SCI_CH1_STOP_TRG_ON | _0001_SCI_CH0_STOP_TRG_ON;
    CGC->PER0 &= ~CGC_PER0_SCI0EN_Msk;
    INTC_DisableIRQ(SR0_IRQn);
    __set_MSP(*(__IO uint32_t*) APP_ADDR);
    //AppRemap(APP_ADDR);
    ((void (*)()) (*(volatile unsigned long *)(APP_ADDR+0x04)))();//to APP
    
    /* Trap the CPU */
    while(1);
}

之后在APP程序中最前面需要运行一个设置中断向量表的函数,用于将程序中断向量定位到App程序的地址。


/*----------------------------------------------------------------
  *Function:        AppCodeRemap
  *Description:        中断重定向
  *Input:            none
  *Output:            none
  *Return:            none
  *Others:            none
//----------------------------------------------------------------*/
void AppCodeRemap(u32 vector_addr)
{
    __disable_irq();
    SCB->VTOR = vector_addr; /*vector address*/
    __DMB();
    __enable_irq();
}

程序到这里就是一个完整的Bootloader引导过程。接下来是接收更新部分,这个根据通讯协议来,制定一个指令用于接收数据。

接收更新

  1. 协议帧

本协议为固定长格式的通讯协议,固定 64Byte,数据采用小端模式。

  • 主机发送
控制码包编号数据域校验码
2Byte2Byte58Byte2Byte
  • 从机返回
校验码包编号数据域
2Byte2Byte60Byte
  1. 帧说明
  • 控制码
命令名称对应编号功能描述
CMD_UPDATE_APROM0x00A0更新 APROM
CMD_RESEND_PACKET0x00A1重发包
CMD_RESET0x00A2复位重启/重启之后会执行升级
CMD_RUN_APROM0x00A3跳转运行APP模式
CMD_GET_FWVER0x00A4获取固件版本
CMD_GET_DEVICEID0x00A5获取设备ID
CMD_GET_SIGNAL0x00A6获取设备信号强度
  • 包编号

当前包的编号,此数值会随着通讯进度逐步增加,每一次成功的包传输都会增加1。

  • 数据域

数据域包括包同步信息、更新代码等信息,其含义随控制码的功能而改变。

  • 校验码

控制码+包编号+数据域的内容按照字节累加求和,其累加和的低16位作为校验位。对于从机计算主机发送过来的帧数据后放在帧头返回。

  1. 命令类型
  • 更新APROM
  • 名称:CMD_UPDATE_APROM
  • 功能:更新APP程序
  • 控制码:0x00A0
  • 帧格式

主机:

起始格式:

控制码包编号数据域-数据长度数据域-代码数据校验码
0x00A0N(2Byte)4ByteDATA(54Byte)2Byte

后续格式:

控制码包编号数据域-代码数据校验码
0x0000N(2Byte)DATA(58Byte)2Byte
  • 从机:
校验码包编号数据域
2ByteN+1(2Byte)补0,主机忽略

其他指令不涉及,就不提了,总体参考第一个命令类型就可以。根据协议来将每一帧数据保存到内存,这里有几个点要注意一下。第一个,芯片每次擦除是擦除一页,所以写也是将数据拼接成512个Byte来写一页。第二个,最后不足一页的也要当成一页写。

/*----------------------------------------------------------------
  *Function:        TCPBoot
  *Description:        boot模式
  *Input:            none
  *Output:            none
  *Return:            none
  *Others:            none
//----------------------------------------------------------------*/
static void TCPBoot(void)
{
    char str[100];

    if(gsCountTime.bootCnt > 0)        // 每个步骤等待时间
    {
        gsCountTime.bootCnt--;
    }
    if((gsCountTime.bootCnt == 0) && guModuleInfo.Bit.tcpConnect)
    {
        if(gbDataReturnFlag)
        {
            gsErrorDeal.inCheck7 = 0;
            gbDataReturnFlag = 0;
            gTCPBootStep++;
            if(gTCPBootStep > 2)
            {
                gTCPBootStep = 0;
                if(guSystemInfo.Bit.reboot)    // 系统重启
                {
                    guSystemInfo.Bit.reboot = 0;
                    ResetSystem();                    
                }
                if(guTCPInfo.Bit.exitBoot)
                {
                    guTCPInfo.Bit.exitBoot = 0;
                    gTCPBootStep = 3;
                }
            }
        }
        else
        {
            gsErrorDeal.inCheck7++;
            if(gsErrorDeal.inCheck7 >= 5)   // 超时
            {
                gsErrorDeal.inCheck7 = 0;
                gTCPBootStep = 0;                   // 不再接收
            }
        }
        switch(gTCPBootStep)
        {
            case 0:
                SendBootStatusFunction(NULL, NULL, NULL, NULL, TCPBootCMDHandle, TIME_1S, NULL);
                break;
            case 1:
                sprintf(str, "AT+CIPSEND=%d\r\n",FRAME_LEN);
                SendBootStatusFunction(str, NULL, ">", NULL, TCPBootReceiveHandle, TIME_500MS, NULL);
                break;
            case 2:
                TCPBootPropertyMsg(str, sizeof(str));
                SendBootStatusFunction(str, NULL, "DATA ACCEPT", NULL, TCPBootReceiveHandle, TIME_500MS, FRAME_LEN);
                break;
            case 3:
                gTCPStep = _TCPStepOnline;
                gbDataReturnFlag = 0;
                guTCPInfo.Bit.sent = 1;                // 发送完成表示等待发送
                gsCountTime.sendLoop = UPDATA_TIME;    // 第一次进入立即上报数据
                TCPClean();
                gsBootInfo.bootCMD = (Boot_CMD_t)0;    // 清指令
                gsBootInfo.packCnt = 0;                // 清包计数
                break;
            default:
                break;

        }
    }
}
/*----------------------------------------------------------------
  *Function:        TCPBootReceiveHandle
  *Description:        发送数据后获取模块和服务器信息
  *Input:            none
  *Output:            none
  *Return:            none
  *Others:            none
//----------------------------------------------------------------*/
static void TCPBootCMDHandle(char* dat)
{
    u8 i;
    u32 temp;
    char *pDat = 0;

    if(guUartState.Bit.rxStop)    // 数据接收完毕
    {
        gsBootInfo.checkSum = 0;        // 清零校验码
        for(i = 0; i < FRAME_LEN-2; i++)
        {
            gsBootInfo.checkSum += dat[i];
        }
        temp = dat[FRAME_LEN-1];        // 校验码高位
        temp = (temp << 8) | dat[FRAME_LEN-2];
        if(gsBootInfo.checkSum == (u16)temp)
        {
            temp = 0;
            temp = dat[1];
            temp = (temp << 8) | dat[0];    // 获取cmd类型
            if((gsBootInfo.bootCMD == CMD_UPDATE_APROM) && (temp == 0))    // 已经进入升级模式
            {
                gsBootInfo.bootCMD = CMD_UPDATE_APROM2;
            }
            else
            {
                gsBootInfo.bootCMD = (Boot_CMD_t)(u16)temp;
            }
            temp = dat[3];
            temp = (temp << 8) | dat[2];    // 获取包编号
            if((gsBootInfo.packCnt+1) == temp)
            {
                gsBootInfo.packCnt = (u16)temp;
            }
            else    // 丢包
            {
                gsBootInfo.bootCMD = (Boot_CMD_t)0;
            }
            switch((u16)gsBootInfo.bootCMD)
            {
                case CMD_UPDATE_APROM:        // 升级
                    // 获取数据包长度
                    temp = dat[7];
                    temp = (temp << 8) | dat[6];    
                    temp = (temp << 8) | dat[5];
                    temp = (temp << 8) | dat[4];    
                    gsBootInfo.dataLen = temp;
                    pDat = &dat[8];                        // 指向数据包开始
                    gsBootInfo.farmeDataLen = FRAME_DATA_LEN;
                    guTCPInfo.Bit.firmwareUpdata = 1;    // 开始下载数据包
                    TCPDownLoadHandle(pDat);            // 数据压入内存
                    TCPBootRunNext(0);                    // 下一步
                    break;
                case CMD_UPDATE_APROM2:                    // 升级后续数据
                    pDat = &dat[4];                        // 指向数据包开始    
                    if(gsBootInfo.dataLen >= FRAME_DATA_LEN_2)
                    {
                        gsBootInfo.farmeDataLen = FRAME_DATA_LEN_2;    
                    }
                    else
                    {
                        gsBootInfo.farmeDataLen = gsBootInfo.dataLen;    
                    }
                    TCPDownLoadHandle(pDat);            // 数据压入内存
                    TCPBootRunNext(0);                    // 下一步            
                    break;
                case CMD_RESEND_PACKET:                    // 不需要操作,下次发送升级包时会擦除缓存区内容
                    TCPBootRunNext(0);                    // 下一步    
                    break;
                case CMD_RESET:                            // 重启进入boot
                    guSystemInfo.Bit.reboot = 1;        // 重启
                    TCPBootRunNext(0);                    // 下一步    
                    break;
                case CMD_RUN_APROM:
                    guTCPInfo.Bit.exitBoot = 1;            // 退出boot
                    TCPBootRunNext(0);
                    break;
                case CMD_GET_FWVER:                        // 后面自动上报版本号
                    TCPBootRunNext(0);
                    break;
                case CMD_GET_DEVICEID:                    // 后面自动上报IMEI
                    TCPBootRunNext(0);
                    break;
                case CMD_GET_SIGNAL:                    // 后面自动上报信号强度
                    TCPBootRunNext(0);
                    break;
                default:
                    TCPBootRunNext(0);
                    break;
            }
        }
        
        

        guUartState.Bit.rxStop = 0;   // 信息处理完毕
        RxBufferClean();
    }
}
/*----------------------------------------------------------------
  *Function:        TCPDownLoadHandle
  *Description:        升级数据处理
  *Input:            none
  *Output:            none
  *Return:            none
  *Others:            none
//----------------------------------------------------------------*/
static void TCPDownLoadHandle(char* dat)
{
    u8 i;
    static u16 nBlock = 0, buffCnt;
    static char buff[ONE_PAGE_SIZE];

    if(guTCPInfo.Bit.firmwareUpdata)    // 首次写
    {
        guTCPInfo.Bit.firmwareUpdata = 0;
        nBlock = 0;
        buffCnt = 0;
        memset(buff, NULL, sizeof(buff)); 
    }
    if(nBlock < APP_PAGE_CNT)
    {
        for(i = 0; i < gsBootInfo.farmeDataLen; i++)
        {
            if((buffCnt < ONE_PAGE_SIZE) && (gsBootInfo.dataLen > 0))
            {
                buff[buffCnt] = dat[i];
                buffCnt++;
                gsBootInfo.dataLen--;
            }
        }
        if(buffCnt >= ONE_PAGE_SIZE)
        {
            IAPLoadUpdata(APP_BUFF_ADDRESS, nBlock, buff, buffCnt);    // 每次写1页
            buffCnt = 0;    // buff重新计数
            nBlock++;
        }
        else if(gsBootInfo.dataLen == 0)
        {
            if(buffCnt > 0)
            {
                IAPLoadUpdata(APP_BUFF_ADDRESS, nBlock, buff, buffCnt);    // 每次写1页
                IAPWriteUpdataFlag(IAP_CHECK_ADDRESS);
            }
            
        }
    }
}
/*----------------------------------------------------------------
  *Function:        IAPLoadUpdata
  *Description:        更新一页数据到flash
  *Input:            none
  *Output:            none
  *Return:            none
  *Others:            none
//----------------------------------------------------------------*/
void IAPLoadUpdata(u32 add, u16 nBlock, const char* buff, u16 len)
{
    u16 i;

    if(nBlock == 0)    // 清空所有块
    {
        for(i = 0; i < APP_PAGE_CNT; i++)
        {
            EraseSector(add+i*ONE_PAGE_SIZE);            // 擦当前页
        }
    }
    ProgramPage(add+nBlock*ONE_PAGE_SIZE, len, (u8*)buff);    // 写数据
}

最后

这个升级程序原理上来看是非常简单的,通过更改两个程序的进入地址,可以很容易地切换两个程序的运行。但涉及到最底层的地址操作需要格外小心,毕竟跑程序的时候不小心擦错地址了容易跑飞。


本文作者:HelloGakki

本文链接:https://pinaland.cn/archives/development-log-mcu-bootloader-upgrade.html

版权声明:所有文章除特别声明外均系本人自主创作,本文遵循署名 - 非商业性使用 - 禁止演绎 4.0 国际许可协议,转载请注明出处。