本文重点介绍了以SPI方式驱动外部Flash,并实现读写接口

1. Nor-Flash简介(以GD25Q127C为例)

1.1 SPI通信接口:

除了标准的SPI接口外,大部分Flash存储芯片还支持DSPI(Dual SPI 双线SPI)和QSPI(Quad SPI 四线SPI)接口。

SPI

  • 数据线:使用4条信号线,CS、CLK、MOSI、MISO
  • 传输模式:支持全双工通信,可以同时收发数据
  • 数据速率:本质是穿行通信,取决于时钟CLK的频率

DSPI

  • 数据线:将传统MOSI和MISO合并为双向数据线D0和D1,从而在单一时钟周期内可以传输2位的数据
  • 传输模式:一般为半双工,不能同时收发数据
  • 数据速率:时钟相同情况下,单向通信速率是传统SPI的2倍,双向通信与传统SPI相同

QSPI

  • 数据线:使用4条数据线,D0、D1、D2、D3、加上时钟线CLK和片选CS
  • 传输模式:半双工
  • 数据速率:时钟相同情况下,单想通信速率是传统SPI的4倍,由于QSPI设计就是为半双工服务,一般支持QSPI的设备支持更快的时钟

对存储的操作本质上是半双工的,对一块区域不可能既读有写,总有先后顺序,所以驱动片外Flash芯片非常适合使用QSPI来提高效率。

1.2 存储块的区别

外部闪存的存储空间大小的划分由大到小分为块、扇区、页三个单位,页是最小的操作存储器的单位。扇区由多个页组成,而块由多个扇区组成。

以GD25Q127C为例

Each Device has Each Block has Each Sector has Each Page has
16M 64K 4K 256 bytes
64K 256 16 - pages
4K 16 - - sectors
256 - - - blocks

页是存储器操作的最小单位,并且存储器遵循页、扇区和块的对齐:

  • 如果操作的数据不足256字节,且在同一个页范围内,256个字节都需要操作
  • 如果操作的数据横跨两个页的范围,需要分别对两个页的区域操作

1.3 Flash的写操作特指由“1”置“0”

无论是片外闪存还是片内(MCU内部),Flash的写或者烧录操作本质上是将特定位设置成“0”的过程;而擦除操作是将所有位设置为”1“。
所以,在对Flash进行烧录时,都是先擦除再写入,如果不预先擦除,原本存储位的”0“无法被置”1“。

以上可以参照《Flash原理&浮栅晶体管》。

1.4 片外Flash的操作方法

操作外部Flash的方法依赖于命令,通常有两种类型命令:

  • 命令
  • 命令+数据

使用频率最高的几个命令如下:

  • 写使能 Write Enable(06H)
  • 写禁止 Write Disable(04H)
  • 读数据 Read Data(03H)
  • 页写数据 Page Program(02H)
  • 扇区擦除 Sector Erase(20H)
  • 块擦除 Block Erase(D8H)
  • 整区擦除 Chip Erase(60H)
  • 复位使能 Enable Reset(66H)
  • 复位 Reset(99H)
  • 读芯片ID Read Identification(9FH)
  • 芯片序列号 Manufacturer Device ID(90H)

有关指令的详细用法可以参照GD23G127的数据手册

2. 标准SPI驱动程序

2.1 复位、读取ID、状态寄存器、写使能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
/**
* @brief Reset flash
*
*/
void spi_flash_reset(void) {
/* CS = 0 */
spi_flash_cs_low();
uint8 command = W25X_Reset;
/* Write One Byte */
spi_flash_write_bytes(&command, 1u);
/* CS = 1 */
spi_flash_cs_high();
}

/**
* @brief Read flash chip id
*
* @return uint32
*/
uint32 spi_flash_chip_id(void) {
uint8 temp[3];
uint32 identification = 0;

/* CS = 0 */
spi_flash_cs_low();
uint8 command = W25X_JedecDeviceID;
/* Write One Byte */
spi_flash_write_bytes(&command, 1u);
/* Read three bytes */
spi_flash_read_bytes((uint8*)&temp, 3u);
/* CS = 1 */
spi_flash_cs_high();

identification |= temp[0];
identification <<= 8;
identification |= temp[1];
identification <<= 8;
identification |= temp[2];

return identification;
}

