第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
- QSPI框图
RA6M5的QSPI框图如下图所示:

通过QSPI模块,可以方便地编写程序,使用QSPI协议访问外设。
- SPI总线协议
QSPI向下兼容普通SPI、Dual SPI。所以它支持的SPI总线协议有3种:Single SPI/Dual SPI/Quad SPI,这三种协议需要的控制线和数据线看下表:
| IO | Single SPI | Dual SPI | Quad SPI |
|---|---|---|---|
| QSSL/CS | ☑ | ☑ | ☑ |
| QSPCLK/SCK | ☑ | ☑ | ☑ |
| QIO0 | ☑ | ☑ | ☑ |
| QIO1 | ☑ | ☑ | ☑ |
| QIO2 | ☒ | ☒ | ☑ |
| QIO3 | ☒ | ☒ | ☑ |
- SPI模式
RA6M5的QSPI有2种SPI模式:SPI Mode0和SPI Mode3,对应的特点如下:
- SPI Mode 0:时钟信号线QSPCLK在SPI总线空闲地时候呈低电平;
- SPI Mode 3:时钟信号线QSPCLK在SPI总线空闲地时候呈高电平;
- 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引脚 |
|---|---|---|
| 1 | CS | P306 |
| 2 | SCK | P305 |
| 3 | IO0 | P307 |
| 4 | IO1 | P308 |
| 5 | IO2 | P309 |
| 6 | IO3 | P310 |
其中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的一些基本操作。
- 打开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__);
- 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并返回给调用者;
- 使用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中配置的页大小,则写入失败。
要用好此函数,必须配合外部存储器的内存特性进行计算,得到地址偏移。
本章后面,会把扇区擦除、状态获取、页写函数等,封装出一个更好用的写函数,供读者参考使用。
- 使用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)。
- 状态获取
在写、擦除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。
- W25Q64的指令表
对于W25Q64的所有操作都是执行对应的指令,因而需要先了解W25Q64支持哪些指令,指令表如下:


- MF7-MF0:制造商ID;
- ID15-ID8:内存类型;
- ID7-ID0:生产ID;
- 读取W25Q64的ID
从指令表中可以看到读取ID有好几个指令:0xAB/0x90/0x4B/0x9F,也不难看出指令0x9F获取的ID内容是最丰富的,因而常用的就是0x9F。
对于W25Q64的各个ID的描述,在手册中有说明,如下图:

如果使用0x9F去读取ID,那么Byte2~Byte4组合起来就是0xEF4017。
读取JEDEC ID的时序是先发送指令0x9F,然后连续读取3个字节即可。
- W25Q64的写使能
要想对W25Q64进行写操作或者擦除操作,必须要先使能对W25Q64的写功能。写使能的指令是0x06,操作时序是主机发送一个0x06给W25Q64即可。

- 读取W25Q64的状态寄存器
W25Q64的状态寄存器有2个:register-1和register-2。它们表示W25Q64不同的控制状态,如下表所示:
| S7 | S6 | S5 | S4 | S3 | S2 | S1 | S0 | |
|---|---|---|---|---|---|---|---|---|
| register-1 | SRP0 | SEC | TB | BP2 | BP1 | BP0 | WEL | BUSY |
| S15 | S14 | S13 | S12 | S11 | S10 | S9 | S8 | |
| register-2 | R | R | R | R | R | R | QE | SRP1 |
- 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。
- 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。
在页写的时候,如果写入的数据个数少于一页,且和该页已写的数据加起来也不会超过一页的数据量,那么从该页已写数据的末地址开始写入新数据的话不会影响之前已写的数据。

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

