diff --git a/.reuse/dep5 b/.reuse/dep5 index 4c9264316..e2cc36c24 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -33,6 +33,7 @@ Files: drivers/timer/meson/config.json drivers/timer/goldfish/config.json drivers/timer/cdns/config.json + drivers/spi/opentitan/config.json Copyright: UNSW License: BSD-2-Clause diff --git a/docs/design/design.tex b/docs/design/design.tex index 1e534c823..805664bb8 100644 --- a/docs/design/design.tex +++ b/docs/design/design.tex @@ -1860,7 +1860,39 @@ \section{Serial busses}\label{s:cl-bus} \subsubsection{SPI}\label{s:cl-spi} -\ToCome{Details} +An SPI bus consists of one host device and N client devices; this is reflected with one driver and a +corresponding virtualiser controlling the host, and a maximum of N clients controlling their +respective device. + +All devices share three wires: clock, MISO (client to host) and MOSI (host to client); individual +devices are addressed by pulling down its respective CS line. +SPI allows for full-duplex communication, as MISO and MOSI are always active; however, not all bytes +are meaningful. +The smallest unit of interaction in SPI subsystem is the command: + +\begin{lstlisting}[gobble=2,firstline=2,float=th, + label={l:SPI_command_definition}, + caption={SPI Command definition.}] + + typedef struct spi_cmd { + /* Offset in the data region to start reading from, -1 if not reading */ + size_t read_offset; + /* Offset in the data region to start writing from, -1 if not writing */ + size_t write_offset; + /* Number of bytes to read and/or write, or clocks to toggle if not reading or writing; + will be the same for reading and writing due to the full-duplex nature of the bus */ + uint16_t len; + /* Should be true for all commands in a transaction, except for the last one */ + bool cs_active_after_cmd; + } spi_cmd_t; + +\end{lstlisting} + +The clients control their respective device by enqueueing transactions, which are a sequence of +commands, where the CS line is pulled low during the entire duration. + +The SPI subsystem maps each client's data region into the driver's address space, which is referred +to by commands. \subsubsection{I2C host}\label{s:cl-i2c} diff --git a/docs/drivers.md b/docs/drivers.md index 87103b503..6eacc908b 100644 --- a/docs/drivers.md +++ b/docs/drivers.md @@ -24,6 +24,12 @@ and a list of Device Tree compatible strings it is known to work with. * Meson * `amlogic,meson-axg-i2c` +## SPI + +* OpenTitan (PULP) + * `opentitan,spi-host` + * `lowrisc,spi` + ## Network * DWMAC diff --git a/drivers/spi/opentitan/config.json b/drivers/spi/opentitan/config.json new file mode 100644 index 000000000..e353cb2f9 --- /dev/null +++ b/drivers/spi/opentitan/config.json @@ -0,0 +1,24 @@ +{ + "compatible": [ + "opentitan,spi-host", + "lowrisc,spi" + ], + "resources": { + "regions": [ + { + "name": "regs", + "perms": "rw", + "size": 4096, + "dt_index": 0 + } + ], + "irqs": [ + { + "dt_index": 0 + }, + { + "dt_index": 1 + } + ] + } +} diff --git a/drivers/spi/opentitan/driver.h b/drivers/spi/opentitan/driver.h new file mode 100644 index 000000000..a001fb6bf --- /dev/null +++ b/drivers/spi/opentitan/driver.h @@ -0,0 +1,178 @@ +/* + * Copyright 2025, UNSW + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +/* This driver is based on: + + PULP patches to OpenTitan peripherals + https://github.com/pulp-platform/opentitan_peripherals/blob/master/sw/include/spi_host_regs.h + Commit ID: cd3153de2783abd3d03d0595e6c4b32413c62f14 + + */ + +/* Memory map of the registers */ +struct spi_regs { + uint32_t INTR_STATE; + uint32_t INTR_ENABLE; + uint32_t INTR_TEST; + uint32_t ALERT_TEST; + uint32_t CONTROL; + uint32_t STATUS; + uint32_t CONFIGOPTS0; + uint32_t CONFIGOPTS1; + uint32_t CONFIGOPTS2; + uint32_t CSID; + uint32_t COMMAND; + uint32_t RXDATA; + uint32_t TXDATA; + uint32_t ERROR_ENABLE; + uint32_t ERROR_STATUS; + uint32_t EVENT_ENABLE; +}; + +/* Data required for the driver as it advances through the FSM */ +typedef struct spi_driver_data { + // Per CS state + spi_cs_t cs; + void *data_region; + uint16_t cmd_in_progress; + // Logical command + size_t read_offset; + size_t write_offset; + uint32_t len; // Needs to be a u32, since the u16 is interpretted as [1, 65536] + bool cs_active_after_cmd; + // Logical command in-progress state + uint32_t logical_progress; + // Physical command in-progress state + uint32_t phy_cmd_len; + uint32_t tx_progress; + uint32_t rx_progress; + // Error + spi_status_t err; +} spi_driver_data_t; + +static void spi_reset_state(spi_driver_data_t *s) +{ + s->cs = -1; + s->data_region = NULL; + s->cmd_in_progress = 0; + s->read_offset = -1; + s->write_offset = -1; + s->len = 0; + s->cs_active_after_cmd = false; + s->logical_progress = -1; + s->phy_cmd_len = 0; + s->tx_progress = -1; + s->rx_progress = -1; + s->err = SPI_STATUS_OK; +} + +typedef enum spi_state { + SPI_STATE_IDLE, + SPI_STATE_GET_CS, + SPI_STATE_GET_LOGICAL_CMD, + SPI_STATE_ISSUE_PHY_CMD, + SPI_STATE_EXEC_PHY_CMD, + SPI_STATE_AWAIT_PHY_CMD, + SPI_STATE_RESP, +} spi_state_t; + +typedef struct fsm_state { + spi_state_t nxt_state; + bool yield; +} fsm_state_t; + +char *fsm_str(spi_state_t state) +{ + switch (state) { + case SPI_STATE_IDLE: + return "IDLE"; + case SPI_STATE_GET_CS: + return "GET_CS"; + case SPI_STATE_GET_LOGICAL_CMD: + return "GET_LOGICAL_CMD"; + case SPI_STATE_ISSUE_PHY_CMD: + return "ISSUE_PHY_CMD"; + case SPI_STATE_EXEC_PHY_CMD: + return "EXEC_PHY_CMD"; + case SPI_STATE_AWAIT_PHY_CMD: + return "AWAIT_PHY_CMD"; + case SPI_STATE_RESP: + return "RESP"; + default: + return "INVALID"; + } +} + +#define TIMEOUT_LIMIT (0xFFF) + +#define PULP_MAX_CS_LINE (3) +#define FIFO_DEPTH (64) +#define ERROR_IRQ (0) +#define EVENT_IRQ (1) + +/* Register Macros */ +#define INTR_ERROR (BIT(0)) +#define INTR_SPI_EVENT (BIT(1)) + +#define CONTROL_SPIEN (BIT(31)) +#define CONTROL_SW_RST (BIT(30)) +#define CONTROL_OUTPUT_EN (BIT(29)) +#define CONTROL_TX_WATERMARK(num) (((num) & 0xFF) << 8) +#define CONTROL_RX_WATERMARK_MASK (0xFF) +#define CONTROL_RX_WATERMARK(num) ((num) & CONTROL_RX_WATERMARK_MASK) + +#define CONFIGOPTS_CLKDIV(div) ((div) & 0xFFFF) +#define CONFIGOPTS_CSNIDLE(num) (((num) & 0xF) << 16) +#define CONFIGOPTS_CSNTRAIL(num) (((num) & 0xF) << 20) +#define CONFIGOPTS_CSNLEAD(num) (((num) & 0xF) << 24) +#define CONFIGOPTS_FULLCYC (BIT(29)) +#define CONFIGOPTS_CPHA (BIT(30)) +#define CONFIGOPTS_CPOL (BIT(31)) + +typedef enum command_direction { + COMMAND_DIRECTION_TX_ONLY = BIT(13), + COMMAND_DIRECTION_RX_ONLY = BIT(12), + COMMAND_DIRECTION_BIDIRECTION = BIT(12) | BIT(13), + COMMAND_DIRECTION_DUMMY_CYCLES = 0, +} command_direction_t; + +#define COMMAND_CSAAT (BIT(9)) + +// A small quirk of the hardware, the length reported to the device should be one less than the +// intended length +#define COMMAND_LEN_MAX (0x1FF + 0x1) +#define COMMAND_LEN(length) (((length) - 1) & (COMMAND_LEN_MAX - 1)) + +#define STATUS_READY(status) ((status) & BIT(31)) +#define STATUS_ACTIVE(status) ((status) & BIT(30)) +#define STATUS_TXEMPTY(status) ((status) & BIT(28)) +#define STATUS_TXSTALL(status) ((status) & BIT(27)) +#define STATUS_RXWM(status) ((status) & BIT(20)) +#define STATUS_RXQD(status) (((status) & 0xFF00) >> 8) +#define STATUS_TXQD(status) ((status) & 0xFF) + +/* Access invalid is always active */ +#define ERROR_ACCESSINVAL (BIT(5)) +#define ERROR_CSIDINVAL (BIT(4)) +#define ERROR_CMDINVAL (BIT(3)) +#define ERROR_UNDERFLOW (BIT(2)) +#define ERROR_OVERFLOW (BIT(1)) +#define ERROR_CMDBUSY (BIT(0)) + +#define ERROR_MASK (ERROR_ACCESSINVAL | ERROR_CSIDINVAL | \ + ERROR_CMDINVAL | ERROR_UNDERFLOW | \ + ERROR_OVERFLOW | ERROR_CMDBUSY) + +#define EVENT_ENABLE_IDLE (BIT(5)) +#define EVENT_ENABLE_READY (BIT(4)) +#define EVENT_ENABLE_TXWM (BIT(3)) +#define EVENT_ENABLE_RXWM (BIT(2)) +#define EVENT_ENABLE_TXEMPTY (BIT(1)) +#define EVENT_ENABLE_RXFULL (BIT(0)) diff --git a/drivers/spi/opentitan/spi.c b/drivers/spi/opentitan/spi.c new file mode 100644 index 000000000..f40272009 --- /dev/null +++ b/drivers/spi/opentitan/spi.c @@ -0,0 +1,502 @@ +/* + * Copyright 2025, UNSW + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include "driver.h" + +#define DEBUG_DRIVER +#ifdef DEBUG_DRIVER +#define LOG_DRIVER(...) do{ sddf_dprintf("SPI DRIVER|INFO: "); sddf_dprintf(__VA_ARGS__); }while(0) +#else +#define LOG_DRIVER(...) do{}while(0) +#endif + +#define LOG_DRIVER_ERR(...) do{ sddf_printf("SPI DRIVER|ERROR: "); sddf_printf(__VA_ARGS__); }while(0) + +__attribute((__section__(".spi_driver_config"))) spi_driver_config_t config; + +__attribute((__section__(".device_resources"))) device_resources_t device_resources; + +spi_handle_t virt_handle; + +// MMIO registers +volatile struct spi_regs *regs; + +// FSM state +fsm_state_t fsm_state; +spi_driver_data_t driver_data; + +/** + * Resets the SPI host + */ +static inline void device_reset(void) +{ + // Reset SPI host + regs->CONTROL = CONTROL_SW_RST; + + // Poll status until inactive and both FIFOs are drained + uint32_t status; + do { + status = regs->STATUS; + } while (STATUS_ACTIVE(status) || STATUS_TXQD(status) != 0 || STATUS_RXQD(status) != 0); + + regs->CONTROL = 0; +} + +/** + * + */ +static inline uint32_t pack_config(spi_device_config_t *config) +{ + uint32_t temp = CONFIGOPTS_CLKDIV(config->freq_div); + if (config->cpha) { + temp |= CONFIGOPTS_CPHA; + } + if (config->cpol) { + temp |= CONFIGOPTS_CPOL; + } + return temp; +} + +/** + * Brings the SPI host into a usable state + */ +static inline void device_setup(void) +{ + device_reset(); + + // Enable the device + regs->CONTROL = CONTROL_SPIEN | CONTROL_OUTPUT_EN; + + // Interrupt when IDLE + regs->EVENT_ENABLE = EVENT_ENABLE_IDLE | EVENT_ENABLE_RXFULL | EVENT_ENABLE_TXEMPTY | EVENT_ENABLE_RXWM; + + // Set the receive watermark at the max + regs->CONTROL |= CONTROL_RX_WATERMARK(FIFO_DEPTH / sizeof(uint32_t) - 1); + + // Recieve interrupts for all errors, since an unacknowledged error could halt the device + regs->ERROR_ENABLE = ERROR_MASK; + + // Enable only error interrupts, as enabling event interrupts would spam TXEMPTY + regs->INTR_ENABLE = INTR_ERROR; + + // Setup all CS lines with default arguments + regs->CONFIGOPTS0 = pack_config(&config.dev_config[0]); + regs->CONFIGOPTS1 = pack_config(&config.dev_config[1]); + regs->CONFIGOPTS2 = pack_config(&config.dev_config[2]); + + // Poll status until the device is ready + while (!STATUS_READY(regs->STATUS)) {} +} + +/** + * Poll the status register to see if the RX FIFO has entries + * This is meant to guard against an incorrect driver or hardware + * + * @return 0 upon success, -1 upon timeout + */ +static inline int poll_rx(void) +{ + for (uint16_t timeout = 0; timeout < TIMEOUT_LIMIT; timeout++) { + if (STATUS_RXQD(regs->STATUS)) { + return 0; + } + } + + LOG_DRIVER_ERR("Polling RX timed out\n"); + return -1; +} + +/** + * Transmits len bytes starting from buffer + * + * @param buffer the start of the data + * @param len the len of the data in bytes + */ +void transmit_data(const void *buffer, uint16_t len) +{ + const uint32_t *words = buffer; + uint16_t num_words = len / sizeof(uint32_t); + + for (uint16_t i = 0; i < num_words; i++) { + LOG_DRIVER("Transmitting: %08X\n", words[i]); + regs->TXDATA = words[i]; + } + + // Transmit trailing bytes + const uint8_t *trailing_bytes = buffer + num_words * sizeof(uint32_t); + uint16_t num_trailing_bytes = len % sizeof(uint32_t); + + for (uint16_t i = 0; i < num_trailing_bytes; i++) { + LOG_DRIVER("Transmitting: %02X\n", trailing_bytes[i]); + *((volatile uint8_t *)®s->TXDATA) = trailing_bytes[i]; + } +} + +/** + * Recieves len bytes, and places them starting from buffer + * + * @param buffer the start of where to place the data + * @param len the amount of bytes to recieve + * @return 0 if successful, -1 if timed out while waiting for data + */ +int receive_data(void *buffer, uint16_t len) +{ + uint32_t *words = buffer; + uint16_t num_words = len / sizeof(uint32_t); + + for (uint16_t i = 0; i < num_words; i++) { + if (poll_rx()) { + return -1; + } + + words[i] = regs->RXDATA; + } + + // Retrieve trailing bytes + uint8_t *trailing_bytes = buffer + num_words * sizeof(uint32_t); + uint16_t num_trailing_bytes = len % sizeof(uint32_t); + + if (num_trailing_bytes == 0) { + LOG_DRIVER("returning early\n"); + return 0; + } + LOG_DRIVER("not returning early\n"); + + if (poll_rx()) { + return -1; + } + + uint32_t remaining_data = regs->RXDATA; + + for (uint16_t i = 0; i < num_trailing_bytes; i++) { + trailing_bytes[i] = ((uint8_t *)&remaining_data)[i]; + } + + return 0; +} + +/** + * IDLE: Resets the driver data, and processes the next transaction if available, sleeps otherwise + * + * Succeeds: RESP + * + */ +void state_idle(void) +{ + spi_reset_state(&driver_data); + + if (!spi_cmd_cs_queue_empty(&virt_handle)) { + // Turn on SPI event interrupts, i.e. the device is idling + regs->INTR_ENABLE |= INTR_SPI_EVENT; + + fsm_state.nxt_state = SPI_STATE_GET_CS; + } else { + fsm_state.yield = true; + } +} + +void state_get_cs(void) +{ + assert(spi_dequeue_cmd_cs(&virt_handle, &driver_data.cs)); + driver_data.data_region = config.data[driver_data.cs].vaddr; + + LOG_DRIVER("cs=%u\n", driver_data.cs); + + // Set CS line + regs->CSID = driver_data.cs; + + fsm_state.nxt_state = SPI_STATE_GET_LOGICAL_CMD; +} + +void state_get_logical_cmd(void) +{ + uint16_t cmd_len; + assert(spi_dequeue_cmd(&virt_handle, &driver_data.read_offset, &driver_data.write_offset, &cmd_len, + &driver_data.cs_active_after_cmd)); + driver_data.len = CMD_LEN(cmd_len); + driver_data.logical_progress = 0; + + LOG_DRIVER("read_offset=%lu, write_offset=%lu, len=%u, csaac=%u\n", driver_data.read_offset, + driver_data.write_offset, driver_data.len, driver_data.cs_active_after_cmd); + + fsm_state.nxt_state = SPI_STATE_ISSUE_PHY_CMD; +} + +void state_issue_phy_cmd(void) +{ + driver_data.phy_cmd_len = MIN(driver_data.len - driver_data.logical_progress, COMMAND_LEN_MAX); + if (driver_data.phy_cmd_len == 0) { + if (driver_data.cs_active_after_cmd) { + driver_data.cmd_in_progress++; + fsm_state.nxt_state = SPI_STATE_GET_LOGICAL_CMD; + } else { + driver_data.err = SPI_STATUS_OK; + fsm_state.nxt_state = SPI_STATE_RESP; + } + return; + } + + driver_data.tx_progress = 0; + driver_data.rx_progress = 0; + + uint32_t command = COMMAND_LEN(driver_data.phy_cmd_len); + if (driver_data.read_offset != -1) { + command |= COMMAND_DIRECTION_RX_ONLY; + } + if (driver_data.write_offset != -1) { + command |= COMMAND_DIRECTION_TX_ONLY; + } + bool final_phy_cmd = driver_data.logical_progress + driver_data.phy_cmd_len == driver_data.len; + if (driver_data.cs_active_after_cmd || !final_phy_cmd) { + command |= COMMAND_CSAAT; + } + + LOG_DRIVER("COMMAND=%x\n", command); + + regs->COMMAND = command; + + fsm_state.nxt_state = SPI_STATE_EXEC_PHY_CMD; +} + +void state_exec_phy_cmd(void) +{ + /** + * When simultaneously reading and writing, the FIFO access pattern looks something like this: + * + * TX: ***|***| ... |***|* + * RX: |***| ... |***|***|* + * + * Some of the oddities in the logic (i.e. ternary for determining rx_len, the rw_first and + * rw_last variable, etc.) are to enable the access pattern + */ + bool reading = driver_data.read_offset != -1; + bool writing = driver_data.write_offset != -1; + + const void *tx_buffer = driver_data.data_region + driver_data.write_offset + driver_data.logical_progress + + driver_data.tx_progress; + void *rx_buffer = driver_data.data_region + driver_data.read_offset + driver_data.logical_progress + + driver_data.rx_progress; + uint16_t tx_len = MIN(FIFO_DEPTH, driver_data.phy_cmd_len - driver_data.tx_progress); + uint16_t rx_len = (!writing) ? MIN(FIFO_DEPTH, driver_data.phy_cmd_len - driver_data.rx_progress) + : driver_data.tx_progress - driver_data.rx_progress; + + bool rw_first = driver_data.tx_progress == 0; + bool rw_last = !rw_first && driver_data.rx_progress + rx_len == driver_data.phy_cmd_len; + + if (writing && !(reading && rw_last)) { + transmit_data(tx_buffer, tx_len); + driver_data.tx_progress += tx_len; + } + + if (reading && !(writing && rw_first)) { + if (receive_data(rx_buffer, rx_len)) { + // Re-setup the device to recover + device_setup(); + driver_data.err = SPI_STATUS_ERR_TIMEOUT; + fsm_state.nxt_state = SPI_STATE_RESP; + return; + } + driver_data.rx_progress += rx_len; + } + + fsm_state.yield = true; + fsm_state.nxt_state = SPI_STATE_AWAIT_PHY_CMD; +} + +void state_await_phy_cmd(void) +{ + bool reading = driver_data.read_offset != -1; + bool writing = driver_data.write_offset != -1; + bool read_done = driver_data.rx_progress == driver_data.phy_cmd_len; + bool write_done = driver_data.tx_progress == driver_data.phy_cmd_len; + + bool done; + if (reading && writing) { + done = read_done && write_done; +#ifdef DEBUG_DRIVER + uint32_t status = regs->STATUS; + assert(STATUS_TXQD(status) == 0); + assert(done || STATUS_RXQD(status) > 0); +#endif /* DEBUG_DRIVER */ + } else if (reading) { + done = read_done; +#ifdef DEBUG_DRIVER + assert(done || STATUS_RXQD(regs->STATUS) > 0); +#endif /* DEBUG_DRIVER */ + } else if (writing) { + done = write_done; +#ifdef DEBUG_DRIVER + assert(STATUS_TXQD(regs->STATUS) == 0); +#endif /* DEBUG_DRIVER */ + } else { + done = true; +#ifdef DEBUG_DRIVER + assert(!STATUS_ACTIVE(regs->STATUS)); +#endif /* DEBUG_DRIVER */ + } + + if (done) { + driver_data.logical_progress += driver_data.phy_cmd_len; + fsm_state.nxt_state = SPI_STATE_ISSUE_PHY_CMD; + } else { + fsm_state.nxt_state = SPI_STATE_EXEC_PHY_CMD; + } + + return; +} + +void state_resp(void) +{ + assert(spi_enqueue_resp_cs(&virt_handle, driver_data.cs)); + assert(spi_enqueue_resp(&virt_handle, driver_data.err, driver_data.cmd_in_progress)); + + microkit_notify(config.virt.id); + + // Turn off SPI event interrupts, otherwise the driver will be spammed with TXEMPTY + regs->INTR_ENABLE &= ~INTR_SPI_EVENT; + + fsm_state.nxt_state = SPI_STATE_IDLE; +} + +void fsm(void) +{ + do { + spi_state_t state = fsm_state.nxt_state; + LOG_DRIVER("Entering %s\n", fsm_str(state)); + switch (fsm_state.nxt_state) { + case SPI_STATE_IDLE: + state_idle(); + break; + case SPI_STATE_GET_CS: + state_get_cs(); + break; + case SPI_STATE_GET_LOGICAL_CMD: + state_get_logical_cmd(); + break; + case SPI_STATE_ISSUE_PHY_CMD: + state_issue_phy_cmd(); + break; + case SPI_STATE_EXEC_PHY_CMD: + state_exec_phy_cmd(); + break; + case SPI_STATE_AWAIT_PHY_CMD: + state_await_phy_cmd(); + break; + case SPI_STATE_RESP: + state_resp(); + break; + default: + LOG_DRIVER_ERR("Entered erroneous state, defaulting back to IDLE\n"); + fsm_state.nxt_state = SPI_STATE_IDLE; + fsm_state.yield = false; + } + LOG_DRIVER("Exiting %s\n", fsm_str(state)); + } while (!fsm_state.yield); + + fsm_state.yield = false; + return; +} + +void handle_error(void) +{ + uint32_t error = regs->ERROR_STATUS; + + // Log all errors + if (error & ERROR_ACCESSINVAL) { + LOG_DRIVER_ERR("Zero byte write\n"); + } + if (error & ERROR_CSIDINVAL) { + LOG_DRIVER_ERR("CSID was set incorrectly: 0x%08X\n", regs->CSID); + } + if (error & ERROR_CMDINVAL) { + LOG_DRIVER_ERR("Invalid command\n"); + } + if (error & ERROR_UNDERFLOW) { + LOG_DRIVER_ERR("Attempted to read RX FIFO when empty\n"); + } + if (error & ERROR_OVERFLOW) { + LOG_DRIVER_ERR("Attempted to write to TX FIFO when full\n"); + } + if (error & ERROR_CMDBUSY) { + LOG_DRIVER_ERR("Wrote command while not ready\n"); + } + + if (!(error & ERROR_MASK)) { + LOG_DRIVER_ERR("Recieved error IRQ but no errors\n"); + } else { + // Abort transaction if in progress + switch (fsm_state.nxt_state) { + case SPI_STATE_IDLE: + case SPI_STATE_GET_CS: + LOG_DRIVER_ERR("Going back to IDLE\n"); + fsm_state.nxt_state = SPI_STATE_IDLE; + break; + case SPI_STATE_GET_LOGICAL_CMD: + case SPI_STATE_ISSUE_PHY_CMD: + case SPI_STATE_EXEC_PHY_CMD: + case SPI_STATE_AWAIT_PHY_CMD: + case SPI_STATE_RESP: + LOG_DRIVER_ERR("Going to RESP\n"); + fsm_state.nxt_state = SPI_STATE_RESP; + driver_data.err = SPI_STATUS_ERR_OTHER; + break; + default: + LOG_DRIVER_ERR("Handling error in invalid state, moving to IDLE\n"); + fsm_state.nxt_state = SPI_STATE_IDLE; + break; + } + } + + // Reset the device + device_setup(); +} + +void init(void) +{ + // Check configuration is correct + assert(spi_config_check_magic(&config)); + assert(device_resources_check_magic(&device_resources)); + assert(device_resources.num_irqs == 2); + assert(device_resources.irqs[0].id == ERROR_IRQ); + assert(device_resources.irqs[1].id == EVENT_IRQ); + assert(device_resources.num_regions == 1); + + // Check handle initialization is correct + assert(spi_handle_init(&virt_handle, config.virt.cmd_queue.vaddr, config.virt.resp_queue.vaddr, + config.virt.cmd_cs_queue.vaddr, config.virt.resp_cs_queue.vaddr, + config.virt.queue_capacity_bits)); + + regs = (volatile struct spi_regs *)device_resources.regions[0].region.vaddr; + device_setup(); + + LOG_DRIVER("Driver initialised.\n"); +} + +void notified(microkit_channel ch) +{ + LOG_DRIVER("Notified on channel %d\n", ch); + + if (ch == ERROR_IRQ) { + LOG_DRIVER("Error IRQ recieved: "); + LOG_DRIVER("STATUS=%08X, ERROR=%08X\n", regs->STATUS, regs->ERROR_STATUS); + handle_error(); + fsm(); + microkit_irq_ack(ch); + } else if (ch == EVENT_IRQ) { + LOG_DRIVER("Event IRQ recieved: "); + LOG_DRIVER("STATUS=%08X, ERROR=%08X\n", regs->STATUS, regs->ERROR_STATUS); + fsm(); + microkit_irq_ack(ch); + } else if (ch == config.virt.id) { + fsm(); + } else { + LOG_DRIVER("Supriously notified on channel %u\n", ch); + } +}; diff --git a/drivers/spi/opentitan/spi_driver.mk b/drivers/spi/opentitan/spi_driver.mk new file mode 100644 index 000000000..eed811a7c --- /dev/null +++ b/drivers/spi/opentitan/spi_driver.mk @@ -0,0 +1,27 @@ +# +# Copyright 2024, UNSW +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Include this snippet in your project Makefile to build +# the OpenTitan SPI driver +# +# NOTES: +# Generates spi_driver.elf +# Expects libsddf_util_debug.a in ${LIBS} + +SPI_DIR := $(dir $(lastword $(MAKEFILE_LIST))) + +spi_driver.elf: spi/spi.o + $(LD) $(LDFLAGS) $< $(LIBS) -o $@ + +spi/spi.o: ${SPI_DIR}/spi.c ${SPI_DIR}/driver.h ${CHECK_FLAGS_BOARD_MD5} |spi + ${CC} ${CFLAGS} -o $@ -c $< + +spi: + mkdir -p spi + +clean:: + rm -rf spi +clobber:: + rm -f spi_driver.elf diff --git a/examples/spi/Makefile b/examples/spi/Makefile new file mode 100644 index 000000000..2d025e64e --- /dev/null +++ b/examples/spi/Makefile @@ -0,0 +1,32 @@ +# +# Copyright 2025, UNSW +# +# SPDX-License-Identifier: BSD-2-Clause +# + +ifeq ($(strip $(MICROKIT_SDK)),) +$(error MICROKIT_SDK must be specified) +endif + +ifeq ($(strip $(MICROKIT_BOARD)),) +$(error MICROKIT_BOARD must be specified) +endif +export MICROKIT_BOARD +BUILD_DIR ?= build +export SDDF := $(abspath ../..) +export BUILD_DIR := $(abspath ${BUILD_DIR}) +override MICROKIT_SDK := $(abspath ${MICROKIT_SDK}) + +IMAGE_FILE := $(BUILD_DIR)/loader.img +REPORT_FILE := $(BUILD_DIR)/report.txt + +${IMAGE_FILE} ${REPORT_FILE} clean clobber: $(IMAGE_FILE) ${BUILD_DIR}/Makefile FORCE + ${MAKE} -C ${BUILD_DIR} MICROKIT_SDK=${MICROKIT_SDK} $(notdir $@) + +all: ${IMAGE_FILE} + +${BUILD_DIR}/Makefile: spi.mk + mkdir -p ${BUILD_DIR} + cp spi.mk ${BUILD_DIR}/Makefile + +FORCE: diff --git a/examples/spi/README.md b/examples/spi/README.md new file mode 100644 index 000000000..8d73a2226 --- /dev/null +++ b/examples/spi/README.md @@ -0,0 +1,73 @@ + + +# Timer example + +This is a very simple example where a single client program is setting +timeouts and getting the current time from a timer driver. + +## Building + +The following platforms are supported: +* imx8mm_evk +* imx8mp_evk +* imx8mq_evk +* maaxboard +* odroidc2 +* odroidc4 +* qemu_virt_aarch64 +* qemu_virt_riscv64 +* star64 + +### Make + +```sh +make MICROKIT_SDK= MICROKIT_BOARD= +``` + +After building, the system image to load will be `build/loader.img`. + +If you wish to simulate on the QEMU virt AArch64 platform, you can append `qemu` to your make command +after building for qemu_virt_aarch64. + +### Zig + +You can also build this example with the Zig build system: +```sh +zig build -Dsdk=/path/to/sdk -Dboard= +``` + +The options for `` are the same as the Makefile. + +You can simulate QEMU with: +```sh +zig build -Dsdk=/path/to/sdk -Dboard=qemu_virt_aarch64 qemu +``` + +The final bootable image will be in `zig-out/bin/loader.img`. + +## Running + +When running the example, you should see something similar to the following +output: +``` +CLIENT|INFO: The time now is: 29422640 +CLIENT|INFO: Setting a time out for 1 second +CLIENT|INFO: Got a timeout! +CLIENT|INFO: Now the time (in nanoseconds) is: 1031980992 +CLIENT|INFO: Got a timeout! +CLIENT|INFO: Now the time (in nanoseconds) is: 2032392176 +CLIENT|INFO: Got a timeout! +CLIENT|INFO: Now the time (in nanoseconds) is: 3032782448 +CLIENT|INFO: Got a timeout! +CLIENT|INFO: Now the time (in nanoseconds) is: 4033253600 +CLIENT|INFO: Got a timeout! +CLIENT|INFO: Now the time (in nanoseconds) is: 5033766064 +CLIENT|INFO: Got a timeout! +CLIENT|INFO: Now the time (in nanoseconds) is: 6034408576 +``` + +The client will continuously set timeouts and read the current time. diff --git a/examples/spi/client.c b/examples/spi/client.c new file mode 100644 index 000000000..c0ab84d04 --- /dev/null +++ b/examples/spi/client.c @@ -0,0 +1,108 @@ +/* + * Copyright 2024, UNSW + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include + +__attribute__((__section__(".spi_client_config"))) spi_client_config_t spi_config; + +#define DEBUG_CLIENT +#ifdef DEBUG_CLIENT +#define LOG_CLIENT(...) do{ sddf_dprintf("SPI_CLIENT|INFO: "); sddf_printf(__VA_ARGS__); }while(0) +#else +#define LOG_CLIENT(...) do{}while(0) +#endif +#define LOG_CLIENT_ERR(...) do{ sddf_printf("SPI_CLIENT|ERROR: "); sddf_printf(__VA_ARGS__); }while(0) + +co_control_t libmicrokitco_control; +microkit_cothread_sem_t async_io_semaphore; + +#define STACK_SIZE (4096) +uint8_t client_stack[STACK_SIZE]; +uintptr_t stacks[1]; + +void client_main(void) +{ + libspi_conf_t *conf = (libspi_conf_t *)&spi_config; +#define W25QXX +#ifndef W25QXX + uint8_t *data = spi_config.data.vaddr; + int len = 65; + for (int i = 0; i < len; i++) { + data[i] = i; + } + + spi_status_t err = spi_enqueue_transfer(conf, data + 0x1000, data, len, false); + LOG_CLIENT("err=%s\n", spi_status_str(err)); +// spi_enqueue_read(conf, data + 0x1000, len, false); + spi_notify(conf); + spi_resp_t resp; + spi_read_resp(conf, &resp); + LOG_CLIENT("resp=%s\n", spi_status_str(resp.status)); + for (int i = 0; i < len; i++) { + LOG_CLIENT("read_data[%3d]=%3d\n", i, ((uint8_t *)data + 0x1000)[i]); + } + +#else + w25qxx_conf_t dev_conf = { conf, spi_config.data.vaddr, 0 }; + void *data = dev_conf.data->data; + + w25qxx_reset(&dev_conf); + + uint8_t manufacturer_id; + uint16_t device_id; + w25qxx_get_ids(&dev_conf, &manufacturer_id, &device_id); + LOG_CLIENT("manufacturer_id=%x, device_id=%x\n", manufacturer_id, device_id); + LOG_CLIENT("status_reg_1=%02X\n", w25qxx_get_status_reg_1(&dev_conf)); + + w25qxx_global_unlock(&dev_conf); + w25qxx_erase_block64kb(&dev_conf, 0x23); + + for (uint32_t i = 0; i < 256 * 4; i++) { + ((uint8_t *)data)[i] = i; + } + + for (uint32_t i = 0; i < 4; i++) { + w25qxx_program_page(&dev_conf, i * W25QXX_PG_SZ, data + i * W25QXX_PG_SZ, W25QXX_PG_SZ); + } + + w25qxx_read(&dev_conf, 0x0, data + 0x1000, 4 * W25QXX_PG_SZ); + + for (uint32_t i = 0; i < 4 * W25QXX_PG_SZ / sizeof(uint32_t); i++) { + uint32_t *stuff = ((uint32_t *)(data + 0x1000)); + LOG_CLIENT("%03X: %08X\n", i, stuff[i]); + } + +#endif +} + +void init(void) +{ + LOG_CLIENT("initializing\n"); + + assert(spi_config_check_magic(&spi_config)); + stacks[0] = (uintptr_t)&client_stack; + + // Setup cothreads + microkit_cothread_init(&libmicrokitco_control, STACK_SIZE, stacks); + microkit_cothread_spawn(client_main, NULL); + microkit_cothread_semaphore_init(&async_io_semaphore); + microkit_cothread_yield(); +} + +void notified(microkit_channel ch) +{ + LOG_CLIENT("notified on ch %d\n", ch); + if (ch == spi_config.virt.id) { + microkit_cothread_semaphore_signal(&async_io_semaphore); + } else { + LOG_CLIENT_ERR("Spuriously notified on %d\n", ch); + } +} diff --git a/examples/spi/meta.py b/examples/spi/meta.py new file mode 100644 index 000000000..57f50f694 --- /dev/null +++ b/examples/spi/meta.py @@ -0,0 +1,81 @@ +# Copyright 2025, UNSW +# SPDX-License-Identifier: BSD-2-Clause +import argparse +from typing import List +from dataclasses import dataclass +from sdfgen import SystemDescription, Sddf, DeviceTree +from importlib.metadata import version + +assert version("sdfgen").split(".")[1] == "24", "Unexpected sdfgen version" + +ProtectionDomain = SystemDescription.ProtectionDomain +Irq = SystemDescription.Irq +Map = SystemDescription.Map +MemoryRegion = SystemDescription.MemoryRegion +Channel = SystemDescription.Channel + + +@dataclass +class Board: + name: str + arch: SystemDescription.Arch + paddr_top: int + spi: str + + +BOARDS: List[Board] = [ + Board( + name="cheshire", + arch=SystemDescription.Arch.RISCV64, + paddr_top=0xC0000000, + spi="soc/spi@3004000", + ), +] + + +def generate(sdf_file: str, output_dir: str, dtb: DeviceTree): + spi_driver = ProtectionDomain( + "spi_driver", "spi_driver.elf", priority=3, stack_size=16384 + ) + spi_virt = ProtectionDomain("spi_virt", "spi_virt.elf", priority=2) + spi_client = ProtectionDomain("spi_client", "client.elf", priority=1) + + spi_node = dtb.node(board.spi) + + spi_system = Sddf.Spi(sdf, spi_node, spi_driver, spi_virt) + spi_system.add_client(spi_client, cs=2, freq_div=0x7F, queue_capacity=16) + + pds = [ + spi_client, + spi_virt, + spi_driver, + ] + for pd in pds: + sdf.add_pd(pd) + + assert spi_system.connect() + assert spi_system.serialise_config(output_dir) + + with open(f"{output_dir}/{sdf_file}", "w+") as f: + f.write(sdf.render()) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--dtb", required=True) + parser.add_argument("--sddf", required=True) + parser.add_argument("--board", required=True, choices=[b.name for b in BOARDS]) + parser.add_argument("--output", required=True) + parser.add_argument("--sdf", required=True) + + args = parser.parse_args() + + board = next(filter(lambda b: b.name == args.board, BOARDS)) + + sdf = SystemDescription(board.arch, board.paddr_top) + sddf = Sddf(args.sddf) + + with open(args.dtb, "rb") as f: + dtb = DeviceTree(f.read()) + + generate(args.sdf, args.output, dtb) diff --git a/examples/spi/spi.mk b/examples/spi/spi.mk new file mode 100644 index 000000000..0de140575 --- /dev/null +++ b/examples/spi/spi.mk @@ -0,0 +1,139 @@ +# +# Copyright 2025, UNSW +# +# SPDX-License-Identifier: BSD-2-Clause +# + +ifeq ($(strip $(MICROKIT_SDK)),) +$(error MICROKIT_SDK must be specified) +endif + +ifeq ($(strip $(SDDF)),) +$(error SDDF must be specified) +endif + +ifeq ($(strip $(LIBMICROKITCO_PATH)),) +$(error LIBMICROKITCO_PATH must be specified) +endif + +BUILD_DIR ?= build +# By default we make a debug build so that the client debug prints can be seen. +MICROKIT_CONFIG ?= debug +IMAGE_FILE := loader.img +REPORT_FILE := report.txt + +ifeq ($(strip $(TOOLCHAIN)),) + TOOLCHAIN := clang +endif +ifeq ($(strip $(TOOLCHAIN)), clang) + CC := clang + LD := ld.lld + AR := llvm-ar + RANLIB := llvm-ranlib + OBJCOPY := llvm-objcopy +else + CC := $(TOOLCHAIN)-gcc + LD := $(TOOLCHAIN)-ld + AS := $(TOOLCHAIN)-as + AR := $(TOOLCHAIN)-ar + RANLIB := $(TOOLCHAIN)-ranlib + OBJCOPY := $(TOOLCHAIN)-objcopy +endif +DTC := dtc +PYTHON ?= python3 + +MICROKIT_TOOL ?= $(MICROKIT_SDK)/bin/microkit + +ifeq ($(strip $(MICROKIT_BOARD)), cheshire) + SPI_DRIVER_DIR := opentitan +else +$(error Unsupported MICROKIT_BOARD given) +endif +BOARD := $(MICROKIT_BOARD) +#TODO: this is a workaround to deal with libmicrokitco +CPU := medany +LIBMICROKITCO_INCLUDE_DIR := $(LIBMICROKITCO_PATH) +LIBMICROKITCO_OPTS_DIR := $(SDDF)/include/sddf/spi +LIBMICROKITCO_OPT_PATH := $(LIBMICROKITCO_OPTS_DIR) +LIBMICROKITCO_OBJ := libmicrokitco.a + +LLVM := 1 +TOP:= ${SDDF}/examples/spi +METAPROGRAM := $(TOP)/meta.py +UTIL := $(SDDF)/util +SPI := $(SDDF)/spi +SPI_DRIVER := $(SDDF)/drivers/spi/$(SPI_DRIVER_DIR) +BOARD_DIR := $(MICROKIT_SDK)/board/$(MICROKIT_BOARD)/$(MICROKIT_CONFIG) +SYSTEM_FILE := spi.system +DTS := $(SDDF)/dts/$(MICROKIT_BOARD).dts +DTB := $(MICROKIT_BOARD).dtb +ARCH := ${shell grep 'CONFIG_SEL4_ARCH ' $(BOARD_DIR)/include/kernel/gen_config.h | cut -d' ' -f4} + +IMAGES := spi_driver.elf spi_virt.elf client.elf + +ifeq ($(ARCH),aarch64) + TARGET := aarch64-none-elf + CFLAGS_ARCH := -mcpu=$(CPU) -mstrict-align -target $(TARGET) +else ifeq ($(ARCH),riscv64) + TARGET := riscv64-none-elf + CFLAGS_ARCH := -march=rv64imafdc -target $(TARGET) +endif + +CFLAGS := -nostdlib \ + -ffreestanding \ + -g3 \ + -O3 \ + -Wall -Wno-unused-function -Werror -Wno-unused-command-line-argument \ + -I$(BOARD_DIR)/include \ + -I$(SDDF)/include \ + -I$(SDDF)/include/microkit \ + -I$(LIBMICROKITCO_PATH) \ + -std=gnu23 \ + $(CFLAGS_ARCH) +LDFLAGS := -L$(BOARD_DIR)/lib +LIBS := --start-group -lmicrokit -Tmicrokit.ld libsddf_util_debug.a --end-group + +all: $(IMAGE_FILE) +CHECK_FLAGS_BOARD_MD5:=.board_cflags-$(shell echo -- ${CFLAGS} ${MICROKIT_SDK} ${MICROKIT_BOARD} ${MICROKIT_CONFIG} | shasum | sed 's/ *-//') + +${CHECK_FLAGS_BOARD_MD5}: + -rm -f .board_cflags-* + touch $@ + +export LIBMICROKITCO_PATH LIBMICROKITCO_OPT_PATH TARGET MICROKIT_SDK BUILD_DIR MICROKIT_BOARD MICROKIT_CONFIG CPU LLVM + +libmicrokitco/$(LIBMICROKITCO_OBJ): + make -f $(LIBMICROKITCO_PATH)/Makefile + +include ${SPI_DRIVER}/spi_driver.mk +include ${SDDF}/util/util.mk +include ${SDDF}/spi/components/spi_virt.mk +include ${SPI}/libspi.mk +include ${SDDF}/spi/devices/w25qxx/w25qxx.mk + +${IMAGES}: libsddf_util_debug.a + +# TODO: relocate libmicrokitco_opts.h, as it is currently stored in include/sddf/spi and there is an +# additional include path specified here +client.o: ${TOP}/client.c + $(CC) -c -I$(SDDF)/include/sddf/spi $(CFLAGS) $< -o client.o +client.elf: client.o libspi.a libmicrokitco/$(LIBMICROKITCO_OBJ) w25qxx.o + $(LD) $(LDFLAGS) $^ $(LIBS) -o $@ + +$(DTB): $(DTS) + dtc -q -I dts -O dtb $(DTS) > $(DTB) + +$(SYSTEM_FILE): $(METAPROGRAM) $(IMAGES) $(DTB) spi_virt.elf client.elf spi_driver.elf + $(PYTHON) $(METAPROGRAM) --sddf $(SDDF) --board $(MICROKIT_BOARD) --output . --sdf $(SYSTEM_FILE) --dtb $(DTB) + $(OBJCOPY) --update-section .spi_virt_config=spi_virt.data spi_virt.elf + $(OBJCOPY) --update-section .spi_client_config=spi_client_spi_client.data client.elf + $(OBJCOPY) --update-section .spi_driver_config=spi_driver.data spi_driver.elf + $(OBJCOPY) --update-section .device_resources=spi_driver_device_resources.data spi_driver.elf + +$(IMAGE_FILE) $(REPORT_FILE): $(SYSTEM_FILE) + $(MICROKIT_TOOL) $(SYSTEM_FILE) --search-path $(BUILD_DIR) --board $(MICROKIT_BOARD) --config $(MICROKIT_CONFIG) -o $(IMAGE_FILE) -r $(REPORT_FILE) + +clean:: + rm -f *.o *.elf +clobber:: clean + rm -f client.elf ${IMAGE_FILE} ${REPORT_FILE} diff --git a/include/sddf/spi/config.h b/include/sddf/spi/config.h new file mode 100644 index 000000000..9414b649a --- /dev/null +++ b/include/sddf/spi/config.h @@ -0,0 +1,85 @@ +/* + * Copyright 2025, UNSW + * + * SPDX-License-Identifier: BSD-2-Clause + */ +#pragma once + +#include +#include +#include +#include +#include + +#define SDDF_SPI_MAX_CLIENTS (64) +#define SPI_MAX_CS_LINES SDDF_SPI_MAX_CLIENTS + +#define SDDF_SPI_MAGIC_LEN 5 +static char SDDF_SPI_MAGIC[SDDF_SPI_MAGIC_LEN] = { 's', 'D', 'D', 'F', 0x9 }; + +// zig: DeviceConfig +typedef struct spi_device_config { + bool cpha; + bool cpol; + uint64_t freq_div; +} spi_device_config_t; + +// zig: Client.Connection +typedef struct spi_client_connection_resource { + region_resource_t cmd_queue; + region_resource_t resp_queue; + uint8_t id; + uint8_t queue_capacity_bits; +} spi_client_connection_resource_t; + +// zig: Client +typedef struct spi_client_config { + char magic[SDDF_SPI_MAGIC_LEN]; + spi_client_connection_resource_t virt; + region_resource_t data; +} spi_client_config_t; + +// zig: Driver.Connection +typedef struct spi_driver_connection_resource { + region_resource_t cmd_queue; + region_resource_t resp_queue; + region_resource_t cmd_cs_queue; + region_resource_t resp_cs_queue; + uint8_t id; + uint8_t queue_capacity_bits; +} spi_driver_connection_resource_t; + +// zig: Driver +typedef struct spi_driver_config { + char magic[SDDF_SPI_MAGIC_LEN]; + spi_driver_connection_resource_t virt; + region_resource_t data[SPI_MAX_CS_LINES]; + spi_device_config_t dev_config[SPI_MAX_CS_LINES]; +} spi_driver_config_t; + +// zig: Virt.Client +typedef struct spi_virt_config_client { + spi_client_connection_resource_t conn; + uint64_t data_size; + uint8_t cs; +} spi_virt_config_client_t; + +// zig: Virt +typedef struct spi_virt_config { + char magic[SDDF_SPI_MAGIC_LEN]; + uint8_t num_clients; + spi_virt_config_client_t clients[SDDF_SPI_MAX_CLIENTS]; + spi_driver_connection_resource_t driver; +} spi_virt_config_t; + +static bool spi_config_check_magic(void *config) +{ + char *magic = (char *)config; + for (int i = 0; i < SDDF_SPI_MAGIC_LEN; i++) { + if (magic[i] != SDDF_SPI_MAGIC[i]) { + return false; + } + } + + return true; +} diff --git a/include/sddf/spi/devices/w25qxx.h b/include/sddf/spi/devices/w25qxx.h new file mode 100644 index 000000000..3cd804f3d --- /dev/null +++ b/include/sddf/spi/devices/w25qxx.h @@ -0,0 +1,60 @@ +/* + * Copyright 2025, UNSW + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +#define DEBUG_W25QXX +#ifdef DEBUG_W25QXX +#define LOG_W25QXX(...) do{ sddf_dprintf("W25QXX|INFO: "); sddf_printf(__VA_ARGS__); }while(0) +#else +#define LOG_W25QXX(...) do{}while(0) +#endif +#define LOG_W25QXX_ERR(...) do{ sddf_printf("W25QXX|ERROR: "); sddf_printf(__VA_ARGS__); }while(0) + +typedef struct w25qxx_cmd { + uint8_t inst; + uint8_t addr[3]; +} w25qxx_cmd_t; + +typedef struct w25qxx_layout { + w25qxx_cmd_t cmd[3]; + uint32_t internal_data[1]; + uint32_t data[]; +} w25qxx_layout_t; + +typedef struct w25qxx_conf { + libspi_conf_t *libspi_conf; + w25qxx_layout_t *data; + uint8_t cmd_idx; +} w25qxx_conf_t; + +typedef enum w25qxx_inst { + W25QXX_INST_WRITE_ENABLE = 0x06, + W25QXX_INST_JEDEC_ID = 0x9F, + W25QXX_INST_READ_DATA = 0x03, + W25QXX_INST_PAGE_PROGRAM = 0x02, + W25QXX_INST_BLOCK_ERASE_64KB = 0xD8, + W25QXX_INST_CHIP_ERASE = 0xC7, + W25QXX_INST_READ_STATUS_REGISTER_1 = 0x05, + W25QXX_INST_READ_STATUS_REGISTER_2 = 0x35, + W25QXX_INST_READ_STATUS_REGISTER_3 = 0x15, + W25QXX_INST_GLOBAL_BLOCK_SECTOR_UNLOCK = 0x98, + W25QXX_INST_ENABLE_RESET = 0x66, + W25QXX_INST_RESET_DEVICE = 0x99, +} w25qxx_inst_t; + +#define W25QXX_PG_SZ (256) +#define W25QXX_STATUS_BUSY(status) ((status) & BIT(0)) + +int w25qxx_reset(w25qxx_conf_t *conf); +int w25qxx_get_ids(w25qxx_conf_t *conf, uint8_t *manufacturer_id, uint16_t *device_id); +int w25qxx_read(w25qxx_conf_t *conf, uint32_t addr, void *buffer, uint16_t len); +int w25qxx_write_en(w25qxx_conf_t *conf); +int w25qxx_program_page(w25qxx_conf_t *conf, uint32_t addr, void *buffer, uint16_t len); +uint8_t w25qxx_get_status_reg_1(w25qxx_conf_t *conf); +void w25qxx_erase_chip(w25qxx_conf_t *conf); +void w25qxx_erase_block64kb(w25qxx_conf_t *conf, uint32_t addr); +void w25qxx_global_unlock(w25qxx_conf_t *conf); diff --git a/include/sddf/spi/libmicrokitco_opts.h b/include/sddf/spi/libmicrokitco_opts.h new file mode 100644 index 000000000..e7c11f603 --- /dev/null +++ b/include/sddf/spi/libmicrokitco_opts.h @@ -0,0 +1,7 @@ +/* + * Copyright 2025, UNSW + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#define LIBMICROKITCO_MAX_COTHREADS 2 diff --git a/include/sddf/spi/libspi.h b/include/sddf/spi/libspi.h new file mode 100644 index 000000000..d16185a63 --- /dev/null +++ b/include/sddf/spi/libspi.h @@ -0,0 +1,38 @@ +/* + * Copyright 2025, UNSW + * + * SPDX-License-Identifier: BSD-2-Clause + */ +// SPI interface library for clients. +// Provides helper functions for creating transactions and handing them to the virtualiser. +// +// See spi/queue.h for details about the SPI transport layer. + +// WARNING:: the event cothread is assumed to be available whenever a blocking function is called! +// Be aware of possible danger if your client performs complex multitasking. + +#pragma once +#include +#include +#include +#include +#include + +#ifdef DEBUG_LIBSPI +#define LOG_LIBSPI(...) do{ sddf_dprintf("CLIENT|INFO: "); sddf_printf(__VA_ARGS__); }while(0) +#else +#define LOG_LIBSPI(...) do{}while(0) +#endif +#define LOG_LIBSPI_ERR(...) do{ sddf_printf("LIBSPI|ERROR: "); sddf_printf(__VA_ARGS__); }while(0) + +extern microkit_cothread_sem_t async_io_semaphore; + +typedef spi_client_config_t libspi_conf_t; + +spi_status_t spi_enqueue_write(libspi_conf_t *conf, void *write_buf, uint16_t len, bool cs_active_after_cmd); +spi_status_t spi_enqueue_read(libspi_conf_t *conf, void *read_buf, uint16_t len, bool cs_active_after_cmd); +spi_status_t spi_enqueue_transfer(libspi_conf_t *conf, void *read_buf, void *write_buf, uint16_t len, + bool cs_active_after_cmd); +spi_status_t spi_enqueue_dummy(libspi_conf_t *conf, uint16_t len, bool cs_active_after_cmd); +spi_status_t spi_notify(libspi_conf_t *conf); +spi_status_t spi_read_resp(libspi_conf_t *conf, spi_resp_t *resp); diff --git a/include/sddf/spi/queue.h b/include/sddf/spi/queue.h new file mode 100644 index 000000000..c798a88a0 --- /dev/null +++ b/include/sddf/spi/queue.h @@ -0,0 +1,602 @@ +/* + * Copyright 2025, UNSW + * + * SPDX-License-Identifier: BSD-2-Clause + */ +#pragma once + +#include +#include +#include +#include +#include + +typedef enum spi_status { + SPI_STATUS_OK = 0, + SPI_STATUS_ERR_CMD_OOB = -1, + SPI_STATUS_ERR_CMD_UNALIGNED_OFFSET = -2, + SPI_STATUS_ERR_CMD_PARTIAL_OVERLAP = -3, + SPI_STATUS_ERR_PARTIAL_TXN = -4, + SPI_STATUS_ERR_TIMEOUT = -5, + SPI_STATUS_ERR_CMD_QUEUE_FULL = -6, + SPI_STATUS_ERR_RESP_QUEUE_EMPTY = -7, + SPI_STATUS_ERR_OTHER = -256, // can be used for driver specific implementations +} spi_status_t; + +static inline char *spi_status_str(spi_status_t status) +{ + switch (status) { + case SPI_STATUS_OK: + return "SPI_STATUS_OK"; + case SPI_STATUS_ERR_CMD_OOB: + return "SPI_STATUS_ERR_CMD_OOB"; + case SPI_STATUS_ERR_CMD_UNALIGNED_OFFSET: + return "SPI_STATUS_ERR_CMD_UNALIGNED_OFFSET"; + case SPI_STATUS_ERR_CMD_PARTIAL_OVERLAP: + return "SPI_STATUS_ERR_CMD_PARTIAL_OVERLAP"; + case SPI_STATUS_ERR_PARTIAL_TXN: + return "SPI_STATUS_ERR_PARTIAL_TXN"; + case SPI_STATUS_ERR_TIMEOUT: + return "SPI_STATUS_ERR_TIMEOUT"; + case SPI_STATUS_ERR_OTHER: + return "SPI_STATUS_ERR_OTHER"; + default: + return "NOT_AN_SPI_STATUS"; + } +} + +// As a command length of 0 does not make sense, it should be interpreted as the maximum size +// representable in that type plus 1 +// i.e. shifting the domain of a uint8_t from [0, 65535] to [1, 65536] +#define CMD_LEN(len) (len ? len : UINT16_MAX + 1) + +// The two regions, [read_offset, read_offset + len] and [write_offset, write_offset + len] +// should overlap completely or be disjoint +typedef struct spi_cmd { + size_t read_offset; + size_t write_offset; + uint16_t len; + bool cs_active_after_cmd; +} spi_cmd_t; + +typedef struct spi_resp { + spi_status_t status; + uint16_t err_cmd_idx; +} spi_resp_t; + +typedef uint8_t spi_cs_t; + +typedef struct spi_cmd_queue { + uint32_t head; + uint32_t tail; + spi_cmd_t element[]; +} spi_cmd_queue_t; + +typedef struct spi_resp_queue { + uint32_t head; + uint32_t tail; + spi_resp_t element[]; +} spi_resp_queue_t; + +typedef struct spi_cmd_cs_queue { + uint32_t head; + uint32_t tail; + spi_cs_t element[]; +} spi_cmd_cs_queue_t; + +typedef struct spi_resp_cs_queue { + uint32_t head; + uint32_t tail; + spi_cs_t element[]; +} spi_resp_cs_queue_t; + +typedef struct spi_handle { + spi_cmd_queue_t *spi_cmd_queue; + spi_resp_queue_t *spi_resp_queue; + spi_cmd_cs_queue_t *spi_cmd_cs_queue; + spi_resp_cs_queue_t *spi_resp_cs_queue; + uint8_t queue_capacity_bits; +} spi_handle_t; + +/** + * Initialise the shared queues + * + * @param handle queue handle to use + * @param spi_cmd_queue pointer to cmd queue in shared memory + * @param spi_resp_queue pointer to resp queue in shared memory + * @param spi_cmd_cs_queue pointer to cmd_cs queue in shared memory + * @param spi_resp_cs_queue pointer to resp_cs queue in shared memory + * @param queue_capacity bits the queue capacity should be equal to 1 << queue_capacity_bits + * + * @return whether queue_capacity_bits is longer than the width of the queue index or not + */ +static inline bool spi_handle_init(spi_handle_t *handle, spi_cmd_queue_t *spi_cmd_queue, + spi_resp_queue_t *spi_resp_queue, spi_cmd_cs_queue_t *spi_cmd_cs_queue, + spi_resp_cs_queue_t *spi_resp_cs_queue, uint8_t queue_capacity_bits) +{ + if (queue_capacity_bits >= UINT32_WIDTH) { + return false; + } + + handle->spi_cmd_queue = spi_cmd_queue; + handle->spi_resp_queue = spi_resp_queue; + handle->spi_cmd_cs_queue = spi_cmd_cs_queue; + handle->spi_resp_cs_queue = spi_resp_cs_queue; + handle->queue_capacity_bits = queue_capacity_bits; + + return true; +} + +/** + * Check if the cmd queue is empty + * + * @param handle the queue handle to use + * + * @return whether the cmd queue is empty or not + */ +static inline bool spi_cmd_queue_empty(spi_handle_t *handle) +{ + spi_cmd_queue_t *queue = handle->spi_cmd_queue; + return queue->head == queue->tail; +} + +/** + * Check if the cmd queue is full + * + * @param handle the queue handle to use + * + * @return whether the cmd queue is full or not + */ +static inline bool spi_cmd_queue_full(spi_handle_t *handle) +{ + spi_cmd_queue_t *queue = handle->spi_cmd_queue; + return queue->tail - queue->head == 1 << handle->queue_capacity_bits; +} + +/** + * Get the number of elements in the cmd queue + * + * @param handle the queue handle to use + * + * @return number of elements in the queue + */ +static inline uint32_t spi_cmd_queue_len(spi_handle_t *handle) +{ + spi_cmd_queue_t *queue = handle->spi_cmd_queue; + return queue->tail - queue->head; +} + +/** + * Enqueue an element into the cmd queue + * + * @param handle the queue handle to use + * @param read_offset + * @param write_offset + * @param len + * @param cs_active_after_cmd + * + * @return if the enqueue was successful + */ +static inline bool spi_enqueue_cmd(spi_handle_t *handle, size_t read_offset, size_t write_offset, uint16_t len, + bool cs_active_after_cmd) +{ + if (spi_cmd_queue_full(handle)) { + return false; + } + + spi_cmd_queue_t *queue = handle->spi_cmd_queue; + uint32_t index = queue->tail % (1 << handle->queue_capacity_bits); + queue->element[index].read_offset = read_offset; + queue->element[index].write_offset = write_offset; + queue->element[index].len = len; + queue->element[index].cs_active_after_cmd = cs_active_after_cmd; + + THREAD_MEMORY_RELEASE(); + queue->tail++; + + return true; +} + +/** + * Dequeue an element from the cmd queue + * + * @param handle the queue handle to use + * @param read_offset + * @param write_offset + * @param len + * @param cs_active_after_cmd + * + * @return if the dequeue was successful + */ +static inline bool spi_dequeue_cmd(spi_handle_t *handle, size_t *read_offset, size_t *write_offset, uint16_t *len, + bool *cs_active_after_cmd) +{ + if (spi_cmd_queue_empty(handle)) { + return false; + } + + spi_cmd_queue_t *queue = handle->spi_cmd_queue; + uint32_t index = queue->head % (1 << handle->queue_capacity_bits); + *read_offset = queue->element[index].read_offset; + *write_offset = queue->element[index].write_offset; + *len = queue->element[index].len; + *cs_active_after_cmd = queue->element[index].cs_active_after_cmd; + + THREAD_MEMORY_RELEASE(); + queue->head++; + + return true; +} + +/** + * Enqueue len elements into the cmd queue + * + * @param handle the queue handle to use + * @param element pointer to a contiguous array of spi_cmd_t + * @param len the amount of elements to enqueue + * + * @return if the enqueue was successful + */ +static inline bool spi_mass_enqueue_cmd(spi_handle_t *handle, spi_cmd_t *element, uint32_t len) +{ + if (len + spi_cmd_queue_len(handle) > 1 << handle->queue_capacity_bits) { + return false; + } + + spi_cmd_queue_t *queue = handle->spi_cmd_queue; + for (uint32_t i = 0; i < len; i++) { + uint32_t index = (queue->tail + i) % (1 << handle->queue_capacity_bits); + sddf_memcpy(&queue->element[index], &element[i], sizeof(spi_cmd_t)); + } + + THREAD_MEMORY_RELEASE(); + queue->tail += len; + + return true; +} + +/** + * Check if the resp queue is empty + * + * @param handle the queue handle to use + * + * @return whether the resp queue is empty or not + */ +static inline bool spi_resp_queue_empty(spi_handle_t *handle) +{ + spi_resp_queue_t *queue = handle->spi_resp_queue; + return queue->head == queue->tail; +} + +/** + * Check if the resp queue is full + * + * @param handle the queue handle to use + * + * @return whether the resp queue is full or not + */ +static inline bool spi_resp_queue_full(spi_handle_t *handle) +{ + spi_resp_queue_t *queue = handle->spi_resp_queue; + return queue->tail - queue->head == 1 << handle->queue_capacity_bits; +} + +/** + * Get the number of elements in the resp queue + * + * @param handle the queue handle to use + * + * @return number of elements in the queue + */ +static inline uint32_t spi_resp_queue_len(spi_handle_t *handle) +{ + spi_resp_queue_t *queue = handle->spi_resp_queue; + return queue->tail - queue->head; +} + +/** + * Enqueue an element into the resp queue + * + * @param handle the queue handle to use + * @param status + * @param err_cmd_idx + * + * @return if the enqueue was successful + */ +static inline bool spi_enqueue_resp(spi_handle_t *handle, spi_status_t status, uint16_t err_cmd_idx) +{ + if (spi_resp_queue_full(handle)) { + return false; + } + + spi_resp_queue_t *queue = handle->spi_resp_queue; + uint32_t index = queue->tail % (1 << handle->queue_capacity_bits); + queue->element[index].status = status; + queue->element[index].err_cmd_idx = err_cmd_idx; + + THREAD_MEMORY_RELEASE(); + queue->tail++; + + return true; +} + +/** + * Dequeue an element from the resp queue + * + * @param handle the queue handle to use + * @param status + * @param err_cmd_idx + * + * @return if the dequeue was successful + */ +static inline bool spi_dequeue_resp(spi_handle_t *handle, spi_status_t *status, uint16_t *err_cmd_idx) +{ + if (spi_resp_queue_empty(handle)) { + return false; + } + + spi_resp_queue_t *queue = handle->spi_resp_queue; + uint32_t index = queue->head % (1 << handle->queue_capacity_bits); + *status = queue->element[index].status; + *err_cmd_idx = queue->element[index].err_cmd_idx; + + THREAD_MEMORY_RELEASE(); + queue->head++; + + return true; +} + +/** + * Enqueue len elements into the resp queue + * + * @param handle the queue handle to use + * @param element pointer to a contiguous array of spi_resp_t + * @param len the amount of elements to enqueue + * + * @return if the enqueue was successful + */ +static inline bool spi_mass_enqueue_resp(spi_handle_t *handle, spi_resp_t *element, uint32_t len) +{ + if (len + spi_resp_queue_len(handle) > 1 << handle->queue_capacity_bits) { + return false; + } + + spi_resp_queue_t *queue = handle->spi_resp_queue; + for (uint32_t i = 0; i < len; i++) { + uint32_t index = (queue->tail + i) % (1 << handle->queue_capacity_bits); + sddf_memcpy(&queue->element[index], &element[i], sizeof(spi_resp_t)); + } + + THREAD_MEMORY_RELEASE(); + queue->tail += len; + + return true; +} + +/** + * Check if the cmd_cs queue is empty + * + * @param handle the queue handle to use + * + * @return whether the cmd_cs queue is empty or not + */ +static inline bool spi_cmd_cs_queue_empty(spi_handle_t *handle) +{ + spi_cmd_cs_queue_t *queue = handle->spi_cmd_cs_queue; + return queue->head == queue->tail; +} + +/** + * Check if the cmd_cs queue is full + * + * @param handle the queue handle to use + * + * @return whether the cmd_cs queue is full or not + */ +static inline bool spi_cmd_cs_queue_full(spi_handle_t *handle) +{ + spi_cmd_cs_queue_t *queue = handle->spi_cmd_cs_queue; + return queue->tail - queue->head == 1 << handle->queue_capacity_bits; +} + +/** + * Get the number of elements in the cmd_cs queue + * + * @param handle the queue handle to use + * + * @return number of elements in the queue + */ +static inline uint32_t spi_cmd_cs_queue_len(spi_handle_t *handle) +{ + spi_cmd_cs_queue_t *queue = handle->spi_cmd_cs_queue; + return queue->tail - queue->head; +} + +/** + * Enqueue an element into the cmd_cs queue + * + * @param handle the queue handle to use + * @param cs + * + * @return if the enqueue was successful + */ +static inline bool spi_enqueue_cmd_cs(spi_handle_t *handle, spi_cs_t cs) +{ + if (spi_cmd_cs_queue_full(handle)) { + return false; + } + + spi_cmd_cs_queue_t *queue = handle->spi_cmd_cs_queue; + uint32_t index = queue->tail % (1 << handle->queue_capacity_bits); + queue->element[index] = cs; + + THREAD_MEMORY_RELEASE(); + queue->tail++; + + return true; +} + +/** + * Dequeue an element from the cmd_cs queue + * + * @param handle the queue handle to use + * @param cs + * + * @return if the dequeue was successful + */ +static inline bool spi_dequeue_cmd_cs(spi_handle_t *handle, spi_cs_t *cs) +{ + if (spi_cmd_cs_queue_empty(handle)) { + return false; + } + + spi_cmd_cs_queue_t *queue = handle->spi_cmd_cs_queue; + uint32_t index = queue->head % (1 << handle->queue_capacity_bits); + *cs = queue->element[index]; + + THREAD_MEMORY_RELEASE(); + queue->head++; + + return true; +} + +/** + * Enqueue len elements into the cmd_cs queue + * + * @param handle the queue handle to use + * @param element pointer to a contiguous array of spi_cmd_cs_t + * @param len the amount of elements to enqueue + * + * @return if the enqueue was successful + */ +static inline bool spi_mass_enqueue_cmd_cs(spi_handle_t *handle, spi_cs_t *element, uint32_t len) +{ + if (len + spi_cmd_cs_queue_len(handle) > 1 << handle->queue_capacity_bits) { + return false; + } + + spi_cmd_cs_queue_t *queue = handle->spi_cmd_cs_queue; + for (uint32_t i = 0; i < len; i++) { + uint32_t index = (queue->tail + i) % (1 << handle->queue_capacity_bits); + sddf_memcpy(&queue->element[index], &element[i], sizeof(spi_cs_t)); + } + + THREAD_MEMORY_RELEASE(); + queue->tail += len; + + return true; +} + +/** + * Check if the resp_cs queue is empty + * + * @param handle the queue handle to use + * + * @return whether the resp_cs queue is empty or not + */ +static inline bool spi_resp_cs_queue_empty(spi_handle_t *handle) +{ + spi_resp_cs_queue_t *queue = handle->spi_resp_cs_queue; + return queue->head == queue->tail; +} + +/** + * Check if the resp_cs queue is full + * + * @param handle the queue handle to use + * + * @return whether the resp_cs queue is full or not + */ +static inline bool spi_resp_cs_queue_full(spi_handle_t *handle) +{ + spi_resp_cs_queue_t *queue = handle->spi_resp_cs_queue; + return queue->tail - queue->head == 1 << handle->queue_capacity_bits; +} + +/** + * Get the number of elements in the resp_cs queue + * + * @param handle the queue handle to use + * + * @return number of elements in the queue + */ +static inline uint32_t spi_resp_cs_queue_len(spi_handle_t *handle) +{ + spi_resp_cs_queue_t *queue = handle->spi_resp_cs_queue; + return queue->tail - queue->head; +} + +/** + * Enqueue an element into the resp_cs queue + * + * @param handle the queue handle to use + * @param cs + * + * @return if the enqueue was successful + */ +static inline bool spi_enqueue_resp_cs(spi_handle_t *handle, spi_cs_t cs) +{ + if (spi_resp_cs_queue_full(handle)) { + return false; + } + + spi_resp_cs_queue_t *queue = handle->spi_resp_cs_queue; + uint32_t index = queue->tail % (1 << handle->queue_capacity_bits); + queue->element[index] = cs; + + THREAD_MEMORY_RELEASE(); + queue->tail++; + + return true; +} + +/** + * Dequeue an element from the resp_cs queue + * + * @param handle the queue handle to use + * @param cs + * + * @return if the dequeue was successful + */ +static inline bool spi_dequeue_resp_cs(spi_handle_t *handle, spi_cs_t *cs) +{ + if (spi_resp_cs_queue_empty(handle)) { + return false; + } + + spi_resp_cs_queue_t *queue = handle->spi_resp_cs_queue; + uint32_t index = queue->head % (1 << handle->queue_capacity_bits); + *cs = queue->element[index]; + + THREAD_MEMORY_RELEASE(); + queue->head++; + + return true; +} + +/** + * Enqueue len elements into the resp_cs queue + * + * @param handle the queue handle to use + * @param element pointer to a contiguous array of spi_resp_cs_t + * @param len the amount of elements to enqueue + * + * @return if the enqueue was successful + */ +static inline bool spi_mass_enqueue_resp_cs(spi_handle_t *handle, spi_cs_t *element, uint32_t len) +{ + if (len + spi_resp_cs_queue_len(handle) > 1 << handle->queue_capacity_bits) { + return false; + } + + spi_resp_cs_queue_t *queue = handle->spi_resp_cs_queue; + for (uint32_t i = 0; i < len; i++) { + uint32_t index = (queue->tail + i) % (1 << handle->queue_capacity_bits); + sddf_memcpy(&queue->element[index], &element[i], sizeof(spi_cs_t)); + } + + THREAD_MEMORY_RELEASE(); + queue->tail += len; + + return true; +} diff --git a/include/sddf/util/util.h b/include/sddf/util/util.h index 2c0c612b2..d2eb6f5e8 100644 --- a/include/sddf/util/util.h +++ b/include/sddf/util/util.h @@ -10,7 +10,9 @@ #ifndef ARRAY_SIZE #define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0])) #endif -#define ALIGN(x, align) (((x) + (align) - 1) & ~((align) - 1)) + +#define __ALIGN_MASK(x, mask) (((x) + (mask)) & ~(mask)) +#define ALIGN(x, align) ((typeof(x)) __ALIGN_MASK(((typeof(align)) (x)), ((align) - 1))) #define BIT(nr) (1UL << (nr)) diff --git a/spi/components/spi_virt.mk b/spi/components/spi_virt.mk new file mode 100644 index 000000000..0469c685e --- /dev/null +++ b/spi/components/spi_virt.mk @@ -0,0 +1,17 @@ +# +# Copyright 2024, UNSW +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Include this snippet in your project Makefile to build +# the spi virtualiser +# +# NOTES: +# Builds spi_virt.elf +# Depends on ${SDDF}/util/util.mk also being included + +spi_virt.o: ${SDDF}/spi/components/virt.c + ${CC} ${CFLAGS} -c -o $@ $< + +spi_virt.elf: spi_virt.o libsddf_util_debug.a + $(LD) $(LDFLAGS) $^ $(LIBS) -o $@ diff --git a/spi/components/virt.c b/spi/components/virt.c new file mode 100644 index 000000000..6cf60852a --- /dev/null +++ b/spi/components/virt.c @@ -0,0 +1,219 @@ +/* + * Copyright 2024, UNSW + * + * SPDX-License-Identifier: BSD-2-Clause + */ +#include +#include +#include +#include +#include + +#define DEBUG_VIRT +#ifdef DEBUG_VIRT +#define LOG_VIRT(...) do{ sddf_dprintf("SPI VIRT|INFO: "); sddf_dprintf(__VA_ARGS__); }while(0) +#else +#define LOG_VIRT(...) do{}while(0) +#endif + +#define LOG_VIRT_ERR(...) do{ sddf_printf("SPI VIRT|ERROR: "); sddf_printf(__VA_ARGS__); }while(0) + +#define CLIENT_CH_OFFSET (1) + +uint64_t cs_to_client_id[SPI_MAX_CS_LINES]; + +spi_handle_t driver_handle; +spi_handle_t client_handles[SDDF_SPI_MAX_CLIENTS]; + +//TODO +spi_cmd_t tx[1 << 12]; +uint32_t tx_len; +bool tx_cached; +spi_cs_t tx_cs; + +__attribute__((__section__(".spi_virt_config"))) spi_virt_config_t config; + +static inline void log_enqueue_resp_failure(spi_status_t status, uint8_t err_cmd_idx, uint64_t client_id, spi_cs_t cs) +{ + LOG_VIRT("Failed to enqueue response (status=%s, err_cmd_idx=%u) into client %lu's CS %u queue\n", + spi_status_str(status), err_cmd_idx, client_id, cs); +} + +spi_status_t validate_cmd(spi_cmd_t *cmd, uint64_t data_region_size) +{ + size_t read_offset = cmd->read_offset; + size_t write_offset = cmd->read_offset; + uint32_t len = CMD_LEN(cmd->len); + LOG_VIRT("CMD_LEN(len)=%u\n", len); + LOG_VIRT("data_size=%lu\n", data_region_size); + + bool reading = read_offset != -1; + bool writing = write_offset != -1; + size_t read_end = read_offset + len; + size_t write_end = write_offset + len; + + bool read_unaligned = reading && read_offset & sizeof(uint32_t) - 1; + bool write_unaligned = writing && write_offset & sizeof(uint32_t) - 1; + if (read_unaligned || write_unaligned) { + return SPI_STATUS_ERR_CMD_UNALIGNED_OFFSET; + } + + bool read_oob = reading && read_end >= data_region_size; + bool write_oob = writing && write_end >= data_region_size; + if (read_oob || write_oob) { + return SPI_STATUS_ERR_CMD_OOB; + } + + bool partial_overlap = reading && writing + && ((write_offset < read_offset && read_offset < write_end) + || (read_offset < write_offset && write_offset < read_end)); + if (partial_overlap) { + return SPI_STATUS_ERR_CMD_PARTIAL_OVERLAP; + } + + return SPI_STATUS_OK; +} + +spi_resp_t find_next_tx(spi_handle_t *handle, uint64_t data_region_size) +{ + spi_resp_t resp = (spi_resp_t) { SPI_STATUS_OK, -1 }; + + uint32_t queue_len = spi_cmd_queue_len(handle); + for (tx_len = 0; tx_len < queue_len; tx_len++) { + spi_cmd_t *cmd = &tx[tx_len]; + // TODO: remove assert since clients can cause this to fail in a multicore scenario + assert(spi_dequeue_cmd(handle, &cmd->read_offset, &cmd->write_offset, &cmd->len, &cmd->cs_active_after_cmd)); + + LOG_VIRT("cmd %d: read_offset=%lu, write_offset=%lu, len=%u, csaac=%b\n", tx_len, cmd->read_offset, + cmd->write_offset, cmd->len, cmd->cs_active_after_cmd); + + // Only validate the command if no other errors were encountered + if (resp.status == SPI_STATUS_OK) { + resp = (spi_resp_t) { validate_cmd(cmd, data_region_size), tx_len }; + } + + // Only return early if the TX has terminated + if (!cmd->cs_active_after_cmd) { + tx_len++; + return resp; + } + } + + // Should only be reachable if TX has not been terminated, or queue is empty + return (spi_resp_t) { SPI_STATUS_ERR_PARTIAL_TXN, -1 }; +} + +void process_request(uint32_t client_id) +{ + // TODO: is this even logical? + if (tx_cached) { + // The driver's request queue should be empty + assert(spi_mass_enqueue_cmd(&driver_handle, tx, tx_len)); + assert(spi_enqueue_cmd_cs(&driver_handle, tx_cs)); + } + tx_cached = false; + + spi_handle_t *client_handle = &client_handles[client_id]; + spi_cs_t cs = config.clients[client_id].cs; + uint64_t data_region_size = config.clients[client_id].data_size; + + do { + spi_resp_t resp = find_next_tx(client_handle, data_region_size); + LOG_VIRT("resp.status=%s\n", spi_status_str(resp.status)); + if (resp.status == SPI_STATUS_OK) { + if (!spi_mass_enqueue_cmd(&driver_handle, tx, tx_len)) { + tx_cached = true; + tx_cs = cs; + } else { + assert(spi_enqueue_cmd_cs(&driver_handle, cs)); + microkit_deferred_notify(config.driver.id); + } + return; + } + + if (spi_enqueue_resp(&driver_handle, resp.status, resp.err_cmd_idx)) { + log_enqueue_resp_failure(resp.status, resp.err_cmd_idx, client_id, cs); + } + if (resp.status == SPI_STATUS_ERR_PARTIAL_TXN) { + microkit_deferred_notify(config.clients[client_id].conn.id); + return; + } + } while (spi_cmd_queue_len(client_handle) > 0); + + // All erroneous and terminated transactions, notify the client + microkit_deferred_notify(config.clients[client_id].conn.id); + return; +} + +void process_response() +{ + while (!spi_resp_queue_empty(&driver_handle)) { + spi_cs_t cs; + uint16_t err_cmd_idx; + spi_status_t err; + + LOG_VIRT("handle.queue_capacity_bits=%d\n", driver_handle.queue_capacity_bits); + LOG_VIRT("resp_cs_queue.len=%d\n", spi_resp_cs_queue_len(&driver_handle)); + LOG_VIRT("resp_queue.len=%d\n", spi_resp_queue_len(&driver_handle)); + LOG_VIRT("&ptr=%p\n", driver_handle.spi_resp_cs_queue); + + assert(spi_dequeue_resp_cs(&driver_handle, &cs)); + assert(spi_dequeue_resp(&driver_handle, &err, &err_cmd_idx)); + + LOG_VIRT("cs=%d\n", cs); + + uint64_t client_id = cs_to_client_id[cs]; + LOG_VIRT("client_id=%lu\n", client_id); + if (!spi_enqueue_resp(&client_handles[client_id], err, err_cmd_idx)) { + log_enqueue_resp_failure(err, err_cmd_idx, client_id, cs); + } else { + microkit_notify(CLIENT_CH_OFFSET + client_id); + } + } +} + +void init(void) +{ + assert(spi_config_check_magic(&config)); + assert(config.driver.id == 0); + assert(config.num_clients > 0); + + assert(spi_handle_init(&driver_handle, config.driver.cmd_queue.vaddr, config.driver.resp_queue.vaddr, + config.driver.cmd_cs_queue.vaddr, config.driver.resp_cs_queue.vaddr, + config.driver.queue_capacity_bits)); + + for (uint8_t i = 0; i < config.num_clients; i++) { + assert(spi_handle_init(&client_handles[i], config.clients[i].conn.cmd_queue.vaddr, + config.clients[i].conn.resp_queue.vaddr, NULL, NULL, + config.clients[i].conn.queue_capacity_bits)); + } + + tx_cached = false; + + //TODO probe HW to check if all CS lines are valid + + // Reverse lookup table from CS to client IDs + for (uint8_t i = 0; i < SPI_MAX_CS_LINES; i++) { + cs_to_client_id[i] = -1; + } + for (uint8_t i = 0; i < config.num_clients; i++) { + spi_cs_t cs = config.clients[i].cs; + uint64_t client_id = config.clients[i].conn.id - CLIENT_CH_OFFSET; + LOG_VIRT("%d: cs=%d, client_id=%ld\n", i, cs, client_id); + cs_to_client_id[cs] = client_id; + } +} + +void notified(microkit_channel ch) +{ + microkit_channel client_id = ch - CLIENT_CH_OFFSET; + if (ch == config.driver.id) { + LOG_VIRT("notified by driver\n"); + process_response(); + } else if (client_id < config.num_clients) { + LOG_VIRT("notified by client %u\n", client_id); + process_request(client_id); + } else { + LOG_VIRT_ERR("spuriously notified on channel %d\n", ch); + } +} diff --git a/spi/devices/w25qxx/w25qxx.c b/spi/devices/w25qxx/w25qxx.c new file mode 100644 index 000000000..dfb4b5735 --- /dev/null +++ b/spi/devices/w25qxx/w25qxx.c @@ -0,0 +1,157 @@ +/* + * Copyright 2025, UNSW + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +inline static w25qxx_cmd_t _pack_cmd(w25qxx_inst_t inst, uint32_t addr) +{ + return (w25qxx_cmd_t) { .inst = inst, + .addr = { + // Switch the addr from little to big-endian + (addr & 0xFF0000) >> 16, + (addr & 0x00FF00) >> 8, + (addr & 0x0000FF) >> 0, + } }; +} + +inline static uint32_t _cmd_addr(w25qxx_cmd_t cmd) +{ + return (cmd.addr[0] == 0xFF && cmd.addr[1] == 0xFF && cmd.addr[2] == 0xFF) + ? -1 + : + // Switch the addr from big to little-endian + ((uint32_t)cmd.addr[2]) << 0 | ((uint32_t)cmd.addr[1]) << 8 | ((uint32_t)cmd.addr[0]) << 16; +} + +inline static int _enqueue_cmd(w25qxx_conf_t *conf, w25qxx_cmd_t cmd, bool final_cmd) +{ + conf->data->cmd[conf->cmd_idx] = cmd; + return spi_enqueue_write(conf->libspi_conf, &conf->data->cmd[conf->cmd_idx++], + _cmd_addr(cmd) == -1 ? sizeof(cmd.inst) : sizeof(w25qxx_cmd_t), !final_cmd); +} + +inline static int _block(w25qxx_conf_t *conf) +{ + spi_notify(conf->libspi_conf); + + spi_resp_t resp; + spi_read_resp(conf->libspi_conf, &resp); + + conf->cmd_idx = 0; + + // TODO + return 0; +} + +// Public interface + +int w25qxx_reset(w25qxx_conf_t *conf) +{ + _enqueue_cmd(conf, _pack_cmd(W25QXX_INST_ENABLE_RESET, -1), false); + _enqueue_cmd(conf, _pack_cmd(W25QXX_INST_RESET_DEVICE, -1), true); + + return _block(conf); +} + +int w25qxx_get_ids(w25qxx_conf_t *conf, uint8_t *manufacturer_id, uint16_t *device_id) +{ + _enqueue_cmd(conf, _pack_cmd(W25QXX_INST_JEDEC_ID, -1), false); + spi_enqueue_read(conf->libspi_conf, conf->data->internal_data, 3, false); + + _block(conf); + + uint32_t id = conf->data->internal_data[0]; + + *manufacturer_id = id & 0xFF; + // Switch endianess from big to little-endian + *device_id = (id & 0x00FF00) | (id & 0xFF0000) >> 16; + + return 0; +} + +int w25qxx_read(w25qxx_conf_t *conf, uint32_t addr, void *buffer, uint16_t len) +{ + if (buffer < (void *)conf->data->data) { + return -1; + } + + _enqueue_cmd(conf, _pack_cmd(W25QXX_INST_READ_DATA, addr), false); + spi_enqueue_read(conf->libspi_conf, buffer, len, false); + + return _block(conf); +} + +int w25qxx_write_en(w25qxx_conf_t *conf) +{ + _enqueue_cmd(conf, _pack_cmd(W25QXX_INST_WRITE_ENABLE, -1), true); + return _block(conf); +} + +int w25qxx_program_page(w25qxx_conf_t *conf, uint32_t addr, void *buffer, uint16_t len) +{ + if (buffer < (void *)conf->data->data) { + return -1; + } + + uint16_t bytes_until_aligned = ALIGN(addr, W25QXX_PG_SZ) - addr; + //TODO + bytes_until_aligned = (bytes_until_aligned) ? bytes_until_aligned : W25QXX_PG_SZ; + if (len > bytes_until_aligned) { + return -1; + } + + w25qxx_write_en(conf); + _enqueue_cmd(conf, _pack_cmd(W25QXX_INST_PAGE_PROGRAM, addr), false); + spi_enqueue_write(conf->libspi_conf, buffer, len, false); + + return _block(conf); +} + +uint8_t w25qxx_get_status_reg_1(w25qxx_conf_t *conf) +{ + _enqueue_cmd(conf, _pack_cmd(W25QXX_INST_READ_STATUS_REGISTER_1, -1), false); + spi_enqueue_read(conf->libspi_conf, conf->data->internal_data, 1, false); + + _block(conf); + + return conf->data->internal_data[0]; +} + +void w25qxx_erase_chip(w25qxx_conf_t *conf) +{ + w25qxx_write_en(conf); + _enqueue_cmd(conf, _pack_cmd(W25QXX_INST_CHIP_ERASE, -1), true); + + _block(conf); + + uint32_t status; + do { + status = w25qxx_get_status_reg_1(conf); + LOG_W25QXX("STATUS_REG_1=%02X\n", status); + } while (W25QXX_STATUS_BUSY(status)); +} + +void w25qxx_erase_block64kb(w25qxx_conf_t *conf, uint32_t addr) +{ + w25qxx_write_en(conf); + _enqueue_cmd(conf, _pack_cmd(W25QXX_INST_BLOCK_ERASE_64KB, addr), true); + + _block(conf); + + uint32_t status; + do { + status = w25qxx_get_status_reg_1(conf); + LOG_W25QXX("STATUS_REG_1=%02X\n", status); + } while (W25QXX_STATUS_BUSY(status)); +} + +void w25qxx_global_unlock(w25qxx_conf_t *conf) +{ + w25qxx_write_en(conf); + _enqueue_cmd(conf, _pack_cmd(W25QXX_INST_GLOBAL_BLOCK_SECTOR_UNLOCK, -1), true); + + _block(conf); +} diff --git a/spi/devices/w25qxx/w25qxx.mk b/spi/devices/w25qxx/w25qxx.mk new file mode 100644 index 000000000..6ef0d914f --- /dev/null +++ b/spi/devices/w25qxx/w25qxx.mk @@ -0,0 +1,14 @@ +# +# Copyright 2025, UNSW +# +# SPDX-License-Identifier: BSD-2-Clause +# + +ifeq ($(strip $(LIBMICROKITCO_OPTS_DIR)),) +$(error LIBMICROKITCO_OPTS_DIR must be specified for w25qxx) +endif + +W25QXX_DRIVER := $(SDDF)/spi/devices/w25qxx + +w25qxx.o: ${W25QXX_DRIVER}/w25qxx.c + ${CC} ${CFLAGS} -I$(LIBMICROKITCO_OPTS_DIR) -c -o $@ $< diff --git a/spi/libspi.c b/spi/libspi.c new file mode 100644 index 000000000..ba37f0898 --- /dev/null +++ b/spi/libspi.c @@ -0,0 +1,88 @@ +/* + * Copyright 2025, UNSW + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +static spi_status_t _spi_cmd_validate(size_t read_offset, size_t write_offset, uint16_t len, uint64_t data_region_size) +{ + bool reading = read_offset != -1; + bool writing = write_offset != -1; + size_t read_end = read_offset + CMD_LEN(len); + size_t write_end = write_offset + CMD_LEN(len); + + bool read_unaligned = reading && (read_offset & sizeof(uint32_t) - 1) != 0; + bool write_unaligned = writing && (write_offset & sizeof(uint32_t) - 1) != 0; + if (read_unaligned || write_unaligned) { + return SPI_STATUS_ERR_CMD_UNALIGNED_OFFSET; + } + + bool read_oob = reading && read_end >= data_region_size; + bool write_oob = writing && write_end >= data_region_size; + if (read_oob || write_oob) { + return SPI_STATUS_ERR_CMD_OOB; + } + + bool partial_overlap = reading && writing + && ((write_offset < read_offset && read_offset < write_end) + || (read_offset < write_offset && write_offset < read_end)); + if (partial_overlap) { + return SPI_STATUS_ERR_CMD_PARTIAL_OVERLAP; + } + + return SPI_STATUS_OK; +} + +spi_status_t spi_enqueue_write(libspi_conf_t *conf, void *write_buf, uint16_t len, bool cs_active_after_cmd) +{ + return spi_enqueue_transfer(conf, NULL, write_buf, len, cs_active_after_cmd); +} + +spi_status_t spi_enqueue_read(libspi_conf_t *conf, void *read_buf, uint16_t len, bool cs_active_after_cmd) +{ + return spi_enqueue_transfer(conf, read_buf, NULL, len, cs_active_after_cmd); +} + +spi_status_t spi_enqueue_transfer(libspi_conf_t *conf, void *read_buf, void *write_buf, uint16_t len, + bool cs_active_after_cmd) +{ + size_t read_offset = (read_buf) ? read_buf - conf->data.vaddr : -1; + size_t write_offset = (write_buf) ? write_buf - conf->data.vaddr : -1; + spi_status_t err = _spi_cmd_validate(read_offset, write_offset, len, conf->data.size); + if (err != SPI_STATUS_OK) { + return err; + } + + spi_handle_t handle; + assert(spi_handle_init(&handle, conf->virt.cmd_queue.vaddr, conf->virt.resp_queue.vaddr, NULL, NULL, + conf->virt.queue_capacity_bits)); + return spi_enqueue_cmd(&handle, read_offset, write_offset, len, cs_active_after_cmd) ? SPI_STATUS_ERR_CMD_QUEUE_FULL + : SPI_STATUS_OK; +} + +spi_status_t spi_enqueue_dummy(libspi_conf_t *conf, uint16_t len, bool cs_active_after_cmd) +{ + return spi_enqueue_transfer(conf, NULL, NULL, len, cs_active_after_cmd); +} + +spi_status_t spi_notify(libspi_conf_t *conf) +{ + spi_cmd_queue_t *queue = conf->virt.cmd_queue.vaddr; + if (queue->element[queue->tail].cs_active_after_cmd) { + return SPI_STATUS_ERR_PARTIAL_TXN; + } + + microkit_deferred_notify(conf->virt.id); + microkit_cothread_semaphore_wait(&async_io_semaphore); + return SPI_STATUS_OK; +} + +spi_status_t spi_read_resp(libspi_conf_t *conf, spi_resp_t *resp) +{ + spi_handle_t handle; + assert(spi_handle_init(&handle, conf->virt.cmd_queue.vaddr, conf->virt.resp_queue.vaddr, NULL, NULL, + conf->virt.queue_capacity_bits)); + return spi_dequeue_resp(&handle, &resp->err, &resp->err_cmd_idx) ? SPI_STATUS_ERR_RESP_QUEUE_EMPTY : SPI_STATUS_OK; +} diff --git a/spi/libspi.mk b/spi/libspi.mk new file mode 100644 index 000000000..126392109 --- /dev/null +++ b/spi/libspi.mk @@ -0,0 +1,31 @@ +# +# Copyright 2025, UNSW +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Include this make snippet to build libspi.a +# Requires libmicrokitco to be available +# +# Required variables: +# LIBMICROKITCO_INCLUDE_DIR - Path to libmicrokitco include directory +# LIBMICROKITCO_OPTS_DIR - Path to directory containing libmicrokitco_opts.h + +ifeq ($(strip $(LIBMICROKITCO_INCLUDE_DIR)),) +$(error LIBMICROKITCO_INCLUDE_DIR must be specified for libspi) +endif + +ifeq ($(strip $(LIBMICROKITCO_OPTS_DIR)),) +$(error LIBMICROKITCO_OPTS_DIR must be specified for libspi) +endif + +LIBSPI_DIR := $(SDDF)/spi +LIBSPI_OBJ := libspi.o + +libspi.o: $(LIBSPI_DIR)/libspi.c + $(CC) $(CFLAGS) -c -I$(SDDF)/include -I$(SDDF)/include/microkit -I$(LIBMICROKITCO_INCLUDE_DIR) -I$(LIBMICROKITCO_OPTS_DIR) -o $@ $< + +libspi.a: $(LIBSPI_OBJ) + $(AR) crv $@ $(LIBSPI_OBJ) + $(RANLIB) $@ + +-include $(LIBSPI_OBJ:.o=.d)