IAP详解

单片机中,升级固件的方式有很多种,ICP,ISP和IAP是最常见的几种方式。 而今天,我就用at32f421官网上的例程,对IAP进行介绍。

具体的例程代码在这里可以下载

选择这个

在这里插入图片描述

下载后解压,IAP例程就在utilities文件夹中。

IAP

IAP是In Application Programming 的缩写,即在应用编程。 是用户通过自己编写的程序,在程序运行过程中,对User Flash的部分区域进行烧写,达到升级固件作用的方式。 IAP的目的是为了在产品发布后,可以通过预留的通信口对产品中的固件程序进行更新升级。

通常,为了实现IAP功能,需要在设计固件程序时编写两个项目代码,第一个项目程序不执行正常的功能操作,而只是通过某种通信方式接收程序或数据,执行对第二部分代码的更新,这个项目程序称为boot loader程序。第二个项目代码才是真正的功能代码。 这两部分代码都同时烧录在User Flash中。

下面对这两个项目代码分别进行介绍:

boot loader程序

正如上面所介绍的,boot loader程序的最大作用就是通过某种通信方式接收程序或者数据。 而在这个例程中,我们使用USART串口通信,因此,在boot loader中需要我们初始化串口。其次,作为一个接收程序或数据的程序,何时开始接收程序和数据也是我们需要考虑的问题。 因此,在boot loader中,我们也需要进行判断。 而最重要的,当然就是如何接收程序或数据。

因此boot loader程序的大体如下:

int main(void)
{
  system_clock_config();
    
  at32_board_init();
    
  nvic_priority_group_config(NVIC_PRIORITY_GROUP_4);

  if(flash_upgrade_flag_read() == RESET)
  {
    if(((*(uint32_t*)(APP_START_ADDR + 4)) & 0xFF000000) == 0x08000000)
      app_load(APP_START_ADDR);
  }

  uart_init(115200);

  if(flash_upgrade_flag_read() != RESET)
    back_ok();
  
  tmr_init();

  while(1)
  {
    iap_upgrade_app_handle();
  }
}

system_clock_config();,at32_board_init();nvic_priority_group_config(NVIC_PRIORITY_GROUP_4);三个函数的主要作用是初始化开发板和定义中断优先级,这三个函数在所有单片机的程序中都有实现,就不仔细介绍了. 主要介绍的是后面有关IAP的内容

if(flash_upgrade_flag_read() == RESET)
  {
    if(((*(uint32_t*)(APP_START_ADDR + 4)) & 0xFF000000) == 0x08000000)
      app_load(APP_START_ADDR);
  }

这里就是检查IAP更新标志,更新标志为RESET,说明没有新的程序或数据需要接收,因此boot loader程序跳转到APP应用程序中.

其中跳转到APP应用程序函数的细节如下:

void app_load(uint32_t app_addr)
{
  /* check the address of stack */
  if(((*(uint32_t*)app_addr) - 0x20000000) <= (SRAM_SIZE * 1024))
  {
    /* disable periph clock */
    crm_periph_clock_enable(CRM_TMR3_PERIPH_CLOCK, FALSE);
    crm_periph_clock_enable(CRM_USART1_PERIPH_CLOCK, FALSE);
    crm_periph_clock_enable(CRM_GPIOA_PERIPH_CLOCK, FALSE);

    /* disable nvic irq and clear pending */
    nvic_irq_disable(USART1_IRQn);
    nvic_irq_disable(TMR3_GLOBAL_IRQn);
    __NVIC_ClearPendingIRQ(USART1_IRQn);
    __NVIC_ClearPendingIRQ(TMR3_GLOBAL_IRQn);

    jump_to_app = (iapfun)*(uint32_t*)(app_addr + 4);        /* code second word is reset address */
    __set_MSP(*(uint32_t*)app_addr);                        /* init app stack pointer(code first word is stack address) */
    jump_to_app();                                          /* jump to user app */
  }
}

当程序从boot loader程序中跳转到APP程序的时候,需要禁止时钟和中断等. 然后再执行跳转.

typedef void (*iapfun)(void);

iapfun jump_to_app;

jump_to_app = (iapfun)*(uint32_t*)(app_addr + 4);        
__set_MSP(*(uint32_t*)app_addr);                        
jump_to_app();    

