【GD32】史上最优裸机版NOR Flash驱动 | 快速读+写操作页对齐+擦除扇区对齐+保留数据擦除(GD32F470ZGT6)

前言

        在单片机应用中,我们经常使用外部Flash来存储用户数据,NOR Flash是当中最常用的,一片几块钱的NOR Flash就能够提供好几MB的空间。NOR Flash一般采用SPI接口来通讯;当然,市面上大部分的NOR Flash都支持QPI甚至OPI接口,能大大提高数据传输速率。

        在平时的开发过程中为了追求开发速度,都会直接去网上copy一份别人写的驱动,但对于我这个完美主义者来说,网上99%的驱动都没有达到我的要求,很多人的驱动都是追求简单的能用即可,但我总是会希望能用尽硬件的性能,榨干硬件的性能!!!

        因此经过一段时间的研究,我写了这个应该是史上最优的NOR Flash驱动了。相比于常见的写法,主要有以下几个改进:

  1. 读操作使用快速读命令;
  2. 写操作支持页对齐;
  3. 擦除操作支持扇区对齐;
  4. 擦除操作支持非扇区对齐区域。

硬件

开发板:立创梁山派(GD32F470ZGT6)

NOR Flash:W25Q64JVSTIQ

代码

初始化

uint32_t flash_init(void)
{
	/* 初始化RCU */
    rcu_periph_clock_enable(RCU_GPIOF);
    rcu_periph_clock_enable(RCU_SPI4);

	/* 初始化GPIO */
    gpio_af_set(GPIOF, GPIO_AF_5, GPIO_PIN_7 | GPIO_PIN_8 | GPIO_PIN_9);
    gpio_mode_set(GPIOF, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_PIN_7 | GPIO_PIN_8 | GPIO_PIN_9);
    gpio_output_options_set(GPIOF, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, GPIO_PIN_7 | GPIO_PIN_8 | GPIO_PIN_9);

	FLASH_CS_HIGH();
    gpio_mode_set(GPIOF, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_PIN_6);
    gpio_output_options_set(GPIOF, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, GPIO_PIN_6);

	/* 初始化SPI */
    spi_parameter_struct spi_init_struct = {0};

    spi_init_struct.trans_mode           = SPI_TRANSMODE_FULLDUPLEX;  // 全双工
    spi_init_struct.device_mode          = SPI_MASTER;  // 主机模式
    spi_init_struct.frame_size           = SPI_FRAMESIZE_8BIT;  // 8bit数据
    spi_init_struct.clock_polarity_phase = SPI_CK_PL_HIGH_PH_2EDGE;  // 空闲高电平,第2上升沿采集
    spi_init_struct.nss                  = SPI_NSS_SOFT;  // 软件片选
    spi_init_struct.prescale             = SPI_PSC_2;  // 2分频,SPI时钟频率 = 120MHz / 2 = 60MHz
    spi_init_struct.endian               = SPI_ENDIAN_MSB;  // 高位在前
    spi_init(SPI4, &spi_init_struct);
	
	spi_enable(SPI4);
	
	/* 读芯片ID */
	uint32_t id = 0;
	
	flash_wait_busy();
	FLASH_CS_LOW();
	flash_transfer(0x9F);
	((uint8_t *)&id)[2] = flash_transfer(FLASH_DUMMY);
	((uint8_t *)&id)[1] = flash_transfer(FLASH_DUMMY);
	((uint8_t *)&id)[0] = flash_transfer(FLASH_DUMMY);
	FLASH_CS_HIGH();
	
	return id;
}

        初始化和正常的写法差不多,初始化时钟、GPIO和SPI。这颗Flash的最高运行速率为133MHz,为了使用最高速率,GPIO的速率要配置成GPIO_SPEED_MAX,这样可以允许GPIO速率超过50MHZ。

        我们板载这颗Flash是用SPI4,挂载在APB2总线,总线最高速率为120MHz,但SPI的分频最低也要2分频,所以在这里Flash的速率最高只能到60MHz

        片选线选择软件片选,配置GPIO的时候建议选择上拉推挽模式。片选线的控制简单用宏定义实现。

#define FLASH_CS_LOW() gpio_bit_reset(GPIOF, GPIO_PIN_6)
#define FLASH_CS_HIGH() gpio_bit_set(GPIOF, GPIO_PIN_6)

        时序我选择的是模式3,即空闲高电平,第2个上升沿采集;一般的Flash同时也支持模式0,这里根据需求配置即可。

        初始化后会读一次Flash ID并作为返回,用户可以根据ID值判断Flash的型号和是否正常初始化。

忙等待

static void flash_wait_busy(void)
{
	FLASH_CS_LOW();
	/* 读状态寄存器命令 */
	flash_transfer(0x05);
	while (flash_transfer(FLASH_DUMMY) & 0x01);
	FLASH_CS_HIGH();
}

        在Flash的任何时候都可以读它的状态寄存器,通常我们只管注它的忙标志,位于第一个寄存器位,在进行写或擦除操作时需要根据忙标志判断操作是否完成。发送读状态寄存器命令后,Flash会发送寄存器的值,程序会阻塞直至忙标志复位。

        这里跟常见的写法有一个不同,读状态寄存器指令其实是可以连续读的,只要我们不结束接收那么Flash就会不断地发送寄存器的值给我们,所以这样可以省下重新拉低片选和发送命令的时间。

全双工传输

static uint8_t flash_transfer(uint8_t val)
{
	while (RESET == spi_i2s_flag_get(SPI4, SPI_FLAG_TBE));
	spi_i2s_data_transmit(SPI4, val);
	while (RESET == spi_i2s_flag_get(SPI4, SPI_FLAG_RBNE));
	return (uint8_t) spi_i2s_data_receive(SPI4);
}

        这个没什么特别的,就是经典的写法。

写使能

static void flash_write_enable(void)
{
	FLASH_CS_LOW();
	flash_transfer(0x06);
	FLASH_CS_HIGH();
}

         Flash是默认开启写禁止的,所以在写操作和操作操作前记得开启写使能。

读数据

void flash_read(uint32_t addr, uint8_t *buf, uint32_t len)
{
	flash_wait_busy();
	
	FLASH_CS_LOW();
	
	/* 快速读命令 */
	flash_transfer(0x0B);
	/* 写地址 */
	flash_transfer((uint8_t)(addr >> 16));
	flash_transfer((uint8_t)(addr >> 8));
	flash_transfer((uint8_t)addr);
	/* 空操作 */
	flash_transfer(FLASH_DUMMY);
	/* 读数据 */
	for (uint32_t i = 0; i < len; i++) {
		buf[i] = flash_transfer(FLASH_DUMMY);
	}
	
	FLASH_CS_HIGH();
}

         常规的读操作都是使用0x03这个命令,但有一个问题是Flash的读操作是要比写操作要慢的多的,以我这颗Flash为例,它的写操作最高可达133MHz,但普通的读操作只能达到50MHz

        所以这里我使用快速读指令(0x0B),这样可以使读操作的速率也能达到133MHz,不过代价就是在接收数据前需要给一个空操作,之后才能开始接收数据,这个空操作主要是给Flash时间调整内部时钟电路,下面是数据手册给出的时序图。

写数据

