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

来源:互联网 发布:群组推荐算法 编辑:程序博客网 时间:2024/06/05 23:55

  最近一周一直在基于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:
这里写图片描述
  Boot是放在这个默认起始地址的,App则要往后移动,这里设置为0x08008000:
这里写图片描述
  需要注意,这是在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);//定义函数指针,用于跳转到APPtypedef  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上。关键要注意上述几点内容。当然,上述内容属于个人见解。

原创粉丝点击