上面是实现跳转到APP的具体步骤.

先用typedef定义一个函数指针类型iapfun,这个指针指向一个无返回值,无参数的函数.

然后再声明一个名为jump_to_app的变量,他的类型是iapfun.

然后从app_addr地址中读取一个32位的值,将其解释为一个函数指针,赋值给jump_to_app这个变量.其中app_addr是APP程序的入口点地址.

__set_MSP(*(uint32_t*)app_addr);这一句将栈指针设置为应用程序的地址处存储值.

最后通过调用jump_to_app指向的函数,跳转到APP程序的入口点,开始执行APP程序.

如果IAP更新标志为SET, 说明有新的程序或数据需要接收,因此不执行if语句中的内容,程序继续执行.

后续的程序则为接收程序或数据所需要的内容:串口初始化,发送ok给上位机,时钟初始化和最重要的处理IAP升级APP.

串口初始化

由于这个demo是通过USART串口实现IAP功能的,因此需要对串口进行初始化.

串口初始化其实就是对串口的通信协议进行规定,像波特率,数据位,起始位,停止位,校验位等.

uart_init(115200);

void uart_init(uint32_t baudrate)
{
  gpio_init_type gpio_init_struct;
  /* enable the usart and it's io clock */
  crm_periph_clock_enable(CRM_USART1_PERIPH_CLOCK, TRUE);
  crm_periph_clock_enable(CRM_GPIOA_PERIPH_CLOCK, TRUE);

  /* set default parameter */
  gpio_default_para_init(&gpio_init_struct);

  /* configure the usart1_tx/rx pa9/pa10 */
  gpio_init_struct.gpio_drive_strength = GPIO_DRIVE_STRENGTH_STRONGER;
  gpio_init_struct.gpio_out_type  = GPIO_OUTPUT_PUSH_PULL;
  gpio_init_struct.gpio_mode = GPIO_MODE_MUX;
  gpio_init_struct.gpio_pins = GPIO_PINS_9 | GPIO_PINS_10;
  gpio_init_struct.gpio_pull = GPIO_PULL_NONE;
  gpio_init(GPIOA, &gpio_init_struct);

  /* config usart1 iomux */
  gpio_pin_mux_config(GPIOA, GPIO_PINS_SOURCE9, GPIO_MUX_1);
  gpio_pin_mux_config(GPIOA, GPIO_PINS_SOURCE10, GPIO_MUX_1);

  /*configure usart nvic interrupt */
  nvic_irq_enable(USART1_IRQn, 0, 0);

  /*configure usart param*/
  usart_init(USART1, baudrate, USART_DATA_8BITS, USART_STOP_1_BIT);
  usart_transmitter_enable(USART1, TRUE);
  usart_receiver_enable(USART1, TRUE);
  usart_interrupt_enable(USART1, USART_RDBF_INT, TRUE);
  usart_enable(USART1, TRUE);
}

从代码中可以看出,这次通讯使用的IO口为A9和A10,复用功能都是MUX1,其中TX为A9,RX为A10.

波特率为115200,8个数据位,1个停止位. 串口的初始化虽然重要,但是其实就是改变一下串口的数据,并不复杂.

返回OK给上位机

当上位机给下位机发送了需要通过IAP升级固件的信号后,下位机会返回一个信号,告知上位机可以开始发送程序或数据,下位机已经准备好了接收.

 if(flash_upgrade_flag_read() != RESET)
    back_ok();


void back_ok(void)
{
  usart_data_transmit(USART1, 0xCC);
  while(usart_flag_get(USART1, USART_TDC_FLAG) == RESET);
  usart_data_transmit(USART1, 0xDD);
  while(usart_flag_get(USART1, USART_TDC_FLAG) == RESET);
}

而这里则是通过串口向上位机发送0xCC0xDD来实现的. 当上位机接收到这两个信号后,就开始发送正式的升级所需的程序或数据.

计数器初始化

这里的计数器初始化是为了展示boot loader程序正在进行而闪烁的LED2灯所设置的,和IAP本身的功能没有太多的关系,哪怕没有这段代码,boot loader也能正常的运行.

处理IAP升级APP

boot loader中最重要的部分就是如何实现固件升级的功能. 而这个功能,就在处理IAP升级APP中实现.

