第14章_瑞萨MCU零基础入门系列教程之QSPI

本教程基于韦东山百问网出的 DShanMCU-RA6M5开发板 进行编写,需要的同学可以在这里获取: https://item.taobao.com/item.htm?id=728461040949

配套资料获取:https://renesas-docs.100ask.net

瑞萨MCU零基础入门系列教程汇总: https://blog.csdn.net/qq_35181236/article/details/132779862


第14章 QSPI

本章目标

  • 使用RASC快速配置QSPI模块
  • 学会使用QSPI的API对W25Q64进行数据读写

14.1 认识QSPI

QSPI是Quad SPI的简写,是Motorola公司推出的SPI接口的扩展协议,比普通SPI增加了两条数据线。

14.1.1 SPI和QSPI的区别

普通SPI协议有很多扩展:Dual SPI、Quad SPI等。

通过前面章节对SPI接口协议的分析已经知道,普通SPI有4个IO控制:CS/SCK/MOSI/MISO,在通信的时候由主机通过CS选中从机设备,发出SCK时钟,主机把数据驱动到MOSI线上发给从机,主机从MISO线上将数据读进来。

对于Dual SPI(双线串行外设接口),它同样也由4根线共同完成通信:CS/SCK/IO0/IO1,和SPI不同的是,Dual SPI在收发数据的时候是使用2根数据线IO0和IO1进行的,而不是像SPI那样收发数据分别只使用1条数据线。因而,在单向数据传输上,Dual SPI的传输速度是SPI的2倍。Dual SPI是半双工的。

对于Quad SPI(四线串行外设接口),它较之于Dual SPI则是多了2根数据线IO2/IO3。在通信的时候,收发数据使用4根数据线进行,在单向传输上,速率是SPI的4倍,是Dual SPI的2倍。Quad SPI是半双工的。

14.1.2 RA6M5系列的QSPI

  1. QSPI框图

RA6M5的QSPI框图如下图所示:

通过QSPI模块,可以方便地编写程序,使用QSPI协议访问外设。

  1. SPI总线协议

QSPI向下兼容普通SPI、Dual SPI。所以它支持的SPI总线协议有3种:Single SPI/Dual SPI/Quad SPI,这三种协议需要的控制线和数据线看下表:

IOSingle SPIDual SPIQuad SPI
QSSL/CS
QSPCLK/SCK
QIO0
QIO1
QIO2
QIO3
  1. SPI模式

RA6M5的QSPI有2种SPI模式:SPI Mode0和SPI Mode3,对应的特点如下:

  • SPI Mode 0:时钟信号线QSPCLK在SPI总线空闲地时候呈低电平;
  • SPI Mode 3:时钟信号线QSPCLK在SPI总线空闲地时候呈高电平;
  1. QSPI的内存映射

当使用QSPI连接外部存储设备时,RA6M5系列处理器的地址和外部存储设备的地址映射如下图所示:

从图中可以看出,其映射首地址是0x60000000,结束地址是0x68000000,共有128MB大,但是用于映射外部存储器的大小只有低64MB的空间——这就是一个Bank的大小。还可通过QSPI.EXT[5:0]来选择64个Bank(只使用63个Bank),所以QSPI可以访问的最大容量为64MB*63。

使用QSPI的直接通信模式时,并不涉及这些映射关系,直接发出存储设备上的偏移地址即可。

使用XIP模式时,才需要使用这样的地址:0x60000000+存储设备偏移地址。

14.1.3 QSPI的XIP控制

XIP,execute in place,直接翻译过来就是“就地执行”。就地执行什么?就地执行存储设备上的程序。使用XIP的好处是不用将存储设备的程序复制到RAM上才能运行,而是直接就地执行。

RA6M5的QSPI就支持XIP控制方法,通过寄存器SFMXD[7:0]和SFMSDC实现进入和退出XIP控制模式。

什么情况下XIP方法呢?当内部Flash不足需要把程序保存到QSPI外设上,并且RAM也不够大导致无法把QSPI外设的程序读到RAM里运行,才会考虑这种方式。但是使用这种方式时,程序执行效率会慢一些,毕竟使用QSPI读取指令的速度远低于从RAM上读取指令的速度。

14.1.4 直接通信模式

对于RA6M5的QSPI,它还有一种通信模式叫做直接通信模式Direct Communication Mode。瑞萨考虑到市面上的外部存储设备有不同的访问方法,需要使用直接通信模式来发送定制化的指令以读写数据。

14.2 QSPI模块的使用

14.2.1 配置QSPI模块

本章实验驱动的W25Q64模块为板载模块,原理图如下图所示:

W25Q64连接到QSPI0模块,使用的GPIO引脚如下:

序号模块引脚芯片SPI0引脚
1CSP306
2SCKP305
3IO0P307
4IO1P308
5IO2P309
6IO3P310

其中CS是QSPI的片选信号引脚,IO0~IO3是QSPI的4根数据线。

在RASC中配置QSPI,首先在RASC的“Pin Configuration”中的“Peripherals”里展开“Storage:QSPI”,选中里面的QSPI0,如下图所示:

配置QSPI的引脚时,“Pin Group Selection”选择组别时,有混合型Mixed和组别独有型,如图所示:

