STM32第十九课(SDIO,FATFS,HAL)

STM32F4 的 SDIO 控制器包含 2 个部分: SDIO 适配器模块和 APB2 总线接口,
复位后默认情况下 SDIO_D0 用于数据传输。初始化后主机可以改变数据总线的宽度
SDIO_D0、 SDIO_D[3:0]或 SDIO_D[7:0]可以用于数据传输。
MMC 版本 V3.31 和之前版本的协议只支持 1 位数据线,所以只能用 SDIO_D0(为了通用性考虑,在程序里面我们只要检测到是 MMC 卡就设置为 1 位总线数据)
如果一个 SD 或 SD I/O 卡接到了总线上,可以通过主机配置数据传输使用 SDIO_D0 或SDIO_D[3:0]。所有的数据线都工作在推挽模式。

SDIO 总共有 3 个时钟,分别是:
卡时钟(SDIO_CK):每个时钟周期在命令和数据线上传输 1 位命令或数据。
SDIO 适配器时钟(SDIOCLK):该时钟用于驱动 SDIO 适配器,来自 PLL48CK,一般为48Mhz,并用于产生 SDIO_CK 时钟。
APB2总线接口时钟(PCLK2):该时钟用于驱动SDIO的APB2总线接口,其频率为HCLK/2,一般为 84Mhz。

SDIO_CK=SDIOCLK/(2+CLKDIV)
SDIOCLK 为 PLL48CK,一般是 48Mhz,而 CLKDIV 则是分配系数,当时钟分频器旁路时, SDIO_CK 直接等于 SDIOCLK。

在 SD 卡刚刚初始化的时候, 其时钟频率(SDIO_CK)是不能超过 400Khz的,否则可能无法完成初始化。
SDIO 的所有命令和响应都是通过 SDIO_CMD 引脚传输的,任何命令的长度都是固定为 48 位,

对于 SDI/SDIO 存储器,数据是以数据块的形式传输的,而对于 MMC 卡,数据是以数据块或者数据流的形式传输。
多块数据读的时候, SD 卡将一直发送数据给主机,直到接到主机发送的 STOP 命令(CMD12)。
数据块写的时候,多了一个繁忙判断,新的数据块必须在 SD 卡非繁忙的时候发送。繁忙信号由 SD 卡拉低 SDIO_D0,以表示繁忙,
+++++++++++++++++++++++++++++++++++

SDIO 时钟控制寄存器(SDIO_CLKCR),该寄存器主要用于设置 SDIO_CK的分配系数,开关等,并可以设置 SDIO 的数据位宽,
WIDBUS 用于设置 SDIO 总线位宽,正常使用的时候,设置为 1,即 4 位宽度。
BYPASS 用于设置分频器是否旁路,我们一般要使用分频器,所以这里设置为 0,禁止旁路。
CLKEN 则用于设置是否使能 SDIO_CK,我们设置为 1。
CLKDIV,则用于控制 SDIO_CK 的分频,一般设置为 0,即可得到 24Mhz 的 SDIO_CK 频率。

SDIO 参数制寄存器(SDIO_ARG),用于存储命令参数,不过需要注意的是,必须在写命令之前先写这个参数寄存
器!
SDIO 命令响应寄存器(SDIO_RESPCMD),存储最后收到的命令响应中的命令索引。
SDIO 响应寄存器组(SDIO_RESP1~SDIO_RESP4),存放接收到的卡响应部分信息。如果收到短响应,则数据存
放在 SDIO_RESP1 寄存器里面,其他三个寄存器没有用到。而如果收到长响应,则依次存放在SDIO_RESP1~ SDIO_RESP4 里面,

SDIO 数据定时器寄存器(SDIO_DTIMER),存储以卡总线时钟(SDIO_CK)为周期的数据超时时间, 一个计数器将从 SDIO_DTIMER 寄存器加载数值,并在数据通道状态机(DPSM)进入 Wait_R 或繁忙状态时进行递减计数,当 DPSM 处在这些状态时,如果计数器减为 0,则设置超时标志。
注意:在写入数据控制寄存器,进行数据传输之前,必须先写入该寄存器(SDIO_DTIMER)和数据长度寄存器(SDIO_DLEN)!