while(1)
  {
    /* upgrade handle */
    iap_upgrade_app_handle();
  }

这里通过一个while循环不断的进行判断和处理,直到固件升级完成.

void iap_upgrade_app_handle(void)
{
  command_handle();
  app_update_handle();
}

而这个过程又被分成了处理command命令和处理APP升级两个部分.下面分别对这两个部分进行介绍.

command处理

下面是command_handle函数的具体实现

void command_handle(void)
{
  uint8_t val, checksum;
  uint16_t index;
  /* check whether received usart data */
  if(usart_group_struct.count > 0)
    val = data_take();
  else
    return;

  if(update_status == UPDATE_PRE)
  {
    if(cmd_ctr_step == CMD_CTR_IDLE)
    {
      if(val == 0x5A)
        cmd_ctr_step = CMD_CTR_INIT;
    }
    else if(cmd_ctr_step == CMD_CTR_INIT)
    {
      if(val == 0x01)
      {
        cmd_ctr_step = CMD_CTR_DONE;
      }
      else if(val == 0xA5)
      {
        cmd_ctr_step = CMD_CTR_APP;
      }
      else
      {
        cmd_ctr_step = CMD_CTR_ERR;
      }
    }
  }
  else if(update_status == UPDATE_ING)
  {
    switch(cmd_data_step)
    {
      case CMD_DATA_IDLE:
        if(val == 0x31)
        {
          cmd_data_step = CMD_DATA_ADDR;
          cmd_addr_cnt = 0;
          cmd_data_cnt = 0;
        }
        if(val == 0x5A)
        {
          if(cmd_ctr_step == CMD_CTR_IDLE)
          {
            cmd_ctr_step = CMD_CTR_INIT;
            update_status = UPDATE_DONE;
          }
        }
        break;
      case CMD_DATA_ADDR:
        cmd_data_group_struct.cmd_addr[cmd_addr_cnt] = val;
        cmd_addr_cnt++;
        if(cmd_addr_cnt >= 4)
        {
          cmd_addr_cnt = 0;
          cmd_data_step = CMD_DATA_BUF;
        }
        break;
      case CMD_DATA_BUF:
        cmd_data_group_struct.cmd_buf[cmd_data_cnt] = val;
        cmd_data_cnt++;
        if(cmd_data_cnt >= 2048)
        {
          cmd_data_cnt = 0;
          cmd_data_step = CMD_DATA_CHACK;
        }
        break;
      case CMD_DATA_CHACK:
        checksum = 0;
        for(index = 0;index < 4;index++)
        {
          checksum += cmd_data_group_struct.cmd_addr[index];
        }
        for(index = 0;index < 0x800; index++)
        {
          checksum += cmd_data_group_struct.cmd_buf[index];
        }
        if(checksum == val)
        {
          cmd_data_step = CMD_DATA_DONE;
        }
        else
        {
          cmd_data_step = CMD_DATA_ERR;
        }
        break;
      default:
        break;
    }
  }
  else if(update_status == UPDATE_DONE)
  {
    if(cmd_ctr_step == CMD_CTR_IDLE)
    {
      if(val == 0x5A)
      {
        cmd_ctr_step = CMD_CTR_INIT;
      }
    }
    else if(cmd_ctr_step == CMD_CTR_INIT)
    {
      if(val == 0x02)
      {
        cmd_ctr_step = CMD_CTR_DONE;
      }
      else
      {
        cmd_ctr_step = CMD_CTR_ERR;
      }
    }
  }
}

总的来说,这个函数可以分为三个部分,通过if elseupdate_status进行判断,并划分.

而在判断之前,有一个前提,需要检查接收缓冲区是否有数据,如果有则调用data_take()函数从缓冲区中取出一个字节的数据,否则直接返回不执行任何操作. 在取出一个字节的数据赋值给val后,进入判断

if(update_status == UPDATE_PRE)
  • UPDATE_PRE 状态下,检查命令控制器的状态。如果命令控制器处于空闲状态并且接收到的数据是 0x5A,则将命令控制器状态设置为 CMD_CTR_INIT(初始化状态)。
  • 如果命令控制器处于初始化状态,根据接收到的数据设置命令控制器的状态为 CMD_CTR_DONE(完成状态)、CMD_CTR_APP(应用状态)或 CMD_CTR_ERR(错误状态)。