在不清楚引脚属于哪一个组别的情况下可以使用Mixed组别来手动指定。

对于QSPI的操作模式“Operation Mode”,支持自定义Custom模式、Single SPI和Dual SPI模式以及Quad SPI模式。如下图所示:

本章选择的是Quad SPI模式。

配置完引脚和操作模式后,就要去“Stacks”里添加QSPI的Stack模块。点击“New Stack”,选择里面的“Storage”中的“QSPI(r_qspi)”,如图所示:

添加了QSPI的Stack模块后再去配置参数。首先是General通用参数,需要在这里设置QSPI的协议、地址位数、读写模式、空闲时钟和页大小等,下图是根据W25Q64的特点进行的设置:

具体参数参考下表:

接着是命令定义,也就是设置QSPI通信设备的一些控制命令。FSP默认的一些命令能够满足大部分都QSPI设备,如下图所示:

这些命令需要根据通信的设备来设置,需要仔细核对。对于本章使用的W25Q64而言,这些命令都是可以使用的,本章没有对其进行修改,使用的是默认的指令。

最后是QSPI总线的时间设置,如下图所示:

在RASC中仅设置QSPI的时钟分频系数、在失能(Deselect)后的片选信号保持高电平的最小时钟个数。同样的也是需要根据通信设备的要求来设置,本章这里还是使用的是默认设置。

设置好QSPI的Stack模块之后,点击右上角的“Generate Project Content”生成代码。接下来就是去工程中看一下RASC的配置在工程中是如何体现的。

14.2.2 配置信息解读

配置信息分为两部分:引脚的配置信息、QSPI模块的配置信息。

QSPI涉及的引脚,它们配置信息在工程的pin_data.c中生成。在RASC里配置的每一个引脚,都会在pin_data.c生成一个ioport_pin_cfg_t数组项,里面的内容跟配置时选择的参数一致。代码如下:

const ioport_pin_cfg_t g_bsp_pin_cfg_data[] = {......(省略内容){   .pin = BSP_IO_PORT_03_PIN_05,.pin_cfg = ((uint32_t) IOPORT_CFG_PERIPHERAL_PIN | (uint32_t) IOPORT_PERIPHERAL_QSPI)},{   .pin = BSP_IO_PORT_03_PIN_06,.pin_cfg = ((uint32_t) IOPORT_CFG_PERIPHERAL_PIN | (uint32_t) IOPORT_PERIPHERAL_QSPI)},{   .pin = BSP_IO_PORT_03_PIN_07,.pin_cfg = ((uint32_t) IOPORT_CFG_PERIPHERAL_PIN | (uint32_t) IOPORT_PERIPHERAL_QSPI)},{   .pin = BSP_IO_PORT_03_PIN_08,.pin_cfg = ((uint32_t) IOPORT_CFG_PERIPHERAL_PIN | (uint32_t) IOPORT_PERIPHERAL_QSPI)},{   .pin = BSP_IO_PORT_03_PIN_09,.pin_cfg = ((uint32_t) IOPORT_CFG_PERIPHERAL_PIN | (uint32_t) IOPORT_PERIPHERAL_QSPI)},{   .pin = BSP_IO_PORT_03_PIN_10,.pin_cfg = ((uint32_t) IOPORT_CFG_PERIPHERAL_PIN | (uint32_t) IOPORT_PERIPHERAL_QSPI)},......(省略内容)
};

这段代码将QSPI的引脚P305~P310配置为QSPI外设复用功能,当使用GPIO的open函数时就会配置好这些引脚。

QSPI的配置信息,是在hal_data.c中生成。它定义了一个spi_flash_cfg_t结构体常量g_qspi0_cfg:

const spi_flash_cfg_t g_qspi0_cfg =
{.spi_protocol        = SPI_FLASH_PROTOCOL_EXTENDED_SPI,.read_mode           = SPI_FLASH_READ_MODE_FAST_READ_QUAD_IO,.address_bytes       = SPI_FLASH_ADDRESS_BYTES_3,.dummy_clocks        = SPI_FLASH_DUMMY_CLOCKS_4,.page_program_address_lines = SPI_FLASH_DATA_LINES_1,.page_size_bytes     = 256,.page_program_command = 0x02,.write_enable_command = 0x06,.status_command = 0x05,.write_status_bit    = 0,.xip_enter_command   = 0x20,.xip_exit_command    = 0xFF,.p_erase_command_list = &g_qspi0_erase_command_list[0],.erase_command_list_length = sizeof(g_qspi0_erase_command_list) / sizeof(g_qspi0_erase_command_list[0]),.p_extend            = &g_qspi0_extended_cfg,
};

这个结构体的成员就包含了QSPI的协议、模式、地址位数等配置信息。

14.2.3 API接口及其用法

在路径1401_qspi_w25q/ra/fsp/inc/api/r_spi_flash_api.h中定义了SPI Flash模块的接口,它定义了一个结构体类型spi_flash_api_t,内容如下:

