尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32 FatFs移植实战:SD卡驱动从SPI到SDIO的避坑指南

STM32 FatFs移植实战:SD卡驱动从SPI到SDIO的避坑指南 1. 为什么FatFs移植总在SD卡驱动上翻车搞STM32的人多少都碰过文件系统这块硬骨头。项目标题里说“5分钟搞定”听起来像标题党但如果你把SD卡驱动那层理顺了FatFs本身的对接确实就是喝杯茶的功夫。问题在于绝大多数人卡住的地方根本不是FatFs而是SD卡初始化死活过不去、读写返回错误、挂载时提示FR_NO_FILESYSTEM。我见过太多人在这上面耗掉一整个周末最后发现是SPI模式没切对或者供电不稳导致CMD0都发不出去。这篇内容面向的是手里已经有STM32最小系统板、一张SD卡、一个读卡器模块的嵌入式开发者。不管你是用标准库还是HAL库不管你是SPI模式还是SDIO模式核心逻辑是通的。我会把FatFs的对接层、SD卡的底层驱动、以及两者之间的胶水代码全部拆开讲清楚让你不仅能把代码跑起来还能明白每一行在干什么。适合正在做数据采集项目、需要本地存储日志、或者做毕业设计要存文件的学生和工程师参考。先说清楚一个基本认知FatFs是一个纯软件的文件系统模块它不关心你的存储介质是SD卡、NAND Flash还是U盘。它只要求你提供几个底层接口函数——初始化、读扇区、写扇区、控制命令。SD卡驱动则是另一回事它负责跟卡本身通信完成上电初始化、识别卡类型、设置块长度、读写数据块。这两层之间通过一个叫diskio.c的文件连接起来。移植的本质工作就是把diskio.c里的几个函数填好让FatFs能通过它们间接操作SD卡。那为什么很多人觉得难因为SD卡的协议本身有门槛。SD卡有两种通信模式SDIO模式和SPI模式。SDIO模式速度快但引脚多、时序要求严STM32的SDIO外设配置起来寄存器一大堆。SPI模式速度慢一些但引脚少、协议简单、调试方便特别适合初学者和引脚紧张的项目。我个人的建议是如果你的项目对写入速度要求不超过几百KB/s直接用SPI模式省心省力。如果你要做音频录制或者高速数据采集那再考虑SDIO。还有一个容易被忽略的点SD卡的版本差异。SDSC标准容量最大2GB、SDHC高容量4GB到32GB、SDXC扩展容量64GB以上在初始化流程上有区别。SDHC和SDXC用的是块地址SDSC用的是字节地址。如果你拿一张32GB的卡用SDSC的流程去初始化CMD0之后CMD8就会返回错误。这个坑我在早期项目中踩过当时手头只有一张16GB的卡代码是按2GB卡写的结果死活初始化不了换了张小容量卡立马就通了。后来查规范才知道CMD8是区分卡类型的关键命令。所以这一章的核心结论是FatFs移植的难点不在FatFs而在SD卡驱动的正确实现。把SD卡的初始化流程、读写时序、命令响应搞明白FatFs的对接就是填几个函数的事。接下来的章节我会从整体设计思路开始一步步拆到代码层面。2. 整体方案设计与关键选型考量2.1 SPI模式还是SDIO模式一张表帮你做决定选SPI还是SDIO这是移植前第一个要拍板的事。我整理了一个对比表基于实际项目经验不是抄手册的。对比项SPI模式SDIO模式引脚数量4根CS、SCK、MISO、MOSI6根CLK、CMD、D0-D3最高时钟通常18-36MHz48MHz理论速度约2-4MB/s约10-20MB/s配置复杂度低普通SPI外设即可高需配置SDIO外设和DMA调试难度低逻辑分析仪一抓就明白中时序复杂卡兼容性好几乎所有卡都支持好但部分老卡可能有问题占用CPU较高需轮询或中断低DMA自动搬运适合场景数据日志、配置文件、中低速存储音频、图像、高速采集我一般建议第一次移植选SPI。原因很简单SPI的协议是线性的你发一个命令、等一个响应、读一个数据块逻辑清晰。SDIO的协议是并行的四条数据线同时工作出问题时你很难判断是命令通道的问题还是数据通道的问题。而且SPI模式下你甚至可以用普通的GPIO模拟SPI来调试虽然速度慢但能帮你确认卡本身是好的。2.2 FatFs版本选择与裁剪策略FatFs的版本更新不算频繁目前主流的是R0.14和R0.15。R0.15对长文件名和Unicode的支持更好但代码量也更大。如果你的STM32Flash空间紧张比如只有64KB建议用R0.14并关闭长文件名支持。我实测过R0.14在关闭LFN和Unicode后编译出来大约12KB左右R0.15大概15KB。裁剪的关键在ffconf.h这个文件。几个必须关注的宏FF_USE_LFN长文件名支持。设为0最省空间设为1或2需要额外的缓冲区。如果你的文件名都是8.3格式比如LOG001.TXT直接设0。FF_FS_MINIMIZE最小化级别。设为0保留全部功能设为3只保留最基本的读写。我一般设0因为f_mkdir和f_stat这些函数在实际项目中很常用。FF_USE_STRFUNC字符串函数支持。如果你要用f_puts和f_gets设为1。否则设0。FF_FS_RPATH相对路径支持。设0的话所有路径都要从根目录写起设1或2支持相对路径。建议设1。FF_VOLUMES逻辑驱动器数量。只挂一个SD卡就设1。还有一个容易忽略的FF_MAX_SS。这个值决定了扇区缓冲区的大小。SD卡的扇区是512字节所以设512就行。如果你设成4096会浪费RAM。但如果你用的是某些SPI Flash扇区可能是4096那就要对应修改。2.3 底层驱动与FatFs的对接层设计FatFs要求你提供6个函数在diskio.c里实现DSTATUS disk_initialize (BYTE pdrv); DSTATUS disk_status (BYTE pdrv); DRESULT disk_read (BYTE pdrv, BYTE* buff, LBA_t sector, UINT count); DRESULT disk_write (BYTE pdrv, const BYTE* buff, LBA_t sector, UINT count); DRESULT disk_ioctl (BYTE pdrv, BYTE cmd, void* buff); DWORD get_fattime (void);这6个函数里disk_initialize和disk_read是最核心的。disk_initialize负责调用SD卡驱动的初始化函数disk_read负责把FatFs请求的扇区数据读出来。disk_write只在你要写文件时才需要如果只是读可以暂时返回RES_OK或者干脆不实现。get_fattime是给文件打时间戳的。如果你没有RTC直接返回一个固定值就行比如return ((DWORD)(2024-1980)25 | (1)21 | (1)16);。这个不影响功能只是文件属性里的时间不对。对接层的设计原则是diskio.c只做协议转换不包含任何硬件操作。硬件操作全部封装在sd_driver.c里。这样你换一个MCU或者换一个SD卡模块只需要改sd_driver.cdiskio.c和FatFs都不用动。这个分层思想在实际项目中非常重要我见过太多人把SPI读写和FatFs接口混在一起写后来换硬件时整个文件重写。3. SD卡驱动适配的核心细节3.1 SD卡上电初始化的完整流程SD卡的初始化流程是有严格顺序的跳步或者顺序错了都会失败。SPI模式下的标准流程如下上电后等待至少1ms让卡内部稳压器稳定。发送至少74个时钟脉冲CS保持高电平。这一步是让卡进入SPI模式。CS拉低发送CMD0GO_IDLE_STATE参数0CRC固定为0x95。卡应返回0x01。发送CMD8SEND_IF_COND参数0x1AACRC固定为0x87。如果返回0x01说明是SDHC/SDXC卡如果返回0x05说明是SDSC卡或老卡。发送CMD58READ_OCR读取OCR寄存器确认电压范围。循环发送CMD55ACMD41直到返回0x00。这一步是等待卡初始化完成可能需要几百毫秒。发送CMD58再次读取OCR检查CCS位判断是块地址还是字节地址。发送CMD16SET_BLOCKLEN设置块长度为512字节。注意SDHC卡不需要这一步因为块长度固定512。这里面有几个关键点。第一CMD0的CRC是固定的0x95CMD8的CRC是0x87其他命令的CRC可以随便填因为SPI模式下默认不校验CRC。但CMD0和CMD8必须填对否则卡不响应。第二ACMD41的循环要有超时我一般设200次每次延时1ms总共200ms。如果超时还没返回0x00基本可以判断卡有问题或者接线不对。第三CMD58读出来的OCR寄存器bit30是CCS位1表示SDHC/SDXC0表示SDSC。这个决定了后续读写时地址是块地址还是字节地址。我实际调试时遇到过一个情况CMD0返回0x01CMD8返回0x01但ACMD41一直返回0x01。查了半天发现是SPI时钟太快了初始化阶段必须把SPI时钟降到400KHz以下。STM32的SPI分频系数要设大一点比如72MHz主频下分频256得到281KHz。等初始化完成后再把时钟升上去。3.2 SPI时序配置与速度切换策略SPI的配置看似简单但有几个细节直接影响SD卡能不能稳定工作。首先是时钟极性CPOL和时钟相位CPHA。SD卡在SPI模式下要求CPOL0CPHA0也就是空闲时SCK为低电平数据在SCK上升沿采样。这个跟很多SPI Flash的配置不一样别搞混了。其次是数据位宽。STM32的SPI可以配置为8位或16位。SD卡的命令和响应都是8位的所以必须配成8位。如果你配成16位发CMD0的时候会多发一个字节卡就懵了。然后是速度切换。初始化阶段SPI时钟必须低于400KHz这是SD卡规范的要求。初始化完成后可以切换到高速。我一般分三档初始化阶段281KHz72MHz / 256读写阶段18MHz72MHz / 4如果信号质量好可以尝试36MHz72MHz / 2但速度不是越高越好。我实测过用杜邦线连接SD卡模块时18MHz以上就容易出现读写错误。如果你把SD卡直接焊在PCB上走线短且阻抗匹配好36MHz也能稳定跑。所以速度切换后一定要做读写测试确认数据正确再正式使用。还有一个细节SPI的NSS引脚。如果你用硬件NSSSTM32在某些模式下会自动控制NSS但SD卡需要手动控制CS。我建议直接用普通GPIO控制CSSPI配置为软件NSS模式。这样你能精确控制CS的拉低和拉高时机特别是在发送命令序列时。3.3 命令发送与响应接收的实现细节SD卡的SPI通信是“命令-响应”模式。发送一个命令后卡会在若干个时钟周期后返回响应。响应有几种格式R11字节、R22字节、R35字节、R75字节。最常用的是R1。发送命令的函数大概长这样uint8_t SD_SendCmd(uint8_t cmd, uint32_t arg, uint8_t crc) { uint8_t r1; uint8_t retry 0; SD_CS_LOW(); SPI_ReadWriteByte(0xFF); // 发送一个填充字节 SPI_ReadWriteByte(cmd | 0x40); // 命令号 起始位 SPI_ReadWriteByte(arg 24); SPI_ReadWriteByte(arg 16); SPI_ReadWriteByte(arg 8); SPI_ReadWriteByte(arg); SPI_ReadWriteByte(crc); do { r1 SPI_ReadWriteByte(0xFF); retry; } while ((r1 0x80) (retry 200)); SD_CS_HIGH(); SPI_ReadWriteByte(0xFF); // 补一个时钟 return r1; }这里的关键是等待响应的循环。卡在收到命令后会在最多8个时钟周期内返回响应。响应字节的最高位是0表示有效。如果最高位是1说明卡还在忙需要继续等待。我设了200次重试实际测试中一般几次就返回了。读数据块的时候卡会先返回一个0xFE的起始令牌然后是512字节数据最后是2字节CRC。写数据块时你需要先发送0xFE然后发512字节数据最后发2字节CRC。卡会返回一个数据响应字节低5位是状态0x05表示写入成功。这里有个坑写数据后卡会进入忙状态DO线被拉低。你必须等待DO线变高才能发送下一个命令。我见过有人写完数据直接发下一个命令结果卡完全不响应。正确的做法是// 等待卡忙状态结束 while (SPI_ReadWriteByte(0xFF) ! 0xFF);这个等待可能要几十毫秒特别是写入大文件时。所以写操作比读操作慢很多这是正常现象。4. FatFs对接与完整移植实操4.1 diskio.c的完整实现与参数说明diskio.c是FatFs和SD卡驱动之间的桥梁。我直接给出经过验证的实现然后逐段解释。#include diskio.h #include sd_driver.h #define DEV_SD 0 DSTATUS disk_initialize (BYTE pdrv) { if (pdrv ! DEV_SD) return RES_PARERR; if (SD_Init() SD_OK) { return 0; } return STA_NOINIT; } DSTATUS disk_status (BYTE pdrv) { if (pdrv ! DEV_SD) return RES_PARERR; return 0; } DRESULT disk_read (BYTE pdrv, BYTE* buff, LBA_t sector, UINT count) { if (pdrv ! DEV_SD) return RES_PARERR; if (SD_ReadBlocks(buff, sector, count) SD_OK) { return RES_OK; } return RES_ERROR; } DRESULT disk_write (BYTE pdrv, const BYTE* buff, LBA_t sector, UINT count) { if (pdrv ! DEV_SD) return RES_PARERR; if (SD_WriteBlocks((BYTE*)buff, sector, count) SD_OK) { return RES_OK; } return RES_ERROR; } DRESULT disk_ioctl (BYTE pdrv, BYTE cmd, void* buff) { if (pdrv ! DEV_SD) return RES_PARERR; switch (cmd) { case CTRL_SYNC: return RES_OK; case GET_SECTOR_COUNT: *(DWORD*)buff SD_GetSectorCount(); return RES_OK; case GET_SECTOR_SIZE: *(WORD*)buff 512; return RES_OK; case GET_BLOCK_SIZE: *(DWORD*)buff 1; return RES_OK; default: return RES_PARERR; } } DWORD get_fattime (void) { return ((DWORD)(2024 - 1980) 25) | ((DWORD)1 21) | ((DWORD)1 16); }disk_initialize里调用SD_Init()这个函数完成前面说的初始化流程。如果成功返回0失败返回STA_NOINIT。注意返回值0表示成功不是RES_OK。这是FatFs的规范别搞混了。disk_read和disk_write直接调用SD卡的块读写函数。这里的sector参数是逻辑块地址对于SDHC卡就是块地址对于SDSC卡需要转换成字节地址。这个转换在SD_ReadBlocks内部处理。disk_ioctl里最重要的是GET_SECTOR_COUNTFatFs用它来计算文件系统的总容量。SD_GetSectorCount()返回卡的总扇区数这个值在初始化时通过CMD9SEND_CSD读取CSD寄存器计算得到。get_fattime返回一个固定时间。如果你有RTC可以在这里读取实际时间。格式是bit31-25是年从1980算起bit24-21是月bit20-16是日bit15-11是时bit10-5是分bit4-0是秒除以2。4.2 f_mount与文件读写测试对接层写好后就可以在main函数里挂载和测试了。完整流程如下#include ff.h FATFS fs; FIL fil; FRESULT fres; UINT bw, br; BYTE buffer[512]; int main(void) { // 硬件初始化 SystemInit(); USART1_Init(115200); SD_SPI_Init(); // 挂载文件系统 fres f_mount(fs, 0:, 1); if (fres ! FR_OK) { printf(Mount failed: %d\r\n, fres); while(1); } printf(Mount OK\r\n); // 写文件测试 fres f_open(fil, 0:/test.txt, FA_CREATE_ALWAYS | FA_WRITE); if (fres FR_OK) { f_write(fil, Hello FatFs!, 12, bw); f_close(fil); printf(Write OK, %d bytes\r\n, bw); } // 读文件测试 fres f_open(fil, 0:/test.txt, FA_READ); if (fres FR_OK) { f_read(fil, buffer, sizeof(buffer), br); f_close(fil); buffer[br] \0; printf(Read: %s\r\n, buffer); } while(1); }f_mount的第三个参数是立即挂载标志。设为1表示立即执行挂载操作设为0表示延迟挂载。我建议设1这样能立刻知道挂载是否成功。如果f_mount返回FR_NO_FILESYSTEM说明卡上没有FatFs能识别的文件系统。这时候你需要先格式化。可以用f_mkfs函数fres f_mkfs(0:, 0, 0);但注意f_mkfs会擦除卡上所有数据。如果卡里有重要文件先在电脑上备份。如果f_mount返回FR_DISK_ERR说明底层读写有问题。这时候要回到disk_read去排查用逻辑分析仪抓SPI波形看命令和响应是否正确。4.3 性能优化与缓冲区配置FatFs的性能很大程度上取决于缓冲区策略。ffconf.h里有几个关键配置FF_FS_TINY设为1时FatFs使用文件对象自带的缓冲区不单独分配扇区缓冲区。省RAM但速度慢。设为0时每个文件对象有一个512字节的缓冲区。建议设0。FF_USE_FASTSEEK快速定位支持。如果你需要频繁在文件中间读写设为1并配置clust表。一般项目设0就行。FF_FS_EXFATexFAT支持。如果你要用64GB以上的卡必须开这个。但代码量会增加不少。写文件时f_write并不是每次调用都直接写卡。FatFs会先把数据攒在缓冲区里等缓冲区满了或者调用f_sync/f_close时才真正写入。所以如果你写小文件后不调用f_close数据可能还在缓冲区里。我见过有人写完文件直接断电结果文件是空的。养成好习惯写完就f_close或者定期f_sync。读文件时如果你要连续读大块数据可以用f_read一次读多个扇区。FatFs内部会调用disk_readcount参数就是扇区数。我的SD_ReadBlocks支持多块连续读用CMD18READ_MULTIPLE_BLOCK实现比单块读快很多。实测数据SPI时钟18MHz下单块读速度约300KB/s多块读能到800KB/s左右。写速度单块约150KB/s多块约400KB/s。这个速度对于数据日志和配置文件完全够用。如果你要存音频建议用SDIO模式。5. 常见问题排查与避坑指南5.1 初始化失败问题速查表现象可能原因排查方法解决方案CMD0无响应接线错误/卡未供电万用表测电压检查CS、SCK、MOSI、MISO确认3.3V供电CS拉低CMD0返回0xFFSPI模式未进入检查74个时钟脉冲上电后发至少74个时钟CMD8返回0x05卡不支持CMD8正常说明是SDSC卡跳过CMD8直接ACMD41ACMD41一直0x01初始化超时降低SPI时钟到400KHz以下分频系数设256ACMD41返回0x00但CMD58失败电压不匹配读OCR确认电压范围检查卡是否支持3.3Vf_mount返回FR_NO_FILESYSTEM卡未格式化用读卡器在电脑上看用f_mkfs格式化f_mount返回FR_DISK_ERR读写失败逻辑分析仪抓波形检查disk_read实现写文件后内容为空未关闭文件检查是否调用f_close写完必须f_close或f_sync这个表里的问题我基本都遇到过。最常见的是CMD0无响应十有八九是接线问题。SD卡模块的引脚标注有时候不清晰MOSI和MISO容易接反。记住MOSI是主机输出从机输入接卡的DIMISO是主机输入从机输出接卡的DO。还有一个隐蔽的问题SD卡模块的电平转换。有些模块用AMS1117稳压到3.3V但信号线没有电平转换。如果你的STM32是5V系统直接连可能会烧卡。STM32的IO一般是3.3V可以直接连。但如果你用5V的MCU必须加电平转换电路。5.2 读写不稳定问题的排查思路读写不稳定比初始化失败更让人头疼因为它时好时坏。我总结了几条经验第一电源问题。SD卡在写入时电流会突然增大如果电源滤波不好电压会跌落导致写入失败。解决方法是在SD卡模块的VCC和GND之间并一个100uF的电解电容和一个0.1uF的陶瓷电容。我实测过不加电容时写入大文件经常出错加了之后稳定很多。第二SPI时钟太快。前面说过杜邦线连接时18MHz以上容易出错。如果你发现读写偶尔失败先把SPI时钟降到9MHz试试。如果降速后稳定了说明是信号完整性问题。第三CS控制时机。SD卡要求在发送命令前拉低CS收到响应后拉高CS。但有些实现里CS拉高太早导致卡还没处理完命令就结束了。我的做法是收到响应后再多发一个0xFF时钟然后才拉高CS。这个额外的时钟给卡一点处理时间。第四多块读写的中断处理。如果你用DMA做SPI传输要注意DMA完成中断和SPI忙状态的判断。我见过有人在DMA还没传完就拉高CS导致数据丢失。正确的做法是等DMA传输完成标志置位再等SPI的BSY位清零最后拉高CS。5.3 实际项目中的经验技巧分享几个我在实际项目中总结的技巧都是文档里不会写的。技巧一在SD_Init里加一个重试机制。有时候卡第一次初始化会失败但复位后第二次就成功了。我一般重试3次每次之间延时100ms。这个技巧解决了我遇到的“偶尔上电初始化失败”的问题。技巧二用卡的热插拔检测。SD卡模块一般有一个CD引脚卡插入时接地。你可以用外部中断检测这个引脚卡拔出时调用f_mount(NULL, 0:, 0)卸载文件系统卡插入时重新挂载。这样避免卡拔出后程序还在读写导致死机。技巧三文件写入用“写临时文件重命名”策略。直接写目标文件时如果中途断电文件会损坏。更安全的做法是先写一个.tmp文件写完后f_close然后f_rename成目标文件名。这样即使断电最多丢失临时文件不会破坏已有数据。技巧四定期f_sync。如果你做数据日志每隔几分钟调用一次f_sync把缓冲区数据刷到卡上。这样即使突然断电最多丢失最后几分钟的数据不会整个文件都丢。技巧五注意扇区对齐。FatFs的读写是按扇区来的如果你频繁写小数据比如每次写几个字节效率很低。更好的做法是在RAM里攒够一个扇区的数据再写。我一般用一个512字节的缓冲区攒满了调用f_write。5.4 从SPI迁移到SDIO的注意事项如果你的项目后期需要更高速度从SPI迁移到SDIO是可行的但要注意几点。首先SDIO的初始化流程和SPI不同。SDIO模式下CMD0之后要发CMD8然后ACMD41但不需要发74个时钟脉冲。SDIO外设会自动处理时钟。其次SDIO的读写是以块为单位支持1位和4位模式。4位模式速度快但需要D0-D3四根数据线。如果你的PCB只留了SPI的引脚那就只能用1位模式速度提升有限。第三SDIO的中断和DMA配置更复杂。STM32的SDIO外设有自己的DMA请求需要配置DMA通道。如果配置不当会出现数据错位或者传输不完整。第四FatFs的diskio.c基本不用改只需要把SD_ReadBlocks和SD_WriteBlocks的实现换成SDIO版本。这就是分层设计的好处。我个人的建议是如果SPI模式能满足需求就不要折腾SDIO。SDIO的调试成本比SPI高很多而且STM32的SDIO外设在不同系列之间有些差异移植时可能要重新调。除非你的项目明确需要高速存储否则SPI模式是性价比最高的选择。6. 移植完成后的验证与扩展思路移植完成后不要只跑一个简单的读写测试就完事。我一般会做一套完整的验证确保文件系统在各种场景下都能稳定工作。验证一连续写入大文件。创建一个1MB的文件循环写入数据写完后读出来对比。这个测试能暴露缓冲区管理和多块读写的问题。验证二多文件操作。同时打开多个文件交替读写。这个测试能验证FatFs的文件对象管理是否正确。验证三异常断电测试。在写入过程中直接断电重新上电后检查文件系统是否能正常挂载已有文件是否损坏。这个测试能验证你的f_sync策略是否合理。验证四长时间运行测试。让设备连续写入日志24小时检查是否有写入失败或者文件系统损坏。这个测试能暴露电源稳定性和SD卡寿命问题。扩展方面如果你要做数据采集项目可以在FatFs之上封装一层日志模块。比如定义一个LOG_Write函数内部处理文件创建、追加写入、定期sync。这样上层应用不用关心文件系统的细节。如果你要做OTA升级可以把固件文件存在SD卡里Bootloader读取SD卡中的固件写入Flash。这时候FatFs的读性能就很重要建议用多块读。如果你要做配置文件管理可以用f_gets和f_puts按行读写。注意文本文件的换行符处理Windows是\r\nLinux是\n。FatFs默认按二进制处理你需要自己处理换行。最后说一个我踩过的坑SD卡的寿命。SD卡有写入次数限制特别是便宜的卡可能几千次写入后就坏了。如果你做高频数据日志建议用工业级SD卡或者加一个磨损均衡算法。但磨损均衡在FatFs层面不好做更实际的做法是定期更换SD卡或者用eMMC替代。这个项目后续还可以扩展的方向很多比如加一个Web服务器通过SD卡提供文件下载或者加一个FTP服务器实现远程文件管理。但那些都是上层应用的事了底层文件系统这一层把FatFs和SD卡驱动理顺后面就是搭积木。
返回列表