STM32CubeMX学习笔记(48)——USB接口使用(MSC基于外部Flash模拟U盘)_stm32 usb flash

3.4 USB Device

USB有主机(Host)和设备(Device)之分。一般电脑的USB接口为主机接口,而键盘、鼠标、U盘等则为设备。

部分型号的STM32芯片有1~2个USB接口。像STM32F103系列的有一个USB Device接口,STM32F407系列的有2个USB接口,既可以作为HOST,又可以作为Device,还可以作为OTG接口。

Middleware 中选择 USB_DEVICE 设置,在 Class For FS IP 设备类别选择 Mass Storage Class(HID) 大容量存储设备类。

参数配置保持默认(或根据存储介质的最小存储单元修改缓冲区大小)。

  • MSC_MEDIA_PACKET (Media I/O buffer Size)(读写缓冲区大小): 4096(默认为512,这个的大小对于USB读写速度会有一些影响,最好和存储介质的最小存储单元一致)

本实验板使用的W25Q64是华邦公司推出的大容量SPI FLASH产品,其容量为64Mb。该25Q系列的器件在灵活性和性能方面远远超过普通的串行闪存器件。W25Q64将8M字节的容量分为128个块,每个块大小为64K字节,每个块又分为16个扇区,每个扇区4K个字节。W25Q64的最小擦除单位为一个扇区,也就是每次必须擦除4K个字节。所以,这需要给W25Q64开辟一个至少4K的缓存区,这样必须要求芯片有4K以上的SRAM才能有很好的操作。

设备描述符保持默认。

四、SPI1

4.1 参数配置

Connectivity 中选择 SPI1 设置,并选择 Full-Duplex Master 全双工主模式,不开启 NSS 即不使用硬件片选信号

image

原理图中虽然将 CS 片选接到了硬件 SPI1 的 NSS 引脚,因为硬件 NSS 使用比较麻烦,所以后面直接把 PA4 配置为普通 GPIO,手动控制片选信号。

在右边图中找到 SPI1 NSS 对应引脚,选择 GPIO_Output纠正:野火STM32F103指南者开发板SPI1 NSS须配置为PC0

image

修改输出高电平 High,标签为 W25Q64_CHIP_SELECT
image

SPI 为默认设置不作修改。只需注意一下,Prescaler 分频系数最低为 4,波特率 (Baud Rate) 为 18.0 MBits/s。这里被限制了,SPI1 最高通信速率可达 36Mbtis/s。
image

  • Clock Polarity(CPOL):SPI 通讯设备处于空闲状态时,SCK 信号线的电平信号(即 SPI 通讯开始前、 NSS 线为高电平时 SCK 的状态)。CPOL=0 时, SCK 在空闲状态时为低电平,CPOL=1 时,则相反。
  • Clock Phase(CPHA):指数据的采样的时刻,当 CPHA=0 时,MOSI 或 MISO 数据线上的信号将会在 SCK 时钟线的“奇数边沿”被采样。当 CPHA=1 时,数据线在 SCK 的“偶数边沿”采样。
    image
    根据 FLASH 芯片的说明,它支持 SPI 模式0模式 3,支持双线全双工,使用 MSB 先行模式,数据帧长度为 8 位。
    image
    所以这里配置 CPOL 为 Low,CPHA 为 1 Edge 即 SPI 模式0
    image

五、生成代码

输入项目名和项目路径

选择应用的 IDE 开发环境 MDK-ARM V5

每个外设生成独立的 ’.c/.h’ 文件
不勾:所有初始化代码都生成在 main.c
勾选:初始化代码生成在对应的外设文件。 如 GPIO 初始化代码生成在 gpio.c 中。

点击 GENERATE CODE 生成代码

六、编写W25Q64的驱动程序

驱动程序有问题后面Windows无法格式化硬盘

6.1 添加宏定义

/\* Private define ------------------------------------------------------------\*/
/\* USER CODE BEGIN PD \*/
#define W25Q80 0XEF13 
#define W25Q16 0XEF14
#define W25Q32 0XEF15
#define W25Q64 0XEF16
#define W25Q128 0XEF17
#define W25Q256 0XEF18