/** SPI flash implementations follow this API. */
typedef struct st_spi_flash_api
{fsp_err_t (* open)(spi_flash_ctrl_t * p_ctrl, spi_flash_cfg_t const * const p_cfg);fsp_err_t (* directWrite)(spi_flash_ctrl_t * p_ctrl, uint8_t const * const p_src, uint32_t const bytes,bool const read_after_write);fsp_err_t (* directRead)(spi_flash_ctrl_t * p_ctrl, uint8_t * const p_dest, uint32_t const bytes);fsp_err_t (* directTransfer)(spi_flash_ctrl_t * p_ctrl, spi_flash_direct_transfer_t * const p_transfer,spi_flash_direct_transfer_dir_t direction);fsp_err_t (* spiProtocolSet)(spi_flash_ctrl_t * p_ctrl, spi_flash_protocol_t spi_protocol);fsp_err_t (* write)(spi_flash_ctrl_t * p_ctrl, uint8_t const * const p_src, uint8_t * const p_dest,uint32_t byte_count);fsp_err_t (* erase)(spi_flash_ctrl_t * p_ctrl, uint8_t * const p_device_address, uint32_t byte_count);fsp_err_t (* statusGet)(spi_flash_ctrl_t * p_ctrl, spi_flash_status_t * const p_status);fsp_err_t (* xipEnter)(spi_flash_ctrl_t * p_ctrl);fsp_err_t (* xipExit)(spi_flash_ctrl_t * p_ctrl);fsp_err_t (* bankSet)(spi_flash_ctrl_t * p_ctrl, uint32_t bank);fsp_err_t (* autoCalibrate)(spi_flash_ctrl_t * p_ctrl);fsp_err_t (* close)(spi_flash_ctrl_t * p_ctrl);
} spi_flash_api_t;

在r_qspi.c中实现了上述结构体:

const spi_flash_api_t g_qspi_on_spi_flash =
{.open           = R_QSPI_Open,.directWrite    = R_QSPI_DirectWrite,.directRead     = R_QSPI_DirectRead,.directTransfer = R_QSPI_DirectTransfer,.spiProtocolSet = R_QSPI_SpiProtocolSet,.write          = R_QSPI_Write,.erase          = R_QSPI_Erase,.statusGet      = R_QSPI_StatusGet,.xipEnter       = R_QSPI_XipEnter,.xipExit        = R_QSPI_XipExit,.bankSet        = R_QSPI_BankSet,.autoCalibrate  = R_QSPI_AutoCalibrate,.close          = R_QSPI_Close,
};

对于QSPI的操作,开发者可以使用spi_flash_api_t结构体的函数指针,也可以直接调用r_qspi.c实现的R_QSPI_xxx()函数。本书使用的是面向对象的编程思想,选择使用函数指针的操作方式。

接下来我们就来看下FSP中QSPI的一些基本操作。

  1. 打开QSPI设备

打开QSPI设备的函数指针原型如下:

fsp_err_t (* open)(spi_flash_ctrl_t * p_ctrl, spi_flash_cfg_t const * const p_cfg);

它有两个参数:

  • spi_flash_ctrl_t:这是一个void类型指针,实际会指向一个qspi_instance_ctrl_t的结构体变量,qspi_instance_ctrl_t的原型如下:
typedef struct st_qspi_instance_ctrl
{spi_flash_cfg_t const * p_cfg;            // Pointer to initial configurationspi_flash_data_lines_t  data_lines;       // Data linesuint32_t                total_size_bytes; // Total size of the flash in bytesuint32_t                open;             // Whether or not driver is open
} qspi_instance_ctrl_t;

这个结构体会指明QSPI设备的open状态和配置参数spi_flash_cfg_t;

  • spi_flash_cfg_t:QSPI设备的配置参数结构体,其原型如下:
typedef struct st_spi_flash_cfg
{spi_flash_protocol_t      spi_protocol;                      ///< Initial SPI protocol.  SPI protocol can be changed in @ref spi_flash_api_t::spiProtocolSet.spi_flash_read_mode_t     read_mode;                         ///< Read modespi_flash_address_bytes_t address_bytes;                     ///< Number of bytes used to represent the addressspi_flash_dummy_clocks_t  dummy_clocks;                      ///< Number of dummy clocks to use for fast read operations/** Number of lines used to send address for page program command. This should either be 1 or match the number of lines used in* the selected read mode. */spi_flash_data_lines_t            page_program_address_lines;uint8_t                           write_status_bit;          ///< Which bit determines write statusuint8_t                           write_enable_bit;          ///< Which bit determines write statusuint32_t                          page_size_bytes;           ///< Page size in bytes (maximum number of bytes for page program). Used to specify single continuous write size (bytes) in case of OSPI RAM.uint8_t                           page_program_command;      ///< Page program commanduint8_t                           write_enable_command;      ///< Command to enable write or erase, typically 0x06uint8_t                           status_command;            ///< Command to read the write statusuint8_t                           read_command;              ///< Read command - OSPI SPI mode onlyuint8_t                           xip_enter_command;         ///< Command to enter XIP modeuint8_t                           xip_exit_command;          ///< Command to exit XIP modeuint8_t                           erase_command_list_length; ///< Length of erase command listspi_flash_erase_command_t const * p_erase_command_list;      ///< List of all erase commands and associated sizesvoid const                      * p_extend;                  ///< Pointer to implementation specific extended configurations
} spi_flash_cfg_t;

在使用RASC配置QSPI的参数并生成工程后,会在hal_data.c定义一个常量g_qspi0_cfg,在调用open函数时就会用到g_qspi0_cfg(下列代码的g_qspi0.p_cfg):

