文章目录
1. stm32程序升级方法
1.1 st-link / j-link下载
这个应该是最基本的方法,只要自己写过程序的应该都会,将编译生成的hex文件使用st-link工具或者j-link工具直接下载进 flash 即可。keil中点击下载也能一键下载。
下载时可以看到地址是从0x0800 0000,即 flash 的起始地址开始下载的。
优点:简单,插上下载器直接下载即可。
 缺点:在产品中嵌入式板卡封装起来之后,因为下载口没有实际功能,所以很多时候下不拆机是没办法插上下载器的。这时候就不方便。
简单补充一句,bin文件和hex文件的区别:
- bin文件不带地址信息,因此下载的时候需要指定下载地址。
- hex文件自带地址信息,直接点击下载自己会找到要下载到的地址(默认0x0800 0000)。
1.2 isp(in system programing)
我们常见的一键下载电路就是用的这种方式。这个是利用了 stm32 自带的 bootloader 升级程序。
在用户参考手册中,可以看到下表,关于启动模式设置的。
 
 中文版的如下:
 
 程序运行时的启动过程:
一般在我们的程序运行的时候,boot0 是接地的,也就是boot0 = 0。也就是程序是从主存储器flash开始启动的,启动的地址为0x08000000。
使用isp下载时的启动过程:
- 首先,硬件上将 stm32 的 boot0 引脚拉高、boot1 引脚拉低,即boot0 = 1、boot1 = 0。
- 此时,程序会从系统存储器中的程序启动,这段程序会接收串口数据(我们编译好的程序文件),并将这写数据放到主闪存存储器(flash)当中。
- 最后,硬件上重新将 boot0 接地,也就是boot0 = 0,然后复位引脚拉低,程序复位重启,从 flash 中开始运行程序。
这样也就完成了我们的一键下载过程。
 比如,正点原子的一键下载电路如下,左侧的三极管就是用来完成 boot引脚和复位引脚的操作的。

沁恒官网也有专门的一键下载芯片,原理上其实一样的。
 
isp的下载方式:
优点:提供了一种升级方式,无需代码支持。
 缺点:需要相应的硬件支持,成本增加;使用的接口也是固定的,并且很多时候串口可能用于其他功能了已经。
1.3 iap(in applicating programing)
iap 和 isp 其实基本上是一样的。
但是:isp 是由厂商已经提供好的,因此接口固定;iap可以自定义使用任何接口接收应用程序。也正是因为这一点,使得用户可以用多种不同的方式进行升级。
1.3.1 正常程序运行流程
在看iap之前,要先看一下正常情况下,程序从flash启动时的启动流程,如下图所示:
- 首先程序从flash启动,根据中断向量表找到复位中断处理函数的地址(0x0800 0004处是中断向量表的起始地址,记录了复位中断处理函数的地址)。
- 执行复位中断处理函数,初始化系统环境之后,该函数最后会跳转到main函数继续运行。
- 在main函数的死循环中一直运行,直到有中断发生时(外设中断等等),重新跳转到中断向量表起始处。
- 在中断向量表中根据中断信号源来判断要执行的中断处理函数,然后跳转到相应的中断处理函数。
- 中断处理函数运行完成之后继续跳转到main函数处继续运行。

1.3.2 有iap时程序运行流程
接下来看下有了iap之后的启动流程,如下图所示,下图中可以看到,在flash中存储了 两“套” 程序(套的意思是,不仅只有用户程序,配套的中断向量等也都有)。
其中第一套为:bootloader程序,该程序的功能是,接收某个接口的数据,并把这些数据存储到flash中。
第二套才是我们真正的应用程序。
- 首先程序从flash启动,根据中断向量表找到复位中断处理函数的地址(0x0800 0004处是中断向量表的起始地址,记录了复位中断处理函数的地址)。
- 执行复位中断处理函数,该函数最后会跳转到bootloader的main函数继续运行。这个main函数的任务就是,判断是否接收新的app程序,如果有,就把新的app程序文件保存到flash(就是第二套程序的位置)然后跳转到第二套程序中运行。如果无,就直接跳转到第二套程序中运行。
- 跳转之后的过程也是和正常程序运行的流程一样,一旦进入新的app程序的起始地址,就会根据中断向量表找到复位中断处理函数,然后进入app程序的main函数运行。
- 但是如果发生中断,是强制跳转到bootloader程序的中断向量表进行查询的,而我们需要的肯定是需要跳转到app程序的中断处理函数处运行。所以,在进行到app程序后,app程序一定要修改中断向量表的偏移,让查找对应中断处理函数的时候偏移一段地址到app程序的中断处理函数处,否则app程序中的中断发生时,就无法跳转到app程序的中断处理函数了。

 到这,我们就知道bootloader程序是干啥的了。