#define W25X\_WriteEnable 0x06 
#define W25X\_WriteDisable 0x04 
#define W25X\_ReadStatusReg1 0x05 
#define W25X\_ReadStatusReg2 0x35 
#define W25X\_ReadStatusReg3 0x15 
#define W25X\_WriteStatusReg1 0x01 
#define W25X\_WriteStatusReg2 0x31 
#define W25X\_WriteStatusReg3 0x11 
#define W25X\_ReadData 0x03 
#define W25X\_FastReadData 0x0B 
#define W25X\_FastReadDual 0x3B 
#define W25X\_PageProgram 0x02 
#define W25X\_BlockErase 0xD8 
#define W25X\_SectorErase 0x20 
#define W25X\_ChipErase 0xC7 
#define W25X\_PowerDown 0xB9 
#define W25X\_ReleasePowerDown 0xAB 
#define W25X\_DeviceID 0xAB 
#define W25X\_ManufactDeviceID 0x90 
#define W25X\_JedecDeviceID 0x9F 
#define W25X\_Enable4ByteAddr 0xB7
#define W25X\_Exit4ByteAddr 0xE9
/\* USER CODE END PD \*/

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

  • 发送数据的同时读取数据的函数
//SPI 读写一个字节
//TxData:要写入的字节
//返回值:读取到的字节
uint8\_t SPI1\_ReadWriteByte(uint8\_t TxData)
{
	uint8\_t Rxdata;
    HAL\_SPI\_TransmitReceive(&hspi1, &TxData, &Rxdata, 1, 1000);       
 	return Rxdata;          		    //返回收到的数据 
}

6.3 读取 Manufacture ID 和 Device ID

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

  • 检测 SPI Flash 是否存在
  • 可以根据 ID 判断 Flash 具体型号
    image
uint16\_t W25QXX_TYPE;					//定义W25QXX芯片型号
uint16\_t W25QXX\_ReadID(void)
{
	uint16\_t Temp = 0;	  
    /\* 使能片选 \*/
	HAL\_GPIO\_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);				    
	SPI1\_ReadWriteByte(0x90);//发送读取ID命令 
	SPI1\_ReadWriteByte(0x00); 	    
	SPI1\_ReadWriteByte(0x00); 	    
	SPI1\_ReadWriteByte(0x00); 	 			   
	Temp|=SPI1\_ReadWriteByte(0xFF)<<8;  
	Temp|=SPI1\_ReadWriteByte(0xFF);	 
	W25QXX_TYPE=Temp;
    /\* 取消片选 \*/
	HAL\_GPIO\_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);				    
	return Temp;
} 

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

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

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

//读取W25QXX的状态寄存器,W25QXX一共有3个状态寄存器
//状态寄存器1:
//BIT7 6 5 4 3 2 1 0
//SPR RV TB BP2 BP1 BP0 WEL BUSY
//SPR:默认0,状态寄存器保护位,配合WP使用
//TB,BP2,BP1,BP0:FLASH区域写保护设置
//WEL:写使能锁定
//BUSY:忙标记位(1,忙;0,空闲)
//默认:0x00
//状态寄存器2:
//BIT7 6 5 4 3 2 1 0
//SUS CMP LB3 LB2 LB1 (R) QE SRP1
//状态寄存器3:
//BIT7 6 5 4 3 2 1 0
//HOLD/RST DRV1 DRV0 (R) (R) WPS ADP ADS
//regno:状态寄存器号,范:1~3
//返回值:状态寄存器值
uint8\_t W25QXX\_ReadSR(uint8\_t regno)   
{  
	uint8\_t byte=0,command=0; 
    switch(regno)
    {
        case 1:
            command=W25X_ReadStatusReg1;    //读状态寄存器1指令
            break;
        case 2:
            command=W25X_ReadStatusReg2;    //读状态寄存器2指令
            break;
        case 3:
            command=W25X_ReadStatusReg3;    //读状态寄存器3指令
            break;
        default:
            command=W25X_ReadStatusReg1;    
            break;
    }    
    /\* 使能片选 \*/
	HAL\_GPIO\_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET); 
	SPI1\_ReadWriteByte(command);            //发送读取状态寄存器命令 
	byte=SPI1\_ReadWriteByte(0Xff);          //读取一个字节 
    /\* 取消片选 \*/
	HAL\_GPIO\_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);   
	return byte;   
} 

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

//等待空闲
void W25QXX\_Wait\_Busy(void)   
{   
	while((W25QXX\_ReadSR(1)&0x01)==0x01);   // 等待BUSY位清空
}

6.5 读取数据

SPI Flash 读取数据可以任意地址(地址长度32bit)读任意长度数据(最大 65535 Byte),没有任何限制。
image

//读取SPI FLASH 
//在指定地址开始读取指定长度的数据
//pBuffer:数据存储区
//ReadAddr:开始读取的地址(24bit)
//NumByteToRead:要读取的字节数(最大65535)
void W25QXX\_Read(uint8\_t\* pBuffer,uint32\_t ReadAddr,uint16\_t NumByteToRead)   
{ 
 	uint16\_t i;   							
    /\* 使能片选 \*/
	HAL\_GPIO\_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);  
    SPI1\_ReadWriteByte(W25X_ReadData);      //发送读取命令 
    if(W25QXX_TYPE==W25Q256)                //如果是W25Q256的话地址为4字节的,要发送最高8位
    {
        SPI1\_ReadWriteByte((uint8\_t)((ReadAddr)>>24));    
    }
    SPI1\_ReadWriteByte((uint8\_t)((ReadAddr)>>16));   //发送24bit地址 
    SPI1\_ReadWriteByte((uint8\_t)((ReadAddr)>>8));   
    SPI1\_ReadWriteByte((uint8\_t)ReadAddr);   
    for(i=0;i<NumByteToRead;i++)
	{ 
        pBuffer[i]=SPI1\_ReadWriteByte(0XFF);    //循环读数 
    }
    /\* 取消片选 \*/
	HAL\_GPIO\_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);  				    	      
}

6.6 写使能/禁止

Flash 芯片默认禁止写数据,所以在向 Flash 写数据之前,必须发送命令开启写使能。
image
image

//W25QXX写使能 
//将WEL置位 
void W25QXX\_Write\_Enable(void)   
{
	HAL\_GPIO\_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);                            //使能器件 
    SPI1\_ReadWriteByte(W25X_WriteEnable);   //发送写使能 
	HAL\_GPIO\_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);                            //取消片选 
} 

//W25QXX写禁止 
//将WEL清零 
void W25QXX\_Write\_Disable(void)   
{  
	HAL\_GPIO\_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);                            //使能器件 
    SPI1\_ReadWriteByte(W25X_WriteDisable);  //发送写禁止指令 
	HAL\_GPIO\_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);                            //取消片选 
} 

6.7 擦除扇区

SPI Flash有个特性:

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

所以在向 Flash 写数据之前,必须要先进行擦除操作,并且 Flash 最小只能擦除一个扇区,擦除之后该扇区所有的数据变为 0xFF(即全为1)。
image

//擦除一个扇区
//Dst\_Addr:扇区地址 根据实际容量设置
//擦除一个扇区的最少时间:150ms
void W25QXX\_Erase\_Sector(uint32\_t Dst_Addr)   
{  
	//监视falsh擦除情况,测试用 
 	//printf("fe:%x\r\n",Dst\_Addr); 
 	Dst_Addr\*=4096;
    W25QXX\_Write\_Enable();                  //SET WEL 
    W25QXX\_Wait\_Busy();   
    /\* 使能片选 \*/
  	HAL\_GPIO\_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);   
    SPI1\_ReadWriteByte(W25X_SectorErase);   //发送扇区擦除指令 
    if(W25QXX_TYPE==W25Q256)                //如果是W25Q256的话地址为4字节的,要发送最高8位
    {
        SPI1\_ReadWriteByte((uint8\_t)((Dst_Addr)>>24)); 
    }
    SPI1\_ReadWriteByte((uint8\_t)((Dst_Addr)>>16));  //发送24bit地址 
    SPI1\_ReadWriteByte((uint8\_t)((Dst_Addr)>>8));   
    SPI1\_ReadWriteByte((uint8\_t)Dst_Addr);  
    /\* 取消片选 \*/
	HAL\_GPIO\_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);    	      
    W25QXX\_Wait\_Busy();   				    //等待擦除完成
} 

6.8 页写入操作

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

