如何使用外部储存、如何使用标准文件系统读写信息,是比较大的课题,我想用几篇文章的幅度详细介绍自己写驱动程序、移植文件系统库,在不依赖ESP官方库的情况下完成ESP32S3对SD卡的控制,此过程解剖了SD卡操作和文件系统操作的核心。
这是系列文章,链接如下:
【ESP-IDF】ESP32S3用SPI读写 MicroSD/TF卡(二)读写正文数据_spi读写sd卡驱动需要自己写吗-CSDN博客
【ESP-IDF】ESP32S3用SPI读写 MicroSD/TF卡(三)移植FATFS文件系统-CSDN博客
此篇先解决物理驱动层,就是如何用SPI协议操控SD卡的寄存器。重点与硬件和SD卡数据手册交涉。
我用 ESPIDF自带的sdspi库失败,失败定位在SD卡初始化过程,发送多次ACMD41仍无法接收SD卡0x00回复,超时(主程序返回0x107错误),初始化失败。于是自己写了SD卡SPI模式的通讯协议查看究竟。
一、硬件
SD卡、MicroSD、TF卡都是用一样的代码一样的协议。MMC卡的协议流程略有不同。SD卡里面还有细分为超大容量SDXC、大容量SDHC和小容量SDSC(2GB以下)三种,接线一样,代码上略有区别,而这些区别挺致命的。
一开始用图1的MicroSD卡模块,有弹簧自锁的,还有3.3v稳压器,电路设计比图二稳当,但到了ACMD41初始化时尝试5000次都总是失败,CMD0和CMD8倒是正常。换过其他sd卡试过,耗了我好多天后,终于下单换了图二的模块,简陋一点,但原代码测试ACMD41就通过了,R1返回0x00。
我用这种256MB的便宜小卡,属于SDSC。
SD卡结构分为控制寄存器和储存单元,我们的指令就是操作控制寄存器。
二、SD卡手册关于指令、参数、response的说明
2.1 指令和参数
#define pin_CS 9
#define pin_MOSI 10
#define pin_MISO 12
#define pin_CLK 11
/*CMD type,包含从命令到CRC共48bits*/
#define CMD0 0x400000000095
#define CMD1 0x4100000000FF
#define CMD8 0x48000001AA87
#define CMD55 0x770000000065
#define ACMD41 0x694000000077
#define CMD58 0x7A00000000FF
#define CMD10 0x4A00000000FF //接收CID信息
#define CMD12 0x4C00000000FF //中止读写
#define CMD17 0x5100000000FF //读1个block 512bytes
#define CMD18 0x5200000000FF //读多个blocks
#define CMD24 0x5800000000FF //写1block
#define CMD25 0x5900000000FF //写多blocks
#define CMD13 0x4D00000000FF //写存储后查看卡status,R2响应
#define CMD32 0x6000000000FF //erase start addr
#define CMD33 0x6100000000FF //erase end addr
#define CMD38 0x6600000000FF //erase all
/*response type, the figure indicates length in bytes*/
typedef enum _response_t{R1=1,R7=5,R2=17,R3=5}response_t;
以上就是SD卡常用操作指令,而本篇需要用到的指令到CMD58为止,就能完成SD卡物理初始化过程,指令按出场顺序排好了。
说一下指令的数值构成。CMD0的数值就是0,CMD8就是8,CMD10就是十进制的10。以CMD10为例,10转变成十六进制就是0x0A,加上0x40就是0x4A了。所有指令都是从0x40开始加上去,看CMD0就知道了。
ACMD41也是跟普通CMD命令一样计算。十进制的41就是十六进制的0x29,加到0x40上等于0x69。ACMD41的第三位可以是4也可以是0,小容量卡用0也可,大容量卡用4,其实无论你用什么都不影响SD卡回复你的消息。
注意主机发出的所有指令,SD卡都会在后面回复一个response。
2.2 CRC计算
从CMD0至ACMD41都要强调CRC计算准确,ACMD41之后的指令随意填个CRC就可,SD卡手册这样说的。因为SPI模式下,SD卡不检查CRC。但是ACMD41之前卡片都没完成初始化,SD卡不清楚主机采用哪种方式来沟通,因此要正确的CRC。
计算CRC的工具在这个网址:
command用CRC7的模式,计算工具使用过程如下:
我这是用CMD55、0x770000000065作为例子。输入7700000000,选择CRC7模型,按计算按钮,在计算结果Bin栏看到结果是7bits,自己在最右边补一个1就是完整8位,补上1后,就是0x65了,所以CMD55的命令CRC应该是65。末尾补1是SD卡协议的要求,命令传输结束时末尾置1。
2.3 SD卡手册
去SD卡联盟下载simplified版本的SD卡手册(我文章有提供下载,但可能被CSDN弄成付费资源)。几百页很繁琐,我提几个注意点。这个手册包含了几种传输协议:SDIO协议、SPI协议、UHS-II协议、PCIe协议,不要看花眼了,我一开始看了SDIO的协议,连寄存器和指令的操作都不太一样,response也不一样,浪费了时间。
SD卡版本的话,我们知道近10年的卡都是2.0版本之后的就行了。
SD卡分容量,SDSC是2GB以下,SDHC是大杯,SDXC是超大杯,SDUC是Ultra杯。
SD卡状态的话,主要分为idle、ready、busy这几种重要状态,CMD8命令之前的阶段response返回0x01(idle)表示通过,ACMD41阶段要返回0x00才表示通过,不是所有阶段都是0x01,要看SD卡进入到哪个状态来判断。
左图是SDIO模式的初始化指令ACMD41,右图是SPI模式的ACMD41指令。SPI环节都讲得很简略,可以看到右图ACMD41参数0至31bits大部分都是置0,而左图的参数包含了FB、XPC、OCR等要设置成有用的数值。
左图的Response是R3类型,48bits,右图的response类型只是R1,7bits。所以差别很大,不要看花眼啦。我们只看SPI有关的。
除此外,网上有很多文章提到的指令如CMD3用来切换到data transfer阶段,实际上是不能用在SPI模式的,张冠李戴。又比如CMD7对RCA(相对偏移地址)设置,SPI模式是不用设置这个的。看下图第2列,不是所有指令都适合在SPI模式使用的,第二列写NO的不适合SPI模式。
网上也有不少文章提到R1的格式是48bits,实际上SPI不需要校验CRC,SPI模式的R1只有8bits。看下图。
很多人文章都是复制粘贴过来,鱼龙混杂。
三、ESP-IDF的SPI如何配置
先列一下参数,我使用的是四线硬件SPI,一个时钟周期传输1bit,全双工,开DMA。SD卡的SPI接口也只有四线的形式,要更快的话可能要用SDIO传输协议?四线指CS、CLK、MOSI、MISO,更多线的SPI会体现在多几条数据传输线,可以一次传好几bit。
3.1 主机SPI接口
ESP32S3配备了4个SPI外设硬件,我们能用的只有SPI2和SPI3,选channel时记得在这两个channel里面选。要更快的话就选SPI2,能直接连接IOMUX,但要特定GPIO引脚;SPI3只能通过GPIO MATRIX传输,慢一点,但可随意指定GPIO引脚,其实速度也有20Mhz,对SD卡来讲足够快了。
3.2 SPI传输协议
传输格式为command+address+dummy+data四块,实际上对硬件来说无区别,都是发送0101这些bit,因此这四块都不是必选,我的话前三块都不用,只发送data,把SD卡的指令、参数、crc都编成一条data发出。区别就是在传输函数内自己写多几行简单代码处理一下data的先后顺序。
SPI传输很简单,把CS线拉低电平后就可以开始传输正文data内容,要结束的话拉高CS。如要接收8bits内容的话,主机也是做发送操作,发送内容为8个0xFF字节,就是8个dummy,然后在接收数组上取出内容即可。发的同时立马就收,发1bit就自动同步收1bit。
SPI传输内容长度无限制。
3.3 SPI代码配置
下文有完整代码,这儿先分析一下如何配置SPI。
要配置3块。分别是主机SPI bus配置、从机device特性配置、每次数据传输交易的配置。
第一块是spi bus。如下代码,就是选择引脚号码、选择主机或从机身份、选择GPIO MATRIX或IOMUX方式。调用initialize()函数选择channel 2。
spi_bus_config_t buscnf = {
.mosi_io_num = pin_MOSI,
.miso_io_num = pin_MISO,
.sclk_io_num = pin_CLK,
.flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_GPIO_PINS,
};
spi_bus_initialize(SPI2_HOST,&buscnf,SPI_DMA_CH_AUTO);
第二块是spi device。不同从机设备的通讯要求都是有些不同的,所以要配置一下。比如命令格式是多少位、参数格式是多少位、通讯时钟速率等,参考SD卡的数据手册来定义。由于CS选择线我是手动上下拉,所以这儿将.spics_io_num配置为-1,不需要SPI硬件自动帮我处理cs了。另外我将command、address长度设为0,因为我自己来编制指令,全部塞在data段里发送,不需要SPI设备操心。
注意device_handle和devicecnf,作为全局变量,因为这个handle到后面发生数据传输时要用起来。
spi_device_handle_t device_handle;
spi_device_interface_config_t devicecnf={
.command_bits=0,
.address_bits=0,
.spics_io_num=-1,
.clock_speed_hz=400000,
.queue_size=1,
};
spi_bus_add_device(SPI2_HOST,&devicecnf,&device_handle);
前两块都是一次过配置好,后面基本就不会动的了。
第三块有点特殊,是transaction_t,每次发生数据传输都要再配置一次。下面用发送一个dummy(0xFF)为例。定义一个spi_transaction_t结构体,里面的属性就是关于本次要传输交易的数据特性。发送长度是1byte即8bits,length设为8。tx_buffer指向要发送的内容。.rx_buffer指定用哪个数组来接收内容。
配置好这三块后,就可以用spi_device_polling_transmit()函数将内容发出去啦。其实写和读都是要调用spi_device_polling_transmit这个传输函数,区别在于读内容时发送的是dummy字节,写内容时不关心rx_buff接收什么信息,聚焦点不同。
uint8_t dummy=0xFF;
uint8_t re_buff[1];
spi_transaction_t transcnf={
.length=8*1,
.tx_buffer=&dummy,
.rx_buffer=re_buff,
};
spi_device_polling_transmit(device_handle,&transcnf);
这儿要提到ESP32的SPI的两种传输机制,一种是轮询Polling,一种是中断。我用的是轮询,就是MCU要一直等到数据传输完毕才切换去别的任务,专一。如果是中断方式传输,就调用spi_device_transmit()函数,会将传输内容和任务放在Freertos里面排队,数据传输过程MCU允许去干别的任务,传输结束时触发中断提醒你处理。轮询方式实时性强能优先处理SPI传输任务。中断方式的配置要麻烦一些,要在transaction_t里面配置回调函数。
按我以上的配置,用示波器检查过,spi_device_polling_transmit()只会发送我要他发的字节,不会拉高拉低cs线。考虑到SD卡上电时序等非常规操作,所以我选择手动控制CS线。
3.4 关于SPI读信息
MOSI发送dummy,MISO接收要读的信息,这是SPI读信息的过程。我测试了以下几种情况分享一下:
(1)如下代码,若transaction_t里的tx_buffer指向1字节的dummy,而想接收10字节信息,结果是会接收到一些奇怪的信息,不完全是我们想要的。
uint8_t dummy=0xFF;
uint8_t re_buff[10];
spi_transaction_t transcnf={
.length=80,
.tx_buffer=&dummy,
.rx_buffer=re_buff,
};
(2)如下代码,若transaction_t里根本没定义tx_buffer,想要接收10字节信息,结果会是空,什么信息都接收不到。
uint8_t re_buff[10];
spi_transaction_t transcnf={
.length=80,
.rx_buffer=re_buff,
};
结论是,dummy也要谨慎处理,最正确就是一致长度的0xFF dummy。
四、SD卡操作时序
4.1 初始化程序的时序
2.0版本以上的SD卡可以概括为以下初始化时序:
上电:CS保持拉高,主机发送74个bits的数据,最好是15个dummy即15个0xFF,帮助SD卡完成上电电压爬升阶段,因为是同步通讯,一般从机设备都需要主机发时钟过去协调其内部程序执行。我发现发多一些dummy会帮助下一阶段稳定获得正常response。
复位命令:CS拉低,主机发送CMD0,0x400000000095,CS拉高。这阶段有可能主机连着很多张SD卡,所以这个CMD0是个广播命令,让所有SD卡复位,进入idle状态。同时这个操作高速SD卡接下来是用SPI模式。R1值是0x01表示成功。
电压检查:CS拉低,主机发送CMD8,0x48000001AA87,CS拉高。CMD8又叫SEND_IF_COND,IF代表interface,就是双方的接口检查,主要是电压对接。这儿也是一条广播命令。01代表主机告诉从机我支持3.3V电压。AA只是一个check pattern,相当于暗号,设置成别的也行,但设别的话,CRC也要记得改,这个AA暗号作用就是检查SPI通讯是否准确,若正常,response R3也会带着AA暗号。R3的最高一位字节也是R1,最高位返回0x01表示成功。如果返回其他值,可以重复发CMD8,如果也不行,考虑是MMC卡或1.0版本的卡。
卡片初始化:CS拉低,主机发送CMD55,0x770000000065,CS拉高;CS拉低,主机发送ACMD41,0x694000000077,CS拉高。因为发任何ACMD命令前都要发CMD55。期望ACMD41返回的response R1是0x00,代表脱离了idle阶段进入ready阶段。如果ACMD41一直不返回0x00,而是0x01或其他值,就要重复这个CMD55+ACMD41的发送操作。我这边重复到第3次就成功收到0x00了。注意初始化不是格式化,卡片的内容不会被清除。这一步前,都要注意CRC计算准确。初始化是主机跟某张SD卡建立一对一专线通讯过程的操作。
检查初始化是否成功:CS拉低,主机发送CMD58,0x7A00000000FF,CS拉高。这一步不是必须的,只是个检查。接收R3,0x00 80 FF 80 00表示成功。R3里面除了最高一位是R1,剩余就是卡片的OCR寄存器的信息,读OCR我们主要看两个参数。第30位CCS,SDSC低容量卡片就是0,SDHC以上卡片(2GB)就是1。第31位power up status位,如果ACMD41步骤不成功,这个位就是0,如果初始化成功这个位就是1。我的实操返回结果是0x00 80 FF 80 00。OCR寄存器看下图:
到此为止,卡片初始化就完成,主机完成了从多张SD卡里面选择出这一张SD卡的程序。后面的通讯就不再是广播了,都是针对这张卡地址的通讯,包括读SD卡容量出厂信息、读写数据等,都是直接操作读写命令即可,不用再做上述初始化步骤。
4.2 数据传输的时序
具体一条指令和响应怎样传输和接收?
注意SD卡的响应不是同步接收的。command和response之间是有空档的,经过实测,间隔为一个dummy字节。发一个指令,接收一个response的时序应该如下:
(1)CS拉低
(2)主机MOSI发48bits内容,指令+参数+CRC
(3) 主机MOSI发一字节dummy即0xFF
(4)主机MOSI根据response的长度比如5字节就发5字节dummy,同时在.rx_buffer上读取response
(5)CS拉高
(6)主机发送一字节dummy协助卡片处理其他事宜,结束。
五、初始化部分的代码
这是自建的component中的fatfs2.c文件
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <string.h>
#include "driver/sdspi_host.h"
static spi_device_handle_t device_handle;
static spi_device_interface_config_t devicecnf;
uint8_t tx_dummy[514];
void cs_enable()
{
gpio_set_level(pin_CS,0);
}
void cs_disable()
{
gpio_set_level(pin_CS,1);
}
esp_err_t spi_init()
{
esp_err_t ret;
gpio_config_t gpiocnf={
.pull_up_en=GPIO_PULLUP_ENABLE,
.mode=GPIO_MODE_OUTPUT_OD,
.pin_bit_mask=1UL << pin_CS,
};
gpio_config(&gpiocnf);
static spi_bus_config_t buscnf = {
.mosi_io_num = pin_MOSI,
.miso_io_num = pin_MISO,
.sclk_io_num = pin_CLK,
.flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_GPIO_PINS,
};
ret=spi_bus_initialize(SPI2_HOST,&buscnf,SPI_DMA_CH_AUTO);
ESP_ERROR_CHECK(ret);
static spi_device_interface_config_t devicecnf={
.command_bits=0,
.address_bits=0,
.spics_io_num=-1,
.clock_speed_hz=400000,
.queue_size=1,
};
ret=spi_bus_add_device(SPI2_HOST,&devicecnf,&device_handle);
memset(tx_dummy,0xFF,sizeof(uint8_t)*514);//初始化tx_dummy
return ret;
}
/*MOSI发len个字节的0xFF,手动控制cs线*/
esp_err_t send_dummy(uint8_t len)
{
spi_transaction_t transcnf={
.length=8*len,
.tx_buffer=&tx_dummy,
};
spi_device_polling_transmit(device_handle,&transcnf);
return(ESP_OK);
}
/*手动控制cs,传入48bits的cmd包含crc*/
esp_err_t write_cmd(uint64_t cmd)
{
esp_err_t ret;
/*cmd转化成6byte数组*/
uint8_t buff[6];
for(uint8_t i=0;i<6;i++)
{
buff[i]=(uint8_t)(cmd >> (5-i)*8);
}
spi_transaction_t transcnf={
.length=48,
.tx_buffer=buff,
};
ret=spi_device_polling_transmit(device_handle,&transcnf);
return(ret);
}
/*response type, the figure indicates length in bytes*/
typedef enum _response_t{R1=1,R7=5,R2=2,R3=5}response_t;
/*MOSI发0xFF等候response*/
void wait_response(response_t response)
{
uint8_t re_buff[response];
spi_transaction_t transcnf={
.length=8*response,
.rx_buffer=re_buff,
.tx_buffer=&tx_dummy,
};
send_dummy(1); //先发一个dummy
spi_device_polling_transmit(device_handle,&transcnf);
printf("Response is:0x");
for(int i=0;i<response;i++)
{
printf("%02X ",re_buff[i]);
}
printf("\n");
}
void speed_change()
{
devicecnf.clock_speed_hz =20000*1000,
spi_bus_add_device(SPI2_HOST,&devicecnf,&device_handle);
}
主程序main.c代码如下:
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <string.h>
#include <sys/unistd.h>
#include <sys/stat.h>
#include "driver/sdspi_host.h"
void card_init()
{
spi_init();
/*模拟sd卡上电*/
cs_disable();
send_dummy(20);
printf("上电发74时钟:\n\n");
vTaskDelay(5 / portTICK_PERIOD_MS);
/*发送cmd0,带R1*/
cs_enable();
write_cmd(CMD0);
printf("写入cmd0结果:\n");
wait_response(R1);
send_dummy(1);
cs_disable();
send_dummy(1);
printf("\n");
vTaskDelay(5 / portTICK_PERIOD_MS);
/*发cmd8识别是否2.0卡*/
cs_enable();
write_cmd(CMD8);
printf("写入cmd8结果:\n");
wait_response(R7);
send_dummy(1);
cs_disable();
send_dummy(1);
printf("\n");
vTaskDelay(5 / portTICK_PERIOD_MS);
/*发cmd55准备为acmd41做准备*/
for (uint32_t i = 0; i < 5; i++)
{
cs_enable();
write_cmd(CMD55);
printf("写入CMD55:\n");
wait_response(R1);
cs_disable();
send_dummy(1);
/*发ACMD41初始化卡*/
cs_enable();
write_cmd(ACMD41);
printf("写入ACMD41初始化\n");
wait_response(R1);
cs_disable();
send_dummy(1);
vTaskDelay(1 / portTICK_PERIOD_MS);
}
printf("\n");
/*发cmd58*/
cs_enable();
write_cmd(CMD58);
printf("写入cmd58结果:\n");
wait_response(R3);
cs_disable();
send_dummy(1);
printf("\n");
/*传输模式改成高速*/
printf("传输模式改成高速.\n");
speed_change();
}
void app_main(void)
{
card_init();
}
六、输出结果
2024-06-13 16:48:13 上电发74时钟:ESP_OK
2024-06-13 16:48:13
2024-06-13 16:48:14 写入cmd0结果:
2024-06-13 16:48:14 response is:0x01
2024-06-13 16:48:14
2024-06-13 16:48:15 写入cmd8结果:
2024-06-13 16:48:15 response is:0x01 00 00 01 AA
2024-06-13 16:48:15
2024-06-13 16:48:16 写入CMD55:
2024-06-13 16:48:16 response is:0x01
2024-06-13 16:48:16 写入ACMD41初始化
2024-06-13 16:48:16 response is:0x01
2024-06-13 16:48:16 写入CMD55:
2024-06-13 16:48:16 response is:0x01
2024-06-13 16:48:16 写入ACMD41初始化
2024-06-13 16:48:16 response is:0x01
2024-06-13 16:48:16 写入CMD55:
2024-06-13 16:48:16 response is:0x01
2024-06-13 16:48:16 写入ACMD41初始化
2024-06-13 16:48:16 response is:0x00
2024-06-13 16:48:16 写入CMD55:
2024-06-13 16:48:16 response is:0x00
2024-06-13 16:48:16 写入ACMD41初始化
2024-06-13 16:48:16 response is:0x00
2024-06-13 16:48:16 写入CMD55:
2024-06-13 16:48:16 response is:0x00
2024-06-13 16:48:16 写入ACMD41初始化
2024-06-13 16:48:16 response is:0x00
2024-06-13 16:48:16
2024-06-13 16:48:21 写入cmd58结果:
2024-06-13 16:48:21 response is:0x00 80 FF 80 00
2024-06-13 16:48:21
七、故障解读
若从CMD0开始,response都是接收0xFF的反馈,则可以检查以下情况:
(1)硬件接线是否接错引脚,是否有线虚接,是否正常通电,电压是否正确;
(2)检查dummy是否正常,包括dummy内容是否初始化为0xFF,dummy长度是否正确;
(3)SD卡或读卡模块是否物理上已损坏;
若CMD0~CMD8正确接收0x01的response,从ACMD41开始都是0x01,表示一直在忙,则却确定SPI通讯是正确的,要检查:
(1)SD卡模块引脚是否电阻不对;
(2)SD卡种类是否不对;
(3)由于ACMD41的response是4字节,查看spi_transaction_t定义的数据传输长度和dummy是否设置正确。
这篇解决了指令操作和SD卡的物理初始化过程,下一篇就讲SD卡读写数据内容。