void flash_write(uint32_t addr, const uint8_t *buf, uint32_t len, bool erase)
{
	if (erase) {
		flash_erase(addr, len, true);
	}

	flash_write_enable();
	flash_wait_busy();
	
	uint32_t idx = 0;
	
	/* 页对齐 */
	if (addr % FLASH_PAGE_SIZE) {
		uint32_t cnt = FLASH_PAGE_SIZE - (addr % FLASH_PAGE_SIZE);
		FLASH_CS_LOW();
		/* 写命令 */
		flash_transfer(0x02);
		/* 写地址 */
		flash_transfer((uint8_t)(addr >> 16));
		flash_transfer((uint8_t)(addr >> 8));
		flash_transfer((uint8_t)addr);
		/* 写数据 */
		while (idx < cnt) {
			flash_transfer(buf[idx++]);
		}
		FLASH_CS_HIGH();
		/* 等待写入完成 */
		flash_wait_busy();
	}
	
	while (idx < len) {
		FLASH_CS_LOW();
		/* 写命令 */
		flash_transfer(0x02);
		/* 写地址 */
		flash_transfer((uint8_t)(addr >> 16));
		flash_transfer((uint8_t)(addr >> 8));
		flash_transfer((uint8_t)addr);
		/* 写数据 */
		uint32_t cnt = (len - idx >= FLASH_PAGE_SIZE ? FLASH_PAGE_SIZE : len - idx);
		for (uint32_t i = 0; i < cnt; i++) {
			flash_transfer(buf[idx++]);
		}
		FLASH_CS_HIGH();
		/* 等待写入完成 */
		flash_wait_busy();
	}
}

         Flash的写是按页进行的,一般一页就是256字节。网上大部分的写法是没有考虑写入时的页对齐的,这会导致如果传入一个没有页对齐的地址那么写入的数据将有可能不是我们所期望的。根据数据手册的说法,如果传入的起始地址没有页对齐,即最后8位不是全0的情况,Flash也是会正常写入的,但前提是不能超过这个地址所在页的剩余空间,要是超过了这个页的大小那么数据会回到该页的起始地址进行写入

        所以我这里先判断地址有没有页对齐,如果没有,就先写入没对齐的那页数据,然后后面的数据都可以按页来写入了;当然还要处理一下如果最后一页不足256字节的情况。

        写操作这里我还加了一个预擦除选项,就是最后一个erase参数。我们都知道,要往有数据的区域写入新的数据要先擦除再写入;但一般我们都不管三七二十一,反正擦了再写肯定是没问题的,正常调用就默认都预擦除就行了。

擦除数据

void flash_erase(uint32_t addr, uint32_t len, bool reserve)
{
	if (reserve) {
		/* 保存非擦除区域数据 */
		uint8_t *l_buf = NULL;
		uint32_t l_offset = addr % FLASH_SECTOR_SIZE;
		uint32_t l_addr = addr - l_offset;
		if (l_offset) {
			l_buf = (uint8_t *) malloc(l_offset);
			flash_read(l_addr, l_buf, l_offset);
		}
		
		uint8_t *r_buf = NULL;
		uint32_t r_addr = addr + len;
		uint32_t r_offset = r_addr % FLASH_SECTOR_SIZE ? FLASH_SECTOR_SIZE - (r_addr % FLASH_SECTOR_SIZE) : 0;
		if (r_offset) {
			r_buf = (uint8_t *) malloc(r_offset);
			flash_read(r_addr, r_buf, r_offset);
		}

		flash_write_enable();
		flash_wait_busy();

		/* 逐个扇区擦除 */
		for (uint32_t i = l_addr; i < l_addr + l_offset + len + r_offset; i += FLASH_SECTOR_SIZE) {
			FLASH_CS_LOW();
			/* 读命令 */
			flash_transfer(0x20);
			/* 写地址 */
			flash_transfer((uint8_t)(i >> 16));
			flash_transfer((uint8_t)(i >> 8));
			flash_transfer((uint8_t)i);
			FLASH_CS_HIGH();
			/* 等待擦除完成 */
			flash_wait_busy();
		}
		
		/* 写回原数据并释放内存 */
		if (l_buf) {
			flash_write(l_addr, l_buf, l_offset, false);
			free(l_buf);
		}
		if (r_buf) {
			flash_write(r_addr, r_buf, r_offset, false);
			free(r_buf);
		}
	} else {
		/* 扇区对齐 */
		addr -= addr % FLASH_SECTOR_SIZE;
		len += addr % FLASH_SECTOR_SIZE;
		len += len % FLASH_SECTOR_SIZE ? (FLASH_SECTOR_SIZE - (len % FLASH_SECTOR_SIZE)) : 0;
		
		flash_write_enable();
		flash_wait_busy();

		/* 逐个扇区擦除 */
		for (uint32_t i = addr; i < addr + len; i += FLASH_SECTOR_SIZE) {
			FLASH_CS_LOW();
			/* 读命令 */
			flash_transfer(0x20);
			/* 写地址 */
			flash_transfer((uint8_t)(i >> 16));
			flash_transfer((uint8_t)(i >> 8));
			flash_transfer((uint8_t)i);
			FLASH_CS_HIGH();
			/* 等待擦除完成 */
			flash_wait_busy();
		}	
	}
}

        擦除部分的代码还比较复杂,因为我考虑了比较多的情况。首先,Flash的擦除是有好几种选择的,一般是按扇区擦除,一个扇区就是4k大小;但也可以选择按32k块擦除、64k块擦除和全片擦除。

        因为擦除是按扇区擦除的,所以传入的地址也要按扇区对齐,擦除的数据大小也要是扇区的整数倍,不然有可能会擦了一些不该擦的数据;不过我的代码是支持不规则擦除区域的,因此即使没有遵守上面的规定也是没有问题的,代码会自动进行扇区对齐。

        不过这里要考虑一个情况,如果用户传入了一个未对齐的区域,像上面的红色部分,那么代码会对齐然后再擦除,相当于左右的蓝色部分也被擦了。但有时候我就是想只擦红色区域的,蓝色区域数据保留,函数的最后一个reserve参数就提供了这个选择,可以只擦指定区域,其他区域不受影响。

        这个的原理其实就是,在擦之前先申请两块内存,保存蓝色区域的内容,再对齐扇区擦除,擦完之后再把原先的数据写回去,最后释放内存。但因为需要申请堆区内存,考虑最坏的情况,程序最多需要申请4k的内存,所以使用该功能时确保堆区分配的内存要大于等于4k