fsp_err_t err = g_qspi0.p_api->open(g_qspi0.p_ctrl, g_qspi0.p_cfg);
if(FSP_SUCCESS != err)printf("Function:%s\tLine:%d\r\n", __FUNCTION__, __LINE__);
  1. QSPI的直接写和直接读

QSPI的直接写函数原型如下:

fsp_err_t (* directWrite)(spi_flash_ctrl_t * p_ctrl, uint8_t const * const p_src, uint32_t const bytes,bool const read_after_write);
  • p_ctrl:spi flash控制结构体指针;
  • p_src:源数据(要发送的数据)地址;
  • bytes:发送数据的个数;
  • read_after_write:发送数据结束后是否要立即读数据的标志位,true-会立即读;

QSPI直接读的函数原型如下:

fsp_err_t (* directRead)(spi_flash_ctrl_t * p_ctrl, uint8_t * const p_dest, uint32_t const bytes);
  • p_ctrl:spi flash控制结构体指针;
  • p_src:源数据(要发送的数据)地址;
  • bytes:读取数据的个数;

对于很多QSPI接口的存储器而言,想要读取它们的某些信息,比如ID、储存的数据等,往往都是要先发送读取指令,接着发送读取地址后,主机才能读到数据。因而对于读操作,需要先发起写操作。

下面是一个例子,读取W25Q64的ID:

unsigned int W25QDrvReadID(void)
{uint8_t cmd = 0x9F;uint8_t id[3] = {0};g_qspi0.p_api->directWrite(g_qspi0.p_ctrl, &cmd, 1, true);g_qspi0.p_api->directRead(g_qspi0.p_ctrl, id, 3);unsigned int ID = (id[0]<<16) | (id[1]<<8) | (id[2]);return ID;
}
  • 第03行:读取ID的指令是0x9F;
  • 第05~08行:使用直接写函数发送读ID指令,并且表明紧跟着会读取数据;
  • 第09行:使用直接读函数将读到的ID保存到数据id中;
  • 第10~11行:组合ID并返回给调用者;
  1. 使用QSPI对存储设备进行页写

QSPI封装了一个对QSPI接口存储器的页写函数,原型如下:

fsp_err_t (* write)(spi_flash_ctrl_t * p_ctrl, uint8_t const * const p_src, uint8_t * const p_dest,uint32_t byte_count);
  • p_ctrl:spi flash控制指针;
  • p_src:源数据(要发送的数据)地址;
  • p_dest:存储设备的目标地址;
  • byte_count:数据个数

这是一个页写功能的函数,如果byte_count超过RASC中配置的页大小,则写入失败。

要用好此函数,必须配合外部存储器的内存特性进行计算,得到地址偏移。

本章后面,会把扇区擦除、状态获取、页写函数等,封装出一个更好用的写函数,供读者参考使用。

  1. 使用QSPI擦除存储设备的扇区

对于一些ROM型的外部存储器,要想写入数据,必须先擦除。

以本章使用的实验对象W25Q64而言,它的最小擦除单位是1个扇区(一个扇区是由16个页组成),而FSP恰好也封装了一个对扇区的擦除函数,原型如下:

fsp_err_t (* erase)(spi_flash_ctrl_t * p_ctrl, uint8_t * const p_device_address, uint32_t byte_count);
  • p_device_address:要擦除的外部存储器的起始地址;
  • byte_count:要擦除的数据个数;

执行此函数时,byte_count必须等于可擦除块的大小(可能有多个取值,比如4096、32768、65536、SPI_FLASH_ERASE_SIZE_CHIP_ERASE)。

  1. 状态获取

在写、擦除QSPI之前,需要判断上一个操作是否完成。FSP也对这个功能进行了封装,函数原型如下:

fsp_err_t (* statusGet)(spi_flash_ctrl_t * p_ctrl, spi_flash_status_t * const p_status);
  • p_status:指向保存状态值的地址,是一个spi_flash_status_t结构体类型,此结构体的原型是:
typedef struct st_spi_flash_status
{bool write_in_progress;
} spi_flash_status_t;

只有一个bool成员write_in_progress,用来表示外部存储器是否“正在处理写操作”。

读者可以参考以下代码来获取状态值:

spi_flash_status_t status = {.write_in_progress = true};
while(status.write_in_progress == true)
{fsp_err_t err = g_qspi0.p_api->statusGet(g_qspi0.p_ctrl, &status);if(FSP_SUCCESS != err){printf("Function:%s\tLine:%d\r\n", __FUNCTION__, __LINE__);return;}
}

14.3 QSPI读写外部Flash

14.3.1 硬件连接

使用QSPI连接W25Q64的原理图如下图所示:

14.3.2 W25Q64驱动解析

W25Q64是华邦电子的一款支持SPI、扩展SPI(Single/Dual/Quad SPI)接口的外部存储器,存储大小是64M-bit。

写操作的单位是页,每一页有256 bytes。擦除操作的单位是扇区,每16页为一个扇区,也就是进行擦除操作时,最小的擦除单位是16*256 bytes=4096 bytes。

  1. W25Q64的指令表