else if(update_status == UPDATE_ING)

UPDATE_ING 状态下,根据 cmd_data_step(命令数据处理步骤)执行不同的操作。

  • 如果处于空闲状态并且接收到的数据是 0x31,则将 cmd_data_step 设置为 CMD_DATA_ADDR(地址状态)并初始化计数器。
  • 如果接收到的数据是 0x5A 并且命令控制器处于空闲状态,则将命令控制器状态设置为 CMD_CTR_INIT 并将更新状态设置为 UPDATE_DONE
  • 在地址状态下,将接收到的数据存储到命令数据组结构的地址数组中,并更新计数器。当地址数组存满后,将 cmd_data_step 设置为 CMD_DATA_BUF(缓冲区状态)。
  • 在缓冲区状态下,将接收到的数据存储到命令数据组结构的缓冲区数组中,并更新计数器。当缓冲区数组存满后,将 cmd_data_step 设置为 CMD_DATA_CHACK(校验状态)。
  • 在校验状态下,计算地址数组和缓冲区数组的校验和,与接收到的校验值进行比较。如果校验通过,将 cmd_data_step 设置为 CMD_DATA_DONE(完成状态),否则设置为 CMD_DATA_ERR(错误状态)。
else if(update_status == UPDATE_DONE)

UPDATE_DONE 状态下,根据命令控制器的状态执行不同的操作。

  • 如果命令控制器处于空闲状态并且接收到的数据是 0x5A,则将命令控制器状态设置为 CMD_CTR_INIT(初始化状态)。
  • 如果命令控制器处于初始化状态并且接收到的数据是 0x02,则将命令控制器状态设置为 CMD_CTR_DONE(完成状态),否则设置为 CMD_CTR_ERR(错误状态)。
APP升级处理

处理APP升级函数的实现如下所示:

void app_update_handle(void)
{
  uint32_t write_addr=0;
  if(update_status == UPDATE_PRE)
  {
    if(cmd_ctr_step == CMD_CTR_DONE)
    {
      cmd_ctr_step = CMD_CTR_IDLE;
      update_status = UPDATE_CLEAR_FLAG;
    }
    else if(cmd_ctr_step == CMD_CTR_APP)
    {
      cmd_ctr_step = CMD_CTR_IDLE;
      back_ok();
    }
    else if(cmd_ctr_step == CMD_CTR_ERR)
    {
      cmd_ctr_step = CMD_CTR_IDLE;
      back_err();
    }
  }
  else if(update_status == UPDATE_CLEAR_FLAG)
  {
    get_data_from_usart_flag = 1;
    update_status = UPDATE_ING;
    back_ok();
  }
  else if(update_status == UPDATE_ING)
  {
    if(cmd_data_step == CMD_DATA_DONE)
    {
      write_addr = (cmd_data_group_struct.cmd_addr[0] << 24) + (cmd_data_group_struct.cmd_addr[1] << 16) + \
                   (cmd_data_group_struct.cmd_addr[2] << 8) + cmd_data_group_struct.cmd_addr[3];
      if((write_addr >= APP_START_ADDR) && (write_addr < FLASH_BASE + 1024 * FLASH_SIZE))
      {
        flash_2kb_write(write_addr, cmd_data_group_struct.cmd_buf);
        cmd_data_step = CMD_DATA_IDLE;
        back_ok();
      }
      else
      {
        cmd_data_step = CMD_DATA_IDLE;
        back_err();
      }
    }
    else if(cmd_data_step == CMD_DATA_ERR)
    {
      cmd_data_step = CMD_DATA_IDLE;
      back_err();
    }
  }
  else if(update_status == UPDATE_DONE)
  {
    if(cmd_ctr_step == CMD_CTR_DONE)
    {
      cmd_ctr_step = CMD_CTR_IDLE;
      back_ok();
      /* check app starting address whether 0x08xxxxxx */
      if(((*(uint32_t*)(APP_START_ADDR + 4)) & 0xFF000000) == 0x08000000)
      {
        crm_reset();
        /* jump and run in app */
        app_load(APP_START_ADDR);
      }
      else
      {
        cmd_ctr_step = CMD_CTR_ERR;
      }
    }
    else if(cmd_ctr_step == CMD_CTR_ERR)
    {
      cmd_ctr_step = CMD_CTR_IDLE;
      back_err();
    }
  }
}

