Skip to content

Instantly share code, notes, and snippets.

@sago35
Created June 25, 2020 10:15
Show Gist options
  • Save sago35/c9a9d2c96279fbcbaf8d09cf0ec4a113 to your computer and use it in GitHub Desktop.
Save sago35/c9a9d2c96279fbcbaf8d09cf0ec4a113 to your computer and use it in GitHub Desktop.
tinygo spi sdcard driver
package main
import (
"fmt"
"io"
"machine"
"os"
"strconv"
"strings"
"time"
"tinygo.org/x/drivers/sdcard"
)
const consoleBufLen = 64
const storageBufLen = 512
var (
debug = false
input [consoleBufLen]byte
store [storageBufLen]byte
console = machine.UART0
dev *sdcard.Device
commands map[string]cmdfunc = map[string]cmdfunc{
"": cmdfunc(noop),
"dbg": cmdfunc(dbg),
"erase": cmdfunc(erase),
"lsblk": cmdfunc(lsblk),
"write": cmdfunc(write),
"xxd": cmdfunc(xxd),
}
his history
)
type history struct {
buf [32]string
wp int
idx int
}
func (h *history) Add(cmd string) {
if len(cmd) == 0 {
h.idx = h.wp
return
}
if h.wp == len(h.buf)-1 {
for i := 1; i < len(h.buf); i++ {
h.buf[i-1] = h.buf[i]
}
h.wp--
}
h.buf[h.wp] = cmd
h.wp++
h.idx = h.wp
}
func (h *history) PeekPrev() string {
if h.idx > 0 {
h.idx--
}
return h.buf[h.idx]
}
func (h *history) PeekNext() string {
if h.idx < h.wp {
h.idx++
}
return h.buf[h.idx]
}
type cmdfunc func(argv []string)
const (
StateInput = iota
StateEscape
StateEscBrc
StateCSI
)
func RunFor(device *sdcard.Device) {
dev = device
dev.Configure()
prompt()
var state = StateInput
for i := 0; ; {
if console.Buffered() > 0 {
data, _ := console.ReadByte()
if debug {
fmt.Printf("\rdata: %x, his.idx: %d\r\n\r", data, his.idx)
prompt()
console.Write(input[:i])
}
switch state {
case StateInput:
switch data {
case 0x8:
fallthrough
case 0x7f: // this is probably wrong... works on my machine tho :)
// backspace
if i > 0 {
i -= 1
console.Write([]byte{0x8, 0x20, 0x8})
}
case 13:
// return key
console.Write([]byte("\r\n"))
runCommand(string(input[:i]))
his.Add(string(input[:i]))
prompt()
i = 0
continue
case 27:
// escape
state = StateEscape
default:
// anything else, just echo the character if it is printable
if strconv.IsPrint(rune(data)) {
if i < (consoleBufLen - 1) {
console.WriteByte(data)
input[i] = data
i++
}
}
}
case StateEscape:
switch data {
case 0x5b:
state = StateEscBrc
default:
state = StateInput
}
case StateEscBrc:
switch data {
case 0x41:
// up
println()
prompt()
cmd := his.PeekPrev()
i = len(cmd)
copy(input[:i], []byte(cmd))
console.Write(input[:i])
state = StateInput
case 0x42:
//down
println()
prompt()
cmd := his.PeekNext()
i = len(cmd)
copy(input[:i], []byte(cmd))
console.Write(input[:i])
state = StateInput
default:
// TODO: handle escape sequences
state = StateInput
}
default:
// TODO: handle escape sequences
state = StateInput
}
} else {
time.Sleep(1 * time.Millisecond)
}
}
}
func runCommand(line string) {
argv := strings.SplitN(strings.TrimSpace(line), " ", -1)
cmd := argv[0]
cmdfn, ok := commands[cmd]
if !ok {
println("unknown command: " + line)
return
}
cmdfn(argv)
}
func noop(argv []string) {}
func dbg(argv []string) {
if debug {
debug = false
println("Console debbuging off")
} else {
debug = true
println("Console debbuging on")
}
}
func lsblk(argv []string) {
csd := make([]byte, 16)
err := dev.ReadCSD(csd)
if err != nil {
fmt.Printf("%s\r\n", err.Error())
return
}
sectors := int64(0)
if csd[0]&0xC0 == 0x40 {
// CSD version 2.0
sectors = ((((int64(csd[7]) & 0x3F) << 16) | int64(csd[8])<<8 | int64(csd[9])) + 1) * 1024
} else if csd[0]&0xC0 == 0x00 {
// CSD version 1.0 (old, <=2GB)
c_size := int64(csd[6])&0b11 | int64(csd[7])<<2 | (int64(csd[8])&0b11000000)<<4
c_size_mult := ((int64(csd[9]) & 0b11) << 1) | int64(csd[10])>>7
sectors = (c_size + 1) * (2 << ((c_size_mult + 2) - 1))
} else {
fmt.Printf("SD card CSD format not supported\r\n")
return
}
cid, err := dev.ReadCID()
if err != nil {
fmt.Printf("%s\r\n", err.Error())
return
}
fmt.Printf(
"\n-------------------------------------\r\n"+
" Device Information: \r\n"+
"-------------------------------------\r\n"+
" JEDEC ID: %v\r\n"+
" Serial: %v\r\n"+
" Status 1: %02x\r\n"+
" Status 2: %02x\r\n"+
" \r\n"+
" Max clock speed (MHz): %d\r\n"+
" Has Sector Protection: %t\r\n"+
" Supports Fast Reads: %t\r\n"+
" Supports QSPI Reads: %t\r\n"+
" Supports QSPI Write: %t\r\n"+
" Write Status Split: %t\r\n"+
" Single Status Byte: %t\r\n"+
"-Sectors: %d\r\n"+
"-Bytes (Sectors * 512) %d\r\n"+
"-ManufacturerID %02X\r\n"+
"-OEMApplicationID %04X\r\n"+
"-ProductName %s\r\n"+
"-ProductVersion %s\r\n"+
"-ProductSerialNumber %08X\r\n"+
"-ManufacturingYear %02X\r\n"+
"-ManufacturingMonth %02X\r\n"+
"-Always1 %d\r\n"+
"-CRC %02X\r\n"+
"-------------------------------------\r\n\r\n",
"attrs.JedecID", // attrs.JedecID,
cid.ProductSerialNumber, // serialNumber1,
0, // status1,
0, // status2,
csd[3], // attrs.MaxClockSpeedMHz,
false, // attrs.HasSectorProtection,
false, // attrs.SupportsFastRead,
false, // attrs.SupportsQSPI,
false, // attrs.SupportsQSPIWrites,
false, // attrs.WriteStatusSplit,
false, // attrs.SingleStatusByte,
sectors,
int64(sectors)*512,
cid.ManufacturerID,
cid.OEMApplicationID,
cid.ProductName,
cid.ProductVersion,
cid.ProductSerialNumber,
cid.ManufacturingYear,
cid.ManufacturingMonth,
cid.Always1,
cid.CRC,
)
}
func erase(argv []string) {
fmt.Printf("erase - not impl\r\n")
}
func write(argv []string) {
if len(argv) < 3 {
println("usage: write <hex offset> <bytes>")
return
}
var err error
var addr uint64 = 0x0
if addr, err = strconv.ParseUint(argv[1], 16, 32); err != nil {
println("Invalid address: " + err.Error() + "\r\n")
return
}
//buf := []byte(argv[2])
buf := []byte{}
//println("argv[2] : " + argv[2] + "\r\n")
for i := 0; i < len(argv[2]); i += 2 {
//fmt.Printf("argv[2][%d:%d] : %q\r\n", i, i+2, argv[2][i:i+2])
var b uint64
if b, err = strconv.ParseUint(argv[2][i:i+2], 16, 8); err != nil {
println("Invalid bytes: " + err.Error() + "\r\n")
return
}
buf = append(buf, byte(b))
}
if _, err = dev.WriteAt(buf, int64(addr)); err != nil {
println("Write error: " + err.Error() + "\r\n")
}
}
func xxd(argv []string) {
var err error
var addr uint64 = 0x0
var size int = 64
switch len(argv) {
case 3:
if size, err = strconv.Atoi(argv[2]); err != nil {
println("Invalid size argument: " + err.Error() + "\r\n")
return
}
if size > storageBufLen || size < 1 {
fmt.Printf("Size of hexdump must be greater than 0 and less than %d\r\n", storageBufLen)
return
}
fallthrough
case 2:
if addr, err = strconv.ParseUint(argv[1], 16, 32); err != nil {
println("Invalid address: " + err.Error() + "\r\n")
return
}
fallthrough
case 1:
// no args supplied, so nothing to do here, just use the defaults
default:
println("usage: xxd <hex address, ex: 0xA0> <size of hexdump in bytes>\r\n")
return
}
buf := store[0:size]
dev.ReadAt(buf, int64(addr))
xxdfprint(os.Stdout, uint32(addr), buf)
}
func xxdfprint(w io.Writer, offset uint32, b []byte) {
var l int
var buf16 = make([]byte, 16)
for i, c := 0, len(b); i < c; i += 16 {
l = i + 16
if l >= c {
l = c
}
fmt.Fprintf(w, "%08x: % x ", offset+uint32(i), b[i:l])
for j, n := 0, l-i; j < 16; j++ {
if j >= n || !strconv.IsPrint(rune(b[i+j])) {
buf16[j] = '.'
} else {
buf16[j] = b[i+j]
}
}
console.Write(buf16)
println()
}
}
func prompt() {
print("==> ")
}
package main
import (
"fmt"
"machine"
"time"
"tinygo.org/x/drivers/sdcard"
)
func main() {
led := machine.LED
led.Configure(machine.PinConfig{Mode: machine.PinOutput})
machine.SPI2.Configure(machine.SPIConfig{
SCK: machine.SCK2,
MOSI: machine.MOSI2,
MISO: machine.MISO2,
Frequency: 100000,
LSBFirst: false,
Mode: 0, // phase=0, polarity=0
})
sd := sdcard.New(machine.SPI2, machine.SS2)
err := sd.Configure()
if err != nil {
fmt.Printf("%s\r\n", err.Error())
for {
time.Sleep(time.Hour)
}
}
go RunFor(&sd)
for {
led.Toggle()
time.Sleep(200 * time.Millisecond)
}
}
package sdcard
import (
"fmt"
"machine"
"time"
)
const (
_CMD_TIMEOUT = 100
_R1_IDLE_STATE = 1 << 0
_R1_ERASE_RESET = 1 << 1
_R1_ILLEGAL_COMMAND = 1 << 2
_R1_COM_CRC_ERROR = 1 << 3
_R1_ERASE_SEQUENCE_ERROR = 1 << 4
_R1_ADDRESS_ERROR = 1 << 5
_R1_PARAMETER_ERROR = 1 << 6
_TOKEN_CMD25 = 0xFC
_TOKEN_STOP_TRAN = 0xFD
_TOKEN_DATA = 0xFE
// card types
/** Standard capacity V1 SD card */
SD_CARD_TYPE_SD1 = 1
/** Standard capacity V2 SD card */
SD_CARD_TYPE_SD2 = 2
/** High Capacity SD card */
SD_CARD_TYPE_SDHC = 3
)
var (
block_ uint32
inBlock_ uint32
offset_ uint16
partialBlockRead_ uint32
)
type Config struct {
baudrate uint32
}
type Device struct {
bus machine.SPI
cs machine.Pin
cmdbuf []byte
dummybuf []byte
tokenbuf []byte
dummybufMemoryView []byte
baudrate uint32
cdv uint32
sectors uint32
sdCardType byte
}
func New(b machine.SPI, cs machine.Pin) Device {
dummybuf := make([]byte, 512)
for i := range dummybuf {
dummybuf[i] = 0xFF
}
dummybufMemoryView := dummybuf
return Device{
bus: b,
cs: cs,
cmdbuf: make([]byte, 6),
dummybuf: dummybuf,
tokenbuf: make([]byte, 1),
dummybufMemoryView: dummybufMemoryView,
cdv: 0,
sectors: 0,
sdCardType: 0,
}
}
func (d Device) Configure() error {
return d.initCard()
}
//func (d Device) initSpi(config machine.SPIConfig) error {
// return d.bus.Configure(machine.SPIConfig{
// SCK: config.SCK,
// MOSI: config.MOSI,
// MISO: config.MISO,
// Frequency: config.Frequency,
// LSBFirst: false,
// Mode: 0, // phase=0, polarity=0
// })
//}
func (d Device) initCard() error {
// set pin modes
d.cs.Configure(machine.PinConfig{Mode: machine.PinOutput})
d.cs.High()
//// init SPI bus; use low data rate for initialisation
//d.initSpi(machine.SPIConfig{
// //SCK: machine.SCK, // TODO
// //MOSI: machine.MOSI, // TODO
// //MISO: machine.MISO, // TODO
// SCK: machine.SCK2, // TODO
// MOSI: machine.MOSI2, // TODO
// MISO: machine.MISO2, // TODO
// Frequency: 100000,
//})
// clock card at least 100 cycles with cs high
for i := 0; i < 16; i++ {
d.bus.Transfer(byte(0xFF))
}
d.cs.Low()
// CMD0: init card; sould return _R1_IDLE_STATE (allow 5 attempts)
ok := false
for i := 0; i < 2000; i++ {
// Arduino に合わせて約 2 sec まで実施する
if d.cmd(0, 0, 0x95) == _R1_IDLE_STATE {
ok = true
break
}
time.Sleep(1 * time.Millisecond)
}
if !ok {
return fmt.Errorf("no SD card")
}
// CMD8: determine card version
r := d.cmd(8, 0x01AA, 0x87)
if (r & _R1_ILLEGAL_COMMAND) == _R1_ILLEGAL_COMMAND {
d.sdCardType = SD_CARD_TYPE_SD1
return fmt.Errorf("init_card_v1 not impl\r\n")
} else {
// r7 response
status := byte(0)
for i := 0; i < 3; i++ {
var err error
status, err = d.bus.Transfer(byte(0xFF))
if err != nil {
return err
}
}
if (status & 0x0F) != 0x01 {
return fmt.Errorf("SD_CARD_ERROR_CMD8 %02X", status)
}
for i := 3; i < 4; i++ {
var err error
status, err = d.bus.Transfer(byte(0xFF))
if err != nil {
return err
}
}
if status != 0xAA {
return fmt.Errorf("SD_CARD_ERROR_CMD8 %02X", status)
}
d.sdCardType = SD_CARD_TYPE_SD2
}
// initialize card and send host supports SDHC if SD2
arg := uint32(0)
if d.sdCardType == SD_CARD_TYPE_SD2 {
arg = 0x40000000
}
// check for timeout
ok = false
for i := 0; i < 2000; i++ {
if d.cardAcmd(41, arg) == 0 {
ok = true
break
}
time.Sleep(1 * time.Millisecond)
}
if !ok {
return fmt.Errorf("SD_CARD_ERROR_ACMD41")
}
// if SD2 read OCR register to check for SDHC card
if d.sdCardType == SD_CARD_TYPE_SD2 {
if d.cmd(58, 0, 0) != 0 {
return fmt.Errorf("SD_CARD_ERROR_CMD58")
}
status, err := d.bus.Transfer(byte(0xFF))
if err != nil {
return err
}
if (status & 0xC0) == 0xC0 {
d.sdCardType = SD_CARD_TYPE_SDHC
}
// discard rest of ocr - contains allowed voltage range
for i := 1; i < 4; i++ {
d.bus.Transfer(byte(0xFF))
}
}
if d.sdCardType == SD_CARD_TYPE_SD1 {
fmt.Printf("SD_CARD_TYPE_SD1\r\n")
} else if d.sdCardType == SD_CARD_TYPE_SD2 {
fmt.Printf("SD_CARD_TYPE_SD2\r\n")
} else if d.sdCardType == SD_CARD_TYPE_SDHC {
fmt.Printf("SD_CARD_TYPE_SDHC\r\n")
} else {
fmt.Printf("SD_CARD_TYPE_ERROR\r\n")
}
d.cs.High()
return nil
}
func (d Device) CardSize() error {
return nil
}
func (d Device) cardAcmd(cmd byte, arg uint32) byte {
d.cmd(55, 0, 0)
return d.cmd(cmd, arg, 0)
}
func (d Device) cmd(cmd byte, arg uint32, crc byte) byte {
d.readEnd()
d.cs.Low()
d.waitNotBusy(300)
// create and send the command
buf := d.cmdbuf
buf[0] = 0x40 | cmd
buf[1] = byte(arg >> 24)
buf[2] = byte(arg >> 16)
buf[3] = byte(arg >> 8)
buf[4] = byte(arg)
buf[5] = crc
d.bus.Tx(buf, nil)
// wait for the response (response[7] == 0)
for i := 0; i < 0xFFFF; i++ {
d.bus.Tx([]byte{0xFF}, d.tokenbuf)
response := d.tokenbuf[0]
if (response & 0x80) == 0 {
return response
}
}
// TODO
//// timeout
d.cs.High()
d.bus.Transfer(byte(0xFF))
return 0xFF // -1
}
func (d Device) readEnd() {
if inBlock_ > 0 {
for {
offset_++
if offset_ < 514 {
break
}
d.bus.Transfer(byte(0xFF))
}
d.cs.High()
inBlock_ = 0
}
}
func (d Device) waitNotBusy(timeoutMs int) error {
for i := 0; i < timeoutMs; i++ {
r, err := d.bus.Transfer(byte(0xFF))
if err != nil {
return err
}
if r == 0xFF {
return nil
}
time.Sleep(1 * time.Millisecond)
}
return nil
}
func (d Device) waitStartBlock() error {
status := byte(0xFF)
for i := 0; i < 300; i++ {
var err error
status, err = d.bus.Transfer(byte(0xFF))
if err != nil {
d.cs.High()
return err
}
if status != 0xFF {
break
}
time.Sleep(1 * time.Millisecond)
}
if status != 254 {
d.cs.High()
return fmt.Errorf("SD_CARD_START_BLOCK")
}
return nil
}
func (d Device) readinto(buf []byte) error {
//d.cs.Low()
//// read until start byte (0xff)
//ok := false
//for i := 0; i < _CMD_TIMEOUT; i++ {
// d.bus.Tx([]byte{0xFF}, d.tokenbuf)
// if d.tokenbuf[0] == _TOKEN_DATA {
// ok = true
// break
// }
//}
//if !ok {
// d.cs.High()
// fmt.Printf("timeout waiting for response\r\n")
// return fmt.Errorf("timeout waiting for response")
//}
//// read data
//mv := d.dummybufMemoryView // TODO: 初期化
//if len(buf) != len(mv) {
// mv = mv[:len(buf)]
//}
//d.bus.Tx(mv, buf)
//// read checksum
//d.bus.Transfer(byte(0xFF))
//d.bus.Transfer(byte(0xFF))
//d.cs.High()
//d.bus.Transfer(byte(0xFF))
return nil
}
func (d Device) Write(token byte, buf []byte) error {
d.cs.Low()
d.bus.Transfer(token)
d.bus.Tx(buf, nil)
d.bus.Transfer(byte(0xFF))
d.bus.Transfer(byte(0xFF))
// check the response
b, err := d.bus.Transfer(0xFF)
if err != nil {
return err
}
if (b & 0x1F) != 0x05 {
d.cs.High()
d.bus.Transfer(byte(0xFF))
return nil
}
// wait for write to finish
for {
b, err := d.bus.Transfer(0xFF)
if err != nil {
return err
}
if b != 0x00 {
break
}
}
d.cs.High()
d.bus.Transfer(byte(0xFF))
return nil
}
func (d Device) writeToken() {
}
func (d Device) Erase(firstBlock, lastBlock uint32) error {
if !d.eraseSingleBlockEnable() {
return fmt.Errorf("SD_CARD_ERROR_ERASE_SINGLE_BLOCK")
}
if d.sdCardType != SD_CARD_TYPE_SDHC {
firstBlock <<= 9
lastBlock <<= 9
}
if d.cmd(32, firstBlock, 0) != 0 ||
d.cmd(33, lastBlock, 0) != 0 ||
d.cmd(38, 0, 0) != 0 {
return fmt.Errorf("SD_CARD_ERROR_ERASE")
}
err := d.waitNotBusy(10000)
if err != nil {
return err
}
d.cs.High()
return nil
}
func (d Device) ReadCSD(csd []byte) error {
return d.readRegister(9, csd)
}
func (d Device) ReadCID(csd []byte) error {
return d.readRegister(10, csd)
}
func (d Device) eraseSingleBlockEnable() bool {
//csd := make([]byte, 16)
//err := d.readCSD(csd)
//if err != nil {
// return false
//}
return true
}
func (d Device) readRegister(cmd uint8, dst []byte) error {
if d.cmd(cmd, 0, 0) != 0 {
return fmt.Errorf("SD_CARD_ERROR_READ_REG")
}
if err := d.waitStartBlock(); err != nil {
return err
}
// transfer data
for i := uint16(0); i < 16; i++ {
r, err := d.bus.Transfer(byte(0xFF))
if err != nil {
return err
}
dst[i] = r
}
d.bus.Transfer(byte(0xFF))
d.bus.Transfer(byte(0xFF))
d.cs.High()
return nil
}
func (d Device) ReadBlock(block uint32, dst []byte) error {
return d.ReadData(block, 0, 512, dst)
}
func (d Device) ReadData(block uint32, offset, count uint16, dst []byte) error {
if count == 0 {
return nil
}
if (count + offset) > 512 {
return fmt.Errorf("count + offset > 512")
}
//if inBlock_ != 0 || block != block_ || offset < offset_
{
fmt.Printf("CMD17\r\n")
block_ = block
// use address if not SDHC card
if d.sdCardType != SD_CARD_TYPE_SDHC {
block <<= 9
}
if d.cmd(17, block, 0) != 0 {
return fmt.Errorf("CMD17 error")
}
if err := d.waitStartBlock(); err != nil {
return fmt.Errorf("waitStartBlock()")
}
offset_ = 0
inBlock_ = 1
}
// skip data before offset
for ; offset_ < offset; offset_++ {
d.bus.Transfer(byte(0xFF))
}
// transfer data
for i := uint16(0); i < count; i++ {
r, err := d.bus.Transfer(byte(0xFF))
if err != nil {
return err
}
dst[i] = r
}
fmt.Printf("CMD17 read done\r\n")
offset_ += count
if partialBlockRead_ > 0 || offset_ >= 512 {
d.readEnd()
}
return nil
}
func (d Device) WriteBlock(blockNumber uint32, src []byte, blocking bool) error {
// #if SD_PROTECT_BLOCK_ZERO
if blockNumber == 0 {
return fmt.Errorf("SD_CARD_ERROR_WRITE_BLOCK_ZERO")
}
// #endif
if d.sdCardType != SD_CARD_TYPE_SDHC {
blockNumber <<= 9
}
if d.cmd(24, blockNumber, 0) != 0 {
return fmt.Errorf("SD_CARD_ERROR_CMD24")
}
err := d.writeData_(254, src)
if err != nil {
return err
}
if blocking {
err := d.waitNotBusy(600)
if err != nil {
return fmt.Errorf("SD_CARD_ERROR_WRITE_TIMEOUT")
}
if d.cmd(13, 0, 0) == 0 {
// ok
r, err := d.bus.Transfer(byte(0xFF))
if err != nil {
return err
}
if r > 0 {
fmt.Errorf("SD_CARD_ERROR_WRITE_PROGRAMMING")
}
} else {
fmt.Errorf("SD_CARD_ERROR_WRITE_PROGRAMMING")
}
}
d.cs.High()
return nil
}
func (d Device) writeData(src []byte) error {
err := d.waitNotBusy(600)
if err != nil {
d.cs.High()
return fmt.Errorf("SD_CARD_ERROR_WRITE_MULTIPLE")
}
return d.writeData_(252, src)
}
func (d Device) writeData_(token byte, src []byte) error {
d.bus.Transfer(token)
for i := 0; i < 512; i++ {
d.bus.Transfer(src[i])
}
d.bus.Transfer(byte(0xFF))
d.bus.Transfer(byte(0xFF))
r, err := d.bus.Transfer(byte(0xFF))
if err != nil {
return err
}
if (r & 0x1F) != 0x05 {
return fmt.Errorf("SD_CARD_ERROR_WRITE")
}
return nil
}
func (d Device) WriteStart(blockNumber uint32, eraseCount uint32) error {
// #if SD_PROTECT_BLOCK_ZERO
if blockNumber == 0 {
d.cs.High()
return fmt.Errorf("SD_CARD_ERROR_WRITE_BLOCK_ZERO")
}
// #endif
// send pre-erase count
if d.cardAcmd(23, eraseCount) != 0 {
d.cs.High()
return fmt.Errorf("SD_CARD_ERROR_ACMD23")
}
// use address if not SDHC card
if d.sdCardType != SD_CARD_TYPE_SDHC {
blockNumber <<= 9
}
if d.cmd(25, blockNumber, 0) != 0 {
d.cs.High()
return fmt.Errorf("SD_CARD_ERROR_CMD25")
}
return nil
}
func (d Device) WriteStop() error {
err := d.waitNotBusy(600)
if err != nil {
return nil
}
d.bus.Transfer(253)
err = d.waitNotBusy(600)
if err != nil {
return nil
}
d.cs.High()
return nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment