使用Xmodem-1k协议通过串口升级的BootLoader

1 阅读10分钟

硬件使用江科大stm32套件,写一个采用Xmodem-1k协议通过串口升级的bootloader,并预留W25Q接口用于后续的远程升级

功能概述

要实现Xmodem-1k协议通过串口升级的bootloader,需实现以下部分

        实现flash分区、操作及状态管理,即实现对flash的boot和app分区,flash的读写和状态管理

        实现串口中断接收状态切换,即实现升级命令到接收升级包的状态切换

        实现Xmodem-k协议完成升级包的校验与接收

        实现接收完升级包后进行新程序位置的跳转执行

升级流程

升级流程主函数如下

        上电进行优先级划分与串口初始化

        重置IAP状态,即初始化接收模式,清空数据缓冲计数,清除内部Flash擦除标志

        做W25Q初始化,并根据是否初始化成功选择升级或直接跳转应用,即升级依赖W25Q初始化(用于后续远程升级)

        进入升级流程,根据主机发送开始升级标志进入Xmodem-1k下载模式

        接收升级包校验完成后,跳转app地址,执行新程序

int main(void)
{
        uint8_t ret = 0;
        NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);  // 设置中断优先级分组2
        Usart1_Init(115200);
     
        Iap_Up_Init();  // 重置Iap全局状态,清除内部Flash擦除标志缓存
        ret = W25QXX_Init();  // 初始化W25QXX SPI FLASH,读取ID并重试判断是否为指定型号,依赖W25Q正常工作,本地或者之后支持远程升级都是将数据下载到W25Q中

        if(ret == 1)
        {
            Check_Iap_Up_Data();  // W25Q初始化成功,进行本地或之后支持远程升级检查
        }
        else
        {
            Boot_Jump(APP_START_ADDR);  // W25Q初始化失败,直接跳转应用
        }
        
        Boot_Jump(APP_START_ADDR);  // 最终跳转应用
    
        for(;;)
        {
    
        }

}

Flash分区、操作及状态管理

Flash的分区划分

c8t6的Falsh为64KB,一页为1KB,即64页。这里使用前16页作为boot占用,后48页用于app使用。

// C8T6 Flash 参数
#define STM32_FLASH_BASE        ((uint32_t)0x08000000)
#define FLASH_PAGE_SIZE         (1024U)
#define TOTAL_FLASH_PAGES       (64U)
#define FLASH_END_ADDR          (STM32_FLASH_BASE + 64*1024 - 1)

// Bootloader占用前16KB(16页)
#define BOOTLOADER_SIZE_KB      (16U)
#define APP_START_ADDR          (STM32_FLASH_BASE + BOOTLOADER_SIZE_KB * 1024)

// 获取某页起始地址
#define ADDR_FLASH_PAGE(n)      (STM32_FLASH_BASE + (n) * FLASH_PAGE_SIZE)

实现对于Flash的操作与状态管理

Flash_Erase_Page_Manage

管理Flash页擦除标志,避免重复擦除同一页

        维护一个静态数据用于记录每一页是否进行擦除

Get_Flash_PageNumber

通过传入的地址判断其在那一页,通过地址偏移量和Flash页大小得到页数

STMFLASH_Write

将数据写入到内部Flash(需擦除相关页),writeAddr写入的起始地址(word对齐),pBuffer指向写入的内容,numToWrite需要写入的word数量(word即4字节)

        通过写入地址和需写入的word数量,得到写入结束地址

        通过Get_Flash_PageNumber得到起始和结束的Flash页码

        得到每一页的起始地址,对页进行擦除,首先解锁Flash

        Flash擦除完成后进行按word写,写完成后对Flash进行上锁

STMFLASH_ReadWord

从Flash中读取一个word,即根据传入的地址,读取32位数据

STM32FLASH_Read

从Flash读取任意长度字节数据,addr即起始地址,buf接收缓冲区,len要读取的字节数

        通过len判断要读取多少个word,使用STMFLASH_ReadWord从地址读取,并写入到buf中

IAP_Write_AppBin

将升级固件写入到app区域,appAddr目标写入地址,appBuf指向固件数据,appSize固件总字节数

        将需要写入的字节组成word

        创建一个全局静态缓冲区,将word写入其中,每次缓冲区满的时候,进行一次STMFLASH_Write

        最后将不足一个缓冲区的数组写入Flash

// 函数声明
uint32_t Get_Flash_PageNumber(uint32_t addr);
void     STMFLASH_Write(uint32_t writeAddr, uint32_t *pBuffer, uint32_t numToWrite);
uint32_t STMFLASH_ReadWord(uint32_t addr);
void     STM32FLASH_Read(uint32_t addr, uint8_t *buf, uint32_t len);
void     IAP_Write_AppBin(uint32_t appAddr, uint8_t *appBuf, uint32_t appSize);
int      Flash_Erase_Page_Manage(uint8_t flag, uint32_t addr);

串口数据接收与状态切换

实现上电启动时,当串口接收到发送的download字符串时,切换文件下载模式,根据Xmodem-1K协议头判断是否出错

void USART1_IRQHandler(void)
{
    uint8_t res;
    int i = 0;
    unsigned int r_buf_2048_cnt = 0;
    if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
    {
        res = USART_ReceiveData(USART1);  // 读取串口接收到数据
        r_buf_2048_cnt = g_com_iap.r_buf_2048_cnt;
        g_com_iap.r_buf_2048[r_buf_2048_cnt] = res;
        r_buf_2048_cnt = r_buf_2048_cnt + 1;

        if(r_buf_2048_cnt >= 2047)
        {
            r_buf_2048_cnt = 0;
        }

        if(g_com_iap.recvfileMod == 1)  // 文件下载状态
        {
            if((g_com_iap.r_buf_2048[0] != XM_STX))
            {
                r_buf_2048_cnt = 0;
            }
        }
        else if(g_com_iap.recvfileMod == 0)
        {
            if(r_buf_2048_cnt >= 10)
            {
                if((g_com_iap.r_buf_2048[r_buf_2048_cnt - 2] == 0x0d) && (g_com_iap.r_buf_2048[r_buf_2048_cnt - 1] == 0x0a))
                {
                    for(i = 0; i < 10; i++)
                    {
                        g_com_iap.r_cmdBuf[i] = g_com_iap.r_buf_2048[i];
                    }
                    if(com_strncmp(g_com_iap.r_cmdBuf, "download\r\n", 10))
                    {
                        g_com_iap.recvfileMod = 1;
                    }
                }
                r_buf_2048_cnt = 0;
            }
        }
        g_com_iap.r_buf_2048_cnt = r_buf_2048_cnt;
    }
}

实现Xmodem-k协议完成升级包的校验与接收

即IAP的核心,对接受的数据进行接收、校验与跳转(涉及到Xmode-1k协议相关内容,参考上一篇Xmodem-1k内容)

这里定义Xmodem-1k的相关标志,以及核心的com_iap全局结构体

Iap_Up_Init

初始化IAP状态,即初始化接收模式,清空数据缓冲计数,清除内部Flash擦除标志

Check_Iap_Up_Data

        上电判断是否需要进行本地下载升级包

        上电一段时间内轮询recvfileMod标志,判断是否进入升级路程,若轮询结束未收到升级标志,则直接跳转app地址执行

        recvfileMod为1则进入本地Xmodem-1k升级路程,向上位机打印输出升级提醒,并进入down_load_file,根据返回值判断跳转

#define PAR_READ       (1)    //读
#define PAR_WRITE      (2)    //写
#define PAR_BACKUP     (3)    //备份
#define PAR_RECOVERY   (4)    //恢复

//1K-Xmode 协议头
#define XM_DLY_1S                65536    //在1S内xm_timer被调用的次数,小于65536
#define XM_RETRY                15        //retry次数
#define XM_SOH                  0x01    //Xmodem数据头
#define XM_STX                  0x02    //1K-xmodem数据头
#define XM_EOT                  0x04    //发送结束
#define XM_ACK                  0x06    //认可应答
#define XM_NAK                  0x15    //不认可应答
#define XM_CAN                  0x18    //丛机撤销传输
#define XM_EOF                     0x1A    //数据包填充
#define XM_OK                       0
#define XM_ERR                     -1
#define MAXRETRANS                    25

#define W25Q_PAGE_SIZE          4096         //存储器页大小定义
#define W25Q_ADDR_START         0x000000     //存储器存储起始大小

// iap全局结构体
struct com_iap
{
    unsigned int uiHaveNewRemotePack;  //0x00 无新固件  0x12345678 有新固件      需存储;远程升级标志
    unsigned int uiFileSize;           //文件总大小     4字节                    需存储;固件总大小
      
    uint8_t recvfileMod;               //接收模式,0 为命令模式,1 为文件下载模式
    
    char r_cmdBuf[24];                   //文本命令缓冲区 用于检测download\r\n
    
    char r_buf_2048[2048];
    unsigned int r_buf_2048_cnt;         //串口接收与XModem数据缓冲
    
    unsigned int uiPackedID;            //包ID Xmodem数据包编号
      
    char t_buf_2048[2048];               //临时数据缓冲,用于 Flash 读回校验和远程拷贝
};

extern struct com_iap g_com_iap;

void Boot_Jump(u32 new_Code_Addr);  // 从指定地址读取MSP和Reset_Handler,设置向量偏移表、MSP并跳转执行应用程序
int  Check_Iap_Up_Data(void);  // 上电后统一处理本地升级和远程升级逻辑,并在需要时跳转应用
void Iap_Up_Init(void);  // 初始化IAP相关状态

down_load_file

实现接收校验并写入Flash逻辑

        根据Xmodem-1k协议,给平台发送C,提示平台开始发送升级包,接收到Xmodem-1k的XM_STX数据头,则开始接收

        根据Xmodem-k协议,在start_recv进行接收

int down_load_File(void)
{
    uint8_t i = 0;
    uint8_t getChar;

    unsigned char ucStartBuf[1] = {'C'};

    g_com_iap.uiPackedID = 1;

    for(i = 0; i < XM_RETRY; i++)  // 根据Xmodem-1K协议,每间隔1s给平台发送启动字符'C',尝试XM_RETRY此,直到接收到第一个字符
    {
        Usart1_SendArray(ucStartBuf, 1);  // 给调试串口发送数据
        Delay_Ms(200);
        getChar = g_com_iap.r_buf_2048[0];
        if(getChar == XM_STX)
        {
            break;
        }else
        {
            Delay_Ms(1000);
        }
    }
    while(1)
    {
        getChar = g_com_iap.r_buf_2048[0];
        if(XM_STX == getChar)
        {
            start_recv();
        }
        else if(XM_EOT == getChar)
        {
            com_xmodem_finish();
            Boot_Jump(APP_START_ADDR);
            break;
        }
        else if(XM_CAN == getChar)
        {
            // 从机撤销传输
        }
        Delay_Ms(200);
    }
    return 1;
}

start_recv

完成升级包的接收、校验与写入

        若数据包头为XM_STX,则正常接收,对收到的数据包进行长度、标志头、CRC、包id和包序进行校验

        校验通过,则清空数据缓冲计数,用于下一包接收,并将接收到的数据包使用IAP_Write_AppBin写入到Flash中

        写入Flash后,在进行一次crc校验,即对写入的数据计算crc,并再次读取写入的数据包,计算crc。二者进行校验

        成功则向平台发送XM_ACK,并更新包id和下一次写入Flash位置,失败则关闭中断并复位

        平台根据Xmodem-1k发送XM_EOT,即升级包发生接收完毕,此时返回应答,并跳转Boot_Jump

