第二十五章 FLASH
目录
1 W55MH32 FLASH 简介
WMH32的闪存模块由主存储器、信息块和闪存存储器接口寄存器等 3 部分组成。
主存储器,该部分用来存放代码和数据常数(如 const 类型的数据)。对于大容量产品,其被划分为 256 页,每一页 2K 字节。注意,小容量和中容量产品则每页只有 1K 字节。从上图可以看出主存储器的起始地址就是 0x08000000,B0、B1 都接 GND 的时候,就是从0x08000000 开始运行代码的。
信息块,该部分分为 2 个小部分,其中启动程序代码,用来存储 ST 自带的启动程序,用来串口下载代码,当 B0 接 3V3,B1 接 GND 的时候,运行的就是这部分代码。用户选中字节,则一般用于配置写保护、读保护等功能,本章不作介绍了。
闪存存储器接口寄存器,该部分用于控制闪存读写等,是整个闪存模块的控制结构。对主存储器和信息块的写入由内嵌的闪存编程/擦除控制器(FPEC)管理;编程与擦除的高电压由内部产生。在执行闪存写操作时,任何对闪存的读操作都会锁住总线,在写操作完成后读操作才能正确地进行。既在进行写或擦除操作时,不能进行代码或数据的读取操作。
型号 | W55MH32L | W55MH32Q |
Flash(KB) | 1024 | 1024 |
SRAM(KB) | 96 | 96 |
高级定时器 | 2 | 2 |
通用定时器 | 10 | 10 |
基本定时器 | 2 | 2 |
SPI | 2 | 2 |
I2C | 2 | 2 |
USART/UART | 5 | 3 |
USB | 1 | 1 |
CAN | 1 | 1 |
SDIO | 1 | - |
Ethernet | 1 | 1 |
GPIO 端口 | 66 | 36 |
12 位 ADC(通道数) | 3(12 个通道) | 3(12 个通道) |
12 位 DAC(通道数) | 2(2 个通道) | 2(2 个通道) |
随机数模块 | 支持 | 支持 |
硬件加密算法单元 | 支持 | 支持 |
页大小(K 字节) | 4 | 4 |
CPU 频率 | 216M | 216M |
工作电压 | 2.0 - 3.6V | 2.0 - 3.6V |
工作温度 | -40 - +85℃ | -40 - +85℃ |
2 闪存的读取
内置闪存模块可以在通用地址空间直接寻址,任何 32 位数据的读操作都能访问闪存模块的内容并得到相应的数据。读接口在闪存端包含一个读控制器,还包含一个 AHB 接口与 CPU衔接。这个接口的主要工作是产生读内存的控制信号并预取 CPU 要求的指令块,预取指令块仅用于在 I-Code 总线上的取指操作,数据常量是通过 D-Code 总线访问的。这两条总线的访问目标是相同的闪存模块,访问 D-Code 将比预取指令优先级高。
这里要特别留意一个闪存等待时间,因为 CPU 运行速度比 FLASH 快得多,W55MH32的 FLASH 最快访问速度≤24Mhz,如果 CPU 频率超过这个速度,那么必须加入等待时间,比如我们一般使用 72Mhz 的主频,那么 FLASH 等待周期就必须设置为 2,该设置通过FLASH_ACR 寄存器设置。
例如,我们要从地址 addr,读取一个半字(半字为 16 位,字为 32 位),可以通过如下的
语句读取:
data = *(vu16*)addr;
将 addr 强制转换为 vu16 指针,然后取该指针所指向的地址的值,即得到了 addr 地址的值。类似的,将上面的 vu16 改为 vu8,即可读取指定地址的一个字节。相对 FLASH 读取来说,W55MH32 FLASH 的写就复杂一点了。
3 闪存的编程和擦除
W55MH32 的闪存编程是由 FPEC(闪存编程和擦除控制器)模块处理的,这个模块包含 7 个 32位寄存器,它们分别是:
- FPEC 键寄存器(FLASH_KEYR)
- 选择字节键寄存器(FLASH_OPTKEYR)
- 闪存控制寄存器(FLASH_CR)
- 闪存状态寄存器(FLASH_SR)
- 闪存地址寄存器(FLASH_AR)
- 选择字节寄存器(FLASH_WRPR)
其中 FPEC 键寄存器总共有 3 个键值:
- RDPRT 键 = 0X0000 00A5
- KEY1 = 0X4567 0123
- KEY2 = 0XCDEF 89AB
W55MH32 复位后,FPEC 模块是被保护的,不能写入 FLASH_CR 寄存器;通过写入特定的序列到 FLASH_KEYR 寄存器可以打开 FPEC 模块(即写入 KEY1 和 KEY2),只有在写保护被解除后,我们才能操作相关寄存器。
W55MH32 闪存的编程每次必须写入 16 位(不能单纯的写入 8 位数据),当 FLASH_CR 寄存器的 PG 位为‘1’时,在一个闪存地址写入一个半字将启动一次编程;写入任何非半字的数据,FPEC 都会产生总线错误。在编程过程中(BSY 位为’1’),任何读写内存的操作都会使CPU 暂停,直到此次闪存编程结束。同样,W55MH32 的 FLASH 在编程的时候,也必须要求其写入地址的 FLASH 是被擦除了的(其值必须是 0xFFFF),否则无法写入,在 FLASH_SR 寄存器的 PGERR 位将得到一个警告。W55MH32 的 FLASH 编程过程如图所示:
闪存编程过程
从上图可以得到闪存的编程顺序如下:
1)检查 FLASH_CR 的 LOCK 是否解锁,如果没有则先解锁
2)检查 FLASH_SR 寄存器的 BSY 位,以确认没有其他正在进行的编程操作
3)设置 FLASH_CR 寄存器的 PG 位为‘1’
4)在指定的地址写入要编程的半字
5)等待 BSY 位变为‘0’
6)读出写入地址并验证数据
前面提到,我们在 W55MH32 的 FLASH 编程的时候,要先判断缩写地址是否被擦出了,所以,我们有必要再介绍一下 W55MH32 的闪存擦除,W55MH32 的闪存擦除分为两种:页擦除和整片擦除。页擦除过程如图 所示:
闪存页擦除过程
从上图可以看出,W55MH32 的页擦除顺序为:
1)检查 FLASH_CR 和 LOCK 是否解锁,如果没有则先解锁
2)检查 FLASH_SR 寄存器的 BSY 位,以确认没有其他正在进行的闪存操作
3)设置 FLASH_CR 寄存器的 PER 位为‘1’
4)用 FLASH_AR 寄存器选择要擦除的页
5)设置 FLASH_CR 寄存器的 STRT 位为‘1’
6)等待 BSY 位变为‘0’
7)读出被擦除的页并做验证
本章我们只用到了 W55MH32 页擦除功能,整片擦除功能我们在这里就不介绍了。
4 FLASH 寄存器
通过上面的讲解,我们基本对 W55MH32 闪存的读写执行步骤有所了解。接下来,我们介绍本实验需要用到的一些 FLASH 寄存器。
4.1 FPEC 键寄存器(FLASH_KEYR)
FPEC 键寄存器描述如图 所示:
FLASH_KEYR 寄存器
该寄存器主要用来解锁 FPEC,必须在该寄存器写入特定的序列(KEY1 和 KEY2)解锁后,才能对 FLASH_CR 寄存器进行写操作。
4.2 FLASH 控制寄存器(FLASH_CR)
FLASH 控制寄存器描述如图所示:
图 FLASH_CR 寄存器
该寄存器我们本章只用到了它的 LOCK、STRT、PER 和 PG 等 4 个位。LOCK 位,该位用于指示 FLASH_CR 寄存器是否被锁住,该位在检测到正确的解锁序列后,硬件将其清零。在一次不成功的解锁操作后,在下次系统复位之前,该位将不再改变。
STRT 位,该位用于开始一次擦除操作。在该位写入 1,将执行一次擦除操作。
PER 位,该位用于选择页擦除操作,在页擦除的时候,需要将该位置 1。
PG 位,该位用于选择编程操作,在往 FLASH 写数据的时候,该位需要置 1。
其他位,我们就不在这里介绍了,请大家参考《W55MH32xxx参考手册》。
4.3 闪存状态寄存器(FLASH_SR)
闪存状态寄存器描述如图所示:
图 FLASH_SR 寄存器
该寄存器主要用来指示当前 FPEC 的操作编程状态。由于寄存器中描述比较详细,这里就不重复了。
4.4 闪存地址寄存器(FLASH_AR)
闪存地址寄存器描述如图所示:
图FLASH_AR 寄存器
该寄存器在本章,我们主要用来设置要擦除的页。关 于 W55MH32 FLASH 的 介 绍 , 我 们 就 介 绍 到 这 里 。 更 详 细 的 介 绍 , 可 以 参 考《W55MH32参考手册》。
5 例程设计
5.1 FLASH_Eeprom
该通过例程串口输出系统时钟信息和测试提示,在无限循环里不断对 WIZnet FLASH 进行读写操作,并且把读取的数据通过串口输出,以此实现对 FLASH 的读写测试。
5.1 函数声明
声明了UART_Configuration()函数,其功能是配置串口参数。
5.2 常量与数组定义
定义了一个常量字符串数组TEXT_Buffer,内容为"WIZnet FLASH TEST"。
借助SIZE宏定义获取数组长度。
设定了FLASH_SAVE_ADDR常量,此为 FLASH 的保存地址。
5.3 主函数main()
int main(void)
{
uint8_t datatemp[SIZE], i;
RCC_ClocksTypeDef clocks;
delay_init();
UART_Configuration(115200);
RCC_GetClocksFreq(&clocks);
printf("\n");
printf("SYSCLK: %3.1fMhz, HCLK: %3.1fMhz, PCLK1: %3.1fMhz, PCLK2: %3.1fMhz, ADCCLK: %3.1fMhz\n",
(float)clocks.SYSCLK_Frequency / 1000000, (float)clocks.HCLK_Frequency / 1000000,
(float)clocks.PCLK1_Frequency / 1000000, (float)clocks.PCLK2_Frequency / 1000000, (float)clocks.ADCCLK_Frequency / 1000000);
printf("FLASH EEPROM Tset.\n");
while (1)
{
WIZFLASH_Write(FLASH_SAVE_ADDR, (u16 *)TEXT_Buffer, SIZE);
WIZFLASH_Read(FLASH_SAVE_ADDR, (u16 *)datatemp, SIZE);
for (i = 0; i < SIZE; i++)
{
printf("%s\n", datatemp);
}
memset(datatemp, 0x00, sizeof(datatemp));
delay_ms(1000);
}
}
定义了一个用于存储从 FLASH 读取数据的数组datatemp,以及一个循环变量i。
初始化延迟函数、串口配置,并且获取系统时钟频率。
通过printf()函数输出系统时钟频率信息以及测试提示信息。
进入一个无限循环:调用WIZFLASH_Write()函数把TEXT_Buffer数组内容写入到指定的 FLASH 地址。
- 调用WIZFLASH_Read()函数从该地址读取数据到datatemp数组。
- 运用for循环遍历datatemp数组,然后通过printf()函数输出数组内容。
- 调用memset()函数把datatemp数组清零。
- 调用delay_ms()函数延迟 1 秒。
5.4 串口配置函数UART_Coniguration()
void UART_Configuration(uint32_t bound)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
USART_InitStructure.USART_BaudRate = bound;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART_TEST, &USART_InitStructure);
USART_Cmd(USART_TEST, ENABLE);
}
对 GPIO 和 USART 的初始化结构体进行定义。
使能 USART1 和 GPIOA 的时钟。
配置 GPIOA 的引脚 9 为复用推挽输出模式,引脚 10 为浮空输入模式。
对 USART 的波特率、数据位、停止位、奇偶校验等参数进行配置。
初始化 USART 并使能。
5.5 字符发送函数SER_PutChar()
int SER_PutChar(int ch)
{
while (!USART_GetFlagStatus(USART_TEST, USART_FLAG_TC));
USART_SendData(USART_TEST, (uint8_t)ch);
return ch;
}
等待串口发送完成标志位USART_FLAG_TC置位。
把字符发送到串口。
5.6 重定向函数fputc()
若要发送的字符是换行符\n,则先发送回车符\r。
调用SER_PutChar()函数发送字符。
6 下载验证
6.1 FLASH_Eeprom
进入主循环后,程序会不断执行以下操作:
- 把TEXT_Buffer数组中的字符串 "WIZnet FLASH TEST" 写入到指定的 FLASH 地址。
- 从该 FLASH 地址读取数据到datatemp数组。
- 借助printf()函数将datatemp数组的内容通过串口输出。
由于for循环的存在,每次读取到的数据会被重复输出SIZE次。不过,原代码里printf("%s\n", datatemp);存在错误,它会把整个datatemp数组当作字符串输出,而不是逐个字符输出。正确的做法应该是printf("%c", datatemp[i]);。修正后,串口会每隔 1 秒输出一次 "WIZnet FLASH TEST"。