赞
踩
这个应该是最基本的方法,只要自己写过程序的应该都会,将编译生成的hex
文件使用ST-Link工具或者J-Link工具直接下载进 Flash 即可。Keil中点击下载也能一键下载。
下载时可以看到地址是从0x0800 0000,即 Flash 的起始地址开始下载的。
优点:简单,插上下载器直接下载即可。
缺点:在产品中嵌入式板卡封装起来之后,因为下载口没有实际功能,所以很多时候下不拆机是没办法插上下载器的。这时候就不方便。
简单补充一句,bin文件和hex文件的区别:
我们常见的一键下载电路就是用的这种方式。这个是利用了 STM32 自带的 Bootloader 升级程序。
在用户参考手册中,可以看到下表,关于启动模式设置的。
中文版的如下:
程序运行时的启动过程:
一般在我们的程序运行的时候,BOOT0 是接地的,也就是BOOT0 = 0
。也就是程序是从主存储器Flash开始启动的,启动的地址为0x08000000
。
使用ISP下载时的启动过程:
BOOT0 = 1
、BOOT1 = 0
。BOOT0 = 0
,然后复位引脚拉低,程序复位重启,从 Flash 中开始运行程序。可以这么理解:芯片出厂时,系统存储器中已经存储了一段程序,这段程序的功能是将串口1(固定的)收到的数据,放到主闪存存储器(Flash)中,从0x0800 0000地址处开始。
这样也就完成了我们的一键下载过程。
比如,正点原子的一键下载电路如下,左侧的三极管就是用来完成 BOOT引脚和复位引脚的操作的。
具体的硬件引脚电平如何变化百度搜一下,分析应该挺多的。我也没详细看过。
沁恒官网也有专门的一键下载芯片,原理上其实一样的。
链接:https://www.wch.cn/application/575.html
ISP的下载方式:
优点:提供了一种升级方式,无需代码支持。
缺点:需要相应的硬件支持,成本增加;使用的接口也是固定的,并且很多时候串口可能用于其他功能了已经。
IAP 和 ISP 其实基本上是一样的。
但是:ISP 是由厂商已经提供好的,因此接口固定;IAP可以自定义使用任何接口接收应用程序。也正是因为这一点,使得用户可以用多种不同的方式进行升级。
在看IAP之前,要先看一下正常情况下,程序从Flash启动时的启动流程,如下图所示:
接下来看下有了IAP之后的启动流程,如下图所示,下图中可以看到,在Flash中存储了 两“套” 程序(套的意思是,不仅只有用户程序,配套的中断向量等也都有)。
其中第一套为:Bootloader程序,该程序的功能是,接收某个接口的数据,并把这些数据存储到Flash中。
这些数据其实就是后面的一套APP程序,这套应用程序可以通过Bootlaoder程序保存到Flash中,这样就不用再使用专门的下载器。
第二套才是我们真正的应用程序。
到这,我们就知道Bootloader程序是干啥的了。
这个分析过程,是2.1小节中的方式一。其他方式跟这种原理上基本是一样一样的。
对比:
ISP | IAP |
---|---|
在系统存储器中存储了一套接收串口1数据的程序 | IAP是将Flash分成了两份,在第一份中存储了一套接收某个接口的程序 |
使用硬件BOOT引脚设置进行跳转 | 使用软件(直接修改PC指针)进行跳转 |
仅能使用芯片厂商设置好的接口(串口1) | 用户自定义,理论上只要能接收数据的接口都可以用 |
开发板:STM32F401CCU6最小系统板(淘宝十几块钱),Flash大小:256KB,RAM大小:64KB。
TODO:这里记录一下,我看到的或者想到的所有形式。每实现一个会过来贴代码地址。
代码汇总地址:https://gitee.com/HzoZi/bootloader
这应该是最常见、也是最简单的一种了。也就是上面1.3.2小节的分析的情况。
需要实现四个部分:
第一步:新建工程
调试口勾上,时钟设置最大,设置生成单独的.c文件。
3. 设置一个按键
生成工程即可。
第二步:配置串口,实现串口接收功能
我这里放到了 usart.c 的最后。
// 需要调用stdio.h文件 #include <stdio.h> // 取消ARM的半主机工作模式 #pragma import(__use_no_semihosting) struct __FILE { int handle; }; FILE __stdout; void _sys_exit(int x) // 定义_sys_exit()以避免使用半主机模式 { x = x; } int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff); return ch; }
我这里放到了 main.c 的上面。
uint8_t single_buff[1]; // 按字节保存APP程序 uint8_t app_buff[40 * 1024] = {0}; // 保存接收到的APP程序(最大40K) volatile uint32_t app_buff_len = 0; // APP程序的长度(字节) void start_uart_rx(void) { while(HAL_UART_Receive_IT(&huart1, single_buff, sizeof(single_buff)) != HAL_OK); } void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if(huart->Instance == USART1) { // 把接收到的数据,放到缓冲区中 app_buff[app_buff_len] = single_buff[0]; app_buff_len++; } // 重新开启中断接收 start_uart_rx(); }
第三步:IAP实现
其中,STM32Flash读写部分 其实有现成的代码可以用。使用 RT_Thread Studio 创建一个一样芯片的工程就可以直接抄了。
我们新建一个文件,存放 IAP 实现的相关代码。
注意,这里划分Flash的时候,尤其注意App程序起始地址,根据芯片不同起始地址倍数关系也不同
#ifndef __IAP_H #define __IAP_H #include "stm32f4xx_hal.h" #include "stdint.h" /* 以下宏定义名字尽量不要随便换 */ /* Flash定义,根据使用的芯片修改 */ #define ROM_START ((uint32_t)0x08000000) #define ROM_SIZE (256 * 1024) #define ROM_END ((uint32_t)(ROM_START + ROM_SIZE)) #define STM32_FLASH_START_ADRESS ROM_START #define STM32_FLASH_SIZE ROM_SIZE #define STM32_FLASH_END_ADDRESS ROM_END /* Flash扇区定义,STM32F401CCU6:256KB,根据使用的芯片修改 */ #define ADDR_FLASH_SECTOR_0 ((uint32_t)0x08000000) /* Base @ of Sector 0, 16 Kbytes */ #define ADDR_FLASH_SECTOR_1 ((uint32_t)0x08004000) /* Base @ of Sector 1, 16 Kbytes */ #define ADDR_FLASH_SECTOR_2 ((uint32_t)0x08008000) /* Base @ of Sector 2, 16 Kbytes */ #define ADDR_FLASH_SECTOR_3 ((uint32_t)0x0800C000) /* Base @ of Sector 3, 16 Kbytes */ #define ADDR_FLASH_SECTOR_4 ((uint32_t)0x08010000) /* Base @ of Sector 4, 64 Kbytes */ #define ADDR_FLASH_SECTOR_5 ((uint32_t)0x08020000) /* Base @ of Sector 5, 128 Kbytes */ /* Bootloader、APP 分区定义,根据个人需求修改 */ #define BOOT_START_ADDR 0x08000000 // FLASH_START_ADDR #define BOOT_FLASH_SIZE 0x4000 // 16K #define APP_START_ADDR 0x08004000 // BOOT_START_ADDR + BOOT_FLASH_SIZE #define APP_FLASH_SIZE 0x3C000 // 240K /* 对外接口 */ void show_boot_info(void); uint8_t jump_app(uint32_t app_addr); /* 对外接口 */ int stm32_flash_read(uint32_t addr, uint8_t *buf, size_t size); int stm32_flash_write(uint32_t addr, const uint8_t *buf, size_t size); int stm32_flash_erase(uint32_t addr, size_t size); #endif /* __IAP_H */
我这里直接从RT-Thread的程序中抄的,要注意,不同系列芯片的读写函数略有差异,主要就是地址差异,App起始地址只要没问题,这里可以不用考虑,知道这回事就行。
抄过来之后如下图所示
#include "iap.h" #include "stdio.h" void show_boot_info(void) { printf("---------- Enter BootLoader ----------\r\n"); printf("\r\n"); printf("======== flash pration table =========\r\n"); printf("| name | offset | size |\r\n"); printf("--------------------------------------\r\n"); printf("| boot | 0x%08X | 0x%08X |\r\n", BOOT_START_ADDR, BOOT_FLASH_SIZE); printf("| app | 0x%08X | 0x%08X |\r\n", APP_START_ADDR, APP_FLASH_SIZE); printf("======================================\r\n"); } typedef void (*jump_callback)(void); /** * @note 跳转至App运行 * * @param App起始地址 * * @return result */ uint8_t jump_app(uint32_t app_addr) { uint32_t jump_addr; jump_callback cb; if (((*(volatile uint32_t *)app_addr) & 0x2FFE0000 ) == 0x20000000) { // 复位向量位于程序起始地址+4处 jump_addr = *(volatile uint32_t*)(app_addr + 4); cb = (jump_callback)jump_addr; // 设置主堆栈指针指向 APP 程序起始地址 __set_MSP(*(volatile uint32_t*)app_addr); cb(); return 1; } return 0; } /** * @brief Gets the sector of a given address * @param None * @retval The sector of a given address */ static uint8_t GetSector(uint32_t Address) { if((Address < ADDR_FLASH_SECTOR_1) && (Address >= ADDR_FLASH_SECTOR_0)) { return FLASH_SECTOR_0; } else if((Address < ADDR_FLASH_SECTOR_2) && (Address >= ADDR_FLASH_SECTOR_1)) { return FLASH_SECTOR_1; } else if((Address < ADDR_FLASH_SECTOR_3) && (Address >= ADDR_FLASH_SECTOR_2)) { return FLASH_SECTOR_2; } else if((Address < ADDR_FLASH_SECTOR_4) && (Address >= ADDR_FLASH_SECTOR_3)) { return FLASH_SECTOR_3; } else if((Address < ADDR_FLASH_SECTOR_5) && (Address >= ADDR_FLASH_SECTOR_4)) { return FLASH_SECTOR_4; } else { return FLASH_SECTOR_5; } } /** * Read data from flash. * @note This operation's units is word. * * @param addr flash address * @param buf buffer to store read data * @param size read bytes size * * @return result */ int stm32_flash_read(uint32_t addr, uint8_t *buf, size_t size) { size_t i; if ((addr + size) > STM32_FLASH_END_ADDRESS) { printf("read outrange flash size! addr is (0x%p)", (void*)(addr + size)); return -1; } for (i = 0; i < size; i++, buf++, addr++) { *buf = *(uint8_t *) addr; } return size; } /** * Write data to flash. * @note This operation's units is word. * @note This operation must after erase. @see flash_erase. * * @param addr flash address * @param buf the write data buffer * @param size write bytes size * * @return result */ int stm32_flash_write(uint32_t addr, const uint8_t *buf, size_t size) { int8_t result = 0; uint32_t end_addr = addr + size; if ((end_addr) > STM32_FLASH_END_ADDRESS) { printf("write outrange flash size! addr is (0x%p)", (void*)(addr + size)); return -1; } if (size < 1) { return -1; } HAL_FLASH_Unlock(); __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_OPERR | FLASH_FLAG_WRPERR | FLASH_FLAG_PGAERR | FLASH_FLAG_PGPERR | FLASH_FLAG_PGSERR); for (size_t i = 0; i < size; i++, addr++, buf++) { /* write data to flash */ if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_BYTE, addr, (uint64_t)(*buf)) == HAL_OK) { if (*(uint8_t *)addr != *buf) { result = -1; break; } } else { result = -1; break; } } HAL_FLASH_Lock(); if (result != 0) { return result; } return size; } /** * Erase data on flash. * @note This operation is irreversible. * @note This operation's units is different which on many chips. * * @param addr flash address * @param size erase bytes size * * @return result */ int stm32_flash_erase(uint32_t addr, size_t size) { int8_t result = 0; uint32_t FirstSector = 0, NbOfSectors = 0; uint32_t SECTORError = 0; if ((addr + size) > STM32_FLASH_END_ADDRESS) { printf("ERROR: erase outrange flash size! addr is (0x%p)\n", (void*)(addr + size)); return -1; } /*Variable used for Erase procedure*/ FLASH_EraseInitTypeDef EraseInitStruct; /* Unlock the Flash to enable the flash control register access */ HAL_FLASH_Unlock(); __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_OPERR | FLASH_FLAG_WRPERR | FLASH_FLAG_PGAERR | FLASH_FLAG_PGPERR | FLASH_FLAG_PGSERR); /* Get the 1st sector to erase */ FirstSector = GetSector(addr); /* Get the number of sector to erase from 1st sector*/ NbOfSectors = GetSector(addr + size - 1) - FirstSector + 1; /* Fill EraseInit structure*/ EraseInitStruct.TypeErase = FLASH_TYPEERASE_SECTORS; EraseInitStruct.VoltageRange = FLASH_VOLTAGE_RANGE_3; EraseInitStruct.Sector = FirstSector; EraseInitStruct.NbSectors = NbOfSectors; if (HAL_FLASHEx_Erase(&EraseInitStruct, (uint32_t *)&SECTORError) != HAL_OK) { result = -1; goto __exit; } __exit: HAL_FLASH_Lock(); if (result != 0) { return result; } printf("erase done: addr (0x%p), size %d", (void*)addr, size); return size; }
#include "iap.h" #include "stdio.h" int main(void) { // 这里只是我自己写的部分,cubemx自动生成的这里没有,记得填上 start_uart_rx(); // 开始中断接收 show_boot_info(); // 输出分区信息 while (1) { printf("waitting input... \r\n"); // 判断是否需要更新程序(5s) for(uint16_t i = 0; i < 5000; i++) { // 如果按键按下, 说明需要更新程序 if(0 == HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0)) { HAL_Delay(20); if(0 == HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0)) { // 按键按下了,更新程序并跳转(先传程序,再按下按键) // 擦除APP区域 printf("erase app... \r\n"); stm32_flash_erase(APP_START_ADDR, APP_FLASH_SIZE); HAL_Delay(100); // 写入APP程序 printf("write app... \r\n"); stm32_flash_write(APP_START_ADDR, app_buff, app_buff_len);//更新FLASH代码 break; } } HAL_Delay(1); } // 跳转到APP if(0 == jump_app(APP_START_ADDR)) { printf("jump app failed... \r\n"); while(1); } } }
这里 Bootloader程序给分配大小是 16KB(看 iap 头文件),不是默认的全部 Flash,因此在 Keil 中修改一下。
至此,Bootloader程序就完成了。
App程序比较简单,创建一个LED闪烁工程 或 串口输出工程即可。
// 设置中断向量偏移量
#define NVIC_VTOR_MASK 0x3FFFFF80
#define APP_PART_ADDR 0x08004000
SCB->VTOR = APP_PART_ADDR & NVIC_VTOR_MASK;
设置编译输出 bin 文件(App只能用 bin 文件,程序保存地址由 bootloader 程序指定)
fromelf --bin --output "$L@L.bin" "#L"
app程序
至此,App程序就写完了,编译完成之后可以在工程的输出文件夹中看到编译生成的 bin 文件。
使用同样的方法可以创建两个App,分别为app1、app2两个的效果可以不一样。
这里设置app1输出:app_1 run,app2输出:app_2 run。
测试步骤
尝试更换两个app,查看不同效果。
代码汇总地址:https://gitee.com/HzoZi/bootloader
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。