对于W25Q64的所有操作都是执行对应的指令,因而需要先了解W25Q64支持哪些指令,指令表如下:

  • MF7-MF0:制造商ID;
  • ID15-ID8:内存类型;
  • ID7-ID0:生产ID;
  1. 读取W25Q64的ID

从指令表中可以看到读取ID有好几个指令:0xAB/0x90/0x4B/0x9F,也不难看出指令0x9F获取的ID内容是最丰富的,因而常用的就是0x9F。

对于W25Q64的各个ID的描述,在手册中有说明,如下图:

如果使用0x9F去读取ID,那么Byte2~Byte4组合起来就是0xEF4017。

读取JEDEC ID的时序是先发送指令0x9F,然后连续读取3个字节即可。

  1. W25Q64的写使能

要想对W25Q64进行写操作或者擦除操作,必须要先使能对W25Q64的写功能。写使能的指令是0x06,操作时序是主机发送一个0x06给W25Q64即可。

  1. 读取W25Q64的状态寄存器

W25Q64的状态寄存器有2个:register-1和register-2。它们表示W25Q64不同的控制状态,如下表所示:

S7S6S5S4S3S2S1S0
register-1SRP0SECTBBP2BP1BP0WELBUSY
S15S14S13S12S11S10S9S8
register-2RRRRRRQESRP1
  • BUSY:Erase or Write in process;1-busy;0-free;
  • WEL:Write Enable Latch;
  • BP1~BP2:Block Protect;
  • TB:Top/Bottom Write Protect;
  • SEC:Sector Protect;
  • SRP0:Status Register Protect;
  • SRP1: Status Register Protect 1;
  • QE:Quad Enable;

指令0x05读取状态寄存器1;指令0x35读取状态寄存器2。

  1. W25Q64的数据写操作

W25Q64有两种写操作:Page Program和Quad Input Page Program。这两者都是页写功能,它们有2个差别:指令不同(Page Program的指令是0x02,而Quad Input Page Program的指令是0x32),使用的数据线不同(Page Program使用1根数据线,Quad Input Page Program使用4跟数据线)。

下图是Page Program的时序图:

下图是Quad Input Page Program的时序图:

不管是Page Program还是Quad Input Page Program,都只能在已经擦除过的地方写数据,并且必须先发送使能指令0x01。

在页写的时候,如果写入的数据个数少于一页,且和该页已写的数据加起来也不会超过一页的数据量,那么从该页已写数据的末地址开始写入新数据的话不会影响之前已写的数据。

当写入的数据加上该页已写入的数据超过一页的数据量,那么超过的数据将会被舍弃,并不会写入到下一页。

  1. W25Q64的数据擦除操作

W25Q64有4种擦除方式:扇区擦除(0x20)、32KB块擦除(0x52)、64KB块擦除(0xD8)和整片擦除(0xC7/0x60)。

使用扇区擦除、块擦除需要在发送指令后,紧接着发送要擦除的扇区地址或者块地址,地址必须是扇区地址或块地址的整数倍。

当芯片内部执行擦除操纵时,状态标志寄存器1的BUSY会被置1,表示“处于忙状态”。

  1. W25Q64的数据读操作

W25Q64的读操作就有很多种方式了:

a) Read Data(0x03)

b) Fast Read Data(0x08)

c) Fast Read Dual Output(0x3B)

d) Fast Read Quad Output(0x6B)

e) Fast Read Dual IO(0xBB)

f) Fast Read Quad IO(0xEB)

不管是哪种读方式,都必须是在QSPI总线处于空闲状态下才能执行。

使用上述6种方式中的哪一种,需要结合QSPI模式来考虑。比如硬件上使用Quad模式,那么为了提高读取速度,软件上也应该使用Quad的方式去读。

只是在使用Fast Read读取数据的时候,需要根据手册的时序图来设置空读时钟,例如Fast Read Data方式下就需要空读一个字节,如下图所示:

14.3.3 W25Q64驱动程序

在介绍QSPI的API的时候已经知道,FSP已经封装了直接读写函数、扇区擦除函数和页写函数,本章实验是在这些函数的基础上封装了3个函数:读ID、状态等待、读写任意地址任意大小的数据。

  1. 读取ID

在分析W25Q64的ID读取到时候就已经清楚,要想读ID,首先要发送一个字节的指令数据,然后读3个字节的数据即可。函数代码如下:

unsigned int W25QDrvReadID(void)
{uint8_t cmd = 0x9F;uint8_t id[3] = {0};g_qspi0.p_api->directWrite(g_qspi0.p_ctrl, &cmd, 1, true);g_qspi0.p_api->directRead(g_qspi0.p_ctrl, id, 3);unsigned int ID = (id[0]<<16) | (id[1]<<8) | (id[2]);return ID;
}
  • 第05行:使用直接写函数发送cmd指令,且表示紧跟着会读取数据;
  • 第06行:读取3个字节的ID;
  • 第07行:将ID组合以便当作返回值提供给调用者;
  1. 等待擦除或写数据完成

在擦除或者写数据的时候,需要在QSPI总线空闲地时候进行,也就是状态寄存器1的BUSY位为0才可,因而设计了如下的等待函数:

