qf-rs/_posts/2021-05-20-基于SPI模式的SD卡驱动.md

8.4 KiB
Raw Permalink Blame History

layout title subtitle date author cover categories tags
post 基于SPI模式的SD卡驱动 编写驱动实现SD卡数据的访问和存储来保存文件系统到SD卡上 2021-05-20 刘一鸣 /assets/img/sd_spi_init.png 硬件驱动 SPI协议 SD协议 软件设计

SD卡驱动是xv6-k210项目文件系统开发的一环。在设计中我们希望FAT32文件系统能够保存在SD卡这样的外存上而不是通过类似于内存盘的方式保存在内存中。为此操作系统需要SD卡驱动来实现对SD卡上数据访存。

早在2020年末的时候车春池@hustccc同学就试图移植勘智的官方代码到xv6-k210项目中来实现SD卡驱动。但所移植的官方代码并不稳定在读写SD的过程中不时地会出现未知的错误。同时官方代码与SD协议规范中定义的SD卡驱动流程有着大量的出入同时官方代码的码风也实在让人不敢恭维。因此出于种种考虑我最终决定重新按照SD协议规范中的描述重新编写SD卡驱动。

1. SD卡驱动的编写

1.1 SD卡驱动的基础

出于简单考虑选择通过SPI协议完成上位机与SD卡之间的数据通信。SPI协议只需要四个引脚就可以实现且工作机制简单易懂。勘智官方为我们实现K210下的SPI通信协议主要表现为kernel/spi.c文件)。故我们可以直接使用该代码来完成与通信。

关于SPI协议的相关细节可以参考SPI通信协议详解这篇文章。

在解决了通信协议的问题之后我们便可以开始着手SD卡驱动的开发了。作为参考SD Association给出的文档_Part1_Physical_Layer_Simplified_Specification_Ver8.00.pdf_以下简称“文档”的第7章“SPI Mode”中对此有着详细的描述。感兴趣的读者可以自行查阅文档。

上位机对SD卡的操作是通过发送CMD命令来完成的。在SPI模式下一条完整的CMD命令包含命令序号Command Index、命令参数Argument以及CRC7校验位。而为了简单起见大部分时候我们都将使用“CMD X”来代指某一条命令其中_X_为该命令的序号。CMD命令的格式如下图所示。同时在SPI模式下发送任何一条命令都必然能够收到回复Response可以通过回复内容判断命令的执行情况。关于CMD命令和回复的详细内容详见文档7.3节。

1.2 SD卡的初始化

选择在SPI模式下对SD卡进行初始化。文档在7.2.1小节“Mode Selection and Initialization”中详细地描述了这一过程。其大致流程如下图所示

上图中的操作可大致分为如下几个部分:

  1. 让SD卡进入SPI模式。这一操作是通过发送CMD0命令来完成的。
  2. 确认SD卡的“Interface Operation Condition”。该操作通过CMD8命令来完成。通过发送该命令Host会确认其所提供的工作电压是否满足SD卡的要求。需要注意的是CMD8并不总是能收到SD卡的回复。能否收到回复取决于SD卡的版本。
  3. 进一步确定SD卡的工作电压区间满足要求。这是通过CMD58命令实现的。如下图所示该命令的回复为SD卡OCR寄存器中的内容。这里我们主要检查“VDD Voltage Window“位段。
  4. 使SD卡脱离空闲状态Idle状态。这是通过发送应用命令ACMD41命令完成的。需要注意的是尽管流程图中没有画出来但是在发送所有的ACMD命令前需要额外地发送一条CMD55命令。CMD55命令的作用是告诉SD其所接收到的下一条命令会是一条应用命令或者称作ACMD命令。另外因为ACMD41命令会让SD卡脱离空闲状态所以在ACMD41之后的所有命令所收到的R1回复中idle位都不再应该为1在此之前idle位应该是置1的
  5. 确定SD卡的容量Capacity类型。这同样是通过CMD58来完成的。通过OCR寄存器的CCS位段可以知道当前SD卡是Standard Capacity、SDHC还是SDXC通常容量小于2GB的SD卡为Standard Capacity大于2GB的为SDHC/SDXC。我们需要知道SD卡容量类型主要是出于SD卡读写的考虑——Standard Capacity类型的卡与SDHC/SDXC类型的卡在寻址时有所区别。前者使用字节作为地址的基本单位而后者使用块BlockSDHC/SDXC规定一个Block的大小为512字节作为基本单位。

1.3 SD卡的读操作

SD卡的读操作主要是通过CMD17读取单个块和CMD18读取多个连续块来完成的。为了简单起见在代码中我们仅实现了CMD17读取单个块的操作。对于多个块的读操作有兴趣的读者可参考文档7.2.3节自行实现。

上图描述了SD卡进行读操作时SPI的MOSIMaster Out Slave In即上位机的数据输出端口、MISOMaster In Slave Out即上位机的数据输入端口信号的数据时序情况。可以看到在CMD17命令发送后上位机会首先收到对应的回复图中的Response然后才开始接收数据。这里有几处细节在图中并未表现但需要注意

  1. CMD17的参数是所需要读取的Data Block的起始地址。如1.2节所言该地址的单位会因SD卡的类型而不同。对于Standard Capacity的卡来说其地址是以字节为单位的。同时对于Block Size为512字节的设置来说该地址需要保证512字节对齐。
  2. Response与Data Block两段数据之间并不是连续的其间往往会混在有大量的垃圾数据。在收到回复后会有一个长度为1字节的'Start Token'其值为0xfe来提示接下来的数据属于Data Block。
  3. Data Block的长度在Standard Capacity类型的卡中由SD卡的Block Size决定其可以通过CMD16设定。而在SDHC/SDXC的卡中则强制为512字节。而紧随Data Block的CRC数据段的长度则为两个字节。

1.4 SD卡的写操作

SD卡的写操作与读操作类似。上位机可以通过CMD24/CMD25来进行单个/多个连续数据块的写入操作。出于简单起见代码中同样也只实现了写入单个数据块的操作读者可参考文档的7.2.4节实现对于多个连续数据块的写操作。操作时MOSI和MISO的数据时序情况如下。

有几点需要注意:

  1. 该图是从SD卡的角度出发的。图中的DataIn、DataOut均是相对于SD卡而言的。
  2. CMD24的参数同样也是需要写入的起始地址其要求与CMD17一致。
  3. 在上位机发送完Data Block中的数据后需要发送一段长度为两个字节的CRC。但SD卡并不会根据CRC对所收到的数据进行校验。
  4. 在SD卡完成Data Block的接收后SD卡才会通过DataOut发送一个字节的Data Response Token其取值详见文档7.3.3.1节。然后SD卡会花费一段时间处理上位机发送的数据。在此期间内SD卡会将DataOut端口置为低电平直至数据处理完成。可以通过轮询的方式访问DataOut端口判断SD卡的处理是否完成。
  5. 在确认SD卡处理完成后应当发送CMD13确认SD卡的处理结果。CMD13所收到的回复格式如下。

尾声

本来在最开始是没打算自己编写SD驱动的毕竟重复造轮子不可取。但是奈何使用勘智的代码总能跑出奇怪的BUG就算定位了问题也不知道怎么修改就索性找到SD Association提供的Specification自己实现一个了。因为是自己编写的知根知底日后维护起来也会轻松一些。

在这里我要感谢车春池@hustccc同学在此前所做的大量的代码移植工作代码中SPI协议的部分均来自于他的工作。同时我也要感谢陆思彤@AtomHeartCoder同学他为xv6-k210系统编写了对FAT32文件系统的相关支持。他的工作为我所编写的代码提供了大量的测试同时他本人也在代码的编写过程中提多次指出代码中所存在的问题。

最后,受制于时间有限、笔者能力有限等因素,代码以及文档中不可避免地会存在疏漏,欢迎各位读者指正。

作者:刘一鸣 @retrhelo artyomliu@foxmail.com