继续固件升级的一些内容-下位机部分

发布于 2020-04-21  462 次阅读


继续固件升级的一些内容-下位机部分

本地固件升级分上位机和下位机两部分内容,我们分开讨论。

关于下位机

固件升级下位机部分我们也可以理解为两部分。BootLoader和APP,前面也简单介绍过这两部分的作用和一些注意事项。现在再详细说说。

BootLoader

BootLoader简单来说主要有两个作用:

  • 接收并固件包
  • 指定程序的跳转

当然也可以做其他的事,比如,在这部分对程序进行一定的保护,比如基于ChipUniqueID对程序进行加密(前面专门介绍过),对Flash进行读、写保护或者解除Flash的读写保护、程序更新失败的回滚或者升级备份等等。

如何接收固件包

关于如何接收固件包,建议大家采用一个协议,无论是现成的Ymodem、Xmodem之类的或者自定义的也好,这样可以给固件包的传输提供一定的保障(解决丢包、包错误等问题)。
每个包有固定的包序号,包大小,帧头帧尾和CRC校验等。我前面介绍的Ymodem协议就是一个不错的选择。
接收固件包的时候要注意一边接收检验一边写Flash,因为大多数单片机的RAM空间并不足以在运行BootLoader的同时还能存放所有的固件包数据。
写Flash之前一定要解锁FLASH_Unlock()。本来在初始化程序的时候就解锁了Flash,但是在程序加密的时候不小心又上锁了FLASH_Lock(),然后就导致接收完一帧数据写入失败,刚开始还以为是协议哪里出了问题,花大半天时间去研究Ymodem协议,所幸后来很快就通过在线调试定位到了问题。
接收数据的手段也有很多,SPIIICUARTUSB等等,有兴趣的,甚至可以研究一下通过IO来接收数据。常用的有串口(UART)升级、SD卡升级、U盘(USB)升级。因为产品是通过串口通信的,所以这里选择的是串口IAP升级。

另外就是在串口结束数据的时候,有网友说利用空闲中断接收过长的数据很容易出现问题,所以建议关闭串口中断,不使用中断的方式接收数据。

USART_ITConfig(USART2, USART_IT_RXNE, DISABLE);
USART_ITConfig(USART2, USART_IT_IDLE, DISABLE);

关于Ymodem协议的CRC16计算:

/**
  * @brief  Cal CRC16 for YModem Packet
  * @param  data
  * @param  length
  * @retval CRC value
  */
uint16_t Cal_CRC16(const uint8_t* pData, uint32_t len)
{

    uint16_t crc = 0;
  while(len--)
  {
    crc ^= (uint16_t)(*(pData++)) << 8;

    for(int i = 0; i < 8; i++)
    {
      if(crc & 0x8000)
      {
        crc = (crc << 1) ^ 0x1021;
      }
      else
      {
        crc = crc << 1;
      }
    }
  }
  return crc;
}

Ymodem接收数据:

/**
  * @brief  Receive a packet from sender
  * @param  data
  * @param  length
  * @param  timeout
  *          0: end of transmission
  *          -1: abort by sender
  *          >0: packet length
  * @retval 0: normally return
  *         -1: timeout or packet error
  *         1: abort by user
  */
