基于STM32F429的IAP升级(HAL库/RS485)

  最近一周一直在基于STM32F429项目的IAP工程,耗时4天才完成,得空记录下来。

文章主要涵盖了以下几点:
1. IAP是什么?
2. bin文件和hex文件的差别
3. ymodem协议介绍及其缺陷
4. RS485通讯
5. IAP的main()函数代码片段

  项目的框架如下:

                   ymodem协议
    PC机_超级终端 -----------------> STM32产品的串口
                   RS485通讯线

  从ST官网下载的IAP的SDK,其中包含了经典的ymodem协议和基于STM32F429的HAL库,工程就是基于该SDK开发的。

1. IAP的概念

  IAP的具体概念解析网上搜索一大堆,在这里简单描述。IAP(In Application Programming)即在应用编程,常听说的还有ISP(In System Programming)即在系统编程。ISP指的是将通过JTAG等接口将单片机程序烧录进单片机的FLASH(当然也可以是其他存储介质,如SRAM),而IAP指的是采用引导程序(Boot) + 应用程序(App)的方式烧写单片机程序,App是真正实现业务逻辑功能的代码。

  一般产品的调试口,也就是JTAG口是被置于机壳里面的,烧写需要打开机壳,也需要专业工具和电脑桌面软件。IAP的Boot程序通过ISP的方式烧录到单片机的低地址的FLASH处,每次单片机复位后会先执行Boot程序,在Boot程序中进行判断,用户是否要升级,若是则从串口(或者网口/CAN通信口)读取App程序写到高地址的FLASH,读写完毕后再跳转到FLASH上App的起始地址,执行业务逻辑功能代码,若否则直接跳转到App的代码处理:

                             |-- 要升级 --> 读取读取串口发来的APP程序,写入FLASAH目标地址 --|
                             |                                                              |
    Boot: 判断是否要升级App -|                                                             |
                             |-- 不升级 ---------------------------------------------------------> 跳转到APP程序起始地址处理

  Boot和App都是单片机程序,只是实现的功能不同,前者是为了引导App,后者是为了实现业务逻辑功能。这里有一个关键的动作,就是跳转,即从Boot跳转到App起始地址处。
  需要清晰2个概念:
  (1) 程序的起始地址
  程序的起始地址默认是被放在FLASH的起始地址处,即0x08000000:
基于STM32F429的IAP升级(HAL库/RS485)_第1张图片
  Boot是放在这个默认起始地址的,App则要往后移动,这里设置为0x08008000:
基于STM32F429的IAP升级(HAL库/RS485)_第2张图片
  需要注意,这是在App没有采用分散加载时设置的程序存放起始地址,若采用了分散加载,则需要修改工程中的.sct文件。详细内容可参照杜春雷的《ARM体系结构及编程》。

  (2) 中断向量表的地址
  对于STM32来说,每个单片机程序都有一张中断向量表,也就是说,在存有Boot和App的FLASH上就有两张中断向量表,Boot根据Boot程序中的中断向量表发生中断跳转,同理,App就要根据App程序的中断向量表发生中断跳转。中断向量表的摆放位置正是程序的开始地址。所以需要将App的中断向量表的摆放位置放在0x08004000。在system_stm32f4xx.c中:

#ifdef VECT_TAB_SRAM
  SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM */
#else
  SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH */
#endif

  程序运行时候发生中断/异常/系统调度时就会去读取SCB->VTOR以获取中断向量表的地址;FLASH_BASE即STM32F429的(内置)FLASH的起始地址:

#define FLASH_BASE  ((uint32_t)0x08000000)

  VECT_TAB_OFFSET是偏移量,所以我们可以通过设置此值指定中断向量表的存放地址。

  讲到这里,顺便介绍STM32的中断向量表。表的形态如下:(摘自startup_stm32f429xx.s)

