- /* FLASH起始地址 */
- #define FLASH_START_ADDR (FMC_BASE)
- /* FLASH总大小 */
- #define FLASH_TOTAL_SIZE ((uint32_t)0x20000) /* 128KB */
- /* APP大小 */
- #define APP_TOTAL_SIZE ((uint32_t)1024 * 50) /* 50KB */
- /* 参数存储大小 */
- #define PARAM_SIZE ((uint32_t)1024 * 2) /* 2KB */
- /* 版本号偏移位置: 软件版本号 - 位于存储的前第1KB处 */
- #define VERSION_OFFSET ((uint32_t)0x400) /* 1KB */
- /* BOOT起始地址 */
- #define BOOT_START_ADDR FLASH_START_ADDR
- /* BOOT大小 */
- #define BOOT_TOTAL_SIZE ((uint32_t)0xC00) /* 3KB */
- /* 运行APP起始地址 */
- #define APP_RUN_START_ADDR (BOOT_START_ADDR + BOOT_TOTAL_SIZE)
- /* 运行APP大小 */
- #define APP_RUN_TOTAL_SIZE APP_TOTAL_SIZE /* 50KB */
- /* 升级APP起始地址 */
- #define APP_IAP_START_ADDR (APP_RUN_START_ADDR + APP_RUN_TOTAL_SIZE)
- /* 升级APP大小 */
- #define APP_IAP_TOTAL_SIZE APP_TOTAL_SIZE /* 50KB */
- /* 参数存储区1起始地址 */
- #define PARAM1_START_ADDR (APP_IAP_START_ADDR + APP_IAP_TOTAL_SIZE) /* 2KB */
- #define PARAM1_TOTAL_SIZE PARAM_SIZE
- /* 参数存储区2起始地址 */
- #define PARAM2_START_ADDR (PARAM1_START_ADDR + PARAM1_TOTAL_SIZE) /* 2KB */
- #define PARAM2_TOTAL_SIZE PARAM_SIZE
Boot的main.c代码如下:
指定Boot的软件版本位于自身区域的前第1KB处,目的是方便App区程序运行过程中能随时准确获取得到Boot的版本。
同时,程序开始运行时,需指定中断向量表地址为Boot区起始地址(程序的开头都是中断向量表)。
- // 软件版本号 - 位于存储的前第1KB处
- static const char software_version[10] __attribute__((at(BOOT_START_ADDR + VERSION_OFFSET))) = {
- 'B', 'O', 'O', 'T', '_', '0', '1', '.', '1', '0'
- };
- /*!
- * @brief Main program
- *
- * @param None
- *
- * @retval None
- *
- */
- int main(void)
- {
- /* 设置中断向量表地址 */
- SCB->VTOR = BOOT_START_ADDR;
-
- bsp_init();
- app_init();
-
- while (1) {
- app_task();
- }
- }
Boot每次上电都检查升级完成标志,此标志为固定的字符串“IAP COMPLETE”,当标志匹配,则执行后续的App擦除更新流程。
App的擦除和拷贝都尝试两次,当完成流程后,只需要把开头的扇区擦除即可,因为判断是否需要升级的所有标志和数据,都在区域的起始扇区内。
检查升级包的过程中,会读取IAP区的所有数据,并计算CRC_8校验,此操作是为了确保IAP区内的程序数据是有效未被非法修改的,否则异常数据拷贝到App运行区使用可能会产生非预期的异常。
- // 应用初始化
- void app_init(void)
- {
- app_iap_check_and_copy();
- app_iap_jump_to_run_app();
- }
- /*
- * @brief 检查IAP升级程序并拷贝
- *
- * @param None
- *
- * @retval None
- *
- */
- void app_iap_check_and_copy(void)
- {
- uint8_t *iap_complete_f = (uint8_t *)APP_IAP_START_ADDR;
- uint32_t app_size = 0;
- uint8_t crc = 0;
-
- /* 检查升级完成标志 */
- if (memcmp(IAP_COMPLETE_FLAG, iap_complete_f, sizeof(IAP_COMPLETE_FLAG)) != 0) {
- return;
- }
- /* 检查升级包 */
- app_size = *(uint32_t *)(APP_IAP_START_ADDR + sizeof(IAP_COMPLETE_FLAG));
- crc = *(uint8_t *)(APP_IAP_START_ADDR + IAP_APP_OFFSET + app_size - 1);
- if (crc != crc8_calc(0, (uint8_t *)(APP_IAP_START_ADDR + IAP_APP_OFFSET), (app_size - 1))) {
- return ;
- }
- /* 擦除运行APP */
- if (bsp_flash_erase(APP_RUN_START_ADDR, APP_RUN_TOTAL_SIZE) < APP_RUN_TOTAL_SIZE) {
- if (bsp_flash_erase(APP_RUN_START_ADDR, APP_RUN_TOTAL_SIZE) < APP_RUN_TOTAL_SIZE) {
- return ;
- }
- }
- /* 拷贝升级包到运行APP区 */
- if (bsp_flash_write(APP_RUN_START_ADDR,
- (uint8_t *)(APP_IAP_START_ADDR + IAP_APP_OFFSET), app_size) < app_size) {
- if (bsp_flash_write(APP_RUN_START_ADDR,
- (uint8_t *)(APP_IAP_START_ADDR + IAP_APP_OFFSET), app_size) < app_size) {
- /* 异常则擦除头部 */
- if (bsp_flash_erase(APP_RUN_START_ADDR, IAP_APP_OFFSET) < IAP_APP_OFFSET) {
- if (bsp_flash_erase(APP_RUN_START_ADDR, IAP_APP_OFFSET) < IAP_APP_OFFSET) {
- return ;
- }
- }
- return ;
- }
- }
- /* 擦除IAP程序头部1页大小 */
- if (bsp_flash_erase(APP_IAP_START_ADDR, IAP_APP_OFFSET) < IAP_APP_OFFSET) {
- if (bsp_flash_erase(APP_IAP_START_ADDR, IAP_APP_OFFSET) < IAP_APP_OFFSET) {
- return ;
- }
- }
- }
跳转App运行前,需要检查程序开始的4个字节栈地址是否在合法的SRAM地址范围内,编译产生的bin,起始4个字节是栈地址。
因为system.c文件已经将MCU的主频配置为PLL倍频的时钟,不是使用MCU默认上电的HSI时钟,所以跳转前改为HSI时钟,让App开始运行时,如同刚从复位上电时运行一样。
- /*
- * @brief 检查程序堆栈
- *
- * @param app_addr: 程序地址
- *
- * @retval 0-成功, 其他-失败
- *
- */
- static int8_t iap_app_sp_check(uint32_t app_addr)
- {
- memcpy((void *)&vec_tab, (uint32_t *)(app_addr), sizeof(struct arm_vector_table));
- /* 指针需在SRAM的起始范围内 */
- if (((vec_tab.msp) & 0xFFFE0000) != SRAM_BASE) {
- return -1;
- }
- return 0;
- }
- /*
- * @brief 跳转运行APP
- *
- * @param None
- *
- * @retval None
- *
- */
- void app_iap_jump_to_run_app(void)
- {
- /* 检查新的运行程序 */
- if (iap_app_sp_check(APP_RUN_START_ADDR) != 0) {
- return ;
- }
- /* 恢复默认HSI运行并清理环境 */
- if (RCM_ReadSYSCLKFreq() != HSI_VALUE) {
- RCM_EnableHSI();
- while (RCM_ReadStatusFlag(RCM_FLAG_HSIRDY) != SET);
- RCM_ConfigSYSCLK(RCM_SYSCLK_SEL_HSI);
- while (RCM_ReadSYSCLKSource() != RCM_SYSCLK_SEL_HSI);
- }
- app_iap_clear();
- jump_to_run_app = (p_app_func)vec_tab.reset;;
- /* 必须先设置特权模式再设置主堆栈指针,否则无法跳转至 APP */
- __set_CONTROL(0); /* 在RTOS工程,这条语句很重要,设置为特权级模式,使用MSP指针 */
- __set_MSP(vec_tab.msp); /* 设置主堆栈指针 */
- __ISB(); /* 强制 CPU 重新取指,确保前面的配置立刻生效 */
- /* 跳转运行APP */
- jump_to_run_app();
- while (1);
- }
Boot区的Flash位于MCU的起始位置,即0x08000000,大小为3KB(0xC00),SRAM使用完整的空间即可。
App程序的main.c开始运行程序时,需重新设置中断向量表地址,因为Boot程序已经将中断向量表改为了Boot区,这里修改为App区,后续产生的中断,都使用App区内的中断服务函数。
- /*!
- * @brief Main program
- *
- * @param None
- *
- * @retval None
- *
- */
- int main(void)
- {
- /* 设置中断向量表地址 */
- SCB->VTOR = APP_RUN_START_ADDR;
-
- bsp_init();
- app_init();
-
- while (1) {
- app_task();
- }
- }
App程序的后续数据处理,都是基于串口的通讯数据解析。
- // 外设初始化
- void bsp_init(void)
- {
- bsp_uart_init();
- }
- // 应用初始化
- void app_init(void)
- {
- }
- // 应用任务
- void app_task(void)
- {
- app_comm_parse();
- }
此处自定义了一份通讯协议,方便MCU与电脑上位机进行交互,传输数据:
通讯协议的封装代码,请查阅源码。如下是通讯数据解析:
获取App版本和Boot版本,都是从固定的Flash地址处取数据即可;
升级请求和升级包则调用对应的IAP接口函数进行处理;
特别注意:当IAP数据包发送完成并且处理正确后,MCU需要自动重启已运行最新版的App程序,而在自动重启前,需确保串口数据完成发送,否则上位机接收不到完整的应答数据,不确定MCU的升级状态。
- /* 请求升级 */
- #define CMD_UPDATE_REQUEST (0xA1)
- /* 升级包 */
- #define CMD_UPDATE_PACKET (0xA2)
- /* 获取APP版本 */
- #define CMD_APP_VERSION (0xB1)
- /* 获取BOOT版本 */
- #define CMD_BOOT_VERSION (0xB2)
- /*
- * @brief 通讯解析
- *
- * @param None
- *
- * @retval None
- *
- */
- void app_comm_parse(void)
- {
- uint16_t rx_len = 0;
- uint16_t send_len = 0;
- uint32_t app_size = 0;
- uint32_t data_offset = 0;
- uint32_t version_addr = 0;
-
- /* 检查通讯 */
- if (bsp_uart_is_rx_complete() != 0) {
- rx_len = bsp_uart_get_rx_len();
- bsp_uart_reset_recv();
- }
- /* 解析通讯数据 */
- if ((rx_len != 0) && (comm_pro_parse(&comm_pro, bsp_uart_get_rx_buf(), rx_len) != 0)) {
- switch (comm_pro.cmd) {
- /* 请求升级 */
- case CMD_UPDATE_REQUEST:
- if (comm_pro.len == (MIN_FRAME_LEN + 4)) {
- memcpy(&app_size, comm_pro.data, 4);
- respond_data[0] = (app_iap_request(app_size) == 0) ? 0 : 0xFF;
- send_len = comm_pro_frame_make(&comm_pro, respond_data, 1, send_buf);
- }
- break;
-
- /* 升级包 */
- case CMD_UPDATE_PACKET:
- if (comm_pro.len > (MIN_FRAME_LEN + 4)) {
- memcpy(&data_offset, comm_pro.data, 4);
- respond_data[0] = (app_iap_updating(data_offset, &comm_pro.data[4], (comm_pro.len - (MIN_FRAME_LEN + 4))) >= 0) ? 0 : 0xFF;
- send_len = comm_pro_frame_make(&comm_pro, respond_data, 1, send_buf);
- }
- break;
-
- /* 获取APP版本 */
- case CMD_APP_VERSION:
- version_addr = APP_RUN_START_ADDR + VERSION_OFFSET + 5;
- respond_data[0] = (*(uint8_t *)version_addr) - '0';
- respond_data[1] = (*(uint8_t *)(version_addr + 1)) - '0';
- respond_data[2] = (*(uint8_t *)(version_addr + 3)) - '0';
- respond_data[3] = (*(uint8_t *)(version_addr + 4)) - '0';
- send_len = comm_pro_frame_make(&comm_pro, respond_data, 4, send_buf);
- break;
-
- /* 获取BOOT版本 */
- case CMD_BOOT_VERSION:
- version_addr = BOOT_START_ADDR + VERSION_OFFSET + 5;
- respond_data[0] = (*(uint8_t *)version_addr) - '0';
- respond_data[1] = (*(uint8_t *)(version_addr + 1)) - '0';
- respond_data[2] = (*(uint8_t *)(version_addr + 3)) - '0';
- respond_data[3] = (*(uint8_t *)(version_addr + 4)) - '0';
- send_len = comm_pro_frame_make(&comm_pro, respond_data, 4, send_buf);
- break;
-
- default:
- break;
- }
-
- /* 发送数据 */
- if (send_len != 0) {
- bsp_uart_send(send_buf, send_len);
- /* 最后一包升级数据发送完成,则等到通讯结束重启 */
- if (app_iap_status() == IAP_WAIT_FOR_RESET) {
- while (bsp_uart_is_tx_complete() == 0);
- app_iap_reset_to_update();
- }
- }
- }
- }
App程序的iap.c文件内,也有一个App版本:
- /* 软件版本号 - 位于存储的前第1KB处 */
- static const char software_version[10] __attribute__((at(APP_RUN_START_ADDR + VERSION_OFFSET))) = {
- 'A', 'P', 'P', '_', '_', '0', '1', '.', '1', '2'
- };
App的这个版本定义还有一个好处是,可以使用J-Flash打开编译后的bin文件看得到程序内部的版本信息,文件名可能存在改错名的情况,但bin文件的内容可不好随意乱改。
申请升级函数,需要检查升级包的大小是否超过APP允许的总大小,没问题则一次性擦除需要的全部Flash空间。
多擦除IAP_APP_OFFSET大小,是因为这段区域要用作程序大小和升级完成标志存储的地方,后面紧接才是真正的bin文件数据。
- /*
- * @brief 申请升级
- *
- * @param app_size: bin文件大小
- *
- * @retval 0-成功, 其他-失败
- *
- */
- int8_t app_iap_request(uint32_t app_size)
- {
- /* 升级包大小不合理 */
- if (app_size > (APP_TOTAL_SIZE - IAP_APP_OFFSET)) {
- return -1;
- }
- /* 擦除Flash: 一次擦完全部所需 */
- if (bsp_flash_erase(APP_IAP_START_ADDR, (app_size + IAP_APP_OFFSET)) < (app_size + IAP_APP_OFFSET)) {
- return -1;
- }
- iap.app_size = app_size;
- iap.app_data_offset = 0;
- iap.iap_status = IAP_UPDATING;
- iap.app_crc = 0;
- return 0;
- }
升级包函数,需检查bin文件的数据是连续的,同时不能丢包或超大小;因为申请升级时,已经把需要的所有Flash空间都擦除干净,所以此处只管写入数据即可。
写入的数据,再读取出来,计算循环的CRC_8校验码,bin文件的最后一个字节数据为使用bin文件全部数据计算的CRC_8校验码,当全部bin文件数据都传输并写入Flash程序后,需比对CRC_8是否一致,以确保bin文件数据无误才能使用。
一切都正确后,最后写入升级完成标志字符串和bin文件的总大小,Boot程序需要这两个数据执行App程序的更新流程。
- /*
- * @brief 升级
- *
- * @param data_offset: 数据偏移
- * data: 数据
- * data_len: 数据大小
- *
- * @retval 0-成功, 其他-失败
- *
- */
- int8_t app_iap_updating(uint32_t data_offset, uint8_t *data, uint16_t data_len)
- {
- uint32_t addr = 0;
- uint8_t crc = 0;
- if (iap.iap_status != IAP_UPDATING) {
- return -1;
- }
- /* 检查入参 */
- if ((data == ((void *)0)) || (data_len < 1)) {
- return -1;
- }
- /* 超大小 */
- if ((data_offset + data_len) > iap.app_size) {
- return -2;
- }
- /* 丢包则失败 */
- if (iap.app_data_offset < data_offset) {
- return -3;
- }
- /* 重发不处理 */
- if (iap.app_data_offset > data_offset) {
- return 1;
- }
- /* 写Flash */
- addr = APP_IAP_START_ADDR + IAP_APP_OFFSET + iap.app_data_offset;
- if (bsp_flash_write(addr, data, data_len) < data_len) {
- return -5;
- }
- iap.app_data_offset = iap.app_data_offset + data_len;
- /* 升级完成 */
- if (iap.app_data_offset == iap.app_size) {
- /* 最后1字节为CRC8 */
- iap.app_crc = crc8_calc(iap.app_crc, (uint8_t *)addr, (data_len - 1));
- crc = data[data_len - 1];
- if (iap.app_crc != crc) {
- return -6;
- }
- /* 写升级完成标志 */
- if (bsp_flash_write(APP_IAP_START_ADDR,
- (uint8_t *)IAP_COMPLETE_FLAG, sizeof(IAP_COMPLETE_FLAG)) < sizeof(IAP_COMPLETE_FLAG)) {
- return -7;
- }
- /* 写数据包大小 */
- if (bsp_flash_write((APP_IAP_START_ADDR + sizeof(IAP_COMPLETE_FLAG)),
- (uint8_t *)&iap.app_size, 4) < 4) {
- return -8;
- }
- iap.iap_status = IAP_WAIT_FOR_RESET;
- /* 复位升级 */
- // app_iap_reset_to_update();
- } else {
- /* 回读Flash 累计CRC */
- iap.app_crc = crc8_calc(iap.app_crc, (uint8_t *)addr, data_len);
- }
- return 0;
- }
当升级包传输完成,并且串口应答上位机的报文也都发送完成后,则需要复位程序。复位程序前依然执行恢复为HSI默认时钟的操作,同时清理代码把当前App程序开启的所有外设都关闭,以恢复一个干净的MCU环境。
注意:执行NVIC_SystemReset软复位前,需屏蔽中断,否则复位过程会有可能被打断而不产生复位。
- /*
- * @brief 清理环境
- *
- * @param None
- *
- * @retval None
- *
- */
- static void app_iap_clear(void)
- {
- /* 清理的目的是恢复各个外设为初始MCU复位状态,
- 防止跳转程序过程中产生中断 */
- /* 当前程序初始化了多少外设,则清理多少外设 */
- bsp_uart_deinit();
- }
- /*
- * @brief 复位重启
- *
- * @param None
- *
- * @retval None
- *
- */
- void app_iap_reset_to_update(void)
- {
- /* 恢复默认HSI时钟 */
- if (RCM_ReadSYSCLKFreq() != HSI_VALUE) {
- RCM_EnableHSI();
- while (RCM_ReadStatusFlag(RCM_FLAG_HSIRDY) != SET);
- RCM_ConfigSYSCLK(RCM_SYSCLK_SEL_HSI);
- while (RCM_ReadSYSCLKSource() != RCM_SYSCLK_SEL_HSI);
- }
- /* 清理环境 */
- app_iap_clear();
- /* 复位前需屏蔽中断,否则复位过程会有可能被打断 */
- __disable_irq();
- /* 复位 */
- NVIC_SystemReset();
- }
App区的Flash从Boot区后开始即0x08000C00,大小为50KB(0xC800),SRAM使用完整的空间即可。
同时User界面需要加入生成bin文件指令和在bin文件末尾添加CRC_8校验码的脚本命令:
fromelf.exe --bin --output ..\bin\app.bin !L
..\bin\CRC8_Add.exe ..\bin\app.bin
生成的bin文件放入bin/文件夹内,同时CRC8_Add.exe也在此文件夹内。
4. 测试
更改App的版本号,编译2个版本测试:
打开上位机软件进行测试:
获取到当前的Boot和App版本如下:
选择Bin文件进行升级后,再次读取App版本:
升级程序成功。
5. 移植说明
CRC8_Add和上位机代码均使用QT进行开发,可使用QT重新定制,更改的部分如果涉及MCU端的处理,则也需要同步更改到MCU端。
6. 详细代码
因单个文件大小限制,需要把opengl32sw文件解压并放到Host_PC文件夹内的realse文件夹内才是完整的上位机运行。
CRC8_Add.zip
(14.56 MB)
Host_PC.zip
(16.07 MB)
opengl32sw.zip
(5.66 MB)