static int32_t Receive_Packet (uint8_t *data, int32_t *length, uint32_t timeout)
{
  uint16_t i, packet_size, computedcrc;
  uint8_t c;
  *length = 0;

  if (Receive_Byte(&c, timeout) != 0)
  {
    return -1;
  }
  switch (c)  //The first byte of the received data
  {
    case SOH: //Packet start
      packet_size = PACKET_SIZE;
      break;
    case STX: //Start of Text
      packet_size = PACKET_1K_SIZE;
      break;
    case EOT:  //End of packet
      return 0;
    case CA:  //The sender stop transmission
      if ((Receive_Byte(&c, timeout) == 0) && (c == CA))
      {
        *length = -1;
        return 0;
      }
      else
      {
                printf(" stop transmission!\r\n");
        return -1;
      }
    case ABORT1:  
    case ABORT2: 
      return 1;
    default:
      return -1;
  }
  *data = c;
    /*Acquiring the remaining data (in bytes)*/
  for (i = 1; i < (packet_size + PACKET_OVERHEAD); i ++)  
  {     
    if (Receive_Byte(data + i, timeout) != 0)
    {
            printf(" Acquiring the remaining data err!\r\n");
      return -1;
    }
  }
    /*Check serial number and complement*/
  if (data[PACKET_SEQNO_INDEX] != ((data[PACKET_SEQNO_COMP_INDEX] ^ 0xff) & 0xff))  
  {
        printf(" Check serial number and complement err!\r\n");
    return -1;
  }

  /* Compute the CRC */
  computedcrc = Cal_CRC16(&data[PACKET_HEADER], (uint32_t)packet_size);
  /* Check that received CRC match the already computed CRC value
     data[packet_size+3]<<8) | data[packet_size+4] contains the received CRC 
     computedcrc contains the computed CRC value */
  if (computedcrc != (uint16_t)((data[packet_size+3]<<8) | data[packet_size+4]))
  {
    /* CRC error */
    return -1;
  }

  *length = packet_size;
  return 0;
}
/**
  * @brief  Receive a file using the ymodem protocol
  * @param  buf: Address of the first byte
  * @retval The size of the file
  */
int32_t Ymodem_Receive (uint8_t *buf)
{
  uint8_t packet_data[PACKET_1K_SIZE + PACKET_OVERHEAD], file_size[FILE_SIZE_LENGTH], *file_ptr, *buf_ptr;
  int32_t i, packet_length, session_done, file_done, packets_received, errors, session_begin, size = 0;
  uint32_t flashdestination, ramsource;

  /* Initialize flashdestination variable */
  flashdestination = APPLICATION_ADDRESS;
  Send_Byte(CRC16);
  for (session_done = 0, errors = 0, session_begin = 0; ;)
  {
    for (packets_received = 0, file_done = 0, buf_ptr = buf; ;)
    {
      switch (Receive_Packet(packet_data, &packet_length, NAK_TIMEOUT))
      {
        case 0:
          errors = 0;
          switch (packet_length)
          {
            /* Abort by sender */
            case - 1:
              Send_Byte(ACK);
              return 0;
            /* End of transmission */
            case 0:
              Send_Byte(ACK);
              file_done = 1;
              break;
            /* Normal packet */
            default:
              if ((packet_data[PACKET_SEQNO_INDEX] & 0xff) != (packets_received & 0xff))
              {
                Send_Byte(NAK);
              }
              else
              {
                if (packets_received == 0)
                {
                  /* Filename packet */
                  if (packet_data[PACKET_HEADER] != 0)
                  {
                    /* Filename packet has valid data */
                    for (i = 0, file_ptr = packet_data + PACKET_HEADER; (*file_ptr != 0) && (i < FILE_NAME_LENGTH);)
                    {
                      FileName[i++] = *file_ptr++;
                    }
                    FileName[i++] = '\0';
                    for (i = 0, file_ptr ++; (*file_ptr != ' ') && (i < (FILE_SIZE_LENGTH - 1));)
                    {
                      file_size[i++] = *file_ptr++;
                    }
                    file_size[i++] = '\0';
                    Str2Int(file_size, &size);

                    /* Test the size of the image to be sent */
                    /* Image size is greater than Flash size */
                    if (size > (USER_FLASH_SIZE + 1))
                    {
                      /* End session */
                      Send_Byte(CA);
                      Send_Byte(CA);
                      return -1;
                    }
                    /* erase user application area */
                    FLASH_If_Erase(APPLICATION_ADDRESS);
                    Send_Byte(ACK);
                    Send_Byte(CRC16);
                  }
                  /* Filename packet is empty, end session */
                  else
                  {
                    Send_Byte(ACK);
                    file_done = 1;
                    session_done = 1;
                    break;
                  }
                }
                /* Data packet */
                else
                {
                  memcpy(buf_ptr, packet_data + PACKET_HEADER, packet_length);
                  ramsource = (uint32_t)buf;                

                  /* Write received data in Flash */
                  if ( FLASH_If_Write(&flashdestination, (uint32_t*) ramsource, (uint16_t) packet_length/4) == 0)
                  {
                    Send_Byte(ACK);
                  }
                  else /* An error occurred while writing to Flash memory */
                  {
                    /* End session */
                    Send_Byte(CA);
                    Send_Byte(CA);
                    return -2;
                  }                                 
                }
                packets_received ++;
                session_begin = 1;
              }
          }
          break;
        case 1:
          Send_Byte(CA);
          Send_Byte(CA);
          return -3;
        default:
          if (session_begin > 0)
          {
            errors ++;
          }
          if (errors > MAX_ERRORS)
          {
            Send_Byte(CA);
            Send_Byte(CA);
            return 0;
          }
          Send_Byte(CRC16);
          break;
      }
      if (file_done != 0)
      {
        break;
      }
    }
    if (session_done != 0)
    {
      break;
    }
  }
  return (int32_t)size;
}

