最新教程下载:http://www.armbbs.cn/forum.php?mod=viewthread&tid=93255
第36章 STM32F407的SPI 总线应用之SPI Flash的MDK下载算法制作
本章节为大家讲解MDK下载算法制作方法。
36.1 初学者重要提示
36.2 MDK下载算法基础知识
36.3 创建MDK下载算法通用流程
36.4 SPI Flash的MDK下载算法制作
36.5 SPI Flash的MDK下载算法使用方法
36.6 实验例程说明
36.7 总结
36.1 初学者重要提示
- SPI Flash的相关知识点可以看第31章和32章。
- SPI Flash下载算法文件直接采用HAL库制作,方便大家自己修改。
36.2 MDK下载算法基础知识
Flash编程算法是一种用于擦除应用程序或将应用程序下载到Flash的程序代码。MDK本身支持的各种器件都自带下载算法,存放在MDK各种器件的软件包里面,以STM32F4为例,算法存放在KeilSTM32F4xx_DFP2.15.0CMSISFlash(软件包版本不同,数值2.15.0不同),但不支持的需要我们自己制作,本章教程为此而生。
36.2.1 程序能够通过下载算法下载到芯片的核心思想
认识到这点很重要:通过MDK创建一批与地址信息无关的函数,实现的功能主要有初始化,擦除,编程,读取,校验等,然后MDK调试下载阶段,会将算法文件加载到芯片的内部RAM里面(加载地址可以通过MDK设置),然后MDK通过与这个算法文件的交互,实现程序下载,调试阶段数据读取等操作。
36.2.2 算法程序中擦除操作执行流程
擦除操作大致流程:
- 加载算法到芯片RAM。
- 执行初始化函数Init。
- 执行擦除操作,根据用户的MDK配置,这里可以选择整个芯片擦除或者扇区擦除。
- 执行Uinit函数。
- 操作完毕。
36.2.3 算法程序中编程操作执行流程
编程操作大致流程:
- 针对MDK生成的axf可执行文件做Init初始化,这个axf文件是指的大家自己创建应用程序生成的。
- 查看Flash算法是否在FLM文件。如果没有在,操作失败。如果在:
- 加载算法到RAM。
- 执行Init函数。
- 加载用户到RAM缓冲。
- 执行Program Page页编程函数。
- 执行Uninit函数。
- 操作完毕。
36.2.4 算法程序中校验操作执行流程
校验操作大致流程:
- 校验要用到MDK生成的axf可执行文件。校验就是axf文件中下载到芯片的程序和实际下载的程序读出来做比较。
- 查看Flash算法是否在FLM文件。如果没有在,操作失败。如果在:
- 加载算法到RAM。
- 执行Init函数。
- 查看校验算法是否存在
- 如果有,加载应用程序到RAM并执行校验。
- 如果没有,计算CRC,将芯片中读取出来的数据和RAM中加载应用计算输出的CRC值做比较。
- 执行Uninit函数。
- 替换BKPT(BreakPoint断点指令)为 B. 死循环指令。
- 执行RecoverySupportStop,恢复支持停止。
- 执行DebugCoreStop,调试内核停止。
- 运行应用:
- 执行失败。
- 执行成功,再执行硬件复位。
- 操作完毕,停止调试端口。
36.3 创建MDK下载算法通用流程
下面是MDK给的一种大致操作流程,不限制必须采用这种方法,自己创建也可以的。
36.3.1 第1步,使用MDK提供好的程序模板
位于路径:KeilARMPackARMCMSISversionDevice_Template_Flash。
效果如下:
36.3.2 第2步,修改工程名
MDK提供的工程模板原始名字是NewDevice.uvprojx,大家可以根据自己的需要做修改。比如修改为MyDevice.uvprojx。
36.3.3 第3步,修改使用的器件
在MDK的Option选项里面设置使用的器件。
36.3.4 第4步,修改输出算法文件的名字
这个名字是方便用户查看的,比如设置为stm32h7,那么输出的算法文件就是stm32h7.flm。
注:MDK这里设置的名字与下面位置识别出来的算法名无关:
这个名字是在FlashDev.c里面定义的。
36.3.5 第5步,修改编程算法文件FlashPrg.c
模板工程里面仅提供了接口函数,内容需要用户自己填。
代码语言:javascript复制/*
Mandatory Flash Programming Functions (Called by FlashOS):
int Init (unsigned long adr, // Initialize Flash
unsigned long clk,
unsigned long fnc);
int UnInit (unsigned long fnc); // De-initialize Flash
int EraseSector (unsigned long adr); // Erase Sector Function
int ProgramPage (unsigned long adr, // Program Page Function
unsigned long sz,
unsigned char *buf);
Optional Flash Programming Functions (Called by FlashOS):
int BlankCheck (unsigned long adr, // Blank Check
unsigned long sz,
unsigned char pat);
int EraseChip (void); // Erase complete Device
unsigned long Verify (unsigned long adr, // Verify Function
unsigned long sz,
unsigned char *buf);
- BlanckCheck is necessary if Flash space is not mapped into CPU memory space
- Verify is necessary if Flash space is not mapped into CPU memory space
- if EraseChip is not provided than EraseSector for all sectors is called
*/
/*
* Initialize Flash Programming Functions
* Parameter: adr: Device Base Address
* clk: Clock Frequency (Hz)
* fnc: Function Code (1 - Erase, 2 - Program, 3 - Verify)
* Return Value: 0 - OK, 1 - Failed
*/
int Init (unsigned long adr, unsigned long clk, unsigned long fnc) {
/* Add your Code */
return (0); // Finished without Errors
}
/*
* De-Initialize Flash Programming Functions
* Parameter: fnc: Function Code (1 - Erase, 2 - Program, 3 - Verify)
* Return Value: 0 - OK, 1 - Failed
*/
int UnInit (unsigned long fnc) {
/* Add your Code */
return (0); // Finished without Errors
}
/*
* Erase complete Flash Memory
* Return Value: 0 - OK, 1 - Failed
*/
int EraseChip (void) {
/* Add your Code */
return (0); // Finished without Errors
}
/*
* Erase Sector in Flash Memory
* Parameter: adr: Sector Address
* Return Value: 0 - OK, 1 - Failed
*/
int EraseSector (unsigned long adr) {
/* Add your Code */
return (0); // Finished without Errors
}
/*
* Program Page in Flash Memory
* Parameter: adr: Page Start Address
* sz: Page Size
* buf: Page Data
* Return Value: 0 - OK, 1 - Failed
*/
int ProgramPage (unsigned long adr, unsigned long sz, unsigned char *buf) {
/* Add your Code */
return (0); // Finished without Errors
}
36.3.6 第6步,修改配置文件FlashDev.c
模板工程里面提供简单的配置说明:
代码语言:javascript复制struct FlashDevice const FlashDevice = {
FLASH_DRV_VERS, // Driver Version, do not modify!
"New Device 256kB Flash", // Device Name
ONCHIP, // Device Type
0x00000000, // Device Start Address
0x00040000, // Device Size in Bytes (256kB)
1024, // Programming Page Size
0, // Reserved, must be 0
0xFF, // Initial Content of Erased Memory
100, // Program Page Timeout 100 mSec
3000, // Erase Sector Timeout 3000 mSec
// Specify Size and Address of Sectors
0x002000, 0x000000, // Sector Size 8kB (8 Sectors)
0x010000, 0x010000, // Sector Size 64kB (2 Sectors)
0x002000, 0x030000, // Sector Size 8kB (8 Sectors)
SECTOR_END
};
注:名字New Device 256kB Flash就是我们第4步所说的。MDK的Option选项里面会识别出这个名字。
36.3.7 第7步,保证生成的算法文件中RO和RW段的独立性,即与地址无关
C和汇编的配置都勾选上:
汇编:
如果程序的所有只读段都与位置无关,则该程序为只读位置无关(ROPI, Read-only position independence)。ROPI段通常是位置无关代码(PIC,position-independent code),但可以是只读数据,也可以是PIC和只读数据的组合。选择“ ROPI”选项,可以避免用户不得不将代码加载到内存中的特定位置。这对于以下例程特别有用:
(1)加载以响应运行事件。
(2)在不同情况下使用其他例程的不同组合加载到内存中。
(3)在执行期间映射到不同的地址。
使用Read-Write position independence同理,表示的可读可写数据段。
36.3.8 第8步,将程序可执行文件axf修改为flm格式
通过下面的命令就可以将生成的axf可执行文件修改为flm。
36.3.9 第9步,分散加载设置
我们这里的分散加载文件直接使用MDK模板工程里提供好的即可,无需任何修改。
分散加载文件中的内容如下:
代码语言:javascript复制; Linker Control File (scatter-loading)
;
PRG 0 PI ; Programming Functions
{
PrgCode 0 ; Code
{
* ( RO)
}
PrgData 0 ; Data
{
* ( RW, ZI)
}
}
DSCR 0 ; Device Description
{
DevDscr 0
{
FlashDev.o
}
}
--diag_suppress L6305用于屏蔽L6503类型警告信息。
特别注意,设置了分散加载后,此处的配置就不再起作用了:
36.4 SPI Flash的MDK下载算法制作
下面将QSPI Flash算法制作过程中的几个关键点为大家做个说明。
36.4.1 第1步,制作前重要提示
这两点非常重要:
- 程序里面不要开启任何中断,全部查询方式。
- HAL库里面各种时间基准相关的API全部处理掉。简单省事些,我们这里是直接注释,采用死等即可。无需做超时等待,因为超时后,已经意味着操作失败了,跟死等没有区别。
36.4.2 第2步,准备一个工程模板
推荐大家直接使用我们本章工程准备好的模板即可,如果大家自己制作,注意一点,请使用当前最新的HAL库。
36.4.3 第3步,修改HAL库
大家可以更新需要修改以下三个文件(当前配套程序未做修改):
36.4.4 第4步,时钟初始化
我们已经用不到滴答定时器了,直接在bsp.c文件里面对滴答初始化函数做重定向:
代码语言:javascript复制/*
*********************************************************************************************************
* 函 数 名: HAL_InitTick
* 功能说明: 重定向,不使用
* 形 参: TickPriority
* 返 回 值: 无
*********************************************************************************************************
*/
HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
{
return HAL_OK;
}
然后就是HSE外置晶振的配置,大家根据自己的板子实际外挂晶振大小,修改stm32f4xx_hal_conf.h文件中HSE_VALUE大小,实际晶振多大,这里就修改为多大:
代码语言:javascript复制#if !defined (HSE_VALUE)
#define HSE_VALUE ((uint32_t) 8000000U) /*!< Value of the External oscillator in Hz */
#endif /* HSE_VALUE */
最后修改PLL:
代码语言:javascript复制/*
*********************************************************************************************************
* 函 数 名: SystemClock_Config
* 功能说明: 初始化系统时钟
* System Clock source = PLL (HSE)
* SYSCLK(Hz) = 168000000 (CPU Clock)
* HCLK = SYSCLK / 1 = 168000000 (AHB1Periph)
* PCLK2 = HCLK / 2 = 84000000 (APB2Periph)
* PCLK1 = HCLK / 4 = 42000000 (APB1Periph)
* HSE Frequency(Hz) = 25000000
* PLL_M = 25
* PLL_N = 336
* PLL_P = 2
* PLL_Q = 4
* VDD(V) = 3.3
* Flash Latency(WS) = 5
* 形 参: 无
* 返 回 值: 无
*********************************************************************************************************
*/
int SystemClock_Config(void)
{
RCC_ClkInitTypeDef RCC_ClkInitStruct;
RCC_OscInitTypeDef RCC_OscInitStruct;
/* 芯片内部的LDO稳压器输出的电压范围,选用的PWR_REGULATOR_VOLTAGE_SCALE1 */
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
/* 使能HSE,并选择HSE作为PLL时钟源 */
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = 25;
RCC_OscInitStruct.PLL.PLLN = 336;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLQ = 4;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
return 1;
}
/*
选择PLL的输出作为系统时钟
HCLK = SYSCLK / 1 (AHB1Periph)
PCLK2 = HCLK / 2 (APB2Periph)
PCLK1 = HCLK / 4 (APB1Periph)
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;
/* 此函数会更新SystemCoreClock,并重新配置HAL_InitTick */
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK)
{
return 1;
}
/* 使能SYS时钟和IO补偿 */
__HAL_RCC_SYSCFG_CLK_ENABLE() ;
HAL_EnableCompensationCell();
return 0;
}
36.4.5 第5步,配置文件FlashDev.c的实现
配置如下:
代码语言:javascript复制struct FlashDevice const FlashDevice = {
FLASH_DRV_VERS, /* 驱动版本,勿修改,这个是MDK定的 */
"ARMFLY_STM32F407_SPI_W25Q64", /* 算法名,添加算法到MDK安装目录会显示此名字 */
EXTSPI, /* 设备类型 */
0xC0000000, /* Flash起始地址 */
8 * 1024 * 1024, /* Flash大小,8MB */
4096, /* 编程页大小 */
0, /* 保留,必须为0 */
0xFF, /* 擦除后的数值 */
6000, /* 页编程等待时间 */
6000, /* 扇区擦除等待时间 */
4 * 1024, 0x000000, /* 扇区大小,扇区地址 */
SECTOR_END
};
注释已经比较详细,大家根据自己的需要做修改即可。注意一点,算法名ARMFLY_STM32F407_SPI_W25Q64会反馈到这个地方:
36.4.6 第6步,编程文件FlashPrg.c的实现
下面将文件中实现的几个函数为大家做个说明:
- 初始化函数Init
/*
*********************************************************************************************************
* 函 数 名: Init
* 功能说明: Flash编程初始化
* 形 参: adr Flash基地址,芯片首地址。
* clk 时钟频率
* fnc 函数代码,1 - Erase, 2 - Program, 3 - Verify
* 返 回 值: 0 表示成功, 1表示失败
*********************************************************************************************************
*/
int Init (unsigned long adr, unsigned long clk, unsigned long fnc)
{
int result = 0;
/* 系统初始化 */
SystemInit();
/* 时钟初始化 */
result = SystemClock_Config();
if (result != 0)
{
return 1;
}
bsp_InitSPIBus();
bsp_InitSFlash();
return 0;
}
- 复位初始化函数Uinit
擦除,编程和校验函数后都会调用此函数,我们这里未使用。
代码语言:javascript复制/*
*********************************************************************************************************
* 函 数 名: UnInit
* 功能说明: 复位初始化
* 形 参: fnc 函数代码,1 - Erase, 2 - Program, 3 - Verify
* 返 回 值: 0 表示成功, 1表示失败
*********************************************************************************************************
*/
int UnInit (unsigned long fnc)
{
return (0);
}
复位初始化这里,直接将其设置为内存映射模式。
- 整个芯片擦除函数EraseChip
如果大家配置勾选了MDK Option选项中此处的配置,会调用的整个芯片擦除:
实际应用中不推荐大家勾选这里,因为整个芯片擦除太耽误时间。
另外,如果大家的算法工程里面没有添加此函数,MDK会调用扇区擦除函数来实现,直到所有扇区擦除完毕。
代码语言:javascript复制/*
*********************************************************************************************************
* 函 数 名: EraseChip
* 功能说明: 整个芯片擦除
* 形 参: 无
* 返 回 值: 0 表示成功, 1表示失败
*********************************************************************************************************
*/
int EraseChip (void)
{
sf_EraseChip();
return 0;
}
- 扇区擦除函数EraseSector
如果大家配置勾选了MDK Option选项中此处的配置,会调用扇区擦除:
代码语言:javascript复制/*
*********************************************************************************************************
* 函 数 名: EraseSector
* 功能说明: 扇区擦除
* 形 参: adr 擦除地址
* 返 回 值: 无
*********************************************************************************************************
*/
int EraseSector (unsigned long adr)
{
adr -= SPI_FLASH_MEM_ADDR;
sf_EraseSector(adr);
return 0;
}
这里要注意两点:
(1) 程序里面的操作adr -= SPI_FLASH_MEM_ADDR,实际传递进来的地址是带了首地址的,即0xC0000000。特别注意,我们这里的0xC0000000是随意设置的,因为STM32F4的标准SPI外设并不支持内存映射。
(2) 这里执行的擦除大小要前面FlashDev.c文件中配置的扇区大小一致,这里是执行的4KB为扇区进行擦除。
- 页编程函数ProgramPage
页编程函数实现如下:
代码语言:javascript复制/*
*********************************************************************************************************
* 函 数 名: ProgramPage
* 功能说明: 页编程
* 形 参: adr 页起始地址
* sz 页大小
* buf 要写入的数据地址
* 返 回 值: 无
*********************************************************************************************************
*/
int ProgramPage (unsigned long adr, unsigned long sz, unsigned char *buf)
{
adr -= SPI_FLASH_MEM_ADDR;
sf_WriteBuffer(buf, adr, sz);
return (0);
}
这里注意两点:
(1) W25Q64的页大小是256字节,前面FlashDev.c中将页编程大小设置为4096字节,主要是方便擦除操作。
(2) 程序里面的操作adr -= SPI_FLASH_MEM_ADDR,实际传递进来的地址是带了首地址的,即0xC0000000。
- 读取和校验函数
校验函数实现如下:
代码语言:javascript复制/*
*********************************************************************************************************
* 函 数 名: Verify
* 功能说明: 校验
* 形 参: adr 起始地址
* sz 数据大小
* buf 要校验的数据缓冲地址
* 返 回 值: -
*********************************************************************************************************
*/
unsigned char aux_buf[4096];
unsigned long Verify (unsigned long adr, unsigned long sz, unsigned char *buf)
{
int i;
adr -= SPI_FLASH_MEM_ADDR;
sf_ReadBuffer(aux_buf, adr, sz);
for (i = 0; i< sz; i )
{
if (aux_buf[i] != buf[i])
return (adr i); /* 校验失败 */
}
adr = SPI_FLASH_MEM_ADDR;
return (adr sz); /* 校验成功 */
}
36.4.7 第7步,修改SPI Flash驱动文件(引脚,命令等)
最后一步就是SPI Flash(W25Q64)的驱动修改,大家可以根据自己的需求做修改。使用的引脚定义在文件bsp_spi_bus.c:
代码语言:javascript复制/*
*********************************************************************************************************
* 时钟,引脚,DMA,中断等宏定义
*********************************************************************************************************
*/
#define SPIx SPI1
#define SPIx_CLK_ENABLE() __HAL_RCC_SPI1_CLK_ENABLE()
#define DMAx_CLK_ENABLE() __HAL_RCC_DMA2_CLK_ENABLE()
#define SPIx_FORCE_RESET() __HAL_RCC_SPI1_FORCE_RESET()
#define SPIx_RELEASE_RESET() __HAL_RCC_SPI1_RELEASE_RESET()
#define SPIx_SCK_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE()
#define SPIx_SCK_GPIO GPIOB
#define SPIx_SCK_PIN GPIO_PIN_3
#define SPIx_SCK_AF GPIO_AF5_SPI1
#define SPIx_MISO_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE()
#define SPIx_MISO_GPIO GPIOB
#define SPIx_MISO_PIN GPIO_PIN_4
#define SPIx_MISO_AF GPIO_AF5_SPI1
#define SPIx_MOSI_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE()
#define SPIx_MOSI_GPIO GPIOB
#define SPIx_MOSI_PIN GPIO_PIN_5
#define SPIx_MOSI_AF GPIO_AF5_SPI1
硬件设置了之后,剩下就是SPI Flash相关的几个配置和片选引脚配置,在文件bsp_spi_flash.c:
主要是下面这几个:
代码语言:javascript复制/* 串行Flash的片选GPIO端口, PD13 */
#define SF_CS_CLK_ENABLE() __HAL_RCC_GPIOF_CLK_ENABLE()
#define SF_CS_GPIO GPIOF
#define SF_CS_PIN GPIO_PIN_8
#define SF_CS_0() SF_CS_GPIO->BSRR = ((uint32_t)SF_CS_PIN << 16U)
#define SF_CS_1() SF_CS_GPIO->BSRR = SF_CS_PIN
#define CMD_AAI 0xAD /* AAI 连续编程指令(FOR SST25VF016B) */
#define CMD_DISWR 0x04 /* 禁止写, 退出AAI状态 */
#define CMD_EWRSR 0x50 /* 允许写状态寄存器的命令 */
#define CMD_WRSR 0x01 /* 写状态寄存器命令 */
#define CMD_WREN 0x06 /* 写使能命令 */
#define CMD_READ 0x03 /* 读数据区命令 */
#define CMD_RDSR 0x05 /* 读状态寄存器命令 */
#define CMD_RDID 0x9F /* 读器件ID命令 */
#define CMD_SE 0x20 /* 擦除扇区命令 */
#define CMD_BE 0xC7 /* 批量擦除命令 */
#define DUMMY_BYTE 0xA5 /* 哑命令,可以为任意值,用于读操作 */
#define WIP_FLAG 0x01 /* 状态寄存器中的正在编程标志(WIP) */
36.5 SPI Flash的MDK下载算法使用方法
编译本章教程配套的例子,生成的算法文件位于此路径下:
36.5.1 下载算法存放位置
生成算法文件后,需要大家将其存到MDK安装目录,有两个位置可以存放,任选其一,推荐第2种:
- 第1种:存放到MDK的STM32F4软包安装目录里面:KeilSTM32F4xx_DFP2.15.0CMSISFlash(软包版本不同,数值2.15.0不同)。
- 第2种:MDK的安装目录 ARMFlash里面。
36.5.2 下载配置
注意这里一定要够大,否则会提示算法文件无法加载:
如果要下载程序到SPI Flash里面,需要做如下配置:
36.5.3 验证算法文件是否可以正常使用
为了验证算法文件是否可以正常使用,大家可以运行本教程第37章配套的例子。
36.6 实验例程说明
本章配套例子:V5-018_SPI Flash的MDK下载算法制作
编译后,算法文件会存到此路径下:
36.7 总结
本章节就为大家讲解这么多,为了熟练掌握,大家可以尝试自己实现一个Flash下载算法。