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流程就讲完了,自己动手做一做还是蛮简单的。
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流程就讲完了,自己动手做一做还是蛮简单的。
举报