static void W25QDrvWaitXfer(void)
{spi_flash_status_t status = {.write_in_progress = true};while(status.write_in_progress == true){fsp_err_t err = g_qspi0.p_api->statusGet(g_qspi0.p_ctrl, &status);if(FSP_SUCCESS != err){printf("Function:%s\tLine:%d\r\n", __FUNCTION__, __LINE__);return;}}
}
  1. 读取数据

本章使用的数据读取方式就是普通的QSPI Read Data方式,使用的指令是0x03。根据它的时序图,需要先用直接写函数发送1个字节的指令,再发送3个字节的地址,紧接着读取n字节的数据,因而封装了如下代码:

int W25QDrvRead(unsigned int dwAddr, unsigned char *buf, unsigned int dwSize)
{dwAddr = QSPI_DEVICE_START_ADDRESS + dwAddr;unsigned char cmd[4] = {0};cmd[0] = 0x03;  // read data commandcmd[1] = (dwAddr>>16)&0xFF;cmd[2] = (dwAddr>>8)&0xFF;cmd[3] = (dwAddr)&0xFF;W25QDrvWaitXfer();fsp_err_t err = g_qspi0.p_api->directWrite(g_qspi0.p_ctrl, cmd, 4, true);if(FSP_SUCCESS != err){printf("Function:%s\tLine:%d\r\n", __FUNCTION__, __LINE__);return -1;}err = g_qspi0.p_api->directRead(g_qspi0.p_ctrl, buf, dwSize);if(FSP_SUCCESS != err){printf("Function:%s\tLine:%d\r\n", __FUNCTION__, __LINE__);return -1;}// 返回独处数据的个数return (int)dwSize;
}
  • 第03行:因为QSPI接的外部存储器在内存中的映射起始地址是0x60000000,为了方便使用,调用者在调用读取数据的函数时只需要传入0~64MB的地址值即可,在此函数内部做内存映射偏移;
  • 第09行:需要等待QSPI总线空闲时才能读;
  • 第10行:使用直接写函数发送4字节数据:指令和地址,并且表示紧接着会读取数据;
  • 第16行:使用直接读函数从dwAddr处读取dwSize个字节的数据,存到buf;
  1. 写数据

要想在任意地址写入任意大小的数据,就稍微有点复杂了。需要考虑以下几个问题:

a) 要写入到这片区域是否擦除了?要擦除几个扇区?
b) 要写入到这个地址是否和页地址对齐?
c) 要写入到数据个数是否超过一页?
d) 数据个数超过一页又该如何写?

考虑到这些问题,本章设计了如下图这样的流程图:

再根据此流程图设计了下面的函数:可以在任意地址处写入任意个数据。

int W25QDrvWrite(unsigned int dwAddr, unsigned char *buf, unsigned int dwSize)
{/* 第1步 擦除扇区 */unsigned int dwOffsetAddr = 0;unsigned int dwStartAddr = dwAddr/4096*4096;unsigned int dwSectorCount = (dwSize + dwAddr%4096)/4096 + 1;while(dwSectorCount--){unsigned int nAddr = dwStartAddr + dwOffsetAddr;fsp_err_t  err = g_qspi0.p_api->erase(g_qspi0.p_ctrl, (uint8_t*)(QSPI_DEVICE_START_ADDRESS+nAddr), 4096);if(FSP_SUCCESS != err){printf("Function:%s\tLine:%d\r\n", __FUNCTION__, __LINE__);return -1;}W25QDrvWaitXfer();dwOffsetAddr += 4096;}/* 第2部 分页写 */unsigned int dwPageCount = (dwSize + dwAddr%256)/256 + 1;// 如果从起始地址开始偏移dwSize都没有超过一页就从起始地址开始写dwSize字节if(dwPageCount == 1){fsp_err_t err = g_qspi0.p_api->write(g_qspi0.p_ctrl, buf, (uint8_t*)(QSPI_DEVICE_START_ADDRESS+dwAddr), dwSize);if(FSP_SUCCESS != err){printf("Function:%s\tLine:%d\r\n", __FUNCTION__, __LINE__);return -1;}W25QDrvWaitXfer();}else{unsigned int nAddr = dwAddr;// 如果超过了一页则先将起始地址所在页填充满unsigned int dwFirstBytes = 256 - dwAddr%256;// 计算出写满起始地址所在页后剩余要写的数据个数unsigned int dwRestBytes = dwSize - dwFirstBytes;// 填充起始地址所在页fsp_err_t err = g_qspi0.p_api->write(g_qspi0.p_ctrl, buf, (uint8_t*)(QSPI_DEVICE_START_ADDRESS+nAddr), dwFirstBytes);if(FSP_SUCCESS != err){printf("Function:%s\tLine:%d\r\n", __FUNCTION__, __LINE__);return -1;}W25QDrvWaitXfer();// 将W25Q的地址偏移到下一页的起始地址nAddr += dwFirstBytes;// 要写入的数据buff地址也要更新buf += dwFirstBytes;// 开始将剩下的数据写入到W25Qwhile(dwRestBytes != 0){unsigned int nBytes = 0;// 剩下的数据个数不满一页的话// 最后一次就将剩下的数据全部写入到最后要写的这一页if(dwRestBytes <= 256)nBytes = dwRestBytes;elsenBytes = 256;err = g_qspi0.p_api->write(g_qspi0.p_ctrl, buf, (uint8_t*)(QSPI_DEVICE_START_ADDRESS+nAddr), nBytes);if(FSP_SUCCESS != err){printf("Function:%s\tLine:%d\r\n", __FUNCTION__, __LINE__);return -1;}W25QDrvWaitXfer();// W25Q地址和buf地址偏移,剩余个数递减刚才写入的数据个数nAddr += nBytes;buf += nBytes;dwRestBytes -= nBytes;}}// 返回写入数据的个数return (int)dwSize;
}