app_update_handle函数和command_handle函数类似,也是根据update_statuscmd_ctr_step的不同,做出不同的操作. 可以大体上分成四个部分

if(update_status == UPDATE_PRE)

update_statusUPDATE_PRE 时,根据命令控制器的状态执行不同的操作:

  • 如果命令控制器状态为 CMD_CTR_DONE,表示更新完成,将状态置为 CMD_CTR_IDLE,并将 update_status 设置为 UPDATE_CLEAR_FLAG
  • 如果命令控制器状态为 CMD_CTR_APP,表示应用程序更新完成,将状态置为 CMD_CTR_IDLE,并执行 back_ok() 函数。
  • 如果命令控制器状态为 CMD_CTR_ERR,表示更新过程中发生错误,将状态置为 CMD_CTR_IDLE,并执行 back_err() 函数。
else if(update_status == UPDATE_CLEAR_FLAG)

update_statusUPDATE_CLEAR_FLAG 时,表示需要清除更新标志位,将 get_data_from_usart_flag 设置为 1,将 update_status 设置为 UPDATE_ING,并执行 back_ok() 函数。

else if(update_status == UPDATE_ING)

update_statusUPDATE_ING 时,根据数据处理的状态 cmd_data_step 执行不同的操作:

  • 如果数据处理完成 (CMD_DATA_DONE),将接收到的地址和数据写入到Flash中,然后将 cmd_data_step 置为 CMD_DATA_IDLE,并执行 back_ok() 函数。
  • 如果数据处理出错 (CMD_DATA_ERR),将 cmd_data_step 置为 CMD_DATA_IDLE,并执行 back_err() 函数。
else if(update_status == UPDATE_DONE)

update_statusUPDATE_DONE 时,表示更新已经完成:

  • 如果命令控制器状态为 CMD_CTR_DONE,表示更新成功,将命令控制器状态置为 CMD_CTR_IDLE,并执行 back_ok() 函数。此外,还会检查应用程序的起始地址是否为0x08xxxxxx,如果是,则进行软件重启,并跳转到应用程序地址执行,否则将命令控制器状态置为 CMD_CTR_ERR

  • 如果命令控制器状态为 CMD_CTR_ERR,表示更新失败,将命令控制器状态置为 CMD_CTR_IDLE,并执行 back_err() 函数。

上面所介绍的就是官网例程中boot loader程序的所有内容了。为了实现IAP功能,除了boot loader程序,当然还需要有APP程序,下面就对例程中的APP程序进行介绍。

APP程序

例程中的APP程序有两个,分别名为app_led3_toogleapp_led4_toogle. 两个程序的大体内容都是相同的,唯一的不同点就是时钟中断控制的led灯有所不同,因此我这里只介绍app_led3_toogle这个例程.

这个例程的主体函数如下:

int main(void)
{
  /* config vector table offset */
  nvic_vector_table_set(NVIC_VECTTAB_FLASH, 0x4000);

  /* config nvic priority group */
  nvic_priority_group_config(NVIC_PRIORITY_GROUP_4);

  system_clock_config();
  at32_board_init();

  /* check and clear iap_upgrade_flag flag */
  
  iap_init();
  /* init usart used for app update */
  uart_init(115200);
	
  /* init tmr used for show code running state(led cycle toggle) */
  tmr_init();

  while(1)
  {
    iap_command_handle();
  }
}

下面我对APP程序进行详细的介绍。

nvic_vector_table_set(NVIC_VECTTAB_FLASH, 0x4000);这行代码得作用将中断向量表设置一定的偏移量.

并且设置中断优先级. 例程APP中,设置中断优先级为4bits 作为抢占优先级,而0bit作为响应优先级.

设置好中断相关的内容后, 就将系统的时钟配置好.

这里需要注意的是例程APP中的时钟,hex为8000000 Hz, 而pll时钟源乘以的是15.

不同的板子所支持的时钟是不同的,因此,对于不同板子的哪怕实现相同的功能,配置时钟的时候也需要进行一定的修改.

配置好时钟后,就需要将使用到的开发板进行初始化了. 这里初始化就是需要什么就对什么进行初始化,和没有进行IAP功能设置的时候是一样的.