串口数据的接收和发送函数就粘贴了,一个字节的收发而已,没什么需要特别注意的。

关于固件的保存

BootLoader从接收固件到保存固件,主要有下面几个步骤:

  • 初始化Flash(主要是解锁)
  • 基于Ymodem协议从串口接收数据
  • 校验接收的数据
  • 擦除Flash
  • 写Flash,写完之后再读出来,和接收内存中的数据比对一下,防止写FLASH错误

初始化Flash:

/**
  * @brief  Initialize the FLASH.
  * @param  None
  * @retval FLASH Status
  */
FLASH_Status stm32_flash_init(void)
{
    /*Unlock the Flash to enable the flash control register access*/
    FLASH_Unlock();
    /*Clear pending flags*/
    FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);   
    /*Lock the Flash to disable the flash control register access*/ 
    return FLASH_COMPLETE;
}

擦除Flash:

/**
  * @brief  This function does an erase of all user flash area
  * @param  StartSector: start of user flash area
  * @retval 0: user flash area successfully erased
  *         1: error occurred
  */
uint32_t FLASH_If_Erase(uint32_t StartSector)
{
  uint32_t flashaddress;

  flashaddress = StartSector;

  while (flashaddress <= (uint32_t) USER_FLASH_LAST_PAGE_ADDRESS)
  {
    if (FLASH_ErasePage(flashaddress) == FLASH_COMPLETE)
    {
      flashaddress += FLASH_PAGE_SIZE;
    }
    else
    {
      /* Error occurred while page erase */
      return (1);
    }
  }
  return (0);
}

写固件到Flash:

/**
  * @brief  This function writes a data buffer in flash (data are 32-bit aligned).
  * @note   After writing data buffer, the flash content is checked.
  * @param  FlashAddress: start address for writing data buffer
  * @param  Data: pointer on data buffer
  * @param  DataLength: length of data buffer (unit is 32-bit word)   
  * @retval 0: Data successfully written to Flash memory
  *         1: Error occurred while writing data in Flash memory
  *         2: Written Data in flash memory is different from expected one
  */
uint32_t FLASH_If_Write(__IO uint32_t* FlashAddress, uint32_t* Data ,uint16_t DataLength)
{
  uint32_t i = 0;

  for (i = 0; (i < DataLength) && (*FlashAddress <= (USER_FLASH_END_ADDRESS-4)); i++)
  {
    /* the operation will be done by word */ 
    if (FLASH_ProgramWord(*FlashAddress, *(uint32_t*)(Data+i)) == FLASH_COMPLETE)
    {
     /* Check the written value */
      if (*(uint32_t*)*FlashAddress != *(uint32_t*)(Data+i))
      {
        /* Flash content doesn't match SRAM content */
        return(2);
      }
      /* Increment FLASH destination address */
      *FlashAddress += 4;
    }
    else
    {
      /* Error occurred while writing data in Flash memory */
      return (1);
    }
  }
  return (0);
}