14.3.4 测试程序

本章使用QSPI驱动测试W25Q64,就是读取它的ID,以及在随机地址处写入随机大小的数据进去,然后再读出比较,测试代码如下:

#define TEST_SIZE       (300)
uint8_t wBuf[TEST_SIZE] = {0};
uint8_t rBuf[TEST_SIZE] = {0};void W25QAppTest(void)
{W25QDrvInit();printf("Device ID: 0x%.8x\r\n", W25QDrvReadID());for(uint16_t i=0; i<TEST_SIZE; i++)wBuf[i] = (uint8_t)(rand()%256);uint32_t dwAddr = (uint32_t)rand()%1000;uint32_t dwSize = (uint32_t)rand()%TEST_SIZE;W25QDrvWrite(dwAddr, wBuf, dwSize);W25QDrvRead(dwAddr, rBuf, dwSize);uint16_t wCnt = 0;for(uint16_t i=0; i<dwSize; i++){if(wBuf[i] != rBuf[i])wCnt++;}printf("\tRW Addr: 0x%.4x\r\n\tRW Size: %d\r\n\tError count: %d\r\n", dwAddr, dwSize, wCnt);
}

14.3.5 上机实验

在hal_entry.c中的hal_enter()函数调用测试函数,例如下代码:

#include "app.h"
#include "hal_systick.h"
#include "drv_w25q.h"
#include "drv_uart.h"
#include "hal_data.h"void hal_entry(void)
{/* TODO: add your own code here */SystickInit();UARTDrvInit();W25QAppTest();
}

将编译出来的二进制可执行文件烧录到处理器中运行,然后在串口助手中可以看到如下图这样的调试打印信息:

读者自行测试的时候,读写的地址和数据个数可能会和本书的不一样。


本章完

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/132229.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【golang】调度系列之m

调度系列 调度系列之goroutine 上一篇中介绍了goroutine&#xff0c;最本质的一句话就是goroutine是用户态的任务。我们通常说的goroutine运行其实严格来说并不准确&#xff0c;因为任务只能被执行。那么goroutine是被谁执行呢&#xff1f;是被m执行。 在GMP的架构中&#xff…

大橙子vfed 5.0去授权完美破解主题模版源码 | 苹果CMS

大橙子vfed 5.0去授权完美破解主题模版源码 | 苹果cms 大橙模版算是在苹果cms众多主题里&#xff0c;较为亮眼的一款了&#xff0c;主题简洁&#xff0c;功能众多&#xff0c;非常的齐全。 今天分享的就是大橙5.0版本模板&#xff0c;完美破解&#xff0c;自测无后门&#xf…

mysql中GROUP_CONCAT函数详解

GROUP_CONCAT是MySQL中的一个聚合函数&#xff0c;它用于将多行数据按照指定的顺序连接成一个字符串&#xff0c;并返回结果。下面是对GROUP_CONCAT函数的详解&#xff1a; 语法&#xff1a; GROUP_CONCAT([DISTINCT] expr [,expr …] [ORDER BY {unsigned_integer | col_name…

笔记(二)图的基本表示【斯坦福CS224W图机器学习】

1、基础知识 图是由节点和连接组成的 本体图&#xff0c;具体图是本体图的实例化&#xff0c;取决于想要解决什么问题 2、图的种类 异质图 异质图-二分图 异质图-二分图-展开 3、节点连接数 节点的度、入度和出度 4、图的基本表示 邻接矩阵 无向图的邻接矩阵是对称阵&#…

盘点中国光博会CIOE2023上的国货

一、中国光博会历史地位 CIOE中国光博会首次举办于1999年&#xff0c;经历24年在行业的深耕及拓展&#xff0c;是全球极具规模及影响力的综合型展会&#xff0c;见证了中国光电行业的变化及蓬勃发展。无论是超高清视频领域还是AI应用领域&#xff0c;均属于近年来的热门赛道&a…

【SpringMVC】JSON数据传输与异常处理的使用

文章目录 一、Jackson1.1 Jackson是什么1.2 常用注解1.3 实例1.3.1导入依赖1.3.2 配置spring-mvc.xml1.3.3 JsonController.java 二、Spring MVC异常处理机制2.1 使用原因2.2 SpringMVC异常处理2.2.1 异常处理机制流程图2.2.2 异常处理的三种方式 一、Jackson 1.1 Jackson是什…

中项系统集成项目管理2023上半年真题及解析

中项系统集成项目管理2023上半年真题及解析 上午题1. 在 (1) 领域&#xff0c;我国还远未达到世界先进水平&#xff0c;需要发挥新型举国体制优势&#xff0c;集中政府和市场两方面的力量全力发展2. ChatGPT于 2022年 11 月 30 日发布&#xff0c;它是人工智能驱动的 (2) 工具3…

