【ESP-IDF】ESP32S3用SPI读写 MicroSD/TF卡(二)读写正文数据

        上一篇讲了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:读和写的token

        图1:data response token是一个专用于写数据过程的反馈结果,返回结果0x05表示成功写入,其他结果出现的可能性较低,毕竟SPI模式不用验证CRC。如果地址写错,会在R1就反馈了,不用在这反馈(看下图2)。start block token则是数据正文的起始标志,写和读数据过程都需要。

二、写时序(SPI模式)

        本文读和写都是单个block,512bytes,暂时不做多block连续读写。

        SDSC卡(2GB以下)可以定义一个block是多少字节,其他容量的卡都规定了一个block是512 bytes。我们就不搞特殊,都是读写单个block,512字节。即使我们仅想写入4个"aaaa",也要将aaaa硬编入一个512字节数组中再整个数组写到SD卡上。每次写入的地址一定是一个block的起始地址,写入的数据长度一定是512bytes,要求block对齐。我这儿讲的都是纯SD卡协议,不是FATFS的要求。

图2:数据写时序

        经过大量实践,写数据应该按照下表顺序操作:

写时序
拉低CS1字节1字节1字节1字节514字节1字节6字节拉高CS
MOSICMD24dummydummy0xFE512字节data正文+2字节CRCdummydummy
MISO--R1--0x055个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卡的数据储存区了。

图3:Winhex查看SD卡写入的结果

        我写512个0x61到SD卡第6簇,0x61表示a。关于怎样确认写指令的地址参数问题,后面会重点讲。先剧透,虽然内容写进去了,但用电脑读SD卡时发现里面为空,看不到任何文档存在,这时就知道FATFS文件系统的作用了,因为还需要修改FAT表和根目录才能在FATFS系统显示一个完整的文档。电脑用的FAT32系统会去默认地方找目录,找不到目录的话,哪怕内容存在,也会报告文档不存在。

三、读时序

        刚才写进去的内容,我们现在就可读出来,看是否写正确了。

        读比较简单。        

图4:数据读时序

       

读时序
CS拉低1字节1字节1字节10字节514字节CS拉高
MOSICMD17dummydummydummydummy
MISO--R10xFE512字节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:电脑打开的hello.txt文档

       图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卡认的地址不是字节地址,而是簇的号码,这个留待下篇文章再讲。

(二)winhex软件和FATFS文件系统

       FATFS文件系统就是装载在0x00000000的SD卡地址上,FATFS文件系统默认从0地址开始读文件系统相关的文件,包括目录、文件大小、文件名、文件内容映射的地址等放在SD卡最前面的地址中,以此来组织数据的文件化。官方库中mount()这一步就是要根据我上面的SD卡写协议方法将FATFS文件系统从0x000000000卡地址开始顺序烧写进去,烧写进去后,就不用那么原始朴素的手法来烧写数据内容了,可以通过fopen()之类的函数让FATFS根据地址映射来写或读数据正文了。

        我这小容量卡是FAT16系统。

        实际上,你可以不装FATFS系统在SD卡,装别系统的也行,只要这套系统自己能自洽找到数据地址映射关系就行。SD卡就很单纯提供数据储存的地址,它不懂什么是文件系统,FATFS系统本身也只是存储在卡上的数据而已。

图6:Winhex查看的SD卡全局储存情况

        图6展示SD卡全部储存情况,我的卡是小容量卡。第一个用户文件hello.txt开始的地址0x41E800(这儿表示物理地址)就是数据正文的开始存储地址,在这之前的地址都是FATFS文件系统安装的地方。

图7:Winhex查看的根目录

        找到文件根目录地址和数据正文地址,并且知道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模式比较简单的。

(四)对官方FATFS库的理解

    

图8:ESP-IDF自带的FATFS官方组件代码

        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;
}

        望拍砖、提问、赐教。

  • 10
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值