diff --git a/sw/cheri/common/filesys-utils.hh b/sw/cheri/common/filesys-utils.hh new file mode 100644 index 00000000..ec9707fc --- /dev/null +++ b/sw/cheri/common/filesys-utils.hh @@ -0,0 +1,678 @@ +/** + * Copyright lowRISC contributors. + * Licensed under the Apache License, Version 2.0, see LICENSE for details. + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once +#include + +#include + +#include "console.hh" +#include "sdcard-utils.hh" + +/** + * Very simple layer for read access to the files within the root directory + * of a FAT32 partition on an SD card. + * + * If a more sophisticated, feature-rich filing system layer including, e.g. + * support for writing data, is required, there are a number of open source + * implementations of FAT32 support available. + * + * The code will locate the first FAT32 paritition, and only Master Boot Record (MBR) + * partitioning is supported, which is how blanks microSD cards are shipped by + * manufacturers, so avoid the use of GPT if reformatting. + * + * https://en.wikipedia.org/wiki/File_Allocation_Table#FAT32 + * https://en.wikipedia.org/wiki/Design_of_the_FAT_file_system + */ + +class fileSysUtils { + private: + // Access to debug/diagnostic logging. + Log *log; + // SD card access. + SdCard *sd; + + // Some SD cards support only a 512-byte block size, and SPI mode transfers are + // always in terms of that anyway. + static constexpr unsigned kBytesPerBlockShift = 9u; + static constexpr unsigned kBlockLen = (1u << kBytesPerBlockShift); + + // Properties of the FAT32 partition. + bool partValid; + // The logical volume consists of sectors, which are not necessarily the same size as + // the blocks used at (SD card) driver level. + uint8_t bytesPerSectorShift; + uint8_t secsPerClusterShift; + uint8_t blksPerClusterShift; + // First block of the FAT, relative to the medium start. + uint32_t fatStart; + // First block of the cluster heap. + uint32_t clusterHeapStart; + // First block of the root directory. + uint32_t rootStart; + // First cluster holding the root directory. + uint32_t rootCluster; + // Cluster size in bytes. + uint32_t clusterBytes; + // Mask used to extract the byte offset within the current cluster + // (= cluster size in bytes - 1). + uint32_t clusterMask; + + // Single block buffer for use when reading partitions and FAT contents. + uint8_t dataBuffer[kBlockLen]; + + // Number of entries in the block cache. + static constexpr unsigned kCacheEntries = 8u; + // Denotes an unused entry in the block cache. + static constexpr uint32_t kInvalidBlock = ~(uint32_t)0u; + // Block cache for next eviction. + unsigned blockCacheNext; + // Each block within the cache. + struct { + // Block number of the data occuping this cache entry (or kInvalidBlock). + uint32_t block; + // Data for this block. + uint8_t buf[kBlockLen]; + } blockCache[kCacheEntries]; + + // Object state flags. + enum { Flag_Valid = 1U << 31 }; + + // State information on an object being accessed; this may be either a file or a directory. + struct objState { + // Flags specifying validity/properties of this object. + uint32_t flags; + // Current offset (bytes) within the object. + uint32_t offset; + // Object length in bytes. + uint32_t length; + // Cluster number of the cluster holding the data at the current offset. + uint32_t currCluster; + // Cluster number of the first cluster holding the data for this object. + uint32_t firstCluster; + }; + + // Set of open files. + static constexpr unsigned kMaxFiles = 4u; + objState files[kMaxFiles]; + + // Set of open directories. + static constexpr unsigned kMaxDirs = 2u; + objState dirs[kMaxDirs]; + + // Copy a sequence of bytes; destination and source must _not_ overlap. + static void copy_bytes(uint8_t *dst, const uint8_t *src, size_t len) { + const uint8_t *esrc = src + len; + // Check there is no overlap between source and destination buffers; + // this expression avoids issues with address addition wrapping. + assert(dst < src || dst - src >= len); + assert(src < dst || src - dst >= len); + while (src < esrc) { + *dst++ = *src++; + } + } + + // Ensure that the specified block is available in memory for access. + int block_ensure(uint32_t block) { + // Check whether this block is already available. + int idx = 0; + while (idx < kCacheEntries) { + if (block == blockCache[idx].block) { + return idx; + } + idx++; + } + idx = blockCacheNext; + if (log) { + log->println(" (reading blk {:#x})", block); + } + if (sd->read_blocks(block, blockCache[idx].buf, 1u)) { + blockCache[idx].block = block; + // Round-robin replacement of cached blocks. + if (++blockCacheNext >= kCacheEntries) { + blockCacheNext = 0u; + } + return idx; + } + return -1; + } + + // Is the specified cluster number an End of Chain marker? + // (a number of different values are used as EOC markers.) + inline bool end_of_chain(uint32_t cluster) { return (cluster <= 1u) || (cluster >= 0x0ffffff8u); } + + // Read the next cluster in the cluster chain of an object. + bool cluster_next(uint32_t &nextCluster, uint32_t cluster) { + // Byte offset of the corresponding entry within the FAT. + uint32_t byteOffset = cluster << 2; + // Determine the block number of the part of the FAT that describes this cluster. + uint32_t block = fatStart + (byteOffset >> kBytesPerBlockShift); + int idx = block_ensure(block); + if (idx < 0) { + // Failed to read the block from the medium. + return false; + } + nextCluster = read32le(&blockCache[idx].buf[byteOffset & (kBlockLen - 1u)]); + // The upper nibble of the cluster must be ignored; reserved for future use. + nextCluster &= ~0xf0000000u; + return true; + } + + // Seek to the given offset within an object (file/directory). + bool object_seek(objState &obj, uint32_t offset) { + // First validate the requested offset. + if (offset > obj.length) { + return false; + } + // Start either from the current file offset (trusted) or the beginning of the file. + uint32_t currCluster = obj.currCluster; + uint32_t currOffset = obj.offset & ~clusterMask; + if (offset < currOffset) { + currCluster = obj.firstCluster; + currOffset = 0u; + } + // Scan forwards through the cluster chain until we find the correct cluster. + while (offset - currOffset >= clusterBytes) { + uint32_t nextCluster; + if (!cluster_next(nextCluster, currCluster)) { + // Leave the current position unchanged. + return false; + } + currCluster = nextCluster; + currOffset += clusterBytes; + } + // Atomically update the current position with a consistent cluster number and offset. + obj.currCluster = currCluster; + obj.offset = offset; + return true; + } + + // Read a contiguous sequence of bytes from an object (file/directory). + size_t object_read(objState &obj, uint8_t *buf, size_t len) { + if (log) { + log->println("reading {:#x} byte(s) at offset {:#x}", len, obj.offset); + } + + size_t bytesRead = 0u; + while (len > 0u && obj.offset < obj.length) { + uint32_t currBlock = block_number(obj.currCluster, obj.offset & clusterMask); + + // Ensure that the block containing the current offset is available for use, if it + // can be read from the medium. + int idx = block_ensure(currBlock); + if (idx < 0) { + return bytesRead; + } + // Locate this block within the block cache; its availability is guaranteed at this point. + const uint8_t *dataBuf = blockCache[idx].buf; + + // How much data do we have available at the current offset? + size_t blockOffset = obj.offset & (kBlockLen - 1u); + size_t blockBytesLeft = kBlockLen - blockOffset; + size_t objBytesLeft = obj.length - obj.offset; + size_t bytesAvail = (objBytesLeft > blockBytesLeft) ? blockBytesLeft : objBytesLeft; + // Limit this request to the bytes immediately available. + size_t chunk_len = (len > bytesAvail) ? bytesAvail : len; + + // Have we reached the end of this cluster but not the end of the object data? + uint32_t next_offset = obj.offset + chunk_len; + if (!(next_offset & clusterMask) && obj.length > next_offset) { + uint32_t nextCluster; + if (!cluster_next(nextCluster, obj.currCluster)) { + // Note: we're leaving the object state consistent here, despite the read failure. + return bytesRead; + } + // Store the updated cluster number for the new offset. + obj.currCluster = nextCluster; + } + // Advance the current offset, now that we know that the new offset is consistent wtih the + // cluster number. + obj.offset += chunk_len; + + // We have no memcpy implementation presently. + copy_bytes(buf, &dataBuf[blockOffset], chunk_len); + buf += chunk_len; + len -= chunk_len; + bytesRead += chunk_len; + } + return bytesRead; + } + + public: + // Opaque handle to an open file. + typedef uint8_t fileHandle; + // Invalid file handle, returned by a failed `file_open` call. + static constexpr uint8_t kInvalidFileHandle = 0xffu; + + // Opaque handle to an open directory. + typedef uint8_t dirHandle; + // Invalid directory handle, returned by a failed 'dir_open' call. + static constexpr uint8_t kInvalidDirHandle = 0xffu; + + struct dirEntry { + uint8_t entryType; + uint8_t customDefined[0x13]; + uint32_t firstCluster; + uint32_t dataLength; + }; + + fileSysUtils(Log *log_ = nullptr) : log(log_), sd(nullptr) { + // Initialise all state information; no partition details, empty block cache, + // no file/dir handles. + fin(); + } + + // Test for the presence of a FAT32 partition, read the partition properties + // and then locate the cluster heap and root directory. + bool init(SdCard *sd_) { + /// Retain access to the SD card. + assert(sd_); + sd = sd_; + + // Read the Master Boot Record (MBR) from block 0 at the very start of the medium. + if (!sd->read_blocks(0, dataBuffer, 1u)) { + if (log) { + log->println("Unable to read the MBR of the SD card"); + } + return false; + } + + // We require MBR, as used by manufacturers for greatest compatibility, not GPT. + if (dataBuffer[0x1fe] != 0x55 || dataBuffer[0x1ff] != 0xaa) { + if (log) { + log->println("Unable to parse the MBR of the SD card"); + } + return false; + } + + // The MBR describes up to four primary partitions. + uint32_t blk_offset; + bool use_lba = true; + bool found = false; + + for (unsigned part = 0u; part < 1u; part++) { + const unsigned partDesc = 0x1be + (part << 4); + uint8_t part_type = dataBuffer[partDesc + 4]; + uint32_t lba_start = read32le(&dataBuffer[partDesc + 8]); + uint32_t num_secs = read32le(&dataBuffer[partDesc + 12]); + uint16_t start_c, end_c; + uint8_t start_h, end_h; + uint8_t start_s, end_s; + read_chs(start_c, start_h, start_s, &dataBuffer[partDesc + 1]); + read_chs(end_c, end_h, end_s, &dataBuffer[partDesc + 5]); + if (log) { + log->println("Partition {} : type {} : start C {} H {} S {} : end C {} H {} S {}", part, part_type, start_c, + start_h, start_s, end_c, end_h, end_s); + log->println(" LBA start: {:#010x} sectors: {:#010x}", lba_start, num_secs); + } + switch (part_type) { + // FAT32 a viable alternative. + case 0x0B: + use_lba = false; + // no break + case 0x0C: { + const uint16_t nheads = 255u; + const uint16_t nsecs = 63u; + if (use_lba) { + blk_offset = lba_start; + } else { + blk_offset = chs_to_lba(start_c, start_h, start_s, nheads, nsecs); + } + if (log) { + log->println("Expecting EBR at block {:#x}", blk_offset); + } + found = true; + } break; + default: + if (log) { + log->println("Not a suitable partition"); + } + break; + } + } + + if (!found) { + if (log) { + log->println("Unable to locate a suitable partition"); + } + return false; + } + + // Read the EBR at the start of the partition. + if (log) { + log->println("Reading block {}", blk_offset); + } + sd->read_blocks(blk_offset, dataBuffer, 1u); + if (log) { + dump_bytes(dataBuffer, kBlockLen, *log); + } + + uint16_t bytesPerSector = read16le(&dataBuffer[0x0b]); + uint16_t resvdSectors = read16le(&dataBuffer[0xe]); + uint8_t numFATs = dataBuffer[0x10]; + uint32_t secsPerFAT = read32le(&dataBuffer[0x24]); + rootCluster = read32le(&dataBuffer[0x2c]); + + if (log) { + log->println("FAT32 {} FATs, secs per FAT {}, bytes/sec {}", numFATs, secsPerFAT, bytesPerSector); + log->println(" resvdSectors {}", resvdSectors); + } + + // TODO: replace with functions. + bytesPerSectorShift = 0u; + while (bytesPerSector > 1u) { + bytesPerSector >>= 1; + bytesPerSectorShift++; + } + uint8_t secsPerCluster = dataBuffer[0xd]; + secsPerClusterShift = 0u; + while (secsPerCluster > 1u) { + secsPerCluster >>= 1; + secsPerClusterShift++; + } + + uint32_t fatOffset = resvdSectors; + uint32_t clusterHeapOffset = ((resvdSectors + (numFATs * secsPerFAT)) << bytesPerSectorShift) / kBlockLen; + + // TODO: we do not cope with a difference between blocks and sectors. + blksPerClusterShift = secsPerClusterShift; + + // Remember the volume-relative block numbers at which the (first) FAT, the cluster heap and + // the root directory commence. + rootStart = ((rootCluster - 2) << secsPerClusterShift << bytesPerSectorShift) / kBlockLen; + rootStart += blk_offset + clusterHeapOffset; + clusterHeapStart = blk_offset + clusterHeapOffset; + fatStart = blk_offset + fatOffset; + + if (log) { + log->println("Cluster heap offset {} Root cluster {} log2(bytes/sec) {} log2(secs/cluster) {}", clusterHeapOffset, + rootCluster, bytesPerSectorShift, secsPerClusterShift); + } + + // Sanity check the parameters, listing all objections. + partValid = true; + if (bytesPerSectorShift < 9 || bytesPerSectorShift > 12) { + if (log) { + log->println(" - bytes/sector is invalid"); + } + partValid = false; + } + if (secsPerClusterShift > 25 - bytesPerSectorShift) { + if (log) { + log->println(" - sectors/cluster is invalid"); + } + partValid = false; + } + if (!partValid) { + if (log) { + log->println("Unable to use this partition"); + } + return false; + } + + // Calculate derived properties. + clusterBytes = 1u << (secsPerClusterShift + bytesPerSectorShift); + clusterMask = clusterBytes - 1u; + + // Record the fact that we have a valid partition. + partValid = true; + // We should now have access to the root directory when required. + return true; + } + + // Finalise access to a filesystem. + void fin() { + // Forget all files. + for (unsigned idx = 0u; idx < kMaxFiles; idx++) { + files[idx].flags = 0u; + } + // Forget all directories. + for (unsigned idx = 0u; idx < kMaxDirs; idx++) { + dirs[idx].flags = 0u; + } + // Forget all cached blocks. + for (unsigned idx = 0u; idx < kCacheEntries; idx++) { + blockCache[idx].block = kInvalidBlock; + } + blockCacheNext = 0u; + // Forget the medium itself. + partValid = false; + } + + // Return the block number corresponding to the given byte offset within the specified cluster + // of the file system, or UINT32_MAX if invalid. + uint32_t block_number(uint32_t cluster, uint32_t offset) { + // TODO: clusterCount not yet available. + // assert(cluster >= 2u && cluster < clusterCount); + offset >>= kBytesPerBlockShift; + return clusterHeapStart + ((cluster - 2u) << blksPerClusterShift) + offset; + } + + // Validate directory handle. + inline bool dh_valid(dirHandle dh) { return dh < kMaxDirs && (dirs[dh].flags & Flag_Valid); } + + // Validate file handle. + inline bool fh_valid(fileHandle fh) { return fh < kMaxFiles && (files[fh].flags & Flag_Valid); } + + // Get a handle to the root directory of the mounted partition. + dirHandle rootdir_open() { + if (!partValid) { + return kInvalidDirHandle; + } + return dir_open(rootCluster); + } + + // + dirHandle dir_open(uint32_t cluster) { + // Ensure that we have a directory handle available + dirHandle dh = 0u; + while (dirs[dh].flags & Flag_Valid) { + if (++dh >= kMaxDirs) { + return kInvalidDirHandle; + } + } + // Initialise directory state. + dirs[dh].flags = Flag_Valid; + dirs[dh].offset = 0u; + // TODO: How DOES one ascertain the length of the root directory? + // Are all directories essentially indeterminate length? + dirs[dh].length = ~0u; // dir.dataLength; + dirs[dh].currCluster = cluster; + dirs[dh].firstCluster = cluster; + return dh; + } + + bool dir_next(dirHandle dh, dirEntry &entry) { + if (dh >= kMaxDirs) { + return false; + } + + uint8_t entryType; + do { + uint8_t dir_entry[0x20u]; + if (sizeof(dir_entry) != object_read(dirs[dh], dir_entry, sizeof(dir_entry))) { + return false; + } + if (log) { + log->println("Dir entry:"); + dump_bytes(dir_entry, sizeof(dir_entry), *log); + } + entryType = dir_entry[0]; + // TODO: nail down this checking, handle LFN etc. + if (entryType >= 0x20 && entryType != 0x2e && entryType != 0xe5) { + uint32_t cluster = ((uint32_t)read16le(&dir_entry[0x14]) << 16) | read16le(&dir_entry[0x1a]); + // the upper nibble of the cluster must be ignored; reserved for future use. + cluster &= ~0xf0000000u; + + entry.entryType = dir_entry[0]; + // TODO: structure this appropriately for FAT32 + copy_bytes(entry.customDefined, &dir_entry[0], 11); // sizeof(entry.customDefined)); + entry.customDefined[11] = '\0'; + entry.firstCluster = cluster; + entry.dataLength = read32le(&dir_entry[0x1c]); + return true; + } + } while (entryType); + + return false; + } + + // Release access to the given directory. + void dir_close(dirHandle dh) { + if (dh < kMaxDirs) { + dirs[dh].flags = 0u; + } + } + + // TODO: May be useful to export this in a fuller form, with LFN support? + static int namecmp(const uint8_t *d, const char *s, size_t n) { + while (n-- > 0) { + if (*d++ != *s++) return 1; + } + return 0; + } + + // Open the file described by the given directory entry. + fileHandle file_open(const dirEntry &entry) { + // Ensure that we have a file handle available + fileHandle fh = 0u; + while (files[fh].flags & Flag_Valid) { + if (++fh >= kMaxFiles) { + return kInvalidFileHandle; + } + } + // Initialise file state. + files[fh].flags = Flag_Valid; + files[fh].offset = 0u; + files[fh].length = entry.dataLength; + files[fh].currCluster = entry.firstCluster; + files[fh].firstCluster = entry.firstCluster; + if (log) { + log->println("Opened file of {} byte(s) at cluster {:#10x}", entry.dataLength, entry.firstCluster); + } + return fh; + } + + // Initiate read access to the given file and return a handle to the file, or InvalidFileHandle if + // the operation is unsuccessful. + fileHandle file_open(const char *file) { + // Maintain the pretence of supporting full pathnames; they may be supported at some point. + if (*file == '/' || *file == '\\') file++; + + fileHandle fh = kInvalidFileHandle; + dirHandle dh = rootdir_open(); + if (dh != kInvalidDirHandle) { + dirEntry entry; + while (dir_next(dh, entry)) { + if (!namecmp(entry.customDefined, file, 11)) { + fh = file_open(entry); + break; + } + // Advance to the next entry. + } + dir_close(dh); + } + return fh; + } + + // Return the length of an open file, or a negative value if the file handle is invalid. + ssize_t file_length(fileHandle fh) { return fh_valid(fh) ? (ssize_t)files[fh].length : -1; } + + // Read data from a file at the supplied offset, reading the requested number of bytes. + size_t file_read(fileHandle fh, uint8_t *buf, size_t len) { + if (!fh_valid(fh)) { + return 0u; + } + return object_read(files[fh], buf, len); + } + + // Return a list of clusters holding the contents of this file, starting from the current file offset, + // and updating it upon return. + ssize_t file_clusters(fileHandle fh, uint8_t &clusterShift, uint32_t *buf, size_t len) { + // Check that the file handle is valid. + if (!fh_valid(fh)) { + return -1; + } + // Indicate how many blocks form a cluster for this partition. + clusterShift = blksPerClusterShift; + // Run forwards from the current position, permitting incremental retrieval. + uint32_t cluster = files[fh].currCluster; + // Ensure that the offset is aligned to the start of the cluster. + uint32_t offset = files[fh].offset & ~clusterMask; + size_t n = 0u; + while (len-- > 0u && !end_of_chain(cluster)) { + uint32_t nextCluster; + *buf++ = cluster; + n++; + if (!cluster_next(nextCluster, cluster)) { + break; + } + // Remember this position within the file. + offset += clusterBytes; + files[fh].offset = offset; + files[fh].currCluster = cluster; + cluster = nextCluster; + } + return n; + } + + // Finalise read access to the given file. + void file_close(fileHandle fh) { + if (fh_valid(fh)) { + files[fh].flags = 0u; + } + } + + // Read Cylinder, Head and Sector ('CHS address'), as stored in a partition table entry. + static inline void read_chs(uint16_t &c, uint8_t &h, uint8_t &s, const uint8_t *p) { + // Head numbers are 0-based. + h = p[0]; + // Note that sector numbers are 1-based. + s = (p[1] & 0x3fu); + // Cylinder numbers are 0-based. + c = ((p[1] << 2) & 0x300u) | p[2]; + } + + // Utility function that converts Cylinder, Head, Sector (CHS) addressing into Logical Block Addressing + // (LBA), according to the specified disk geometry. + static uint32_t chs_to_lba(uint16_t c, uint8_t h, uint8_t s, uint8_t nheads, uint8_t nsecs) { + // Notes: cylinder and head are zero-based but sector number is 1-based (0 is invalid). + // CHS-addressed drives were limited to 255 heads and 63 sectors. + if (h >= nheads || !s || s > nsecs) { + return UINT32_MAX; + } + return ((c * nheads + h) * nsecs) + (s - 1); + } + + // Read 32-bit Little Endian word. + static inline uint32_t read32le(const uint8_t *p) { + return p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); + } + + // Read 16-bit Little Endian word. + static inline uint16_t read16le(const uint8_t *p) { return p[0] | ((uint16_t)p[1] << 8); } + + // Dump out a sequence of bytes as hexadecimal and ASCII text. + // TODO: Perhaps this should be moved to uart-utils.hh/console.hh + static void dump_bytes(const uint8_t *buf, size_t blkBytes, Log &log) { + for (size_t off = 0u; off < blkBytes; ++off) { + log.print("{:02x}", buf[off]); + if ((off & 0xfu) == 0xfu) { + log.print(" : "); + for (size_t aoff = (off & ~0xfu); aoff <= off; aoff++) { + char text[2]; + text[0] = buf[aoff]; + if (!isprint(text[0])) text[0] = '.'; + text[1] = '\0'; + log.print(text); + } + log.println(""); + } else { + log.print(" "); + } + } + } +}; diff --git a/sw/cheri/tests/lorem_text.hh b/sw/cheri/tests/lorem_text.hh new file mode 100644 index 00000000..66b2f98e --- /dev/null +++ b/sw/cheri/tests/lorem_text.hh @@ -0,0 +1,85 @@ +/** + * Copyright lowRISC contributors. + * Licensed under the Apache License, Version 2.0, see LICENSE for details. + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * This is a `lorem ipsum` (https://en.wikipedia.org/wiki/Lorem_ipsum) text + * used to test reading from the microSD card. The text can be emitted over + * the UART by setting `emitText` to true, captured and stored as `LOREM.IPS` + * in the root directory of a FAT32-formatted microSD card. + * + * The data read from the file on the microSD card is then compared with this + * text. + */ +static const char lorem_text[] = { + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sociosqu " + "consectetur, tempor nisl si rutrum nibh. Ullamcorper iaculis ornare mauris " + "eleifend, eu convallis porttitor pharetra nisi. Nullam condimentum " + "tincidunt, vulputate facilisi, maecenas tortor. Eu convallis, feugiat " + "facilisis per magna venenatis. Sodales natoque, lectus tristique aptent " + "scelerisque, ac sociis ligula. Augue nisl torquent magnis, mi platea " + "eleifend suspendisse. Morbi dapibus montes mattis, magna do sociis posuere. " + "Natoque, taciti volutpat porttitor, ultricies amet. Sapien varius euismod, " + "dignissim ad sociis, molestie maximus phasellus." + "\r\n" + "\r\n" + "Feugiat, sociosqu parturient fringilla, do aliquet facilisis quisque " + "vulputate. Elit vitae sagittis sapien mattis, at phasellus blandit " + "consectetur ligula dictumst tortor. Proin dignissim suspendisse, lectus ad " + "natoque, interdum libero augue. Scelerisque lacinia mauris morbi. Feugiat " + "sagittis proin iaculis si augue fermentum. Sapien lectus euismod, lorem " + "suspendisse, justo sociis. Arcu ultrices commodo amet, sociosqu rutrum, " + "facilisis mus convallis. Neque condimentum nisl orci dolor, si mattis sed " + "magnis. Proin lorem, vulputate fusce, id feugiat adipiscing commodo aliquet. " + "Tortor, litora natoque lacinia, mattis posuere ullamcorper vitae. Dictumst " + "lectus imperdiet, consectetur porttitor maecenas gravida. Condimentum " + "tristique ac natoque nascetur praesent rutrum. Mauris aliquam fringilla, " + "per gravida eget, eu scelerisque. Praesent egestas cursus class condimentum, " + "mi mattis posuere tempor semper ridiculus vulputate." + "\r\n" + "\r\n" + "Aliquet euismod ante pellentesque, gravida tincidunt, per luctus morbi. " + "Varius, montes magna pulvinar, molestie eu tempor. Platea pharetra laoreet, " + "ut viverra lacinia, mus dignissim. Mattis suspendisse luctus gravida, " + "penatibus per tempor nisl. Torquent arcu porttitor nec iaculis, at vitae " + "posuere condimentum lacinia nostra aliquet. Parturient, praesent penatibus " + "adipiscing, fusce duis consectetur. Justo feugiat porttitor, tempor vivamus, " + "turpis rutrum. Parturient facilisi potenti consectetur, natoque nibh, vel " + "ullamcorper iaculis. Taciti feugiat, lorem ipsum, non vehicula lectus cursus. " + "Dolor, urna convallis lacinia, in natoque interdum, enim vulputate. Justo " + "varius nisl, pharetra imperdiet, at velit tortor. Platea morbi inceptos, " + "volutpat laoreet, ut vehicula aliquam. Penatibus risus, elit gravida erat " + "ullamcorper condimentum. Odio dolor, vulputate imperdiet ad eleifend mollis." + "\r\n" + "\r\n" + "Arcu, elit tempor cursus, vel gravida sed, litora lorem. Tortor, nulla " + "parturient sollicitudin, at dolor nascetur. Elit penatibus interdum, " + "pellentesque tristique orci iaculis, per convallis. Gravida, litora amet " + "efficitur, si vitae ultricies, mus lorem. Torquent mattis posuere, vulputate " + "ligula, eu dictum scelerisque. Cras consectetur sagittis, magnis pulvinar " + "felis volutpat, do parturient. Ante aliquet venenatis, gravida fusce, purus " + "pellentesque. Habitasse condimentum, eleifend euismod, ac magna sagittis mus " + "mattis. Parturient, ante pharetra facilisis, erat litora aliquam. Aliquet " + "bibendum etiam pellentesque, si magnis, himenaeos vulputate blandit. " + "Parturient nibh, turpis volutpat interdum congue morbi. Pharetra, orci amet " + "fermentum, magnis nascetur, do vehicula scelerisque. Odio parturient posuere " + "dis aliquet, mi aliquam ligula augue. Mus mattis vivamus, rutrum at " + "vulputate, suspendisse orci lorem." + "\r\n" + "\r\n" + "Habitasse morbi, pharetra venenatis dictum tempor, eu iaculis tristique. Dis " + "vulputate, convallis blandit, gravida bibendum mus volutpat. Eleifend " + "efficitur habitasse dolor, a vitae porttitor ullamcorper lorem. Rutrum " + "venenatis maximus egestas, orci augue cursus. Purus pellentesque, tempor " + "lacinia, per eleifend erat neque consectetur. Dolor, eu aliquet fusce, si " + "phasellus fringilla sapien amet. Eleifend, sociosqu penatibus ultrices, ac " + "scelerisque euismod. Massa vitae, arcu sollicitudin, et ante parturient " + "lacinia. Sapien phasellus interdum, condimentum semper, tellus gravida. Orci " + "feugiat bibendum congue penatibus, mi morbi nisl volutpat imperdiet praesent " + "convallis. Gravida tristique curabitur pellentesque, at vulputate lacinia " + "mauris varius interdum eleifend. Tempor tincidunt odio penatibus, do " + "ridiculus phasellus ultrices." + "\r\n" + "\r\n"}; diff --git a/sw/cheri/tests/sdcard_tests.hh b/sw/cheri/tests/sdcard_tests.hh new file mode 100644 index 00000000..19f0fdd3 --- /dev/null +++ b/sw/cheri/tests/sdcard_tests.hh @@ -0,0 +1,291 @@ +/** + * Copyright lowRISC contributors. + * Licensed under the Apache License, Version 2.0, see LICENSE for details. + * SPDX-License-Identifier: Apache-2.0 + */ + +#define CHERIOT_NO_AMBIENT_MALLOC +#define CHERIOT_NO_NEW_DELETE +#define CHERIOT_PLATFORM_CUSTOM_UART + +#include +#include +#include + +// clang-format off +#include "../../common/defs.h" +// clang-format on +#include +#include + +#include "../common/console.hh" +#include "../common/filesys-utils.hh" +#include "../common/platform-pinmux.hh" +#include "../common/sdcard-utils.hh" +#include "../common/sonata-devices.hh" + +#include "../tests/test_runner.hh" + +// Lorem Ipsum sample text. +#include "lorem_text.hh" + +using namespace CHERI; + +#define MAX_BLOCKS 0x10 +#define BLOCK_LEN 0x200 + +// Set this for manual operation rather than automated regression testing. +static constexpr bool manual = false; + +// Set this to true to enable diagnostic logging. +static constexpr bool logging = false; + +// Set this to true to emit the `lorem ipsum` sample text for capture and subsequent +// writing to a FAT32-formatted microSD card as `LOREM.IPS` within the root directory. +static constexpr bool emitText = false; + +static uint8_t fileBuffer[BLOCK_LEN]; + +// Dump out a sequence of bytes as hexadecimal and ASCII text. +static void dump_bytes(const uint8_t *buf, size_t blkBytes, Log &log) { + for (size_t off = 0u; off < blkBytes; ++off) { + log.print("{:02x}", buf[off]); + if ((off & 0xfu) == 0xfu) { + log.print(" : "); + for (size_t aoff = (off & ~0xfu); aoff <= off; aoff++) { + char text[2]; + text[0] = buf[aoff]; + if (!isprint(text[0])) text[0] = '.'; + text[1] = '\0'; + log.print(text); + } + log.println(""); + } else { + log.print(" "); + } + } +} + +// Compare a sequence of bytes against a reference, returning the number of mismatches. +static int compare_bytes(const char *ref, unsigned &offset, const uint8_t *data, size_t len, Log &log) { + unsigned mismatches = 0u; + while (len-- > 0u) { + // Compare retrieved data byte against reference text. + uint8_t dch = *data++; + char ch = ref[offset++]; + // It's quite likely that the data stored on the card is LF-terminated rather than + // the CR,LF termination that we expect, so we permit that and continue checking. + if ((char)dch == '\n' && ch == '\r') { + ch = ref[offset++]; + } + mismatches += (char)dch != ch; + } + return mismatches; +} + +// Read and report the properties of the SD card itself (CSD and CID). +static int read_card_properties(SdCard &sd, Log &log, bool logging = true) { + int failures = 0u; + uint8_t buf[16]; + for (int i = 0; i < sizeof(buf); i++) { + buf[i] = 0xbd; + } + log.print(" Reading Card Specific Data (CSD)"); + if (sd.read_csd(buf, sizeof(buf))) { + if (logging) { + dump_bytes(buf, sizeof(buf), log); + } + // The final byte contains a CRC7 field within its MSBs. + uint8_t crc = 1u | (SdCard::calc_crc7(buf, sizeof(buf) - 1u) << 1); + failures += (crc != buf[sizeof(buf) - 1u]); + } else { + failures++; + } + write_test_result(log, failures); + + for (int i = 0; i < sizeof(buf); i++) { + buf[i] = 0xbd; + } + log.print(" Reading Card Identification (CID)"); + if (sd.read_cid(buf, sizeof(buf))) { + if (logging) { + dump_bytes(buf, sizeof(buf), log); + } + // The final byte contains a CRC7 field within its MSBs. + uint8_t crc = 1u | (SdCard::calc_crc7(buf, sizeof(buf) - 1u) << 1); + failures += (crc != buf[sizeof(buf) - 1u]); + } else { + failures++; + } + write_test_result(log, failures); + + return failures; +} + +/** + * Run the set of SD card tests; test card presence, read access to the card itself + * and then the data stored within the flash. The test expects a FAT32-formatted + * SD card with a sample file called `LOREM.IPS` in the root directory. + */ +void sdcard_tests(CapRoot &root, Log &log) { + // Have we been asked to emit the sample text? + if (emitText) { + log.println( + "Capture everything between the dotted lines, being careful not " + "to introduce any additional line breaks."); + log.println("--------"); + log.print(lorem_text); + log.println("--------"); + log.println( + "Each of these single-line paragraphs shall be CR,LF terminated " + "and followed by a blank line."); + log.println("This includes the final one, and thus the file itself ends with a blank line."); + log.println("The file should be 4,210 bytes in length."); + } + + // The SPI controller talkes to the microSD card in SPI mode. + Capability spi = root.cast(); + spi.address() = SPI_ADDRESS + 2 * SPI_RANGE; + spi.bounds() = SPI_BOUNDS; + + // We need to use the pinmux to select the microSD card for SPI controller 2 reads (CIPO), + // as well as preventing outbound traffic to the microSD card also reaching the application + // flash (for safety; it _should_ ignore traffic not accompanied by Chip Select assertion). + auto pin_output = pin_sinks_ptr(root); + SonataPinmux::Sink appspi_cs = pin_output->get(SonataPinmux::PinSink::appspi_cs); + SonataPinmux::Sink appspi_clk = pin_output->get(SonataPinmux::PinSink::appspi_cs); + SonataPinmux::Sink appspi_d0 = pin_output->get(SonataPinmux::PinSink::appspi_d0); + SonataPinmux::Sink microsd_dat3 = pin_output->get(SonataPinmux::PinSink::microsd_dat3); + SonataPinmux::Sink microsd_clk = pin_output->get(SonataPinmux::PinSink::microsd_clk); + SonataPinmux::Sink microsd_cmd = pin_output->get(SonataPinmux::PinSink::microsd_cmd); + + auto block_input = block_sinks_ptr(root); + SonataPinmux::Sink spi_0_cipo = block_input->get(SonataPinmux::BlockSink::spi_0_cipo); + + // Suppress traffic to the application flash. + appspi_cs.disable(); + appspi_clk.disable(); + appspi_d0.disable(); + // Direct SPI controller 2 to drive the microSD pins. + microsd_dat3.default_selection(); + microsd_clk.default_selection(); + microsd_cmd.default_selection(); + // Select microSD CIPO as SPI controller input. + constexpr uint8_t PmuxSpi0CipoToSdDat0 = 2; + spi_0_cipo.select(PmuxSpi0CipoToSdDat0); + + // We need to use the GPIO to detect card presence. + Capability gpio = root.cast(); + gpio.address() = GPIO_ADDRESS; + gpio.bounds() = GPIO_BOUNDS; + + // microSD card is on Chip Select 1 (0 goes to the application flash). + constexpr unsigned csBit = 1u; + // microSD card detection bit is on input 16. + constexpr unsigned detBit = 16u; + + // Initialise SD card access, using CRCs on all traffic. + SdCard sd(spi, gpio, csBit, detBit, true); //, &log); + + int failures = 0u; + if (!sd.present()) { + if (manual) { + // Wait until a card is detected. + log.println("Please insert a microSD card into the slot..."); + while (!sd.present()); + } else { + log.println("No microSD card detected"); + failures++; + } + } + if (sd.present()) { + sd.init(); + + log.println("Reading card properties.... "); + failures += read_card_properties(sd, log); + + log.println("Reading card contents.... "); + fileSysUtils fs; //(&log); + + failures += !fs.init(&sd); + write_test_result(log, failures); + + if (!failures) { + // List the files and subdirectories in the root directory. + log.println("Reading root directory.... "); + fileSysUtils::dirHandle dh = fs.rootdir_open(); + if (dh == fileSysUtils::kInvalidDirHandle) { + failures++; + } else { + fileSysUtils::dirEntry entry; + while (fs.dir_next(dh, entry)) { + // TODO: We should have a cleaner/robust way to dump string parameters! + log.print((char *)entry.customDefined); + log.println(" length {:#x} cluster {:#x}", entry.dataLength, entry.firstCluster); + } + fs.dir_close(dh); + } + write_test_result(log, failures); + + // Locate and check the LOREM.IPS test file in the root directory. + fileSysUtils::fileHandle fh = fs.file_open("LOREM IPS"); + if (fh == fileSysUtils::kInvalidFileHandle) { + log.println("Unable to locate file"); + failures++; + } else { + // Determine the length of the file. + ssize_t fileLen = fs.file_length(fh); + if (fileLen < 0) { + log.println("Failed to read file length"); + failures++; + } else { + log.println("File is {} byte(s)", fileLen); + } + uint32_t sampleOffset = 0u; + while (fileLen > 0 && sampleOffset < sizeof(lorem_text)) { + // Work out how many bytes we can compare. + uint32_t chunkLen = (fileLen >= sizeof(fileBuffer)) ? sizeof(fileBuffer) : fileLen; + if (chunkLen > sizeof(lorem_text) - sampleOffset) { + chunkLen = sizeof(lorem_text) - sampleOffset; + } + // Read data from the SD card into our buffer. + size_t read = fs.file_read(fh, fileBuffer, chunkLen); + if (read != chunkLen) { + // We did not read the expected number of bytes. + log.println("File read did not return the requested number of bytes"); + failures++; + } + if (logging) { + dump_bytes(fileBuffer, chunkLen, log); + } + // Compare this data against the sample text. + failures += compare_bytes(lorem_text, sampleOffset, fileBuffer, chunkLen, log); + fileLen -= chunkLen; + } + log.println("Done text comparison"); + // If we have not compared the entire file, count that as a failure. + failures += (fileLen > 0); + fs.file_close(fh); + } + write_test_result(log, failures); + } else { + log.println("No valid Master Boot Record found (signature not detected)"); + failures++; + } + } + write_test_result(log, failures); + check_result(log, !failures); + + // Be a good citizen and put the pinmux back in its default state. + microsd_dat3.disable(); + microsd_clk.disable(); + microsd_cmd.disable(); + // Suppress traffic to the application flash. + appspi_cs.default_selection(); + appspi_clk.default_selection(); + appspi_d0.default_selection(); + // Direct SPI controller 2 to drive the microSD pins. + // Select microSD CIPO as SPI controller input. + constexpr uint8_t PmuxSpi0CipoToAppSpiD1 = 1; + spi_0_cipo.select(PmuxSpi0CipoToAppSpiD1); +} diff --git a/sw/cheri/tests/test_runner.cc b/sw/cheri/tests/test_runner.cc index 5d18299a..6ad9d6a4 100644 --- a/sw/cheri/tests/test_runner.cc +++ b/sw/cheri/tests/test_runner.cc @@ -22,6 +22,7 @@ #include "../common/uart-utils.hh" #include "hyperram_tests.hh" #include "i2c_tests.hh" +#include "sdcard_tests.hh" #include "spi_tests.hh" #include "pinmux_tests.hh" #include "pwm_tests.hh" @@ -39,6 +40,7 @@ extern "C" void entry_point(void *rwRoot) { pwm_tests(root, log); uart_tests(root, log); i2c_tests(root, log); + sdcard_tests(root, log); spi_tests(root, log); hyperram_tests(root, log); usbdev_tests(root, log);