机器学习基础之《分类算法(6)—决策树》

一、决策树 1、认识决策树 决策树思想的来源非常朴素&#xff0c;程序设计中的条件分支结构就是if-else结构&#xff0c;最早的决策树就是利用这类结构分割数据的一种分类学习方法 2、一个对话的例子 想一想这个女生为什么把年龄放在最上面判断&#xff01;&#xff01;&…

RabbitMQ常见问题

一、RabbitMQ如何保证消息不丢失&#xff1f; 这是面试时最喜欢问的问题&#xff0c;其实这是个所有MQ的一个共性的问题&#xff0c;大致的解 决思路也是差不多的&#xff0c;但是针对不同的MQ产品会有不同的解决方案。而RabbitMQ 设计之处就是针对企业内部系统之间进行调用设…

递归学习——记忆化搜索

目录 ​编辑 一&#xff0c;概念和效果 二&#xff0c;题目 1.斐波那契数 1.题目 2.题目接口 3.解题思路 2.不同的路径 1.题目 2.题目接口 3.解题思路 3.最长增长子序列 1.题目 2.题目接口 3.解题思路 4.猜数字游戏II 1.题目 2.题目接口 3.解题思路 总结&a…

【SpringMVC】Jrebel 插件实现热部署与文件上传

目录 一、JRebel 1.1 Jrebel介绍 1.2 Jrebel插件下载 1.3 Jrebel服务下载并启动 1.4 在线生成GUID 1.5 JRebel激活 1.6 相关设置 注意❗ 二、文件上传、下载 2.1 导入pom依赖 2.2 配置文件上传解析器 2.3 文件上传表单设置 2.4 文件上传实现 2.5 文件下载实现 2…

微信会员卡开发流程

功能需求&#xff1a; 通过微信第三方平台创建的模板小程序&#xff0c;想要实现用户在小程序支付一定金额后领取会员卡&#xff0c;领取会员卡后可给用户下发一定数量的优惠券&#xff0c;并且实现用户在小程序消费享受商品折扣。 开发流程&#xff1a; 一、了解微信的3个平…

HTTP协议的基本概念与理解!

一、什么是HTTP协议 HTTP&#xff08;超文本传输协议&#xff09;是一个基于请求与响应&#xff0c;无状态的&#xff0c;应用层的协议&#xff0c;常基于TCP/IP协议传输数据&#xff0c;互联网上应用最为广泛的一种网络协议,所有的WWW文件都必须遵守这个标准。设计HTTP的初衷…

【RocketMQ】消息的拉取

在上一讲中&#xff0c;介绍了消息的存储&#xff0c;生产者向Broker发送消息之后&#xff0c;数据会写入到CommitLog中&#xff0c;这一讲&#xff0c;就来看一下消费者是如何从Broker拉取消息的。 RocketMQ消息的消费以组为单位&#xff0c;有两种消费模式&#xff1a; 广播…

实时显示当前文件夹下的文件大小,shell脚本实现

图片来源于网络&#xff0c;如果侵权请联系博主删除&#xff01; 需求&#xff1a; 写一个shell终端命令&#xff0c;实时显示当前文件夹下的文件大小 实现&#xff1a; 您可以使用以下的Shell脚本命令来实时显示当前文件夹下的文件大小&#xff1a; while true; docleardu …

百度飞浆OCR识别表格入门python实践

1. 百度飞桨&#xff08;PaddlePaddle&#xff09; 百度飞桨&#xff08;PaddlePaddle&#xff09;是百度推出的一款深度学习平台&#xff0c;旨在为开发者提供强大的深度学习框架和工具。飞桨提供了包括OCR&#xff08;光学字符识别&#xff09;在内的多种功能&#xff0c;可…

《动手学深度学习 Pytorch版》 4.10 实战Kaggle比赛:预测比赛

4.10.1 下载和缓存数据集 import hashlib import os import tarfile import zipfile import requests#save DATA_HUB dict() DATA_URL http://d2l-data.s3-accelerate.amazonaws.com/def download(name, cache_diros.path.join(.., data)): #save"""下载一个…

【chromium】windows 获取源码到本地

从github的chromium 镜像git clone 到2.5G失败了官方说不能,要去 windows_build_instructions vs2017和19都是32位的 vs2022是x64的 vs2022_install You may also have to set variable vs2022_install to your installation path of Visual Studio 2022,

自定义Dynamics 365实施和发布业务解决方案 3. 开箱即用自定义

在本章中,您将开始开发SBMA会员应用程序。在开发的最初阶段,主要关注开箱即用的定制。在第2章中,我们讨论了如何创建基本解决方案的细节,在本章中,将创建作为解决方案补丁的基本自定义,并展示将解决方案添加到源代码管理和目标环境的步骤。 表单自定义 若要开始表单自定…

宠物行业如何进行软文营销

如今&#xff0c;宠物已经成为了人们生活中不可或缺的一部分&#xff0c;大众对于萌宠的喜爱与日俱增&#xff0c;随着“萌宠经济”升温&#xff0c;越来越多的商机开始出现&#xff0c;伴随着宠物市场竞争的日益激烈&#xff0c;宠物行业的营销光靠硬广告很难吸引受众&#xf…