diff --git a/doc/ref/dev_guide.md b/doc/ref/dev_guide.md index aa9c08845..4aa9fbae6 100644 --- a/doc/ref/dev_guide.md +++ b/doc/ref/dev_guide.md @@ -180,6 +180,8 @@ Then load the bitstream onto Genesys 2: openFPGALoader -b genesys2 build/lowrisc_mocha_chip_mocha_genesys2_0/synth-vivado/lowrisc_mocha_chip_mocha_genesys2_0.bit ``` +For the SD card tests, you will also need to insert a FAT32-formatted microSD card containing the "lorem.ips" file (see sw/device/tests/spi_host/lorem_text.h). + ## Verification To run block-level verification you can use the following command: diff --git a/hw/top_chip/data/pins_genesys2.xdc b/hw/top_chip/data/pins_genesys2.xdc index 6b43f3339..e9874783f 100644 --- a/hw/top_chip/data/pins_genesys2.xdc +++ b/hw/top_chip/data/pins_genesys2.xdc @@ -12,6 +12,7 @@ set_property -dict { PACKAGE_PIN AB25 IOSTANDARD LVCMOS33 } [get_ports { ftdi_r ## GPIO # Inputs +# User switches set_property -dict { PACKAGE_PIN G19 IOSTANDARD LVCMOS18 } [get_ports { gpio_i[0] }]; # SW0 (VADJ) set_property -dict { PACKAGE_PIN G25 IOSTANDARD LVCMOS18 } [get_ports { gpio_i[1] }]; # SW1 (VADJ) set_property -dict { PACKAGE_PIN H24 IOSTANDARD LVCMOS18 } [get_ports { gpio_i[2] }]; # SW2 (VADJ) @@ -20,11 +21,13 @@ set_property -dict { PACKAGE_PIN N19 IOSTANDARD LVCMOS18 } [get_ports { gpio_i set_property -dict { PACKAGE_PIN P19 IOSTANDARD LVCMOS18 } [get_ports { gpio_i[5] }]; # SW5 (VADJ) set_property -dict { PACKAGE_PIN P26 IOSTANDARD LVCMOS33 } [get_ports { gpio_i[6] }]; # SW6 (VCC3V3) set_property -dict { PACKAGE_PIN P27 IOSTANDARD LVCMOS33 } [get_ports { gpio_i[7] }]; # SW7 (VCC3V3) - -# Bootstrap pin, should be pulled down during boot to enter bootstrap mode. -set_property -dict { PACKAGE_PIN AB29 IOSTANDARD LVCMOS33 PULLTYPE PULLUP } [get_ports { gpio_i[8] }]; +# Bootstrap pin, should be pulled down during boot to enter bootstrap mode. +set_property -dict { PACKAGE_PIN AB29 IOSTANDARD LVCMOS33 PULLTYPE PULLUP } [get_ports { gpio_i[8] }]; # PROG_RXFN +# Micro SD card presence detect line (low = card present) +set_property -dict { PACKAGE_PIN P28 IOSTANDARD LVCMOS33 } [get_ports { gpio_i[9] }]; # SD_CD # Outputs +# User LEDs set_property -dict { PACKAGE_PIN T28 IOSTANDARD LVCMOS33 } [get_ports { gpio_o[0] }]; # LED0 set_property -dict { PACKAGE_PIN V19 IOSTANDARD LVCMOS33 } [get_ports { gpio_o[1] }]; # LED1 set_property -dict { PACKAGE_PIN U30 IOSTANDARD LVCMOS33 } [get_ports { gpio_o[2] }]; # LED2 @@ -33,6 +36,8 @@ set_property -dict { PACKAGE_PIN V20 IOSTANDARD LVCMOS33 } [get_ports { gpio_o set_property -dict { PACKAGE_PIN V26 IOSTANDARD LVCMOS33 } [get_ports { gpio_o[5] }]; # LED5 set_property -dict { PACKAGE_PIN W24 IOSTANDARD LVCMOS33 } [get_ports { gpio_o[6] }]; # LED6 set_property -dict { PACKAGE_PIN W23 IOSTANDARD LVCMOS33 } [get_ports { gpio_o[7] }]; # LED7 +# Micro SD card power (VDD) control (active-low, ext. pull-up) +set_property -dict { PACKAGE_PIN AE24 IOSTANDARD LVCMOS33 } [get_ports { gpio_o[8] }]; # SD_RESET ## UART set_property -dict { PACKAGE_PIN Y20 IOSTANDARD LVCMOS33 } [get_ports { uart_rx_i }]; @@ -48,7 +53,7 @@ set_property -dict { PACKAGE_PIN Y23 IOSTANDARD LVCMOS33 } [get_ports { uart_t set_property -dict { PACKAGE_PIN T26 IOSTANDARD LVCMOS33 PULLTYPE PULLUP } [get_ports { i2c_scl_io }]; set_property -dict { PACKAGE_PIN T27 IOSTANDARD LVCMOS33 PULLTYPE PULLUP } [get_ports { i2c_sda_io }]; -## SPI (PMOD Header JD) +## SPI Device (PMOD Header JD) set_property -dict { PACKAGE_PIN W28 IOSTANDARD LVCMOS33 PULLTYPE PULLDOWN } [get_ports { spi_device_sd_o }]; set_property -dict { PACKAGE_PIN W27 IOSTANDARD LVCMOS33 PULLTYPE PULLDOWN } [get_ports { spi_device_sd_i }]; set_property -dict { PACKAGE_PIN W29 IOSTANDARD LVCMOS33 PULLTYPE PULLUP } [get_ports { spi_device_csb_i }]; @@ -71,3 +76,16 @@ set_property -dict { PACKAGE_PIN AJ11 IOSTANDARD LVCMOS15 } [get_ports { eth_tx set_property -dict { PACKAGE_PIN AK10 IOSTANDARD LVCMOS15 } [get_ports { eth_tx_d[3] }]; set_property -dict { PACKAGE_PIN AF12 IOSTANDARD LVCMOS15 } [get_ports { eth_mdc }]; set_property -dict { PACKAGE_PIN AG12 IOSTANDARD LVCMOS15 } [get_ports { eth_mdio }]; + +## SPI Host (MicroSD card slot) +set_property -dict { PACKAGE_PIN R28 IOSTANDARD LVCMOS33 } [get_ports { spi_host_sck_o }]; # SD_SCLK +set_property -dict { PACKAGE_PIN R26 IOSTANDARD LVCMOS33 } [get_ports { spi_host_sd_i }]; # SD_DAT0 +# set_property -dict { PACKAGE_PIN R30 IOSTANDARD LVCMOS33 } [get_ports { microsd_dat1 }]; # SD_DAT1 unused in SPI bus mode +# set_property -dict { PACKAGE_PIN P29 IOSTANDARD LVCMOS33 } [get_ports { microsd_dat2 }]; # SD_DAT2 unused in SPI bus mode +set_property -dict { PACKAGE_PIN T30 IOSTANDARD LVCMOS33 } [get_ports { spi_host_csb_o }]; # SD_DAT3 +set_property -dict { PACKAGE_PIN R29 IOSTANDARD LVCMOS33 } [get_ports { spi_host_sd_o }]; # SD_CMD +# SPI Host signal copies for external logic analyser on PMOD "JB" +set_property -dict { PACKAGE_PIN W26 IOSTANDARD LVCMOS33 } [get_ports { spi_host_sck_o_dbg_o }]; # JB2_N (PMOD 4) +set_property -dict { PACKAGE_PIN V25 IOSTANDARD LVCMOS33 } [get_ports { spi_host_sd_i_dbg_o }]; # JB2_P (PMOD 3) +set_property -dict { PACKAGE_PIN V29 IOSTANDARD LVCMOS33 } [get_ports { spi_host_csb_o_dbg_o }]; # JB1_P (PMOD 1) +set_property -dict { PACKAGE_PIN V30 IOSTANDARD LVCMOS33 } [get_ports { spi_host_sd_o_dbg_o }]; # JB1_N (PMOD 2) diff --git a/hw/top_chip/dv/verilator/top_chip_verilator.core b/hw/top_chip/dv/verilator/top_chip_verilator.core index e5310efcf..41b132fd6 100644 --- a/hw/top_chip/dv/verilator/top_chip_verilator.core +++ b/hw/top_chip/dv/verilator/top_chip_verilator.core @@ -26,6 +26,7 @@ filesets: - lowrisc:dv:sw_test_status - lowrisc:dv:dv_test_status - lowrisc:sonata:i2cdpi + - lowrisc:sonata:spidevicedpi files: - dram_wrapper_sim.sv: { file_type: systemVerilogSource } diff --git a/hw/top_chip/dv/verilator/top_chip_verilator.sv b/hw/top_chip/dv/verilator/top_chip_verilator.sv index a4845be0e..d215f330f 100644 --- a/hw/top_chip/dv/verilator/top_chip_verilator.sv +++ b/hw/top_chip/dv/verilator/top_chip_verilator.sv @@ -30,20 +30,25 @@ module top_chip_verilator ( logic uart_rx; logic uart_tx; - // SPI signals + // SPI device signals logic spi_device_sck; logic spi_device_csb; logic [3:0] qspi_device_sdo; logic [3:0] qspi_device_sdo_en; logic spi_device_sdi; + // SPI host signals + logic spi_host_sck_output, spi_host_csb_output; + logic spi_host_sck_en_output, spi_host_csb_en_output; + logic spi_host_input; + logic [3:0] spi_host_sd_output; + logic [3:0] spi_host_sd_en_output; + logic microsd_det; + // AXI signals top_pkg::axi_dram_req_t dram_req; top_pkg::axi_dram_resp_t dram_resp; - logic [3:0] spi_host_sd; - logic [3:0] spi_host_sd_en; - // CHERI Mocha top top_chip_system #( .SramInitFile(""), @@ -52,7 +57,7 @@ module top_chip_verilator ( .clk_i, .rst_ni, - .gpio_i (gpio_inputs), + .gpio_i ({gpio_inputs[31:10], microsd_det, gpio_inputs[8:0]}), .gpio_o (gpio_outputs), .gpio_en_o (gpio_en_outputs), @@ -77,17 +82,15 @@ module top_chip_verilator ( .spi_device_sd_i ({3'h0, spi_device_sdi}), // SPI MOSI = QSPI DQ0 .spi_device_tpm_csb_i ('0), - .spi_host_sck_o ( ), - .spi_host_sck_en_o ( ), - .spi_host_csb_o ( ), - .spi_host_csb_en_o ( ), - .spi_host_sd_o (spi_host_sd), - .spi_host_sd_en_o (spi_host_sd_en), - // Mapping output 0 to input 1 because legacy SPI does not allow - // bi-directional wires. - // This only works in standard mode where sd_o[0]=COPI and - // sd_i[1]=CIPO. - .spi_host_sd_i ({2'b0, spi_host_sd_en[0] ? spi_host_sd[0] : 1'b0, 1'b0}), + .spi_host_sck_o (spi_host_sck_output), + .spi_host_sck_en_o (spi_host_sck_en_output), + .spi_host_csb_o (spi_host_csb_output), + .spi_host_csb_en_o (spi_host_csb_en_output), + .spi_host_sd_o (spi_host_sd_output), + .spi_host_sd_en_o (spi_host_sd_en_output), + // Legacy SPI present in SD cards does not allow bi-directional wires. + // Work in standard mode where sd_o[0]=COPI and sd_i[1]=CIPO. + .spi_host_sd_i ({2'b0, spi_host_input, 1'b0}), // SPI CIPO = QSPI DQ1 .dram_req_o (dram_req), .dram_resp_i (dram_resp), @@ -99,8 +102,7 @@ module top_chip_verilator ( ); // No support for dual or quad SPI in loopback mode right now. - logic unused_spi_host = (|spi_host_sd[3:2]) | spi_host_sd[0] | - (|spi_host_sd_en[3:2]) | spi_host_sd_en[0]; + logic unused_spi_host = |{spi_host_sd_output[3:1], spi_host_sd_en_output[3:1]}; // Virtual GPIO gpiodpi #( @@ -152,8 +154,25 @@ module top_chip_verilator ( .spi_device_sck_o (spi_device_sck), .spi_device_csb_o (spi_device_csb), .spi_device_sdi_o (spi_device_sdi), - .spi_device_sdo_i (qspi_device_sdo[1]), // SPI MISO = QSPI DQ1 - .spi_device_sdo_en_i(qspi_device_sdo_en[1]) // SPI MISO = QSPI DQ1 + .spi_device_sdo_i (qspi_device_sdo[1]), // SPI CIPO = QSPI DQ1 + .spi_device_sdo_en_i(qspi_device_sdo_en[1]) // SPI CIPO = QSPI DQ1 + ); + + // Virtual SPI Device - model an SD card (in a limited way) + spidevicedpi #( + .ID ("microsd"), + .NDevices (1), + .DataW (1), + .OOB_InW (1), + .OOB_OutW (1) + ) u_spidevicedpi_microsd ( + .rst_ni, + .sck (spi_host_sck_en_output ? spi_host_sck_output : 1'b0), + .cs (spi_host_csb_en_output ? spi_host_csb_output : 1'b0), + .copi (spi_host_sd_en_output[0] ? spi_host_sd_output[0] : 1'b0), // SPI COPI = QSPI DQ0 + .cipo (spi_host_input), + .oob_in ( ), // not used + .oob_out(microsd_det) ); `define DUT u_top_chip_system diff --git a/hw/top_chip/rtl/chip_mocha_genesys2.sv b/hw/top_chip/rtl/chip_mocha_genesys2.sv index c4d34ea5a..d839aed54 100644 --- a/hw/top_chip/rtl/chip_mocha_genesys2.sv +++ b/hw/top_chip/rtl/chip_mocha_genesys2.sv @@ -14,9 +14,9 @@ module chip_mocha_genesys2 #( input logic ext_rst_ni, input logic ftdi_rst_ni, - // GPIO - enough for the user switches and LEDs as a starting point - input logic [8:0] gpio_i, - output logic [7:0] gpio_o, + // GPIO + input logic [9:0] gpio_i, + output logic [8:0] gpio_o, // UART input logic uart_rx_i, @@ -26,13 +26,24 @@ module chip_mocha_genesys2 #( inout logic i2c_scl_io, inout logic i2c_sda_io, - // SPI + // SPI Device input logic spi_device_sck_i, input logic spi_device_csb_i, input logic spi_device_sd_i, output logic spi_device_sd_o, output logic spien, + // SPI Host + output logic spi_host_sck_o, + output logic spi_host_csb_o, + input logic spi_host_sd_i, + output logic spi_host_sd_o, + // SPI Host signal copies for external logic analyser on PMOD "JB" + output logic spi_host_sck_o_dbg_o, + output logic spi_host_csb_o_dbg_o, + output logic spi_host_sd_i_dbg_o, + output logic spi_host_sd_o_dbg_o, + // DDR3 inout wire [31:0] ddr3_dq, inout wire [ 3:0] ddr3_dqs_n, @@ -116,9 +127,10 @@ module chip_mocha_genesys2 #( logic i2c_scl_en_output, i2c_sda_en_output; logic [3:0] qspi_device_sdo; logic [3:0] qspi_device_sdo_en; - - logic [3:0] spi_host_sd; - logic [3:0] spi_host_sd_en; + logic spi_host_sck_output, spi_host_csb_output; + logic spi_host_sck_en_output, spi_host_csb_en_output; + logic [3:0] spi_host_sd_output; + logic [3:0] spi_host_sd_en_output; // AXI signals // Tag controller to CDC FIFO, synchronous to u_top_chip_system.clkmgr_clocks.clk_main_infra @@ -213,7 +225,7 @@ module chip_mocha_genesys2 #( .rst_ni (rst_n_sync_50m), // GPIO - .gpio_i ({23'd0, gpio_i}), + .gpio_i (32'(gpio_i)), .gpio_o (gpio_outputs), .gpio_en_o (gpio_en_outputs), @@ -239,21 +251,17 @@ module chip_mocha_genesys2 #( .spi_device_csb_i (spi_device_csb_i), .spi_device_sd_o (qspi_device_sdo), .spi_device_sd_en_o (qspi_device_sdo_en), - .spi_device_sd_i ({3'h0, spi_device_sd_i}), // SPI MOSI = QSPI DQ0 + .spi_device_sd_i ({3'h0, spi_device_sd_i}), // SPI COPI = QSPI DQ0 .spi_device_tpm_csb_i ('0), // SPI host - .spi_host_sck_o ( ), - .spi_host_sck_en_o ( ), - .spi_host_csb_o ( ), - .spi_host_csb_en_o ( ), - .spi_host_sd_o (spi_host_sd), - .spi_host_sd_en_o (spi_host_sd_en), - // Mapping output 0 to input 1 because legacy SPI does not allow - // bi-directional wires. - // This only works in standard mode where sd_o[0]=COPI and - // sd_i[1]=CIPO. - .spi_host_sd_i ({2'b0, spi_host_sd_en[0] ? spi_host_sd[0] : 1'b0, 1'b0}), + .spi_host_sck_o (spi_host_sck_output), + .spi_host_sck_en_o (spi_host_sck_output_en), + .spi_host_csb_o (spi_host_csb_output), + .spi_host_csb_en_o (spi_host_csb_output_en), + .spi_host_sd_o (spi_host_sd_output), + .spi_host_sd_en_o (spi_host_sd_en_output), + .spi_host_sd_i ({2'b00, spi_host_sd_i, 1'b0}), // SPI CIPO = QSPI DQ1 // DRAM .dram_req_o (dram_req), @@ -269,7 +277,7 @@ module chip_mocha_genesys2 #( // GPIO tri-state output drivers // Instantiate for only the outputs connected to an FPGA pin - for (genvar ii = 0; ii < 8; ii++) begin : gen_gpio_o + for (genvar ii = 0; ii < $bits(gpio_o); ii++) begin : gen_gpio_o OBUFT obuft ( .I(gpio_outputs[ii]), .T(~gpio_en_outputs[ii]), @@ -291,13 +299,50 @@ module chip_mocha_genesys2 #( .O (i2c_sda_input) ); - // SPI tri-state output driver - OBUFT spi_obuft ( - .I(qspi_device_sdo[1]), // SPI MISO = QSPI DQ1 - .T(~qspi_device_sdo_en[1]), // SPI MISO = QSPI DQ1 + // SPI device tri-state output driver + OBUFT spi_device_obuft ( + .I(qspi_device_sdo[1]), // SPI CIPO = QSPI DQ1 + .T(~qspi_device_sdo_en[1]), // SPI CIPO = QSPI DQ1 .O(spi_device_sd_o) ); + // SPI host tri-state output drivers + OBUFT spi_host_sck_obuft ( + .I(spi_host_sck_output), + .T(~spi_host_sck_output_en), + .O(spi_host_sck_o) + ); + OBUFT spi_host_csb_obuft ( + .I(spi_host_csb_output), + .T(~spi_host_csb_output_en), + .O(spi_host_csb_o) + ); + // Legacy SPI present in SD cards does not allow bi-directional wires. + // Work in standard mode where sd_o[0]=COPI and sd_i[1]=CIPO. + // Other data outputs are unused. + OBUFT spi_host_sd_obuft ( + .I(spi_host_sd_output[0]), // SPI COPI = QSPI DQ0 + .T(~spi_host_sd_en_output[0]), // SPI COPI = QSPI DQ0 + .O(spi_host_sd_o) + ); + // SPI Host signal copies for external logic analyser on PMOD "JB" + assign spi_host_sd_i_dbg_o = spi_host_sd_i; + OBUFT spi_host_sck_obuft_dbg ( + .I(spi_host_sck_output), + .T(~spi_host_sck_output_en), + .O(spi_host_sck_o_dbg_o) + ); + OBUFT spi_host_csb_obuft_dbg ( + .I(spi_host_csb_output), + .T(~spi_host_csb_output_en), + .O(spi_host_csb_o_dbg_o) + ); + OBUFT spi_host_sd_obuft_dbg ( + .I(spi_host_sd_output[0]), // SPI COPI = QSPI DQ0 + .T(~spi_host_sd_en_output[0]), // SPI COPI = QSPI DQ0 + .O(spi_host_sd_o_dbg_o) + ); + // Async AXI FIFO from tag controller to MIG axi_cdc #( .aw_chan_t (top_pkg::axi_dram_aw_chan_t), diff --git a/sw/device/lib/hal/spi_host.h b/sw/device/lib/hal/spi_host.h index 5c806e5a0..0153cde44 100644 --- a/sw/device/lib/hal/spi_host.h +++ b/sw/device/lib/hal/spi_host.h @@ -15,17 +15,24 @@ #define SPI_HOST_CONTROL_SPIEN_MASK (1u << 31) #define SPI_HOST_CONTROL_OUTPUTEN_MASK (1u << 29) #define SPI_HOST_STATUS_REG (0x14) +#define SPI_HOST_STATUS_READY_MASK (1u << 31) +#define SPI_HOST_STATUS_ACTIVE_MASK (1u << 30) +#define SPI_HOST_STATUS_TXFULL_MASK (1u << 29) +#define SPI_HOST_STATUS_RXEMPTY_MASK (1u << 24) #define SPI_HOST_CONFIGOPTS_REG (0x18) #define SPI_HOST_CSID_REG (0x1C) #define SPI_HOST_COMMAND_REG (0x20) +#define SPI_HOST_COMMAND_CSAAT_OFFSET (0) #define SPI_HOST_COMMAND_DIRECTION_OFFSET (3) #define SPI_HOST_COMMAND_DIRECTION_RECEIVE (1 << SPI_HOST_COMMAND_DIRECTION_OFFSET) #define SPI_HOST_COMMAND_DIRECTION_TRANSMIT (2 << SPI_HOST_COMMAND_DIRECTION_OFFSET) #define SPI_HOST_COMMAND_DIRECTION_BIDIRECTIONAL (3 << SPI_HOST_COMMAND_DIRECTION_OFFSET) #define SPI_HOST_COMMAND_LEN_OFF (5) -#define SPI_HOST_COMMAND_LEN_MASK (0xFFFFF << SPI_HOST_COMMAND_LEN_OFF) +#define SPI_HOST_COMMAND_LEN_MAX (0xFFFFF) +#define SPI_HOST_COMMAND_LEN_MASK (SPI_HOST_COMMAND_LEN_MAX << SPI_HOST_COMMAND_LEN_OFF) #define SPI_HOST_RXDATA_REG (0x24) #define SPI_HOST_TXDATA_REG (0x28) +#define SPI_HOST_ERROR_STATUS_REG (0x30) typedef void *spi_host_t; diff --git a/sw/device/lib/hal/uart.c b/sw/device/lib/hal/uart.c index a9895df26..1d61f0596 100644 --- a/sw/device/lib/hal/uart.c +++ b/sw/device/lib/hal/uart.c @@ -5,6 +5,7 @@ #include "hal/uart.h" #include "hal/mmio.h" #include "hal/mocha.h" +#include #include void uart_init(uart_t uart) @@ -106,3 +107,27 @@ void uart_puts(uart_t uart, const char *str) uart_putchar(uart, *str++); } } + +// Dump out a sequence of bytes as hexadecimal and ASCII text. +void uart_dump_bytes(uart_t uart, const uint8_t buf[], size_t len) +{ + for (size_t off = 0u; off < len; ++off) { + uart_putchar(uart, '0' + (buf[off] >> 4)); + uart_putchar(uart, '0' + (buf[off] & 0xfu)); + if ((off & 0xfu) == 0xfu) { + uart_puts(uart, " : "); + for (size_t aoff = (off & ~0xfu); aoff <= off; aoff++) { + char text[2]; + text[0] = buf[aoff]; + if (text[0] < ' ' || '~' < text[0]) { // non-printable + text[0] = '.'; + } + text[1] = '\0'; + uart_puts(uart, text); + } + uart_putchar(uart, '\n'); + } else { + uart_putchar(uart, ' '); + } + } +} diff --git a/sw/device/lib/hal/uart.h b/sw/device/lib/hal/uart.h index b71433110..fe4411014 100644 --- a/sw/device/lib/hal/uart.h +++ b/sw/device/lib/hal/uart.h @@ -6,6 +6,7 @@ #include "autogen/uart.h" #include +#include #include #define BAUD_RATE (1000000u) @@ -28,3 +29,4 @@ char uart_in(uart_t uart); void uart_out(uart_t uart, char ch); void uart_putchar(uart_t uart, char ch); void uart_puts(uart_t uart, const char *str); +void uart_dump_bytes(uart_t uart, const uint8_t buf[], size_t len); diff --git a/sw/device/lib/runtime/CMakeLists.txt b/sw/device/lib/runtime/CMakeLists.txt index 8aa2bb427..f0fe5b4b5 100644 --- a/sw/device/lib/runtime/CMakeLists.txt +++ b/sw/device/lib/runtime/CMakeLists.txt @@ -2,7 +2,7 @@ # Licensed under the Apache License, Version 2.0, see LICENSE for details. # SPDX-License-Identifier: Apache-2.0 -set(SRCS print.c string.c) +set(SRCS filesys_utils.c print.c string.c sdcard.c) set(LIBS hal) diff --git a/sw/device/lib/runtime/filesys_utils.c b/sw/device/lib/runtime/filesys_utils.c new file mode 100644 index 000000000..a8ec574eb --- /dev/null +++ b/sw/device/lib/runtime/filesys_utils.c @@ -0,0 +1,852 @@ +// Copyright lowRISC contributors (COSMIC project). +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +#include "runtime/filesys_utils.h" +#include "runtime/print.h" +#include "runtime/sdcard.h" +// #include + +// 'Private' function declarations +static void copy_bytes(uint8_t *dst, const uint8_t *src, size_t len); +static int block_ensure(fs_utils_state_t *fs, uint32_t block); +static bool end_of_chain(uint32_t cluster); +static bool cluster_next(fs_utils_state_t *fs, uint32_t *nextCluster, uint32_t cluster); +static bool object_seek(fs_utils_state_t *fs, fs_utils_obj_state_t *obj, uint32_t offset); +static size_t object_read(fs_utils_state_t *fs, fs_utils_obj_state_t *obj, uint8_t buf[], + size_t len); +static uint8_t floor_log2(uint16_t n); +static uint16_t as_lower_case(uint8_t ch); +static void generate_lfn(uint16_t *ucs, size_t ucs_max, const fs_utils_dir_entry_t *entry); +static bool include_entry(fs_utils_dir_entry_flags_t entryFlags, fs_utils_dir_flags_t flags); + + +// 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. +static int block_ensure(fs_utils_state_t *fs, uint32_t block) +{ + // Check whether this block is already available. + unsigned int idx = 0; + while (idx < FS_UTILS_CACHE_ENTRIES) { + if (block == fs->blockCache[idx].block) { + return (int)idx; + } + idx++; + } + idx = fs->blockCacheNext; + if (fs->uart) { + uprintf(fs->uart, " (reading blk 0x%x)\n", block); + } + if (read_blocks(fs->spi, block, fs->blockCache[idx].buf, 1u, fs->uart)) { + fs->blockCache[idx].block = block; + // Round-robin replacement of cached blocks. + if (++fs->blockCacheNext >= FS_UTILS_CACHE_ENTRIES) { + fs->blockCacheNext = 0u; + } + return (int)idx; + } + return -1; +} + +// Is the specified cluster number an End of Chain marker? +// (a number of different values are used as EOC markers.) +static inline bool end_of_chain(uint32_t cluster) +{ + return (cluster <= 1u) || (cluster >= 0x0ffffff8u); +} + +// Read the next cluster in the cluster chain of an object. +static bool cluster_next(fs_utils_state_t *fs, 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 = fs->fatStart + (byteOffset >> FS_UTILS_BYTES_PER_BLOCK_SHIFT); + int idx = block_ensure(fs, block); + if (idx < 0) { + // Failed to read the block from the medium. + return false; + } + *nextCluster = read32le(&fs->blockCache[idx].buf[byteOffset & (FS_UTILS_BLOCK_LEN - 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(fs_utils_state_t *fs, fs_utils_obj_state_t *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 & ~fs->clusterMask; + if (offset < currOffset) { + currCluster = obj->firstCluster; + currOffset = 0u; + } + // Scan forwards through the cluster chain until we find the correct cluster. + while (offset - currOffset >= fs->clusterBytes) { + uint32_t nextCluster; + if (!cluster_next(fs, &nextCluster, currCluster)) { + // Leave the current position unchanged. + return false; + } + currCluster = nextCluster; + currOffset += fs->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). +static size_t object_read(fs_utils_state_t *fs, fs_utils_obj_state_t *obj, uint8_t buf[], + size_t len) +{ + if (fs->uart) { + uprintf(fs->uart, "reading 0x%x byte(s) at offset 0x%x\n", (unsigned int)len, obj->offset); + } + + size_t bytesRead = 0u; + while (len > 0u && obj->offset < obj->length) { + uint32_t currBlock = block_number(fs, obj->currCluster, obj->offset & fs->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(fs, currBlock); + if (idx < 0) { + return bytesRead; + } + // Locate this block within the block cache; its availability is guaranteed at this point. + const uint8_t *dataBuf = fs->blockCache[idx].buf; + + // How much data do we have available at the current offset? + size_t blockOffset = obj->offset & (FS_UTILS_BLOCK_LEN - 1u); + size_t blockBytesLeft = FS_UTILS_BLOCK_LEN - 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 & fs->clusterMask) && obj->length > next_offset) { + uint32_t nextCluster; + if (!cluster_next(fs, &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: +// 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; +} + +// Test for the presence of a FAT32 partition, read the partition properties +// and then locate the cluster heap and root directory. +bool fs_utils_init(fs_utils_state_t *fs, spi_host_t spi, uart_t uart) +{ + // Initialise all state information; no partition details, empty block cache, + // no file/dir handles. + fin(fs); + + // Store pointers to SPI Host for SD comms and (optional) UART for logging + fs->spi = spi; + fs->uart = uart; + + // Read the Master Boot Record (MBR) from block 0 at the very start of the medium. + uint8_t *dataBuffer = fs->buf.dataBuffer; + if (!read_blocks(fs->spi, 0, dataBuffer, 1u, fs->uart)) { + if (fs->uart) { + uart_puts(fs->uart, "Unable to read the MBR of the SD card\n"); + } + return false; + } + + // We require MBR, as used by manufacturers for greatest compatibility, not GPT. + if (dataBuffer[0x1fe] != 0x55 || dataBuffer[0x1ff] != 0xaa) { + if (fs->uart) { + uart_puts(fs->uart, "Unable to parse the MBR of the SD card\n"); + } + 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 (fs->uart) { + uprintf(fs->uart, + "Partition 0x%x : type 0x%x : start C 0x%x H 0x%x S 0x%x : end C 0x%x H 0x%x S " + "0x%x\n", + part, part_type, start_c, start_h, start_s, end_c, end_h, end_s); + uprintf(fs->uart, " LBA start: 0x%x sectors: 0x%x\n", 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 (fs->uart) { + uprintf(fs->uart, "Expecting EBR at block 0x%x\n", blk_offset); + } + found = true; + } break; + default: + if (fs->uart) { + uart_puts(fs->uart, "Not a suitable partition\n"); + } + break; + } + } + + if (!found) { + if (fs->uart) { + uart_puts(fs->uart, "Unable to locate a suitable partition\n"); + } + return false; + } + + // Read the EBR at the start of the partition. + if (fs->uart) { + uprintf(fs->uart, "Reading block 0x%x\n", blk_offset); + } + read_blocks(fs->spi, blk_offset, dataBuffer, 1u, fs->uart); + if (fs->uart) { + // uart_dump_bytes(fs->uart, dataBuffer, FS_UTILS_BLOCK_LEN); + } + + 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]); + fs->rootCluster = read32le(&dataBuffer[0x2c]); + + if (fs->uart) { + uprintf(fs->uart, "FAT32 0x%x FATs, secs per FAT 0x%x, bytes/sec 0x%x\n", numFATs, + secsPerFAT, bytesPerSector); + uprintf(fs->uart, " resvdSectors 0x%x\n", resvdSectors); + } + + fs->bytesPerSectorShift = floor_log2(bytesPerSector); + fs->secsPerClusterShift = floor_log2(secsPerCluster); + + uint32_t fatOffset = resvdSectors; + uint32_t clusterHeapOffset = + ((resvdSectors + (numFATs * secsPerFAT)) << fs->bytesPerSectorShift) / FS_UTILS_BLOCK_LEN; + + // TODO: we do not fully cope with a difference between blocks and sectors at present. + fs->blksPerClusterShift = fs->secsPerClusterShift; + + // Remember the volume-relative block numbers at which the (first) FAT, the cluster heap and + // the root directory commence. + fs->rootStart = ((fs->rootCluster - 2) << fs->secsPerClusterShift << fs->bytesPerSectorShift) / + FS_UTILS_BLOCK_LEN; + fs->rootStart += blk_offset + clusterHeapOffset; + fs->clusterHeapStart = blk_offset + clusterHeapOffset; + fs->fatStart = blk_offset + fatOffset; + + if (fs->uart) { + uprintf(fs->uart, + "Cluster heap offset 0x%x Root cluster 0x%x log2(bytes/sec) 0x%x " + "log2(secs/cluster) 0x%x\n", + clusterHeapOffset, fs->rootCluster, fs->bytesPerSectorShift, + fs->secsPerClusterShift); + } + + // Sanity check the parameters, listing all objections. + fs->partValid = true; + if (fs->bytesPerSectorShift < 9 || fs->bytesPerSectorShift > 12) { + if (fs->uart) { + uart_puts(fs->uart, " - bytes/sector is invalid\n"); + } + fs->partValid = false; + } + if (fs->secsPerClusterShift > 25 - fs->bytesPerSectorShift) { + if (fs->uart) { + uart_puts(fs->uart, " - sectors/cluster is invalid\n"); + } + fs->partValid = false; + } + if (!fs->partValid) { + if (fs->uart) { + uart_puts(fs->uart, "Unable to use this partition\n"); + } + return false; + } + + // Calculate derived properties. + fs->clusterBytes = 1u << (fs->secsPerClusterShift + fs->bytesPerSectorShift); + fs->clusterMask = fs->clusterBytes - 1u; + + // Record the fact that we have a valid partition. + fs->partValid = true; + // We should now have access to the root directory when required. + return true; +} + +// Finalise access to a filesystem. +void fin(fs_utils_state_t *fs) +{ + // Forget all files. + for (unsigned idx = 0u; idx < FS_UTILS_MAX_FILES; idx++) { + fs->files[idx].flags = 0u; + } + // Forget all directories. + for (unsigned idx = 0u; idx < FS_UTILS_MAX_DIRS; idx++) { + fs->dirs[idx].flags = 0u; + } + // Forget all cached blocks. + for (unsigned idx = 0u; idx < FS_UTILS_CACHE_ENTRIES; idx++) { + fs->blockCache[idx].block = FS_UTILS_INVALID_BLOCK; + } + fs->blockCacheNext = 0u; + // Forget the medium itself. + fs->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(fs_utils_state_t *fs, uint32_t cluster, uint32_t offset) +{ + // TODO: clusterCount not yet available. + // assert(cluster >= 2u && cluster < clusterCount); + offset >>= FS_UTILS_BYTES_PER_BLOCK_SHIFT; + return fs->clusterHeapStart + ((cluster - 2u) << fs->blksPerClusterShift) + offset; +} + +// Validate directory handle. +inline bool dh_valid(fs_utils_state_t *fs, fs_utils_dir_handle_t dh) +{ + return dh < FS_UTILS_MAX_DIRS && (fs->dirs[dh].flags & FS_UTILS_FLAG_VALID); +} + +// Validate file handle. +inline bool fh_valid(fs_utils_state_t *fs, fs_utils_file_handle_t fh) +{ + return fh < FS_UTILS_MAX_FILES && (fs->files[fh].flags & FS_UTILS_FLAG_VALID); +} + +// Get a handle to the root directory of the mounted partition. +fs_utils_dir_handle_t rootdir_open(fs_utils_state_t *fs) +{ + if (!fs->partValid) { + return FS_UTILS_INVALID_DIR_HANDLE; + } + return dir_open(fs, fs->rootCluster); +} + +// Open a directory object that started in the given cluster. +fs_utils_dir_handle_t dir_open(fs_utils_state_t *fs, uint32_t cluster) +{ + // Ensure that we have a directory handle available + fs_utils_dir_handle_t dh = 0u; + while (fs->dirs[dh].flags & FS_UTILS_FLAG_VALID) { + if (++dh >= FS_UTILS_MAX_DIRS) { + return FS_UTILS_INVALID_DIR_HANDLE; + } + } + // Initialise directory fs. + fs->dirs[dh].flags = FS_UTILS_FLAG_VALID; + fs->dirs[dh].offset = 0u; + fs->dirs[dh].length = ~0u; // A special directory entry marks its end. + fs->dirs[dh].currCluster = cluster; + fs->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(fs_utils_state_t *fs, fs_utils_dir_handle_t dh, fs_utils_dir_entry_t *entry, + fs_utils_dir_flags_t flags, uint16_t *ucs, size_t ucs_max) +{ + if (!dh_valid(fs, dh)) { + return false; + } + + uint8_t entryType; + bool hasLFN = false; + do { + fs_utils_dir_entry_flags_t entryFlags = 0u; + uint8_t dir_entry[0x20u]; + if (sizeof(dir_entry) != object_read(fs, &fs->dirs[dh], dir_entry, sizeof(dir_entry))) { + return false; + } + if (fs->uart) { + uart_puts(fs->uart, "Dir entry:\n"); + uart_dump_bytes(fs->uart, 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 = entryFlags | DirEntryFlag_HasLongName; + } + if (attribs & 0x08) { + entryFlags = entryFlags | DirEntryFlag_VolumeLabel; + } + if (attribs & 0x010) { + entryFlags = entryFlags | DirEntryFlag_Subdirectory; + } + if (entryType == 0xe5) { + entryFlags = 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(fs_utils_state_t *fs, fs_utils_dir_handle_t dh, fs_utils_dir_entry_t *entry, + const char *name, uint16_t *ucs, size_t ucs_max) +{ + if (!dh_valid(fs, dh)) { + return false; + } + while (dir_next(fs, dh, entry, DirFlags_Default, fs->buf.nameBuffer, + sizeof(fs->buf.nameBuffer) / 2)) { + // Using the full name buffer here guarantees that 'dir_next' will have appended a NUL. + if (!ucs2_char_compare(fs->buf.nameBuffer, name, ~0u)) { + if (ucs) { + ucs2_copy(ucs, fs->buf.nameBuffer, ucs_max); + } + return true; + } + } + return false; +} + +// Variant using full UCS-2 filename encoding. +bool dir_find_ucs2(fs_utils_state_t *fs, fs_utils_dir_handle_t dh, fs_utils_dir_entry_t *entry, + const uint16_t *ucs_name) +{ + if (!dh_valid(fs, dh)) { + return false; + } + while (dir_next(fs, dh, entry, DirFlags_Default, fs->buf.nameBuffer, + sizeof(fs->buf.nameBuffer) / 2)) { + // Using the full name buffer here guarantees that 'dir_next' will have appended a NUL. + if (!ucs2_compare(fs->buf.nameBuffer, ucs_name, ~0u)) { + return true; + } + } + return false; +} + +// Release access to the given directory. +void dir_close(fs_utils_state_t *fs, fs_utils_dir_handle_t dh) +{ + if (dh < FS_UTILS_MAX_DIRS) { + fs->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. +fs_utils_file_handle_t file_open(fs_utils_state_t *fs, const fs_utils_dir_entry_t *entry) +{ + // Ensure that we have a file handle available + fs_utils_file_handle_t fh = 0u; + while (fs->files[fh].flags & FS_UTILS_FLAG_VALID) { + if (++fh >= FS_UTILS_MAX_FILES) { + return FS_UTILS_INVALID_FILE_HANDLE; + } + } + // Initialise file state. + fs->files[fh].flags = FS_UTILS_FLAG_VALID; + fs->files[fh].offset = 0u; + fs->files[fh].length = entry->dataLength; + fs->files[fh].currCluster = entry->firstCluster; + fs->files[fh].firstCluster = entry->firstCluster; + if (fs->uart) { + uprintf(fs->uart, "Opened file of 0x%x byte(s) at cluster 0x%x\n", 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). +fs_utils_file_handle_t file_open_str(fs_utils_state_t *fs, const char *name) +{ + // Maintain the pretence of supporting full pathnames; they may be supported at some point. + if (*name == '/' || *name == '\\') { + name++; + } + + fs_utils_file_handle_t fh = FS_UTILS_INVALID_FILE_HANDLE; + fs_utils_dir_handle_t dh = rootdir_open(fs); + if (dh_valid(fs, dh)) { + fs_utils_dir_entry_t entry; + if (dir_find(fs, dh, &entry, name, NULL, 0u)) { + fh = file_open(fs, &entry); + } + dir_close(fs, dh); + } + return fh; +} +fs_utils_file_handle_t file_open_ucs2(fs_utils_state_t *fs, const uint16_t *name) +{ + fs_utils_file_handle_t fh = FS_UTILS_INVALID_FILE_HANDLE; + fs_utils_dir_handle_t dh = rootdir_open(fs); + if (dh_valid(fs, dh)) { + fs_utils_dir_entry_t entry; + if (dir_find_ucs2(fs, dh, &entry, name)) { + fh = file_open(fs, &entry); + } + dir_close(fs, dh); + } + return fh; +} + +// Return the length of an open file, or a negative value if the file handle is invalid. +int64_t file_length(fs_utils_state_t *fs, fs_utils_file_handle_t fh) +{ + return fh_valid(fs, fh) ? (int64_t)fs->files[fh].length : -1; +} + +// Set the read position within a file. +bool file_seek(fs_utils_state_t *fs, fs_utils_file_handle_t fh, uint32_t offset) +{ + if (!fh_valid(fs, fh)) { + return 0u; + } + return object_seek(fs, &fs->files[fh], offset); +} + +// Read data from a file at the supplied offset, reading the requested number of bytes. +size_t file_read(fs_utils_state_t *fs, fs_utils_file_handle_t fh, uint8_t buf[], size_t len) +{ + if (!fh_valid(fs, fh)) { + return 0u; + } + return object_read(fs, &fs->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. +int64_t file_clusters(fs_utils_state_t *fs, fs_utils_file_handle_t fh, uint8_t *clusterShift, + uint32_t buf[], size_t len) +{ + // Check that the file handle is valid. + if (!fh_valid(fs, fh)) { + return -1; + } + // Indicate how many blocks form a cluster for this partition. + *clusterShift = fs->blksPerClusterShift; + // Run forwards from the current position, permitting incremental retrieval. + uint32_t cluster = fs->files[fh].currCluster; + // Ensure that the offset is aligned to the start of the cluster. + uint32_t offset = fs->files[fh].offset & ~fs->clusterMask; + size_t n = 0u; + while (len-- > 0u && !end_of_chain(cluster)) { + uint32_t nextCluster; + *buf++ = cluster; + n++; + if (!cluster_next(fs, &nextCluster, cluster)) { + break; + } + // Remember this position within the file. + offset += fs->clusterBytes; + fs->files[fh].offset = offset; + fs->files[fh].currCluster = cluster; + cluster = nextCluster; + } + return (int64_t)n; +} + +// Finalise read access to the given file. +void file_close(fs_utils_state_t *fs, fs_utils_file_handle_t fh) +{ + if (fh_valid(fs, fh)) { + fs->files[fh].flags = 0u; + } +} + +// Read Cylinder, Head and Sector ('CHS address'), as stored in a partition table entry. +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. +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. +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. +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. +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. +void generate_lfn(uint16_t *ucs, size_t ucs_max, const fs_utils_dir_entry_t *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. +inline bool include_entry(fs_utils_dir_entry_flags_t entryFlags, fs_utils_dir_flags_t flags) +{ + return (!(entryFlags & DirEntryFlag_Deleted) || (flags & DirFlag_IncludeDeleted)) && + (!(entryFlags & DirEntryFlag_Hidden) || (flags & DirFlag_IncludeHidden)) && + !(entryFlags & DirEntryFlag_VolumeLabel); +} diff --git a/sw/device/lib/runtime/filesys_utils.h b/sw/device/lib/runtime/filesys_utils.h new file mode 100644 index 000000000..b09135e7a --- /dev/null +++ b/sw/device/lib/runtime/filesys_utils.h @@ -0,0 +1,195 @@ +// Copyright lowRISC contributors (COSMIC project). +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +/** + * -- Ported from sonata-system C++ implementation -- + * + * 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 partition, 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 + */ + +#pragma once + +#include "hal/spi_host.h" +#include "hal/uart.h" +#include +#include + +// Some SD cards support only a 512-byte block size, and SPI mode transfers are +// always in terms of that anyway. +#define FS_UTILS_BYTES_PER_BLOCK_SHIFT (9u) +#define FS_UTILS_BLOCK_LEN (1u << FS_UTILS_BYTES_PER_BLOCK_SHIFT) + +// Number of entries in the block cache. +#define FS_UTILS_CACHE_ENTRIES (8u) +// Denotes an unused entry in the block cache. +#define FS_UTILS_INVALID_BLOCK (~(uint32_t)0u) + +// Object fs flags. +#define FS_UTILS_FLAG_VALID (1u << 31) + +// Open file and directory limits. +#define FS_UTILS_MAX_FILES (4u) +#define FS_UTILS_MAX_DIRS (2u) + +// Invalid file handle, returned by a failed `file_open` call. +#define FS_UTILS_INVALID_FILE_HANDLE (0xffu) +// Invalid directory handle, returned by a failed 'dir_open' call. +#define FS_UTILS_INVALID_DIR_HANDLE (0xffu) + +// State information on an object being accessed; this may be either a file or a directory. +typedef struct fs_utils_obj_state { + // 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; +} fs_utils_obj_state_t; + +typedef struct fs_utils_state { + // Access to debug/diagnostic logging. + uart_t uart; + // SD card access. + spi_host_t spi; + + // 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[FS_UTILS_BLOCK_LEN]; + uint16_t nameBuffer[0x100u]; + } buf; + + // Block cache for next eviction. + unsigned blockCacheNext; + // Each block within the cache. + struct { + // Block number of the data occupying this cache entry (or FS_UTILS_INVALID_BLOCK). + uint32_t block; + // Data for this block. + uint8_t buf[FS_UTILS_BLOCK_LEN]; + } blockCache[FS_UTILS_CACHE_ENTRIES]; + + // Set of open files. + fs_utils_obj_state_t files[FS_UTILS_MAX_FILES]; + + // Set of open directories. + fs_utils_obj_state_t dirs[FS_UTILS_MAX_DIRS]; +} fs_utils_state_t; + +// Opaque handle to an open file. +typedef uint8_t fs_utils_file_handle_t; + +// Opaque handle to an open directory. +typedef uint8_t fs_utils_dir_handle_t; + +// Flags specifying the type of directory access required. +typedef enum fs_utils_dir_flags { + DirFlag_Raw = 1u, + DirFlag_IncludeDeleted = 2u, + DirFlag_IncludeHidden = 4u, + + DirFlags_Default = 0u, +} fs_utils_dir_flags_t; + +// Directory entry type flags; this just makes the most common types of entries more accessible. +typedef enum fs_utils_dir_entry_flags { + DirEntryFlag_Deleted = 0x01u, + DirEntryFlag_Hidden = 0x02u, + DirEntryFlag_VolumeLabel = 0x04u, + DirEntryFlag_Subdirectory = 0x08u, + DirEntryFlag_HasLongName = 0x10u +} fs_utils_dir_entry_flags_t; + +// Description of an entry within a directory object. +typedef struct fs_utils_dir_entry { + fs_utils_dir_entry_flags_t 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; +} fs_utils_dir_entry_t; + +// 'Public' function declarations +bool fs_utils_init(fs_utils_state_t *fs, spi_host_t spi, uart_t uart); +void fin(fs_utils_state_t *fs); +uint32_t block_number(fs_utils_state_t *fs, uint32_t cluster, uint32_t offset); +bool dh_valid(fs_utils_state_t *fs, fs_utils_dir_handle_t dh); +bool fh_valid(fs_utils_state_t *fs, fs_utils_file_handle_t fh); +fs_utils_dir_handle_t rootdir_open(fs_utils_state_t *fs); +fs_utils_dir_handle_t dir_open(fs_utils_state_t *fs, uint32_t cluster); +bool dir_next(fs_utils_state_t *fs, fs_utils_dir_handle_t dh, fs_utils_dir_entry_t *entry, + fs_utils_dir_flags_t flags, uint16_t *ucs, size_t ucs_max); +bool dir_find(fs_utils_state_t *fs, fs_utils_dir_handle_t dh, fs_utils_dir_entry_t *entry, + const char *name, uint16_t *ucs, size_t ucs_max); +bool dir_find_ucs2(fs_utils_state_t *fs, fs_utils_dir_handle_t dh, fs_utils_dir_entry_t *entry, + const uint16_t *ucs_name); +void dir_close(fs_utils_state_t *fs, fs_utils_dir_handle_t dh); +int ucs2_compare(const uint16_t *ucs1, const uint16_t *ucs2, size_t len); +int ucs2_char_compare(const uint16_t *ucs1, const char *s2, size_t len); +void ucs2_copy(uint16_t *d, const uint16_t *s, size_t n); +fs_utils_file_handle_t file_open(fs_utils_state_t *fs, const fs_utils_dir_entry_t *entry); +fs_utils_file_handle_t file_open_str(fs_utils_state_t *fs, const char *name); +fs_utils_file_handle_t file_open_ucs2(fs_utils_state_t *fs, const uint16_t *name); +int64_t file_length(fs_utils_state_t *fs, fs_utils_file_handle_t fh); +bool file_seek(fs_utils_state_t *fs, fs_utils_file_handle_t fh, uint32_t offset); +size_t file_read(fs_utils_state_t *fs, fs_utils_file_handle_t fh, uint8_t buf[], size_t len); +int64_t file_clusters(fs_utils_state_t *fs, fs_utils_file_handle_t fh, uint8_t *clusterShift, + uint32_t buf[], size_t len); +void file_close(fs_utils_state_t *fs, fs_utils_file_handle_t fh); +void read_chs(uint16_t *c, uint8_t *h, uint8_t *s, const uint8_t *p); +uint32_t chs_to_lba(uint16_t c, uint8_t h, uint8_t s, uint8_t nheads, uint8_t nsecs); +uint32_t read32le(const uint8_t *p); +uint16_t read16le(const uint8_t *p); diff --git a/sw/device/lib/runtime/sdcard.c b/sw/device/lib/runtime/sdcard.c new file mode 100644 index 000000000..29b8c9cbb --- /dev/null +++ b/sw/device/lib/runtime/sdcard.c @@ -0,0 +1,505 @@ +// Copyright lowRISC contributors (COSMIC project). +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +#include "runtime/sdcard.h" +#include "runtime/print.h" +// #include + +// 'Private' function declarations +static bool collected_data(spi_host_t spi, uint8_t buf[], size_t len, uart_t uart); +static bool read_cid_csd(spi_host_t spi, uint8_t cmd, uint8_t buf[], size_t len, uart_t uart); +static void read_card_data(spi_host_t spi, uint8_t data[], size_t len); +static void wait_idle(spi_host_t spi); +static void nonblocking_cycles(spi_host_t spi, uint32_t cycles, bool csaat); +static void nonblocking_write(spi_host_t spi, const uint8_t data[], size_t len); + + +void deselect_card(spi_host_t spi) +{ + // Use a eight-cycle transaction with CSAAT unset to deassert the Chip-Select line. + // Using only one cycle seemed to screw-up the next transaction, so we use eight. + nonblocking_cycles(spi, 8, false); +} + +// Initialise the SD card ready for use. +bool sdcard_init(spi_host_t spi, uart_t uart) +{ + // Every card tried seems to be more than capable of keeping up with 12.5Mbps. + const unsigned kSpiSpeed = 1u; // 50 MHz / (1 + 1) = 12.5 MHz + DEV_WRITE(spi + SPI_HOST_CONFIGOPTS_REG, 0xFFFF & kSpiSpeed); + DEV_WRITE(spi + SPI_HOST_CONTROL_REG, + SPI_HOST_CONTROL_SPIEN_MASK); // keep output_en low for now to disable Chip-Select + + // Apparently we're required to send at least 74 SD CLK cycles with + // the device _not_ selected before talking to it. + nonblocking_cycles(spi, 74, false); + wait_idle(spi); + + DEV_WRITE(spi + SPI_HOST_CONTROL_REG, + (SPI_HOST_CONTROL_SPIEN_MASK | + SPI_HOST_CONTROL_OUTPUTEN_MASK)); // re-enable non-SCK outputs + + // 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(spi, SDCARD_CMD_GO_IDLE_STATE, 0u, uart); + } while (0x01 != get_response_R1(spi, uart)); + + send_command(spi, SDCARD_CMD_SEND_IF_COND, 0x1aau, uart); + get_response_R3(spi, uart); + + // Instruct the SD card whether to check CRC values on commands. + send_command(spi, SDCARD_CMD_CRC_ON_OFF, (uint32_t)SDCARD_CRC_ON, uart); + get_response_R1(spi, uart); + + // Read supported voltage range of the card. + send_command(spi, SDCARD_CMD_READ_OCR, 0, uart); + get_response_R3(spi, uart); + + do { + send_command(spi, SDCARD_CMD_APP_CMD, 0, uart); + (void)get_response_R1(spi, uart); + // Specify Host Capacity Support as 1. + send_command(spi, SDCARD_SD_SEND_OP_COND, 1u << 30, uart); + } while (0x01 & get_response_R1(spi, uart)); + + if (uart) { + uprintf(uart, "Setting block length to 0x%x\n", SDCARD_BLOCK_LEN); + } + + // Read card capacity information. + send_command(spi, SDCARD_CMD_READ_OCR, 0, uart); + get_response_R3(spi, uart); + + send_command(spi, SDCARD_CMD_SET_BLOCKLEN, SDCARD_BLOCK_LEN, uart); + uint8_t rd = get_response_R1(spi, uart); + if (uart) { + uprintf(uart, "Response: 0x%x\n", rd); + } + deselect_card(spi); + + return true; +} + +// Read Card Identification Data (CID). +bool read_cid(spi_host_t spi, uint8_t buf[], size_t len, uart_t uart) +{ + return read_cid_csd(spi, SDCARD_CMD_SEND_CID, buf, len, uart); +} + +// Read Card Specific Data (CSD). +bool read_csd(spi_host_t spi, uint8_t buf[], size_t len, uart_t uart) +{ + return read_cid_csd(spi, SDCARD_CMD_SEND_CSD, buf, len, uart); +} + +// Read a number of contiguous blocks from the SD card. +bool read_blocks(spi_host_t spi, uint32_t block, uint8_t buf[], size_t num_blocks, uart_t uart) +{ + const bool multi = num_blocks > 1u; + + bool ok = true; + for (size_t blk = 0u; blk < num_blocks; blk++) { + if (uart) { + uprintf(uart, "Reading block 0x%x\n", (unsigned int)(block + blk)); + } + + if (multi) { + // Is this the first block of the read request? + if (!blk) { + send_command(spi, SDCARD_CMD_READ_MULTIPLE_BLOCK, block, uart); + (void)get_response_R1(spi, uart); + } + } else { + send_command(spi, SDCARD_CMD_READ_SINGLE_BLOCK, block + blk, uart); + (void)get_response_R1(spi, uart); + } + + if (!collected_data(spi, &buf[blk * SDCARD_BLOCK_LEN], SDCARD_BLOCK_LEN, uart)) { + ok = false; + break; + } + } + + if (multi) { + send_command(spi, SDCARD_CMD_STOP_TRANSMISSION, 0u, uart); + (void)get_response_R1b(spi, uart); + } + + deselect_card(spi); + return ok; +} + +// Write a number of contiguous blocks from the SD card. +bool write_blocks(spi_host_t spi, uint32_t block, uint8_t buf[], size_t num_blocks, uart_t uart) +{ + const bool multi = num_blocks > 1u; + uint8_t crc16[2]; + crc16[1] = crc16[0] = 0xffu; // CRC16 not required by default for SPI mode. + + bool ok = true; + for (size_t blk = 0u; blk < num_blocks; blk++) { + if (SDCARD_CRC_ON) { + // CRC16 bytes follow the data block. + uint16_t crc = calc_crc16(&buf[blk * SDCARD_BLOCK_LEN], SDCARD_BLOCK_LEN); + crc16[0] = (uint8_t)(crc >> 8); + crc16[1] = (uint8_t)crc; + } + if (uart) { + uprintf(uart, "Writing block 0x%x\n", (unsigned int)(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(spi, SDCARD_CMD_WRITE_MULTIPLE_BLOCK, block, uart); + (void)get_response_R1(spi, uart); + } + start_token = SDCARD_START_BLOCK_TOKEN_MW; + } else { + send_command(spi, SDCARD_CMD_WRITE_SINGLE_BLOCK, block + blk, uart); + (void)get_response_R1(spi, uart); + start_token = SDCARD_START_BLOCK_TOKEN; + } + + nonblocking_write(spi, &start_token, 1u); + nonblocking_write(spi, &buf[blk * SDCARD_BLOCK_LEN], SDCARD_BLOCK_LEN); + nonblocking_write(spi, crc16, sizeof(crc16)); + // Collect data_response and wait until the card is no longer busy. + if (5 != (0x1f & get_data_response_busy(spi))) { + // Data not accepted because of an error. + ok = false; + break; + } + } + + if (multi) { + const uint8_t stop_tran_token = (uint8_t)SDCARD_STOP_TRAN_TOKEN; + nonblocking_write(spi, &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(spi)) { + } // Detect falling edge. + // Card will signal busy with zeros. + wait_not_busy(spi); + } + + deselect_card(spi); + return ok; +} + +// Send a command to the SD card with the supplied 32-bit argument. +void send_command(spi_host_t spi, uint8_t cmdCode, uint32_t arg, uart_t uart) +{ + uint8_t cmd[6]; + if (uart) { + uprintf(uart, "Sending command 0x%x\n", 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. + nonblocking_cycles(spi, 8u, true); + + 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 (SDCARD_CRC_ON || cmdCode == SDCARD_CMD_GO_IDLE_STATE || + cmdCode == SDCARD_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; + } + nonblocking_write(spi, 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(spi_host_t spi) +{ + uint8_t r; + read_card_data(spi, &r, 1u); + return r; +} + +// Get response type R1 from the SD card. +uint8_t get_response_R1(spi_host_t spi, uart_t uart) +{ + wait_idle(spi); + while (true) { + uint8_t rd1 = get_response_byte(spi); + // Whilst there is no response we read 0xff; an actual R1 response commences + // with a leading 0 bit (MSB). + if (!(rd1 & 0x80u)) { + if (uart) { + uprintf(uart, "R1 0x%x\n", rd1); + } + return rd1; + } + } +} + +// Wait until the SD card declares that it is no longer busy. +inline void wait_not_busy(spi_host_t spi) +{ + while (0x00 == get_response_byte(spi)) { + } // Wait whilst device is busy. +} + +// Get response type R1b from the SD card. +uint8_t get_response_R1b(spi_host_t spi, uart_t uart) +{ + wait_idle(spi); + uint8_t rd1 = get_response_R1(spi, uart); + // Card may signal busy with zero bytes. + wait_not_busy(spi); + return rd1; +} + +// Get data_response after sending a block of write data to the SD card. +uint8_t get_data_response_busy(spi_host_t spi) +{ + uint8_t rd1; + wait_idle(spi); + do { + rd1 = get_response_byte(spi); + } while ((rd1 & 0x11u) != 0x01u); + wait_not_busy(spi); + return rd1; +} + +// Get response type R3 (5 bytes) from the SD card. +void get_response_R3(spi_host_t spi, uart_t uart) +{ + volatile uint8_t rd2; + (void)get_response_R1(spi, uart); + for (int r = 0; r < 4; ++r) { + // Wait until SPI Host hardware is ready for a command + while (!(DEV_READ(spi + SPI_HOST_STATUS_REG) & SPI_HOST_STATUS_READY_MASK)) { + } + // Program an RX command segment + DEV_WRITE(spi + SPI_HOST_COMMAND_REG, + ((1 << SPI_HOST_COMMAND_CSAAT_OFFSET) | SPI_HOST_COMMAND_DIRECTION_RECEIVE)); + wait_idle(spi); + while (DEV_READ(spi + SPI_HOST_STATUS_REG) & SPI_HOST_STATUS_RXEMPTY_MASK) { + } + rd2 = (uint8_t)(DEV_READ(spi + SPI_HOST_RXDATA_REG)); + } + // 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. +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. +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; +} + +// Collect an expect number of bytes into the given buffer, and return an indication of whether +// they were collected without error. +static bool collected_data(spi_host_t spi, uint8_t buf[], size_t len, uart_t uart) +{ + uint8_t crc16[2]; + + // Collect the data, prepended with the Start Block Token and followed by the CRC16 bytes. + while (SDCARD_START_BLOCK_TOKEN != get_response_byte(spi)) { + } + // 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(spi, buf, len); + read_card_data(spi, crc16, sizeof(crc16)); + + // Shall we validate the CRC16 of the received data block? + if (SDCARD_CRC_ON) { + uint16_t exp_crc16 = calc_crc16(buf, len); + uint16_t obs_crc16 = ((uint16_t)crc16[0] << 8) | crc16[1]; + if (uart) { + uprintf(uart, "Read block CRC 0x%x\n", obs_crc16); + uprintf(uart, "Calculated CRC 0x%x\n", exp_crc16); + } + if (obs_crc16 != exp_crc16) { + deselect_card(spi); + if (uart) { + uart_puts(uart, "CRC16 mismatch\n"); + } + return false; + } + } + return true; +} + +// Shared implementation for SDCARD_CMD_SEND_CID and SDCARD_CMD_SEND_CSD. +static bool read_cid_csd(spi_host_t spi, uint8_t cmd, uint8_t buf[], size_t len, uart_t uart) +{ + send_command(spi, cmd, 0, uart); + (void)get_response_R1(spi, uart); + bool ok = collected_data(spi, buf, len, uart); + deselect_card(spi); + 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. + */ +static void read_card_data(spi_host_t spi, uint8_t data[], size_t len) +{ + // assert((len-1) <= SPI_HOST_COMMAND_LEN_MAX); + wait_idle(spi); + // Do not attempt a zero-byte transfer; not supported by the controller. + if (len) { + // Wait until SPI Host hardware is ready for a command + while (!(DEV_READ(spi + SPI_HOST_STATUS_REG) & SPI_HOST_STATUS_READY_MASK)) { + } + // Program an RX command segment + DEV_WRITE(spi + SPI_HOST_COMMAND_REG, + ((1 << SPI_HOST_COMMAND_CSAAT_OFFSET) | SPI_HOST_COMMAND_DIRECTION_RECEIVE | + (((len - 1) << SPI_HOST_COMMAND_LEN_OFF) & SPI_HOST_COMMAND_LEN_MASK))); + // Pull data from the RX FIFO as it becomes available + const uint8_t *end = data + len - 1; + while (data <= end) { + // Wait for data + while (DEV_READ(spi + SPI_HOST_STATUS_REG) & SPI_HOST_STATUS_RXEMPTY_MASK) { + } + // Read a 32-bit RX FIFO word and pack relevant bytes into the destination array + uint32_t data_word = DEV_READ(spi + SPI_HOST_RXDATA_REG); + for (uint32_t by = 0; (by < 4) && (data <= end); by++) { + *data++ = (uint8_t)(data_word); + data_word >>= 8; + } + } + } +} + +/* + * Poll the Status register until the Active flag has been cleared + */ +static void wait_idle(spi_host_t spi) +{ + // One cycle of delay is needed to avoid a race condition when this + // function is called directly after writing a command segment. + __asm__("nop"); + while (SPI_HOST_STATUS_ACTIVE_MASK & DEV_READ(spi + SPI_HOST_STATUS_REG)) { + } +} + +/* + * Program a directionless segment (SCK and CS, but no data RX or TX) + * to be 'transmitted' by the SPI Host hardware. + * This is that same as transmitting all-ones due to the way the + * data output enable has been used in hardware. + * + * Note that length is specified in SCK cycles, rather than bytes. + * + * Leave the chip-select line asserted afterwards if `csaat` is set. + */ +static void nonblocking_cycles(spi_host_t spi, uint32_t cycles, bool csaat) +{ + if (cycles) { + // Wait until SPI Host hardware is ready for a command + while (!(DEV_READ(spi + SPI_HOST_STATUS_REG) & SPI_HOST_STATUS_READY_MASK)) { + } + // Program a directionless command segment + DEV_WRITE(spi + SPI_HOST_COMMAND_REG, + ((csaat << SPI_HOST_COMMAND_CSAAT_OFFSET) | + (((cycles - 1) << SPI_HOST_COMMAND_LEN_OFF) & SPI_HOST_COMMAND_LEN_MASK))); + } +} + +/* + * Program the transmission of `len` bytes starting with `data[0]` + * by the SPI Host hardware. + */ +static void nonblocking_write(spi_host_t spi, const uint8_t data[], size_t len) +{ + // assert((len-1) <= SPI_HOST_COMMAND_LEN_MAX); + // Wait until SPI Host hardware is ready for a command + while (!(DEV_READ(spi + SPI_HOST_STATUS_REG) & SPI_HOST_STATUS_READY_MASK)) { + } + // Program a TX command segment. + // Doing this before providing TX data avoids the TX FIFO size being a hard limit. + DEV_WRITE(spi + SPI_HOST_COMMAND_REG, + ((1 << SPI_HOST_COMMAND_CSAAT_OFFSET) | SPI_HOST_COMMAND_DIRECTION_TRANSMIT | + (((len - 1) << SPI_HOST_COMMAND_LEN_OFF) & SPI_HOST_COMMAND_LEN_MASK))); + // Load TX data using a fast full-word loop followed by a slower clean-up loop. + // The hope with the fast loop is + size_t by = 0; + uint32_t data_word; + if (3 < len) { + while (by < (len - 3u)) { + // Prepare a full 32-bit word of data + data_word = data[by]; + data_word |= data[by + 1] << 8; + data_word |= data[by + 2] << 16; + data_word |= data[by + 3] << 24; + // Wait for TX FIFO space + while (DEV_READ(spi + SPI_HOST_STATUS_REG) & SPI_HOST_STATUS_TXFULL_MASK) { + } + // Write data to the SPI Host TX FIFO + DEV_WRITE(spi + SPI_HOST_TXDATA_REG, data_word); + by += 4; + } + } + if (by < len) { + // Prepare a partial word containing remaining data + data_word = data[by]; + if (len & 0x2) { + data_word |= data[by + 1] << 8; + if (len & 0x1) { + data_word |= data[by + 2] << 16; + } + } + // Wait for TX FIFO space + while (DEV_READ(spi + SPI_HOST_STATUS_REG) & SPI_HOST_STATUS_TXFULL_MASK) { + } + // Write data to the SPI Host TX FIFO + DEV_WRITE(spi + SPI_HOST_TXDATA_REG, data_word); + } +} diff --git a/sw/device/lib/runtime/sdcard.h b/sw/device/lib/runtime/sdcard.h new file mode 100644 index 000000000..2df140173 --- /dev/null +++ b/sw/device/lib/runtime/sdcard.h @@ -0,0 +1,71 @@ +// Copyright lowRISC contributors (COSMIC project). +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +/** + * -- Ported from sonata-system C++ implementation -- + * + * A basic set of functions for interfacing with an SD Card using the SPI Host + * hardware block. + * + * Note that the included SD Card initialisation process is particularly + * stripped-down and will only work with certain cards until it is improved. + */ + +#pragma once + +#include "hal/mmio.h" +#include "hal/spi_host.h" +#include "hal/uart.h" +#include +#include +#include + +// Compile-switches for sdcard-utils code. +// - Enable/disable CRC checking on SD Card traffic +#define SDCARD_CRC_ON true + +// Transfers in SPI mode are always in terms of 512-byte blocks. +#define SDCARD_BLOCK_LEN (512u) + +// SD command codes. (Section 7.3.1) +#define SDCARD_CMD_GO_IDLE_STATE (0u) +#define SDCARD_CMD_SEND_OP_COND (1u) +#define SDCARD_CMD_SEND_IF_COND (8u) +#define SDCARD_CMD_SEND_CSD (9u) +#define SDCARD_CMD_SEND_CID (10u) +#define SDCARD_CMD_STOP_TRANSMISSION (12u) +#define SDCARD_CMD_SET_BLOCKLEN (16u) +#define SDCARD_CMD_READ_SINGLE_BLOCK (17u) +#define SDCARD_CMD_READ_MULTIPLE_BLOCK (18u) +#define SDCARD_CMD_WRITE_SINGLE_BLOCK (24u) +#define SDCARD_CMD_WRITE_MULTIPLE_BLOCK (25u) +#define SDCARD_SD_SEND_OP_COND (41u) +#define SDCARD_CMD_APP_CMD (55u) +#define SDCARD_CMD_READ_OCR (58u) +#define SDCARD_CMD_CRC_ON_OFF (59u) + +// SD Control Tokens. (Section 7.3.3) +// Start Block Token precedes data block, for all but Multiple Block Write. +#define SDCARD_START_BLOCK_TOKEN (0xfeu) +// Start Block Token used for Multiple Block Write operations. +#define SDCARD_START_BLOCK_TOKEN_MW (0xfcu) +// Stop Transaction Token, for Multiple Block Writes. +#define SDCARD_STOP_TRAN_TOKEN (0xfdu) + +// 'Public' function declarations +void deselect_card(spi_host_t spi); +bool sdcard_init(spi_host_t spi, uart_t uart); +bool read_cid(spi_host_t spi, uint8_t buf[], size_t len, uart_t uart); +bool read_csd(spi_host_t spi, uint8_t buf[], size_t len, uart_t uart); +bool read_blocks(spi_host_t spi, uint32_t block, uint8_t buf[], size_t num_blocks, uart_t uart); +bool write_blocks(spi_host_t spi, uint32_t block, uint8_t buf[], size_t num_blocks, uart_t uart); +void send_command(spi_host_t spi, uint8_t cmdCode, uint32_t arg, uart_t uart); +uint8_t get_response_byte(spi_host_t spi); +uint8_t get_response_R1(spi_host_t spi, uart_t uart); +void wait_not_busy(spi_host_t spi); +uint8_t get_response_R1b(spi_host_t spi, uart_t uart); +uint8_t get_data_response_busy(spi_host_t spi); +void get_response_R3(spi_host_t spi, uart_t uart); +uint8_t calc_crc7(const uint8_t data[], size_t len); +uint16_t calc_crc16(const uint8_t data[], size_t len); diff --git a/sw/device/tests/CMakeLists.txt b/sw/device/tests/CMakeLists.txt index 63c319207..6b28a8bd1 100644 --- a/sw/device/tests/CMakeLists.txt +++ b/sw/device/tests/CMakeLists.txt @@ -19,7 +19,7 @@ mocha_add_test(NAME mailbox_smoketest SOURCES mailbox/smoketest.c LIBRARIES ${LI mocha_add_test(NAME plic_smoketest SOURCES plic/smoketest.c LIBRARIES ${LIBS}) mocha_add_test(NAME rstmgr_software_reset SOURCES rstmgr/software_reset.c LIBRARIES ${LIBS}) mocha_add_test(NAME spi_device_smoketest SOURCES spi_device/smoketest.c LIBRARIES ${LIBS}) -mocha_add_test(NAME spi_host_smoketest SOURCES spi_host/smoketest.c LIBRARIES ${LIBS}) +mocha_add_test(NAME spi_host_sdcard_tests SOURCES spi_host/sdcard_tests.c LIBRARIES ${LIBS}) mocha_add_test(NAME tag_controller_smoketest SOURCES tag_controller/smoketest.c LIBRARIES ${LIBS}) mocha_add_test(NAME tag_controller_tag_test SOURCES tag_controller/tag_test.c LIBRARIES ${LIBS}) mocha_add_test(NAME timer_smoketest SOURCES timer/smoketest.c LIBRARIES ${LIBS}) diff --git a/sw/device/tests/spi_host/lorem_text.h b/sw/device/tests/spi_host/lorem_text.h new file mode 100644 index 000000000..6c79dff72 --- /dev/null +++ b/sw/device/tests/spi_host/lorem_text.h @@ -0,0 +1,85 @@ + +// Copyright lowRISC contributors (COSMIC project). +// 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/device/tests/spi_host/sdcard_tests.c b/sw/device/tests/spi_host/sdcard_tests.c new file mode 100644 index 000000000..bbe59abf6 --- /dev/null +++ b/sw/device/tests/spi_host/sdcard_tests.c @@ -0,0 +1,270 @@ +// Copyright lowRISC contributors (COSMIC project). +// Licensed under the Apache License, Version 2.0, see LICENSE for details. +// SPDX-License-Identifier: Apache-2.0 + +#include "runtime/sdcard.h" +#include "hal/gpio.h" +#include "hal/mocha.h" +#include "hal/spi_host.h" +#include "hal/uart.h" +#include "runtime/filesys_utils.h" +#include "runtime/print.h" +#include +#include + +// Lorem Ipsum sample text. +#include "lorem_text.h" + +#define MAX_BLOCKS 0x10 +#define BLOCK_LEN 0x200 + + +// Set this for manual operation rather than automated regression testing. +const bool manual = false; + +// Set this to true to enable diagnostic logging. +const 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. +const bool emitText = false; + +// Scratch workspace for reading file blocks or Long FileName. +uint8_t fileBuffer[BLOCK_LEN]; + +// Emit the UCS-2 long filename in a readable form; we cannot do this properly; we're just +// handling ASCII in practice. +static void write_str_ucs2(uart_t uart, const uint16_t *ucs, size_t ucs_max) +{ + 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] : '.'; + uart_puts(uart, str); + idx++; + } +} + +static void write_test_result(uart_t uart, unsigned int failures) +{ + if (failures == 0) { + uart_puts(uart, "PASS!\n"); + } else { + uart_puts(uart, "FAIL!\n"); + } +} + +// Compare a sequence of bytes against a reference, returning the number of mismatches. +static unsigned int compare_bytes(const char ref[], unsigned *offset, const uint8_t data[], + size_t len) +{ + unsigned int 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 unsigned int read_card_properties(bool *validCID, spi_host_t spi, uart_t uart, bool logging) +{ + unsigned int failures = 0u; + uint8_t buf[16]; + for (size_t i = 0; i < sizeof(buf); i++) { + buf[i] = 0xbd; + } + uart_puts(uart, " Reading Card Specific Data (CSD) "); + if (read_csd(spi, (uint8_t *)&buf, sizeof(buf), uart)) { + if (logging) { + uart_dump_bytes(uart, (const uint8_t *)&buf, sizeof(buf)); + } + // The final byte contains a CRC7 field within its MSBs. + uint8_t crc = 1u | (calc_crc7((const uint8_t *)&buf, sizeof(buf) - 1u) << 1); + failures += (crc != buf[sizeof(buf) - 1u]); + } else { + failures++; + } + write_test_result(uart, failures); + + for (size_t i = 0; i < sizeof(buf); i++) { + buf[i] = 0xbd; + } + uart_puts(uart, " Reading Card Identification (CID) "); + *validCID = false; + if (read_cid(spi, (uint8_t *)&buf, sizeof(buf), uart)) { + if (logging) { + uart_dump_bytes(uart, (const uint8_t *)&buf, sizeof(buf)); + } + // The final byte contains a CRC7 field within its MSBs. + uint8_t crc = 1u | (calc_crc7((const uint8_t *)&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(uart, 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. + * + * Use UART for logging as it is the only option at present. + */ +bool sdcard_tests(spi_host_t spi, gpio_t gpio, uart_t uart) +{ + // Have we been asked to emit the sample text? + if (emitText) { + uart_puts(uart, "Capture everything between the dotted lines, being careful not " + "to introduce any additional line breaks.\n"); + uart_puts(uart, "--------\n"); + uart_puts(uart, lorem_text); + uart_puts(uart, "--------\n"); + uart_puts(uart, "Each of these single-line paragraphs shall be CR,LF terminated " + "and followed by a blank line.\n"); + uart_puts( + uart, + "This includes the final one, and thus the file itself ends with a blank line.\n"); + uart_puts(uart, "The file should be 4,210 bytes in length.\n"); + } + + + // microSD card detection bit is on GPIO input 9 (0-indexed). + // Input is forced low if card is present, or pulled high if absent. + const unsigned detBit = 9u; + + unsigned int failures = 0u; + if (gpio_read_pin(gpio, detBit)) { + if (manual) { + // Wait until a card is detected. + uart_puts(uart, "Please insert a microSD card into the slot...\n"); + while (gpio_read_pin(gpio, detBit)) { + } + } else { + uart_puts(uart, "No microSD card detected\n"); + failures++; + } + } + if (!gpio_read_pin(gpio, detBit)) { + bool validCID; + + // Initialise SD card access + sdcard_init(spi, uart); + + uart_puts(uart, "Reading card properties.... \n"); + failures += read_card_properties(&validCID, spi, uart, true); + + // 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) { + uart_puts(uart, "Reading card contents.... \n"); + // Filesystem state struct is too big for the stack, so statically allocate it. + static fs_utils_state_t fs; + + failures += !fs_utils_init(&fs, spi, uart); + write_test_result(uart, failures); + + if (!failures) { + // List the files and subdirectories in the root directory. + uart_puts(uart, "Reading root directory.... \n"); + fs_utils_dir_handle_t dh = rootdir_open(&fs); + if (dh == FS_UTILS_INVALID_DIR_HANDLE) { + failures++; + } else { + uint16_t *ucs = (uint16_t *)fileBuffer; + const size_t ucs_max = sizeof(fileBuffer) / 2; + fs_utils_dir_entry_t entry; + while (dir_next(&fs, dh, &entry, DirFlags_Default, ucs, ucs_max)) { + uart_puts(uart, "'"); + write_str_ucs2(uart, ucs, ucs_max); + uprintf(uart, "' : length 0x%x cluster 0x%x\n", entry.dataLength, + entry.firstCluster); + } + dir_close(&fs, dh); + } + write_test_result(uart, failures); + + // Locate and check the LOREM.IPS test file in the root directory. + fs_utils_file_handle_t fh = file_open_str(&fs, "lorem.ips"); + if (fh == FS_UTILS_INVALID_FILE_HANDLE) { + uart_puts(uart, "Unable to locate file\n"); + failures++; + } else { + // Determine the length of the file. + int64_t fileLen = file_length(&fs, fh); + if (fileLen < 0) { + uart_puts(uart, "Failed to read file length\n"); + failures++; + } else { + uprintf(uart, "File is 0x%x byte(s)\n", (unsigned int)fileLen); + } + uint32_t sampleOffset = 0u; + while (fileLen > 0 && sampleOffset < sizeof(lorem_text)) { + // Work out how many bytes we can compare. + uint32_t chunkLen = ((uint64_t)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 = file_read(&fs, fh, fileBuffer, chunkLen); + if (read != chunkLen) { + // We did not read the expected number of bytes. + uart_puts(uart, + "File read did not return the requested number of bytes\n"); + failures++; + } + if (logging) { + uart_dump_bytes(uart, (const uint8_t *)fileBuffer, chunkLen); + } + // Compare this data against the sample text. + failures += compare_bytes(lorem_text, &sampleOffset, fileBuffer, chunkLen); + fileLen -= chunkLen; + } + uart_puts(uart, "Done text comparison\n"); + // If we have not compared the entire file, count that as a failure. + failures += (fileLen > 0); + file_close(&fs, fh); + } + write_test_result(uart, failures); + } else { + uart_puts(uart, "No valid Master Boot Record found (signature not detected)\n"); + failures++; + } + } + } + write_test_result(uart, failures); + return failures == 0; +} + + +bool test_main(uart_t console) +{ + // The SPI controller talks to the microSD card in SPI mode. + spi_host_t spi = mocha_system_spi_host(); + spi_host_init(spi); + + // We need to use the GPIO to detect card presence. + gpio_t gpio = mocha_system_gpio(); + + // Run tests + return sdcard_tests(spi, gpio, console); +} diff --git a/sw/device/tests/spi_host/smoketest.c b/sw/device/tests/spi_host/smoketest.c deleted file mode 100644 index 658f22cdf..000000000 --- a/sw/device/tests/spi_host/smoketest.c +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright lowRISC contributors (COSMIC project). -// Licensed under the Apache License, Version 2.0, see LICENSE for details. -// SPDX-License-Identifier: Apache-2.0 - -#include "hal/mocha.h" -#include "hal/spi_host.h" -#include -#include - -bool test_main() -{ - uint32_t tx_data = 0xDEADC0DE; - uint32_t rx_data; - spi_host_t spi_host; - - spi_host = mocha_system_spi_host(); - spi_host_init(spi_host); - spi_host_write(spi_host, tx_data); - rx_data = spi_host_read(spi_host); - return tx_data == rx_data; -}