|
| 1 | +package sd |
| 2 | + |
| 3 | +import ( |
| 4 | + "errors" |
| 5 | + "io" |
| 6 | + "math/bits" |
| 7 | +) |
| 8 | + |
| 9 | +var ( |
| 10 | + errNegativeOffset = errors.New("sd: negative offset") |
| 11 | +) |
| 12 | + |
| 13 | +// Compile time guarantee of interface implementation. |
| 14 | +var _ Card = (*SPICard)(nil) |
| 15 | +var _ io.ReaderAt = (*BlockDevice)(nil) |
| 16 | +var _ io.WriterAt = (*BlockDevice)(nil) |
| 17 | + |
| 18 | +// Card is the interface implemented by SD card drivers such as [SPICard]. |
| 19 | +// It provides block-aligned I/O over the card's contents. Use [NewBlockDevice] |
| 20 | +// to wrap a Card with byte-addressed [io.ReaderAt] and [io.WriterAt] interfaces. |
| 21 | +type Card interface { |
| 22 | + // WriteBlocks writes the given data to the card, starting at the given block index. |
| 23 | + // The data must be a multiple of the block size. |
| 24 | + WriteBlocks(data []byte, startBlockIdx int64) (int, error) |
| 25 | + // ReadBlocks reads the given number of blocks from the card, starting at the given block index. |
| 26 | + // The dst buffer must be a multiple of the block size. |
| 27 | + ReadBlocks(dst []byte, startBlockIdx int64) (int, error) |
| 28 | + // EraseBlocks erases blocks starting at startBlockIdx to startBlockIdx+numBlocks. |
| 29 | + EraseBlocks(startBlock, numBlocks int64) error |
| 30 | +} |
| 31 | + |
| 32 | +// NewBlockDevice creates a new [BlockDevice] from a Card. blockSize must be a |
| 33 | +// power of 2. For an initialized [SPICard], blockSize is typically the CSD's |
| 34 | +// [CSD.ReadBlockLen] and numBlocks is [SPICard.NumberOfBlocks]. |
| 35 | +func NewBlockDevice(card Card, blockSize int, numBlocks int64) (*BlockDevice, error) { |
| 36 | + if card == nil || blockSize <= 0 || numBlocks <= 0 { |
| 37 | + return nil, errors.New("invalid argument(s)") |
| 38 | + } |
| 39 | + blk, err := makeBlockIndexer(blockSize) |
| 40 | + if err != nil { |
| 41 | + return nil, err |
| 42 | + } |
| 43 | + bd := &BlockDevice{ |
| 44 | + card: card, |
| 45 | + blockbuf: make([]byte, blockSize), |
| 46 | + blk: blk, |
| 47 | + numblocks: int64(numBlocks), |
| 48 | + } |
| 49 | + return bd, nil |
| 50 | +} |
| 51 | + |
| 52 | +// BlockDevice implements the tinyfs.BlockDevice interface for a [Card], |
| 53 | +// providing byte-addressed reads and writes at arbitrary offsets by buffering |
| 54 | +// non-block-aligned accesses through an internal single-block buffer. |
| 55 | +// BlockDevice is not safe for concurrent use. |
| 56 | +type BlockDevice struct { |
| 57 | + card Card |
| 58 | + blockbuf []byte |
| 59 | + blk blkIdxer |
| 60 | + numblocks int64 |
| 61 | +} |
| 62 | + |
| 63 | +// ReadAt implements the [io.ReaderAt] interface for an SD card. |
| 64 | +// Reads need not be aligned to block boundaries. |
| 65 | +func (bd *BlockDevice) ReadAt(p []byte, off int64) (n int, err error) { |
| 66 | + if off < 0 { |
| 67 | + return 0, errNegativeOffset |
| 68 | + } |
| 69 | + |
| 70 | + blockIdx := bd.blk.idx(off) |
| 71 | + blockOff := bd.blk.off(off) |
| 72 | + if blockOff != 0 { |
| 73 | + // Non-aligned first block case. |
| 74 | + if _, err = bd.card.ReadBlocks(bd.blockbuf, blockIdx); err != nil { |
| 75 | + return n, err |
| 76 | + } |
| 77 | + n += copy(p, bd.blockbuf[blockOff:]) |
| 78 | + p = p[n:] |
| 79 | + blockIdx++ |
| 80 | + } |
| 81 | + |
| 82 | + fullBlocksToRead := bd.blk.idx(int64(len(p))) |
| 83 | + if fullBlocksToRead > 0 { |
| 84 | + // 1 or more full blocks case. |
| 85 | + endOffset := fullBlocksToRead * bd.blk.size() |
| 86 | + ngot, err := bd.card.ReadBlocks(p[:endOffset], blockIdx) |
| 87 | + if err != nil { |
| 88 | + return n + ngot, err |
| 89 | + } |
| 90 | + p = p[endOffset:] |
| 91 | + n += ngot |
| 92 | + blockIdx += fullBlocksToRead |
| 93 | + } |
| 94 | + |
| 95 | + if len(p) > 0 { |
| 96 | + // Non-aligned last block case. |
| 97 | + if _, err := bd.card.ReadBlocks(bd.blockbuf, blockIdx); err != nil { |
| 98 | + return n, err |
| 99 | + } |
| 100 | + n += copy(p, bd.blockbuf) |
| 101 | + } |
| 102 | + return n, nil |
| 103 | +} |
| 104 | + |
| 105 | +// WriteAt implements the [io.WriterAt] interface for an SD card. Writes need |
| 106 | +// not be aligned to block boundaries: partial blocks are read, modified and |
| 107 | +// written back. |
| 108 | +func (bd *BlockDevice) WriteAt(p []byte, off int64) (n int, err error) { |
| 109 | + if off < 0 { |
| 110 | + return 0, errNegativeOffset |
| 111 | + } |
| 112 | + |
| 113 | + blockIdx := bd.blk.idx(off) |
| 114 | + blockOff := bd.blk.off(off) |
| 115 | + if blockOff != 0 { |
| 116 | + // Non-aligned first block case. |
| 117 | + if _, err := bd.card.ReadBlocks(bd.blockbuf, blockIdx); err != nil { |
| 118 | + return n, err |
| 119 | + } |
| 120 | + nexpect := copy(bd.blockbuf[blockOff:], p) |
| 121 | + ngot, err := bd.card.WriteBlocks(bd.blockbuf, blockIdx) |
| 122 | + if err != nil { |
| 123 | + return n, err |
| 124 | + } else if ngot != len(bd.blockbuf) { |
| 125 | + return n, io.ErrShortWrite |
| 126 | + } |
| 127 | + n += nexpect |
| 128 | + p = p[nexpect:] |
| 129 | + blockIdx++ |
| 130 | + } |
| 131 | + |
| 132 | + fullBlocksToWrite := bd.blk.idx(int64(len(p))) |
| 133 | + if fullBlocksToWrite > 0 { |
| 134 | + // 1 or more full blocks case. |
| 135 | + endOffset := fullBlocksToWrite * bd.blk.size() |
| 136 | + ngot, err := bd.card.WriteBlocks(p[:endOffset], blockIdx) |
| 137 | + n += ngot |
| 138 | + if err != nil { |
| 139 | + return n, err |
| 140 | + } else if ngot != int(endOffset) { |
| 141 | + return n, io.ErrShortWrite |
| 142 | + } |
| 143 | + p = p[ngot:] |
| 144 | + blockIdx += fullBlocksToWrite |
| 145 | + } |
| 146 | + |
| 147 | + if len(p) > 0 { |
| 148 | + // Non-aligned last block case. |
| 149 | + if _, err := bd.card.ReadBlocks(bd.blockbuf, blockIdx); err != nil { |
| 150 | + return n, err |
| 151 | + } |
| 152 | + copy(bd.blockbuf, p) |
| 153 | + ngot, err := bd.card.WriteBlocks(bd.blockbuf, blockIdx) |
| 154 | + if err != nil { |
| 155 | + return n, err |
| 156 | + } else if ngot != len(bd.blockbuf) { |
| 157 | + return n, io.ErrShortWrite |
| 158 | + } |
| 159 | + n += len(p) |
| 160 | + } |
| 161 | + return n, nil |
| 162 | +} |
| 163 | + |
| 164 | +// Size returns the number of bytes in this block device. |
| 165 | +func (bd *BlockDevice) Size() int64 { |
| 166 | + return bd.BlockSize() * bd.numblocks |
| 167 | +} |
| 168 | + |
| 169 | +// BlockSize returns the size of a block in bytes. |
| 170 | +func (bd *BlockDevice) BlockSize() int64 { |
| 171 | + return bd.blk.size() |
| 172 | +} |
| 173 | + |
| 174 | +// EraseBlocks erases the given number of blocks. An implementation may |
| 175 | +// transparently coalesce ranges of blocks into larger bundles if the chip |
| 176 | +// supports this. The start and len parameters are in block numbers, use |
| 177 | +// EraseBlockSize to map addresses to blocks. |
| 178 | +func (bd *BlockDevice) EraseBlocks(startEraseBlockIdx, len int64) error { |
| 179 | + return bd.card.EraseBlocks(startEraseBlockIdx, len) |
| 180 | +} |
| 181 | + |
| 182 | +// blkIdxer is a helper for calculating block indices and offsets. |
| 183 | +type blkIdxer struct { |
| 184 | + blockshift int64 |
| 185 | + blockmask int64 |
| 186 | +} |
| 187 | + |
| 188 | +// makeBlockIndexer returns a blkIdxer for the given block size, |
| 189 | +// which must be a power of 2. |
| 190 | +func makeBlockIndexer(blockSize int) (blkIdxer, error) { |
| 191 | + if blockSize <= 0 { |
| 192 | + return blkIdxer{}, errNoblocks |
| 193 | + } |
| 194 | + tz := bits.TrailingZeros(uint(blockSize)) |
| 195 | + if blockSize>>tz != 1 { |
| 196 | + return blkIdxer{}, errors.New("blockSize must be a power of 2") |
| 197 | + } |
| 198 | + blk := blkIdxer{ |
| 199 | + blockshift: int64(tz), |
| 200 | + blockmask: (1 << tz) - 1, |
| 201 | + } |
| 202 | + return blk, nil |
| 203 | +} |
| 204 | + |
| 205 | +// size returns the size of a block in bytes. |
| 206 | +func (blk *blkIdxer) size() int64 { |
| 207 | + return 1 << blk.blockshift |
| 208 | +} |
| 209 | + |
| 210 | +// off gets the offset of the byte at byteIdx from the start of its block. |
| 211 | +// |
| 212 | +//go:inline |
| 213 | +func (blk *blkIdxer) off(byteIdx int64) int64 { |
| 214 | + return blk._moduloBlockSize(byteIdx) |
| 215 | +} |
| 216 | + |
| 217 | +// idx gets the block index that contains the byte at byteIdx. |
| 218 | +// |
| 219 | +//go:inline |
| 220 | +func (blk *blkIdxer) idx(byteIdx int64) int64 { |
| 221 | + return blk._divideBlockSize(byteIdx) |
| 222 | +} |
| 223 | + |
| 224 | +// modulo and divide are defined in terms of bit operations for speed since |
| 225 | +// blockSize is a power of 2. |
| 226 | + |
| 227 | +//go:inline |
| 228 | +func (blk *blkIdxer) _moduloBlockSize(n int64) int64 { return n & blk.blockmask } |
| 229 | + |
| 230 | +//go:inline |
| 231 | +func (blk *blkIdxer) _divideBlockSize(n int64) int64 { return n >> blk.blockshift } |
0 commit comments