单片机中,升级固件的方式有很多种,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);
}
而这里则是通过串口向上位机发送0xCC
和0xDD
来实现的. 当上位机接收到这两个信号后,就开始发送正式的升级所需的程序或数据.
计数器初始化
这里的计数器初始化是为了展示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 else
对update_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_status
和cmd_ctr_step
的不同,做出不同的操作. 可以大体上分成四个部分
if(update_status == UPDATE_PRE)
当 update_status
为 UPDATE_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_status
为 UPDATE_CLEAR_FLAG
时,表示需要清除更新标志位,将 get_data_from_usart_flag
设置为 1,将 update_status
设置为 UPDATE_ING
,并执行 back_ok()
函数。
else if(update_status == UPDATE_ING)
当 update_status
为 UPDATE_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_status
为 UPDATE_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_toogle
和app_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_ADDR
为IAP_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
使用的是PA19
和PA10
作为串口,因此不管是boot loader程序还是APP程序在串口初始化的时候都需要设置为PA19
和PA10
.
稍微不同的是中断函数的定义. 虽然在串口初始化的时候,中断触发的条件是一样的:
#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;
}
}
函数使用的参数为USART1
和USART_RDBF_FLAG
的时候,当串口接收数据缓冲区不满的时候,int_status的值为0,也就是RESET
,此时函数就会返回RESET
. 当串口接收缓冲区满的时候,int_status的值为1,也就是SET
. 此时,通过usart_x->sts & flag
对usart_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_flag
为IAP_REV_FLAG_DONE
的时候,根据上述串口中断服务函数可知,此时IAP升级过程已经完成 ,则进行如下操作:
- 将flash解锁,方便对flash进行擦除和写入
- 将flash中
IAP_UPGRADE_FLAG_ADDR
对应的扇区擦除 - 重新对
IAP_UPGRADE_FALG_ADDR
写入IAP_UPGRADE_FLAG
- 将flash再次锁上
- 将系统复位,重新从
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);
}
}
它所进行的操作为:
- 清除溢出标志
- 更改指定的LED灯状态(开->关/关->开)
官网上提供的两个例程中,唯一不同的地方其实就是at32_led_toggle( );
这个函数的参数,一个是LED 3,一个是LED 4.
理解
以下是我对这两个项目的理解.
- 在boot loader和APP程序中,对于时钟的配置一定要和板子相对应, 否则时钟配置错误无法正常进行IAP升级.
- 类似的,串口之间的通信也不能出错,对于同一块板子,boot loader和APP程序复用IO口作为串口需要一致,否则会出现串口通信错误。
- 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升级. - boot loader程序中的
iap_upgrade_handle
函数也有类似的,当升级完成后,需要直接跳转到APP程序中,因此将update_status
设置为UPGRADE_DONE
后,需要进行判断,当程序成功升级后,直接跳转到APP程序中. - 两个程序的串口中断服务函数也是IAP升级的关键, 需要根据规定好的通信协议对串口中断服务函数和相对应的IAP升级函数进行编写.
- 在boot loader程序中,对于板子的初始化只需要进行简单的初始化,能够支撑最基本的IAP升级功能就行. 在APP中还需要对板子进行一次初始化,这一步才需要对APP所需要的所有硬件固件进行初始化
- boot loader的大小和APP中中断向量表的偏移量相关. 因为boot loader和APP程序,都是存在单片机中的FLASH中的. at32f421系列的单片机的FLASH地址从
0x08000000
开始,每次程序上电后都是从这里开始运行的. 而最开始存放在FLASH中的程序就是boot loader程序,因此当我们把boot loader程序确定下来后,我们就可以在APP程序中将中断向量表偏移一定的量,让APP程序中的中断能够正确的进入相对应的中断向量表中,执行正确的中断服务函数. - 以上.