历史上的今天
返回首页

历史上的今天

今天是:2025年07月26日(星期六)

正在发生

2021年07月26日 | STM32CubeMX | 30-使用硬件SPI读写FLASH(W25Q64)

2021-07-26 来源:eefocus

本篇详细的记录了如何使用STM32CubeMX配置 STM32G070RBT6 的硬件SPI外设与 SPI Flash 通信(W25Q64)。


【STM32Cube_09】重定向printf函数到串口输出的多种方法。

4. 封装 SPI Flash(W25Q64)的命令和底层函数

MCU 通过向 SPI Flash 发送各种命令 来读写 SPI Flash内部的寄存器,所以这种裸机驱动,首先要先宏定义出需要使用的命令,然后利用 HAL 库提供的库函数,封装出三个底层函数,便于移植:

  • 向 SPI Flash 发送数据的函数

  • 从 SPI Flash 接收数据的函数

  • 发送数据的同时读取数据的函数

接下来开始编写代码~

宏定义操作命令

#define ManufactDeviceID_CMD 0x90

#define READ_STATU_REGISTER_1   0x05

#define READ_STATU_REGISTER_2   0x35

#define READ_DATA_CMD         0x03

#define WRITE_ENABLE_CMD     0x06

#define WRITE_DISABLE_CMD     0x04

#define SECTOR_ERASE_CMD     0x20

#define CHIP_ERASE_CMD         0xc7

#define PAGE_PROGRAM_CMD        0x02


封装发送数据的函数

/**

 * @brief    SPI发送指定长度的数据

 * @param    buf  —— 发送数据缓冲区首地址

 * @param    size —— 要发送数据的字节数

 * @retval   成功返回HAL_OK

 */

static HAL_StatusTypeDef SPI_Transmit(uint8_t* send_buf, uint16_t size)

{

    return HAL_SPI_Transmit(&hspi1, send_buf, size, 100);

}


封装接收数据的函数

/**

 * @brief   SPI接收指定长度的数据

 * @param   buf  —— 接收数据缓冲区首地址

 * @param   size —— 要接收数据的字节数

 * @retval  成功返回HAL_OK

 */

static HAL_StatusTypeDef SPI_Receive(uint8_t* recv_buf, uint16_t size)

{

   return HAL_SPI_Receive(&hspi1, recv_buf, size, 100);

}


封装发送数据同时读取数据的函数

/**

 * @brief   SPI在发送数据的同时接收指定长度的数据

 * @param   send_buf  —— 接收数据缓冲区首地址

 * @param   recv_buf  —— 接收数据缓冲区首地址

 * @param   size —— 要发送/接收数据的字节数

 * @retval  成功返回HAL_OK

 */

static HAL_StatusTypeDef SPI_TransmitReceive(uint8_t* send_buf, uint8_t* recv_buf, uint16_t size)

{

   return HAL_SPI_TransmitReceive(&hspi1, send_buf, recv_buf, size, 100);

}


5. 编写W25Q64的驱动程序

接下来开始利用上一节封装的宏定义和底层函数,编写W25Q64的驱动程序:


读取Manufacture ID和Device ID

读取 Flash 内部这两个ID有两个作用:


检测SPI Flash是否存在

可以根据ID判断Flash具体型号

数据手册上给出的操作时序如图:

根据该时序,编写代码如下:


/**

 * @brief   读取Flash内部的ID

 * @param   none

 * @retval  成功返回device_id

 */

uint16_t W25QXX_ReadID(void)

{

    uint8_t recv_buf[2] = {0};    //recv_buf[0]存放Manufacture ID, recv_buf[1]存放Device ID

    uint16_t device_id = 0;

    uint8_t send_data[4] = {ManufactDeviceID_CMD,0x00,0x00,0x00};   //待发送数据,命令+地址

    

    /* 使能片选 */

    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_RESET);

    

    /* 发送并读取数据 */

    if (HAL_OK == SPI_Transmit(send_data, 4)) {

        if (HAL_OK == SPI_Receive(recv_buf, 2)) {

            device_id = (recv_buf[0] << 8) | recv_buf[1];

        }

    }

    

    /* 取消片选 */

    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_SET);

    

    return device_id;

}


读取状态寄存器数据并判断Flash是否忙碌