/**
* @brief enable write operation
*
*/
static inline void spi_flash_write_enable(void) {
/* CS = 0 */
spi_flash_cs_low();
uint8 command = W25X_WriteEnable;
/* Write One Byte */
spi_flash_write_bytes(&command, 1u);
/* CS = 1 */
spi_flash_cs_high();
}

/**
* @brief Continuously read status reg until the operation is idle
*
*/
static inline void spi_flash_wait_idle(void) {
uint8 status = 0xff;
/* CS = 0 */
spi_flash_cs_low();
uint8 command = W25X_ReadStatusReg;
/* Write One Byte */
uint8 ret = spi_flash_write_bytes(&command, 1u);
/* if busy,waitting... */
uint16 time_out = 0;
while(time_out < 20000) {
/* Read one bytes */
ret = spi_flash_read_bytes(&status, 1u);
if((ret == HAL_OK) && (status & 0x1) == 0x0) {
break;
}
time_out++;
}
/* CS = 1 */
spi_flash_cs_high();
}

2.2 扇区擦除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* @brief erase sector
*
* @param sector_addr
*/
void spi_flash_sector_erase(uint32 sector_addr) {
uint8 ret = 0;
/* send write enable command */
spi_flash_write_enable();
spi_flash_wait_idle();
uint8 send_byte = W25X_SectorErase;
/* cs = 0 */
spi_flash_cs_low();
/* send sector erase command*/
ret = spi_flash_write_bytes(&send_byte, 1u);
/* send address high byte */
send_byte = (sector_addr & 0xFF0000) >> 16;
ret = spi_flash_write_bytes(&send_byte, 1u);
/* send address middle byte */
send_byte = (sector_addr & 0xFF00) >> 8;
ret = spi_flash_write_bytes(&send_byte, 1u);
/* send address low byte */
send_byte = sector_addr & 0xFF;
ret = spi_flash_write_bytes(&send_byte, 1u);
/* cs = 1 */
spi_flash_cs_high();
/* 等待擦除完毕*/
spi_flash_wait_idle();
}

2.3 读任意长度的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* @brief read specified length bytes from address
*
* @param pBuffer
* @param addr
* @param len
*/
void spi_flash_read(uint8* pBuffer, uint32 addr, uint32 len) {
uint8 ret = 0;
/* 选择FLASH: CS低电平 */
uint8 send_byte = W25X_ReadData;
spi_flash_cs_low();

/* 发送 读 指令 */
ret = spi_flash_write_bytes(&send_byte, 1u);

/* 发送 读 地址高位 */
send_byte = (addr & 0xFF0000) >> 16;
ret = spi_flash_write_bytes(&send_byte, 1u);
/* 发送 读 地址中位 */
send_byte = (addr & 0xFF00) >> 8;
ret = spi_flash_write_bytes(&send_byte, 1u);
/* 发送 读 地址低位 */
send_byte = addr & 0xFF;
ret = spi_flash_write_bytes(&send_byte, 1u);

spi_flash_read_bytes(pBuffer, len);

/* 停止信号 FLASH: CS 高电平 */
spi_flash_cs_high();
}

2.4 写任意长度的数据

最大写单位为页,一个页占256字节,如果写超过256字节的数据,需要分页写入。驱动上先实现写页数据,然后实现写任意长度数据。要实现写任意地址和任意长度的数据,一共需要分四类情况情况进行考虑:

  • 地址为页对齐地址,数据长度小于等于256
  • 地址为页对齐地址,数据长度大于256
  • 地址非页对齐地址,数据长度在小于所在页的剩余空间大小
  • 地址非页对齐地址,数据长度大于所在页剩余空间大小

基于上述四种情况,具体实现逻辑如下:

先判断页地址是否对齐:

Branch #1

Branch #1.1.2

Branh 2