对比:
| isp | iap | 
|---|---|
| 在系统存储器中存储了一套接收串口1数据的程序 | iap是将flash分成了两份,在第一份中存储了一套接收某个接口的程序 | 
| 使用硬件boot引脚设置进行跳转 | 使用软件(直接修改pc指针)进行跳转 | 
| 仅能使用芯片厂商设置好的接口(串口1) | 用户自定义,理论上只要能接收数据的接口都可以用 | 
2. stm32 bootloader实现
开发板:stm32f401ccu6最小系统板(淘宝十几块钱),flash大小:256kb,ram大小:64kb。
todo:这里记录一下,我看到的或者想到的所有形式。每实现一个会过来贴代码地址。
- bootloader_app
- bootloader_setting_app
- bootloader_app1_app2
- u盘拖拽
- 无线升级
- …
代码汇总地址:https://gitee.com/hzozi/bootloader
2.1 方式一:boot_app(已实现)
这应该是最常见、也是最简单的一种了。也就是上面1.3.2小节的分析的情况。

2.1.1 bootloader
需要实现四个部分:
- 串口接收程序,用于接收app程序;
- stm32 flash 写入接口,用于将app程序写入到 flash;
- app跳转实现,用于跳转到app程序处开始运行。
- 设置 keil 参数。
第一步:新建工程
调试口勾上,时钟设置最大,设置生成单独的.c文件。
- 勾选一个串口,参数默认即可。
  
- 串口中断也勾上

 3. 设置一个按键

 生成工程即可。
第二步:配置串口,实现串口接收功能
- 添加 printf 函数支持
我这里放到了 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;
}
- 实现中断接收app程序
我这里放到了 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程序起始地址,根据芯片不同起始地址倍数关系也不同
- stm32f1:地址必须是4的倍数,因为每次写入只能写入32位数据,即4个字节。
- stm32f4:地址可以从任意地址开始,因为每次写入可以写入8位数据,每个地址就是8位数据。
- stm32l4:地址必须是8的倍数,因为每次写入只能写入64位数据,即8个字节。

#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 *)§orerror) != 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;
}
- main函数:

#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);
		}
	}
}
- keil 配置:
这里 bootloader程序给分配大小是 16kb(看 iap 头文件),不是默认的全部 flash,因此在 keil 中修改一下。

 至此,bootloader程序就完成了。
2.1.2 app
app程序比较简单,创建一个led闪烁工程 或 串口输出工程即可。
- 修改中断向量表偏移

// 设置中断向量偏移量
#define nvic_vtor_mask       0x3fffff80
#define app_part_addr        0x08004000
scb->vtor = app_part_addr & nvic_vtor_mask;
- 修改flash起始地址和flash大小

-  设置编译输出 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。
2.1.3 测试
测试步骤
- 先把bootloader程序烧录到芯片中
- 复位运行,可以看到bootloader程序的提示信息
- 使用串口助手传输app程序,传输完成之后按下按键,等待写入完成之后自动跳入app程序开始运行
- 如果一直没有按键按下,5秒之后直接跳入app开始运行
尝试更换两个app,查看不同效果。
 
代码汇总地址:https://gitee.com/hzozi/bootloader
 
             我要评论
我要评论 
                                             
                                             
                                             
                                            
发表评论