SDIO 数据长度寄存器(SDIO_DLEN),用于设置需要传输的数据字节长度。对于块数据传输,该寄存器的数值,必须是数据块长度(通过 SDIO_DCTRL 设置)的倍数。

SDIO 数据控制寄存器(SDIO_DCTRL),用于控制数据通道状态机(DPSM),

SDIO 的数据 FIFO 寄存器(SDIO_FIFO),包括接收和发送 FIFO,他们由一组连续的 32 个地址上的 32 个寄存器组成, CPU 可以使用 FIFO 读写多个操作数。SDIO 将这 32 个地址分为 16 个一组,发送接收各占一半。每次读写的时候,最多就是读取发送 FIFO 或写入接收 FIFO 的一半大小的数据,也就是 8 个字(32 个字节),
这里特别提醒,我们操作 SDIO_FIFO(不论读出还是写入)必须是以 4 字节对齐的内存进行操作,否则将导致出错!

+++++++++++++++++++++++++++++++++++++
HAL 库 SDIO 支持文件 stm32f4xx_hal_sdio.c以及头文件 stm32f4xx_hal_sdio.h,

HAL_SD_Init 函数,对SD进行初始化。
HAL_SD_GetCardInfo,获取SD卡的类型信息。
HAL_SD_ConfigWideBusOperation,配置SD卡的位宽信息。
HAL_SD_GetCardState,获取SDK的状态,是ready还是busy。

HAL_SD_ReadBlocks_DMA,以DMA方式,从SD卡中读取数据。
HAL_SD_WriteBlocks_DMA,以DMA方式,向SD卡中写入数据。

++++++++++++++++++++++++++++++++++++
实战,
在cubemx中,配置sdio,
mode 设置为sd 4bit,

设置clock trans 为rising edge ,
设置clock bypass 为 disable,
设置clock output enable for power save 为 disable,
设置hard flow control 为 disable,
设置SDIOCLK clkdiv factor 为1,即2分频,

设置DMA,
add一个DMA request,sdio_rx,
设置为dma2_stream3,方向为peri to mem, priority为 very high,
mode 设置为 peri flow control,
addr inc设置为mem inc, data width 为word,
使用fifo , threshold 为full,
burst size设置为4 increment,

add一个DMA request,sdio_tx,
设置为dma2_stream3,方向为mem to peri, priority为 very high,
其他类似。

+++++++++++++++++++++++++++++++++
我们需要自建实用库,用来对SD卡的block进行读写。
这里,我们使用DMA 方式进行SD卡读写。

在SRAM和SD卡之间传递数据。

		testsram[0] =0x12;
        testsram[1] =0x34;
        testsram[2] =0x56;
        testsram[3] =0x78;
        testsram[4] =0x90;
        testsram[5] =0xab;
        testsram[6] =0xcd;
        testsram[7] =0xef;
        
      
        
        HAL_SD_WriteBlocks_DMA(&hsd,testsram,1000,1);           
        HAL_Delay(100);
        HAL_SD_ReadBlocks_DMA(&hsd,testsd,1000,1);
        HAL_Delay(100);

+++++++++++++++++++++++++++++++++
FATFS 是一个完全免费开源的 FAT 文件系统模块,专门为小型的嵌入式系统而设计。它支持 FATl2、 FATl6 和 FAT32,
使用者无需理会 FATFS 的内部结构和复杂的 FAT 协议,只需要调用FATFS 模块提供给用户的一系列应用接口函数,如 f_open, f_read, f_write 和 f_close 等,
中间层 FATFS 模块, 实现了 FAT 文件读/写协议。 FATFS 模块提供的是 ff.c 和 ff.h。
需要我们编写移植代码的是 FATFS 模块提供的底层接口,它包括存储媒介读/写接口(diskI/O) 和供给文件创建修改时间的实时时钟。