例程中,开发板初始化的详细代码如下:

void at32_board_init()
{
  /* initialize delay function */
  delay_init();

  /* configure led in at_start_board */
	
  at32_led_init(LED2);
  at32_led_init(LED3);
  at32_led_init(LED4);
  at32_led_off(LED2);
  at32_led_off(LED3);
  at32_led_off(LED4);

  /* configure button in at_start board */
  at32_button_init();
}

由于例程中需要使用到的固件库不多,因此就只是一些简单的初始化.

延时初始化,对开发板上的led灯都进行初始化,并且初始化完成后都关上.

然后对开发板上的按钮也进行初始化,这个按钮的具体作用我目前还不清楚,在测试IAP功能的时候也没有使用过,因此就先不去了解了.如果有知道这个按钮是什么用的,可以评论告诉我,非常感谢.

后续的内容,则是关于实现IAP功能,最重要的部分了. 大致上可以分成三个部分. 我分别对此进行介绍.

iap_init();

遇到的问题

如果我将这行代码注释掉,烧录到开发板后,按下reset键,led3会停止闪烁,led2会持续闪烁,说明处于boot loader程序中,而并没有进入APP程序中

具体函数如下:

void iap_init(void)
{
  if((*(uint32_t*)IAP_UPGRADE_FLAG_ADDR) == IAP_UPGRADE_FLAG)
  {
    flash_unlock();
    flash_sector_erase(IAP_UPGRADE_FLAG_ADDR);
    flash_lock();
  }
}

这个函数的作用是,当IAP_UPGRADE_FLAG_ADDRIAP_UPGRADE_FLAG时,会擦除相应的扇区.

和boot loader程序中的

if(flash_upgrade_flag_read() == RESET)
  {
    /* check app starting address whether 0x08xxxxxx */
    if(((*(uint32_t*)(APP_START_ADDR + 4)) & 0xFF000000) == 0x08000000)
			delay_ms(150);
      app_load(APP_START_ADDR);
  }


flag_status flash_upgrade_flag_read(void)
{
  if((*(uint32_t*)IAP_UPGRADE_FLAG_ADDR) == IAP_UPGRADE_FLAG)
    return SET;
  else
    return RESET;
}

相结合起来. 可以看出,当iap_init执行后,flash_upgrade_flag_read函数运行的结果为RESET,因此boot loader程序就会跳转到APP程序中. 因此如果我注释掉这行代码,程序没有将IAP_UPGRADE_FLAG擦除的话,当我按下复位键或者给开发板重新上电的时候,boot loader程序就无法跳转到APP程序中了,因此这一步是十分关键的.

uart_init();

串口发送接收数据的关键,和boot loader中的串口需要对应!

和boot loader程序类似,APP程序中也有相对应的串口初始化函数. 我们在实现IAP功能的时候,boot loader程序和APP程序虽然是两个独立的程序,但是这两个程序是要相互对应的. 由于开发板上面的串口USART1使用的是PA19PA10作为串口,因此不管是boot loader程序还是APP程序在串口初始化的时候都需要设置为PA19PA10.

稍微不同的是中断函数的定义. 虽然在串口初始化的时候,中断触发的条件是一样的:

#define USART_RDBF_INT                   MAKE_VALUE(0x0C,0x05) /*!< usart receive data buffer full interrupt */

nvic_irq_enable(USART1_IRQn, 0, 0);

usart_interrupt_enable(USART1, USART_RDBF_INT, TRUE);

但是中断发生的时候,两个程序所进行的操作确实不同的.

在boot loader程序中,串口的中断服务函数为:

void USART1_IRQHandler(void)
{
  uint16_t reval;
  time_ira_cnt = 0;  /* clear upgrade time out flag */
  if(usart_interrupt_flag_get(USART1, USART_RDBF_FLAG) != RESET)
  {
    reval = usart_data_receive(USART1);
    if(usart_group_struct.count > (USART_REC_LEN - 1))
    {
      usart_group_struct.count = 0;
      usart_group_struct.head = 0;
      usart_group_struct.tail = 0;
    }
    else
    {
      usart_group_struct.count++;
      usart_group_struct.buf[usart_group_struct.head++] = reval;
      if(usart_group_struct.head > (USART_REC_LEN - 1))
      {
        usart_group_struct.head = 0;
      }
    }
  }
}

