赞
踩
上一篇讲了SPI设置和SD卡初始化,这篇讲SD卡数据内容的读和写,也是在物理驱动层这一层。我的细节应该是足的了,希望能帮到大家,有任何疑问欢迎留言。
SPI读写SD卡的系列文章:
【ESP-IDF】ESP32S3用SPI读写 MicroSD/TF卡(一)SD卡初始化_esp sd spi-CSDN博客
【ESP-IDF】ESP32S3用SPI读写 MicroSD/TF卡(三)移植FATFS文件系统-CSDN博客
SD卡官方手册实在.....太不科学了,像你画我猜游戏,一副大概的图让你去猜中间要填补什么信息。先来吐槽一下官方手册的含糊地方:
读数据过程,command和response之间要等多少个dummy?response与data block之间要等多少个dummy?实际上每个data block(512字节的)前面会有个start block token,写时序的图展示出来了,但读时序的图漏画呢!写时序的data response后面还有busy,什么东西?这些官方手册都含糊带过。而CRC,明明在SPI模式是不用验证的,官方手册却浓墨重彩写。无力~~我这篇将填这些坑。
本篇要用到的指令和参数如下:
- #define CMD17 0x5100000000FF //读1个block 512bytes
- #define CMD18 0x5200000000FF //读多个blocks
- #define CMD24 0x5800000000FF //写1block
- #define CMD25 0x5900000000FF //写多blocks
- #define CMD13 0x4D00000000FF //写存储后查看卡status
要注意读和写指令中间的4字节0要替换为32bits SD卡储存地址,关于这个地址不同容量的卡会有不同的定义,很容易踩坑。这篇文章用的是SDSC卡,地址值就是byte address,1字节占一个地址值。
要用到的response有:
(1)R1:响应CMD17、CMD18,响应结果0x00表示读\写指令正确接收;
(2)R1b:这个官方手册没讲,只是写和擦除这两个指令需要。使用CMD24、CMD25写指令过程中,data_response_token(看下图2)后面还要接收来自SD卡的忙状态信息,这个信息就是R1b,1字节长度。只有两种结果,0x00表示SD卡在忙着烧写,0xFF则表示忙完了。
(3)R2:CMD13命令的response,不是刚需。写完数据后,除了用上面的token判断写入是否成功、sd卡忙不忙外,最准确应该是另外发CMD13命令,响应为R2,2字节。第1字节跟R1一样,第2字节为判断卡片的status,若R2返回0x0000,表示成功传输,卡片不忙,也没有写入错误发生。
图1:data response token是一个专用于写数据过程的反馈结果,返回结果0x05表示成功写入,其他结果出现的可能性较低,毕竟SPI模式不用验证CRC。如果地址写错,会在R1就反馈了,不用在这反馈(看下图2)。start block token则是数据正文的起始标志,写和读数据过程都需要。
本文读和写都是单个block,512bytes,暂时不做多block连续读写。
SDSC卡(2GB以下)可以定义一个block是多少字节,其他容量的卡都规定了一个block是512 bytes。我们就不搞特殊,都是读写单个block,512字节。即使我们仅想写入4个"aaaa",也要将aaaa硬编入一个512字节数组中再整个数组写到SD卡上。每次写入的地址一定是一个block的起始地址,写入的数据长度一定是512bytes,要求block对齐。我这儿讲的都是纯SD卡协议,不是FATFS的要求。
经过大量实践,写数据应该按照下表顺序操作:
拉低CS | 1字节 | 1字节 | 1字节 | 1字节 | 514字节 | 1字节 | 6字节 | 拉高CS | |
MOSI | CMD24 | dummy | dummy | 0xFE | 512字节data正文+2字节CRC | dummy | dummy | ||
MISO | - | - | R1 | - | - | 0x05 | 5个0x00,1个0xFF |
看图2,在主机MISO线接收到R1 response后,主机MOSI线要紧接着发0xFE token表示开始写正文了,中间不能有空隙,图2也很细节表示这个位置是没缝隙的。
发完0xFE token后也不能留缝隙要紧接着发512byte正文数据+2byteCRC。
看图2,data block之后跟data_response之间貌似有很长的间隙,而我这边实践情况是没有缝隙,SD卡马上就响应data token=0x05了,表示data accepted了,没有地址错误或者CRC错误。地址错误我会在后面讲。CRC是不会错误的,因为SPI模式不检查CRC。收到0x05这个data_response后,看图2,后面应该还要继续轮询SD卡是否忙完。官方手册说应该不断发dummy去查询卡片是否完成了烧写的programming过程,我发到第6个dummy就成功了,前5个都是返回0x00表示忙,第6个返回0xFF表示烧写完成。写数据的操作结束!
注意表格标红的这个6字节dummy不是固定的,要看你SD卡轮询多少次。
监视器结果如下:
- 2024-06-19 15:19:29 CMD24开始写。
- 2024-06-19 15:19:29 CMD24 has sent.response is:0x00
- 2024-06-19 15:19:29 token wait1.Token value:05
- 2024-06-19 15:19:29 data token correct.
- 2024-06-19 15:19:29 token wait2.Token value:00
- 2024-06-19 15:19:29 token wait3.Token value:00
- 2024-06-19 15:19:29 token wait4.Token value:00
- 2024-06-19 15:19:29 token wait5.Token value:00
- 2024-06-19 15:19:29 token wait6.Token value:00
- 2024-06-19 15:19:29 token wait7.Token value:FF
- 2024-06-19 15:19:29 card status R2 is:
- 2024-06-19 15:19:29 response is:0x00 00
- 2024-06-19 15:19:29 Write succeed!
完事后我还发了个CMD13查询SD卡状态,收到R2结果为0x00 00,没任何错误了,数据稳稳写入SD卡的数据储存区了。
我写512个0x61到SD卡第6簇,0x61表示a。关于怎样确认写指令的地址参数问题,后面会重点讲。先剧透,虽然内容写进去了,但用电脑读SD卡时发现里面为空,看不到任何文档存在,这时就知道FATFS文件系统的作用了,因为还需要修改FAT表和根目录才能在FATFS系统显示一个完整的文档。电脑用的FAT32系统会去默认地方找目录,找不到目录的话,哪怕内容存在,也会报告文档不存在。
刚才写进去的内容,我们现在就可读出来,看是否写正确了。
读比较简单。
CS拉低 | 1字节 | 1字节 | 1字节 | 10字节 | 514字节 | CS拉高 | |
MOSI | CMD17 | dummy | dummy | dummy | dummy | ||
MISO | - | - | R1 | 0xFE | 512字节data正文+2字节CRC |
看图4,command与response中间隔着一个dummy间隙,这儿老生常谈了。收到response R1结果是0x00,表示SD卡明白了读指令。
response与data_block之间比较玄,不知隔着多少个dummy,要主机不断发dummy过去问SD卡是否准备好发出数据,直到SD卡发回来的token为0xFE,表示512bytes data block的起始标志。我实践情况是发10个dummy,这个你们自己看着来。
主机收到0xFE后,紧接着没有间隙开始接收514字节正文。收完后把CS拉高,读数据结束!
监视器结果如下:
- 2024-06-19 14:28:27 CMD17开始读。
- 2024-06-19 14:28:27 CMD17 has sent.response is:0x00
- 2024-06-19 14:28:27 wait tokenFF
- 2024-06-19 14:28:27 wait tokenFF
- 2024-06-19 14:28:27 wait tokenFF
- 2024-06-19 14:28:27 wait tokenFF
- 2024-06-19 14:28:27 wait tokenFF
- 2024-06-19 14:28:27 wait tokenFF
- 2024-06-19 14:28:27 wait tokenFF
- 2024-06-19 14:28:27 wait tokenFF
- 2024-06-19 14:28:27 wait tokenFF
- 2024-06-19 14:28:27 wait tokenFE
- 2024-06-19 14:28:27 read token correct.read data is:
- 2024-06-19 14:28:27 a b c d a a a a a a a a a a a a
- 2024-06-19 14:28:27 a a a a a a a a a a a a a a a a
- 2024-06-19 14:28:27 a a a a a a a a a a a ` a a a
图5, 再用电脑打开SD卡,看里面的文档内容,果真如监视器发回来的读数据结果一样。
写的过程主机MISO线要等SD卡3次回复,第一次是关于CMD24指令配套的R1;第二次是写完514字节data_block后等SD卡回复token 0x05 表示写入正确;第三次是等SD卡烧写编程结束,等候的结果从0x00忙状态切换到0xFF就完成了。
读的过程主机发完CMD17后,其实MOSI线一直发送dummy就是了,MISO线主机等SD卡两次回复,一次是R1,一次是token 0xFE表示数据正文起始标志。
物理地址就是SD卡硬件上真正的地址,哪怕没有文件系统存在物理地址也存在,SD卡是不懂逻辑地址的。逻辑地址就是经过文件系统软件或算法人为划分的地址,两者之间存在一个偏移offset。
SD卡只认物理地址。CMD17或CMD24的argument部分32bits共4字节,就是填入SD卡读或写的起始物理地址。SD卡一个地址能装8bit,地址值长度4bytes。写或读都是从该地址开始顺序往地址值增长的方向读写。
实例,我的CMD24完整指令是:0x580041E800FF,数据内容在SD卡0x0041E800的物理地址开始写入。
FAT32系统关于地址的情况会有不同,因为FAT32系统适配大容量SDHC以上的SD卡,SD卡认的地址不是字节地址,而是簇的号码,这个留待下篇文章再讲。
FATFS文件系统就是装载在0x00000000的SD卡地址上,FATFS文件系统默认从0地址开始读文件系统相关的文件,包括目录、文件大小、文件名、文件内容映射的地址等放在SD卡最前面的地址中,以此来组织数据的文件化。官方库中mount()这一步就是要根据我上面的SD卡写协议方法将FATFS文件系统从0x000000000卡地址开始顺序烧写进去,烧写进去后,就不用那么原始朴素的手法来烧写数据内容了,可以通过fopen()之类的函数让FATFS根据地址映射来写或读数据正文了。
我这小容量卡是FAT16系统。
实际上,你可以不装FATFS系统在SD卡,装别系统的也行,只要这套系统自己能自洽找到数据地址映射关系就行。SD卡就很单纯提供数据储存的地址,它不懂什么是文件系统,FATFS系统本身也只是存储在卡上的数据而已。
图6展示SD卡全部储存情况,我的卡是小容量卡。第一个用户文件hello.txt开始的地址0x41E800(这儿表示物理地址)就是数据正文的开始存储地址,在这之前的地址都是FATFS文件系统安装的地方。
找到文件根目录地址和数据正文地址,并且知道FATFS的文件组织方式,我们甚至不用FATFS的f_write()函数,用朴素的CMD24命令就能就在sd卡创建一个hello.txt文件,文件内容记录"abcdaaaaaaaaaaaaaaaaaaaaaa"内容了。
我这张SD卡,物理地址=逻辑地址+0x1E000。偏移就是0x1E000。你找到最起始地址0x00那儿,winhex会告诉你逻辑簇和物理簇的差值是多少。我这儿差值是240个簇,240*512=122880字节,换算成十六进制就是0x1E000。之所以要换算,SD卡只接受物理地址参数,而我们用winhex看的是逻辑地址,因此要换算。
综上,FATFS文件系统和hello.txt文档本质就是一个文档头+文档内容,文档头告诉我们地址值0x00400800~0x004009F0的地方存放着文档正文内容,然后将这个抽象的数据打包成具象的文档形式展示出来。有了FATFS系统我们不用自己管理数据的地址。
SD卡读和写都是按一簇为单位,就是一次必须写入512bytes,如果你只想写入"abcd"4个字节,不好意思,需要将abcd放入tx_buff[512 ]数组的0-3号位,剩余4-511号位放0x00。这很麻烦,而FATFS系统则帮我们完成这些违背直觉的事。如果tx_buff[512]没全部初始化,只写入了"abcd",则后面的数据内容会乱码。如果你只定义了tx_buff[4],数组只有4个项,用CMD24写入也会乱码。
FATFS文件的储存,有约定SD卡一个物理簇只能存放一个文件,这个簇哪怕只用了一半,剩余一半空间也不能用。另一个新文件必须从一个新簇开始。因此FATFS的寻址都是按簇为单位进行,不会按sector或字节。
网上许多文章以及官方库代码都看到一些多余操作,其实在SPI模式下,不用通过CMD3进入data transfer state,也不用设定RCA(相对偏移地址),初始化结束后可以直接发CMD24开始写数据内容,SPI模式比较简单的。
SD卡初始化不同格式化,SD卡初始化不会删数据,是一个主机跟SD卡搭建物理沟通的过程。
初始化代码看上一篇文章,我这儿只贴出读和写data的代码。这儿的tx_dummy是一个全局变量数组,tx_dummy[514]已初始化为0xFF。
- /*MOSI发0xFF,MISO接收一字节token*/
- uint8_t wait_datatoken()
- {
- spi_transaction_t transcnf={
- .flags=SPI_TRANS_USE_RXDATA,
- .tx_buffer=&tx_dummy,
- .length=8,
- };
- spi_device_polling_transmit(device_handle,&transcnf);
- return transcnf.rx_data[0];
- }
-
- /*读数据块,参数是SD卡读的地址*/
- esp_err_t read_card(uint32_t start_addr)
- {
- uint64_t cmd=CMD17; //读单个block
- cs_enable();
- write_cmd(cmd | ((uint64_t)start_addr <<8));
- printf("CMD17读命令的地址:0x%02X %02X %02X %02X\n",(uint8_t)(start_addr>>24),(uint8_t)(start_addr>>16),(uint8_t)(start_addr>>8),(uint8_t)start_addr);
- wait_response(R1); //0x00表示成功
- int cnn=20;
- uint8_t temp;
- //要等到token变为0xFE才成功,若为0x00则继续等待,若为0x01~0x0F则为error
- do{
- temp=wait_datatoken();
- printf("wait token:%02X\n",temp);
- if(temp==0xFE)
- {printf("read token correct.");
- break;}
- }while(cnn--);
- if(cnn==0)
- {
- printf("wait token timeout.");
- write_cmd(CMD12);
- send_dummy(1);
- cs_disable();
- return ESP_ERR_TIMEOUT;
- }
- //多出bytes,CRC2byte
- spi_transaction_t transcnf={
- .length=8*(512+2),
- .rx_buffer=buff,
- .tx_buffer=&tx_dummy,
- };
- spi_device_polling_transmit(device_handle,&transcnf);
- send_dummy(1);
- cs_disable();
- send_dummy(1);
- printf("read data is:\n ");
- cnn=31;
- for(uint16_t j=0;j<512;j++)
- {
- printf("%02X ",buff[j]);
- if (cnn-- == 0)
- {
- cnn = 31;
- printf("\n");
- }
- }
- printf("\n");
- return ESP_OK;
- }
-
- /*写数据块,*wr是要写的数据地址指针*/
- esp_err_t write_card(uint32_t start_addr, uint16_t block_num,uint8_t * wr)
- {
- uint64_t cmd=CMD24;
- cs_enable();
- write_cmd(cmd | ((uint64_t)start_addr <<8));
- printf("写数据CMD24 has sent.\n");
- wait_response(R1); //0x00表示成功
-
- uint8_t token=0xFE;
- spi_transaction_t transcnf={
- .length=8,
- .tx_buffer=&token,
- };
- spi_device_polling_transmit(device_handle,&transcnf);
- transcnf.length=8*(512+2);
- transcnf.tx_buffer=wr;
- spi_device_polling_transmit(device_handle,&transcnf);
-
- uint8_t cnn=0;
- uint8_t temp=0x01;
- do{
- temp=wait_datatoken();
- cnn++;
- printf("token wait%d.Token value:%02X\n",cnn,temp); //0xE5表示data accepted, 0x00表示忙线
- if(temp==0xFF) break;
- }while(cnn<20);
- if(cnn==20)
- {
- printf("wait token from card...timeout.");
- cs_disable();
-
- }
- cs_disable();
- send_dummy(1);
- printf("Write succeed!\n");
- return ESP_OK;
- }
望拍砖、提问、赐教。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。