我最近的项目需要使用SD卡来暂存传感器采样数据,本来呢,NodeMCU是自带支持读写SD卡上的文件系统的,但是文件系统的读写时间是不确定的,而且往往会占用很多时间(因为需要各种运算)。这就导致我的采样频率很不稳定。所以我想到了直接读写扇区,这样基本可以做到硬实时,而且也可以充分利用存储空间。
废话不多说,先来介绍原理吧。
============阶段一:SD卡初始化为SPI模式==========
SD卡有两种读写方式,一种是SDIO,一种是SPI。SDIO的读写速度可以非常高,一般手机、电脑、相机等都是使用SDIO的。SPI其实也不慢啦,可以达到10Mbit/s,对于大多数嵌入式应用绰绰有余了。
怎么告知SD卡使用SPI模式而不要使用SDIO模式呢?SD卡在上电时自动进入SDIO模式,在此模式下,使用SPI的时序向SD卡发送复位命令CMD0(命令的概念接下来解释)。如果SD卡在接收CMD0命令过程中CS为低电平,则进入SPI模式,否则工作在SD 总线模式。
那么SD卡的操作命令是怎样的呢?见下图:
每个命令由6个字节构成。第一个字节的高两位固定为01,低6位表示命令编号n(对于命令CMDn)。接下来的4个字节为命令的参数。最后一个字节的最低位为0,高7位为命令的循环冗余校验码(CRC)。
SD卡收到命令后,会在接下来的8个时钟之后返回响应,响应的字节会根据命令的不同而不同。
CMD0为复位命令,同时通过CS引进的电平告知SD卡使用SDIO模式还是SPI模式。既然是编号0,所以第一个字节就是0x40。CMD0没有参数,所以后面的4个字节都为0。CMD0的CRC固定为0x95。CMD0的响应是一个字节,为0x01,表示进入IDLE状态。
复位之后,就需要获取SD卡的类型。SD卡有若干种标准,比如SDv1.1,SDv2.0、SDHC和SDXC等。目前市面上2G的SD卡一般都是SDv2.0标准,而4G以上32G以上的卡一般都是SDHC标准,而64G以上的卡都是SDXC标准。我打算只支持SDv1.1、SDv2.0和SDHC标准。获得SD卡类型的命令是CMD8,参数是0x01AA。该命令返回一个字节,如果第2位为1,则表示为SDv1.1版本,否则是SDv2.0或者SDHC版本。
接下来就需要初始化,初始化使用ACMD41命令。ACMDn表示应用命令,具体而言就是先发送CMD55,参数为0,然后发送CMDn,参数就是ACMDn命令的参数。如果是SDv1.1版本,那么ACMD41命令的参数是0,否则是0x40000000。如果命令返回0,那么说明初始化成功。
最后,如果不是SDv1.1的话,还需要进一步区分是SDv2.0还是SDHC,这个是依靠CMD58的返回值来判断的。CMD58返回两个字节,第一个字节为0,第二个字节如果是0xC0,那么说明是SDHC。
至此,初始化过程完成。
另外需要注意,SPI模式下默认是不需要CRC校验的,但是因为初始化完成之前,还未进入SPI模式,所以还是需要CRC的。好在在初始化完成之前,只用到了CMD0和CMD8,而且它们的参数都是固定的,所以对应的CRC也是固定的。
这里可以先给出发送命令和接收回应通用函数:
--发送命令CMDn,并发送其参数 local function sendCommand(cmd,param) --最高两位为01 spi.send(1,bit.bor(0x40,cmd)) --把参数分解为4个字节,高字节先发送 for i=1,4 do local aByte=bit.band(bit.rshift(param,8*(4-i)),0xff) spi.send(1,aByte) end --确定CRC,如果是CMD0,那么CRC为0x95,如果是CMD8,那么CRC为0x87,否则为0xff local crc=0xff if cmd==0 then crc=0x95 elseif cmd==8 then crc=0x87 end --发送CRC spi.send(1,crc) --等待回应,有效的回应最高位为0 local response=0 for i=1,200 do response=string.byte(spi.recv(1,1)) if bit.band(response,0x80)==0x00 then break end end return response end --发送ACMDn,并发送其参数 local function sendAppCommand(acmd,param) sendCommand(55,0) local response=sendCommand(acmd,param) return response end
那么接下来就可以定义初始化SD卡的函数了:
--初始化SD卡,成功返回true,并把SD卡类型存在cardType变量中,失败返回false local function initSD() --拉高CS gpio.mode(csPin,gpio.OUTPUT) gpio.write(csPin,gpio.HIGH) --初始化SPI,速率为80Mhz/256(初始化过程不能超过400Khz) spi.setup(1,spi.MASTER,spi.CPOL_LOW,spi.CPHA_LOW,8,256) --发送80个时钟,让SD卡同步 for i=1,10 do spi.send(1,0xff) end --拉低CS gpio.write(csPin,gpio.LOW) --不停发送CMD0,直到收到0x01 local response=0 for i=1,200 do response=sendCommand(0,0) if response==0x01 then break end end --尝试次数耗尽,拉高CS,返回错误 if response~=0x01 then gpio.write(csPin,gpio.HIGH) return false end --发送CMD8 response=sendCommand(8,0x01aa) --如果第2位为1,则为SD1 if bit.band(response,0x04)~=0 then cardType="SD1" --否则至少是SD2(也可能是SDHC),此时SD卡会返回5个字节,所以需要额外再读4字节 else spi.recv(1,4) cardType="SD2" end --确定ACMD41的参数 local param=0 --如果是SD2(或SDHC),那么参数为0x40000000,否则为0 if(cardType=="SD2") then param=0x40000000 end --不停发送ACMD41,直到收到0 for i=1,200 do response=sendAppCommand(41,param) if response==0x00 then break end end --尝试次数耗尽,拉高CS,返回错误 if response~=0x00 then gpio.write(csPin,gpio.HIGH) return false end --此时初始化已完成,需要进一步确定是SD2还是SDHC if cardType=="SD2" then --发送CMD58 response=sendCommand(58,0) --如果收到的不是0,拉高CS,返回错误 if response~=0x00 then gpio.write(csPin,gpio.HIGH) return false end --CMD58返回5个字节,读取第二个字节 response=string.byte(spi.recv(1,1)) --如果是0xC0,那么就是SDHC if response==0xc0 then cardType="SDHC" end --跳过最后三个字节 spi.recv(1,3) end --拉高CS gpio.write(csPin,gpio.HIGH) --初始化完成,可以提高SPI速率(80Mhz/8)进行高速传输了 spi.setup(1,spi.MASTER,spi.CPOL_LOW,spi.CPHA_LOW,8,8) return true end
================阶段二:获取扇区数量=================
初始化过程除了获取SD卡类型以外,最好还能获取SD卡大小。SD卡大小可以使用扇区数量来表达。
获取扇区数量使用的是CMD9命令。发送CMD9之后,SD卡应该要返回0x00。返回0x00说明CMD9执行成功,那么接下来就需要不断读取,直到读到非0xFF的值(通常是0xFE),则表明SD卡已经准备好数据,MCU接下来要接收数据了。总共需要接收18字节的数据,其中前16字节叫做CSD,包含了扇区数量、块大小等等信息,后2字节是CRC。
CSD有两种标准,一种是v1标准,一种是v2标准,这个是通过第1个字节(从1计数)的最高两位区分的。两种标准下,对16字节的解析方法是不同的。我相信自然语言肯定没有代码来的清楚,我就直接给出代码吧:
--发送CMD9 response=sendCommand(9,0) --结果不为0x00说明出错 if response~=0x00 then gpio.write(csPin,gpio.HIGH) return false end --等待数据开始标志0xFE for i=1,200 do response=string.byte(spi.recv(1,1)) if response==0xfe then break end end --尝试次数耗尽,拉高CS,返回错误 if response~=0xfe then gpio.write(csPin,gpio.HIGH) return false end --读取16字节数据,但是是字符串形式 local csdStr=spi.recv(1,16) --读取2字节CRC(丢弃) spi.recv(1,2) --把16字节的字符串转换成字节数组 local csd={} for i=1,16 do csd[i]=string.byte(string.sub(csdStr,i,i)) end --获取版本号,第1字节的高2位 local version=bit.rshift(csd[1],6) --版本v1.0 if version==0 then --这是什么我也不知道,第6字节的低4位 local readBlLen=bit.band(csd[6],0x0f) --size字段的高部分,第7字节的低2位 local sizeHigh=bit.band(csd[7],0x03) --size字段的中部分,第8字节 local sizeMid=csd[8] --size字段的低部分,第9字节的高2位 local sizeLow=bit.rshift(csd[9],6) --拼装size字段 local size=sizeHigh*1024+sizeMid*4+sizeLow --sizeMulti字段的高部分,第10字节的低2位 local sizeMultiHigh=bit.band(csd[10],0x03) --sizeMulti字段的低部分,第11字节的高1位 local sizeMultiLow=bit.rshift(csd[11],7) --拼装sizeMulti字段 local sizeMulti=sizeMultiHigh*2+sizeMultiLow --按照规定计算扇区数量 blockCount=bit.lshift(size+1,sizeMulti+readBlLen-7) --版本号v2.0 elseif version==1 then --size字段的高部分,第8字节的高2位 local sizeHigh=bit.rshift(csd[8],2) --size字段的中部分,第9字节 local sizeMid=csd[9] --size字段的低部分,第10字节 local sizeLow=csd[10] --拼装size字段 local size=sizeHigh*65536+sizeMid*256+sizeLow --按规定计算扇区数量 blockCount=(size+1)*1024 else gpio.write(csPin,gpio.HIGH) return false end
================阶段三:读取单个扇区=================
完成初始化之后,SD卡已经准备好读写了。这里先讲如何读取单个扇区。
读取单个扇区使用命令CMD17,参数有两种情况。如果是SDv1.1或者SDv2.0,那么CMD17的参数就是以字节为单位表示的地址,比如第0扇区的地址就是0,第1个扇区的地址就是512,第2个扇区的地址就是1024,以此类推。不难理解,这种表示方法很浪费,因为地址的最低9位永远为0。而且因为使用4字节表示地址,所以最大寻址4GB。这就是为什么SDHC改变了参数的含义。SDHC中,CMD17的参数就是指第几个扇区,第0个扇区的地址是0,第1个扇区的地址是1,第2个扇区的地址是2,以此类推。因此参数需要根据cardType确定。
发送了CMD17之后,应该收到回应0x00。之后,MCU应该不停读取,直到读到0xFE,这个是数据开始的标志。0xFE之后的512字节就是扇区的原始数据了。读完512字节的数据之后,还需要读取2字节的CRC。
OK,很简单,代码如下:
--读取一个扇区,参是是扇区号,返回一个512字节长的字符串 local function readBlock(blockNo) --拉第CS gpio.write(csPin,gpio.LOW) --确定CMD17的参数 local param=blockNo --如果不是SDHC,那么参数就是扇区号*512,否则就是扇区号本身 if cardType~="SDHC" then param=blockNo*512 end --发送CMD17 local response=sendCommand(17,param) --如果不回应0x00,则拉高CS,返回错误 if response~=0x00 then gpio.write(csPin,gpio.HIGH) return false end --不停读直到读到0xFE for i=1,200 do response=string.byte(spi.recv(1,1)) if response==0xfe then break end end --尝试次数耗尽,拉高CS,返回错误 if response~=0xfe then gpio.write(csPin,gpio.HIGH) return false end --读取512字节的数据 local block=spi.recv(1,512) --跳过2字节的CRC spi.recv(1,2) --拉高CS gpio.write(csPin,gpio.HIGH) return block end
==================阶段四:写入单个扇区==================
写入单个扇区和读取单个扇区一样简单,使用CMD24。CMD24的参数与CMD17是一样的,表示写入的地址,也需要根据cardType进行调整。发送CMD24之后,需要收到回应0x00。然后发送0xFE,表示开始写入数据,然后写入512字节的数据到扇区中,最后附带两字节的伪CRC。所谓伪CRC,就是说SD卡不会真的去检验,所以可以固定为0xff。之后,SD卡会发送一个字节,如果低5位是00101,说明数据已经被SD卡接收(但可能还未固化)。接着不停读,直到读到0xff,说明数据已经固化。
OK,很简单,直接上代码:
--向一个扇区写入数据,参数为扇区号和数据(字符串) local function writeBlock(blockNo,data) --拉第CS gpio.write(csPin,gpio.LOW) --确定CMD24的参数 local param=blockNo --如果不是SDHC,那么参数是扇区号*512,否则就是扇区号本身 if cardType~="SDHC" then param=blockNo*512 end --发送CMD24 local response=sendCommand(24,param) --如果不回应0x00,拉高CS,返回错误 if response~=0x00 then gpio.write(csPin,gpio.HIGH) return false end --发送0xFE,表示开始写入数据 spi.send(1,0xfe) --获取传入数据的长度 local length=string.len(data) --如果<=512字节 if length<=512 then --先发送data spi.send(1,data) --再用0x00补到512字节长 for i=1,(512-length) do spi.send(1,0x00) end --如果>512字节 else --只发送前512字节 spi.send(1,string.sub(data,1,512)) end --发送伪CRC spi.send(1,0xff,0xff) --接收回应字节,并只取低5位 response=bit.band(string.byte(spi.recv(1,1)),0x1f) --如果低5位不是00101,则拉高CS,返回错误 if response~=0x05 then gpio.write(csPin,gpio.HIGH) return false end --不停读取,直到读到0xff for i=1,200 do response=string.byte(spi.recv(1,1)) if response==0xff then break end end --尝试次数耗尽,则拉高CS,返回错误 if response~=0xff then gpio.write(csPin,gpio.HIGH) return false end gpio.write(csPin,gpio.HIGH) return true end
================阶段五:最终整合=================
好吧,其实我是先有完整版,然后再一段段拆分再注释的。。。
再插一句屁话!Arduino和NodeMCU结合简直完美——各种硬件驱动程序可以看Arduino的源码来翻译,而NodeMCU的基于Lua的交互式编程使得验证代码非常方便!
完整代码:
sdCard.lua
--this file create an object that representing a sd card --csPin: the pin connected to CS of SD card --the object returned contains following methods: --getCardType(): return "SD1" or "SD2" or "SDHC" --getBlockCount(): return the count of blocks --readBlock(blockNo): return 512-byte-length string from block --writeBlock(blockNo,data): write 512 bytes to block function newSdCard(csPin) local cardType=0 local blockCount=0 local function sendCommand(cmd,param) spi.send(1,bit.bor(0x40,cmd)) for i=1,4 do local aByte=bit.band(bit.rshift(param,8*(4-i)),0xff) spi.send(1,aByte) end local crc=0xff if cmd==0 then crc=0x95 elseif cmd==8 then crc=0x87 end spi.send(1,crc) local response=0 for i=1,200 do response=string.byte(spi.recv(1,1)) if bit.band(response,0x80)==0x00 then break end end return response end local function sendAppCommand(acmd,param) sendCommand(55,0) local response=sendCommand(acmd,param) return response end local function initSD() gpio.mode(csPin,gpio.OUTPUT) gpio.write(csPin,gpio.HIGH) spi.setup(1,spi.MASTER,spi.CPOL_LOW,spi.CPHA_LOW,8,256) for i=1,10 do spi.send(1,0xff) end gpio.write(csPin,gpio.LOW) local response=0 for i=1,200 do response=sendCommand(0,0) if response==0x01 then break end end if response~=0x01 then gpio.write(csPin,gpio.HIGH) return false end response=sendCommand(8,0x01aa) if bit.band(response,0x04)~=0 then cardType="SD1" else spi.recv(1,4) cardType="SD2" end local param=0 if(cardType=="SD2") then param=0x40000000 end for i=1,200 do response=sendAppCommand(41,param) if response==0x00 then break end end if response~=0x00 then gpio.write(csPin,gpio.HIGH) return false end if cardType=="SD2" then response=sendCommand(58,0) if response~=0x00 then gpio.write(csPin,gpio.HIGH) return false end response=string.byte(spi.recv(1,1)) if response==0xc0 then cardType="SDHC" end spi.recv(1,3) end response=sendCommand(9,0) if response~=0x00 then gpio.write(csPin,gpio.HIGH) return false end for i=1,200 do response=string.byte(spi.recv(1,1)) if response==0xfe then break end end if response~=0xfe then gpio.write(csPin,gpio.HIGH) return false end local csdStr=spi.recv(1,16) spi.recv(1,2) local csd={} for i=1,16 do csd[i]=string.byte(string.sub(csdStr,i,i)) end local version=bit.rshift(csd[1],6) if version==0 then local readBlLen=bit.band(csd[6],0x0f) local sizeHigh=bit.band(csd[7],0x03) local sizeMid=csd[8] local sizeLow=bit.rshift(csd[9],6) local size=sizeHigh*1024+sizeMid*4+sizeLow local sizeMultiHigh=bit.band(csd[10],0x03) local sizeMultiLow=bit.rshift(csd[11],7) local sizeMulti=sizeMultiHigh*2+sizeMultiLow blockCount=bit.lshift(size+1,sizeMulti+readBlLen-7) elseif version==1 then local sizeHigh=bit.rshift(csd[8],2) local sizeMid=csd[9] local sizeLow=csd[10] local size=sizeHigh*65536+sizeMid*256+sizeLow blockCount=(size+1)*1024 else gpio.write(csPin,gpio.HIGH) return false end gpio.write(csPin,gpio.HIGH) spi.setup(1,spi.MASTER,spi.CPOL_LOW,spi.CPHA_LOW,8,8) return true end local function getBlockCount() return blockCount end local function getCardType() return cardType end local function readBlock(blockNo) gpio.write(csPin,gpio.LOW) local param=blockNo if cardType~="SDHC" then param=blockNo*512 end local response=sendCommand(17,param) if response~=0x00 then gpio.write(csPin,gpio.HIGH) return false end for i=1,200 do response=string.byte(spi.recv(1,1)) if response==0xfe then break end end if response~=0xfe then gpio.write(csPin,gpio.HIGH) return false end local block=spi.recv(1,512) spi.recv(1,2) gpio.write(csPin,gpio.HIGH) return block end local function writeBlock(blockNo,data) gpio.write(csPin,gpio.LOW) local param=blockNo if cardType~="SDHC" then param=blockNo*512 end local response=sendCommand(24,param) if response~=0x00 then gpio.write(csPin,gpio.HIGH) return false end spi.send(1,0xfe) local length=string.len(data) if length<=512 then spi.send(1,data) for i=1,(512-length) do spi.send(1,0x00) end else spi.send(1,string.sub(data,1,512)) end spi.send(1,0xff,0xff) response=bit.band(string.byte(spi.recv(1,1)),0x1f) if response~=0x05 then gpio.write(csPin,gpio.HIGH) return false end for i=1,200 do response=string.byte(spi.recv(1,1)) if response==0xff then break end end if response~=0xff then gpio.write(csPin,gpio.HIGH) return false end gpio.write(csPin,gpio.HIGH) return true end if not initSD() then return false end local sdCard={} sdCard.getCardType=getCardType sdCard.getBlockCount=getBlockCount sdCard.readBlock=readBlock sdCard.writeBlock=writeBlock sendAppCommand=nil initSD=nil newSdCard=nil return sdCard end
=================阶段五:一个小示例===============
使用的SPI模式的SD卡读卡器,如图:
注意这个读卡器使用5V电源(其实感觉真坑爹,SD卡本身使用3.3V的电平,结果这个读卡器用电平转换模块变成5V的电平,然后我的NodeMCU又是3.3V供电。。。)。
读卡器和NodeMCU之间的连线如下表:
读卡器 | NODEMCU |
---|---|
SCK | pin5 |
MISO | pin6 |
MOSI | pin7 |
CS | pin8 |
VCC | Vin |
GND | GND |
插入一张8GB的SD卡,执行代码:
sd=newSdCard(8) print(sd.getCardType()) print(sd.getBlockCount())
正常情况下应该能够看到输出:
SDHC 15126528
当然啦,扇区数量可能会略有不同。
然后执行代码:
writeStr=string.rep("zjs!",128) print(sd.writeBlock(1228,writeStr)) readStr=sd.readBlock(1228) print(readStr)
可以看到先输出true,表示写入成功,然后输出128个“zjs!”,读出的和写入的值相同,说明确实成功了。
上一篇:关于SDNAND的低功耗问题
下一篇:SDNAND扇区分析