而APP程序中的串口中断服务函数为:

void USART1_IRQHandler(void)
{
  uint16_t reval;
  if(usart_interrupt_flag_get(USART1, USART_RDBF_FLAG) != RESET)
  {
    reval = usart_data_receive(USART1);
    if((reval == 0x5A) && (iap_flag == IAP_REV_FLAG_NO))
      iap_flag = IAP_REV_FLAG_5A;
    else if((reval == 0xA5) && (iap_flag == IAP_REV_FLAG_5A))
      iap_flag = IAP_REV_FLAG_DONE;
    else
      iap_flag = IAP_REV_FLAG_NO;
  }
}

其中usart_interrupt_flag_get()函数的具体实现为:

flag_status usart_interrupt_flag_get(usart_type* usart_x, uint32_t flag)
{
  flag_status int_status = RESET;

  switch(flag)
  {
    case USART_CTSCF_FLAG:
      int_status = (flag_status)usart_x->ctrl3_bit.ctscfien;
      break;
    case USART_BFF_FLAG:
      int_status = (flag_status)usart_x->ctrl2_bit.bfien;
      break;
    case USART_TDBE_FLAG:
      int_status = (flag_status)usart_x->ctrl1_bit.tdbeien;
      break;
    case USART_TDC_FLAG:
      int_status = (flag_status)usart_x->ctrl1_bit.tdcien;
      break;
    case USART_RDBF_FLAG:
      int_status = (flag_status)usart_x->ctrl1_bit.rdbfien;
      break;
    case USART_ROERR_FLAG:
      int_status = (flag_status)(usart_x->ctrl1_bit.rdbfien || usart_x->ctrl3_bit.errien);
      break;
    case USART_IDLEF_FLAG:
      int_status = (flag_status)usart_x->ctrl1_bit.idleien;
      break;  
    case USART_NERR_FLAG:
    case USART_FERR_FLAG:
      int_status = (flag_status)usart_x->ctrl3_bit.errien;
      break;
    case USART_PERR_FLAG:
      int_status = (flag_status)usart_x->ctrl1_bit.perrien;
      break;      
    default:
      int_status = RESET;
      break;
  }

  if(int_status != SET)
  {
    return RESET;
  }

  if(usart_x->sts & flag)
  {
    return SET;
  }
  else
  {
    return RESET;
  }
}

函数使用的参数为USART1USART_RDBF_FLAG的时候,当串口接收数据缓冲区不满的时候,int_status的值为0,也就是RESET,此时函数就会返回RESET. 当串口接收缓冲区满的时候,int_status的值为1,也就是SET. 此时,通过usart_x->sts & flagusart_sts状态寄存器的第五位进行判断,当值为1时, 返回SET否则返回RESET.

因此,在串口中断服务函数中,只有当串口接收数据缓冲区满的时候,程序才会进入条件语句中,并且从串口数据寄存器中将数据读取出来,保存在reveal中. 然后根据reveal中的值和iap_flag的状态进行判断,对iap_flag做出改变. 这一步是为了后续的iap_command_handle()函数做准备.

iap_command_handle();

这是处理IAP命令的函数,具体代码为:

 while(1)
  {
		iap_command_handle();
  }


void iap_command_handle(void)
{
  if(iap_flag==IAP_REV_FLAG_DONE)
  {
    flash_unlock();
    flash_sector_erase(IAP_UPGRADE_FLAG_ADDR);
    flash_word_program(IAP_UPGRADE_FLAG_ADDR, IAP_UPGRADE_FLAG);
    flash_lock();
    nvic_system_reset();
  }
}

其实就是在while循环中不断的进行检测iap_flag标志的值是否为IAP_REV_FLAG_DONE. 当iap_flagIAP_REV_FLAG_DONE的时候,根据上述串口中断服务函数可知,此时IAP升级过程已经完成 ,则进行如下操作:

  1. 将flash解锁,方便对flash进行擦除和写入
  2. 将flash中IAP_UPGRADE_FLAG_ADDR对应的扇区擦除
  3. 重新对IAP_UPGRADE_FALG_ADDR写入IAP_UPGRADE_FLAG
  4. 将flash再次锁上
  5. 将系统复位,重新从0x08000000开始运行,也就是从boot loader程序开始运行,然后开始对APP程序进行IAP升级处理.

