Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions examples/sd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package main

import (
"fmt"
"machine"
"time"

"tinygo.org/x/drivers/sd"
)

const (
SPI_RX_PIN = machine.GP16
SPI_TX_PIN = machine.GP19
SPI_SCK_PIN = machine.GP18
SPI_CS_PIN = machine.GP15
)

var (
spibus = machine.SPI0
spicfg = machine.SPIConfig{
Frequency: 250000,
Mode: 0,
SCK: SPI_SCK_PIN,
SDO: SPI_TX_PIN,
SDI: SPI_RX_PIN,
}
)

func main() {
time.Sleep(time.Second)
SPI_CS_PIN.Configure(machine.PinConfig{Mode: machine.PinOutput})
err := spibus.Configure(spicfg)
if err != nil {
panic(err.Error())
}
sdcard := sd.NewSPICard(spibus, SPI_CS_PIN.Set)
println("start init")
err = sdcard.Init()
if err != nil {
panic("sd card init:" + err.Error())
}
// After initialization it's safe to increase SPI clock speed.
csd := sdcard.CSD()
kbps := csd.TransferSpeed().RateKilobits()
spicfg.Frequency = uint32(kbps * 1000)
err = spibus.Configure(spicfg)

cid := sdcard.CID()
fmt.Printf("name=%s\ncsd=\n%s\n", cid.ProductName(), csd.String())

bd, err := sd.NewBlockDevice(sdcard, csd.ReadBlockLen(), csd.NumberOfBlocks())
if err != nil {
panic("block device creation:" + err.Error())
}
var mc MemChecker

ok, badBlkIdx, err := mc.MemCheck(bd, 2, 100)
if err != nil {
panic("memcheck:" + err.Error())
}
if !ok {
println("bad block", badBlkIdx)
} else {
println("memcheck ok")
}
}

type MemChecker struct {
rdBuf []byte
storeBuf []byte
wrBuf []byte
}

func (mc *MemChecker) MemCheck(bd *sd.BlockDevice, blockIdx, numBlocks int64) (memOK bool, badBlockIdx int64, err error) {
size := bd.BlockSize() * numBlocks
if len(mc.rdBuf) < int(size) {
mc.rdBuf = make([]byte, size)
mc.wrBuf = make([]byte, size)
mc.storeBuf = make([]byte, size)
for i := range mc.wrBuf {
mc.wrBuf[i] = byte(i)
}
}
// Start by storing the original block contents.
_, err = bd.ReadAt(mc.storeBuf, blockIdx)
if err != nil {
return false, blockIdx, err
}

// Write the test pattern.
_, err = bd.WriteAt(mc.wrBuf, blockIdx)
if err != nil {
return false, blockIdx, err
}
// Read back the test pattern.
_, err = bd.ReadAt(mc.rdBuf, blockIdx)
if err != nil {
return false, blockIdx, err
}
for j := 0; j < len(mc.rdBuf); j++ {
// Compare the read back data with the test pattern.
if mc.rdBuf[j] != mc.wrBuf[j] {
badBlock := blockIdx + int64(j)/bd.BlockSize()
return false, badBlock, nil
}
mc.rdBuf[j] = 0
}
// Leave the card in it's previous state.
_, err = bd.WriteAt(mc.storeBuf, blockIdx)
return true, -1, nil
}
11 changes: 11 additions & 0 deletions sd/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## `sd` package

File map:
* `blockdevice.go`: Contains logic for creating an `io.WriterAt` and `io.ReaderAt` with the `sd.BlockDevice` concrete type
from the `sd.Card` interface which is intrinsically a blocked reader and writer.

* `spicard.go`: Contains the `sd.SpiCard` driver for controlling an SD card over SPI using the most commonly available circuit boards.

* `responses.go`: Contains a currently unused SD response implementations as per the latest specification.

* `definitions.go`: Contains SD Card specification definitions such as the CSD and CID types as well as encoding/decoding logic, as well as CRC logic.
231 changes: 231 additions & 0 deletions sd/blockdevice.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
package sd

import (
"errors"
"io"
"math/bits"
)

var (
errNegativeOffset = errors.New("sd: negative offset")
)

// Compile time guarantee of interface implementation.
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.
WriteBlocks(data []byte, startBlockIdx int64) (int, error)
// ReadBlocks reads the given number of blocks from the card, starting at the given block index.
// The dst buffer must be a multiple of the block size.
ReadBlocks(dst []byte, startBlockIdx int64) (int, error)
// EraseBlocks erases blocks starting at startBlockIdx to startBlockIdx+numBlocks.
EraseBlocks(startBlock, numBlocks int64) error
}

// 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)")
}
blk, err := makeBlockIndexer(blockSize)
if err != nil {
return nil, err
}
bd := &BlockDevice{
card: card,
blockbuf: make([]byte, blockSize),
blk: blk,
numblocks: int64(numBlocks),
}
return bd, nil
}