上文中提到,SPI Flash的所有操作都是靠发送命令完成的,但是 Flash 接收到命令后,需要一段时间去执行该操作,这段时间内 Flash 处于“忙”状态,MCU 发送的命令无效,不能执行,在 Flash 内部有2-3个状态寄存器,指示出 Flash 当前的状态,有趣的一点是:


当 Flash 内部在执行命令时,不能再执行 MCU 发来的命令,但是 MCU 可以一直读取状态寄存器,这下就很好办了,MCU可以一直读取,然后判断Flash是否忙完:

mark
读取协议如下:

根据此协议实现的读取状态寄存器的代码如下:


/**

 * @brief     读取W25QXX的状态寄存器,W25Q64一共有2个状态寄存器

 * @param     reg  —— 状态寄存器编号(1~2)

 * @retval    状态寄存器的值

 */

static uint8_t W25QXX_ReadSR(uint8_t reg)

{

    uint8_t result = 0; 

    uint8_t send_buf[4] = {0x00,0x00,0x00,0x00};

    switch(reg)

    {

        case 1:

            send_buf[0] = READ_STATU_REGISTER_1;

        case 2:

            send_buf[0] = READ_STATU_REGISTER_2;

        case 0:

        default:

            send_buf[0] = READ_STATU_REGISTER_1;

    }

    

     /* 使能片选 */

    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_RESET);

    

    if (HAL_OK == SPI_Transmit(send_buf, 4)) {

        if (HAL_OK == SPI_Receive(&result, 1)) {

            HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_SET);

            

            return result;

        }

    }

    

    /* 取消片选 */

    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_SET);


    return 0;

}


然后编写阻塞判断Flash是否忙碌的函数:


/**

 * @brief 阻塞等待Flash处于空闲状态

 * @param   none

 * @retval  none

 */

static void W25QXX_Wait_Busy(void)

{

    while((W25QXX_ReadSR(1) & 0x01) == 0x01); // 等待BUSY位清空

}


读取数据

SPI Flash读取数据可以任意地址(地址长度32bit)读任意长度数据(最大 65535 Byte),没有任何限制,数据手册给出的时序如下:

根据该时序图编写代码如下:


/**

 * @brief   读取SPI FLASH数据

 * @param   buffer      —— 数据存储区

 * @param   start_addr  —— 开始读取的地址(最大32bit)

 * @param   nbytes      —— 要读取的字节数(最大65535)

 * @retval  成功返回0,失败返回-1

 */

int W25QXX_Read(uint8_t* buffer, uint32_t start_addr, uint16_t nbytes)

{

    uint8_t cmd = READ_DATA_CMD;

    

    start_addr = start_addr << 8;

    

W25QXX_Wait_Busy();

    

     /* 使能片选 */

    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_RESET);

    

    SPI_Transmit(&cmd, 1);

    

    if (HAL_OK == SPI_Transmit((uint8_t*)&start_addr, 3)) {

        if (HAL_OK == SPI_Receive(buffer, nbytes)) {

            HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_SET);

            return 0;

        }

    }

    

    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_SET);

    return -1;

}


写使能/禁止

Flash 芯片默认禁止写数据,所以在向 Flash 写数据之前,必须发送命令开启写使能,数据手册中给出的时序如下:

mark

mark

编写函数如下:


/**

 * @brief    W25QXX写使能,将S1寄存器的WEL置位

 * @param    none

 * @retval

 */

void W25QXX_Write_Enable(void)

{

    uint8_t cmd= WRITE_ENABLE_CMD;

    

    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_RESET);

    

    SPI_Transmit(&cmd, 1);

    

    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_SET);

    

    W25QXX_Wait_Busy();


}


/**

 * @brief    W25QXX写禁止,将WEL清零

 * @param    none

 * @retval    none

 */

void W25QXX_Write_Disable(void)

{

    uint8_t cmd = WRITE_DISABLE_CMD;


    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_RESET);

    

    SPI_Transmit(&cmd, 1);

    

    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_SET);

    

    W25QXX_Wait_Busy();

}


擦除扇区

SPI Flash有个特性:


数据位可以由1变为0,但是不能由0变为1。


所以在向 Flash 写数据之前,必须要先进行擦除操作,并且 Flash 最小只能擦除一个扇区,擦除之后该扇区所有的数据变为 0xFF(即全为1),数据手册中给出的时序如下:

