写在前面: 平台: STM32F407VGT6 + W25Q64FV 8MFLASH
# 为什么要移植 SFUD
记得当初学标准库读写 FLASH 的时候,页写入代码可长可长了,而且不同的 flash 一套检测的宏不一样,改来改去特别麻烦.
而 SFUD 驱动只需要 FLASH 支持 SFDP 协议即可移植 SFUD 后只需实现初始化和写操作就可以愉快的使用 FLASH. 膜拜 armlink 大佬.
是 JEDEC (固态技术协会) 制定的串行 Flash 功能的参数表标准, 最新标准,目前新生产的 flash 都是支持的只有特别老旧的芯片才不支持.
而且 SFUD 对于不支持 SFDP 的 FLASH, 可以手动在 /sfud/inc/sfud_flash_def.h
文件中的参数表里添加芯片参数.
简直就是人间神奇
# 源码获取
建议直接从 github 或 gitee 上拉取,能保证代码是最新的 (3 年就写了这个库,到现在都还在更新,人与人之间的差距啊...).
如果不想用 git 拉去的话,可以直接点击下面的阿里云链接进行下载。百度网盘这里就先不放了 (嫌弃)
Github 传送门
Gitee 传送门
阿里云盘传送门
# 文件结构
# 移植
主要是实现 /sfud/port/sfud_port.c
文件,其实我们只需要实现 sfud_spi_port_init
, spi_write_read
, 即可至于更多的需求,可以视 init 函数里的注释自行扩充
由于 sfud 有 debug 机制,所以需要各位自行实现 printf 的重载.
# sfud_spi_port_init
其实要实现那些函数在注释中已经写的明明白白了.
# 抽个结构体出来
为了方便处理这边用了一个结构体,用于保存用户的 SPI 信息。懒惰,我连个 Led 都想 抽象成结构体
typedef struct | |
{ | |
SPI_TypeDef *spix; | |
GPIO_TypeDef *cs_gpiox; | |
uint16_t cs_gpio_pin; | |
} spi_user_data, *spi_user_data_t; | |
static spi_user_data spi1 = | |
{.spix = SPI1, | |
.cs_gpiox = GPIOB, | |
.cs_gpio_pin = GPIO_Pin_0}; |
# 初始化函数
这里可以抽一层函数出来,写一个某个型号的 FLASH 初始化函数,然后放在 case 中调用
怎么感觉我总喜欢疯狂套娃
sfud_err sfud_spi_port_init(sfud_flash *flash) | |
{ | |
sfud_err result = SFUD_SUCCESS; | |
switch (flash->index) | |
{ | |
case SFUD_W25Q64FV_DEVICE_INDEX: | |
{ | |
/* RCC 初始化 */ | |
rcc_configuration(&spi1); | |
/* GPIO 初始化 */ | |
gpio_configuration(&spi1); | |
/* SPI 外设初始化 */ | |
spi_configuration(&spi1); | |
/* 同步 Flash 移植所需的接口及数据 */ | |
flash->spi.wr = spi_write_read; | |
flash->spi.lock = spi_lock; | |
flash->spi.unlock = spi_unlock; | |
flash->spi.user_data = &spi1; | |
/* about 100 microsecond delay */ | |
flash->retry.delay = retry_delay_100us; | |
/* adout 60 seconds timeout */ | |
flash->retry.times = 60 * 10000; | |
break; | |
} | |
} | |
return result; | |
} |
# 硬件配置
这里我用的 SPI1 软件片选,引脚走的 GPIOB3, GPIOB4, GPIOB5, GPIO0 (CS)
QSPI 在 407 上没有,所以我先不写了,等我有钱搞新板子了在考虑如何实现 (其实用 HAL 库才是趋势...).
/** | |
* @brief SPI 时钟使能 | |
* | |
* @param spi SPI 结构体 | |
*/ | |
static void rcc_configuration(spi_user_data_t spi) | |
{ | |
if (spi->spix == SPI1) | |
{ | |
RCC->APB2ENR |= RCC_APB2ENR_SPI1EN; // SPI Clock Enable | |
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN; // GPIO Clock Enable | |
} | |
} | |
/** | |
* @brief 配置 SPI 的 GPIO | |
* | |
* @param spi SPI 结构体 | |
*/ | |
static void gpio_configuration(spi_user_data_t spi) | |
{ | |
GPIO_InitTypeDef GPIO_InitStructure; | |
if (spi->spix == SPI1) | |
{ | |
/* SCK:PB3 MISO:PB4 MOSI:PA5 */ | |
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_4 | GPIO_Pin_3; | |
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; | |
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; | |
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; | |
GPIO_PinAFConfig(GPIOB, GPIO_PinSource3, GPIO_AF_SPI1); | |
GPIO_PinAFConfig(GPIOB, GPIO_PinSource4, GPIO_AF_SPI1); | |
GPIO_PinAFConfig(GPIOB, GPIO_PinSource5, GPIO_AF_SPI1); | |
GPIO_Init(GPIOB, &GPIO_InitStructure); | |
/* CS: PB0 */ | |
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; | |
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; | |
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; | |
GPIO_Init(GPIOB, &GPIO_InitStructure); | |
GPIO_SetBits(GPIOB, GPIO_Pin_0); | |
} | |
} | |
/** | |
* @brief 配置 SPI 工作模式 | |
* | |
* @param spi SPI 结构体 | |
*/ | |
static void spi_configuration(spi_user_data_t spi) | |
{ | |
SPI_InitTypeDef SPI_InitStructure; | |
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // SPI 设置为双线双向全双工 | |
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; // 设置为主 SPI | |
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // SPI 发送接收 8 位帧结构 | |
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // 时钟悬空低 | |
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; // 数据捕获于第一个时钟沿 | |
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 内部 NSS 信号由 SSI 位控制 | |
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2; // 波特率预分频值为 2 | |
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // 数据传输从 MSB 位开始 | |
SPI_InitStructure.SPI_CRCPolynomial = 7; // CRC 值计算的多项式,不使能 CRC 有效位配置啥都无所谓 | |
SPI_I2S_DeInit(spi->spix); | |
SPI_Init(spi->spix, &SPI_InitStructure); | |
SPI_CalculateCRC(spi->spix, DISABLE); | |
SPI_Cmd(spi->spix, ENABLE); | |
} |
我觉得,这份代码 rcc_configuration
和 gpio_configuration
可以再抽一层出来,在里面放置一个 switch case
调用 SPIx (x 由片上 SPI 数量决定) 的初始化.
但是现在在做项目,也只可能有一个 FLASH, 只能先这样子了。等项目结束我再更新一下.
# SPI 锁
这一个可以实现也可以不实现,主要看自己的需求,如果的写的数据很大我建议还是实现一下.
其实就是关中断这种暴力的方法而已...
static void spi_lock(const sfud_spi *spi) | |
{ | |
__disable_irq(); | |
} | |
static void spi_unlock(const sfud_spi *spi) | |
{ | |
__enable_irq(); | |
} |
# 延时
这个是注释里要求一定需要实现的,我这里走的是 systick
实现的 ys 级延时。反正 100us 左右即可,可以自行实现 (大不了 count--, 狗头).
static void retry_delay_100us(void) | |
{ | |
// uint32_t delay = 120; | |
// while (delay--) | |
// ; | |
delayus(100); | |
} |
# spi_write_read
函数接口如下:
static sfud_err spi_write_read(const sfud_spi *spi, // sfud | |
const uint8_t *write_buf, | |
size_t write_size, | |
uint8_t *read_buf, | |
size_t read_size) |
好多参数..., 其实实现起来和我们直接写自己实现 连续写 spi 没什么区别.
唯一要注意的只有这个地方,获取 sfud 的指向的 spi 设备,如上所示.
其他的无非就,先写入有效数据,然后再写入无效数据作为数据读取。一共写入 read_size+write_size 字节的数据
/** | |
* @brief SPI 写入后读出数据 | |
* | |
* @param spi SPI 结构体 | |
* @param write_buf 要写入的数据 | |
* @param write_size 写入数据大小 | |
* @param read_buf 要读出的数据的缓存 | |
* @param read_size 要读出的数据大小 | |
* @return sfud_err 执行结果 | |
*/ | |
static sfud_err spi_write_read(const sfud_spi *spi, const uint8_t *write_buf, size_t write_size, uint8_t *read_buf, | |
size_t read_size) | |
{ | |
sfud_err result = SFUD_SUCCESS; | |
uint8_t send_data, read_data; | |
spi_user_data_t spi_dev = (spi_user_data_t)spi->user_data; | |
if (write_size) | |
{ | |
SFUD_ASSERT(write_buf); | |
} | |
if (read_size) | |
{ | |
SFUD_ASSERT(read_buf); | |
} | |
GPIO_ResetBits(spi_dev->cs_gpiox, spi_dev->cs_gpio_pin); | |
/* 开始读写数据 */ | |
for (size_t i = 0, retry_times; i < write_size + read_size; i++) | |
{ | |
/* 先写缓冲区中的数据到 SPI 总线,数据写完后,再写 dummy (0xFF) 到 SPI 总线 */ | |
if (i < write_size) | |
{ | |
send_data = *write_buf++; | |
} | |
else | |
{ | |
send_data = SFUD_DUMMY_DATA; | |
} | |
/* 发送数据 */ | |
retry_times = 1000; | |
while (SPI_I2S_GetFlagStatus(spi_dev->spix, SPI_I2S_FLAG_TXE) == RESET) | |
{ | |
SFUD_RETRY_PROCESS(NULL, retry_times, result); | |
} | |
if (result != SFUD_SUCCESS) | |
{ | |
goto exit; | |
} | |
SPI_I2S_SendData(spi_dev->spix, send_data); | |
/* 接收数据 */ | |
retry_times = 1000; | |
while (SPI_I2S_GetFlagStatus(spi_dev->spix, SPI_I2S_FLAG_RXNE) == RESET) | |
{ | |
SFUD_RETRY_PROCESS(NULL, retry_times, result); | |
} | |
if (result != SFUD_SUCCESS) | |
{ | |
goto exit; | |
} | |
read_data = SPI_I2S_ReceiveData(spi_dev->spix); | |
/* 写缓冲区中的数据发完后,再读取 SPI 总线中的数据到读缓冲区 */ | |
if (i >= write_size) | |
{ | |
*read_buf++ = read_data; | |
} | |
} | |
exit: | |
GPIO_SetBits(spi_dev->cs_gpiox, spi_dev->cs_gpio_pin); | |
return result; | |
} |
# 配置文件
除了这些之外,还需要自行配置一下 /sfud/inc/sfud_cfg.h
文件
选择是否使用 SFDP 协议,是否使用 FLASH 信息表,和定义自己所用的 FLASH 对应的枚举变量以及结构体信息
我的配置如下
// #define SFUD_DEBUG_MOD // 不使用 SFUD debug 模式 | |
#define SFUD_USING_SFDP // 使用 SFDP | |
#define SFUD_USING_FLASH_INFO_TABLE // 使用 FLASH 信息表 | |
// FLASH 芯片型号枚举 | |
enum | |
{ | |
SFUD_W25Q64FV_DEVICE_INDEX = 0, | |
}; | |
// FLASH 相关信息和 驱动的硬件 SPI | |
#define SFUD_FLASH_DEVICE_TABLE \ | |
{ \ | |
[SFUD_W25Q64FV_DEVICE_INDEX] = {.name = "W25Q64FV", .spi.name = "SPI1"}, \ | |
} |
# 接口函数
# sfud_init
函数原型如下
sfud_err sfud_init(void); |
初始化函数,这个放在主函数调用判断一下就好了,成功会返回 SFUD_SUCCESS
如果开启了 DEBUG 的宏,会打印一些详细信息.
这个初始化会初始化设备列表中的所有 flash 驱动,如果指向驱动一个 flash 设备,可以考虑使用 sfud_device_init
# sfud_read
函数原型
sfud_err sfud_read(const sfud_flash *flash, uint32_t addr, size_t size, uint8_t *data); |
将数据从 FLASH 中读出,没啥好说,传个 flash 设备进去,给个地址,缓存空间,要读取的数据量没了
# sfud_erase
函数原型
sfud_err sfud_erase(const sfud_flash *flash, uint32_t addr, size_t size); |
擦除函数,用于将 FLASH 里的数据擦除为 dummy 的值
# sfud_write
函数原型
sfud_err sfud_write(const sfud_flash *flash, uint32_t addr, size_t size, const uint8_t *data); |
单纯的写函数,目前我也不知道有什么用,不擦除再写基本上很难保证数据的稳定性和准确性
# sfud_erase_write
函数原型
sfud_err sfud_erase_write(const sfud_flash *flash, uint32_t addr, size_t size, const uint8_t *data); |
这个函数和 write 函数不同的地方在于,先写后擦,能实现数据写入 有效的写入 FLASH
# demo
#define SFUD_DEMO_TEST_BUFFER_SIZE 2048 | |
static uint8_t sfud_demo_test_buf[SFUD_DEMO_TEST_BUFFER_SIZE]; | |
static void sfud_demo(uint32_t addr, size_t size, uint8_t *data) | |
{ | |
sfud_err result = SFUD_SUCCESS; | |
// 在操作之前需要获取 flash 对象 | |
const sfud_flash *flash = sfud_get_device_table() + 0; | |
size_t i; | |
// 初始化 spi | |
if (sfud_init() == SFUD_SUCCESS) | |
{ | |
sfud_demo(0, sizeof(sfud_demo_test_buf), sfud_demo_test_buf); | |
} | |
/* 数据填充 */ | |
for (i = 0; i < size; i++) | |
{ | |
data[i] = i; | |
} | |
/* 擦除扇区 */ | |
result = sfud_erase(flash, addr, size); | |
if (result == SFUD_SUCCESS) | |
{ | |
printf("Erase the %s flash data finish. Start from 0x%08X, size is %ld.\r\n", flash->name, addr, | |
size); | |
} | |
else | |
{ | |
printf("Erase the %s flash data failed.\r\n", flash->name); | |
return; | |
} | |
/* 写入数 */ | |
result = sfud_write(flash, addr, size, data); | |
if (result == SFUD_SUCCESS) | |
{ | |
printf("Write the %s flash data finish. Start from 0x%08X, size is %ld.\r\n", flash->name, addr, | |
size); | |
} | |
else | |
{ | |
printf("Write the %s flash data failed.\r\n", flash->name); | |
return; | |
} | |
/* 读出数据 */ | |
result = sfud_read(flash, addr, size, data); | |
if (result == SFUD_SUCCESS) | |
{ | |
printf("Read the %s flash data success. Start from 0x%08X, size is %ld. The data is:\r\n", flash->name, addr, | |
size); | |
printf("Offset (h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F\r\n"); | |
for (i = 0; i < size; i++) | |
{ | |
if (i % 16 == 0) | |
{ | |
printf("[%08X] ", addr + i); | |
} | |
printf("%02X ", data[i]); | |
if (((i + 1) % 16 == 0) || i == size - 1) | |
{ | |
printf("\r\n"); | |
} | |
} | |
printf("\r\n"); | |
} | |
else | |
{ | |
printf("Read the %s flash data failed.\r\n", flash->name); | |
} | |
/* 数据校验 */ | |
for (i = 0; i < size; i++) | |
{ | |
if (data[i] != i % 256) | |
{ | |
printf("Read and check write data has an error. Write the %s flash data failed.\r\n", flash->name); | |
break; | |
} | |
} | |
if (i == size) | |
{ | |
printf("The %s flash test is success.\r\n", flash->name); | |
} | |
} |
# END
目前我也就只知道这些了,等以后使用的时候出现什么 bug 再来琢磨琢磨
大道五十,天衍四十九,人遁其一!