tmr_init();

在串口初始化和iap_command_handle();之间还有一个计时器的初始化,这个初始化的目的其实是为了控制LED灯的闪烁的.

函数具体实现为:

void tmr_init(void)
{
  crm_clocks_freq_type crm_clocks_freq_struct = {0};

  crm_periph_clock_enable(CRM_TMR3_PERIPH_CLOCK, TRUE);

  /* enable the tmr3 global interrupt */
  nvic_irq_enable(TMR3_GLOBAL_IRQn, 1, 0);

  /* get system clock */
  crm_clocks_freq_get(&crm_clocks_freq_struct);

  /* time base configuration */
  tmr_base_init(TMR3, 10000, crm_clocks_freq_struct.ahb_freq / 10000);
  tmr_cnt_dir_set(TMR3, TMR_COUNT_UP);

  /* overflow interrupt enable */
  tmr_interrupt_enable(TMR3, TMR_OVF_INT, TRUE);

  /* enable tmr */
  tmr_counter_enable(TMR3, TRUE);
}

其实就是设置了一个计数器,通过向上加数的方式,当发生溢出的时候就产生中断,然后在中断中进行我们需要的操作,而这个例程APP中所执行的就是控制lED灯的闪烁.

具体的中断服务函数为:

void TMR3_GLOBAL_IRQHandler(void)
{
  if(tmr_interrupt_flag_get(TMR3, TMR_OVF_FLAG) == SET)
  {
    tmr_flag_clear(TMR3, TMR_OVF_FLAG);
    at32_led_toggle(LED3);
  }
}

它所进行的操作为:

  1. 清除溢出标志
  2. 更改指定的LED灯状态(开->关/关->开)

官网上提供的两个例程中,唯一不同的地方其实就是at32_led_toggle( );这个函数的参数,一个是LED 3,一个是LED 4.

理解

以下是我对这两个项目的理解.

  1. 在boot loader和APP程序中,对于时钟的配置一定要和板子相对应, 否则时钟配置错误无法正常进行IAP升级.
  2. 类似的,串口之间的通信也不能出错,对于同一块板子,boot loader和APP程序复用IO口作为串口需要一致,否则会出现串口通信错误。
  3. APP中的iap_init函数是为了擦除IAP升级标志,让程序复位或者重新上电的时候可以直接跳转到APP程序中,因此在APP程序中不能省略. 类似的iap_command_handle函数是为了在APP程序运行的时候,在接收到IAP升级信号的时候能够直接进行IAP升级,而不需要手动复位. 因此iap_command_handle的作用除了设置IAP升级标志,还需要对系统进行复位,从而让程序从boot loader中运行不会跳转到APP程序中. 由于无法知道IAP升级标志什么时候出现,因此在APP程序中,需要使用while循环不断地进行判断,执行iap_command_handle函数. 如果APP程序中有另一个while循环导致iap_command_handle函数没有不断地被执行,那么程序将无法自动跳转到boot loader程序中进行IAP升级.
  4. boot loader程序中的iap_upgrade_handle函数也有类似的,当升级完成后,需要直接跳转到APP程序中,因此将update_status设置为UPGRADE_DONE后,需要进行判断,当程序成功升级后,直接跳转到APP程序中.
  5. 两个程序的串口中断服务函数也是IAP升级的关键, 需要根据规定好的通信协议对串口中断服务函数和相对应的IAP升级函数进行编写.
  6. 在boot loader程序中,对于板子的初始化只需要进行简单的初始化,能够支撑最基本的IAP升级功能就行. 在APP中还需要对板子进行一次初始化,这一步才需要对APP所需要的所有硬件固件进行初始化
  7. boot loader的大小和APP中中断向量表的偏移量相关. 因为boot loader和APP程序,都是存在单片机中的FLASH中的. at32f421系列的单片机的FLASH地址从0x08000000开始,每次程序上电后都是从这里开始运行的. 而最开始存放在FLASH中的程序就是boot loader程序,因此当我们把boot loader程序确定下来后,我们就可以在APP程序中将中断向量表偏移一定的量,让APP程序中的中断能够正确的进入相对应的中断向量表中,执行正确的中断服务函数.
  8. 以上.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值