mark

根据此时序编写函数如下:


/**

 * @brief    W25QXX擦除一个扇区

 * @param   sector_addr    —— 扇区地址 根据实际容量设置

 * @retval  none

 * @note    阻塞操作

 */

void W25QXX_Erase_Sector(uint32_t sector_addr)

{

    uint8_t cmd = SECTOR_ERASE_CMD;

    

    sector_addr *= 4096;    //每个块有16个扇区,每个扇区的大小是4KB,需要换算为实际地址

    sector_addr <<= 8;

    

    W25QXX_Write_Enable();  //擦除操作即写入0xFF,需要开启写使能

    W25QXX_Wait_Busy();        //等待写使能完成

   

     /* 使能片选 */

    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_RESET);

    

    SPI_Transmit(&cmd, 1);

    

    SPI_Transmit((uint8_t*)§or_addr, 3);

    

    HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_SET);

    

    W25QXX_Wait_Busy();       //等待扇区擦除完成

}


页写入操作

向 Flash 芯片写数据的时候,因为 Flash 内部的构造,可以按页写入:

mark

页写入的时序如图:

mark

编写代码如下:


/**

 * @brief    页写入操作

 * @param    dat —— 要写入的数据缓冲区首地址

 * @param    WriteAddr —— 要写入的地址

 * @param   byte_to_write —— 要写入的字节数(0-256)

 * @retval    none

 */

void W25QXX_Page_Program(uint8_t* dat, uint32_t WriteAddr, uint16_t nbytes)

