【ESP

您所在的位置:网站首页 iphone怎么读取tf卡 【ESP

【ESP

2024-07-17 15:46| 来源: 网络整理| 查看: 265

        如何使用外部储存、如何使用标准文件系统读写信息,是比较大的课题,我想用几篇文章的幅度详细介绍自己写驱动程序、移植文件系统库,在不依赖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:通讯失败的MicrSD卡模块 图2:通讯成功的Microsd卡模块

         一开始用图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的工具在这个网址:

CRC(循环冗余校验)在线计算_ip33.com

        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卡进入到哪个状态来判断。        

非SPI模式 SPI模式的ACMD41

        左图是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 #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 (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


【本文地址】


今日新闻


推荐新闻


    CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3