写在前面: 平台: 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 传送门
阿里云盘传送门

# 文件结构

根目录
demo
sfud

# 移植

主要是实现 /sfud/port/sfud_port.c 文件,其实我们只需要实现 sfud_spi_port_init , spi_write_read , 即可至于更多的需求,可以视 init 函数里的注释自行扩充
由于 sfud 有 debug 机制,所以需要各位自行实现 printf 的重载.

# sfud_spi_port_init

其实要实现那些函数在注释中已经写的明明白白了.
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_configurationgpio_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 没什么区别.
上面代码里的强转转的是这个结构体里的void 类型
唯一要注意的只有这个地方,获取 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 再来琢磨琢磨


大道五十,天衍四十九,人遁其一!

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

黑羊 支付宝

支付宝