__Vectors       DCD     __initial_sp               ; Top of Stack
                DCD     Reset_Handler              ; Reset Handler
                DCD     NMI_Handler                ; NMI Handler
                ; ...
                ; External Interrupts
                DCD     WWDG_IRQHandler                   ; Window WatchDog                                        
                DCD     PVD_IRQHandler                    ; PVD through EXTI Line detection                        
                ; ...
                DCD     LTDC_ER_IRQHandler                ; LTDC error
                DCD     DMA2D_IRQHandler                  ; DMA2D

__Vectors_End
__Vectors_Size  EQU  __Vectors_End - __Vectors

  显然,Boot程序结束后程序运行指针跳转到App的__initial_sp处,__initial_sp处存放的是App的栈的起始地址。中断向量表的作用是当程序发生异常/中断的时候会根据表中的标号而跳转到具体对应的中断处理函数(ISR)。

2. ymodem协议介绍

  关于ymodem协议的接收网上资料甚多,不做过多的介绍了。简单分析ymodem的数据包格式:

/* /-------- Packet in IAP memory ------------------------------------------\
 * | 0      |  1    |  2     |  3   |  4      | ... | n+4     | n+5  | n+6  | 
 * |------------------------------------------------------------------------|
 * | unused | start | number | !num | data[0] | ... | data[n] | crc0 | crc1 |
 * \------------------------------------------------------------------------/
 * the first byte is left unused for memory alignment reasons                 */

  (1) unused:无意义,用于字节对齐
  (2) start:起始信号,”SOH”表本数据包的数据区有128字节,”STX”则表示有1024字节
  (3) number:数据包的编号,编号为0x00-0xFF。到数据包编号到达255后,将会从0开始计数。
  (4) !num:数据包编号的反码
  (5) data[0]…data[n]:数据区。对于第一个数据包(编号为0),存放的是文件名
  (6) CRC0、CRC1:校验码(只有数据区参与校验)
  下面是通讯流程:
  (1) 接收方发送一个字符’C’,也就是十六进制0x43。代表接收方已经处于接收数据的状态
  (2) 发送方接收到’C’之后,发送头帧数据包,数据包格式如上所述,此时数据区的数据是文件名和文件大小
  (3) 接收方收到数据包后发送ACK应答正确,然后发送一个字符’C’,发送方收到’C’后开始发送第二帧数据,第二帧数据即使第一个数据包
  (4) 接收方收好数据包后,发送ACK正确应答,然后等待下一包数据传送完毕,继续ACK应答,如此循环
  (5) 数据传输完毕后,发送方第一次发EOT,第一次接收方以NAK应答,进行二次确认
  (6) 发送方收到NAK后,第二次发EOT。接收方第二次收到结束符,依次以ACK和C做应答
  (7) 发送方收到ACK和C之后,发送结束符
  (8) 接收方收到结束符之后,以ACK做应答,然后通信正式结束

  ymodem协议除了接收数据包外还会将数据包按照指定地址写入FLASH。本SDK使用的是HAL库。
  这部分的实现在ymodem.c的Ymodem_Receive()函数中:

#define PACKET_HEADER_SIZE      ((uint32_t)3)
#define PACKET_DATA_INDEX       ((uint32_t)4)
#define PACKET_START_INDEX      ((uint32_t)1)
#define PACKET_NUMBER_INDEX     ((uint32_t)2)
#define PACKET_CNUMBER_INDEX    ((uint32_t)3)
#define PACKET_TRAILER_SIZE     ((uint32_t)2)
#define PACKET_OVERHEAD_SIZE    (PACKET_HEADER_SIZE + PACKET_TRAILER_SIZE - 1)
#define PACKET_SIZE             ((uint32_t)128)
#define PACKET_1K_SIZE          ((uint32_t)1024)

uint32_t flashdestination;
uint8_t aPacketData[PACKET_1K_SIZE + PACKET_DATA_INDEX + PACKET_TRAILER_SIZE];

