[toc]
IAP(In Application Programming,在应用编程)是用户自己的程序在运行过程中对 User Flash 的部分区域进行烧写。简单来说,就是开发者代码出 bug 了或者添加新功能了,能够利用预留的通讯接口,对代码进行升级。
UART、SPI、IIC、USB 等等,当然还有 WIFI、4G、Bluetooth 等无线通讯手段,都可以作为 IAP 升级的方式,今天主要介绍如何使用串口对固件进行升级。
这里有一点需要特殊注意,就是在 MCU 中,有一个特殊区域被称为 System memory
。在这块区域中存放了 ST 公司自己的 Bootloader 程序,它是在 MCU 出厂时,有 ST 固化到芯片中的,后续不能再更改。其中的 Bootloader 程序也可以对 MCU 进行升级(DFU 对芯片的编程应该就是用的这个 Bootloader)。而且,芯片不同,BootLoader 的功能也是有区别的。ST 官网对于这些也是有详细文档的。下图为部分芯片 BootLoader 版本及功能:
![](1.png)
一、概述
在学习 IAP 之前,最好先了解一下 SMT32 芯片的启动过程,可以参考:STM32 芯片启动过程。
这里再简单说一下,权威指南讲到:芯片复位后首先会从向量表里面取出两个值:
- 从
0x0000 0000
地址取出 MSP(主堆栈寄存器)的值
- 从
0x0000 0004
地址取出 PC(程序计数器)的值
- 然后取出第一条指令执行
不过,STM32 比较特殊,它对地址做了一个重定向(由 MCU 启动配置决定的),一般它是将 0x0000 0000
地址映射到 0x0800 0000
,也就是说:
- 从
0x0800 0000
地址取出 MSP(主堆栈寄存器)的值
- 从
0x0800 0004
地址取出 PC(程序计数器)的值
- 然后取出第一条指令执行
为什么要设置到 0x0800 0000
,而不直接使用 0x0000 0000
?
因为 STM32 不仅可以从内部 Flash 启动,还可以从系统存储器(可以实现串口 ISP,USB DFU 等程序下载方式,这个程序是 ST 固化好的程序代码)和从内部 SRAM 启动,
我们将内部 Flash 安排到 0x0000 0000
显然是不行的。这样会导致系统存储器或者内部 SRAM 无法重映射到 0x0000 0000
了。
二、IAP 实现
为了实现 IAP,整个程序分为两个部分:
- Bootloader:引导程序,接收来自串口的固件包并写入 Flash(擦除和写入) 完成升级
- App:用户程序
两者在 Flash 中的结构如下:
![](2.png)
如下图所示流程中:
- STM32F407 复位后,还是从 0x08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,在运行完复位中断服务程序之后跳转到 IAP 的 main 函数,如图标号①所示;
- 在执行完 IAP 以后(即将新的 APP 代码写入 STM32F407 的 FLASH,灰底部分。新程序的复位中断向量起始地址为
0x08000004+N+M
),跳转至新写入程序的复位向量表,取出新程序的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至新程序的main 函数,如图标号②和③所示,同样 main 函数为一个死循环,并且注意到此时 STM32F407 的 FLASH,在不同位置上,共有两个中断向量表。
在 main 函数执行过程中,如果 CPU 得到一个中断请求:
- PC 指针仍然会强制跳转到地址 0x08000004 中断向量表处,而不是新程序的中断向量表,如图标号④所示;
- 程序再根据我们设置的中断向量表偏移量,跳转到对应中断源新的中断服务程序中,如图标号⑤所示;
- 在执行完中断服务程序后,程序返回 main 函数继续运行,如图标号⑥所示。
![](3.png)
三、IAP 程序
1、串口部分
首先,串口是至关重要的一部分,毕竟数据是通过串口传递过来的。
首先是定义了串口数据接收缓冲区的大小为 120 kb,下面的 UART_RX_BUF_BIN
即数据缓冲区,UART_RX_CNT
记录了 UART_RX_BUF_BIN
数组的大小。
1 2 3 4 5
| #define RX_BUFFER_SIZE 120*1024
extern uint8_t UART_RX_BUF_BIN[RX_BUFFER_SIZE]; extern uint32_t UART_RX_CNT;
|
下面是 USART1 的中断处理函数,当有数据发送过来时,就会执行这段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| uint8_t UART_RX_BUF_BIN[RX_BUFFER_SIZE] __attribute__ ((at(0X20001000))); uint32_t UART_RX_CNT=0;
void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { uint8_t data = USART_ReceiveData(USART1); if (UART_RX_CNT < RX_BUFFER_SIZE) { UART_RX_BUF_BIN[UART_RX_CNT] = data; UART_RX_CNT++; } } }
|
可以看到,这里将接收到的数据放到了 UART_RX_BUF_BIN
缓冲区中,方便后面写入到 Flash 中。注意这里:
1
| __attribute__ ((at(0X20001000)))
|
通过 __attribute__
将缓冲区放到地址 0X20001000
。
2、iap 程序
1 2 3 4 5 6 7
| typedef int (*entry_t)(void); #define FLASH_APP1_ADDR 0x08010000
void bl_iap_load_app(uint32_t appxaddr); void bl_iap_write_app_bin(uint32_t appxaddr,uint8_t *appbuf,uint32_t applen); void MSR_MSP(uint32_t addr);
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
| uint32_t iapbuf[512];
void board_lowlevel_deinit(void) { __disable_irq(); SysTick->CTRL = 0; SysTick->LOAD = 0; SysTick->VAL = 0;
RCC_Deinit(); for (int i = 0; i < 8; ++i) { NVIC->ICER[i] = 0xFFFFFFFF; NVIC->ICPR[i] = 0xFFFFFFFF; }
__enable_irq(); }
void bl_iap_write_app_bin(uint32_t addr, u8 *buf, uint32_t size) { uint32_t t; u16 i = 0; uint32_t fwaddr = addr; for (t = 0; t < size; t += 4) { iapbuf[i++] = (uint32_t)(buf[t + 3] << 24) | (uint32_t)(buf[t +2] << 16) | (uint32_t)(buf[t + 1] << 8) | (uint32_t)(buf[t]); if (i == 512) { i = 0; bl_norflash_write(fwaddr, iapbuf, 512); fwaddr += 2048; } } if (i) { bl_norflash_write(fwaddr, iapbuf, i); } }
void bl_iap_load_app(uint32_t addr) { if ( ( ( *(volatile uint32_t *)addr ) & 0x2FFE0000 ) != 0x20000000 ) { printf("Stack pointer is not valid!\r\n"); return; }
uint32_t _sp = *(volatile uint32_t*)(addr + 0); uint32_t _pc = *(volatile uint32_t*)(addr + 4);
entry_t app_entry = (entry_t)_pc; MSR_MSP(_sp);
board_lowlevel_deinit(); app_entry(); }
__asm void MSR_MSP(uint32_t addr) { MSR MSP, r0; BX r14; }
|
有一点需要注意,在由 IAP 跳转到 APP 时, 一定注意把 IAP 中开启的外设全部关闭(包括 SysTick 中断),否则在刚进入 APP 中时,如果产生中断将导致死机等问题。
3、内部 flash 读写
1 2 3 4 5 6 7
| #define STM32_FLASH_BASE 0x08000000
uint32_t bl_norflash_read_word(uint32_t addr); void bl_norflash_write(uint32_t write_addr,uint32_t *data,uint32_t size); void bl_norflash_read(uint32_t read_addr,uint32_t *data,uint32_t size);
|
内部 flash 的读写操作比较简单。不过,需要注意的是,写操作要注意写之前要保证是没有写过的区域即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
| typedef struct { uint32_t sector; uint32_t size; } sector_desc_t;
static const sector_desc_t sector_descs[] = { {FLASH_Sector_0, 16 * 1024}, {FLASH_Sector_1, 16 * 1024}, {FLASH_Sector_2, 16 * 1024}, {FLASH_Sector_3, 16 * 1024}, {FLASH_Sector_4, 64 * 1024}, {FLASH_Sector_5, 128 * 1024}, {FLASH_Sector_6, 128 * 1024}, {FLASH_Sector_7, 128 * 1024}, {FLASH_Sector_8, 128 * 1024}, {FLASH_Sector_9, 128 * 1024}, {FLASH_Sector_10, 128 * 1024}, {FLASH_Sector_11, 128 * 1024}, };
uint32_t bl_norflash_read_word(uint32_t faddr) { return *(volatile uint32_t *)faddr; }
static uint16_t bl_norflash_get_flash_sector(uint32_t addr) { uint32_t address = STM32_FLASH_BASE; for (uint16_t sector = 0; sector < sizeof(sector_descs) / sizeof(sector_desc_t); ++sector) { if (addr < address + sector_descs[sector].size) { return sector_descs[sector].sector; } address += sector_descs[sector].size; }
printf("Flash sector not found!"); return FLASH_Sector_11; }
void bl_norflash_write(uint32_t write_addr, uint32_t *data, uint32_t size) { if (write_addr < STM32_FLASH_BASE || write_addr % 4) { printf("Please check the WriteAddr!"); return; }
FLASH_Status status = FLASH_COMPLETE; uint32_t addr_begin = 0; uint32_t addr_end = 0; FLASH_Unlock(); FLASH_DataCacheCmd(DISABLE);
addr_begin = write_addr; addr_end = write_addr + size * 4; if (addr_begin < 0X1FFF0000) { while (addr_begin < addr_end) { if (bl_norflash_read_word(addr_begin) != 0XFFFFFFFF) { status = FLASH_EraseSector(bl_norflash_get_flash_sector(addr_begin), VoltageRange_3); if (status != FLASH_COMPLETE) { printf("Flash erase error!"); break; } } else addr_begin += 4; } }
if (status == FLASH_COMPLETE) { while (write_addr < addr_end) { if (FLASH_ProgramWord(write_addr, *data) != FLASH_COMPLETE) { printf("Flash write error!"); break; } write_addr += 4; data++; } } FLASH_DataCacheCmd(ENABLE); FLASH_Lock(); }
void bl_norflash_read(uint32_t read_addr, uint32_t *data, uint32_t size) { if (read_addr < STM32_FLASH_BASE || data == NULL || size == 0) { printf("Please check the ReadAddr or the size!"); return; }
uint32_t i; for (i = 0; i < size; i++) { data[i] = bl_norflash_read_word(read_addr); read_addr += 4; } }
|
4、main 程序
下面是 main 函数逻辑:
- Bootloader 等待 10s
- 10s 内如果没有通过串口发送 “yes”,则自动引导进入用户程序
- 如果发送了 ”yes“,则会等待用户发送新的固件
- 等待固件发送完成后,先判断改固件地址信息是否准确
- 正确则继续执行将其写入 Flash
- 最后进入用户程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
| uint32_t time = 0; uint32_t oldcount = 0; uint32_t applenth = 0; uint8_t start_flag = 0;
start_printf();
while (1) { if (UART_RX_CNT && start_flag == 0) { if (UART_RX_BUF_BIN[0] == 'y' && UART_RX_BUF_BIN[1] == 'e' && UART_RX_BUF_BIN[2] == 's') { start_flag = 1; printf("请发送更新固件包\r\n"); }
UART_RX_CNT = 0; }
if (UART_RX_CNT && start_flag == 1) { if (oldcount == UART_RX_CNT) { applenth = UART_RX_CNT; oldcount = 0; UART_RX_CNT = 0; printf("用户程序接收完成!\r\n"); printf("程序包长度: %d Bytes\r\n", applenth);
if (applenth) { printf("开始更新固件包......\r\n"); if ( ( ( *(__IO uint32_t *)(0X20001000 + 4) ) & 0xFF000000 ) == 0x08000000 ) { bl_iap_write_app_bin(FLASH_APP1_ADDR, UART_RX_BUF_BIN, applenth); printf("地址为(0X20001000 + 4): %X\r\n", *(__IO uint32_t *)(0X20001000 + 4)); printf("固件包更新完成\r\n"); } else { printf("地址错误: %X!!!\r\n", *(__IO uint32_t *)(0X20001000 + 4)); } }
if ( ( ( *(__IO uint32_t *)(FLASH_APP1_ADDR + 4) ) & 0xFF000000 ) == 0x08000000 ) { printf("开始执行Flash应用程序\r\n"); bl_iap_load_app(FLASH_APP1_ADDR); } else { printf("地址错误: %X\r\n!!!", *(__IO uint32_t *)(FLASH_APP1_ADDR + 4)); } } else oldcount = UART_RX_CNT; }
time++; bl_delay_ms(10);
if (time % 100 == 0 && start_flag == 0) printf("倒计时 %d s......\r\n", 11 - time / 100);
if (time >= 1000) time = 1000; if (time == 1000 && start_flag == 0) { printf("开始运行应用程序\r\n"); if ( ( ( *(__IO uint32_t *)(FLASH_APP1_ADDR + 4) ) & 0xFF000000 ) == 0x08000000 ) { printf("开始执行FLASH应用程序\r\n"); bl_iap_load_app(FLASH_APP1_ADDR); } else { printf("地址错误!!!\r\n"); } } }
|
逻辑比较简单,就不多说了。
下面写一个用户程序来验证一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| int main(void) { NVIC_SetPriorityGrouping(NVIC_PriorityGroup_4); NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x10000); RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOE, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); bl_led_init(); bl_uart_init(); bl_tim4_init();
printf("\r\n"); TIM4_Init(1000 - 1, 8400 - 1);
return 0; }
void TIM4_IRQHandler(void) { if(TIM_GetITStatus(TIM4, TIM_IT_Update) == SET) { bl_led_toggle(GPIO_Pin_5); TIM_ClearITPendingBit(TIM4, TIM_IT_Update); } printf("timer 4\r\n"); }
|
一定要注意这段代码:
1
| NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x10000);
|
这里设置了用户程序的中断向量表的偏移地址为 0x10000,如果不设置这个偏移地址,就无法进入定时器4 中断服务函数,LED 就不会闪烁。
另外,同时还要注意设置好用户程序的地址。
![](4.png)