当前位置: 首页 新闻资讯 技术问答

MCU实现SDNAND扇区级读写

SD NAND-贴片式TF卡-贴片式SD卡-免费测试2024-06-07385

我最近的项目需要使用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
SCKpin5
MISOpin6
MOSIpin7
CSpin8
VCCVin
GNDGND

插入一张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贴片式TF卡贴片式SD卡SD FLASHSLC NANDNAND FLASH


SD NAND-贴片式TF卡-贴片式SD卡-免费测试

深圳市芯存者科技有限公司

售前咨询
售前咨询
售后服务
售后服务
联系我们

电话:176-6539-0767

Q Q:135-0379-986

邮箱:1350379986@qq.com

地址:深圳市南山区蛇口街道后海大道1021号B C座C422W8

在线客服 在线客服 QQ客服 微信客服 淘宝店铺 联系我们 返回顶部