1. 概述

SD卡(Secure Digital Memory Card),基于MMC发展而来,被广泛应用于数码产品中。

SD卡可分为3类:SD卡(0~2G)、SDHC卡(2~32G)、SDXC卡(32G~2T)。

SD卡有9个PIN,支持SPI和SDIO模式:

PIN
123456789
SDIOCD/DAT3CMDVSSVCCCLKVSSDAT0DAT1DAT2
SPICSMOSIVSSVCCCLKVSSMISONCNC

2. SD初始化

这里介绍SD卡在SPI模式下的初始化流程。

NIOS2随笔——SD卡之SPI操作_SD卡

在SD卡进入SPI模式后,至少发送74个时钟后才能发送CMD0命令,且时钟周期不能大于400KHz。

SD卡有6类响应:R1/R1b/R2/R3/R6/R7。

在发送ACMD命令前,要先发CMD55命令。

关于具体命令格式和响应内容可参看"SD Specifications Part 1 Physical Layer Simplified Specification"


3. SPI读写操作

SD 单块读 

(1) 发送 CMD17(收到 0x00 表示发送成功) 

(2) 连续读取直到读到 0xFE 

(3) 读一个 BLOCK 

(4) 读 2 字节 CRC 

SD 卡多块读 

(1) 发送 CMD18(收到 0x00 表示发送成功) 

(2) 连续读取直到读到 0xFE 

(3) 读一个 BLOCK 

(4) 读 2 字节 CRC 

(5) 重复(2)-(4) 

(6) 发送 CMD12 停止 

SD 卡单块写 

(1) 发送 CMD24(收到 0x00 表示发送成功) 

(2) 发送若干时钟 

(3) 发送开始标志 0xFE 

(4) 发送一个 BLOCK 

(5) 发送两个字节的 CRC 

(6) 连续读直到 xxx0_0101 表示写入成功 

(7) 连续读忙检测,直到 0xFF 完成 

SD 卡单块写 

(1) 发送 CMD25(收到 0x00 表示发送成功) 

(2) 发送若干时钟 

(3) 发送开始标志 0xFC 

(4) 发送一个 BLOCK 

(5) 发送两个字节的 CRC 

(6) 连续读直到 xxx0_0101 表示写入成功 

(7) 重复(2)-(6) 

(8) 发送 0xFD 停止写操作 

(9) 连续读忙检测,直到 0xFF 完成


4.搭建QSYS系统

选择NIOS2处理器,这里选择快速型的32位处理器。

NIOS2随笔——SD卡之SPI操作_SD卡_02

添加内存、串口、GPIO模块,这里用GPIO模拟SPI接口。

NIOS2随笔——SD卡之SPI操作_nios_03


5. SPI驱动程序

用GPIO模拟SPI接口,设置SPI_CS、SPI_SCLK、SPI_MOSI为输出,SPI_MISO为输入。

SPI时序采用CPOL=1,CPHA=1(Format B),如下图:

NIOS2随笔——SD卡之SPI操作_SD卡_04

驱动代码如下:

#include <io.h>

#define u8  unsigned char
#define u32 unsigned int

#define SD_CS_SET  (IOWR(SPI_CS_BASE,0,1))
#define SD_CS_CLR  (IOWR(SPI_CS_BASE,0,0))

#define SD_SCLK_SET (IOWR(SPI_SCLK_BASE,0,1))
#define SD_SCLK_CLR (IOWR(SPI_SCLK_BASE,0,0))

#define SD_SDI_HIGH (IORD(SPI_MISO_BASE,0)==1)
#define SD_SDI_LOW  (IORD(SPI_MISO_BASE,0)==0)

#define SD_SDO_SET (IOWR(SPI_MOSI_BASE,0,1))
#define SD_SDO_CLR (IOWR(SPI_MOSI_BASE,0,0))


u8  SD_Type=0;
u32 cyc=64;

//SD卡初始化的时候,需要低速
void SD_SPI_SpeedLow(void){
	cyc = 512;
}
//SD卡正常工作的时候,可以高速了
void SD_SPI_SpeedHigh(void){
 	cyc = 8;
}

void delay(void){
	u32 i;
	for(i=0;i<cyc;i++);
}

u8 SpiRead(void){
    u8 i;
    u8 data=0;

    SD_SCLK_SET;
    //read data
    for(i=0;i<8;i++){
    	SD_SCLK_CLR;
    	delay();
    	SD_SCLK_SET;
    	if(SD_SDI_HIGH)
    		data = data | 0x0001;
    	delay();
    	if(i<7)
    		data = data << 0x0001;
    }
    SD_SCLK_SET;
    return data;

}

u8 SpiWrite(u8 data){
    u8 i;

    SD_SCLK_SET;
    //send data
    for(i=0;i<8;i++){
    	SD_SCLK_CLR;
    	IOWR(SPI_MOSI_BASE,0,(data<<i)>>7);
    	delay();
    	SD_SCLK_SET;
    	delay();
    }
    SD_SCLK_SET;
    return 0;
}


6. SD初始化与读写程序

//取消选择,释放SPI总线
void SD_DisSelect(void)
{
	SD_CS_SET;
	SpiWrite(0xff);//提供额外的8个时钟
}
//选择sd卡,并且等待卡准备OK
//返回值:0,成功;1,失败;
u8 SD_Select(void)
{
	SD_CS_CLR;
	if(SD_WaitReady()==0)return 0;//等待成功
	SD_DisSelect();
	return 1;//等待失败
}
//等待卡准备好
//返回值:0,准备好了;其他,错误代码
u8 SD_WaitReady(void)
{
	u32 t=0;
	do
	{
		if(SpiRead()==0XFF)return 0;//OK
		t++;
	}while(t<0XFFFFFF);//等待
	return 1;
}
//等待SD卡回应
//Response:要得到的回应值
//返回值:0,成功得到了该回应值
//    其他,得到回应值失败
u8 SD_GetResponse(u8 Response)
{
	u16 Count=0xFFFF;//等待次数
	while ((SpiRead()!=Response)&&Count)
		Count--;//等待得到准确的回应
	if (Count==0)
		return MSD_RESPONSE_FAILURE;//得到回应失败
	else
		return MSD_RESPONSE_NO_ERROR;//正确回应
}
//从sd卡读取一个数据包的内容
//buf:数据缓存区
//len:要读取的数据长度.
//返回值:0,成功;其他,失败;
u8 SD_RecvData(u8*buf,u16 len)
{
	if(SD_GetResponse(0xFE))return 1;//等待SD卡发回数据起始令牌0xFE
    while(len--)//开始接收数据
    {
        *buf=SpiRead();
        buf++;
    }
    //下面是2个伪CRC(dummy CRC)
    SpiWrite(0xFF);
    SpiWrite(0xFF);
    return 0;//读取成功
}
//向sd卡写入一个数据包的内容 512字节
//buf:数据缓存区
//cmd:指令
//返回值:0,成功;其他,失败;
u8 SD_SendBlock(u8*buf,u8 cmd)
{
	u16 t;
	if(SD_WaitReady())return 1;//等待准备失效
	SpiWrite(cmd);
	if(cmd!=0XFD)//不是结束指令
	{
		for(t=0;t<512;t++)
			SpiWrite(buf[t]);//提高速度,减少函数传参时间
		SpiWrite(0xFF);//忽略crc
		SpiWrite(0xFF);
		t=SpiRead();//接收响应
		if((t&0x1F)!=0x05)return 2;//响应错误
	}
    return 0;//写入成功
}
//向SD卡发送一个命令
//输入: u8 cmd   命令
//      u32 arg  命令参数
//      u8 crc   crc校验值
//返回值:SD卡返回的响应
u8 SD_SendCmd(u8 cmd, u32 arg, u8 crc)
{
    u8 r1;
	u8 Retry=0;
	SD_DisSelect();//取消上次片选
	if(SD_Select())return 0XFF;//片选失效
	//发送
	SpiWrite(cmd | 0x40);//分别写入命令
	SpiWrite(arg >> 24);
	SpiWrite(arg >> 16);
	SpiWrite(arg >> 8);
	SpiWrite(arg);
	SpiWrite(crc);
	if(cmd==CMD12)SpiWrite(0xff);//Skip a stuff byte when stop reading
    //等待响应,或超时退出
	Retry=0X1F;
	do
	{
		r1=SpiRead();
	}while((r1&0X80) && Retry--);
	//返回状态值
    return r1;
}
//获取SD卡的CID信息,包括制造商信息
//输入: u8 *cid_data(存放CID的内存,至少16Byte)
//返回值:0:NO_ERR
//		 1:错误
u8 SD_GetCID(u8 *cid_data)
{
    u8 r1;
    //发CMD10命令,读CID
    r1=SD_SendCmd(CMD10,0,0x01);
    if(r1==0x00)
	{
		r1=SD_RecvData(cid_data,16);//接收16个字节的数据
    }
	SD_DisSelect();//取消片选
	if(r1)
		return 1;
	else
		return 0;
}
//获取SD卡的CSD信息,包括容量和速度信息
//输入:u8 *cid_data(存放CID的内存,至少16Byte)
//返回值:0:NO_ERR
//		 1:错误
u8 SD_GetCSD(u8 *csd_data)
{
    u8 r1;
    r1=SD_SendCmd(CMD9,0,0x01);//发CMD9命令,读CSD
    if(r1==0)
	{
    	r1=SD_RecvData(csd_data, 16);//接收16个字节的数据
    }
	SD_DisSelect();//取消片选
	if(r1)
		return 1;
	else
		return 0;
}
//获取SD卡的总扇区数(扇区数)
//返回值:0: 取容量出错
//       其他:SD卡的容量(扇区数/512字节)
//每扇区的字节数必为512,因为如果不是512,则初始化不能通过.
u32 SD_GetSectorCount(void)
{
    u8 csd[16];
    u32 Capacity;
    u8 n;
	u16 csize;
	//取CSD信息,如果期间出错,返回0
    if(SD_GetCSD(csd)!=0) return 0;
    //如果为SDHC卡,按照下面方式计算
    if((csd[0]&0xC0)==0x40)	 //V2.00的卡
    {
		csize = csd[9] + ((u16)csd[8] << 8) + 1;
		Capacity = (u32)csize << 10;//得到扇区数
    }else//V1.XX的卡
    {
		n = (csd[5] & 15) + ((csd[10] & 128) >> 7) + ((csd[9] & 3) << 1) + 2;
		csize = (csd[8] >> 6) + ((u16)csd[7] << 2) + ((u16)(csd[6] & 3) << 10) + 1;
		Capacity= (u32)csize << (n - 9);//得到扇区数
    }
    return Capacity;
}
//初始化SD卡
u8 SD_Initialize(void)
{
    u8 r1;      // 存放SD卡的返回值
    u16 retry;  // 用来进行超时计数
    u8 buf[4];
	u16 i;

    SD_SDO_SET;
    SD_SCLK_SET;
    SD_CS_SET;

 	SD_SPI_SpeedLow();	//设置到低速模式
 	for(i=0;i<10;i++)
 		SpiWrite(0xFF);//发送最少74个脉冲

	retry=20;
	do
	{
		r1=SD_SendCmd(CMD0,0,0x95);//进入IDLE状态
	}while((r1!=0x01) && retry--);

	//log


 	SD_Type=0;//默认无卡

	if(r1==0X01)
	{
		if(SD_SendCmd(CMD8,0x1AA,0x87)==1)//SD V2.0
		{
			for(i=0;i<4;i++)
			{
				buf[i]=SpiRead();	//Get trailing return value of R7 resp
			}
			if(buf[2]==0X01&&buf[3]==0XAA)//2.7~3.6V
			{
				retry=0XFFFE;
				do
				{
					SD_SendCmd(CMD55,0,0X01);	//
					r1=SD_SendCmd(CMD41,0x40000000,0X01);//
				}while(r1&&retry--);
				if(retry&&SD_SendCmd(CMD58,0,0X01)==0)//
				{
					for(i=0;i<4;i++)
						buf[i]=SpiRead();//
					if(buf[0]&0x40)
						SD_Type=SD_TYPE_V2HC;    //
					else
						SD_Type=SD_TYPE_V2;
				}
			}
		}
		else//SD V1.x/ MMC	V3
		{
			SD_SendCmd(CMD55,0,0X01);		//发送CMD55
			r1=SD_SendCmd(CMD41,0,0X01);	//发送CMD41
			if(r1<=1)
			{
				SD_Type=SD_TYPE_V1;
				retry=0XFFFE;
				do //等待退出IDLE模式
				{
					SD_SendCmd(CMD55,0,0X01);	//发送CMD55
					r1=SD_SendCmd(CMD41,0,0X01);//发送CMD41
				}while(r1&&retry--);
			}else
			{
				SD_Type=SD_TYPE_MMC;//MMC V3
				retry=0XFFFE;
				do //等待退出IDLE模式
				{
					r1=SD_SendCmd(CMD1,0,0X01);//发送CMD1
				}while(r1&&retry--);
			}
			if(retry==0||SD_SendCmd(CMD16,512,0X01)!=0)
				SD_Type=SD_TYPE_ERR;//错误的卡
		}
	}

	SD_DisSelect();//取消片选
	SD_SPI_SpeedHigh();//高速

	if(SD_Type)
		return 0;
	else if(r1)
		return r1;
	return 0xaa;//其他错误
}
//读SD卡
//buf:数据缓存区
//sector:扇区
//cnt:扇区数
//返回值:0,ok;其他,失败.
u8 SD_ReadDisk(u8*buf,u32 sector,u8 cnt)
{
	u8 r1;
	if(SD_Type!=SD_TYPE_V2HC)sector <<= 9;//转换为字节地址
	if(cnt==1)
	{
		r1=SD_SendCmd(CMD17,sector,0X01);//读命令
		if(r1==0)//指令发送成功
		{
			r1=SD_RecvData(buf,512);//接收512个字节
		}
	}else
	{
		r1=SD_SendCmd(CMD18,sector,0X01);//连续读命令
		do
		{
			r1=SD_RecvData(buf,512);//接收512个字节
			buf+=512;
		}while(--cnt && r1==0);
		SD_SendCmd(CMD12,0,0X01);	//发送停止命令
	}
	SD_DisSelect();//取消片选
	return r1;//
}
//写SD卡
//buf:数据缓存区
//sector:起始扇区
//cnt:扇区数
//返回值:0,ok;其他,失败.
u8 SD_WriteDisk(u8*buf,u32 sector,u8 cnt)
{
	u8 r1;
	if(SD_Type!=SD_TYPE_V2HC)sector *= 512;//转换为字节地址
	if(cnt==1)
	{
		r1=SD_SendCmd(CMD24,sector,0X01);//读命令
		if(r1==0)//指令发送成功
		{
			r1=SD_SendBlock(buf,0xFE);//写512个字节
		}
	}else
	{
		if(SD_Type!=SD_TYPE_MMC)
		{
			SD_SendCmd(CMD55,0,0X01);
			SD_SendCmd(CMD23,cnt,0X01);//发送指令
		}
 		r1=SD_SendCmd(CMD25,sector,0X01);//连续读命令
		if(r1==0)
		{
			do
			{
				r1=SD_SendBlock(buf,0xFC);//接收512个字节
				buf+=512;
			}while(--cnt && r1==0);
			r1=SD_SendBlock(0,0xFD);//接收512个字节
		}
	}
	SD_DisSelect();//取消片选
	return r1;//
}


7. 测试结果

写入512个循环递增的字节,并将其读出打印在串口终端:

NIOS2随笔——SD卡之SPI操作_nios_05