mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-07-26 10:38:41 +00:00
232 lines
6.5 KiB
Go
232 lines
6.5 KiB
Go
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) {
|
|
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) {
|
|
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 }
|