STM32通过CubeMX配置FreeRTOS+USB_HOST+Fatfs+U盘进行IAP。

目录

一.使用CubeMX配置STM32

二.U盘IAP原理

三.U盘IAP过程


 

MCU:STM32F107

开发环境:CubeMX V6.1.1+Keil MDK V5.26

一.使用CubeMX配置STM32

1.FreeRTOS配置,如果只是做一些验证或者测试,基本上使用默认配置即可:

2.USB外设配置:

USB_OTG配置为USB_HOST:

开启USB中断,注意中断优先级设为5,有时默认的中断优先级为0,由于引入RTOS处理USB且RTOS管理的中断优先级为5级以下,0优先级的USB中断会导致程序卡死:

配置USB_HOST所支持的设备类型。由于我的程序内USB还做通讯使用,所以配置支持所有类设备,如果只做U盘读取,配置支持MSC类即可,需要注意的是USB任务堆栈别太小,否则读写U盘时容易卡死:

3.FATFS配置:

有了USB接口、有了U盘,那么剩下的就是使用文件系统来读写U盘了,所以我们对U盘的读写都是通过FATFS文件系统的API来完成的。

同样,几乎使用默认配置即可,不过要想支持中文,CODE_PAGE一定要设为GBK简体中文。

其他的一些外设配置、时钟配置不多说,最后生成代码前,要把系统的堆栈空间设大一些,否则同样容易卡死,或者大容量的U盘可能会无法正常识别:

点击生成代码,剩下的就得啃代码了。

二.U盘IAP原理

IAP就是让我们能够脱离下载器(例如常用的ST-Link,J-Link等),在MCU的Flash里写入新的程序。常见的iap方式有串口iap、网口iap、U盘iap等,今天我们要实现的的就是U盘iap方式。这几种iap方式,都是通过一个加载程序,将要更新的程序文件(一般为BIN文件)写入到Flash中,我们称这个加载程序为Bootloader(简称BOOT)程序,要更新的程序称为Application(简称APP)程序,BOOT只负责加载,APP实现我们的应用功能。所以,带有IAP功能的单片机其实运行的是有两个程序,BOOT+APP,这两个程序都在Flash中,BOOT程序放在Flash的起始地址,APP程序放到BOOT程序地址后面,上电后单片机先运行的是BOOT程序,在BOOT程序里判断是否需要更新APP程序,如果需要更新就把更新文件写入到存放APP程序地址的Flash中,如果不需要更新就直接跳转至APP程序。

例如手上用到的这个单片机Flash内存大小为128KB,Flash地址为0x8000000~0x8020000。BOOT占用内存大小为32KB,起始地址0X8000000,也就是MCU的Flash起始地址;APP程序占用94KB的内存,起始地址为0x8008800,也就是BOOT内存区域的终点地址。上电后程序先从0x8000000开始运行BOOT程序,如果不要更新程序就跳转至0x8008800继续运行APP程序,所以从BOOT程序切换至APP程序就是一个PC指针跳转的过程,至于如何跳转,后面介绍。

U盘iap的方式,U盘就是充当扩展的外部内存的角色,存放需要更新的BIN文件,程序更新过程中MCU先将U盘里的BIN文件读取到MCU内部的Ram中,然后再将Ram中的数据写入到Flash中,过程如下:

如果更新文件比较大,超过MCU的RAM大小,可以将更新文件分割成若干部分,然后从前往后依次读写即可。

三.U盘IAP过程

1.对MCU内存进行分区。前面讲到要想实现IAP那么MCU就得运行两个程序,要想运行两个程序就得对MCU内存分区来存放这两个程序。使用MDK很容易实现MCU内存分区,如下图:

这个Start地址就是当前程序要下载到Flash中的起始地址,前面讲到APP的起始地址为0x808800,所以这儿就设置为0x808800.

2.设置APP程序的向量表偏移地址。

由于我们程序的起始地址改了,那么中断向量的地址当然也要跟着改变,否则程序无法正常进入中断,而中断地址的改变就是通过向量偏移量来实现的,程序的起始地址从0x8000000变为0x8008800,地址偏移了0x8800,那么中断向量同样偏移了0x8800,所以定义VECT_TAB_OFFSET为0x8800即可。新版的CubeMX重定义偏移量是需要先定义USER_VECT_TAB_ADDRESS。BOOT程序的地址还是从0x8000000开始的,无需设置中断向量偏移。

3.编写BOOT程序里的Flash读写函数。

MCU内部Flash的读写不像读写Ram那样简单,需要对Flash进行相关配置,Flash的擦写函数以及初始化函数参照了官方BSP里的IAP代码:

/*-----------------------------------------------------------------------------
* 函 数 名         : FLASH_If_Init()
* 函数功能		   : Flash初始化
* 输    入         : 无
* 输    出         : 无
-----------------------------------------------------------------------------*/
void FLASH_If_Init(void)
{
	HAL_FLASH_Unlock();

	__HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPERR);

	HAL_FLASH_Lock();
}

/*-----------------------------------------------------------------------------
* 函 数 名         : FLASH_If_Erase()
* 函数功能		   : Flash擦除函数
* 输    入         : 无
* 输    出         : 无
-----------------------------------------------------------------------------*/
uint32_t FLASH_If_Erase(uint32_t start)
{
	uint32_t NbrOfPages = 0;
	uint32_t PageError = 0;
	FLASH_EraseInitTypeDef pEraseInit;
	HAL_StatusTypeDef status = HAL_OK;

	HAL_FLASH_Unlock();

	NbrOfPages = (USER_FLASH_END_ADDRESS - start)/PAGE_SIZE;

	pEraseInit.TypeErase = FLASH_TYPEERASE_PAGES;
	pEraseInit.PageAddress = start;
	pEraseInit.Banks = FLASH_BANK_1;
	pEraseInit.NbPages = NbrOfPages;
	status = HAL_FLASHEx_Erase(&pEraseInit, &PageError);

	HAL_FLASH_Lock();

	if (status != HAL_OK)
	{
		return FLASHIF_ERASEKO;
	}

	return FLASHIF_OK;
}

/*-----------------------------------------------------------------------------
* 函 数 名         : FLASH_If_Write()
* 函数功能		   : Flash写函数
* 输    入         : destination:当前要写入的Flash起始地址
                     p_source:指向储存的待更新数据
                     length:本次写入Flash的数据长度
* 输    出         : 无
-----------------------------------------------------------------------------*/
uint32_t FLASH_If_Write(uint32_t destination, uint32_t *p_source, uint32_t length)
{
	uint32_t i = 0;

	HAL_FLASH_Unlock();
	for (i = 0; (i < length) && (destination <= (USER_FLASH_END_ADDRESS-4)); i++)
	{
		if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, destination, *(uint32_t*)(p_source+i)) == HAL_OK)      
		{
			if (*(uint32_t*)destination != *(uint32_t*)(p_source+i))
			{
				return(FLASHIF_WRITINGCTRL_ERROR);
			}
			destination += 4;
		}
		else
		{
			return (FLASHIF_WRITING_ERROR);
		}
	}
		
	AppAddresssCurren = destination;
	HAL_FLASH_Lock();

	return (FLASHIF_OK);
}

这里用到AppAddresssCurren这个变量来记录本次Flash写入数据后的地址,以便下次写Flash时就从这个地址开始继续往后面写。

4.读取U盘内的更新文件,写入MCU的Flash中。

有了Flash的操作函数后,剩下的就是读取U盘里的更新文件,然后写入到Flash中,函数如下:

#define PAGE_SIZE                     ((uint16_t)0x400)   		/*1 Kbytes     Size of page*/
#define FLASH_SIZE                    ((uint32_t)0x20000)  		/*128 KBytes   Flash*/
#define APPLICATION_ADDRESS 		  ((uint32_t)0X08008800)
#define USER_FLASH_END_ADDRESS        ((uint32_t)0x08020000)
#define USER_FLASH_SIZE               ((uint32_t)0x00017800) 	/*94 KBytes   User flash size*/


#define RAM_BUFFER_SIZE               ((uint32_t)50*1024)       /*KBytes*/
/*-----------------------------------------------------------------------------
* 函 数 名         : AppWrite()
* 函数功能		  : U盘IAP函数
* 输    入         : 无
* 输    出         : 无
-----------------------------------------------------------------------------*/
uint8_t RAM_Buffer[RAM_BUFFER_SIZE] ={0x00};
void AppWrite(void)
{
	FRESULT res; 
		
	retUSBH = FATFS_LinkDriver(&USBH_Driver, USBHPath);
	AppAddresssCurren = APPLICATION_ADDRESS;
		
	res = f_mount(&USBHFatFS, (TCHAR const*)USBHPath, 0);
	if(res != FR_OK)
	{
		Error_Handler();
	}
		
	res = f_open(&USBHFile,"TestApp1.BIN", FA_READ);
	if(res != FR_OK)
	{
		Error_Handler();
	}

	res = f_read(&USBHFile, RAM_Buffer, sizeof(RAM_Buffer), (void *)&APP_Size);
	if(res != FR_OK)
	{
		Error_Handler();
	}
		
	if(((10*1024)<APP_Size) && (APP_Size<RAM_BUFFER_SIZE))
	{	
		FLASH_If_Erase(APPLICATION_ADDRESS);
		FLASH_If_Write(AppAddresssCurren, (uint32_t*) RAM_Buffer, APP_Size/4);
	}

	f_close(&USBHFile);
	FATFS_UnLinkDriver(USBHPath);
}

这里我们定义了一个RAM_Buffer[RAM_BUFFER_SIZE]数组来存放读取的BIN文件,数组的大小根据时间的Ram大小决定,我这里是64KB的所以定义了50KB的Ram来读取BIN文件,然后将读取的BIN文件通过FLASH_If_Write()函数写入Flash中,完成外部新的程序的更新至Flash中。

5.跳转。

前面说了从BOOT切换至APP程序其实就是PC指针的一次跳转,这个跳转通过下面这个函数实现的,参照官方BSP里的IAP例程:

typedef  void (*pFunction)(void);
pFunction JumpToApplication;

/*-----------------------------------------------------------------------------
* 函 数 名         : JumpToAPP()
* 函数功能		  	 : 程序跳转函数
* 输    入         : 无
* 输    出         : 无
-----------------------------------------------------------------------------*/
void JumpToAPP(void)
{
	__disable_irq();

    if (((*(__IO uint32_t*)APPLICATION_ADDRESS) & 0x2FFE0000 ) == 0x20000000)
    {
		JumpAddress = *(__IO uint32_t*) (APPLICATION_ADDRESS + 4);
		JumpToApplication = (pFunction) JumpAddress;
		__set_MSP(*(__IO uint32_t*) APPLICATION_ADDRESS);
		JumpToApplication();
	}
}

在跳转前需要调用__disable_irq()函数来关闭中断,否则如果BOOT里开的有中断(比如说我这里开的有定时中断和串口中断),跳转至APP后会卡死。

/*****************************************************************************/

到这里我们整个IAP流程就讲完了,自己动手做一做还是蛮简单的。


版权声明:本文为Chen_ry原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。