基于 STM32F407 的串口 IAP


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 版本及功能:

一、概述

在学习 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 中的结构如下:

如下图所示流程中:

  • STM32F407 复位后,还是从 0x08000004 地址取出复位中断向量的地址,并跳转到复位中断服务程序,在运行完复位中断服务程序之后跳转到 IAP 的 main 函数,如图标号①所示;
  • 在执行完 IAP 以后(即将新的 APP 代码写入 STM32F407 的 FLASH,灰底部分。新程序的复位中断向量起始地址为 0x08000004+N+M ),跳转至新写入程序的复位向量表,取出新程序的复位中断向量的地址,并跳转执行新程序的复位中断服务程序,随后跳转至新程序的main 函数,如图标号②和③所示,同样 main 函数为一个死循环,并且注意到此时 STM32F407 的 FLASH,在不同位置上,共有两个中断向量表。

在 main 函数执行过程中,如果 CPU 得到一个中断请求:

  1. PC 指针仍然会强制跳转到地址 0x08000004 中断向量表处,而不是新程序的中断向量表,如图标号④所示;
  2. 程序再根据我们设置的中断向量表偏移量,跳转到对应中断源新的中断服务程序中,如图标号⑤所示;
  3. 在执行完中断服务程序后,程序返回 main 函数继续运行,如图标号⑥所示。

三、IAP 程序

1、串口部分

首先,串口是至关重要的一部分,毕竟数据是通过串口传递过来的。

首先是定义了串口数据接收缓冲区的大小为 120 kb,下面的 UART_RX_BUF_BIN 即数据缓冲区,UART_RX_CNT 记录了 UART_RX_BUF_BIN 数组的大小。

// uart.h
#define RX_BUFFER_SIZE 120*1024

extern uint8_t 	UART_RX_BUF_BIN[RX_BUFFER_SIZE];
extern uint32_t UART_RX_CNT;

下面是 USART1 的中断处理函数,当有数据发送过来时,就会执行这段代码:

// uart.c
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 中。注意这里:

__attribute__ ((at(0X20001000)))

通过 __attribute__ 将缓冲区放到地址 0X20001000

2、iap 程序

// iap.h
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);
// iap.c
uint32_t iapbuf[512];

void board_lowlevel_deinit(void)
{
	/* 关闭全局中断 */
	__disable_irq(); 
	
	/* 关闭滴答定时器,复位到默认值 */
	SysTick->CTRL = 0;
	SysTick->LOAD = 0;
	SysTick->VAL  = 0;

	/* 设置所有时钟到默认状态,使用HSI时钟 */
	RCC_Deinit();
	
	/* 关闭所有中断,清除所有字段挂起标志 */
	for (int i = 0; i < 8; ++i)
	{
		NVIC->ICER[i] = 0xFFFFFFFF;
		NVIC->ICPR[i] = 0xFFFFFFFF;
	}

	/* 使能全局中断 */
	__enable_irq();
}

/******************************************************************************
 * @brief      向Flash写入应用程序  
 * 
 * @param[in]  addr    :     要写入的地址
 * @param[in]  buf     :     要写入的数据
 * @param[in]  size    :     数据大小
 * 
 * @return     none
 * 
******************************************************************************/
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; // 偏移2048  512*4=2048
        }
    }
    if (i) {
        bl_norflash_write(fwaddr, iapbuf, i); // 将最后的一些内容字节写进去.
    }
}

/******************************************************************************
 * @brief      跳转到应用程序段
 * 
 * @param[in]  addr    :    用户代码起始地址
 * 
 * @return     none
 * 
******************************************************************************/
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);  							    // 初始化APP堆栈指针

	board_lowlevel_deinit();					// 关中断
    app_entry();                                // 跳转到APP.
}

__asm void MSR_MSP(uint32_t addr)
{
    MSR MSP, r0;
    BX r14;
}

有一点需要注意,在由 IAP 跳转到 APP 时, 一定注意把 IAP 中开启的外设全部关闭(包括 SysTick 中断),否则在刚进入 APP 中时,如果产生中断将导致死机等问题。

3、内部 flash 读写

// flash.h
#define STM32_FLASH_BASE 0x08000000 	//STM32 FLASH的起始地址
 

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 的读写操作比较简单。不过,需要注意的是,写操作要注意写之前要保证是没有写过的区域即可。

typedef struct
{
	uint32_t sector;
	uint32_t size;
} sector_desc_t;

// stm32f4 每个分区的大小描述
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;
}

/******************************************************************************
 * @brief      获取某个地址所在的flash扇区
 * 
 * @param[in]  addr     :   flash地址
 * 
 * @return     uint16_t :   0~11,即addr所在的扇区
 * 
******************************************************************************/
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;
}