有时候可能会对固件写保护,这样可以保护存在MCU上的代码 不会被异常修改,但是在正常的代码更新时要先失能对应的保护位,更新完成再次恢复之前的保护位

/**
  * @brief  Disable FLASH Write ProtectionPages.
    * @param  None
    * @retval None
  */
void FLASH_DisableWriteProtectionPages(void)
{
    uint32_t UserMemoryMask = 0;
    uint32_t useroptionbyte = 0, WRPR = 0;
    uint16_t var1 = OB_IWDG_SW, var2 = OB_STOP_NoRST, var3 = OB_STDBY_NoRST;
    FLASH_Status status = FLASH_BUSY;

    WRPR = FLASH_GetWriteProtectionOptionByte();

    /*Test whether the write protection*/
    if ((WRPR & UserMemoryMask) != UserMemoryMask)
    {
            useroptionbyte = FLASH_GetUserOptionByte();

            UserMemoryMask |= WRPR;

            status = FLASH_EraseOptionBytes();

            if (UserMemoryMask != 0xFFFFFFFF)
            {
                    status = FLASH_EnableWriteProtection((uint32_t)~UserMemoryMask);
            }

            if ((useroptionbyte & 0x07) != 0x07)
            {
                    /*Re-save option byte*/
                    if ((useroptionbyte & 0x01) == 0x0)
                    {
                            var1 = OB_IWDG_HW;
                    }
                    if ((useroptionbyte & 0x02) == 0x0)
                    {
                            var2 = OB_STOP_RST;
                    }
                    if ((useroptionbyte & 0x04) == 0x0)
                    {
                            var3 = OB_STDBY_RST;
                    }

                    FLASH_UserOptionByteConfig(var1, var2, var3);
            }

            if (status == FLASH_COMPLETE)
            {
                    printf("Write Protection disabled...\r\n");

                    printf("...and a System Reset will be generated to re-load the new option bytes\r\n");
                    /*Initiate a system reset request to reset the MCU*/
                    NVIC_SystemReset();
            }
            else
            {
                    printf("Error: Flash write unprotection failed...\r\n");
            }
    }
    else
    {
            printf("Flash memory not write protected\r\n");
    }
}

写保护:

先上代码:

/**
  * @brief  Write protects the desired pages
  * @note   This function can be used for all STM32F10x devices.
  * @param  FLASH_Pages: specifies the address of the pages to be write protected.
  *   This parameter can be:
  *     @arg For @b STM32_Low-density_devices: value between FLASH_WRProt_Pages0to3 and FLASH_WRProt_Pages28to31  
  *     @arg For @b STM32_Medium-density_devices: value between FLASH_WRProt_Pages0to3
  *       and FLASH_WRProt_Pages124to127
  *     @arg For @b STM32_High-density_devices: value between FLASH_WRProt_Pages0to1 and
  *       FLASH_WRProt_Pages60to61 or FLASH_WRProt_Pages62to255
  *     @arg For @b STM32_Connectivity_line_devices: value between FLASH_WRProt_Pages0to1 and
  *       FLASH_WRProt_Pages60to61 or FLASH_WRProt_Pages62to127    
  *     @arg For @b STM32_XL-density_devices: value between FLASH_WRProt_Pages0to1 and
  *       FLASH_WRProt_Pages60to61 or FLASH_WRProt_Pages62to511
  *     @arg FLASH_WRProt_AllPages
  * @retval FLASH Status: The returned value can be: FLASH_ERROR_PG,
  *         FLASH_ERROR_WRP, FLASH_COMPLETE or FLASH_TIMEOUT.
  */
