diff --git a/sd/blockdevice.go b/sd/blockdevice.go index f3cabac..d5cd5c4 100644 --- a/sd/blockdevice.go +++ b/sd/blockdevice.go @@ -15,6 +15,9 @@ var _ Card = (*SPICard)(nil) var _ io.ReaderAt = (*BlockDevice)(nil) var _ io.WriterAt = (*BlockDevice)(nil) +// Card is the interface implemented by SD card drivers such as [SPICard]. +// It provides block-aligned I/O over the card's contents. Use [NewBlockDevice] +// to wrap a Card with byte-addressed [io.ReaderAt] and [io.WriterAt] interfaces. type Card interface { // WriteBlocks writes the given data to the card, starting at the given block index. // The data must be a multiple of the block size. @@ -26,7 +29,9 @@ type Card interface { EraseBlocks(startBlock, numBlocks int64) error } -// NewBlockDevice creates a new BlockDevice from a Card. +// NewBlockDevice creates a new [BlockDevice] from a Card. blockSize must be a +// power of 2. For an initialized [SPICard], blockSize is typically the CSD's +// [CSD.ReadBlockLen] and numBlocks is [SPICard.NumberOfBlocks]. func NewBlockDevice(card Card, blockSize int, numBlocks int64) (*BlockDevice, error) { if card == nil || blockSize <= 0 || numBlocks <= 0 { return nil, errors.New("invalid argument(s)") @@ -44,7 +49,10 @@ func NewBlockDevice(card Card, blockSize int, numBlocks int64) (*BlockDevice, er return bd, nil } -// BlockDevice implements tinyfs.BlockDevice interface for an [sd.Card] type. +// BlockDevice implements the tinyfs.BlockDevice interface for a [Card], +// providing byte-addressed reads and writes at arbitrary offsets by buffering +// non-block-aligned accesses through an internal single-block buffer. +// BlockDevice is not safe for concurrent use. type BlockDevice struct { card Card blockbuf []byte @@ -52,7 +60,8 @@ type BlockDevice struct { numblocks int64 } -// ReadAt implements [io.ReadAt] interface for an SD card. +// ReadAt implements the [io.ReaderAt] interface for an SD card. +// Reads need not be aligned to block boundaries. func (bd *BlockDevice) ReadAt(p []byte, off int64) (n int, err error) { if off < 0 { return 0, errNegativeOffset @@ -93,7 +102,9 @@ func (bd *BlockDevice) ReadAt(p []byte, off int64) (n int, err error) { return n, nil } -// WriteAt implements [io.WriterAt] interface for an SD card. +// WriteAt implements the [io.WriterAt] interface for an SD card. Writes need +// not be aligned to block boundaries: partial blocks are read, modified and +// written back. func (bd *BlockDevice) WriteAt(p []byte, off int64) (n int, err error) { if off < 0 { return 0, errNegativeOffset @@ -174,6 +185,8 @@ type blkIdxer struct { blockmask int64 } +// makeBlockIndexer returns a blkIdxer for the given block size, +// which must be a power of 2. func makeBlockIndexer(blockSize int) (blkIdxer, error) { if blockSize <= 0 { return blkIdxer{}, errNoblocks diff --git a/sd/card_test.go b/sd/card_test.go index 2192c65..9fbad85 100644 --- a/sd/card_test.go +++ b/sd/card_test.go @@ -85,6 +85,89 @@ func TestCRC7(t *testing.T) { } } +// TestCSDv2Capacity checks CSDv2 C_SIZE decoding and the capacity formula. +// +// Field layout and formula are from the SD Physical Layer Simplified +// Specification Version 9.10, section 5.3.3 "CSD Register (CSD Version 2.0)": +// C_SIZE occupies CSD bits [69:48] and user memory capacity is +// +// memory capacity = (C_SIZE+1) * 512KByte (512KByte = 524288 bytes) +// +// Specification download: https://www.sdcard.org/downloads/pls/ +// +// Regression test for two past bugs in CSDv2: +// - csize() read byte 7 as data[7]>>2 instead of data[7]&0x3F, dropping +// C_SIZE bits [17:16] (misdecoded cards > 32GiB). +// - DeviceCapacity() computed csize*512000 instead of (csize+1)*524288. +func TestCSDv2Capacity(t *testing.T) { + tests := []struct { + name string + csd []byte + wantCSize uint32 + wantCap int64 + }{ + { + // Same 8GB-card register as in TestCRC7 above, CRC byte appended + // (CRC7=0b1110010 per that test, stored as crc<<1|always1). + // C_SIZE = 0x003C01 = 15361 -> 15362 * 524288 = 8054112256 bytes. + name: "8GB card (in-repo vector)", + csd: []byte{64, 14, 0, 50, 83, 89, 0, 0, 60, 1, 127, 128, 10, 64, 0, 0b1110010<<1 | 1}, + wantCSize: 0x003C01, + wantCap: 8054112256, + }, + // { + // name: "8GB card", + // csd: csdv2Bytes(0x003C01), + // wantCSize: 0x003FFF, + // wantCap: 2 << 40, + // }, + { + // Synthetic: C_SIZE = 0x01FFFF -> 131072 * 524288 = 64GiB. + // Exercises C_SIZE bits [17:16], stored in CSD byte 7 (CSD bits [65:64]). + name: "64GiB synthetic", + csd: csdv2Bytes(0x01FFFF), + wantCSize: 0x01FFFF, + wantCap: 64 << 30, + }, + { + // Synthetic: maximum v2 C_SIZE = 0x3FFFFF -> 4194304 * 524288 = 2TiB, + // the SDXC upper bound per section 5.3.3. + name: "2TiB synthetic (max C_SIZE)", + csd: csdv2Bytes(0x3FFFFF), + wantCSize: 0x3FFFFF, + wantCap: 2 << 40, + }, + } + for _, tt := range tests { + csd, err := DecodeCSD(tt.csd) + if err != nil { + t.Fatalf("%s: DecodeCSD: %v", tt.name, err) + } + v2 := csd.MustV2() + if got := v2.csize(); got != tt.wantCSize { + t.Errorf("%s: csize() = %#x, want %#x", tt.name, got, tt.wantCSize) + } + if got := csd.DeviceCapacity(); got != tt.wantCap { + t.Errorf("%s: DeviceCapacity() = %d, want %d", tt.name, got, tt.wantCap) + } + wantBlocks := tt.wantCap / int64(csd.ReadBlockLen()) + if got := csd.NumberOfBlocks(); got != wantBlocks { + t.Errorf("%s: NumberOfBlocks() = %d, want %d", tt.name, got, wantBlocks) + } + } +} + +// csdv2Bytes returns a 16-byte CSD v2 register with csize spliced into +// CSD bits [69:48] and a freshly computed CRC7+always1 last byte. +// Non-capacity fields are copied from the 8GB-card vector above. +func csdv2Bytes(csize uint32) []byte { + b := []byte{64, 14, 0, 50, 83, 89, 0, 0, 60, 1, 127, 128, 10, 64, 0} + b[7] = byte(csize >> 16 & 0x3F) + b[8] = byte(csize >> 8) + b[9] = byte(csize) + return append(b, crc7noshift(b)|1) +} + func putCmd(dst []byte, cmd command, arg uint32) { dst[0] = byte(cmd) | (1 << 6) dst[1] = byte(arg >> 24) diff --git a/sd/definitions.go b/sd/definitions.go index f58d3ed..52061d5 100644 --- a/sd/definitions.go +++ b/sd/definitions.go @@ -11,8 +11,11 @@ import ( // For reference of CID/CSD structs see: // See https://github.com/arduino-libraries/SD/blob/1c56f58252553c7537f7baf62798cacc625aa543/src/utility/SdInfo.h#L110 +// CardKind classifies an SD card by its capacity class and specification +// version, as discovered during card initialization. type CardKind uint8 +// isTimeout reports whether err is one of the package's timeout errors. func isTimeout(err error) bool { return err == errReadTimeout || err == errWriteTimeout || err == errBusyTimeout } @@ -24,10 +27,16 @@ const ( TypeSDHC CardKind = 3 // High Capacity SD card ) +// CID is the Card Identification register, a 128-bit (16-byte) read-only +// register holding the card's identification information: manufacturer, +// product name, serial number and manufacturing date, among other data. +// It is programmed during card manufacture and cannot be changed. type CID struct { data [16]byte } +// DecodeCID decodes a CID from the first 16 bytes of b. It returns an error +// if b is too short or if the CRC7/always-1 fields are invalid. func DecodeCID(b []byte) (cid CID, _ error) { if len(b) < 16 { return CID{}, io.ErrShortBuffer @@ -52,7 +61,7 @@ func (c *CID) OEMApplicationID() uint16 { return binary.BigEndian.Uint16(c.data[1:3]) } -// The product name is a string, 5-character ASCII string. +// ProductName returns the product name, an ASCII string of up to 5 characters. func (c *CID) ProductName() string { return string(upToNull(c.data[3:8])) } @@ -65,12 +74,13 @@ func (c *CID) ProductRevision() (n, m uint8) { return rev >> 4, rev & 0x0F } -// The Serial Number is 32 bits of binary number. +// ProductSerialNumber returns the product serial number, a 32-bit binary number. func (c *CID) ProductSerialNumber() uint32 { return binary.BigEndian.Uint32(c.data[9:13]) } -// ManufacturingDate returns the manufacturing date of the card. +// ManufacturingDate returns the manufacturing date of the card, +// e.g. year=2023, month=4 for April 2023. func (c *CID) ManufacturingDate() (year uint16, month uint8) { date := binary.BigEndian.Uint16(c.data[13:15]) return (date >> 4) + 2000, uint8(date & 0x0F) @@ -129,6 +139,7 @@ func (c CSD) MustV1() CSDv1 { return CSDv1{CSD: c} } +// MustV2 returns the CSD as a CSDv2. Panics if the CSD is not version 2.0. func (c CSD) MustV2() CSDv2 { if c.csdStructure() != 1 { panic("CSD is not version 2.0") @@ -136,6 +147,7 @@ func (c CSD) MustV2() CSDv2 { return CSDv2{CSD: c} } +// RawCopy returns a copy of the raw CSD data. func (c *CSD) RawCopy() [16]byte { return c.data } // TAAC returns the Time Access Attribute Class (data read access-time-1). @@ -147,17 +159,20 @@ func (c *CSD) NSAC() NSAC { return NSAC(c.data[2]) } // TransferSpeed returns the Max Data Transfer Rate. Either 0x32 or 0x5A. func (c *CSD) TransferSpeed() TransferSpeed { return TransferSpeed(c.data[3]) } -// CommandClasses returns the supported Card Command Classes. -// This is a bitfield, each bit position indicates whether the +// CommandClasses returns the supported Card Command Classes as a bitfield; +// bit position i set means command class i is supported by the card. func (c *CSD) CommandClasses() CommandClasses { return CommandClasses(uint16(c.data[4])<<4 | uint16(c.data[5]&0xf0)>>4) } // ReadBlockLen returns the Max Read Data Block Length in bytes. -func (c *CSD) ReadBlockLen() int { return 1 << c.ReadBlockLenShift() } +func (c *CSD) ReadBlockLen() int { return 1 << c.ReadBlockLenShift() } + +// ReadBlockLenShift returns the base-2 logarithm of [CSD.ReadBlockLen] (READ_BL_LEN field). func (c *CSD) ReadBlockLenShift() uint8 { return c.data[5] & 0x0F } -// AllowsReadBlockPartial should always return true. Indicates that +// AllowsReadBlockPartial indicates that partial block reads (down to a +// single byte) are allowed. Always true for SD cards. func (c *CSD) AllowsReadBlockPartial() bool { return c.data[6]&(1<<7) != 0 } // AllowsWriteBlockMisalignment defines if the data block to be written by one command @@ -183,15 +198,18 @@ func (c *CSD) IsValid() bool { // ImplementsDSR defines if the configurable driver stage is integrated on the card. func (c *CSD) ImplementsDSR() bool { return c.data[6]&(1<<4) != 0 } -// EraseSectorSizeInBlocks represents how much memory is erased in an erase -// command in multiple of block size. +// EraseSectorSizeInBytes returns how much memory is erased by a single +// erase command, in bytes (SectorSize multiplied by the write block length). func (c *CSDv1) EraseSectorSizeInBytes() int64 { blklen := c.WriteBlockLen() numblocks := c.SectorSize() return int64(numblocks) * blklen } -// SectorSize varies in meaning depending on the version. +// SectorSize returns the size of an erasable sector in units of write blocks +// (SECTOR_SIZE field, range 1..128). Its meaning varies with the CSD version: +// for V1 it is the erase unit when [CSD.EraseBlockEnabled] is false; for V2 +// it is fixed to 64KiB and does not reflect the real erase unit. func (c *CSD) SectorSize() uint8 { return 1 + ((c.data[10]&0b11_1111)<<1 | (c.data[11] >> 7)) } @@ -200,6 +218,8 @@ func (c *CSD) SectorSize() uint8 { // If enabled the erase operation can erase either one or multiple units of 512 bytes. func (c *CSD) EraseBlockEnabled() bool { return (c.data[10]>>6)&1 != 0 } +// ReadToWriteFactor returns the typical write time as a power-of-2 multiple of +// the read access time (R2W_FACTOR field), i.e. writeTime = readTime << factor. func (c *CSD) ReadToWriteFactor() uint8 { return (c.data[12] >> 2) & 0b111 } // WriteProtectGroupSizeInSectors indicates the size of a write protected @@ -231,8 +251,12 @@ func (c *CSD) PermWriteProtected() bool { return c.data[14]&(1<<5) != 0 } // IsCopy whether contents are original or have been copied. func (c *CSD) IsCopy() bool { return c.data[14]&(1<<6) != 0 } +// FileFormatGroup returns the file format group bit, which selects between +// the two [FileFormat] tables. Interpret together with [CSD.FileFormat]. func (c *CSD) FileFormatGroup() bool { return c.data[14]&(1<<7) != 0 } +// DeviceCapacity returns the total device capacity in bytes, dispatching on +// the CSD version. Returns 0 for unknown CSD versions. func (c *CSD) DeviceCapacity() (size int64) { switch c.csdStructure() { case 0: @@ -256,14 +280,16 @@ func (c *CSD) NumberOfBlocks() (numBlocks int64) { // After byte 5 CSDv1 and CSDv2 differ in structure at some fields. -// DeviceCapacity returns the device capacity in bytes. +// DeviceCapacity returns the device capacity in bytes: +// (C_SIZE+1) * 512KiB, as per section 5.3.3 of the SD Simplified Specification. func (c *CSDv2) DeviceCapacity() int64 { csize := c.csize() - return int64(csize) * 512_000 + return (int64(csize) + 1) * (512 * 1024) } +// csize returns the 22-bit C_SIZE field (CSD bits 69:48). func (c *CSDv2) csize() uint32 { - return uint32(c.data[7]>>2)<<16 | uint32(c.data[8])<<8 | uint32(c.data[9]) + return uint32(c.data[7]&0x3F)<<16 | uint32(c.data[8])<<8 | uint32(c.data[9]) } // DeviceCapacity returns the total memory capacity of the SDCard in bytes. Max is 2GB for V1. @@ -301,6 +327,7 @@ func (c *CSDv1) VddWriteCurrent() (min, max uint8) { return c.data[9] >> 5, (c.data[9] >> 3) & 0b111 } +// String returns a human-readable multi-line summary of the CSD fields. func (c *CSD) String() string { version := c.csdStructure() + 1 if version > 2 { @@ -424,13 +451,23 @@ const ( acmdCHANGE_SECURE_AREA appcommand = 49 ) -// CSD enum types. +// CSD field types. type ( - TransferSpeed uint8 - TAAC uint8 - FileFormat uint8 + // TransferSpeed is the TRAN_SPEED CSD field: the maximum data transfer + // rate encoded as a rate unit (lower 3 bits) and time value multiplier. + TransferSpeed uint8 + // TAAC is the data read access time CSD field, encoded as a time unit + // (lower 3 bits) and time value multiplier. + TAAC uint8 + // FileFormat is the format of the data stored on the card. See the + // FileFmt* constants for possible values. + FileFormat uint8 + // CommandClasses is the CCC CSD field, a bitfield where bit position i + // set means command class i is supported. CommandClasses uint16 - NSAC uint8 + // NSAC is the data read access time 2 CSD field, given in units of + // 100 clock cycles. + NSAC uint8 ) const ( @@ -440,6 +477,7 @@ const ( FileFmtUnknown ) +// String returns a human-readable name for the file format. func (ff FileFormat) String() (s string) { switch ff { case FileFmtPartition: @@ -466,11 +504,12 @@ var log10table = [...]int64{ 1000000, } -// RateMegabits returns the transfer rate in kilobits per second. +// RateKilobits returns the transfer rate in kilobits per second. func (t TransferSpeed) RateKilobits() int64 { return 100 * log10table[t&0b111] } +// AccessTime returns the asynchronous part of the data access time. func (t TAAC) AccessTime() (d time.Duration) { return time.Duration(log10table[t&0b111]) * time.Nanosecond } diff --git a/sd/doc.go b/sd/doc.go new file mode 100644 index 0000000..bbab297 --- /dev/null +++ b/sd/doc.go @@ -0,0 +1,8 @@ +// Package sd implements SD card drivers and the SD card specification: +// CID and CSD register decoding, command definitions and CRC7/CRC16 checksums. +// +// The [SPICard] type drives an SD card over a SPI bus and implements the +// [Card] interface, which exposes block-aligned I/O. [BlockDevice] wraps any +// [Card] with byte-addressed [io.ReaderAt]/[io.WriterAt] implementations +// suitable for filesystem libraries such as tinyfs. +package sd diff --git a/sd/responses.go b/sd/responses.go index 90f74e9..115097b 100644 --- a/sd/responses.go +++ b/sd/responses.go @@ -2,7 +2,6 @@ package sd import ( "encoding/binary" - "strconv" ) const ( @@ -20,6 +19,8 @@ const ( _DATA_RES_ACCEPTED = 0x05 ) +// response1 is the R1 response token returned by the card in SPI mode +// after every command; a bitfield of error and idle-state flags. type response1 uint8 func (r response1) IsIdle() bool { return r&_R1_IDLE_STATE != 0 } @@ -30,17 +31,17 @@ func (r response1) EraseSeqError() bool { return r&_R1_ERASE_SEQUENCE_ERROR != func (r response1) AddressError() bool { return r&_R1_ADDRESS_ERROR != 0 } func (r response1) ParamError() bool { return r&_R1_PARAMETER_ERROR != 0 } +// response1Err wraps a non-zero response1 status as an error. type response1Err struct { context string status response1 } func (e response1Err) Error() string { - return e.status.Response() if e.context != "" { - return "sd:" + e.context + " " + strconv.Itoa(int(e.status)) + return "sd:" + e.context + " " + e.status.Response() } - return "sd:status " + strconv.Itoa(int(e.status)) + return e.status.Response() } func (e response1) Response() string { @@ -96,6 +97,8 @@ const ( tokWRITE_MULT = 0xfc ) +// state is the card state machine state as encoded in the +// CURRENT_STATE bits of the Card Status register (section 4.10.1). type state uint8 const ( @@ -153,7 +156,8 @@ const ( statusAddrOutOfRange // address out of range ) -// r1 is the normal response to a command. +// r1 is the normal 48-bit response to a command in SD-bus mode, +// as per section 4.9.1. It carries the 32-bit Card Status register. type r1 struct { data [48 / 8]byte // 48 bits of response. } @@ -178,6 +182,8 @@ func (r *r1) IsValid() bool { return r.endbit() && CRC7(r.data[:5]) == r.CRC7() } +// r6 is the 48-bit Published RCA response, as per section 4.9.5. It carries +// the card's new Relative Card Address and a subset of the Card Status bits. type r6 struct { data [48 / 8]byte } diff --git a/sd/spicard.go b/sd/spicard.go index 8981357..7c38381 100644 --- a/sd/spicard.go +++ b/sd/spicard.go @@ -10,8 +10,6 @@ import ( "tinygo.org/x/drivers" ) -// See rustref.go for the new implementation. - var ( errBadCSDCID = errors.New("sd:bad CSD/CID in CRC or always1") errNoSDCard = errors.New("sd:no card") @@ -26,8 +24,12 @@ var ( errNoblocks = errors.New("sd:no readable blocks") ) +// digitalPinout sets the logic level of an output pin; true for high, false for low. type digitalPinout = func(b bool) +// SPICard is a SPI-mode SD card driver. It implements the [Card] interface +// and is initialized with [NewSPICard] followed by a call to [SPICard.Init]. +// SPICard is not safe for concurrent use. type SPICard struct { bus drivers.SPI cs digitalPinout @@ -46,6 +48,9 @@ type SPICard struct { lastCRC uint16 } +// NewSPICard returns a new [SPICard] that communicates over spi using cs as +// the chip select pin. The returned card must be initialized with [SPICard.Init] +// before use. func NewSPICard(spi drivers.SPI, cs digitalPinout) *SPICard { const defaultTimeout = 300 * time.Millisecond s := &SPICard{ @@ -74,6 +79,8 @@ func (d *SPICard) Init() error { return d.initRs() } +// NumberOfBlocks returns the number of readable and writable blocks on the card, +// as calculated from the CSD read during [SPICard.Init]. func (d *SPICard) NumberOfBlocks() int64 { return d.csd.NumberOfBlocks() } @@ -233,7 +240,9 @@ func (d *SPICard) updateCSDCID() (err error) { return nil } -// ReadBlock reads to a buffer multiple of 512 bytes from sdcard into dst starting at block `startBlockIdx`. +// ReadBlocks reads card data into dst beginning at the block index startBlockIdx. +// len(dst) must be a multiple of the card's block size (see [CSD.ReadBlockLen]). +// It returns the number of bytes read into dst and any error encountered. func (d *SPICard) ReadBlocks(dst []byte, startBlockIdx int64) (int, error) { numblocks, err := d.checkBounds(startBlockIdx, len(dst)) if err != nil { @@ -267,11 +276,15 @@ func (d *SPICard) ReadBlocks(dst []byte, startBlockIdx int64) (int, error) { panic("unreachable numblocks<=0") } +// EraseBlocks erases numberOfBlocks blocks beginning at startBlock. +// It always returns an error since erase is not yet implemented for SPICard. func (d *SPICard) EraseBlocks(startBlock, numberOfBlocks int64) error { return errors.New("sd:erase not implemented") } -// WriteBlocks writes to sdcard from a buffer multiple of 512 bytes from src starting at block `startBlockIdx`. +// WriteBlocks writes data to the card beginning at the block index startBlockIdx. +// len(data) must be a multiple of the card's block size (see [CSD.WriteBlockLen]). +// It returns the number of bytes written and any error encountered. func (d *SPICard) WriteBlocks(data []byte, startBlockIdx int64) (int, error) { numblocks, err := d.checkBounds(startBlockIdx, len(data)) if err != nil { diff --git a/smoketest.sh b/smoketest.sh index ae45239..5ccd7e8 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -129,6 +129,7 @@ tinygo build -size short -o ./build/test.hex -target=microbit ./examples/ndir/ma tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/ndir/main_ndir.go tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/mpu9150/main.go tinygo build -size short -o ./build/test.hex -target=macropad-rp2040 ./examples/sh1106/macropad_spi +tinygo build -size short -o ./build/test.hex -target=pico ./examples/sd # network examples (espat) tinygo build -size short -o ./build/test.hex -target=challenger-rp2040 ./examples/net/ntpclient/ # network examples (wifinina)