void start_recv(void)
{
    #define FILE_SIZE_1K 1024
    #define PacHeadLen   3

    static uint32_t curFileSize = 0;
    
    uint16_t crcFlag            = 0;
    int iErr                    = 0;
    int iRight                  = 0;
    uint16_t crc16ValueR        = 0;
    uint16_t crc16ValueW        = 0;

    unsigned char ucSendBuf[1]  = {'C'};
    unsigned char *pData        = (unsigned char *)g_com_iap.r_buf_2048;

    crcFlag = xm_crc16_ccitt((unsigned char *)(pData+3), 1024);  // 详Xmodem-1k数据包格式 

    iRight = 1;
    iRight = iRight && (g_com_iap.r_buf_2048_cnt == 1029);
    iRight = iRight && (pData[0] == XM_STX);
    iRight = iRight && (((crcFlag >> 8) == pData[1027])&&(((uint8_t)(crcFlag) == pData[1028])));
    iRight = iRight && (((unsigned char)g_com_iap.uiPackedID) == pData[1]);
    iRight = iRight && (((unsigned char)pData[1] == (unsigned char)(~pData[2])));

    if(1 == iRight)
    {
        g_com_iap.r_buf_2048_cnt = 0;
        
        IAP_Write_AppBin((APP_START_ADDR + curFileSize*FILE_SIZE_1K), (unsigned char *)(pData + PacHeadLen), FILE_SIZE_1K);  // 更新Flash代码
        crc16ValueW = xm_crc16_ccitt((unsigned char *)(pData + PacHeadLen), FILE_SIZE_1K);

        STM32FLASH_Read((APP_START_ADDR + curFileSize*FILE_SIZE_1K), (unsigned char *)g_com_iap.t_buf_2048, FILE_SIZE_1K);  // 读取Flash
        crc16ValueR = xm_crc16_ccitt((unsigned char *)g_com_iap.t_buf_2048, FILE_SIZE_1K);

        if(crc16ValueW != crc16ValueR)
        {
            g_com_iap.r_buf_2048_cnt = 0;
            printf("*************************Write memory failed**************************\r\n");
            printf("***************** ******Device restart attempt************************\r\n");
            __set_FAULTMASK(1);  // 关闭所有中断
            NVIC_SystemReset();  // 复位
        }
        else
        {
            g_com_iap.r_buf_2048_cnt = 0;
            ucSendBuf[0]             = XM_ACK;
            Usart1_SendArray(ucSendBuf, 1);
            g_com_iap.uiPackedID = g_com_iap.uiPackedID + 1;
            curFileSize = curFileSize + 1;
        }

    }else
    {
        g_com_iap.r_buf_2048_cnt = 0;
        iErr = 1;
    }
    if(1 == iErr)
    {
        g_com_iap.r_buf_2048_cnt = 0;
        ucSendBuf[0]             = XM_NAK;  // 发送NAK,通知平台重发当前数据包
        Usart1_SendArray(ucSendBuf, 1);
    }
    g_com_iap.r_buf_2048_cnt = 0;
}

 实现接收完升级包后进行新程序位置的跳转执行

Boot_Jump

从指定地址读取MSP和Reset_Handler,设置向量偏移表、MSP并跳转执行应用程序

        接收完升级包后,读出栈顶指针和复位向量。

        设置中断向量表,设置主堆栈指针

        跳转新地址执行程序

void Boot_Jump(u32 new_Code_Addr)  
{
    uint8_t  ReadBuf[8] = {0};
    uint32_t msp;
    uint32_t reset;

    #define IROM_ADDR ((uint32_t)0x08000000)
         
    uint32_t base;
    uint32_t offset;

    STM32FLASH_Read(new_Code_Addr,ReadBuf,8);     //1.读取新固件栈顶指针,及其复位向量
    msp    = ((uint32_t)ReadBuf[0]) + (((uint32_t)ReadBuf[1])<< 8) +  (((uint32_t)ReadBuf[2]) << 16) + (((uint32_t)ReadBuf[3]) << 24);
    reset  = ((uint32_t)ReadBuf[4]) + (((uint32_t)ReadBuf[5])<< 8) +  (((uint32_t)ReadBuf[6]) << 16) + (((uint32_t)ReadBuf[7]) << 24);

    base   = (new_Code_Addr > IROM_ADDR) ?(NVIC_VectTab_FLASH):(NVIC_VectTab_RAM);
    offset =  new_Code_Addr - base;

    NVIC_SetVectorTable(base,offset);            //2.设置中断向量表
         
    __set_MSP(msp);                              //3.设置主堆栈指针
         
    ((void(*)())(reset))();                      //4.跳转至内部FLASH特定地址处执行新程序
}

至此,采用Xmodem-1k协议通过串口升级的bootloader编写完毕。