FLASH_Status FLASH_EnableWriteProtection(uint32_t FLASH_Pages)
{
  uint16_t WRP0_Data = 0xFFFF, WRP1_Data = 0xFFFF, WRP2_Data = 0xFFFF, WRP3_Data = 0xFFFF;

  FLASH_Status status = FLASH_COMPLETE;

  /* Check the parameters */
  assert_param(IS_FLASH_WRPROT_PAGE(FLASH_Pages));

  FLASH_Pages = (uint32_t)(~FLASH_Pages);
  WRP0_Data = (uint16_t)(FLASH_Pages & WRP0_Mask);
  WRP1_Data = (uint16_t)((FLASH_Pages & WRP1_Mask) >> 8);
  WRP2_Data = (uint16_t)((FLASH_Pages & WRP2_Mask) >> 16);
  WRP3_Data = (uint16_t)((FLASH_Pages & WRP3_Mask) >> 24);

  /* Wait for last operation to be completed */
  status = FLASH_WaitForLastOperation(ProgramTimeout);

  if(status == FLASH_COMPLETE)
  {
    /* Authorizes the small information block programming */
    FLASH->OPTKEYR = FLASH_KEY1;
    FLASH->OPTKEYR = FLASH_KEY2;
    FLASH->CR |= CR_OPTPG_Set;
    if(WRP0_Data != 0xFF)
    {
      OB->WRP0 = WRP0_Data;

      /* Wait for last operation to be completed */
      status = FLASH_WaitForLastOperation(ProgramTimeout);
    }
    if((status == FLASH_COMPLETE) && (WRP1_Data != 0xFF))
    {
      OB->WRP1 = WRP1_Data;

      /* Wait for last operation to be completed */
      status = FLASH_WaitForLastOperation(ProgramTimeout);
    }
    if((status == FLASH_COMPLETE) && (WRP2_Data != 0xFF))
    {
      OB->WRP2 = WRP2_Data;

      /* Wait for last operation to be completed */
      status = FLASH_WaitForLastOperation(ProgramTimeout);
    }

    if((status == FLASH_COMPLETE)&& (WRP3_Data != 0xFF))
    {
      OB->WRP3 = WRP3_Data;

      /* Wait for last operation to be completed */
      status = FLASH_WaitForLastOperation(ProgramTimeout);
    }

    if(status != FLASH_TIMEOUT)
    {
      /* if the program operation is completed, disable the OPTPG Bit */
      FLASH->CR &= CR_OPTPG_Reset;
    }
  } 
  /* Return the write protection operation Status */
  return status;       
}

这些基本都是官方库里面的操作。

量产的时候可以如此操作,也是对代码的一种保护。

对应的还有读保护解除读保护,后面遇到再说。

关于代码的跳转运行

BootLoader主要负责的还有代码的跳转执行,大体框架如下:

if("升级条件")
    {       
        //升级引导操作;
    }
    else
    {
        /* Test if user code is programmed starting from address "APPLICATION_ADDRESS" */
        if (((*(vu32*)APPLICATION_ADDRESS) & 0x2FFE0000 ) == 0x20000000)
        {
        /* Jump to user application */
        JumpAddress = *(vu32*) (APPLICATION_ADDRESS + 4);
        JumpToApplication = (pFunction) JumpAddress;
        /* Initialize user application's Stack Pointer */
        __set_MSP(*(vu32*) APPLICATION_ADDRESS);
        JumpToApplication();
        }
        else
        {
            printf(" no user Program\r\n\n");
            //升级引导操作/其他操作;
        }
pFunction JumpToApplication;
typedef  void (*pFunction)(void);

这里面注意的有三点:

  • 触发升级的条件
  • 跳转的APP程序地址
  • 跳转方式

如何触发升级:也有很多选择,比如新增加一个按键,或者上电后延时一定的升级选择时间然后跳转APP,在或者置一个标志位。

在本着能不改动硬件就不改动硬件的原则,我一般在APP程序中通过串口接收一个特定的字符串,然后在Flash的某个地方写入一个标志位,比如在备份寄存器写版本号,然后软件复位单片机。

__disable_fault_irq(); 
NVIC_SystemReset(); 

单片机复位后运行BootLoader,先检查该Flash区域有无内容,有则引导升级,升级完成后再擦除对应Flash里面的内容。如果上电后没在对应的区域内检测到内容,则先跳转APP程序。跳转APP程序后也会先判断APP区的内容,没有的话(第一次只烧写了BootLoader,还未烧写APP)再次跳转到升级引导。

跳转的APP程序地址这个是提前要做的内容,根据BootLoader的大小,预留出足够的BootLoader空间和一些必须的Flash空间,然后指定APP的起始地址。

说到Flash空间分配的问题,值得注意的是如果设计OTA升级,最好预留一个升级失败的处理方案,比如原固件的备份与回滚问题,都得提前考虑好。本地升级好处是升级失败了大不了再升级一次,OTA升级失败可能会干扰联网以及数据传输问题。

跳转方式涉及到函数指针的内容,typedef void (*pFunction)(void)定义了一个新的函数指针类型,指向的函数形参为void,返回值为void

关于函数指针

这个本人接触的也比较少,按照网友的说法,一步一步来分析:

  1. 定义一个指向 unsigned char 型的指针:``unsigned char *p`;
  2. 将一个指针转化成指向 unsigned char 数据的的指针:(unsigned char*) p;
  3. 定义一个类型,该类型为int,并定义一个指向该类型的指针p:typedef int INT_Typedef INT_Typedef *p;
  4. 定义一个函数指针,该函数无参数,无返回值: typedef void (*Func) (void);

typedef 的作用是把已知的类型定义新类型,所以新类型(*Func)(void)的返回值是void。

如常见的main(),括号内是main的参数,所以该函数指针指向的函数无形参。其次(*Func)等同于main的地位。所以 Func就是函数指针了。

APP

相对于BootLoader程序,APP程序要做的工作就少很多了。

  • APP程序和BootLoader用的是不同的中断向量表,所以要重新指定中断向量表地址
  • 设置Flash的起始地址和Flash大小

重新指向中断向量飙

先说如何重新定位中断向量表,在程序最前面加上这么一句即可:

NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x5000); 

或者:

SCB->VTOR = FLASH_BASE | 0x5000; 

上面不同的方式其实是同样的方式,只不过第一个是加了一个函数的外壳,Flash基地址加偏移量,偏移量就是在设计BootLoader的时候分配占用的空间大小。

/**
  * @brief  Sets the vector table location and Offset.
  * @param  NVIC_VectTab: specifies if the vector table is in RAM or FLASH memory.
  *   This parameter can be one of the following values:
  *     @arg NVIC_VectTab_RAM
  *     @arg NVIC_VectTab_FLASH
  * @param  Offset: Vector Table base offset field. This value must be a multiple 
  *         of 0x200.
  * @retval None
  */
void NVIC_SetVectorTable(uint32_t NVIC_VectTab, uint32_t Offset)
{ 
  /* Check the parameters */
  assert_param(IS_NVIC_VECTTAB(NVIC_VectTab));
  assert_param(IS_NVIC_OFFSET(Offset));  

  SCB->VTOR = NVIC_VectTab | (Offset & (uint32_t)0x1FFFFF80);
}

APP程序的工程配置

  1. 打开Options for Target
  2. Target:修改IROM1 Start地址Size大小Start地址为Flash基地址0x8000000+BootLoader分配的地址大小,Size一般为单片机总Flash大小减去BootLoader分配的地址大小 IROM1地址:0x8000000->0x8005000 大小剩余:0x80000->0x7B000 内存IRAM无需修改
  3. User:勾选Run#1 fromelf.exe --bin -o "[email protected]" "#L" ,把生成的Hex文件转换为bin文件,省去了上位机进行文件转换的麻烦
  4. Linker --勾选Use Memory Layout from Target Dialog,启用在Target中对Flash和RAM的地址配置

上位机下载

如果选用secure CRT或者Xsehll上位机下载,软件需要设置:选项--会话选项--XYZmodem--1024byte

最后

下位机部分要注意的基本就是这些,如果有遗漏的后面再补充。上位机的部分后面有时间在写。欢迎指正

至此,所有!


这是一个记录个人学习笔记的博客,如果内容涉嫌侵权,请及时联系我删改。