与平台相关的代码(需要用户提供)是:
diskio.c FATFS 和 disk I/O 模块接口层文件
我们一般只需要修改 2 个文件,即 ffconf.h 和 diskio.c。
所有配置项都是存放在 ffconf.h 里面,
几个重要的配置选项。
1) _FS_TINY。使用 FATFS,所以把这个选项定义为 0 即可。
2) _FS_READONLY。读写都用,所以这里设置为 0 即可
3) _USE_STRFUNC。设置是否支持字符串类操作,比如 f_putc, f_puts 等,我们需要用到,故设置这里为 1。
4) _USE_MKFS。是否使能格式化,需要用到,所以设置这里为 1。
5) _USE_FASTSEEK。使能快速定位,我们设置为 1,使能快速定位。
6) _USE_LABEL。设置是否支持磁盘盘符(磁盘名字)读取与设置。我们设置为 1,使能,就可以通过相关函数读取或者设置磁盘的名字了。
7) _CODE_PAGE。设置语言类型,设置为 936,即简体中文(GBK 码,需要 c936.c 文件支持,该文件在 option 文件夹)。
8) _USE_LFN。设置是否支持长文件名(还需要_CODE_PAGE 支持),取值范围为 0~3。 0,表示不支持长文件名, 1~3 是支持长文件名,但是存储地方不一样,我们选择使用 3,通过 ff_memalloc 函数来动态分配长文件名的存储区域。
9) _VOLUMES。用于设置 FATFS 支持的逻辑设备数目,我们设置为 2,即支持 2 个设备。
10) _MAX_SS。扇区缓冲的最大值,一般设置为 512。
++++++++++++++++++++++++++++++++++
FATFS 的移植主要分为 3 步:

数据类型: 在 integer.h 里面去定义好数据的类型。
MDK5 编译器,数据类型和 integer.h 里面定义的一致,所以此步,不需要做任何改动。

配置: 通过 ffconf.h 配置 FATFS 的相关功能,
我们将对应配置修改为我们介绍时候的值即可,其他的配置用默认配置。

函数编写:打开 diskio.c,进行底层驱动编写, 一般需要编写 6 个接口函数,
底层磁盘 I/O 模块并不是 FATFS 的一部分,并且必须由用户提供。
一般有 6 个,在 diskio.c 里面。
首先是 disk_initialize 函数,
第二个函数是 disk_status 函数,
第三个函数是 disk_read 函数,
第四个函数是 disk_write 函数,
第五个函数是 disk_ioctl 函数,
最后一个函数是 get_fattime 函数,

++++++++++++++++++++++++++++++++++
FATFS 提供了很多 API 函数,这些函数 FATFS 的自带介绍文件里面都有详细的介绍(包括参考代码),
需要注意的是,
在使用FATFS的时候,必须先通过f_mount函数注册一个工作区,才能开始后续 API 的使用,
+++++++++++++++++++++++++++++++++
实战,
在cubemx中,配置middleware,选择fatfs,
在mode中,勾选sd card,

在set define中,
read only设置为disable,
minimize设置为disable,
string func 设置为enable with LF to CRLF。
use find 设置为disable,
use mkfs设置为enable,
fastseek设置为enable,
expand设置为disable,
chmod设置为disable,
use label设置为enable,
forward设置为disable,

code page设置为simp chinese,
use lfn设置为enable with dynamic on heap
max lfn设置为255,

volumes logic driver,设置为2,
max ss设置为512,
min ss设置为512,
multi partition设置为disable,
use trim erase feature设置为disable,

fs_tiny设置为disable,

advanced setting,
use dma template,设置为enable,

platform setting中,需要配置一个pulldown的gpio input ,用来作为card detect使用。
我们板子上没有这个信号。
解决方法有两个,
一,找一个未使用的管脚,配置为pulldown-input。
二,带着警告强行生成工程,然后修改BSP中的代码。

__weak uint8_t BSP_SD_IsDetected(void)
{
  __IO uint8_t status = SD_PRESENT;
    
  return status;
}

这是一个弱函数,可以在其他地方覆盖定义。
这个函数返回gpio input上读取的状态,如果是高电平,说明没有sd card被检测到,后续的函数就不能执行。
所以,我们可以强行让这个函数返回0,即低电平。
++++++++++++++++++++++++++++++++++++++
需要包含ff.h文件。

#include "ff.h"

fatfs的使用,使用的是多个控制块和描述块来存放file的相关参数和属性。我们需要使用句柄来对这些结构体对象进行控制。
在单片机中,如果没有使用malloc,那么我们就需要使用全局变量来静态分配这些结构体对象。

FATFS fs[_VOLUMES];
FIL file;	  		//文件1
FIL ftemp;	  		//文件2.
UINT br,bw;			//读写变量
FILINFO fileinfo;	//文件信息
DIR dir;  			//目录

u8 fatbuf[512];			//SD卡数据缓存区

++++++++++++++++++++++++++++++++++++++++++++
常用的的ff的函数,如下实例
1)f_mount,

f_mount(&fs[0],"0:",1); 					//挂载SD卡 

磁盘编号(“0:”/“1:”)
挂载成功后,FATFS结构体对象会被填充。

2)f_open,

f_open(file,(const TCHAR*)path,mode);//打开文件夹或者文件

常用的mode有:
FA_READ
使能读访问。
FA_WRITE
使能写访问。
FA_OPEN_EXISTING
测试并打开。如果文件不存在则返回failed。
FA_CREATE_NEW
测试并创建。如果文件已存在则返回failed。
FA_CREATE_ALWAYS
强制创建。如果文件存在则删除原文件并创建新文件。
FA_OPEN_ALWAYS
强制打开。如果文件存在,则打开文件,如果不存在,则创建文件并打开。
FA_OPEN_APPEND
强制打开。如果文件存在,则打开文件,并设置current positon为文件末尾。如果不存在,则创建文件并打开。

3)f_close,

f_close(file);

4)f_mkdir,

f_mkdir((const TCHAR*)dstpathname);//如果文件夹已经存在,就不创建.如果不存在就创建新的文件夹.

建立一个dir,

5)f_read,

f_read(file,fatbuf,len,&br);

从FILE中记录的current position,读取len个byte的数据。存放到fatbuff数组中。
br用来记录实际传输的字节数。如果FILE中的剩余字节数小于len,那么br中的值将小于len。

6)f_write,

f_write(file,buff,len,&bw);

将buff中的数据,写入len个byte的数据,从FILE的current position开始,依次存放,FILE会更新current position。
bw用来记录实际传输的字节数。如果FILE满了,那么bw中的值将小于len.

7)f_opendir,

f_opendir(&dir,(const TCHAR*)path);	

按照path指定的路径,从FS中解析出文件夹,并存放到dir结构体对象中。

8)f_closedir,

f_closedir(&dir);

9)f_readdir

f_readdir(&dir,&fileinfo);	//读取一个文件的信息

从一个打开的DIR结构体中,读取current position指向的file的信息,存放到FILEINFO结构体对象中。DIR在读取后,会自动更新current position,指向下一个file。

10)f_lseek

f_lseek(file,offset);

调整一个FILE中的current position,调整到offset的位置。

11)f_tell

f_tell(file);

返回FILE的current position,即offset值。

12)f_rename

f_rename((const TCHAR *)oldname,(const TCHAR *)newname);

在当前pwd下,将一个oldname的文件,更名为newname。

13)f_unlink

f_unlink((const TCHAR *)pname);

在当前pwd下,将一个oldname的文件,删除。
或者给定一个绝对路径,删除文件。

14)f_putc

f_putc((TCHAR)c,file);

向FILE中写入一个char,到当前位置。
当_USE_STRFUNC == 2时,字符’\n’被转换为"\r\n"写入文件中。
f_putc函数是f_write的一个封装函数。
f_puts()是f_putc()的一个封装函数。
15)f_puts

f_puts((TCHAR*)c,file);

向FILE中写入一个string,到当前位置。
当_USE_STRFUNC == 2时,字符串中的’\n’被转换为"\r\n"写入文件中。

16)f_printf
f_printf函数是f_putc和f_puts的一个封装函数。
当_USE_STRFUNC == 2时,包含在格式化字符串中的’\n’将被转换成"\r\n"写入文件中。

17)f_gets

f_gets((TCHAR*)fatbuf,size,file);

从FILE中读取一个string,
如果成功,将返回fatbuf的指针,如果不成功,将返回0,即NULL。

如果_USE_STRFUNC == 2,文件中包含的’\r’则被去除。
f_gets函数是f_read的一个封装函数。
当读取到’\n’、文件结束或缓冲区被填冲了Size - 1个字符时,读操作结束。
读取的字符串以’\0’结束。
当文件结束或读操作中发生了任何错误,f_gets()返回一个空字符串。
可以使用宏拟函数f_eof()和f_error()检查EOF和错误状态。

+++++++++++++++++++++++++++++++++++++++++++++++++++++++
实例:

 	retSD = f_mount(&SDFatFS,(TCHAR const*)SDPath,1);
	if(retSD == FR_OK)
	{
        printf("挂载SD卡成功!\n");
	}

	retSD = f_open(&SDFile, "FatFs333.dat",FA_CREATE_ALWAYS | FA_WRITE );
	if(retSD == FR_OK)
	{
   		printf("FatFs333.dat 文件创建成功!\n");
 	}

	retSD=f_write(&SDFile,WriteBuffer,sizeof(WriteBuffer),&fnum);
 	if(retSD == FR_OK)
 	{
     	printf("FatFs333.dat 文件数据写入成功!\n");
	}

	retSD = f_sync(&SDFile);
	if(retSD == FR_OK)
	{
  		printf("FatFs333.dat 文件数据同步成功!\n");
	}

	if(f_close(&SDFile) == FR_OK)
	{
	  	printf("FatFs333.dat 文件关闭成功!\n");
	}

	retSD = f_open(&SDFile1, "FatFs222.txt",FA_CREATE_ALWAYS | FA_WRITE );
	if(retSD == FR_OK)
	{
		 printf("FatFs222.txt 文件创建成功!\n");
	}

	retSD=f_write(&SDFile1,WriteBuffer,sizeof(WriteBuffer),&fnum);
	if(retSD == FR_OK)
 	{
		printf("FatFs222.txt 文件数据写入成功!\n");
	}

 	retSD = f_sync(&SDFile1);
 	if(retSD == FR_OK)
	{
		printf("FatFs222.txt 文件数据同步成功!\n");
	}

	if(f_close(&SDFile1) == FR_OK)
	{
		printf("FatFs222.txt 文件关闭成功!\n");
	}

	retSD = f_opendir(&dir,SDPath);
 	if(retSD == FR_OK)
	{
		printf("打开根目录成功!\n");
 	}


	retSD = f_readdir(&dir,&fno);//读取目录项,索引会自动移到下一项
	if(retSD != FR_OK || fno.fname[0] == 0)
	{
		break;//说明没有文件
	}

++++++++++++++++++++++++++++++++++++++++++++++++++++++
一次完整的读写。

	retsd = f_mount(&fs[0],(const TCHAR *)"0:",1);    //挂载SD卡 
    if(retsd == FR_OK){
        LCD_ShowString(0,0,210,24,24,1,(uint8_t*)"fmount ok");    
    }
    else{
        LCD_ShowString(0,0,210,24,24,1,(uint8_t*)"fmount faild");  
    }
    
    retsd = f_opendir(&dir,(const TCHAR *)"0:/");
    if(retsd == FR_OK){
        LCD_ShowString(0,40,210,24,24,1,(uint8_t*)"fopendir ok");    
    }
    else{
        LCD_ShowString(0,40,210,24,24,1,(uint8_t*)"fopendir faild");
        LCD_ShowxNum(240,40,retsd,2,32,1);
    }
    
    retsd = f_mkdir((const TCHAR*)"test123");
    if(retsd == FR_OK || retsd == FR_EXIST){
        LCD_ShowString(0,80,210,24,24,1,(uint8_t*)"fmkdir ok");    
    }
    else{
        LCD_ShowString(0,80,210,24,24,1,(uint8_t*)"fmkdir faild");
        LCD_ShowxNum(240,80,retsd,2,32,1);
    }
    
    retsd = f_chdir((const TCHAR*)"test123");
    if(retsd == FR_OK){
        LCD_ShowString(0,120,210,24,24,1,(uint8_t*)"fchdir ok");    
    }
    else{
        LCD_ShowString(0,120,210,24,24,1,(uint8_t*)"fchdir faild");
        LCD_ShowxNum(240,120,retsd,2,32,1);
    }
    
    retsd = f_open(&file, (const TCHAR*)"test123.txt", FA_OPEN_ALWAYS | FA_WRITE);
    if(retsd == FR_OK){
        LCD_ShowString(0,160,210,24,24,1,(uint8_t*)"fopen wr ok");    
    }
    else{
        LCD_ShowString(0,160,210,24,24,1,(uint8_t*)"fopen wr faild");
        LCD_ShowxNum(240,160,retsd,2,32,1);
    }
    
    bcnt = f_puts((const TCHAR*)"hello world\n",&file);
    if(bcnt == 0){
        LCD_ShowString(0,200,210,24,24,1,(uint8_t*)"fputs failed");    
    }
    else{
        LCD_ShowString(0,200,210,24,24,1,(uint8_t*)"fputs ok");
        LCD_ShowxNum(240,200,bcnt,2,32,1);
    }
    
    retsd = f_close(&file);
    if(retsd == FR_OK){
        LCD_ShowString(0,240,210,24,24,1,(uint8_t*)"fclose wr ok");    
    }
    else{
        LCD_ShowString(0,240,210,24,24,1,(uint8_t*)"fclose wr faild");
        LCD_ShowxNum(240,240,retsd,2,32,1);
    }
    
    retsd = f_open(&file, (const TCHAR*)"test123.txt", FA_OPEN_ALWAYS | FA_READ);
    if(retsd == FR_OK){
        LCD_ShowString(0,280,210,24,24,1,(uint8_t*)"fopen rd ok");    
    }
    else{
        LCD_ShowString(0,280,210,24,24,1,(uint8_t*)"fopen rd faild");
        LCD_ShowxNum(240,280,retsd,2,32,1);
    }
    
    rptr = f_gets((TCHAR*)fatbuf,512,&file);
    if(rptr == 0){
        LCD_ShowString(0,320,210,24,24,1,(uint8_t*)"fgets failed");    
    }
    else{
        LCD_ShowString(0,320,210,24,24,1,(uint8_t*)rptr);
    }
    
    retsd = f_close(&file);
    if(retsd == FR_OK){
        LCD_ShowString(0,360,210,24,24,1,(uint8_t*)"fclose rd ok");    
    }
    else{
        LCD_ShowString(0,360,210,24,24,1,(uint8_t*)"fclose rd faild");
        LCD_ShowxNum(240,360,retsd,2,32,1);
    }
    
    
    retsd = f_closedir(&dir);
    if(retsd == FR_OK){
        LCD_ShowString(0,400,210,24,24,1,(uint8_t*)"fclosedir ok");    
    }
    else{
        LCD_ShowString(0,400,210,24,24,1,(uint8_t*)"fclosedir faild");
        LCD_ShowxNum(240,400,retsd,2,32,1);
    }

注意,
FATFS中,盘符用字符串表示时,第一个盘符是"0:",第二个盘符是"1:",依次类推。
打开根目录时,也是用字符串表示,首先是盘符,然后是根目录标识符。“0:/”
打开文件,要么使用绝对路径,用盘符作为前导,要么使用相对路径,
如果使用相对路径,那么系统内必须已经存在一个pwd,所以,首先是要opendir,然后chdir。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
STM32CubeMX F429 SDIO是指在STM32F429系列芯片上,使用STM32CubeMX软件进行配置时,选择SDIO接口作为SD卡的通信接口。 在配置SDIO参数设置时,可以选择SDIO的位宽,可以是1位或4位数据线模式。需要确保STM32单片机的SDIO设置的数据线位宽和SD卡上设置的数据线位宽一致。 对于STM32F429IGT6这款芯片,SDIO接口的卡槽没有detect引脚。所以通常的做法是先将hsd.Init.BusWide设为SDIO_BUS_WIDE_1B,执行HAL_SD_Init函数后,再调用HAL_SD_ConfigWideBusOperation函数将STM32和SD卡同时设置为4位模式。 如果使用的是STM32F103系列芯片,如STM32F103ZET6或STM32F103RCT6,直接插上SD卡即可使用。如果使用Micro SD卡需要适配器。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [STM32F429IGT6项目准备3——使用STM32CubeMX初始化SDIO(DMA传输)](https://blog.csdn.net/qq_42039294/article/details/112171317)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* *3* [STM32CubeMX系列09——SDIO(SD卡读写、SD卡移植FATFS文件系统)](https://blog.csdn.net/weixin_46253745/article/details/127865071)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值