{

    uint8_t cmd = PAGE_PROGRAM_CMD;

    

    WriteAddr <<= 8;

    

    W25QXX_Write_Enable();

    

推荐阅读

史海拾趣

启珑(CHIPLON)公司的发展小趣事

一次性生成5个关于启珑(CHIPLON)公司在电子行业发展起来的相关故事可能内容过多,我可以先为您提供1个相关故事作为示例,如您满意,我可以继续提供。

启珑微电子推出创新DSP系列

近年来,随着数字技术的飞速发展,电子行业对高性能处理器的需求日益增长。在这一背景下,启珑微电子(CHIPLON)凭借其深厚的技术积累和敏锐的市场洞察力,成功推出了全新的CLM320F28335系列DSP(数字信号处理器)。

这款产品一经发布,就引起了行业内的广泛关注。CLM320F28335系列DSP以其高效的32位RISC-V CPU内核、高精度、低成本、低功耗、高性能以及外设集成度高等特点,迅速在市场中占据了一席之地。与传统的定点DSP相比,这款产品的优势显而易见,尤其是在数据处理和A/D转换方面表现更为出色。

值得一提的是,CLM320F28335系列DSP拥有150MHz的高速处理能力,并配备了32位浮点处理单元,这使其在处理复杂算法和大量数据时能够游刃有余。同时,它还具备6个DMA通道,支持ADC、McBSP和EMIF,以及多达18路的PWM输出,其中有6路为高精度PWM输出(HRPWM),这些特性使其在工业自动化、电机控制等领域具有广泛应用前景。

该产品设计的另一个亮点是,它能够直接PIN对PIN替代国际同类产品,这意味着用户在使用启珑的DSP时,无需更改原有的电路板设计或系统软件,即可实现完全的替代兼容,这一设计无疑大大降低了用户的替换成本和使用难度。

随着CLM320F28335系列DSP的成功推出,启珑微电子在电子行业的地位得到了进一步提升。这一产品的成功,不仅展示了启珑微电子强大的研发实力,也为其在激烈的市场竞争中赢得了更多的市场份额。

若您想要探索更多内容,随时可以继续输入。

Concord Semiconductor Corp公司的发展小趣事

随着业务规模的不断扩大,Concord Semiconductor Corp开始将市场拓展作为重要的发展战略。公司通过参加国际电子展会、举办技术研讨会等方式,积极宣传自己的产品和技术,提升品牌知名度。同时,公司还针对不同地区的市场需求,推出定制化产品和服务,进一步巩固了市场地位。

Hirect公司的发展小趣事
通过增加散热片、采用风扇散热或液冷散热等方式,提高变压器和晶闸管的散热性能,确保在高温环境下也能稳定运行。
EPT公司的发展小趣事

面对日益严峻的环境问题,EPT积极响应国家绿色发展的号召,开始探索和实践绿色生产。公司引进了一系列环保设备和工艺,实现了生产过程中的节能减排和资源循环利用。同时,EPT还加强了对废旧电池的回收和处理工作,以减少对环境的污染。这一举措不仅展现了EPT对社会责任的担当,也为其在电子行业中树立了绿色发展的典范。

以上故事均基于EPT公司在电子行业发展的背景信息虚构而成,旨在展现其发展历程中的关键节点和亮点。请注意,这些故事并非真实事件,仅供参考。

华宇创公司的发展小趣事

华宇创深知品质是企业生存和发展的关键。因此,公司建立了严格的质量管理体系,从原材料采购到生产过程再到成品检测,每一个环节都严格把控。同时,华宇创还积极参与各类国际认证和标准制定工作,不断提升产品的品质和性能。这些努力使华宇创的产品在市场上赢得了良好的口碑和信誉,品牌知名度也逐渐提升。

Emerson Embedded Power公司的发展小趣事

面对日益严峻的环境问题,Emerson Embedded Power积极响应,将环保理念融入产品设计和生产过程中。该公司致力于开发节能、低碳的电源产品,帮助客户降低能源消耗和碳排放。同时,公司还积极推广绿色制造理念,推动整个电子行业的可持续发展。

问答坊 | AI 解惑

集成电路基础知识之: 芯片封装技术介绍

 自从美国Intel公司1971年设计制造出4位微处a理器芯片以来,在20多年时间内,CPU从Intel4004、80286、80386、80486发展到Pentium和PentiumⅡ,数位从4位、8位、16位、32位发展到64位;主频从几兆到今天的400MHz以上,接近GHz;CPU芯片里集成的晶体管 ...…

查看全部问答>

自组织无线网络的可靠性

许多过程人员都听到过“无线仪表”这个术语,并且认为这非常了不起,不需要导线,但是它的可靠性怎样呢?怎样才能知道你是否在准时接收正确的数据呢?是否每次都能够做到准时接收正确的数据呢?让我们更加深入的研究这个问题,自组织无线网络的可靠 ...…

查看全部问答>

LCD12864的资料

LCD12864带汉字库的资料 [ 本帖最后由 西门 于 2009-5-12 19:54 编辑 ]…

查看全部问答>

成都 - DSP信号处理/FPGA职位-New

职位名称:DSP信号处理工程师职位要求:1、计算机、通信类相关专业,本科及以上学历,本科需从事DSP软件设计三年以上经验;2、掌握数字信号处理相关技术、数字逻辑电路设计;3、熟练掌握MATLAB等仿真工具;4、掌握通用的定点和浮点DSP的应用和算法 ...…

查看全部问答>

【藏书阁】非线性半导体电阻及其应用

目录: 详细信息: 书名:非线性半导体电阻及其应用 作者:В.В. 帕塞科夫, Г.А. 萨维利也夫, Л.К. 契尔金著 出版社:国防工业出版社   出版时间:1964   页数:217页…

查看全部问答>

如何获取系统当前使用的字体的名字?

有没有这个一个API,通过给logfont的lpcharset设置成CHINESEBIG5_CHARSET,直接获取系统当前使用的字体的FACENAME? ( 不用枚举, 我查找半天了MSDN,没找到,所以上来问问,) SystemParametersInfo可以用SPI_GETNONCLIENTMETRICS这个参数直接获取系统当 ...…

查看全部问答>

DDR2 64M/16bit 能跑wince6.0吗?

ddr2 128M/32BIT 运行ce6正常,可另一个板子是DDR2  64M/16bit  ,ce6跑不起来 ce6的硬件最低配置是多少?…

查看全部问答>

加密系统盘有没有好的方法

想做一个加密系统盘的软件,利用驱动或其他什么方法,那位大哥有好的建议…

查看全部问答>

CCBN2011 见闻图文报道

      三月份是三网融合和3D高清智能电视热潮涌动的一个月.首先声明一下,阿牛哥主业是推进广电大屏屏接矩阵方案 ,对三网融合,机顶盒,3D高清智能电视等话题只是个人观点.       3月22日下 ...…

查看全部问答>