static HAL_StatusTypeDef ReceivePacket(uint8_t *p_data, uint32_t *p_length, uint32_t timeout)
{
  uint32_t crc;
  uint32_t packet_size = 0;
  HAL_StatusTypeDef status;
  uint8_t char1;

  *p_length = 0;
  //调用HAL库函数接收字符,UartHandle是数据包的句柄
  status = HAL_UART_Receive(&UartHandle, &char1, 1, timeout);

  if (status == HAL_OK)
  {
    switch (char1)
    {
      case SOH:
        packet_size = PACKET_SIZE;      //PACKET_SIZE为128
        break;
      case STX:
        packet_size = PACKET_1K_SIZE;   //PACKET_1K_SIZE为1024
        break;
      case EOT:                         //数据传输完毕
        break;
      case CA:
        if ((HAL_UART_Receive(&UartHandle, &char1, 1, timeout) == HAL_OK) && (char1 == CA))
        {
          packet_size = 2;
        }
        else
        {
          status = HAL_ERROR;
        }
        break;
      case ABORT1:
      case ABORT2:
        status = HAL_BUSY;
        break;
      default:
        status = HAL_ERROR;
        break;
    }
    *p_data = char1;

    if (packet_size >= PACKET_SIZE )
    {
      //接收真正的数据包
      status = HAL_UART_Receive(&UartHandle, &p_data[PACKET_NUMBER_INDEX], packet_size + PACKET_OVERHEAD_SIZE, timeout);

      //检验数据包
      if (status == HAL_OK )
      {
        if (p_data[PACKET_NUMBER_INDEX] != ((p_data[PACKET_CNUMBER_INDEX]) ^ NEGATIVE_BYTE))
        {
          packet_size = 0;
          status = HAL_ERROR;
        }
        else
        {
          /* Check packet CRC */
          crc = p_data[ packet_size + PACKET_DATA_INDEX ] << 8;
          crc += p_data[ packet_size + PACKET_DATA_INDEX + 1 ];
          if (Cal_CRC16(&p_data[PACKET_DATA_INDEX], packet_size) != crc )
          {
            packet_size = 0;
            status = HAL_ERROR;
          }
        }
      }
      else
      {
        packet_size = 0;
      }
    }
  }
  *p_length = packet_size;
  return status;
}

