目录
一、Flash知识简介
二、SPI知识简介
1.SPI引脚介绍
2.SPI协议介绍
3.ZYNQ Quad SPI说明
三、Vivado工程搭建
四、编写Vitis程序
1.ZYNQ QSPI Flash操作的格式:
2.头文件:qspi_hdl.h
3.源文件:qspi_hdl.c
4.编写QSPI Flash读写测试函数
五、实测结果
例程开发环境:
SOC芯片:ZYNQ7020
开发环境:Vivado2020.2,Vitis2020.2
Flash芯片:W25Q256,即256Mb,32MB
一、Flash知识简介
Flash存储器也叫闪存,是一种非易失性存储器,说白了就是数据掉电不丢失,所以一般用来存放运行程序或者需要掉电保存的数据,并且flash具有操作方便、读写速度快的优点。
Flash存储数据时,只能将1写为0,不能将0写为1,因此对flash进行擦除操作时,就是将flash对应区域全部置1,写数据时,就将对应bit置0即可。
Flash内部区域划分:级别从大到小一般是:整片(chip)>块(block、bulk、bank)>扇区(sector)>页(page)
其中页为最小划分的区域单位,其内部一般包含若干字节,例如一页包含256字节等,但是目前大部分厂商常用的最小单位划分基本都是扇区,具体是啥还是要看芯片手册来确定;
下面是我是用的W25Q256 flash芯片的区域划分情况
- 最小单位为扇区,每个扇区容量大小4KB,即4096字节
- 每个块包含16个扇区,所以每个块大小64KB
- 整片flash共有512个块,共32MB存储空间
二、SPI知识简介
1.SPI引脚介绍
/CS:片选引脚,低电平有效,拉低时,表明使能该芯片的操作
VCC:电源引脚
GND:接地引脚
CLK:输入时钟引脚,用于SPI通信同步
IO0(MISO):数据输入引脚
IO1(MOSI)数据输出引脚
IO2(WP):写保护引脚,低电平时,flash无法被写入数据,在Quad SPI模式下复用为数据引脚
IO3(HOLD):暂停通讯引脚,拉低时,DO为高阻态,flash暂停其余操作保持现有状态,等待HOLD拉高,再恢复之前的通讯,在Quad SPI模式下复用为数据引脚
2.SPI协议介绍
SPI为标准通信协议,不仅可以操作flash,还可以与其他类型器件进行通信,但是由于SPI标准通信协议为全双工,且速度较慢,因此实际读写flash时,一般都使用其扩展协议,即Dual flash和Quad flash
- SPI接口协议:使用IO0(MISO)和IO1(MOSI)这两个进行读写,IO0输入使用,IO1输出使用,可同时进行读取操作,因此为全双工通信,但是一般读写flash时很少使用全双工模式,所以操作flash时,标准SPI协议用的很少
- Dual SPI接口协议:同样使用IO0和IO1这两个进行读写,IO0和IO1只能同时向一个方向发送数据或同时读取数据,因此Dual SPI属于半双工通信,但是同一时刻可以读取2bit或写入2bit数据,是标准SPI协议速度的两倍
- Quad SPI接口协议:增加两个读写flash的IO(WP和HOLD复用为数据IO),同时使用IO0- IO3四线进行读或写,同样为半双工通信,同一时刻可以传输4bit数据,是标准SPI通信速度的4倍,常用于操作flash的读写
注意:Dual SPI和Quad SPI一般只用于读写flash使用,不控其他类型器件
3.ZYNQ Quad SPI说明
ZYNQ QSPI Flash控制器通过MIO与外部 Flash 器件连接,支持三种模式:单个从
器件模式、双从器件并行模式和双从器件堆模式:
(1)单个从器件模式:即外接单个 flash,通过 4bit I/O(即 quad 、dual 或单线)与 flash 进行通信。
(2)双从器件并行模式:把每个 flash 的 IO 进行了单独的连接,扩展成 8bit 用于同时访问两块 flash,实现扩展 QSPI Flash 容量。
(3)双从器件堆叠模式:使用片选 SS 信号进行区分 flash的使能。对 flash 仍然是 4bit,即同一时间只能操作一块 flash。通过使用双从器件模式可以扩展 QSPI Flash
的存储容量
下图是ZYNQ QSPI Flash三种使用模式的框图以及block design中的配置使用方法
三、Vivado工程搭建
ZYNQ QSPI Flash为PS核内置功能,属于硬核,直接对PS端进行配置即可使用,不需要增加PL端的任何IP核;因此本项目工程是在ZYNQ-Vitis(SDK)裸机开发之(一)串口实验工程基础上开发的,一些block design的设计方法,Vitis工程的建立方法等,均在该篇文章中进行了详细的讲解,大家可以去参考:
ZYNQ-Vitis(SDK)裸机开发之(一)串口收发使用:PS串口+PL串口、多个串口使用方法
PS核需要勾选上QSPI,我的只有一片flash,因此选的Signal SS 4-bit IO选项,具体引脚约束根据自己项目原理图确定
四、编写Vitis程序
1.ZYNQ QSPI Flash操作的格式:
(1)在向flash中写数据时,传入的buffer中结构应按照如下放置数据:
第0个字节:放要下发的指令号
第1-3个字节:放要操作数据的起始地址,当然如果某些指令不需要操作数据,例如读取flash ID,这种的话1-3字节就不需要填写数据,后者随便填就行,作为空闲字节使用
从第4个字节开始:为纯数据区,即需要写入flash内部的数据
(2)在从flash中读数据时,读取的buffer中的数据结构如下所示:
使用普通读指令READ_CMD读取数据时,返回的数据结构与写入时的数据结构一致,提取数据时从第4个字节开始提取,可见下图所示:
使用Fast、Dual、Quad这三种模式读取的时候,读取回来的数据结构中,多出一个空闲字节Dummy,在数据区的前面,因此此时提取数据时,应该从第5个字节开始提取
2.头文件:qspi_hdl.h
(1)定义QSPI器件ID号
(2)定义flash芯片操作指令,这个需要根据自己使用芯片的手册进行修改
(3)定义flash操作指令、起始地址、空闲字节、数据等的偏移地址
(4)定义空闲字节Dummy、读ID、擦除指令、buffer头部所占字节数量
(5)定义flash芯片的容量参数,包括页数、页字节、扇区数、扇区字节等
(6)定义要操作的flash部分区域,包括读写起始地址,读写范围、读写的字节数量等等
(7)声明QSPI Flash操作相关的函数,QSPI初始化、读写操作、擦除操作、读flash ID、使能Quad SPI模式等
/*!
\file qspi_hdl.h
\brief firmware functions to manage qspi
\version 2024-04-15, V1.0.0
\author tbj
*/
#ifndef QSPI_HDL_H
#define QSPI_HDL_H
#include "xqspips.h"
//QSPI器件ID
#define QSPI_DEVICE_ID XPAR_XQSPIPS_0_DEVICE_ID
//flash操作指令
#define READ_CMD 0x03 //读指令
#define WRITE_CMD 0x02 //写指令
#define READ_STATUS_CMD 0x05 //读状态指令
#define WRITE_STATUS_CMD 0x01 //写状态指令
#define WRITE_ENABLE_CMD 0x06 //写使能指令
#define WRITE_DISABLE_CMD 0x04 //禁止写使能指令
#define FAST_READ_CMD 0x0B //单通道读取
#define DUAL_READ_CMD 0x3B //双通道读取-半双工
#define QUAD_READ_CMD 0x6B //四通道读取-半双工
#define BULK_ERASE_CMD 0xC7 //擦除整片flash-全部写1
#define SEC_ERASE_CMD 0xD8 //擦除一个扇区-全部写1
#define READ_ID 0x9F //读取flash ID指令
//定义flash操作指令、地址、数据等在读写buffer中的位置
#define COMMAND_OFFSET 0 //flash操作指令在写buffer中的位置(第0个字节)
#define ADDRESS_1_OFFSET 1 //操作flash数据起始地址的高字节在写buffer中的位置(第1个字节)
#define ADDRESS_2_OFFSET 2 //操作flash数据起始地址的中字节在写buffer中的位置(第2个字节)
#define ADDRESS_3_OFFSET 3 //操作flash数据起始地址的低字节在写buffer中的位置(第3个字节)
#define DATA_OFFSET 4 //操作flash的数据,读取或写入的数据,在写buffer中的位置(第4个字节开始是纯数据区)
#define DUMMY_OFFSET 4 //空闲字节的位置,当使用快速、双线、四线模式读取数据时,空闲字节占读buffer的第4个字节,纯数据区从第5个字节开始
//定义各种操作所需字节长度
#define DUMMY_SIZE 1 //空闲字节大小占1个字节(当使用快速、双线、四线模式读取数据时存在dummy byte)
#define RD_ID_SIZE 4 //读取flash ID占字节数,其中第0个字节为读取ID指令号,后3个字节为读取到的flash ID号
#define BULK_ERASE_SIZE 1 //清空整片flash指令占字节数,只需要一个清空整片flash的指令号
#define SEC_ERASE_SIZE 4 //按扇区清空flash指令占字节数,其中第0字节为按扇区清空flash的指令号,后3个字节是起始清空的flash地址
#define OVERHEAD_SIZE 4 //定义读写buffer头部数据长度,包括指令号1字节和操作地址3字节
//定义flash的参数数据
#define SECTOR_SIZE 0x10000 //定义单个扇区大小-64KB(根据自己flash芯片手册确定)
#define NUM_SECTORS 0x200 //定义扇区数量-256个(根据自己flash芯片手册确定)
#define NUM_PAGES 0x20000 //定义页数量-65536个(根据自己flash芯片手册确定)
#define PAGE_SIZE 256 //定义每页字节数-256个字节(根据自己flash芯片手册确定)
//定义实际读写操作的范围和数据大小
#define PAGE_COUNT 16 //定义需要操作读写的页数
#define TEST_ADDRESS 0x01FF0000//0x00055000 //定义读写操作的起始地址
#define UNIQUE_VALUE 0x05 //定义读写操作的起始值
#define MAX_DATA (PAGE_COUNT * PAGE_SIZE) //定义读写操作的最大数据量
//定义QSPI操作结构体对象
XQspiPs QspiInstance;
#ifdef __cplusplus
extern "C" {
#endif
//初始化QSPI控制器
int Qspi_Init(XQspiPs *QspiInstancePtr);
//通过QSPI将数据写入flash中
void FlashWrite(XQspiPs *QspiPtr, u32 Address, u8 *WriteBuf, u32 ByteCount, u8 Command);
//通过QSPI读取flash中的数据
void FlashRead(XQspiPs *QspiPtr, u32 Address, u8 *ReadBuf, u32 ByteCount, u8 Command);
//擦除flash
void FlashErase(XQspiPs *QspiPtr, u32 Address, u32 ByteCount);
//读取flash ID
int FlashReadID(void);
//使能四线模式
void FlashQuadEnable(XQspiPs *QspiPtr);
#ifdef __cplusplus
}
#endif
#endif /* QSPI_HDL_H */
3.源文件:qspi_hdl.c
(1)对头文件总QSPI初始化、读写操作、擦除操作、读flash ID、使能Quad SPI模式等函数进行实现
/*!
\file qspi_hdl.c
\brief firmware functions to manage qspi
\version 2024-04-15, V1.0.0
\author tbj
*/
#include "qspi_hdl.h"
//QSPI读写flash使用的buffer,内部使用
static u8 FlashReadBuffer[MAX_DATA + DATA_OFFSET + DUMMY_SIZE];
static u8 FlashWriteBuffer[PAGE_SIZE + DATA_OFFSET];
/* 功能:初始化QSPI控制器
* 入参1:QSPI控制器实例化对象指针
*/
int Qspi_Init(XQspiPs *QspiInstancePtr){
int Status;
XQspiPs_Config *QspiConfig;
//初始化QSPI控制器
QspiConfig = XQspiPs_LookupConfig(QSPI_DEVICE_ID);
if (QspiConfig == NULL) {
return XST_FAILURE;
}
Status = XQspiPs_CfgInitialize(QspiInstancePtr, QspiConfig,
QspiConfig->BaseAddress);
if (Status != XST_SUCCESS) {
return XST_FAILURE;
}
//QSPI控制器自检,保证初始化成功
Status = XQspiPs_SelfTest(QspiInstancePtr);
if (Status != XST_SUCCESS) {
return XST_FAILURE;
}
//清空读写操作的buffer
memset(FlashWriteBuffer, 0x00, sizeof(FlashWriteBuffer));
memset(FlashReadBuffer, 0x00, sizeof(FlashReadBuffer));
//将flash配置为手动启动、手动片选模式,将hold(reset)引脚配置为高电平,hold低电平,暂停收发数据,高电平恢复收发数据
Status |= XQspiPs_SetOptions(QspiInstancePtr, XQSPIPS_MANUAL_START_OPTION |
XQSPIPS_FORCE_SSELECT_OPTION |
XQSPIPS_HOLD_B_DRIVE_OPTION);
//设置QSPI预分频系数
Status |= XQspiPs_SetClkPrescaler(QspiInstancePtr, XQSPIPS_CLK_PRESCALE_8);
//将片选信号置为有效
Status |= XQspiPs_SetSlaveSelect(QspiInstancePtr);
//读取flash ID
Status |= FlashReadID();
//使能QSPI Quad模式
FlashQuadEnable(QspiInstancePtr);
return Status;
}
/**
* @brief 通过QSPI将数据写入flash中
* @param QSPI结构体指针
* @param 要写入数据的起始地址
* @param 要写入数据的数量-按字节
* @param 写数据指令
* @return 无
* @note 无
* */
void FlashWrite(XQspiPs *QspiPtr, u32 Address, u8 *WriteBuf, u32 ByteCount, u8 Command)
{
u8 WriteEnableCmd = { WRITE_ENABLE_CMD };
u8 ReadStatusCmd[] = { READ_STATUS_CMD, 0 }; /* must send 2 bytes */
u8 FlashStatus[2];
//发送写使能指令
XQspiPs_PolledTransfer(QspiPtr, &WriteEnableCmd, NULL,
sizeof(WriteEnableCmd));
//将写操作指令以及数据地址写入对应待发送buffer的前4个字节,第五个字节开始才是写入的数据
FlashWriteBuffer[COMMAND_OFFSET] = Command;
FlashWriteBuffer[ADDRESS_1_OFFSET] = (u8)((Address & 0xFF0000) >> 16);
FlashWriteBuffer[ADDRESS_2_OFFSET] = (u8)((Address & 0xFF00) >> 8);
FlashWriteBuffer[ADDRESS_3_OFFSET] = (u8)(Address & 0xFF);
//将要写入的纯数据,写入到flash写操作buffer中(flash写操作buffer包括写指令、地址、数据等内容)
memcpy(FlashWriteBuffer + 4, WriteBuf, ByteCount);
//将写指令、写起始地址信息、写入数据内容,写到flash中
XQspiPs_PolledTransfer(QspiPtr, FlashWriteBuffer, NULL,
ByteCount + OVERHEAD_SIZE);
//等待数据写入完毕
while (1) {
//通过发送读状态指令,获取是否已经将数据写完,如果为可读状态,则数据写入完毕
XQspiPs_PolledTransfer(QspiPtr, ReadStatusCmd, FlashStatus,
sizeof(ReadStatusCmd));
//如果读取的状态值是0xff,则证明数据还未写完
FlashStatus[1] |= FlashStatus[0];
if ((FlashStatus[1] & 0x01) == 0) {
break;
}
}
}
/**
* @brief 通过QSPI读取flash中的数据
* @param QSPI结构体指针
* @param 读取数据的起始地址
* @param 读取数据的数量-按字节
* @param 读取数据指令-普通读取、快速、双线、四线读取等
* @return 无
* @note 无
* */
void FlashRead(XQspiPs *QspiPtr, u32 Address, u8 *ReadBuf, u32 ByteCount, u8 Command)
{
//将读指令和读数据首地址写入到要发送的buffer中,它们占4个字节
FlashWriteBuffer[COMMAND_OFFSET] = Command;
FlashWriteBuffer[ADDRESS_1_OFFSET] = (u8)((Address & 0xFF0000) >> 16);
FlashWriteBuffer[ADDRESS_2_OFFSET] = (u8)((Address & 0xFF00) >> 8);
FlashWriteBuffer[ADDRESS_3_OFFSET] = (u8)(Address & 0xFF);
//如果是快速读取、双线读取和四线读取,则需要增加一个空闲字节的长度DUMMY_SIZE
if ((Command == FAST_READ_CMD) || (Command == DUAL_READ_CMD) ||
(Command == QUAD_READ_CMD)) {
ByteCount += DUMMY_SIZE;
}
//将读指令和读地址发送到通过QSPI发送到flash,等待数据读取至ReadBuffer中
XQspiPs_PolledTransfer(QspiPtr, FlashWriteBuffer, FlashReadBuffer,
ByteCount + OVERHEAD_SIZE);
//如果是快速读取、双线读取和四线读取,则需要增加一个虚拟字节的长度DUMMY_SIZE
if ((Command == FAST_READ_CMD) || (Command == DUAL_READ_CMD) ||
(Command == QUAD_READ_CMD)) {
//非普通模式读取,有空闲字节,从第5个字节开始是纯数据
memcpy(ReadBuf, FlashReadBuffer + 5, ByteCount - 1);
}else{
//普通模式读取,无空闲字节,从第4个字节开始是纯数据
memcpy(ReadBuf, FlashReadBuffer + 4, ByteCount);
}
}
/**
* @brief 擦除flash
* @param QSPI结构体指针
* @param 擦除的起始地址
* @param 擦除数据的数量-按字节
* @return 无
* @note 无
* */
void FlashErase(XQspiPs *QspiPtr, u32 Address, u32 ByteCount)
{
u8 WriteEnableCmd = { WRITE_ENABLE_CMD };
u8 ReadStatusCmd[] = { READ_STATUS_CMD, 0 }; /* must send 2 bytes */
u8 FlashStatus[2];
int Sector;
//如果是擦除整片flash,则使用整片擦除指令chip erase
if (ByteCount == (NUM_SECTORS * SECTOR_SIZE)) {
//发送写使能指令
XQspiPs_PolledTransfer(QspiPtr, &WriteEnableCmd, NULL,
sizeof(WriteEnableCmd));
//将整片擦除指令写入到发送buffer的首个字节的位置
FlashWriteBuffer[COMMAND_OFFSET] = BULK_ERASE_CMD;
//将整片擦除指令发送到flash
XQspiPs_PolledTransfer(QspiPtr, FlashWriteBuffer, NULL,
BULK_ERASE_SIZE);
//等待擦除完成
while (1) {
//通过发送读状态指令,获取是否已经将数据写完,如果为可读状态,则数据写入完毕
XQspiPs_PolledTransfer(QspiPtr, ReadStatusCmd,
FlashStatus,
sizeof(ReadStatusCmd));
//如果读取的状态值是0xff,则证明数据还未写完
FlashStatus[1] |= FlashStatus[0];
if ((FlashStatus[1] & 0x01) == 0) {
break;
}
}
return;
}
//如果是部分擦除,则使用扇区sector擦除的指令进行擦除操作
for (Sector = 0; Sector < ((ByteCount / SECTOR_SIZE) + 1); Sector++) {
//发送写使能指令
XQspiPs_PolledTransfer(QspiPtr, &WriteEnableCmd, NULL,
sizeof(WriteEnableCmd));
//将扇区擦除指令,以及开始擦除首地址写入到发送buffer的前四个字节
FlashWriteBuffer[COMMAND_OFFSET] = SEC_ERASE_CMD;
FlashWriteBuffer[ADDRESS_1_OFFSET] = (u8)(Address >> 16);
FlashWriteBuffer[ADDRESS_2_OFFSET] = (u8)(Address >> 8);
FlashWriteBuffer[ADDRESS_3_OFFSET] = (u8)(Address & 0xFF);
//将扇区擦除指令,以及开始擦除首地址发送到flash
XQspiPs_PolledTransfer(QspiPtr, FlashWriteBuffer, NULL,
SEC_ERASE_SIZE);
//等待擦除完成
while (1) {
//通过发送读状态指令,获取是否已经将数据写完,如果为可读状态,则数据写入完毕
XQspiPs_PolledTransfer(QspiPtr, ReadStatusCmd,
FlashStatus,
sizeof(ReadStatusCmd));
//如果读取的状态值是0xff,则证明数据还未写完
FlashStatus[1] |= FlashStatus[0];
if ((FlashStatus[1] & 0x01) == 0) {
break;
}
}
Address += SECTOR_SIZE;
}
}
/**
* @brief 读取flash ID
* @param 无
* @return 无
* @note 无
* */
int FlashReadID(void)
{
int Status;
//读取ID指令,后三个字节是空闲字节,填不填都行,填什么也无所谓
FlashWriteBuffer[COMMAND_OFFSET] = READ_ID;
FlashWriteBuffer[ADDRESS_1_OFFSET] = 0x23;
FlashWriteBuffer[ADDRESS_2_OFFSET] = 0x08;
FlashWriteBuffer[ADDRESS_3_OFFSET] = 0x09;
//将读ID指令发送到flash
Status = XQspiPs_PolledTransfer(&QspiInstance, FlashWriteBuffer, FlashReadBuffer,
RD_ID_SIZE);
if (Status != XST_SUCCESS) {
return XST_FAILURE;
}
xil_printf("FlashID=0x%x 0x%x 0x%x\n\r", FlashReadBuffer[1], FlashReadBuffer[2],
FlashReadBuffer[3]);
return XST_SUCCESS;
}
/**
* @brief 使能四线模式
* @param QSPI结构体指针
* @return 无
* @note 无
* */
void FlashQuadEnable(XQspiPs *QspiPtr)
{
u8 WriteEnableCmd = {WRITE_ENABLE_CMD};
u8 ReadStatusCmd[] = {READ_STATUS_CMD, 0};
u8 QuadEnableCmd[] = {WRITE_STATUS_CMD, 0};
u8 FlashStatus[2];
//判断读取的flash ID是否正确,不加这个判断也行
if (FlashReadBuffer[1] == 0xEF) {
//获取读状态
XQspiPs_PolledTransfer(QspiPtr, ReadStatusCmd,
FlashStatus,
sizeof(ReadStatusCmd));
QuadEnableCmd[1] = FlashStatus[1] | 1 << 6;
//发送写使能指令
XQspiPs_PolledTransfer(QspiPtr, &WriteEnableCmd, NULL,
sizeof(WriteEnableCmd));
//发送Quad配置指令
XQspiPs_PolledTransfer(QspiPtr, QuadEnableCmd, NULL,
sizeof(QuadEnableCmd));
while (1) {
//获取读状态,等待指令写入完毕
XQspiPs_PolledTransfer(QspiPtr, ReadStatusCmd, FlashStatus,
sizeof(ReadStatusCmd));
/*
* 第6it置1,第0bit置0,则Quad模式设置成功、且设备状态准备就绪
*/
if ((FlashStatus[0] == 0x40) && (FlashStatus[1] == 0x40)) {
break;
}
}
}
}
4.编写QSPI Flash读写测试函数
//读写buffer数据长度
#define test_buf_len 255
//QSPI读写flash测试
void QSPI_Flash_Opt(){
u8 nRet = XST_SUCCESS;
u8 write_buf[test_buf_len] = {0};
u8 read_buf[test_buf_len] = {0};
//初始化QSPI控制器
Qspi_Init(&QspiInstance);
//write buffer填写数据
for(int i = 0; i < test_buf_len; i++){
write_buf[i] = i + 1;
}
//清除要写入的flash区域
FlashErase(&QspiInstance, TEST_ADDRESS, test_buf_len);
//将write buffer数据写入flash中
FlashWrite(&QspiInstance, TEST_ADDRESS, write_buf, test_buf_len, WRITE_CMD);
//将写入flash中的数据再进行读取
// FlashRead(&QspiInstance, TEST_ADDRESS, read_buf, test_buf_len, READ_CMD);
// FlashRead(&QspiInstance, TEST_ADDRESS, read_buf, test_buf_len, FAST_READ_CMD);
// FlashRead(&QspiInstance, TEST_ADDRESS, read_buf, test_buf_len, DUAL_READ_CMD);
FlashRead(&QspiInstance, TEST_ADDRESS, read_buf, test_buf_len, QUAD_READ_CMD);
//打印写buffer区数据
for(int i = 0; i < test_buf_len; i++){
printf("%d ", write_buf[i]);
if(i == test_buf_len - 1)
printf("\n");
}
//打印读buffer区数据
for(int i = 0; i < test_buf_len; i++){
printf("%d ", read_buf[i]);
if(i == test_buf_len - 1)
printf("\n");
}
//对比写入和读出的数据是否一致
for(int i = 0; i < test_buf_len; i++){
if(read_buf[i] != write_buf[i]){
nRet = XST_FAILURE;
}
}
if(nRet == XST_SUCCESS){
printf("QSPI Operate flash successful!\n");
}else{
printf("QSPI Operate flash failed!\n");
}
}
5.main函数调用
五、实测结果
创作不易,希望大家点赞、收藏、关注哦!!!ヾ(o◕∀◕)ノ