/******************************************************************************
 * @brief 	   从指定地址开始写入指定长度的数据
 * 
 * @param[in]  write_addr    :    起始地址(此地址必须为4的倍数!!)
 * @param[in]  data  	     :    要写入的数据
 * @param[in]  size    		 : 	  写入数据的大小
 * 
 * @return     none
 * 
 * @note       1. 该函数对OTP区域也有效!可以用来写OTP区(0X1FFF7800~0X1FFF7A0F)!
 * 			   2. 因为STM32F4的扇区太大了,没办法本地保存扇区数据,所以本函数
 *                写地址如果非0XFF,那么会先擦除整个扇区且不保存扇区数据.所以
 *                写非0XFF的地址,将导致整个扇区数据丢失.建议写之前确保扇区里
 *                没有重要数据,最好是整个扇区先擦除了,然后慢慢往后写.
 *    
******************************************************************************/
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); // FLASH擦除期间,必须禁止数据缓存
	/*****************************************************************************/

	addr_begin = write_addr;					  // 写入的起始地址
	addr_end = write_addr + size * 4; 	  // 写入的结束地址
	if (addr_begin < 0X1FFF0000)				  // 只有主存储区,才需要执行擦除操作!!
	{
		while (addr_begin < addr_end) // 扫清一切障碍.(对非FFFFFFFF的地方,先擦除)
		{
			if (bl_norflash_read_word(addr_begin) != 0XFFFFFFFF) // 有非0XFFFFFFFF的地方,要擦除这个扇区
			{
				status = FLASH_EraseSector(bl_norflash_get_flash_sector(addr_begin), VoltageRange_3); // VCC=2.7~3.6V之间!!
				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擦除结束,开启数据缓存
	FLASH_Lock();				// 上锁
}

/******************************************************************************
 * @brief 	   从指定地址开始读出指定长度的数据
 * 
 * @param[in]  read_addr    :    起始地址  
 * @param[in]  data  	    :    存放读取数据
 * @param[in]  size    		:    要读取数据的大小
 * 
 * @return     none
 * 
******************************************************************************/
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); // 读取4个字节.
		read_addr += 4;							  // 偏移4个字节.
	}
}

4、main 程序

下面是 main 函数逻辑:

  1. Bootloader 等待 10s
    1. 10s 内如果没有通过串口发送 “yes”,则自动引导进入用户程序
    2. 如果发送了 ”yes“,则会等待用户发送新的固件
      • 等待固件发送完成后,先判断改固件地址信息是否准确
      • 正确则继续执行将其写入 Flash
      • 最后进入用户程序
	uint32_t time = 0;      // 计时(10s)
	uint32_t oldcount = 0;  // 旧的串口接收数据值
	uint32_t applenth = 0;  // 接收到的app代码长度
	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 ) // 判断是否为0X08XXXXXX.
					{
						bl_iap_write_app_bin(FLASH_APP1_ADDR, UART_RX_BUF_BIN, applenth); // 更新FLASH代码
						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 ) // 判断是否为0X08XXXXXX.
				{
					printf("开始执行Flash应用程序\r\n");
					bl_iap_load_app(FLASH_APP1_ADDR); // 执行FLASH APP代码
				}
				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 ) // 判断是否为0X08XXXXXX.
			{
				printf("开始执行FLASH应用程序\r\n");
				bl_iap_load_app(FLASH_APP1_ADDR); // 执行FLASH APP代码
			}
			else
			{
				printf("地址错误!!!\r\n");
			}
		}
	}

逻辑比较简单,就不多说了。

下面写一个用户程序来验证一下:

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");
}

一定要注意这段代码:

NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x10000);

这里设置了用户程序的中断向量表的偏移地址为 0x10000,如果不设置这个偏移地址,就无法进入定时器4 中断服务函数,LED 就不会闪烁。

另外,同时还要注意设置好用户程序的地址。

STM32F407串口升级程序IAP(In-Application Programming)是一种通过串口实现固件升级的方法。通过这个程序,我们可以通过串口接口将新的固件文件传输到STM32F407芯片上,从而实现对芯片上固件的更新。 串口升级程序IAP的实现需要以下步骤: 首先,我们需要在STM32F407上实现串口通信功能。可以选择UART或USART作为串口通信接口,通过编程配置串口参数,使其能够正常地与上位机通信。 然后,我们需要制定固件升级的协议,即约定好固件传输的格式以及升级的流程。通常,我们可以将固件文件分成固定大小的数据包,并在每个数据包中附加一些校验信息,以确保数据传输的准确性。 接下来,我们通过串口将固件文件分包传输到STM32F407芯片上。在芯片上,我们需要编写相应的程序来接收并解析串口接收到的数据包。当接收到一个完整的数据包后,芯片会进行校验,如果校验通过,则将数据写入指定的固件存储区域。这个过程会不断重复,直到所有的数据包都被正确地写入芯片。 最后,当所有的数据包都被写入芯片后,我们需要对芯片进行复位,使新的固件生效。此时,芯片会重新启动并运行新的固件。 通过以上步骤,使用STM32F407串口升级程序IAP可以有效地实现对芯片上固件的升级。这种方法简单、方便,适用于芯片已经在产品中部署的场景,可以避免对整个产品进行更换。
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值