COM_StatusTypeDef Ymodem_Receive ( uint32_t *p_size )  //p_size为输出型参数,用于存放从PC端发来的文件的大小
{
  uint32_t i, packet_length, session_done = 0, file_done, errors = 0, session_begin = 0;

  uint32_t ramsource, filesize;
  uint8_t *file_ptr;
  uint8_t file_size[FILE_SIZE_LENGTH], tmp, packets_received;
  COM_StatusTypeDef result = COM_OK;

  /* APPLICATION_ADDRESS是App的起始地址 */
  flashdestination = APPLICATION_ADDRESS;

  while ((session_done == 0) && (result == COM_OK))
  {
    packets_received = 0;       //记录接收到的数据包的个数
    file_done = 0;
    while ((file_done == 0) && (result == COM_OK))
    {
      switch (ReceivePacket(aPacketData, &packet_length, DOWNLOAD_TIMEOUT))
      {
        case HAL_OK:
          errors = 0;
          switch (packet_length)
          {
            case 2:
              /* 发送方终止发送 */
              Serial_PutByte(ACK);
              result = COM_ABORT;
              break;
            case 0:
              /* 正常结束传输 */
              Serial_PutByte(ACK);
              file_done = 1;
              break;
            default:
              /* 数据包编号出错 */
              if (aPacketData[PACKET_NUMBER_INDEX] != packets_received)
              {
                Serial_PutByte(NAK);
              }
              else
              {
                if (packets_received == 0)
                {
                  /* 数据包编号为0,证明这是数据区存放文件名的帧数据,事实上这个判断是有误的 */
                  if (aPacketData[PACKET_DATA_INDEX] != 0)
                  {
                    //读取文件名
                    i = 0;
                    file_ptr = aPacketData + PACKET_DATA_INDEX;
                    while ( (*file_ptr != 0) && (i < FILE_NAME_LENGTH))
                    {
                      aFileName[i++] = *file_ptr++;
                    }
                    aFileName[i++] = '\0';

                    //读取文件大小
                    i = 0;
                    file_ptr ++;
                    while ( (*file_ptr != ' ') && (i < FILE_SIZE_LENGTH))
                    {
                      file_size[i++] = *file_ptr++;
                    }
                    file_size[i++] = '\0';
                    Str2Int(file_size, &filesize);

                    if (*p_size > (USER_FLASH_SIZE + 1))  //文件大过于可供存储的FLASH的空间
                    {
                      tmp = CA;
                      HAL_UART_Transmit(&UartHandle, &tmp, 1, NAK_TIMEOUT);
                      HAL_UART_Transmit(&UartHandle, &tmp, 1, NAK_TIMEOUT);
                      result = COM_LIMIT;
                    }

                    /* 擦除扇区 */
                    FLASH_If_Erase(APPLICATION_ADDRESS);
                    *p_size = filesize;

                    Serial_PutByte(ACK);
                    Serial_PutByte(CRC16);
                  }
                  /* 文件头为空,传输结束 */
                  else
                  {
                    Serial_PutByte(ACK);
                    file_done = 1;
                    session_done = 1;
                    break;
                  }
                }
                else /* 真正的数据包 */
                {
                  //将收到的数据存放到FLASH
                  ramsource = (uint32_t) & aPacketData[PACKET_DATA_INDEX];
                  if (FLASH_If_Write(flashdestination, (uint32_t*) ramsource, packet_length/4) == FLASHIF_OK)
                  {
                    flashdestination += packet_length;
                    Serial_PutByte(ACK);
                  }
                  else  //写入失败
                  {
                    /* End session */
                    Serial_PutByte(CA);
                    Serial_PutByte(CA);
                    result = COM_DATA;
                  }
                }
                packets_received ++;
                session_begin = 1;
              }
              break;
          }
          break;
        case HAL_BUSY:
          Serial_PutByte(CA);
          Serial_PutByte(CA);
          result = COM_ABORT;
          break;
        default:
          if (session_begin > 0)
          {
            errors ++;  
          }
          if (errors > MAX_ERRORS) errors大于MAX_ERRORS,PC端将收到"接收端未响应的提示",终止传输
          {
            /* Abort communication */
            Serial_PutByte(CA);
            Serial_PutByte(CA);
          }
          else
          {
            Serial_PutByte(CRC16);  //返回'C'字符,PC端提示接收端未响应并记录次数,超过次数PC端也将提出
          }
          break;
      }
    }
  }
  return result;
}

  事实上,对数据包的接收处理操作是有问题的:packets_received用于记录数据包的个数,它是uint8_t类型,取值是0-255这确实是符合ymodem协议的,但是超过255的数据包呢,观察上面代码可以发现并没有对第超过255个数据包,也就是第256个数据包的处理。第256个数据包的编号也是为0,会进入:

if (packets_received == 0)
{
    if (aPacketData[PACKET_DATA_INDEX] != 0)
    {
        //进行读取文件名、文件大小、擦除FLASH操作
    }
}

  但是编号为0的第256个数据包的数据区事实上是数据,收到该数据包还是要以真正的数据包的写FLASH等操作。

  一开始我利用IAP传输小于256K的APP的时候是正常运行的,后来传输400+k的APP就会出现问题:无法跳转至APP。经过多番调试才定位于此,所以简单修改上面的代码,代码片段为:

volatile int8_t is_first_pack = 1;
switch (RPreturn)
{
    case HAL_OK:
    errors = 0;
    after_isp = 1;
    switch (packet_length)
    {
        case 2:
        Serial_PutByte(ACK);
        result = COM_ABORT;
        break;
    case 0:
        Serial_PutByte(ACK);        
        file_done = 1;
        break;

    default:
    if (aPacketData[PACKET_NUMBER_INDEX] != packets_received)   //ÅжÏÊý¾Ý°üµÄ±àºÅÊÇ·ñÕýÈ·
    {
        Serial_PutByte(NAK);
    }
    else
    {
        if (packets_received == 0 )
        {                           
          if (aPacketData[PACKET_DATA_INDEX] != 0 )
          {
            if (is_first_pack)  //第一个编号为0的数据包
            {
                //...               
                is_first_pack = 0;              
            }
            else    //即使数据编号为0但不是第一个数据包,采取存储操作          
            {
                ramsource = (uint32_t) & aPacketData[PACKET_DATA_INDEX];        
                if (FLASH_If_Write(flashdestination, (uint32_t*) ramsource, packet_length/4) == FLASHIF_OK)
                {
                    flashdestination += packet_length;
                    Serial_PutByte(ACK);
                }
                else
                {
                    Serial_PutByte(CA);
                    Serial_PutByte(CA);
                    result = COM_DATA;
                }                
            }
          }
          else 
          {
            Serial_PutByte(ACK);
            file_done = 1;
            session_done = 1;
            break;
          }
        }
        else
        {

            ramsource = (uint32_t) & aPacketData[PACKET_DATA_INDEX];                        
            if (FLASH_If_Write(flashdestination, (uint32_t*) ramsource, packet_length/4) == FLASHIF_OK)
            {
                flashdestination += packet_length;
                Serial_PutByte(ACK);
            }
            else
            {
                Serial_PutByte(CA);
                Serial_PutByte(CA);
                result = COM_DATA;
            }        
        }
        packets_received ++;
        packets_received = packets_received % 256;
        session_begin = 1;
      }
      break;
    }
    break;
    //..
    break;
}

3. Bin和Hex文件的差别

  第一次接触Hex文件一般是在学习51内核单片机的时候,通过烧录器烧录到开发板上使用的就是Hex格式的文件。Hex文件是以ASCII文本形式保存编译后的二进制文件信息,注意这里强调的是文本文件(而非数据文件,即二进制文件)。文本文件是人们可以看得懂的文件,但是计算机/MCU只认识二进制数据文件(Bin文件),Bin文件才是MCU固件烧写的最终形式,也就是说MCU的ROM中烧写的内容完全是Bin文件。由此可得,我们通过烧录器烧录Hex文件到单片机的ROM时,烧录器其实会将Hex文件的数据转为Bin文件的数据,最后才烧录到ROM。

  其实明白这一点我们就知道在IAP工程中,APP文件需要是Bin格式的文件而不能是Hex文件。既然Bin文件是MCU/计算机最终想要的,为什么我们不直接生成Bin文件,而却要生成Hex文件?其实Hex文件保存的不仅是Bin文件的内容,还有一些附属配置信息,随便拿个项目Hex文件分析:

: 10 9AB0 00 6841298459D0420F9AC4641EFBC10408 2E
: 10 9AC0 00 E7604BC5CC64011029B85A6980413C55 08
: 08 9AD0 00 55557C2964291C81 15
: 04 0000 05 080081AD C1
: 00 0000 01 FF

  Hex文件中的数据是ASCII编码,所以是人们能看懂的。上面3行内容,每行都是以’:’开始的,之后是数据长度、地址域、数据类型、数据域、校验和。

/* /-------- Hex Data format -------------------------------------------------------------\
 * |   0     |  1     |  34  |    5     |    6   |    7   |  ...   |   n     |   n + 1  |
 * |--------------------------------------------------------------------------------------|
 * | : 开始 | 数据长度 |地址域 | 记录类型 | 数据域 | 数据域 | 数据域 | 数据域n |  校验和  |
 * \--------------------------------------------------------------------------------------/
 * 每行数据都是以冒号开始的                 */

  注:记录类型的意义
  (1) 00: 数据记录
  (2) 01: 文件结束记录
  (3) 02: 扩展段地址记录
  (4) 03: 段开始地址记录
  (5) 04: 扩展线性地址记录
  (6) 05: 线性地址开始记录

  由此可见,生成Hex文件的意义在于:
  (1) Hex文件使用ASCII文本保存固件信息,方便查看固件内容
  (2) 文件内容每行的校验和与最后一行的文件结束标志,在文件的传输与保存过程中能够检验固件是否完整

  因此hex文件更适用于保存与传输。相比之下,Bin文件纯二进制文件,内部只包含程序编译后的机器码和变量数据。当文件损坏时,我们也无法知道文件已损坏。不过在IAP中,Bin文件仍旧是不可替代的。

4. RS485通讯

  从ST官网下载的的IAP SDK是基于RS232通讯的,即ymodem默认是从串口接收在APP数据的。但是我这个实际项目中用到的是采用RS485的通讯。RS232转为RS485,在软件上只是多了一步方向控制操作。因为RS485是半双工通讯,所以在发送单片机需要发送数据时需要将RS485总线设置为发送状态,接收数据则需要设置为接受状态。关于收发控制,软件上实现只是拉高/拉低对应控制RS485控制芯片的接收状态的GPIO即可。我的做法是默认是接收状态,当要发送时在发送函数内切换为发送状态,函数退出之前又切回接收状态。具体关于RS485通讯的详细,可阅读http://blog.csdn.net/qq_29344757/article/details/71516037一文中。

5. IAP的main代码片段分析

  IAP升级代码工程,一般需要基于一个能跑起来的通讯(这里是指RS485)和LED灯(用于指示状态,当然也可以用LCD等其他提示状态)运行正常的工程代码。下面是main()函数:

void SerialDownload(void);

//定义函数指针,用于跳转到APP
typedef  void (*pFunction)(void);   
extern pFunction JumpToApplication;
extern uint32_t JumpAddress;
UART_HandleTypeDef UartHandle;

int main(void)
{
    HAL_Init();
    SystemClock_Config();

    LED_BSP_Init();
    FLASH_If_Init();

    //初始化RS485
    UART_Init();
    RS485_RX_ENABLE();      //默认设置为接收状态
    HAL_Delay(10);

IAP:    
    Serial_PutString((uint8_t *)"\r\n====================================================================");
    Serial_PutString((uint8_t *)"\r\n=                       IAP For STM32F429xx                        =");
    Serial_PutString((uint8_t *)"\r\n====================================================================\r\n");

    SerialDownload();   
    HAL_Delay(10);

    //APPLICATION_ADDRESS是在FLASH中存放APP的起始地址
    //此判断是为了保证APP的栈地址是在SRAM中。其实结果并不一定是0x20000000。有些APP可能定义的全局变量较多,那么栈的起始地址会偏移
    if (((*(__IO uint32_t*)APPLICATION_ADDRESS) & 0x2FFE0000 ) == 0x20000000)   
    {

        Serial_PutString((uint8_t *)"\r\n=======================  Run application  ======================= \r\n\n");
        HAL_Delay(10);

        /* Jump to user application */
        JumpAddress = *(__IO uint32_t*) (APPLICATION_ADDRESS + 4);
        JumpToApplication = (pFunction) JumpAddress;
        /* Initialize user application's Stack Pointer */
        __set_MSP(*(__IO uint32_t*) APPLICATION_ADDRESS);
        JumpToApplication();
    }

    Serial_PutString((uint8_t *)"\r\n ======================= Download error, once again ======================= \r\n\n");
goto IAP;
}

  IAP的实现还是十分简单,但是中间走了N多弯路,特别是在调试ymodem接收大于256kb的APP上。关键要注意上述几点内容。当然,上述内容属于个人见解。

你可能感兴趣的:(STM32单片机)