diff --git a/doc/guide/sdcard-setup.md b/doc/guide/sdcard-setup.md new file mode 100644 index 000000000..c72dc434e --- /dev/null +++ b/doc/guide/sdcard-setup.md @@ -0,0 +1,38 @@ +# Setting up SD card image for 'sdcard_tests.hh' + +To set up a FAT32-formatted SD card image 'sd.img' on a Linux system, the following +may be useful: + +https://github.com/procount/fat32images offers various ready-to-download volume images. +The 1GiB example (nnobs1gb.img.zip) should suffice; rename it to 'sd.img' + +Find the offset of the FAT32 partition: + +> fdisk -u sd.img +``` +Disk sd.img: 1 GiB, 1073741824 bytes, 2097152 sectors +Units: sectors of 1 * 512 = 512 bytes +Sector size (logical/physical): 512 bytes / 512 bytes +I/O size (minimum/optimal): 512 bytes / 512 bytes +Disklabel type: dos +Disk identifier: 0x6c4676d7 + +Device Boot Start End Sectors Size Id Type +noobs1gb.img1 8192 2097151 2088960 1020M c W95 FAT32 (LBA) +``` + +Note the 'Start' value (8192) and multiply it by the sector size (512 bytes), +e.g. 4194304 in the command below: + +Create a mount point within the filing system for the FAT32 partition. +> mkdir mnt_point + +Make the FAT32 partition available. +> sudo mount -o loop,offset=4194304 mnt_point sd.img + +Copy the lorem ipsum text (or other files) into the root directory of the FAT32 partition. +> sudo cp mnt_point/lorem.ips + +Unmount the FAT32 partition. +> sudo umount mnt_point + diff --git a/dv/dpi/spidpi/spi_microsd.cc b/dv/dpi/spidpi/spi_microsd.cc new file mode 100644 index 000000000..a8fa905fd --- /dev/null +++ b/dv/dpi/spidpi/spi_microsd.cc @@ -0,0 +1,334 @@ +// Copyright lowRISC contributors. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +/** + * This DPI model provides a simple implementation of a SPI-connected microSD card. + * It provides enough functionality for the existing tests to run in simulation + * but little more than that. + * + * Single Block Read/Write commands are supported and the normal initialisation + * sequence. + * + * The code expects a file of the name specified to the constructor, e.g. 'sd.img', + * and will operate on that as if it were an SD card, performing block transfers + * to/from that file. + */ + +#include + +#include "spi_microsd.hh" + +// SD command codes. +enum { + CMD_GO_IDLE_STATE = 0, + CMD_SEND_OP_COND = 1, + CMD_SEND_IF_COND = 8, + CMD_SEND_CSD = 9, + CMD_SEND_CID = 10, + CMD_STOP_TRANSMISSION = 12, + CMD_SET_BLOCKLEN = 16, + CMD_READ_SINGLE_BLOCK = 17, + CMD_READ_MULTIPLE_BLOCK = 18, + CMD_WRITE_SINGLE_BLOCK = 24, + CMD_WRITE_MULTIPLE_BLOCK = 25, + SD_SEND_OP_COND = 41, + CMD_APP_CMD = 55, + CMD_READ_OCR = 58, + CMD_CRC_ON_OFF = 59, +}; + +// The CI system presently does not have the means to obtain/create a suitable +// SD card image ('sd.img') for use in simulation so set this to 'false' to indicate +// the presence of a card and leave the test code to spot from the CID/CSD that it +// is running in simulation. +static const bool kMustHaveSD = false; + +void spi_microsd::reset() { + spidpi::reset(); + cmdBytes = 0u; + responding = false; + reading = false; + writing = false; + + // Initialise the CID + memset(cid, 0, sizeof(cid)); + // Provide some suggestion of valid details if we have an 'sd.img'; the CI system + // checks the first 3 bytes in simulation to ascertain whether it should attempt + // block transfers and filing system testing. + if (sd) { + cid[0] = 0x70; + cid[1] = 'l'; + cid[2] = 'R'; + } + cid[15] = update_crc7(0, cid, 15); + // Initialise the CSD + memset(csd, 0, sizeof(csd)); + csd[15] = update_crc7(0, csd, 15); +} + +void spi_microsd::writeByte(uint8_t inByte, uint32_t oobIn) { + logText("microSD %0x\n", inByte); + + if (writing) { + switch (writeState) { + case WriteState_StartToken: + if (inByte == 0xfeu) { + writeState = WriteState_DataBytes; + // CRC16 is seeded with all zero bits. + writeCrc16 = 0u; + } + break; + case WriteState_DataBytes: + if (writeBytes < sizeof(writeBuf)) { + writeBuf[writeBytes] = inByte; + } + writeCrc16 = update_crc16(writeCrc16, &inByte, 1u); + if (++writeBytes >= kBlockLen) { + writeState = WriteState_CRC0; + } + break; + case WriteState_CRC0: + if (inByte != (uint8_t)(writeCrc16 >> 8)) { + logText("Mismatched CRC16 on write data"); + } + writeState = WriteState_CRC1; + break; + case WriteState_CRC1: + if (inByte != (uint8_t)writeCrc16) { + logText("Mismatched CRC16 on write data"); + } + writeState = WriteState_StartToken; + // The data_response is 0xe5 indicating that the write data has been accepted, + // and then '0' bytes whilst busy until Idle again. + sendResponse(8u, ((uint64_t)0x01 << 56) | 0xe5); + writing = false; + // At this point we write out the data block. + if (sd && kBlockLen != fwrite(writeBuf, 1, kBlockLen, sd)) { + logText("Failed writing to SD card"); + } + break; + default: + break; + } + } else { + switch (cmdBytes) { + case 0u: + if ((inByte & 0xc0) == 0x40) { + cmd.cmdCode = inByte & 0x3f; + cmdBytes = 1u; + } + break; + case 5u: + cmd.crc = inByte; + startCommand(); + cmdBytes = 0u; + break; + default: + // Collect the next byte of the address. + cmd.address = (cmd.address << 8) | inByte; + cmdBytes++; + break; + } + } +} + +bool spi_microsd::readByte(uint32_t oobIn, uint8_t &outByte, uint32_t &oobOut) { + if (responding) { + if (rspBytes < rspLen) { + outByte = rsp[rspBytes++]; + // OOB data carries SD card detect - absent (1) or present (0). + oobOut = kMustHaveSD && !sd; + return true; + } + if (reading) { + readState = ReadState_DataBytes; + // CRC16 is seeded with all zero bits. + readCrc16 = 0u; + } + responding = false; + } + if (reading) { + // Checksum? + switch (readState) { + case ReadState_DataBytes: { + switch (cmd.cmdCode) { + case CMD_SEND_CID: + outByte = cid[readBytes]; + readCrc16 = update_crc16(readCrc16, &outByte, 1u); + if (++readBytes >= sizeof(cid)) { + readState = ReadState_CRC0; + } + break; + case CMD_SEND_CSD: + outByte = csd[readBytes]; + readCrc16 = update_crc16(readCrc16, &outByte, 1u); + if (++readBytes >= sizeof(csd)) { + readState = ReadState_CRC0; + } + break; + case CMD_READ_MULTIPLE_BLOCK: + case CMD_READ_SINGLE_BLOCK: { + // Return the next byte from the memory. + int ch = 0xffu; + if (sd) { + ch = fgetc(sd); + if (ch == EOF) { + logText("Failed reading from SD card"); + } + } + outByte = ch; + readCrc16 = update_crc16(readCrc16, &outByte, 1u); + if (!(++readBytes & (kBlockLen - 1u))) { + readState = ReadState_CRC0; + } + } + break; + default: + break; + } + } + break; + case ReadState_CRC0: + outByte = (uint8_t)(readCrc16 >> 8); + readState = ReadState_CRC1; + break; + case ReadState_CRC1: + readBytes = 0u; + outByte = (uint8_t)readCrc16; + readState = ReadState_DataBytes; + break; + default: + outByte = 0xffu; + reading = false; + break; + } + + // OOB data carries SD card detect - absent (1) or present (0). + oobOut = kMustHaveSD && !sd; + logText("Read byte 0x%0x\n", outByte); + return true; + } else { + return false; + } +} + +// Transition on one or more CS lines. +void spi_microsd::csChanged(bool csAsserted, uint32_t oobIn) { + cmdBytes = 0u; + responding = false; + reading = false; +} + +// TODO: prevent command processing trying to read from non-extant SD card; +// reject out-of-order command codes etc. +void spi_microsd::startCommand() { + logText("Starting command %02x,%08x,%02x", cmd.cmdCode, cmd.address, cmd.crc); + switch (cmd.cmdCode) { + case CMD_GO_IDLE_STATE: + sendResponse(1u, 0x01); + break; + case CMD_SEND_IF_COND: + sendResponse(5u, 0); + break; + + // Card Identification Data and Card Specific Data may be handled similarly. + case CMD_SEND_CID: + case CMD_SEND_CSD: + sendResponse(2u, 0xfe00); + reading = true; + readState = ReadState_CmdStatus; + readBytes = 0u; + break; + + case CMD_STOP_TRANSMISSION: + break; + case CMD_SET_BLOCKLEN: + sendResponse(1u, 0); + break; + case CMD_READ_MULTIPLE_BLOCK: + case CMD_READ_SINGLE_BLOCK: + // TODO: introduce error code responses. + if (sd) { + logText("Reading from block %08x", cmd.address); + fseek(sd, cmd.address << kLog2BlockLen, SEEK_SET); + } + sendResponse(2u, 0xfe00); + reading = true; + readState = ReadState_CmdStatus; + readBytes = 0u; + break; + case CMD_WRITE_MULTIPLE_BLOCK: + case CMD_WRITE_SINGLE_BLOCK: + // TODO: introduce error code responses. + if (sd) { + logText("Writing to block %08x", cmd.address); + fseek(sd, cmd.address << kLog2BlockLen, SEEK_SET); + } + sendResponse(1u, 0xfe); + writing = true; + writeState = WriteState_StartToken; + writeBytes = 0u; + break; + case SD_SEND_OP_COND: + sendResponse(1, 0x00); + break; + case CMD_APP_CMD: + sendResponse(1, 0x00); + break; + case CMD_READ_OCR: + sendResponse(5u, 0); + break; + + default: + // Treat anything else as an illegal command. + sendResponse(1u, 0x05); + break; + } +} + +void spi_microsd::sendResponse(unsigned len, uint64_t d) { + responding = true; + rspBytes = 0u; + rspLen = len; + rsp[0] = (uint8_t)d; + rsp[1] = (uint8_t)(d >> 8); + rsp[2] = (uint8_t)(d >> 16); + rsp[3] = (uint8_t)(d >> 24); + rsp[4] = (uint8_t)(d >> 32); + rsp[5] = (uint8_t)(d >> 40); + rsp[6] = (uint8_t)(d >> 48); + rsp[7] = (uint8_t)(d >> 56); +} + +// Update the running CRC7 value for a series of bytes (command/response); +// used to generate the CRC for a command or check that of a response if CRC_ON mode is used. +// +// Note: the CRC7 is held in the MSBs of the byte, which should be set to 0 for the first +// invocation, and then the trailing LSB is always 1. +uint8_t spi_microsd::update_crc7(uint8_t crc, const uint8_t *data, size_t len) { + while (len-- > 0u) { + uint8_t d = *data++; + for (unsigned b = 0u; b < 8u; b++) { + crc = (crc << 1) ^ (((crc ^ d) & 0x80u) ? 0x12u : 0u); + d <<= 1; + } + } + // 7 MSBs contain the CRC residual and the trailing bit is 1. + return (crc | 1u); +} + +// Calculate the CRC16 value for a series of bytes (data blocks); +// used for generation or checking, if CRC_ON mode is used. +// +// Note: the CRC shall be zero for the first invocation. +uint16_t spi_microsd::update_crc16(uint16_t crc, const uint8_t *data, size_t len) { + while (len-- > 0u) { + uint16_t d = (uint16_t)*data++ << 8; + for (unsigned b = 0u; b < 8u; b++) { + crc = (crc << 1) ^ (((crc ^ d) & 0x8000u) ? 0x1021u : 0u); + d <<= 1; + } + } + return crc; +} diff --git a/dv/dpi/spidpi/spi_microsd.hh b/dv/dpi/spidpi/spi_microsd.hh new file mode 100644 index 000000000..36e559986 --- /dev/null +++ b/dv/dpi/spidpi/spi_microsd.hh @@ -0,0 +1,107 @@ +// Copyright lowRISC contributors. +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +#include +#include +#include "spidpi.hh" + +// -------------------------- SPI microSD model ------------------------------- +class spi_microsd : public spidpi { +public: + spi_microsd(unsigned dataW, // Number of data lines. + unsigned oobInW, // Width of Out-Of-Band input data (bits). + unsigned oobOutW, // Width of Out-Of-Band output data (bits). + const char *sdFile, // Filename of the SD card image. + bool log = false) : // Enable diagnostic logging? + spidpi(dataW, oobInW, oobOutW, log) { + assert(sdFile); + logText("microSD model attempting to open image '%s'\n", sdFile); + sd = fopen(sdFile, "r+b"); + // If the open fails, it is not an error at this point; it is treated as analogous + // to there being no usable SD card present. + reset(); + } + +protected: + // Device reset. + virtual void reset(); + + // Write a byte to the SPI flash. + virtual void writeByte(uint8_t inByte, uint32_t oobIn); + + // Read a byte of data from the SPI flash. + virtual bool readByte(uint32_t oobIn, uint8_t &outByte, uint32_t &oobOut); + + // Change in the state of the CS line. + virtual void csChanged(bool csAsserted, uint32_t oobIn); + + // SD command received. + virtual void startCommand(); + + // Send response from SD card. + virtual void sendResponse(unsigned len, uint64_t d); // Up to 7 bytes. + +private: + // SPI mode uses only 512-byte blocks, so we choose this as the block size. + static constexpr unsigned kLog2BlockLen = 9u; + static constexpr unsigned kBlockLen = 1u << kLog2BlockLen; + + // SD card commands are 48 bits (6 bytes) with the data being transmitted MSB first. + struct { + uint8_t cmdCode; + uint32_t address; + uint8_t crc; + } cmd; + + // The number of bytes thus far received. + uint8_t cmdBytes; + + // Responding to a command? + bool responding; + // sendResponse can supply 8 bytes. + uint8_t rsp[8u]; + unsigned rspBytes; + unsigned rspLen; + + // Reading data from the microSD card? + bool reading; + enum { + ReadState_CmdStatus, + ReadState_DataBytes, + ReadState_CRC0, + ReadState_CRC1 + } readState; + // Number of bytes returned within the current data packet. + uint32_t readBytes; + // CRC16 for read data block. + uint16_t readCrc16; + + // Writing data to the microSD card? + bool writing; + enum { + WriteState_StartToken, + WriteState_DataBytes, + WriteState_CRC0, + WriteState_CRC1 + } writeState; + // Number of bytes written within the current data packet. + uint32_t writeBytes; + // Collected write data for validation before anything is written. + uint8_t writeBuf[kBlockLen]; + // CRC16 for write data block; updated as the bytes are received. + uint16_t writeCrc16; + + // Card Identification Data, including trailing CRC7 byte. + uint8_t cid[16]; + // Card Specific Data, including trailing CRC7 byte. + uint8_t csd[16]; + + // Storage medium (raw microSD card image; format and contents unspecified). + // If the file handle is NULL this means that there is no microSD card present. + FILE *sd; + + // Utility functions for performing a running update of CRC values. + static uint8_t update_crc7(uint8_t crc, const uint8_t *data, size_t len); + static uint16_t update_crc16(uint16_t crc, const uint8_t *data, size_t len); +}; diff --git a/dv/dpi/spidpi/spidpi.cc b/dv/dpi/spidpi/spidpi.cc index f545dec98..dae198724 100644 --- a/dv/dpi/spidpi/spidpi.cc +++ b/dv/dpi/spidpi/spidpi.cc @@ -6,8 +6,9 @@ #include #include -#include "spi_lcd.hh" #include "spi_flash.hh" +#include "spi_lcd.hh" +#include "spi_microsd.hh" void spidpi::reset() { // Write data to device (COPI). @@ -91,6 +92,8 @@ void *spidpi_create(const char *id, // Bus identification. ctx = new spi_flash(dataW, oobInW, oobOutW, 0xef4019); } else if (!strcmp(id, "lcd")) { ctx = new spi_lcd(dataW, oobInW, oobOutW); + } else if (!strcmp(id, "microsd")) { + ctx = new spi_microsd(dataW, oobInW, oobOutW, "sd.img"); } else if (!strcmp(id, "pmod_sf3")) { ctx = new spi_flash(dataW, oobInW, oobOutW, 0x20ba19); } else { diff --git a/dv/verilator/top_verilator.sv b/dv/verilator/top_verilator.sv index 8c390cf02..1bce3a8e0 100644 --- a/dv/verilator/top_verilator.sv +++ b/dv/verilator/top_verilator.sv @@ -121,17 +121,11 @@ module top_verilator (input logic clk_i, rst_ni); wire appspi_cs = out_to_pins[OUT_PIN_APPSPI_CS]; // microSD card interface. - // Note: no DPI model presently. - wire microsd_clk = out_to_pins[OUT_PIN_MICROSD_CLK]; - // SPI mode: CIPO - wire microsd_dat0 = 1'b1; - assign in_from_pins[IN_PIN_MICROSD_DAT0] = microsd_dat0; - // SPI mode: CS_N - wire microsd_dat3 = out_to_pins[OUT_PIN_MICROSD_DAT3]; - // SPI mode: COPI - wire microsd_cmd = out_to_pins[OUT_PIN_MICROSD_CMD]; - // pulled high to indicate the _absence_ of a microSD card. - wire microsd_det = 1'b1; + wire microsd_clk; // SPI mode: SCLK + wire microsd_dat0; // SPI mode: CIPO + wire microsd_dat3; // SPI mode: CS_N + wire microsd_cmd; // SPI mode: COPI + wire microsd_det; // microSD card detection; 0 = card present. // LCD interface. wire lcd_rst; @@ -154,7 +148,7 @@ module top_verilator (input logic clk_i, rst_ni); // None of these signals is used presently. wire unused_io_ = ^{mb1, ah_tmpio10, rph_g18, rph_g17, rph_g16_ce2, rph_g8_ce0, rph_g7_ce1, - usrLed, microsd_clk, microsd_cmd, microsd_dat3}; + usrLed}; // Reporting of CHERI enable/disable and any exceptions that occur. wire [CheriErrWidth-1:0] cheri_err; @@ -212,6 +206,12 @@ module top_verilator (input logic clk_i, rst_ni); assign uart_aux_tx = out_to_pins[OUT_PIN_SER1_TX]; assign rs485_tx = out_to_pins[OUT_PIN_RS485_TX]; + // Traffic to/from microSD card. + assign microsd_cmd = out_to_pins[OUT_PIN_MICROSD_CMD ]; + assign microsd_clk = out_to_pins[OUT_PIN_MICROSD_CLK ]; + assign microsd_dat3 = out_to_pins[OUT_PIN_MICROSD_DAT3]; + assign in_from_pins[IN_PIN_MICROSD_DAT0] = microsd_dat0; + // Output I2C traffic to the RPi HAT ID EEPROM. assign {scl_rpi0_o, scl_rpi0_oe} = {inout_to_pins[INOUT_PIN_RPH_G1], inout_to_pins_en[INOUT_PIN_RPH_G1]}; @@ -528,6 +528,25 @@ module top_verilator (input logic clk_i, rst_ni); .oob_out ( ) // not used. ); + // SPI connection to microSD card. + spidpi #( + .ID ("microsd"), + .NDevices (1), + .DataW (1), + .OOB_InW (1), + .OOB_OutW (1) + ) u_spidpi_microsd ( + .rst_ni (rst_ni), + + .sck (microsd_clk), + .cs (microsd_dat3), + .copi (microsd_cmd), + .cipo (microsd_dat0), + + .oob_in ( ), + .oob_out (microsd_det) + ); + // SPI connection to PMOD SF3 flash via PMOD1 pins spidpi #( .ID ("pmod_sf3"), diff --git a/sonata.core b/sonata.core index 914c916b9..9f98571ef 100644 --- a/sonata.core +++ b/sonata.core @@ -50,6 +50,8 @@ filesets: - dv/dpi/spidpi/spi_flash.hh: { file_type: cppSource, is_include_file: true } - dv/dpi/spidpi/spi_lcd.cc: { file_type: cppSource } - dv/dpi/spidpi/spi_lcd.hh: { file_type: cppSource, is_include_file: true } + - dv/dpi/spidpi/spi_microsd.cc: { file_type: cppSource } + - dv/dpi/spidpi/spi_microsd.hh: { file_type: cppSource, is_include_file: true } - dv/verilator/top_verilator.sv: { file_type: systemVerilogSource } - dv/verilator/sonata_system.cc: { file_type: cppSource } - dv/verilator/sonata_system.hh: { file_type: cppSource, is_include_file: true } diff --git a/sw/cheri/checks/CMakeLists.txt b/sw/cheri/checks/CMakeLists.txt index 519110028..52bede39a 100644 --- a/sw/cheri/checks/CMakeLists.txt +++ b/sw/cheri/checks/CMakeLists.txt @@ -6,6 +6,7 @@ set(CHECKS gpio_check.cc lcd_check.cc uart_check.cc + sdraw_check.cc spi_test.cc system_info_check.cc revocation_test.cc diff --git a/sw/cheri/checks/sdraw_check.cc b/sw/cheri/checks/sdraw_check.cc new file mode 100644 index 000000000..9ad5ea91b --- /dev/null +++ b/sw/cheri/checks/sdraw_check.cc @@ -0,0 +1,177 @@ +/** + * Copyright lowRISC contributors. + * Licensed under the Apache License, Version 2.0, see LICENSE for details. + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * SD card raw write/read block test; this is a MANUAL check that should be performed only + * on a card THAT DOES NOT CONTAIN ANY DATA OF VALUE. + * + * It does not require formatting or any particular file system to be present. + * + * If there is a card inserted when the test starts up, you will be asked to remove the + * card and then insert it after the warning message is displayed; this is to decrease the + * likelihood of inadvertent data loss. + */ + +#define CHERIOT_NO_AMBIENT_MALLOC +#define CHERIOT_NO_NEW_DELETE +#define CHERIOT_PLATFORM_CUSTOM_UART + +#include "../common/sonata-devices.hh" +// clang-format off +#include +#include +// clang-format on +#include "../common/console.hh" +#include "../common/sdcard-utils.hh" +#include "../common/uart-utils.hh" + +// Maximum number of blocks per transfer; reasonable model of FS activity. +static constexpr unsigned kMaxBlocks = 0x10u; +// Must be a power-of-two. +static_assert(!(kMaxBlocks & (kMaxBlocks - 1u))); + +static constexpr unsigned kBlockLen = 0x200u; + +// Diagnostic logging? +static const bool kLogging = false; + +static uint8_t write_data[kMaxBlocks * kBlockLen]; +static uint8_t read_data[kMaxBlocks * kBlockLen]; +static uint8_t orig_data[kMaxBlocks * kBlockLen]; + +// Compare a sequence of bytes against a reference, returning the number of mismatches. +static int compare_bytes(const uint8_t *ref, const uint8_t *data, size_t len, Log &log) { + unsigned mismatches = 0u; + while (len-- > 0u) { + mismatches += *ref++ != *data++; + } + return mismatches; +} + +[[noreturn]] extern "C" void entry_point(void *rwRoot) { + CapRoot root{rwRoot}; + + auto uart0 = uart_ptr(root); + uart0->init(BAUD_RATE); + WriteUart uart{uart0}; + Log log(uart); + + // The SPI controller talkes to the microSD card in SPI mode. + auto spi = spi_ptr(root, 2); + + // 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. + auto gpio = gpio_ptr(root); + + // 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; + + log.println("SD card raw write/read test."); + + SdCard sd(spi, gpio, csBit, detBit, true); + + // Wait until there is no card in the slot before issuing a warning message. + if (sd.present()) { + log.println("Please remove the microSD card from the slot."); + while (sd.present()) { + // Prevent erroneous optimisation to infinite loop (.l: j l) + asm(""); + } + } + // Wait until a card is detected. + if (!sd.present()) { + log.println("Please insert a microSD card that does not contain any valued data."); + log.println("*** DATA BLOCKS WILL BE OVERWRITTEN ***"); + while (!sd.present()) { + // Prevent erroneous optimisation to infinite loop (.l: j l) + asm(""); + } + } + + log.println("Starting write/read test.... "); + + sd.init(); + + // Initialise random number generation. + ds::xoroshiro::P64R32 prng; + prng.set_state(0xDEADBEEF, 0xBAADCAFE); + + const unsigned numOps = 0x10u; + int failures = 0; + for (unsigned b = 0u; !failures && b < numOps; b++) { + // Choose a random initial block. + uint32_t blk = (uint16_t)prng(); + // Choose a random transfer length. + unsigned num = 1 + ((kMaxBlocks - 1u) & prng()); + log.println("Testing {} block(s) from block {} onwards", num, blk); + // Collect the data from those blocks. + log.println("Reading original data..."); + failures += !sd.read_blocks(blk, orig_data, num); + if (!failures) { + if (kLogging) { + log.println("Original data:"); + dump_bytes(log, orig_data, num * kBlockLen); + } + // Randomise the data that we're going to write. + for (unsigned idx = 0u; idx < num * kBlockLen; idx++) { + write_data[idx] = (uint8_t)prng(); + } + if (kLogging) { + log.println("Write data:"); + dump_bytes(log, write_data, num * kBlockLen); + } + log.println("Writing..."); + // Write out the data to the chosen block. + failures += !sd.write_blocks(blk, write_data, num); + if (!failures) { + log.println("Reading back..."); + // Read it back. + failures += !sd.read_blocks(blk, read_data, num); + if (kLogging) { + log.println("Read data:"); + dump_bytes(log, read_data, num * kBlockLen); + } + // Check each of the read bytes against what we tried to write. + failures += compare_bytes(write_data, read_data, num * kBlockLen, log); + // Try to put the original data back even if there was a failure. + log.println("Restoring..."); + failures += !sd.write_blocks(blk, orig_data, num); + } + } + } + write_test_result(log, failures); + + while (true) { + asm(""); + } +} diff --git a/sw/cheri/common/console.hh b/sw/cheri/common/console.hh index dfcb54784..287d344bf 100644 --- a/sw/cheri/common/console.hh +++ b/sw/cheri/common/console.hh @@ -39,3 +39,36 @@ using Log = reisfmt::Fmt; } set_console_mode(log, CC_RESET); } + +// Dump out a sequence of bytes as hexadecimal and ASCII text. +[[maybe_unused]] static void dump_bytes(Log& log, const uint8_t* buf, size_t blkBytes) { + 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(" "); + } + } +} + +// Emit the UCS-2 long filename in a readable form; we cannot do this properly; we're just +// handling ASCII in practice. +[[maybe_unused]] static void write_str_ucs2(Log& log, const uint16_t* ucs, size_t ucs_max = ~0u) { + size_t idx = 0u; + char str[2]; + str[1] = '\0'; + while (idx < ucs_max && ucs[idx]) { + str[0] = (ucs[idx] >= 0x20u && ucs[idx] < 0x80u) ? ucs[idx] : '.'; + log.print(str); + idx++; + } +} diff --git a/sw/cheri/common/filesys-utils.hh b/sw/cheri/common/filesys-utils.hh new file mode 100644 index 000000000..aa109e0a7 --- /dev/null +++ b/sw/cheri/common/filesys-utils.hh @@ -0,0 +1,901 @@ +/** + * 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; this is a 512-byte + // block as required by SPI mode SD card access, which is conveniently enough to hold the + // longest LFN (255 UCS-2 characters, plus terminator) after initialisation. + union { + uint8_t dataBuffer[kBlockLen]; + uint16_t nameBuffer[0x100u]; + } buf; + + // 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; + } + + // Unfortunately FAT stores the literal values for bytes/sector and sectors/cluster but only + // powers of two are permitted. + static inline uint8_t floor_log2(uint16_t n) { + uint8_t shift = 0u; + while (n > 1u) { + n >>= 1; + shift++; + } + return shift; + } + + 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; + + // Flags specifying the type of directory access required. + enum dirFlags { + DirFlag_Raw = 1u, + DirFlag_IncludeDeleted = 2u, + DirFlag_IncludeHidden = 4u, + + DirFlags_Default = 0u, + }; + + // Directory entry type flags; this just makes the most common types of entries more accessible. + enum dirEntryFlags { + DirEntryFlag_Deleted = 1u, + DirEntryFlag_Hidden = 2u, + DirEntryFlag_VolumeLabel = 4u, + DirEntryFlag_Subdirectory = 8u, + DirEntryFlag_HasLongName = 0x10u + }; + + // Description of an entry within a directory object. + struct dirEntry { + dirEntryFlags flags; + uint8_t entryType; + // Short name of this object (8.3 format) + // Note: these fields are padded with spaces (0x20) and there is no NUL terminator. + uint8_t shortName[8]; + uint8_t shortExt[8]; + // See the FAT file system design for the interpretation of the following fields. + uint8_t attribs; + uint8_t userAttribs; + uint8_t createdFine; + uint16_t createdTime; + uint16_t createdDate; + uint16_t modifiedTime; + uint16_t modifiedDate; + uint16_t accessDate; + // Cluster number of the first cluster holding this object's data. + uint32_t firstCluster; + // Length of the object in bytes. + 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. + uint8_t *dataBuffer = buf.dataBuffer; + 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) { + // Only FAT32 partitions (with or without LBA) are supported. + 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(*log, dataBuffer, kBlockLen); + } + + uint16_t bytesPerSector = read16le(&dataBuffer[0xb]); + uint8_t secsPerCluster = dataBuffer[0xd]; + 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); + } + + bytesPerSectorShift = floor_log2(bytesPerSector); + secsPerClusterShift = floor_log2(secsPerCluster); + + uint32_t fatOffset = resvdSectors; + uint32_t clusterHeapOffset = ((resvdSectors + (numFATs * secsPerFAT)) << bytesPerSectorShift) / kBlockLen; + + // TODO: we do not fully cope with a difference between blocks and sectors at present. + 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); + } + + // Open a directory object that started in the given cluster. + 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; + dirs[dh].length = ~0u; // A special directory entry marks its end. + dirs[dh].currCluster = cluster; + dirs[dh].firstCluster = cluster; + return dh; + } + + // Return the next object within a directory, including optionally the full name of the object + // (LFN support). If 'ucs' is null then the UCS-2 name is not returned. + // + // The returned characters are UCS-2 (not ASCII bytes) and a Long FileName may consist of up to + // 255 UCS-2 characters. + bool dir_next(dirHandle dh, dirEntry &entry, dirFlags flags = DirFlags_Default, uint16_t *ucs = nullptr, + size_t ucs_max = 0u) { + if (!dh_valid(dh)) { + return false; + } + + uint8_t entryType; + bool hasLFN = false; + do { + dirEntryFlags entryFlags = dirEntryFlags(0u); + 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(*log, dir_entry, sizeof(dir_entry)); + } + entryType = dir_entry[0]; + + uint8_t attribs = dir_entry[0xb]; + + // Are we required to return this entry? + // - _Raw demands absolutely no processing; _even_ the end of directory entry is returned. + // + // Ordinarily Deleted/Hidden files will be skipped, but the following flags override that + // behaviour: + // - _IncludeDeleted + // - _IncludeHidden + + // Collect entry flags; + if (hasLFN) entryFlags = dirEntryFlags(entryFlags | DirEntryFlag_HasLongName); + if (attribs & 0x08) entryFlags = dirEntryFlags(entryFlags | DirEntryFlag_VolumeLabel); + if (attribs & 0x010) entryFlags = dirEntryFlags(entryFlags | DirEntryFlag_Subdirectory); + if (entryType == 0xe5) entryFlags = dirEntryFlags(entryFlags | DirEntryFlag_Deleted); + + bool entryWanted = true; + if (!(flags & DirFlag_Raw)) { + if (attribs == 0x0fu) { + // Collect any Long FileName prefix entries. + if (ucs) { + // The sequence number allows us to calculate the offset within the buffer. + uint8_t seqNumber = (entryType & 0x1fu); + if (seqNumber >= 0x01 && seqNumber <= 0x14u) { + // Each entry that forms part of the LFN contributes 13 UCS-2 characters, except the + // final one logically (physically first in the directory object) which may include + // a '0x0000' terminator. + uint16_t offset = (seqNumber - 1) * 13; + if (offset < ucs_max) { + uint8_t lastLogical = (entryType & 0x40u); + // Names are limited to 256 characters including the terminator. + size_t len = (lastLogical && seqNumber >= 0x14u) ? 9 : 13; + if (offset + len > ucs_max) { + len = ucs_max - offset; + } + // The UCS-2 name portion is scattered throughout the directory entry for + // compatibility with earlier systems. + copy_bytes((uint8_t *)&ucs[offset], &dir_entry[1], ((len >= 5) ? 5 : len) * 2); + if (len > 5) { + copy_bytes((uint8_t *)&ucs[offset + 5], &dir_entry[0xe], ((len >= 11) ? 6 : (len - 5)) * 2); + if (len > 11) { + copy_bytes((uint8_t *)&ucs[offset + 11], &dir_entry[0x1c], (len - 11) * 2); + } + } + // Ensure that the returned name is NUL-terminated if there is space. + if (lastLogical && (ucs_max - offset > len)) { + ucs[offset + len] = 0; + } + } + } + } + // The LFN entries prefix the regular entry for a given object. + hasLFN = true; + entryWanted = false; + } else { + entryWanted = entryType && (entryType != 0x2e) && include_entry(entryFlags, flags); + if (!entryWanted) { + // After a regular object that is rejected, reset the LFN flag for the following object. + hasLFN = false; + } + } + } + if (entryWanted) { + 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.flags = entryFlags; + entry.entryType = dir_entry[0]; + // The short name of this file. + copy_bytes(entry.shortName, dir_entry, 8); + // File extension for the short name. + copy_bytes(entry.shortExt, &dir_entry[8], 3); + + // Try to be helpful by reinstating the first character. + if (entryFlags & DirEntryFlag_Deleted) entry.shortName[0] = dir_entry[0xd]; + // Also, since 0xe5 is used to mark a deleted entry, a filename that actually starts with + // 0xe5 has historically been encoded using 0x05. + if (entry.shortName[0] == 0x05) entry.shortName[0] = 0xe5; + // If this object does not have a Long FileName but a buffer has been supplied, then + // provide a conversion. + if (ucs && !hasLFN) { + generate_lfn(ucs, ucs_max, entry); + } + + // See the design of the FAT file system for use/interpretation of these fields. + entry.attribs = dir_entry[0xb]; + entry.userAttribs = dir_entry[0xc]; + entry.createdFine = dir_entry[0xd]; + entry.createdTime = read16le(&dir_entry[0xe]); + entry.createdDate = read16le(&dir_entry[0x10]); + entry.accessDate = read16le(&dir_entry[0x12]); + entry.modifiedTime = read16le(&dir_entry[0x16]); + entry.modifiedDate = read16le(&dir_entry[0x18]); + + // These fields are simply enough and important for file/directory access. + entry.firstCluster = cluster; + entry.dataLength = read32le(&dir_entry[0x1c]); + return true; + } + } while (entryType); + + return false; + } + + // Attempt to find an extant object (file/directory) with the given name in the specified directory; + // the search string is ASCIIZ but may be a Long FileName. + // The UCS-2 name may be retrieved in the event of a match. + bool dir_find(dirHandle dh, dirEntry &entry, const char *name, uint16_t *ucs = nullptr, size_t ucs_max = 0u) { + if (!dh_valid(dh)) { + return false; + } + while (dir_next(dh, entry, DirFlags_Default, buf.nameBuffer, sizeof(buf.nameBuffer) / 2)) { + // Using the full name buffer here guarantees that 'dir_next' will have appended a NUL. + if (!ucs2_char_compare(buf.nameBuffer, name, ~0u)) { + if (ucs) { + ucs2_copy(ucs, buf.nameBuffer, ucs_max); + } + return true; + } + } + return false; + } + + // Variant using full UCS-2 filename encoding. + bool dir_find(dirHandle dh, dirEntry &entry, const uint16_t *ucs_name) { + if (!dh_valid(dh)) { + return false; + } + while (dir_next(dh, entry, DirFlags_Default, buf.nameBuffer, sizeof(buf.nameBuffer) / 2)) { + // Using the full name buffer here guarantees that 'dir_next' will have appended a NUL. + if (!ucs2_compare(buf.nameBuffer, ucs_name, ~0u)) { + return true; + } + } + return false; + } + + // Release access to the given directory. + void dir_close(dirHandle dh) { + if (dh < kMaxDirs) { + dirs[dh].flags = 0u; + } + } + + // Object name comparison; UCS-2 in each case. Case-sensitive matching. + int ucs2_compare(const uint16_t *ucs1, const uint16_t *ucs2, size_t len) { + while (len-- > 0) { + uint16_t c2 = *ucs2++; + uint16_t c1 = *ucs1++; + // This handles the termination case too. + if (!c1 || c1 != c2) { + return (int)c1 - (int)c2; + } + } + return 0; + } + + // Object name comparison; ASCII character name against UCS-2; a convenience when matching against + // LFN entries using an ASCIIZ name. + int ucs2_char_compare(const uint16_t *ucs1, const char *s2, size_t len) { + while (len-- > 0) { + uint8_t c2 = (uint8_t)*s2++; + uint16_t c1 = *ucs1++; + // This handles the termination case too. + if (!c1 || c1 != c2) { + return (int)c1 - (int)c2; + } + } + return 0; + } + + // Utility function that copies a UCS-2 name up to an including any terminator, copying no more + // than 'n' characters. + void ucs2_copy(uint16_t *d, const uint16_t *s, size_t n) { + if (n > 0u) { + unsigned idx = 0u; + uint16_t ch; + do { + ch = s[idx]; + d[idx++] = ch; + } while (ch && idx < n); + } + } + + // 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. + // + // Variants accept either ASCIIZ (char) or full UCS-2 name (uint16_t). + template + fileHandle file_open(const T *name) { + // Maintain the pretence of supporting full pathnames; they may be supported at some point. + if (*name == '/' || *name == '\\') name++; + + fileHandle fh = kInvalidFileHandle; + dirHandle dh = rootdir_open(); + if (dh_valid(dh)) { + dirEntry entry; + if (dir_find(dh, entry, name)) { + fh = file_open(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; } + + // Set the read position within a file. + bool file_seek(fileHandle fh, uint32_t offset) { + if (!fh_valid(fh)) { + return 0u; + } + return object_seek(files[fh], offset); + } + + // 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); } + + private: + // We should perhaps convert to lower case only if the entire name is upper case; we do not have + // access to a 'tolower' implementation. + static inline uint16_t as_lower_case(uint8_t ch) { return (ch >= 'A' && ch <= 'Z') ? (ch - 'A' + 'a') : ch; } + + // Generate a Long FileName from a short form if no long form is available. + static void generate_lfn(uint16_t *ucs, size_t ucs_max, const dirEntry &entry) { + unsigned idx = 0u; + // Short name. + while (ucs_max > 0u && idx < 8u && entry.shortName[idx] > 0x20u) { + *ucs++ = as_lower_case(entry.shortName[idx++]); + ucs_max--; + } + // Period separator between short name and extension. + if (ucs_max > 0u && entry.shortExt[0u] > 0x20u) { + *ucs++ = '.'; + ucs_max--; + } + // Short extension. + idx = 0u; + while (ucs_max > 0u && idx < 3u && entry.shortExt[idx] > 0x20u) { + *ucs++ = as_lower_case(entry.shortExt[idx++]); + ucs_max--; + } + // NUL termination. + if (ucs_max > 0U) { + *ucs = 0; + } + } + + // Decide whether an entry with the given flags shall be returned by a directory traversal. + static inline bool include_entry(dirEntryFlags entryFlags, dirFlags flags) { + return (!(entryFlags & DirEntryFlag_Deleted) || (flags & DirFlag_IncludeDeleted)) && + (!(entryFlags & DirEntryFlag_Hidden) || (flags & DirFlag_IncludeHidden)) && + !(entryFlags & DirEntryFlag_VolumeLabel); + } +}; diff --git a/sw/cheri/common/sdcard-utils.hh b/sw/cheri/common/sdcard-utils.hh new file mode 100644 index 000000000..73a9a185e --- /dev/null +++ b/sw/cheri/common/sdcard-utils.hh @@ -0,0 +1,472 @@ +/** + * 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" + +/** + * SPI mode SD card driver that provides basic detection of microSD card presence, block-level read access + * to the card, and reading of the Card Identification Data (CID) and Card Specific Data (CSD). + * + * 'Part 1 Simplified' Physical Layer Simplified Specification: https://www.sdcard.org/downloads/pls/ + */ + +class SdCard { + private: + // Access to SPI controller. + volatile SonataSpi *spi; + // Access to GPIO block (required for SD card detection). + volatile SonataGpioBase *gpio; + // Chip select (single bit set). + uint32_t cs; + // SD card `detect` pin (single bit set). + uint32_t det; + // CRC_ON mode used? + // - this is off by default for SPI mode, but may be enabled for error detection in both the + // host and the card. + bool crcOn; + // Access to diagnostic logging. + Log *log; + + // We need to clock the device repeatedly at startup; this test pattern is used to ensure that + // we keep the COPI line high and it cannot be misinterpreted as a command. + static constexpr uint8_t ones[] = {0xffu, 0xffu, 0xffu, 0xffu, 0xffu, 0xffu, 0xffu, 0xffu, 0xffu, 0xffu}; + + public: + // Transfers in SPI mode are always in terms of 512-byte blocks. + static constexpr unsigned kBlockLen = 512u; + + // Supplied with SPI controller and its appropriate CS line, and a GPIO block with its input pin + // number. The SPI controller performs the communication with the SD card and the GPIO simply + // provides the card detection indicator (low = present). + // + // CRC calculation is turned on by default, although this requires the CPU to perform a little + // bit of additional work. It may be disabled if desired. SPI mode communication may be performed + // with or without CRC checking. + // + // Logging may optionally be requested. + SdCard(volatile SonataSpi *spi_, volatile SonataGpioBase *gpio_, unsigned cs_ = 1u, unsigned det_ = 16u, + bool crc_ = true, Log *log_ = nullptr) + : spi(spi_), gpio(gpio_), cs(1u << cs_), det(1u << det_), crcOn(crc_), log(log_) {} + + // SD command codes. (Section 7.3.1) + enum CmdCode { + CMD_GO_IDLE_STATE = 0, + CMD_SEND_OP_COND = 1, + CMD_SEND_IF_COND = 8, + CMD_SEND_CSD = 9, + CMD_SEND_CID = 10, + CMD_STOP_TRANSMISSION = 12, + CMD_SET_BLOCKLEN = 16, + CMD_READ_SINGLE_BLOCK = 17, + CMD_READ_MULTIPLE_BLOCK = 18, + CMD_WRITE_SINGLE_BLOCK = 24, + CMD_WRITE_MULTIPLE_BLOCK = 25, + SD_SEND_OP_COND = 41, + CMD_APP_CMD = 55, + CMD_READ_OCR = 58, + CMD_CRC_ON_OFF = 59, + }; + + // SD Control Tokens. (Section 7.3.3) + enum CtrlToken { + // Start Block Token precedes data block, for all but Multiple Block Write. + StartBlockToken = 0xfeu, + // Start Block Token used for Multiple Block Write operations. + StartBlockTokenMW = 0xfcu, + // Stop Transaction Token, for Multiple Block Writes. + StopTranToken = 0xfdu, + }; + + // Indicates whether there is an SD card present in the slot. + bool present() const { return !(gpio->debouncedInput & det); } + + void select_card(bool enable) { spi->cs = enable ? (spi->cs & ~cs) : (spi->cs | cs); } + + // Initialise the SD card ready for use. + bool init() { + // Every card tried seems to be more than capable of keeping up with 20Mbps. + constexpr unsigned kSpiSpeed = 0u; + spi->init(false, false, true, kSpiSpeed); + + // Apparently we're required to send at least 74 SD CLK cycles with + // the device _not_ selected before talking to it. + spi->blocking_write(ones, 10); + spi->wait_idle(); + + select_card(true); + + // Note that this is a very stripped-down card initialisation sequence + // that assumes SDHC version 2, so use a more recent microSD card. + do { + send_command(CMD_GO_IDLE_STATE, 0u); + } while (0x01 != get_response_R1()); + + send_command(CMD_SEND_IF_COND, 0x1aau); + get_response_R3(); + + // Instruct the SD card whether to check CRC values on commands. + send_command(CMD_CRC_ON_OFF, (uint32_t)crcOn); + get_response_R1(); + + // Read supported voltage range of the card. + send_command(CMD_READ_OCR, 0); + get_response_R3(); + + do { + send_command(CMD_APP_CMD, 0); + (void)get_response_R1(); + // Specify Host Capacity Support as 1. + send_command(SD_SEND_OP_COND, 1u << 30); + } while (0x01 & get_response_R1()); + + if (log) { + log->println("Setting block length to {}", kBlockLen); + } + + // Read card capacity information. + send_command(CMD_READ_OCR, 0); + get_response_R3(); + + send_command(CMD_SET_BLOCKLEN, kBlockLen); + uint8_t rd = get_response_R1(); + if (log) { + log->println("Response: {:#04x}", rd); + } + select_card(false); + + return true; + } + + // Read Card Identification Data (CID). + bool read_cid(uint8_t *buf, size_t len) { return read_cid_csd(CMD_SEND_CID, buf, len); } + + // Read Card Specific Data (CSD). + bool read_csd(uint8_t *buf, size_t len) { return read_cid_csd(CMD_SEND_CSD, buf, len); } + + // Read a number of contiguous blocks from the SD card. + // TODO: Support non-blocking operation. + bool read_blocks(uint32_t block, uint8_t *buf, size_t num_blocks = 1u, bool blocking = true) { + const bool multi = num_blocks > 1u; + + select_card(true); + + bool ok(true); + for (size_t blk = 0u; blk < num_blocks; blk++) { + if (log) { + log->println("Reading block {}", block + blk); + } + + if (multi) { + // Is this the first block of the read request? + if (!blk) { + send_command(CMD_READ_MULTIPLE_BLOCK, block); + (void)get_response_R1(); + } + } else { + send_command(CMD_READ_SINGLE_BLOCK, block + blk); + (void)get_response_R1(); + } + + if (!collected_data(&buf[blk * kBlockLen], kBlockLen)) { + ok = false; + break; + } + } + + if (multi) { + send_command(CMD_STOP_TRANSMISSION, 0u); + (void)get_response_R1b(); + } + + select_card(false); + return ok; + } + + // Write a number of contiguous blocks from the SD card. + // TODO: Support non-blocking operation. + bool write_blocks(uint32_t block, uint8_t *buf, size_t num_blocks = 1u, bool blocking = true) { + const bool multi = num_blocks > 1u; + uint8_t crc16[2]; + crc16[1] = crc16[0] = 0xffu; // CRC16 not required by default for SPI mode. + + select_card(true); + + bool ok(true); + for (size_t blk = 0u; blk < num_blocks; blk++) { + if (crcOn) { + // CRC16 bytes follow the data block. + uint16_t crc = calc_crc16(&buf[blk * kBlockLen], kBlockLen); + crc16[0] = (uint8_t)(crc >> 8); + crc16[1] = (uint8_t)crc; + } + if (log) { + log->println("Writing block {}", block + blk); + } + + // Note: the Start Block Token differs between Multiple Block Write commands and + // the other data transfer commands, including the Single Block Write command. + uint8_t start_token; + if (multi) { + // Is this the first block of the read request? + if (!blk) { + send_command(SdCard::CMD_WRITE_MULTIPLE_BLOCK, block); + (void)get_response_R1(); + } + start_token = StartBlockTokenMW; + } else { + send_command(SdCard::CMD_WRITE_SINGLE_BLOCK, block + blk); + (void)get_response_R1(); + start_token = StartBlockToken; + } + + spi->blocking_write(&start_token, 1u); + spi->blocking_write(&buf[blk * kBlockLen], kBlockLen); + spi->blocking_write(crc16, sizeof(crc16)); + // Collect data_response and wait until the card is no longer busy. + if (5 != (0x1f & get_data_response_busy())) { + // Data not accepted because of an error. + ok = false; + break; + } + } + + if (multi) { + const uint8_t stop_tran_token = (uint8_t)StopTranToken; + spi->blocking_write(&stop_tran_token, 1u); + // The card will hold the CIPO line low whilst busy, yielding repeated 0x00 bytes, + // but it seems to drop and raise the line at an arbitrary time with respect to + // the '8-clock counting' logic. + while (0x00 != get_response_byte()); // Detect falling edge. + // Card will signal busy with zeros. + wait_not_busy(); + } + + select_card(false); + return ok; + } + + // Send a command to the SD card with the supplied 32-bit argument. + void send_command(uint8_t cmdCode, uint32_t arg) { + uint8_t cmd[6]; + if (log) { + log->println("Sending command {:#04x}", cmdCode); + } + + // Apparently we need to clock 8 times before sending the command. + // + // TODO: This may well be an issue with not aligning read data on the previous command? + // Without this the initialisation sequence gets stuck trying to specify HCS; the SD card + // does not become ready. + uint8_t dummy = 0xffu; + spi->blocking_write(&dummy, 1u); + + cmd[0] = 0x40u | cmdCode; + cmd[1] = (uint8_t)(arg >> 24); + cmd[2] = (uint8_t)(arg >> 16); + cmd[3] = (uint8_t)(arg >> 8); + cmd[4] = (uint8_t)(arg >> 0); + // The final byte includes the CRC7 which _must_ be valid for two special commands, + // but normally in SPI mode CRC checking is OFF. + if (crcOn || cmdCode == CMD_GO_IDLE_STATE || cmdCode == CMD_SEND_IF_COND) { + cmd[5] = 1u | (calc_crc7(cmd, 5) << 1); + } else { + // No need to expend CPU times calculating the CRC7; it will be ignored. + cmd[5] = 0xffu; + } + spi->blocking_write(cmd, sizeof(cmd)); + } + + // Attempt to collect a single response byte from the device; if it is not driving the + // CIPO line we will read 0xff. + uint8_t get_response_byte() { + uint8_t r; + read_card_data(&r, 1u); + return r; + } + + // Get response type R1 from the SD card. + uint8_t get_response_R1() { + spi->wait_idle(); + while (true) { + uint8_t rd1 = get_response_byte(); + // Whilst there is no response we read 0xff; an actual R1 response commences + // with a leading 0 bit (MSB). + if (!(rd1 & 0x80u)) { + if (log) { + log->println("R1 {:#04x}", rd1); + } + return rd1; + } + } + } + + // Wait until the SD card declares that it is no longer busy. + inline void wait_not_busy() { + while (0x00 == get_response_byte()); // Wait whilst device is busy. + } + + // Get response type R1b from the SD card. + uint8_t get_response_R1b() { + spi->wait_idle(); + uint8_t rd1 = get_response_R1(); + // Card may signal busy with zero bytes. + wait_not_busy(); + return rd1; + } + + // Get data_response after sending a block of write data to the SD card. + uint8_t get_data_response_busy() { + uint8_t rd1; + spi->wait_idle(); + do { + rd1 = get_response_byte(); + } while ((rd1 & 0x11u) != 0x01u); + wait_not_busy(); + return rd1; + } + + // Get response type R3 (5 bytes) from the SD card. + void get_response_R3() { + volatile uint8_t rd2; + (void)get_response_R1(); + for (int r = 0; r < 4; ++r) { + spi->transmitFifo = 0xffu; + spi->control = SonataSpi::ControlTransmitEnable | SonataSpi::ControlReceiveEnable; + spi->start = 1u; + spi->wait_idle(); + while ((spi->status & SonataSpi::StatusRxFifoLevel) == 0) { + } + rd2 = static_cast(spi->receiveFifo); + } + // We need to ensure the FIFO reads occur, but we don't need the data presently. + rd2 = rd2; + } + + // Calculate the CRC7 value for a series of bytes (command/response); + // used to generate the CRC for a command or check that of a response if CRC_ON mode is used. + static uint8_t calc_crc7(const uint8_t *data, size_t len) { + uint8_t crc = 0u; + while (len-- > 0u) { + uint8_t d = *data++; + for (unsigned b = 0u; b < 8u; b++) { + crc = (crc << 1) ^ (((crc ^ d) & 0x80u) ? 0x12u : 0u); + d <<= 1; + } + } + // 7 MSBs contain the CRC residual. + return crc >> 1; + } + + // Calculate the CRC16 value for a series of bytes (data blocks); + // used for generation or checking, if CRC_ON mode is used. + static uint16_t calc_crc16(const uint8_t *data, size_t len) { + uint16_t crc = 0u; + while (len-- > 0u) { + uint16_t d = (uint16_t)*data++ << 8; + for (unsigned b = 0u; b < 8u; b++) { + crc = (crc << 1) ^ (((crc ^ d) & 0x8000u) ? 0x1021u : 0u); + d <<= 1; + } + } + return crc; + } + + private: + // Collect an expect number of bytes into the given buffer, and return an indication of whether + // they were collected without error. + bool collected_data(uint8_t *buf, size_t len) { + uint8_t crc16[2]; + + // Collect the data, prepended with the Start Block Token and followed by the CRC16 bytes. + while (StartBlockToken != get_response_byte()); + // For at least one test card we need to hold the COPI line high during the data read + // otherwise the data becomes corrupted; the card appears to be starting to accept a new + // command. + read_card_data(buf, len); + read_card_data(crc16, sizeof(crc16)); + + // Shall we validate the CRC16 of the received data block? + if (crcOn) { + uint16_t exp_crc16 = calc_crc16(buf, len); + uint16_t obs_crc16 = ((uint16_t)crc16[0] << 8) | crc16[1]; + if (log) { + log->println("Read block CRC {:#06x}", obs_crc16); + log->println("Calculated CRC {:#06x}", exp_crc16); + } + if (obs_crc16 != exp_crc16) { + select_card(false); + if (log) { + log->println("CRC16 mismatch"); + } + return false; + } + } + return true; + } + + // Shared implementation for CMD_SEND_CID and CMD_SEND_CSD. + bool read_cid_csd(CmdCode cmd, uint8_t *buf, size_t len) { + select_card(true); + + send_command(cmd, 0); + (void)get_response_R1(); + bool ok = collected_data(buf, len); + select_card(false); + return ok; + } + + /* + * Receives `len` bytes and puts them in the `data` buffer, + * where `len` is at most `0x7ff`, being careful to keep COPI high by + * also transmitting repeated 0xff bytes. + * + * This method will block until the requested number of bytes has been seen. + * There is currently no timeout. + * + * Note that unlike the 'blocking_read' member function of the SPI object, + * this function intentionally keeps the COPI line high by supplying a 0xff + * byte for each byte read. This prevents COPI line dropping and being + * misinterpreted as the start of a command. + */ + void read_card_data(uint8_t data[], uint16_t len) { + assert(len <= 0x7ff); + len &= SonataSpi::StartByteCountMask; + spi->wait_idle(); + // Do not attempt a zero-byte transfer; not supported by the controller. + if (len) { + spi->control = SonataSpi::ControlReceiveEnable | SonataSpi::ControlTransmitEnable; + spi->start = len; + + // In addition to keeping the COPI line high to avoid inducing failures we want + // to keep the Tx FIFO populated so that the SPI controller remains active despite + // having double the traffic between CPU and SPI (Tx and Rx). + // + // This expression of the code very nearly keeps the SPI controller running at + // SysClkFreq/2 Mbps whilst reading data, so that the data block is retrieved as + // quickly as possible. + // + // Prompt the retrieval of the first byte. + spi->transmitFifo = 0xffu; // Keep COPI high, staying one byte ahead of reception. + const uint8_t *end = data + len - 1; + while (data < end) { + // Prompt the retrieval of the next byte. + spi->transmitFifo = 0xffu; // COPI high for the next byte. + // Wait until it's available, and quickly capture it. + while (spi->status & SonataSpi::StatusRxFifoEmpty) { + } + *data++ = static_cast(spi->receiveFifo); + } + // Collect the final byte. + while (spi->status & SonataSpi::StatusRxFifoEmpty) { + } + *data++ = static_cast(spi->receiveFifo); + } + } +}; diff --git a/sw/cheri/tests/lorem_text.hh b/sw/cheri/tests/lorem_text.hh new file mode 100644 index 000000000..66b2f98e1 --- /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 000000000..dfa41e7ad --- /dev/null +++ b/sw/cheri/tests/sdcard_tests.hh @@ -0,0 +1,278 @@ +/** + * 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 + +#include + +#include "../../common/defs.h" + +#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" + +#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; + +// Scratch workspace for reading file blocks or Long FileName. +static uint8_t fileBuffer[BLOCK_LEN]; + +// 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(bool &validCID, 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(log, buf, sizeof(buf)); + } + // 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) "); + validCID = false; + if (sd.read_cid(buf, sizeof(buf))) { + if (logging) { + dump_bytes(log, buf, sizeof(buf)); + } + // 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]); + // Check that the manufacturer ID is non-zero and the OEM/Application ID contains two + // valid ASCII characters. + if (buf[0] && buf[1] >= 0x20 && buf[1] < 0x7f && buf[2] >= 0x20 && buf[2] < 0x7f) { + validCID = true; + } + } 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. + auto spi = spi_ptr(root, 2); + + // 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_clk); + 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. + auto gpio = gpio_ptr(root); + + // 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); + + 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()) { + bool validCID; + sd.init(); + + log.println("Reading card properties.... "); + failures += read_card_properties(validCID, sd, log); + + // The CI system presently does not have a valid microSD card image and properties in + // simulation. We have already tested SPI traffic so if we haven't read a valid CID, + // skip the block level/filing system testing. + if (validCID) { + log.println("Reading card contents.... "); + fileSysUtils fs; + + 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 { + uint16_t *ucs = reinterpret_cast(fileBuffer); + const size_t ucs_max = sizeof(fileBuffer) / 2; + fileSysUtils::dirEntry entry; + while (fs.dir_next(dh, entry, fileSysUtils::DirFlags_Default, ucs, ucs_max)) { + log.print("'"); + write_str_ucs2(log, ucs, ucs_max); + 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(log, fileBuffer, chunkLen); + } + // 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 5d18299a4..6ad9d6a49 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);