先来个流程图。


这一看,吓尿了。。。。也太复杂了吧。。其实很简单的。。。听我一一道来。
因为我们用的是SDHC卡,标准是SD.0。所以左边的部分都不用管。因为那是SD1.0标准的卡。
1、首先上电后。我们需要发送至少74个时钟。这个时钟是用来唤醒SD卡的。也就是告诉SD卡,嘿,赶快起来给我工作了。
2、唤醒SD卡后,就要发送CMD0命令,对SD卡进行初始化,SD的使用是必须要进行初始化的。否则不能使用。CMD0的响应式0x01.如果接收到0x01响应后。就说明发送CMD0命令成功。
3、接收到CMD0的0x01响应后。然后发送CMD8。这个命令是SD2.0标准加的。用来鉴别SD卡能否在目前的电压下工作。
CMD8的参数是0x1aa,crc是0x87.
CMD8的响应也是0x01. 0x01响应后,可以读取4个字节数据,该数据由对电压有说明。因为SD卡的工作电压我们是已经知道的,所以这里对读取的4个字节数据直接忽略。当然也可以进行处理。
4、发送完CMD8后。就要发送CMD55.CMD55是向SD卡说明,下一个发送的命令是特殊命令。这个要注意,图上没有说明。然后接着发送ACMD41,ACMD41是特殊命令,用来初始化SD卡。如果返回响应是0x00,就说明初始化成功,否则初始化失败。ACMD41的参数是0x40000000,CRC无所谓。至此SD卡的初始化就结束了。
这样看来,初始化其实也很简单的,就是发命令,读响应,再发命令。。。一直到所有命令发完,就初始化完了。
这里初始化完后,还可以发送命令CMD58,读取OCR内部寄存器的值。
下面是代码:
+ 查看代码
uint8_t SD_reset(void)
{
uint8_t i;
uint8_t response;
uint8_t buf[4];
uint16_t retry;
//SD卡复位信号有效,说明现在clk时钟不能太快
sd_reset_flag=1;
//发送160个clk时钟
for(i=0; i<20; i++)
spi_write_byte(0xff);
//发送命令0,发送100次,如果接收不到响应,就说明发送0命令失败
for(i=0;; i++)
{
response = SD_write_command(0,0x00000000,0x95);
if(response == 0x01)
break;
if(i == 100)
{
SPI_CS_HIGH(); //无效片选
return CMD0_ERROR;
}
}
printf("CMD0命令发送成功\n");
//发送CMD8命令 ,回应为0x01
response = SD_write_command(8,0x1aa,0x87);
if(response == 0x01)
{
printf("CMD8命令发送成功\n");
for(retry=0; retry<4; retry++)
buf[retry] = spi_write_byte(0xff);
printf("CMD8命令读取的值为:\n");
printf("%x %x %x %x\n",buf[0],buf[1],buf[2],buf[3]);
if(buf[2]==0X01&&buf[3]==0XAA)//卡是否支持2.7~3.6V
{
retry=0XFFFE;
//发送CMD55和ACMD41
//CMD55响应为0x01
//ACMD41响应为0x00
do
{
SD_write_command(55,0,0X01); //发送CMD55
response=SD_write_command(41,0x40000000,0X01);//发送CMD41
}while(response&&retry--);
if(retry != 0)
printf("CMD41命令成功,初始化成功\n");
else
return CMD41_ERROR;
if(retry&&SD_write_command(58,0,0X01)==0)//鉴别SD2.0卡版本开始
{
for(i=0;i<4;i++)buf[i]=spi_write_byte(0XFF);//得到OCR值
printf("CMD58命令读取的值为:\n");
printf("%x %x %x %x\n",buf[0],buf[1],buf[2],buf[3]);
}
}
}
else
return CMD8_ERROR;
SPI_CS_HIGH(); //取消片选
spi_write_byte(0xff); //片选无效后,要发送8个时钟
sd_reset_flag=0; //复位结束,SPI时钟加快
return RESET_SUCCESS;
}
代码也比较简单。按照这个流程下来,初始化应该是没有问题的。初始化后,就可以进行SD卡的数据读取和数据写入了。
首先是数据读取。
时序图:

这个是读取一个扇区的数据。即一次读取一个扇区的512字节数据。后面会说道读取多个扇区的数据。
时序图也比较清楚,首先拉低CS。发送命令17,然后等待响应0x00.响应后,又在等待读取数据起始令牌0xfe。然后就可以读取512字节数据了。读取后,还要在读取两个字节的CRC,不过这里不关心这数据是什么。然后拉高片选即可。这样就完成了读扇区数据。
有时候,我们是需要读取多个连续扇区数据的,那怎么做了。
首先还是上图。

读取连续扇区的命令式18.和17命令一样的时序,只不过是读取完两字节的CRC后,不要拉高CS。而是等待读取数据起始令牌0xfe。等到后,又继续读取数据。这样就实现了连续读取了。那如果我不想读了怎么办,只要你读完数据后,发送命令12,就停止读取数据了。
这里CMD17和CMD18的参数是扇区地址,CRC无所谓。
代码如下:
+ 查看代码
//从SD的一个扇区读取数据
//输入参数1 读取扇区地址
//输入参数2 存储读取的512个数据的数组
//输入参数3 读取扇区的个数
//返回值是读取成功还是不成功
uint8_t SD_read_sector_data(uint32_t sector, uint8_t data[], uint32_t count)
{
uint16_t i;
uint32_t j;
uint32_t add=0;
SPI_CS_HIGH(); //无效上一次片选
spi_write_byte(0xff);
if(count == 1)
{
//发送17命令,即读一个扇区命令
//正常响应应该为0x00
if(SD_write_command(17,sector,0xff))
{
SPI_CS_HIGH(); //无效片选
return READ_SECTOR_DATA_ERROR;
}
}
else
{
//发送18命令,即读多个扇区命令
//正常响应应该为0x00
if(SD_write_command(18,sector,0xff))
{
SPI_CS_HIGH(); //无效片选
return READ_SECTOR_DATA_ERROR;
}
}
for(j=1; ; j++)
{
//等待读响应令牌
while(spi_write_byte(0xff) != 0xfe);
for(i=0; i<512; i++)
{
data[add+i] = spi_write_byte(0xff);
}
//读取CRC码
spi_write_byte(0xff);
spi_write_byte(0xff);
if(j == count )
{
//发送数据停止读命令
if(count != 1) SD_write_command(12,0,0xff);
break;
}
else
add += 512;
}
SPI_CS_HIGH(); //无效片选
//片选无效之后,在发送8个时钟
spi_write_byte(0xff);
return READ_SECTOR_SUCCESS;
}
代码也比较简单,首先判断是读取单个扇区还是多个扇区,单个扇区就发送CMD17,多个扇区就发送CMD18。如果是读取多个扇区,那么读完后,要发送CMD12.
然后就是数据写了:
首先还是上时序图:

和读取数据时序差不多。首先是写单个扇区。
首先拉低CS,然后发送命令24,即写单个扇区。然后等待响应信号,如果响应为0x00说明命令发送成功。然后发送数据写入起始令牌0xfe。SD卡认为0xfe是写数据的起始令牌,即这个数据后,后面的数据才是真正写入的数据。然后在发送512字节的数据,最后在写两个CRC,这个CRC写什么都可以。然后读取响应,这里第一个响应的最后5位为00101.接收到这个响应后,就等待接收的数据不是0x00,因为0x00表示SD卡正在处理写入的数据。接收到的数据不是0x00后,就拉高CS。就完成了一个扇区的数据写入。
当然,我们是可以写多个扇区的。那多个扇区是怎么处理的。先上图:

对于写多个扇区,命令是25.和写单个扇区时序差不多,不过不一样的地方是,写多个扇区的起始令牌不是0xfe了,而是0xfc。这里要注意。另外当最后读取的数据不是0x00后,不要拉高CS,而是继续写数据。这样就实现了多个扇区的数据写入。数据写完后,只要拉高CS就可以了,不用发送其他命令。
CMD24和CMD25的参数都是扇区的地址。对于CMD24,这扇区地址就是写入数据的扇区地址,而对于CM25,扇区地址就是写入扇区的首地址。
代码如下:
+ 查看代码
//向SD中的一个扇区写数据
//输入参数1 写入扇区地址
//输入参数2 存储512个写入数据的数组
//输入参数3 写入扇区的个数
//返回值是写入成功还是不成功
uint8_t SD_write_sector_data(uint32_t sector ,const uint8_t data[], uint32_t count)
{
uint16_t i;
uint8_t response;
uint32_t j;
uint32_t add=0;
SPI_CS_HIGH(); //无效上一次片选
spi_write_byte(0xff);
if(count == 1) //单个扇区数据写入
{
//发送24命令,即写扇区命令
//正常响应应该为0x00
if(SD_write_command(24,sector,0xff))
{
SPI_CS_HIGH(); //无效片选
return WRITE_SECTOR_DATA_ERROR;
}
spi_write_byte(0xff);
//写数据起始令牌
spi_write_byte(0xfe);//发送数据块标志0xfe
for(i=0; i<512; i++)
{
spi_write_byte(data[add + i]);
}
//发送CRC码
spi_write_byte(0xff);
spi_write_byte(0xff);
for(i=0; ; i++)
{
response = spi_write_byte(0xff);
if((response & 0x0f) == 0x05)
break;
if(i==100)
{
SPI_CS_HIGH(); //无效片选
return WRITE_SECTOR_DATA_ERROR;
}
}
}
else //多个扇区数据写入
{
//发送25命令,即写多个扇区命令
//正常响应应该为0x00
if(SD_write_command(25,sector,0xff))
{
SPI_CS_HIGH(); //无效片选
return WRITE_SECTOR_DATA_ERROR;
}
for(j=1; ; j++)
{
spi_write_byte(0xff);
//写数据起始令牌
spi_write_byte(0xfc);//发送数据块标志0xfc
for(i=0; i<512; i++)
{
spi_write_byte(data[add + i]);
}
//发送CRC码
spi_write_byte(0xff);
spi_write_byte(0xff);
for(i=0; ; i++)
{
response = spi_write_byte(0xff);
if((response & 0x0f) == 0x05)
break;
if(i==100)
{
SPI_CS_HIGH(); //无效片选
return WRITE_SECTOR_DATA_ERROR;
}
}
while(spi_write_byte(0xff) != 0xff);
add += 512;
if(j == count)
{
spi_write_byte(0xFD);
break;
}
}
}
SPI_CS_HIGH(); //无效片选
spi_write_byte(0xff);
return WRITE_SECTOR_SUCCESS;
}
对写单个扇区和多个扇区分开进行处理。也是比较容易的。
这样,通过上面两个函数,就能实现对SD卡进行读写了。不过还有一个命令也是有用的,就是擦除命令。
SD卡支持扇区擦除。能一次擦除多个扇区。

可以看出,擦除的话,需要发送3个命令,CMD32,CMD33,CMD38。
代码如下:
+ 查看代码
//输入参数1 擦除的起始扇区地址
//输入参数2 擦除的结束扇区地址
//返回值 擦除扇区是否成功
uint8_t SD_erase_sector(uint32_t sector_start, uint32_t sector_stop)
{
SPI_CS_HIGH(); //无效上一次片选
spi_write_byte(0xff);
//发送命令32,设置擦除扇区的起始地址
//返回应该为00
if(SD_write_command(32,sector_start,0xff))
{
SPI_CS_HIGH(); //无效片选
return ERASE_SECTOR_ERROR;
}
//发送命令33,设置擦除扇区的终止地址
//返回应该为00
if(SD_write_command(33,sector_stop,0xff))
{
SPI_CS_HIGH(); //无效片选
return ERASE_SECTOR_ERROR;
}
//发送命令38,擦除所选扇区
//返回应该为00
if(SD_write_command(38,0,0xff))
{
SPI_CS_HIGH(); //无效片选
return ERASE_SECTOR_ERROR;
}
SPI_CS_HIGH(); //无效片选
return ERASE_SECTOR_SUCCESS;
}
代码也比较容易了,就发送三个命令就行了。
这样,就完成了整个SD卡的驱动了。对于外部使用,首先调用初始化函数,对SD卡进行初始化,然后就可以调用读,写,擦除函数对SD卡进行操作了。