- W25Q64的数据擦除操作
W25Q64有4种擦除方式:扇区擦除(0x20)、32KB块擦除(0x52)、64KB块擦除(0xD8)和整片擦除(0xC7/0x60)。
使用扇区擦除、块擦除需要在发送指令后,紧接着发送要擦除的扇区地址或者块地址,地址必须是扇区地址或块地址的整数倍。
当芯片内部执行擦除操纵时,状态标志寄存器1的BUSY会被置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、状态等待、读写任意地址任意大小的数据。
- 读取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组合以便当作返回值提供给调用者;
- 等待擦除或写数据完成
在擦除或者写数据的时候,需要在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;}}
}
- 读取数据
本章使用的数据读取方式就是普通的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;
- 写数据
要想在任意地址写入任意大小的数据,就稍微有点复杂了。需要考虑以下几个问题:
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();
}
将编译出来的二进制可执行文件烧录到处理器中运行,然后在串口助手中可以看到如下图这样的调试打印信息:

读者自行测试的时候,读写的地址和数据个数可能会和本书的不一样。
相关文章:
第14章_瑞萨MCU零基础入门系列教程之QSPI
本教程基于韦东山百问网出的 DShanMCU-RA6M5开发板 进行编写,需要的同学可以在这里获取: https://item.taobao.com/item.htm?id728461040949 配套资料获取:https://renesas-docs.100ask.net 瑞萨MCU零基础入门系列教程汇总: ht…...
【pygame】01 pygame制作游戏的最小系统
这次使用sublimepython进行pygame的游戏开发,目的是学习使用python的基本操作和常用模块 添加一个文件夹到工程 最小系统 1.导入使用的模块 2.初始化:pygame.init函数包含了各个子模块的初始化,可以重复调用 3.pygame.display.set_mode返…...
(文末赠书)我为什么推荐应该人手一本《人月神话》
能点进来的朋友,说明你肯定是计算机工作的朋友或者对这本书正在仔细琢磨着的朋友。 文章目录 1、人人都会编程的时代,我们如何留存?2、小故事说明项目管理着为什么必看这本书3、如何评价《人月神话:纪念典藏版》4、本书的目录(好…...
回文串 rust解法
输入一个字符串,判断它是否为回文串。 输入字符串保证不含数字0。所谓回文串,就是反转以后和原串相同,如abba和madam。 样例输入: NOTAPALINDROME ISAPALINILAPASI 样例输出: not huiwen huiwen 解法: u…...
echarts常用参数详解汇总(饼图,柱形图,折线图)持续更新中
常用配置: X/Y轴线的基础设置《通用》 细微的差距只能去官网查看了,基本一致 这里只是做了个汇总方便查看 xAxis/yAxis: {show:false, // 不显示坐标轴线、坐标轴刻度线和坐标轴上的文字axisTick:{// 不显示坐标轴刻度线show:false, alignWithLabel: tru…...
最新ChatGPT网站源码+支持GPT4.0+支持Midjourney绘画+支持国内全AI模型
一、智能创作系统 SparkAi创作系统是基于国外很火的ChatGPT进行开发的Ai智能问答系统。本期针对源码系统整体测试下来非常完美,可以说SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。那么如何搭建部署AI创作ChatGPT?小编这里写一个详细图文教程吧&…...
【MySQL】基础SQL语句——库的操作
文章目录 一. 创建数据库1.1 基础语句1.2 字符集和校验规则1.3 校验规则对读取数据的影响 二. 查看数据库三. 修改数据库四. 删除数据库及备份4.1 删除4.2 备份和还原 结束语 一. 创建数据库 1.1 基础语句 最简洁的创建数据库的SQL语句是: create database db_nam…...
基于YOLOv8模型的海洋生物目标检测系统(PyTorch+Pyside6+YOLOv8模型)
摘要:基于YOLOv8模型的海洋生物目标检测系统可用于日常生活中检测与定位海洋生物目标,利用深度学习算法可实现图片、视频、摄像头等方式的目标检测,另外本系统还支持图片、视频等格式的结果可视化与结果导出。本系统采用YOLOv8目标检测算法训…...
华为星闪联盟:引领无线通信技术创新的先锋
星闪(NearLink),是由华为倡导并发起的新一代无线短距通信技术,它从零到一全新设计,是为了满足万物互联时代个性化、多样化的极致、创新体验需求而诞生的。这项技术汇聚了中国300多家头部企业和机构的集体智慧ÿ…...
炒期权的资金门槛是多少 ?
期权是一种合约,买方向卖方支付一定费用后有权利在特定的时间,以特定的价格买入或卖出一定数量的特定资产,卖方需履行相应义务,期权开户支持线上和零门槛开头,下文介绍炒期权的资金门槛是多少 ?本文来自:期…...
matlab根轨迹绘制
绘制根轨迹目的就是改变系统的闭环极点,使得系统由不稳定变为稳定或者使得稳定的系统变得更加稳定。 在使用PID控制器的时候,首先要确定的参数是Kp,画成框图的形式如下: 也就是想要知道Kp对系统性能有哪些影响,此时就…...
Vue错误记录
文章目录 1. 项目build的时候报错Warning: Accessing non-existent property cat of module exports inside circular dependency2. WebpackOptionsValidationError: Invalid configuration object. Webpack has been initialised using a configuration object that does not …...
Linux 修改SSH的显示样式,修改终端shell显示的样式,美观更改
要修改SSH的显示样式,您可以使用自定义的PS1(提示字符串1)变量来更改命令行提示符的外观。在您的情况下,您想要的格式似乎包括日期和时间,以及当前目录。以下是一个示例PS1设置,可以实现您所描述的样式&…...
day40 设计模式、jdk8新特性
一、代理模式 为其他对象提供一种代理控制此对象的访问 若一个对象不适合直接引用另一个对象, 代理对象在客户端和目标对象之间起到中介作用 组成: 抽象角色:通过接口 抽象类 真实角色实现了哪些方法 代理角色:实现抽象角色…...
【数据结构】链表C++编写的,它定义了一个链表,并实现了一些基本的链表操作,如创建新节点、插入节点、清空链表、输出链表以及查找节点
// 引入标准输入输出流库,用于输出操作 #include <iostream> // 引入标准库中的stdlib,包含了rand()函数和其他相关函数 #include <cstdlib> // 引入标准库中的time,包含了time()函数和其他相关函数 #include <ctim…...
浏览器面试题
浏览器面试题 1.常见的浏览器内核有哪些?2.浏览器的主要组成部分有哪些?3.说一说从输入URL到页面呈现发生了什么?4.浏览器重绘域重排的区别?5.CSS加载会阻塞DOM吗?6.JS会阻塞页面吗?7.说一说浏览器的缓存机…...
Java Controller层异常处理示例【含面试题】
AI绘画关于SD,MJ,GPT,SDXL百科全书 面试题分享点我直达 2023Python面试题 2023最新面试合集链接 2023大厂面试题PDF 面试题PDF版本 java、python面试题 项目实战:AI文本 OCR识别最佳实践 AI Gamma一键生成PPT工具直达链接 玩转cloud Studio 在线编码神器 玩转 GPU AI…...
通过Git Bash将本地文件上传到本地github
1. 新建一个仓库( Repository) 1.1登录Github,点击个人头像,点击Your repositories,点击New。 1.2 填写信息 Repository name: 仓库名称 Description(可选): 仓库描述介绍,不是必填项目。~~建议填写上哦!…...
继承的笔记
继承 对象代表什么, 就得封装对应的数据, 并提供数据对应的行为 对于两种不同的类, 但是具有很多共同的属性的时候我们就想着用继承, 我们可以将共同的属性放置在一个类中, 然后, 只需要新建两个类, 继承共有的类, 然后单独写自己的属性特点 继承类 Java 中提供了一个关键字…...
Android7.1 ROOT权限的获取
修改文件: system/extras/su/su.c system/core/include/private/android_filesystem_config.h system/core/libcutils/fs_config.c frameworks/base/core/jni/com_android_internal_os_Zygote.cpp frameworks/base/cmds/app_process/app_main.cpp device/qcom…...
使用VSCode开发Django指南
使用VSCode开发Django指南 一、概述 Django 是一个高级 Python 框架,专为快速、安全和可扩展的 Web 开发而设计。Django 包含对 URL 路由、页面模板和数据处理的丰富支持。 本文将创建一个简单的 Django 应用,其中包含三个使用通用基本模板的页面。在此…...
Debian系统简介
目录 Debian系统介绍 Debian版本介绍 Debian软件源介绍 软件包管理工具dpkg dpkg核心指令详解 安装软件包 卸载软件包 查询软件包状态 验证软件包完整性 手动处理依赖关系 dpkg vs apt Debian系统介绍 Debian 和 Ubuntu 都是基于 Debian内核 的 Linux 发行版ÿ…...
MySQL中【正则表达式】用法
MySQL 中正则表达式通过 REGEXP 或 RLIKE 操作符实现(两者等价),用于在 WHERE 子句中进行复杂的字符串模式匹配。以下是核心用法和示例: 一、基础语法 SELECT column_name FROM table_name WHERE column_name REGEXP pattern; …...
重启Eureka集群中的节点,对已经注册的服务有什么影响
先看答案,如果正确地操作,重启Eureka集群中的节点,对已经注册的服务影响非常小,甚至可以做到无感知。 但如果操作不当,可能会引发短暂的服务发现问题。 下面我们从Eureka的核心工作原理来详细分析这个问题。 Eureka的…...
Spring是如何解决Bean的循环依赖:三级缓存机制
1、什么是 Bean 的循环依赖 在 Spring框架中,Bean 的循环依赖是指多个 Bean 之间互相持有对方引用,形成闭环依赖关系的现象。 多个 Bean 的依赖关系构成环形链路,例如: 双向依赖:Bean A 依赖 Bean B,同时 Bean B 也依赖 Bean A(A↔B)。链条循环: Bean A → Bean…...
从物理机到云原生:全面解析计算虚拟化技术的演进与应用
前言:我的虚拟化技术探索之旅 我最早接触"虚拟机"的概念是从Java开始的——JVM(Java Virtual Machine)让"一次编写,到处运行"成为可能。这个软件层面的虚拟化让我着迷,但直到后来接触VMware和Doc…...
无需布线的革命:电力载波技术赋能楼宇自控系统-亚川科技
无需布线的革命:电力载波技术赋能楼宇自控系统 在楼宇自动化领域,传统控制系统依赖复杂的专用通信线路,不仅施工成本高昂,后期维护和扩展也极为不便。电力载波技术(PLC)的突破性应用,彻底改变了…...
uniapp获取当前位置和经纬度信息
1.1. 获取当前位置和经纬度信息(需要配置高的SDK) 调用uni-app官方API中的uni.chooseLocation(),即打开地图选择位置。 <button click"getAddress">获取定位</button> const getAddress () > {uni.chooseLocatio…...
SOC-ESP32S3部分:30-I2S音频-麦克风扬声器驱动
飞书文档https://x509p6c8to.feishu.cn/wiki/SKZzwIRH3i7lsckUOlzcuJsdnVf I2S简介 I2S(Inter-Integrated Circuit Sound)是一种用于传输数字音频数据的通信协议,广泛应用于音频设备中。 ESP32-S3 包含 2 个 I2S 外设,通过配置…...
SFTrack:面向警务无人机的自适应多目标跟踪算法——突破小尺度高速运动目标的追踪瓶颈
【导读】 本文针对无人机(UAV)视频中目标尺寸小、运动快导致的多目标跟踪难题,提出一种更简单高效的方法。核心创新在于从低置信度检测启动跟踪(贴合无人机场景特性),并改进传统外观匹配算法以关联此类检测…...