休眠

void flash_sleep(void)
{
	flash_wait_busy();
	
	FLASH_CS_LOW();
	flash_transfer(0xB9);
	FLASH_CS_HIGH();
}

         没啥好说的,就是发命令就行了。

唤醒

void flash_wake(void)
{
	FLASH_CS_LOW();
	flash_transfer(0xAB);
	FLASH_CS_HIGH();
    FLASH_DELAY(1);
}

        也是发命令就行, 但要注意唤醒命令后要等一下它才会进入正常工作状态,每个Flash芯片都不一样,像我这颗就是3us,我留了1ms,应该不可能有Flash的唤醒时间比这个久了。

测试

int main(void)
{
    systick_config();
	debug_init();
	printf("spi flash demo\r\n");
	
	/* 初始化Flash */
	uint32_t id = flash_init();
	printf("id: %08X\r\n", id);
		
	/* 写数据 */
	for (uint32_t i = 0, tmp = 0; i < 1024; i++, tmp = (tmp % 0xFF) + 1) {
		buffer[i] = tmp;
	}
	flash_erase_write(0x000000, buffer, 1024);
	printf("write 1024 bytes to flash from address 0x000000\r\n");
	
	/* 读数据 */
	flash_read(0x000000, recv, 256);
	printf("read flash:\r\n");
	for (uint32_t i = 0; i < sizeof(recv); ) {
		printf("%02X ", recv[i++]);
		if (i % 32 == 0) {
			printf("\r\n");
		}
	}
	
	/* 擦除数据(保留数据) */
	flash_erase(0x000010, 100, true);
	printf("erase 100 bytes from address 0x000010\r\n");
	memset(recv, 0, sizeof(recv));
	flash_read(0x000000, recv, 256);
	printf("read flash:\r\n");
	for (uint32_t i = 0; i < sizeof(recv); ) {
		printf("%02X ", recv[i++]);
		if (i % 32 == 0) {
			printf("\r\n");
		}
	}

	/* 擦除数据(不保留数据) */
	flash_erase_write(0x000000, buffer, 1024);
	printf("write 1024 bytes to flash from address 0x000000\r\n");
	flash_erase(0x000010, 100, false);
	printf("erase 100 bytes from address 0x000010\r\n");
	memset(recv, 0, sizeof(recv));
	flash_read(0x000000, recv, 256);
	printf("read flash:\r\n");
	for (uint32_t i = 0; i < sizeof(recv); ) {
		printf("%02X ", recv[i++]);
		if (i % 32 == 0) {
			printf("\r\n");
		}
	}
	
	while (1) {
	}
}

         简单写一个测试程序,先初始化Flash,然后往前4k的空间写一些数据,后面的话测试两次擦除,一次是保留数据的,另一次是不保留数据的。

        下面的结果可以看到保留数据擦除就只会擦指定的区域,如果不保留数据擦除就会把区域所在的所有扇区都擦掉。

  • 26
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

马浩同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值