stm32驱动SDHC卡 (SPI方式) 二
0赞前面说了关于SDHC卡的一些基础知识。现在就开始进行驱动我们的SDHC卡了。有了前面一章的两个函数的基础,来构建其他的函数。
首先是复位。复位是很重要的一个操作,复位如果不成功的话,那SD卡是不能使用的。所以首先要进行复位操作。复位操作还是相对有点麻烦的。
先来个流程图。
这一看,吓尿了。。。。也太复杂了吧。。其实很简单的。。。听我一一道来。
因为我们用的是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卡进行操作了。