// 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
blk blkIdxer
numblocks int64
}

// 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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No bounds checking against device size in this function.

if off < 0 {
return 0, errNegativeOffset
}

blockIdx := bd.blk.idx(off)
blockOff := bd.blk.off(off)
if blockOff != 0 {
// Non-aligned first block case.
if _, err = bd.card.ReadBlocks(bd.blockbuf, blockIdx); err != nil {
return n, err
}
n += copy(p, bd.blockbuf[blockOff:])
p = p[n:]
blockIdx++
}

fullBlocksToRead := bd.blk.idx(int64(len(p)))
if fullBlocksToRead > 0 {
// 1 or more full blocks case.
endOffset := fullBlocksToRead * bd.blk.size()
ngot, err := bd.card.ReadBlocks(p[:endOffset], blockIdx)
if err != nil {
return n + ngot, err
}
p = p[endOffset:]
n += ngot
blockIdx += fullBlocksToRead
}

if len(p) > 0 {
// Non-aligned last block case.
if _, err := bd.card.ReadBlocks(bd.blockbuf, blockIdx); err != nil {
return n, err
}
n += copy(p, bd.blockbuf)
}
return n, nil
}

// 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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No bounds checking against device size in this function either.

if off < 0 {
return 0, errNegativeOffset
}

blockIdx := bd.blk.idx(off)
blockOff := bd.blk.off(off)
if blockOff != 0 {
// Non-aligned first block case.
if _, err := bd.card.ReadBlocks(bd.blockbuf, blockIdx); err != nil {
return n, err
}
nexpect := copy(bd.blockbuf[blockOff:], p)
ngot, err := bd.card.WriteBlocks(bd.blockbuf, blockIdx)
if err != nil {
return n, err
} else if ngot != len(bd.blockbuf) {
return n, io.ErrShortWrite
}
n += nexpect
p = p[nexpect:]
blockIdx++
}

fullBlocksToWrite := bd.blk.idx(int64(len(p)))
if fullBlocksToWrite > 0 {
// 1 or more full blocks case.
endOffset := fullBlocksToWrite * bd.blk.size()
ngot, err := bd.card.WriteBlocks(p[:endOffset], blockIdx)
n += ngot
if err != nil {
return n, err
} else if ngot != int(endOffset) {
return n, io.ErrShortWrite
}
p = p[ngot:]
blockIdx += fullBlocksToWrite
}

if len(p) > 0 {
// Non-aligned last block case.
if _, err := bd.card.ReadBlocks(bd.blockbuf, blockIdx); err != nil {
return n, err
}
copy(bd.blockbuf, p)
ngot, err := bd.card.WriteBlocks(bd.blockbuf, blockIdx)
if err != nil {
return n, err
} else if ngot != len(bd.blockbuf) {
return n, io.ErrShortWrite
}
n += len(p)
}
return n, nil
}

// Size returns the number of bytes in this block device.
func (bd *BlockDevice) Size() int64 {
return bd.BlockSize() * bd.numblocks
}

// BlockSize returns the size of a block in bytes.
func (bd *BlockDevice) BlockSize() int64 {
return bd.blk.size()
}

// EraseBlocks erases the given number of blocks. An implementation may
// transparently coalesce ranges of blocks into larger bundles if the chip
// supports this. The start and len parameters are in block numbers, use
// EraseBlockSize to map addresses to blocks.
func (bd *BlockDevice) EraseBlocks(startEraseBlockIdx, len int64) error {
return bd.card.EraseBlocks(startEraseBlockIdx, len)
}

// blkIdxer is a helper for calculating block indices and offsets.
type blkIdxer struct {
blockshift int64
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
}
tz := bits.TrailingZeros(uint(blockSize))
if blockSize>>tz != 1 {
return blkIdxer{}, errors.New("blockSize must be a power of 2")
}
blk := blkIdxer{
blockshift: int64(tz),
blockmask: (1 << tz) - 1,
}
return blk, nil
}

// size returns the size of a block in bytes.
func (blk *blkIdxer) size() int64 {
return 1 << blk.blockshift
}

// off gets the offset of the byte at byteIdx from the start of its block.
//
//go:inline
func (blk *blkIdxer) off(byteIdx int64) int64 {
return blk._moduloBlockSize(byteIdx)
}

// idx gets the block index that contains the byte at byteIdx.
//
//go:inline
func (blk *blkIdxer) idx(byteIdx int64) int64 {
return blk._divideBlockSize(byteIdx)
}

// modulo and divide are defined in terms of bit operations for speed since
// blockSize is a power of 2.

//go:inline
func (blk *blkIdxer) _moduloBlockSize(n int64) int64 { return n & blk.blockmask }

//go:inline
func (blk *blkIdxer) _divideBlockSize(n int64) int64 { return n >> blk.blockshift }
Loading