代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
/**
* @brief page write, flash only 1->0, before writing, check sector erased
*
* @param pBuffer
* @param addr
* @param len
*/
void spi_flash_pagewrite(uint8* pBuffer, uint32 addr, uint16 len) {
uint8 ret = 0;
/* 发送FLASH写使能命令 */
spi_flash_write_enable();
uint8 send_byte = W25X_PageProgram;
/* 选择FLASH: CS低电平 */
spi_flash_cs_low();
/* 写送写指令*/
ret = spi_flash_write_bytes(&send_byte, 1u);
/*发送写地址的高位*/
send_byte = (addr & 0xFF0000) >> 16;
ret = spi_flash_write_bytes(&send_byte, 1u);
/*发送写地址的中位*/
send_byte = (addr & 0xFF00) >> 8;
ret = spi_flash_write_bytes(&send_byte, 1u);;
/*发送写地址的低位*/
send_byte = addr & 0xFF;
ret = spi_flash_write_bytes(&send_byte, 1u);

if (len > 256) {
len = 256;
#if LOG_ENABLE
LOG(ERROR, "SPI_FLASH_PageWrite too large!");
#endif
}

ret = spi_flash_write_bytes(pBuffer, len);

/* 停止信号 FLASH: CS 高电平 */
spi_flash_cs_high();

/* 等待写入完毕*/
spi_flash_wait_idle();
}

/**
* @brief write specified length bytes from address
*
* @param pBuffer
* @param addr
* @param len
*/
void spi_flash_write(uint8* pBuffer, uint32 addr, uint32 len) {
uint16 addr_t = 0, num_of_intact_page = 0, num_of_tail_bytes = 0, num_of_entry_bytes = 0;

addr_t = addr % SPI_FLASH_PAGE_SIZE;

/* address entry is not page aligned */
if (addr_t != 0) {
num_of_entry_bytes = SPI_FLASH_PAGE_SIZE - addr_t;
/* if entry page's remaining space less than bytes to write */
if(len > num_of_entry_bytes) {
/* write entry page first */
spi_flash_pagewrite(pBuffer, addr, num_of_entry_bytes);

len -= num_of_entry_bytes;
addr += num_of_entry_bytes;
pBuffer += num_of_entry_bytes;

/* Below process is same as page aligned*/

num_of_intact_page = len / SPI_FLASH_PAGE_SIZE;

if (num_of_intact_page == 0) {
/* write a page at address entry */
spi_flash_pagewrite(pBuffer, addr, len);
} else {
/* write intact pages */
while (num_of_intact_page--) {
spi_flash_pagewrite(pBuffer, addr, SPI_FLASH_PAGE_SIZE);
addr += SPI_FLASH_PAGE_SIZE;
pBuffer += SPI_FLASH_PAGE_SIZE;
}

uint16 num_of_tail_bytes = len % SPI_FLASH_PAGE_SIZE;
/* if needed, write the last page*/
if(num_of_tail_bytes != 0) {
spi_flash_pagewrite(pBuffer, addr, num_of_tail_bytes);
}
}
}
/* if entry page's remaining space covers bytes to write */
else {
spi_flash_pagewrite(pBuffer, addr, len);
}
}
/* address entry is page aligned */
else {
// num_of_entry_bytes = SPI_FLASH_PAGE_SIZE - addr_t;
num_of_intact_page = len / SPI_FLASH_PAGE_SIZE;

if (num_of_intact_page == 0) {
/* write a page at address entry */
spi_flash_pagewrite(pBuffer, addr, len);
} else {
/* write intact pages */
while (num_of_intact_page--) {
spi_flash_pagewrite(pBuffer, addr, SPI_FLASH_PAGE_SIZE);
addr += SPI_FLASH_PAGE_SIZE;
pBuffer += SPI_FLASH_PAGE_SIZE;
}

uint16 num_of_tail_bytes = len % SPI_FLASH_PAGE_SIZE;
/* if needed, write the last page*/
if(num_of_tail_bytes != 0) {
spi_flash_pagewrite(pBuffer, addr, num_of_tail_bytes);
}
}
}
}

上述代码的完整流程图如下: