需要的软件:
Keil
STM32CubeMX
J-Flash
参考文档:
方法1:在Keil中点击Help→uVision Help,然后再搜索框中输入FLM,点击列出主题,可以看到生成下载算法的大致步骤:
方法2:在ARM Keil官网,搜索KAN333,可以找到生成算法说明的PDF文档以及例程源码。链接
方法3:在Keil安装路径下Keil_v5\ARM\Flash_Template,找到Abstract.txt,打开后有一个生成算法说明的文档。链接
先说一下大致原理,生成的算法文件也就是FLM文件,实际上会先下载进你板子的RAM中,然后在板子的RAM中运行,进而去写外部Nor Flash或者片内Flash。
参考上面任意一个说明文档都可以,但是不要完全按照上面描述的来,我之前是按照第二个方法下载的pdf来的,但是折腾了两天才有结果,下面是具体实现的方法。
1.复制Keil安装路径下Keil_v5\ARM\Flash_Template文件夹,并重命名为自己的算法工程名,我这以STM32L4xx_W25Q64为例。
2.将文件夹的只读属性取消,并将keil工程的名字改为自己的工程名。
3.打开工程,选择魔术棒,在Device一栏中选择自己板子上对应的MCU,在Target一栏中ARM Compiler保持默认V5就好,不用像PDF中描述的一样改成V6的,两个版本用的编译方法不一样,V6版本的生成的FLM文件小一点,但是好像没办法使用MicroLIB,会报错。
在Output一栏中改为自己工程的名字
User一栏中不要动,cmd.exe /C copy “Objects%L” ".@L.FLM"在工程编译后会执行。
C/C++和Asm中确认Read-Only Position Independent和Read-Write Position Independent勾选,表明这个是与位置无关的算法,因为要放在RAM中执行。C/C++一栏中Define中对应填入需要的宏,代码量不大的话编译器优化等级可以设置为0,后边会提到代码量具体有什么限制。
Linker一栏中使用自定义分散加载文件,保持默认就好。
4.打开Manage Run-Time Environment,勾选如下内容,并点击绿色按钮,启动STM32CubeMX。
5.在STM32CubeMX中配置工程,先配置为自己需要的时钟频率,不要像PDF中一样保持默认,不然的话时钟频率只有4M。
然后配置好SPI,GPIO等,如果有需要的话也可以配置一下串口用于输出Log,我是配置了两个LED灯,一个用于指示擦除,一个用于指示编写。
在Project Manager中,Project一栏中不用像PDF中描述的勾选Do not generate the main(),这样会生成main函数,可以用于测试生成的代码是否可用,不用自己写。
Code Generator一栏中按如下勾选:
Advanced Settings一栏保持函数调用。
然后点击Generate Code生成代码。
6.在STM32L4xx_W25Q64\RTE\Device\STM32L431CCTx\STCubeGenerated\MDK-ARM路径下,打开生成的的工程,然后可以在里面测试一下生成的工程,添加一下自己的读写外部Nor Flash的代码,能正常操作就行。
7.回到生成算法的工程里面,把main.c中的main函数屏蔽掉,否则在生成算法文件的时候会报错。此外也不要像PDF中描述的重新定义HAL_InitTick(),HAL_Delay()等几个函数,因为使用HAL库的话用到的一些超时检测什么的都会有影响。
8.在Device目录右键Options for Component Class ‘Device’,选择启动文件,取消勾选Include in target build。
9.新建一个目录,添加自己操作外部Nor Flash的代码和System文件。
10.实现FlashDev.c,这个是设备描述文件,改为自己合适的就好。
Device Type根据实际设置。
Device Start Address可以直接从0x00000000开始,这样就不用在FlashPrg.c中额外处理地址。
Programming Page Size根据自己的外部Nor Flash设置,比如W25Q64每页256字节,设置为256的话一个扇区就需要多次调用ProgramPage()函数,所以可以直接设置成一个扇区大小(4096),这样调用ProgramPage()函数的时候直接写一个扇区。
Specify Size and Address of Sectors指的是扇区划分,比如有的MCU有不同大小的扇区,第一个参数就是扇区大小,第二个参数就是扇区起始地址;比如W25Q64,每个扇区都是16个页,每个页256字节,所以第一个参数就是0x001000(4096),第二个表示从0地址开始,如果有其他的扇区大小类型,在SECTOR_END之前添加。
Erase Sector Timeout为擦除超时时间,全片擦除也受这个参数影响,W25Q64全片擦除需要14s左右,所以适当设置大一点。
10.实现FlashPrg.c,必须要实现的函数是Init,UnInit,EraseChip,EraseSector,ProgramPage。可根据需要实现的函数Verify,BlankCheck。
Init函数需要初始化时钟,GPIO,SPI等,正确执行返回0,执行错误返回1。不要像PDF中一样关中断,因为需要用到滴答定时器中断。
int Init (unsigned long adr, unsigned long clk, unsigned long fnc) {
int ret = 0;
volatile int i;
volatile unsigned char * ptr = (volatile unsigned char * )&hspi2;
for (i = 0; i < sizeof(hspi2); i++) {
*ptr++ = 0U;
}
SystemInit();
HAL_Init();
SystemClock_Config();
MX_GPIO_InitLed();
if(W25Q64_Init() != W25Q64_ID)
{
ret = 1;
}
return (ret); // Finished without Errors
}
UnInit函数可以什么都不做,因为我在擦除和写的时候翻转LED,所以添加了关LED操作。
int UnInit (unsigned long fnc) {
HAL_GPIO_WritePin(Prog_Led_GPIO_Port,Prog_Led_Pin,GPIO_PIN_RESET);
HAL_GPIO_WritePin(Erase_Led_GPIO_Port,Erase_Led_Pin,GPIO_PIN_RESET);
return (0); // Finished without Errors
}
擦除分为全片擦除和擦除扇区,流程图如下:
EraseChip函数直接调用自己的全片擦除函数。
int EraseChip (void) {
HAL_GPIO_TogglePin(Erase_Led_GPIO_Port,Erase_Led_Pin);
W25Q64_Erase_Chip();
return (0); // Finished without Errors
}
EraseSector 函数直接调用自己的擦除扇区函数。
int EraseSector (unsigned long adr) {
HAL_GPIO_TogglePin(Erase_Led_GPIO_Port,Erase_Led_Pin);
W25Q64_Erase_Sector((uint32_t)adr);
return (0); // Finished without Errors
}
ProgramPage函数直接调用自己的写扇区函数。要写的大小跟FlashDev.c中的Programming Page Size大小一致。写Flash流程图如下:
int ProgramPage (unsigned long adr, unsigned long sz, unsigned char *buf) {
HAL_GPIO_TogglePin(Prog_Led_GPIO_Port,Prog_Led_Pin);
// W25Q64_Write_Page(buf,(uint32_t)adr,(uint16_t)sz);
W25Q64_Write_Sector(buf,(uint32_t)adr,(uint16_t)sz);
return (0); // Finished without Errors
}
Verify和BlankCheck函数可以不实现。
Verify函数作用是判断写成功了多少字节数据,这个值跟FlashDev.c中Device Start Address值有关,如果Device Start Address值不为0,那么就是Device Start Address加上写成功了多少字节。校验函数流程图:
unsigned long Verify (unsigned long adr, unsigned long sz, unsigned char *buf) {
uint32_t i;
W25Q64_ReadData(W25Q64_BufferA,(uint32_t)adr,(uint16_t)sz);
for (i = 0; i < sz; i++)
{
if (W25Q64_BufferA[i] != buf[i])
{
return (adr + i);
}
}
return (adr + sz);
}
BlankCheck函数作用是在擦除块的时候判断一下这个块需不需要擦除,实际上就是比较块内的值是不是都是FlashDev.c中定义的Initial Content of Erased Memory值一样,如果有一个字节不是,说明块需要擦除。
int BlankCheck (unsigned long adr, unsigned long sz, unsigned char pat) {
uint32_t i, j;
uint32_t read_len = 0;
if((adr + sz) > W25Q64_TOTAL_NUM)
{
return (1);
}
for (i = 0; i < sz; i += W25Q64_SECTOR_NUM)
{
if(sz >= W25Q64_SECTOR_NUM)
{
read_len = W25Q64_SECTOR_NUM;
}
else
{
read_len = sz;
}
W25Q64_ReadData(W25Q64_BufferA,(adr+i),read_len);
for(j = 0; j < read_len; j++)
{
if (W25Q64_BufferA[j] != pat)
return (1);
}
sz -= read_len;
}
return (0); // Memory is blank
}
然后编译之后就会生成下载算法文件FLM。注意不要添加太多没用的调试代码,否则会导致生成的代码量太大,就不得不提高优化等级,还可能导致生成的算法文件使用时报错。可以在编译之后通过map文件确认一下代码占用的空间,不能超过RAM的大小,因为算法是需要在RAM中运行的。
如果算法代码量太大,可能会导致报错RAM area configured for this target is too small.
11.将生成的下载算法文件复制到Keil_v5\ARM\Flash路径下,就可以在KEIL中看到。
12.在J-link安装目录下SEGGER\JLink_V640\Devices中新建一个文件夹,比如命名为W25Qxx,然后把下载算法文件复制过来。
13.在SEGGER\JLink_V640路径下打开JLinkDevices文件,在末尾添加如下内容。
<!-- -->
<!-- W25QXX -->
<!-- -->
<Device>
<ChipInfo Vendor="W25QXX" Name="STM32L4xx_W25Q64" Core="JLINK_CORE_CORTEX_M4" WorkRAMAddr="0x20000000" WorkRAMSize="0x0000C000"/>
<FlashBankInfo Name="SPI Flash" BaseAddr="0x00000000" MaxSize="0x00800000" Loader="Devices/W25Qxx/STM32L4xx_W25Q64.FLM" LoaderType="FLASH_ALGO_TYPE_OPEN" AlwaysPresent="1"/>
</Device>
WorkRAMAddr和WorkRAMSize就是自己MCU的RAM参数,BaseAddr和MaxSize设置的和FlashDev.c中一样。
之后就可以使用J-Flash直接把数据烧写进外部Nor Flash中。
测试:
使用J-Flash新建工程,选择下载算法文件。
全片擦除:
擦除扇区:
写入:
Flash为空:
Flash不为空:
写入并校验: