@robert-hh said in External SD Card:
"""
MicroPython driver for SD cards using SPI bus.
Requires an SPI bus and a CS pin. Provides readblocks and writeblocks
methods so the device can be mounted as a filesystem.
Example usage on xxPy:
import machine, sdcard, os
sd = sdcard.SDCard(machine.SPI(0), machine.Pin("P9"))
os.mount(sd, '/sd2')
os.listdir('/sd2')
"""
from micropython import const
import time
_CMD_TIMEOUT = const(100)
_R1_IDLE_STATE = const(1 << 0)
#R1_ERASE_RESET = const(1 << 1)
_R1_ILLEGAL_COMMAND = const(1 << 2)
#R1_COM_CRC_ERROR = const(1 << 3)
#R1_ERASE_SEQUENCE_ERROR = const(1 << 4)
#R1_ADDRESS_ERROR = const(1 << 5)
#R1_PARAMETER_ERROR = const(1 << 6)
_TOKEN_CMD25 = const(0xfc)
_TOKEN_STOP_TRAN = const(0xfd)
_TOKEN_DATA = const(0xfe)
class SDCard:
def __init__(self, spi, cs):
self.spi = spi
self.cs = cs
self.cmdbuf = bytearray(6)
self.dummybuf = bytearray(512)
for i in range(512):
self.dummybuf[i] = 0xff
self.dummybuf_memoryview = memoryview(self.dummybuf)
# initialise the card
self.init_card()
def init_spi(self, baudrate):
master = self.spi.MASTER
self.spi.init(master, baudrate=baudrate, phase=0, polarity=0)
def init_card(self):
# init CS pin
self.cs.init(self.cs.OUT, value=1)
# init SPI bus; use low data rate for initialisation
self.init_spi(100000)
# clock card at least 100 cycles with cs high
for i in range(16):
self.spi.write(b'\xff')
# CMD0: init card; should return _R1_IDLE_STATE (allow 5 attempts)
for _ in range(5):
if self.cmd(0, 0, 0x95) == _R1_IDLE_STATE:
break
else:
raise OSError("no SD card")
# CMD8: determine card version
r = self.cmd(8, 0x01aa, 0x87, 4)
if r == _R1_IDLE_STATE:
self.init_card_v2()
elif r == (_R1_IDLE_STATE | _R1_ILLEGAL_COMMAND):
self.init_card_v1()
else:
raise OSError("couldn't determine SD card version")
# get the number of sectors
# CMD9: response R2 (R1 byte + 16-byte block read)
if self.cmd(9, 0, 0, 0, False) != 0:
raise OSError("no response from SD card")
csd = bytearray(16)
self.readinto(csd)
if csd[0] & 0xc0 != 0x40:
raise OSError("SD card CSD format not supported")
self.sectors = ((csd[8] << 8 | csd[9]) + 1) * 2014
#print('sectors', self.sectors)
# CMD16: set block length to 512 bytes
if self.cmd(16, 512, 0) != 0:
raise OSError("can't set 512 block size")
# set to high data rate now that it's initialised
#self.init_spi(100000)
#self.init_spi(40000000)
def init_card_v1(self):
for i in range(_CMD_TIMEOUT):
self.cmd(55, 0, 0)
if self.cmd(41, 0, 0) == 0:
self.cdv = 512
#print("[SDCard] v1 card")
return
raise OSError("timeout waiting for v1 card")
def init_card_v2(self):
for i in range(_CMD_TIMEOUT):
time.sleep_ms(50)
self.cmd(58, 0, 0, 4)
self.cmd(55, 0, 0)
if self.cmd(41, 0x40000000, 0) == 0:
self.cmd(58, 0, 0, 4)
self.cdv = 1
#print("[SDCard] v2 card")
return
raise OSError("timeout waiting for v2 card")
def cmd(self, cmd, arg, crc, final=0, release=True):
self.cs(0)
# create and send the command
buf = self.cmdbuf
buf[0] = 0x40 | cmd
buf[1] = arg >> 24
buf[2] = arg >> 16
buf[3] = arg >> 8
buf[4] = arg
buf[5] = crc
self.spi.write(buf)
# wait for the response (response[7] == 0)
for i in range(_CMD_TIMEOUT):
response = self.spi.read(1, write=0xff)[0]
if not (response & 0x80):
# this could be a big-endian integer that we are getting here
for j in range(final):
self.spi.write(b'\xff')
if release:
self.cs(1)
self.spi.write(b'\xff')
return response
# timeout
self.cs(1)
self.spi.write(b'\xff')
return -1
def cmd_nodata(self, cmd):
self.spi.write(cmd)
self.spi.read(1, write=0xff) # ignore stuff byte
for _ in range(_CMD_TIMEOUT):
if self.spi.read(1, write=0xff)[0] == 0xff:
self.cs(1)
self.spi.write(b'\xff')
return 0 # OK
self.cs(1)
self.spi.write(b'\xff')
return 1 # timeout
def readinto(self, buf):
self.cs(0)
# read until start byte (0xff)
while self.spi.read(1, write=0xff)[0] != 0xfe:
pass
# read data
mv = self.dummybuf_memoryview[:len(buf)]
self.spi.write_readinto(mv, buf)
# read checksum
self.spi.write(b'\xff')
self.spi.write(b'\xff')
self.cs(1)
self.spi.write(b'\xff')
def write(self, token, buf):
self.cs(0)
# send: start of block, data, checksum
self.spi.read(1, token)
self.spi.write(buf)
self.spi.write(b'\xff')
self.spi.write(b'\xff')
# check the response
if (self.spi.read(1, 0xff)[0] & 0x1f) != 0x05:
self.cs(1)
self.spi.write(b'\xff')
return
# wait for write to finish
while self.spi.read(1, 0xff)[0] == 0:
pass
self.cs(1)
self.spi.write(b'\xff')
def write_token(self, token):
self.cs(0)
self.spi.read(1, token)
self.spi.write(b'\xff')
# wait for write to finish
while self.spi.read(1, 0xff)[0] == 0x00:
pass
self.cs(1)
self.spi.write(b'\xff')
def count(self):
return self.sectors
def readblocks(self, block_num, buf):
nblocks, err = divmod(len(buf), 512)
assert nblocks and not err, 'Buffer length is invalid'
if nblocks == 1:
# CMD17: set read address for single block
if self.cmd(17, block_num * self.cdv, 0) != 0:
return 1
# receive the data
self.readinto(buf)
else:
# CMD18: set read address for multiple blocks
if self.cmd(18, block_num * self.cdv, 0) != 0:
return 1
offset = 0
mv = memoryview(buf)
while nblocks:
self.readinto(mv[offset : offset + 512])
offset += 512
nblocks -= 1
return self.cmd_nodata(b'\x0c') # cmd 12
return 0
def writeblocks(self, block_num, buf):
nblocks, err = divmod(len(buf), 512)
assert nblocks and not err, 'Buffer length is invalid'
if nblocks == 1:
# CMD24: set write address for single block
if self.cmd(24, block_num * self.cdv, 0) != 0:
return 1
# send the data
self.write(_TOKEN_DATA, buf)
else:
# CMD25: set write address for first block
if self.cmd(25, block_num * self.cdv, 0) != 0:
return 1
# send the data
offset = 0
mv = memoryview(buf)
while nblocks:
self.write(_TOKEN_CMD25, mv[offset : offset + 512])
offset += 512
nblocks -= 1
self.write_token(_TOKEN_STOP_TRAN)
return 0
I know this is a bit old but I just got an SD card module (https://www.sparkfun.com/products/13743) and was trying to get it working with my FiPy. I was running into a lot of issues but was able to make some headway using the library you pasted @robert-hh and thought I would share my results in case other people wanted to get it working.
However, I did have some errors with the library and had to make the following changes:
The pins you used are not the default pins for the SPI init call. So I created my own SPI instance with the pin allocation you had in the post (since I already had them like that) and passed that instance into the SDCard constructor rather than machine.SPI(0). I also removed the additional spi init calls.
I had to set the SPI baudrate to 1000000 instead of 100000
I had to declare the CS pin as an instance outside of the library and pass it in similar to the SPI above.
The command to check idle state at the start of the library self.cmd(0, 0, 0x95) would never return a value of 1. I don't know where the reference of these commands were but testing the other commands manually all returned values as expected. So I commented out the entire block that checks for the SD card or raises the 'no SD card' error. I found that I still need to run this command regardless, I get the same set of (4?) different values that repeat each time it is called. They never equal 1 but I let the code continue and command out the error block. Not calling the 0x95 command will not allow the next set of command to work correctly.
Doing the above I have gotten my external SD card board working fine with the FiPy. For reference below are my library and how I call it.
"""
MicroPython driver for SD cards using SPI bus.
Requires an SPI bus and a CS pin. Provides readblocks and writeblocks
methods so the device can be mounted as a filesystem.
Example usage on xxPy:
import machine, sdcard, os
sd = sdcard.SDCard(machine.SPI(0), machine.Pin("P9"))
os.mount(sd, '/sd2')
os.listdir('/sd2')
"""
from micropython import const
import time
_CMD_TIMEOUT = const(100)
_R1_IDLE_STATE = const(1 << 0)
#R1_ERASE_RESET = const(1 << 1)
_R1_ILLEGAL_COMMAND = const(1 << 2)
#R1_COM_CRC_ERROR = const(1 << 3)
#R1_ERASE_SEQUENCE_ERROR = const(1 << 4)
#R1_ADDRESS_ERROR = const(1 << 5)
#R1_PARAMETER_ERROR = const(1 << 6)
_TOKEN_CMD25 = const(0xfc)
_TOKEN_STOP_TRAN = const(0xfd)
_TOKEN_DATA = const(0xfe)
class SDCard:
def __init__(self, spi, cs):
self.spi = spi
self.cs = cs
self.cmdbuf = bytearray(6)
self.dummybuf = bytearray(512)
for i in range(512):
self.dummybuf[i] = 0xff
self.dummybuf_memoryview = memoryview(self.dummybuf)
# initialise the card
self.init_card()
def init_spi(self, baudrate):
master = self.spi.MASTER
self.spi.init(master, baudrate=baudrate, phase=0, polarity=0)
def init_card(self):
# init CS pin
#self.cs.init(self.cs.OUT, value=0)
# init SPI bus; use low data rate for initialisation
#self.init_spi(100000)
# clock card at least 100 cycles with cs high
for i in range(16):
self.spi.write(b'\xff')
# CMD0: init card; should return _R1_IDLE_STATE (allow 5 attempts)
for _ in range(5):
if self.cmd(0, 0, 0x95) == _R1_IDLE_STATE:
break
'''
else:
raise OSError("no SD card")
'''
# CMD8: determine card version
r = self.cmd(8, 0x01aa, 0x87, 4)
if r == _R1_IDLE_STATE:
self.init_card_v2()
elif r == (_R1_IDLE_STATE | _R1_ILLEGAL_COMMAND):
self.init_card_v1()
else:
raise OSError("couldn't determine SD card version")
# get the number of sectors
# CMD9: response R2 (R1 byte + 16-byte block read)
if self.cmd(9, 0, 0, 0, False) != 0:
raise OSError("no response from SD card")
csd = bytearray(16)
self.readinto(csd)
if csd[0] & 0xc0 != 0x40:
raise OSError("SD card CSD format not supported")
self.sectors = ((csd[8] << 8 | csd[9]) + 1) * 2014
#print('sectors', self.sectors)
# CMD16: set block length to 512 bytes
if self.cmd(16, 512, 0) != 0:
raise OSError("can't set 512 block size")
# set to high data rate now that it's initialised
#self.init_spi(100000)
#self.init_spi(40000000)
def init_card_v1(self):
for i in range(_CMD_TIMEOUT):
self.cmd(55, 0, 0)
if self.cmd(41, 0, 0) == 0:
self.cdv = 512
#print("[SDCard] v1 card")
return
raise OSError("timeout waiting for v1 card")
def init_card_v2(self):
for i in range(_CMD_TIMEOUT):
time.sleep_ms(50)
self.cmd(58, 0, 0, 4)
self.cmd(55, 0, 0)
if self.cmd(41, 0x40000000, 0) == 0:
self.cmd(58, 0, 0, 4)
self.cdv = 1
#print("[SDCard] v2 card")
return
raise OSError("timeout waiting for v2 card")
def cmd(self, cmd, arg, crc, final=0, release=True):
self.cs(0)
# create and send the command
buf = self.cmdbuf
buf[0] = 0x40 | cmd
buf[1] = arg >> 24
buf[2] = arg >> 16
buf[3] = arg >> 8
buf[4] = arg
buf[5] = crc
self.spi.write(buf)
# wait for the response (response[7] == 0)
for i in range(_CMD_TIMEOUT):
response = self.spi.read(1, write=0xff)[0]
if not (response & 0x80):
# this could be a big-endian integer that we are getting here
for j in range(final):
self.spi.write(b'\xff')
if release:
self.cs(1)
self.spi.write(b'\xff')
return response
# timeout
self.cs(1)
self.spi.write(b'\xff')
return -1
def cmd_nodata(self, cmd):
self.spi.write(cmd)
self.spi.read(1, write=0xff) # ignore stuff byte
for _ in range(_CMD_TIMEOUT):
if self.spi.read(1, write=0xff)[0] == 0xff:
self.cs(1)
self.spi.write(b'\xff')
return 0 # OK
self.cs(1)
self.spi.write(b'\xff')
return 1 # timeout
def readinto(self, buf):
self.cs(0)
# read until start byte (0xff)
while self.spi.read(1, write=0xff)[0] != 0xfe:
pass
# read data
mv = self.dummybuf_memoryview[:len(buf)]
self.spi.write_readinto(mv, buf)
# read checksum
self.spi.write(b'\xff')
self.spi.write(b'\xff')
self.cs(1)
self.spi.write(b'\xff')
def write(self, token, buf):
self.cs(0)
# send: start of block, data, checksum
self.spi.read(1, token)
self.spi.write(buf)
self.spi.write(b'\xff')
self.spi.write(b'\xff')
# check the response
if (self.spi.read(1, 0xff)[0] & 0x1f) != 0x05:
self.cs(1)
self.spi.write(b'\xff')
return
# wait for write to finish
while self.spi.read(1, 0xff)[0] == 0:
pass
self.cs(1)
self.spi.write(b'\xff')
def write_token(self, token):
self.cs(0)
self.spi.read(1, token)
self.spi.write(b'\xff')
# wait for write to finish
while self.spi.read(1, 0xff)[0] == 0x00:
pass
self.cs(1)
self.spi.write(b'\xff')
def count(self):
return self.sectors
def readblocks(self, block_num, buf):
nblocks, err = divmod(len(buf), 512)
assert nblocks and not err, 'Buffer length is invalid'
if nblocks == 1:
# CMD17: set read address for single block
if self.cmd(17, block_num * self.cdv, 0) != 0:
return 1
# receive the data
self.readinto(buf)
else:
# CMD18: set read address for multiple blocks
if self.cmd(18, block_num * self.cdv, 0) != 0:
return 1
offset = 0
mv = memoryview(buf)
while nblocks:
self.readinto(mv[offset : offset + 512])
offset += 512
nblocks -= 1
return self.cmd_nodata(b'\x0c') # cmd 12
return 0
def writeblocks(self, block_num, buf):
nblocks, err = divmod(len(buf), 512)
assert nblocks and not err, 'Buffer length is invalid'
if nblocks == 1:
# CMD24: set write address for single block
if self.cmd(24, block_num * self.cdv, 0) != 0:
return 1
# send the data
self.write(_TOKEN_DATA, buf)
else:
# CMD25: set write address for first block
if self.cmd(25, block_num * self.cdv, 0) != 0:
return 1
# send the data
offset = 0
mv = memoryview(buf)
while nblocks:
self.write(_TOKEN_CMD25, mv[offset : offset + 512])
offset += 512
nblocks -= 1
self.write_token(_TOKEN_STOP_TRAN)
return 0
from machine import SPI
from machine import Pin
import machine, sdcard, os
spi = SPI(0, mode=SPI.MASTER, baudrate=1000000, polarity=0, phase=0, pins=('P10','P11','P12'))
cs = Pin('P9', Pin.OUT)
sd = sdcard.SDCard(spi, cs)
os.mount(sd, "/card")
I could probably tidy up the library to initialise the SPI and CS properly internally but this got me going at least.