//SPI在一页(0~65535)内写入少于256个字节的数据
//在指定地址开始写入最大256字节的数据
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit)
//NumByteToWrite:要写入的字节数(最大256),该数不应该超过该页的剩余字节数!!! 
void W25QXX\_Write\_Page(uint8\_t\* pBuffer,uint32\_t WriteAddr,uint16\_t NumByteToWrite)
{
 	uint16\_t i;  
    W25QXX\_Write\_Enable();                  //SET WEL 
    /\* 使能片选 \*/
	HAL\_GPIO\_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);   
    SPI1\_ReadWriteByte(W25X_PageProgram);   //发送写页命令 
    if(W25QXX_TYPE==W25Q256)                //如果是W25Q256的话地址为4字节的,要发送最高8位
    {
        SPI1\_ReadWriteByte((uint8\_t)((WriteAddr)>>24)); 
    }
    SPI1\_ReadWriteByte((uint8\_t)((WriteAddr)>>16)); //发送24bit地址 
    SPI1\_ReadWriteByte((uint8\_t)((WriteAddr)>>8));   
    SPI1\_ReadWriteByte((uint8\_t)WriteAddr);   
    for(i=0;i<NumByteToWrite;i++)SPI1\_ReadWriteByte(pBuffer[i]);//循环写数 
    /\* 取消片选 \*/    
	HAL\_GPIO\_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);
	W25QXX\_Wait\_Busy();					   //等待写入结束
} 

6.9 写入数据

//无检验写SPI FLASH 
//必须确保所写的地址范围内的数据全部为0XFF,否则在非0XFF处写入的数据将失败!
//具有自动换页功能 
//在指定地址开始写入指定长度的数据,但是要确保地址不越界!
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit)
//NumByteToWrite:要写入的字节数(最大65535)
//CHECK OK
void W25QXX\_Write\_NoCheck(uint8\_t\* pBuffer,uint32\_t WriteAddr,uint16\_t NumByteToWrite)   
{ 			 		 
	uint16\_t pageremain;	   
	pageremain=256-WriteAddr%256; //单页剩余的字节数 
	if(NumByteToWrite<=pageremain)pageremain=NumByteToWrite;//不大于256个字节
	while(1)
	{	   
		W25QXX\_Write\_Page(pBuffer,WriteAddr,pageremain);
		if(NumByteToWrite==pageremain)break;//写入结束了
	 	else //NumByteToWrite>pageremain
		{
			pBuffer+=pageremain;
			WriteAddr+=pageremain;	

			NumByteToWrite-=pageremain;			  //减去已经写入了的字节数
			if(NumByteToWrite>256)pageremain=256; //一次可以写入256个字节
			else pageremain=NumByteToWrite; 	  //不够256个字节了
		}
	};	    
} 
//写SPI FLASH 
//在指定地址开始写入指定长度的数据
//该函数带擦除操作!
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit) 
//NumByteToWrite:要写入的字节数(最大65535) 
uint8\_t W25QXX_BUFFER[4096];		 
void W25QXX\_Write(uint8\_t\* pBuffer,uint32\_t WriteAddr,uint16\_t NumByteToWrite)   
{ 
	uint32\_t secpos;
	uint16\_t secoff;
	uint16\_t secremain;	   
 	uint16\_t i;    
	uint8\_t \* W25QXX_BUF;	  
   	W25QXX_BUF=W25QXX_BUFFER;	     
 	secpos=WriteAddr/4096;//扇区地址 
	secoff=WriteAddr%4096;//在扇区内的偏移
	secremain=4096-secoff;//扇区剩余空间大小 
 	//printf("ad:%X,nb:%X\r\n",WriteAddr,NumByteToWrite);//测试用
 	if(NumByteToWrite<=secremain)secremain=NumByteToWrite;//不大于4096个字节
	while(1) 
	{	
		W25QXX\_Read(W25QXX_BUF,secpos\*4096,4096);//读出整个扇区的内容
		for(i=0;i<secremain;i++)//校验数据
		{
			if(W25QXX_BUF[secoff+i]!=0XFF)break;//需要擦除 
		}
		if(i<secremain)//需要擦除
		{
			W25QXX\_Erase\_Sector(secpos);//擦除这个扇区
			for(i=0;i<secremain;i++)	   //复制
			{
				W25QXX_BUF[i+secoff]=pBuffer[i];	  
			}
			W25QXX\_Write\_NoCheck(W25QXX_BUF,secpos\*4096,4096);//写入整个扇区 

		}else W25QXX\_Write\_NoCheck(pBuffer,WriteAddr,secremain);//写已经擦除了的,直接写入扇区剩余区间. 
		if(NumByteToWrite==secremain)break;//写入结束了
		else//写入未结束
		{
			secpos++;//扇区地址增1
			secoff=0;//偏移位置为0 

		   	pBuffer+=secremain;  //指针偏移
			WriteAddr+=secremain;//写地址偏移 
		   	NumByteToWrite-=secremain;				//字节数递减
			if(NumByteToWrite>4096)secremain=4096;	//下一个扇区还是写不完
			else secremain=NumByteToWrite;			//下一个扇区可以写完了
		}	 
	};	 
}

七、修改usbd_storage_if.c

打开工程文件夹Application/User/USB_DEVICE/Appusbd_storage_if.c文件

7.1 修改扇区个数和大小

/\*\* @defgroup USBD\_STORAGE\_Private\_Defines
 \* @brief Private defines.
 \* @{
 \*/

//#define STORAGE\_LUN\_NBR 1
//#define STORAGE\_BLK\_NBR 0x10000
//#define STORAGE\_BLK\_SIZ 0x200

#define STORAGE\_LUN\_NBR 1 // 1个盘
#define STORAGE\_BLK\_NBR 0x2000 // 模拟16个扇区
#define STORAGE\_BLK\_SIZ 0x1000 // W25Q64最小读写单位是4KB

  • STORAGE_BLK_NBR 是扇区个数,此处使用的是0x2000,大小完全根据自己需求定义。W25Q64最大容量为64Mb。
  • STORAGE_BLK_SIZ 扇区大小取决于所用芯片Flash页面的单位,实验所用SPI Flash W25Q64的最小擦除单位为一个扇区,也就是每次必须擦除4K个字节,所以
    STORAGE_BLK_SIZ = FLASH_PAGE_SIZE = 4096即0x1000
  • U盘实际容量 = 扇区个数 * 扇区大小,所以这个U盘最终大小应该是0x2000*4K = 32768K = 32M

7.2 修改存储函数

  • STORAGE_Init_FS 存储介质初始化
/\*\*
 \* @brief Initializes over USB FS IP
 \* @param lun:
 \* @retval USBD\_OK if all operations are OK else USBD\_FAIL
 \*/
int8\_t STORAGE\_Init\_FS(uint8\_t lun)
{
  /\* USER CODE BEGIN 2 \*/
  if(W25QXX\_ReadID())
  {
    return (USBD_OK);
  }
  return (USBD_FAIL);
  /\* USER CODE END 2 \*/
}

  • STORAGE_IsReady_FS 获取存储介质状态

获取介质状态,我们要对存储介质的状态进行判断,这里我们要判断两点,一个是是否正在读写状态中,另外一个就是存储介质是否是不可工作状态。

/\*\*
 \* @brief .
 \* @param lun: .
 \* @retval USBD\_OK if all operations are OK else USBD\_FAIL
 \*/
int8\_t STORAGE\_IsReady\_FS(uint8\_t lun)
{
  /\* USER CODE BEGIN 4 \*/
  return W25QXX\_ReadSR(1);
  /\* USER CODE END 4 \*/
}

  • STORAGE_Read_FS 读取存储介质

读取一个扇区,我们将准备好的读取一个扇区的代码填充进来就好

/\*\*
 \* @brief .
 \* @param lun: .
 \* @retval USBD\_OK if all operations are OK else USBD\_FAIL
 \*/
int8\_t STORAGE\_Read\_FS(uint8\_t lun, uint8\_t \*buf, uint32\_t blk_addr, uint16\_t blk_len)
{
  /\* USER CODE BEGIN 6 \*/
  W25QXX\_Read(buf, blk_addr \* STORAGE_BLK_SIZ, blk_len \* STORAGE_BLK_SIZ);
  return (USBD_OK);
  /\* USER CODE END 6 \*/
}

  • STORAGE_Write_FS 写入存储介质

写入一个扇区,我们将准备好的读取一个扇区的代码填充进来就好

/\*\*
 \* @brief .
 \* @param lun: .
 \* @retval USBD\_OK if all operations are OK else USBD\_FAIL
 \*/
int8\_t STORAGE\_Write\_FS(uint8\_t lun, uint8\_t \*buf, uint32\_t blk_addr, uint16\_t blk_len)
{
  /\* USER CODE BEGIN 7 \*/
  W25QXX\_Write(buf, blk_addr \* STORAGE_BLK_SIZ, blk_len \* STORAGE_BLK_SIZ);
  return (USBD_OK);
  /\* USER CODE END 7 \*/
}

八、查看效果

编译工程,下载到板子上,插上USB线连接到电脑上,识别出为大容量存储设备

注意: 如果设备带有感叹号,则参考下面十、注意事项

弹出格式化对话框,直接格式化就行

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值