| // Copyright lowRISC contributors. | |
| // Licensed under the Apache License, Version 2.0, see LICENSE for details. | |
| // SPDX-License-Identifier: Apache-2.0 | |
| #include "sw/device/lib/dif/dif_spi_host.h" | |
| #include <assert.h> | |
| #include <stdalign.h> | |
| #include <stddef.h> | |
| #include "sw/device/lib/base/bitfield.h" | |
| #include "sw/device/lib/base/memory.h" | |
| #include "sw/device/lib/base/mmio.h" | |
| #include "spi_host_regs.h" // Generated. | |
| // We create weak symbol aliases for the FIFO write and read functions so the | |
| // unit tests can provide mocks. The mocks provide for separate testing of | |
| // the FIFO functions and the overall transaction management functions. | |
| OT_WEAK | |
| OT_ALIAS("dif_spi_host_fifo_write") | |
| dif_result_t spi_host_fifo_write_alias(const dif_spi_host_t *spi_host, | |
| const void *src, uint16_t len); | |
| OT_WEAK | |
| OT_ALIAS("dif_spi_host_fifo_read") | |
| dif_result_t spi_host_fifo_read_alias(const dif_spi_host_t *spi_host, void *dst, | |
| uint16_t len); | |
| static void spi_host_reset(const dif_spi_host_t *spi_host) { | |
| // Set the software reset request bit. | |
| mmio_region_write32( | |
| spi_host->base_addr, SPI_HOST_CONTROL_REG_OFFSET, | |
| bitfield_bit32_write(0, SPI_HOST_CONTROL_SW_RST_BIT, true)); | |
| // Wait for the spi host to go inactive. | |
| bool active; | |
| do { | |
| uint32_t reg = | |
| mmio_region_read32(spi_host->base_addr, SPI_HOST_STATUS_REG_OFFSET); | |
| active = bitfield_bit32_read(reg, SPI_HOST_STATUS_ACTIVE_BIT); | |
| } while (active); | |
| // Wait for the spi host fifos to drain. | |
| uint32_t txqd, rxqd; | |
| do { | |
| uint32_t reg = | |
| mmio_region_read32(spi_host->base_addr, SPI_HOST_STATUS_REG_OFFSET); | |
| txqd = bitfield_field32_read(reg, SPI_HOST_STATUS_TXQD_FIELD); | |
| rxqd = bitfield_field32_read(reg, SPI_HOST_STATUS_RXQD_FIELD); | |
| } while (txqd != 0 || rxqd != 0); | |
| // Clear the software reset request bit. | |
| mmio_region_write32( | |
| spi_host->base_addr, SPI_HOST_CONTROL_REG_OFFSET, | |
| bitfield_bit32_write(0, SPI_HOST_CONTROL_SW_RST_BIT, false)); | |
| } | |
| static void spi_host_enable(const dif_spi_host_t *spi_host, bool enable) { | |
| mmio_region_write32( | |
| spi_host->base_addr, SPI_HOST_CONTROL_REG_OFFSET, | |
| bitfield_bit32_write(0, SPI_HOST_CONTROL_SPIEN_BIT, enable)); | |
| } | |
| dif_result_t dif_spi_host_configure(const dif_spi_host_t *spi_host, | |
| dif_spi_host_config_t config) { | |
| if (spi_host == NULL) { | |
| return kDifBadArg; | |
| } | |
| if (config.peripheral_clock_freq_hz == 0 || config.spi_clock == 0) { | |
| return kDifBadArg; | |
| } | |
| uint32_t divider = | |
| ((config.peripheral_clock_freq_hz / config.spi_clock) / 2) - 1; | |
| if (divider & ~SPI_HOST_CONFIGOPTS_CLKDIV_0_MASK) { | |
| return kDifBadArg; | |
| } | |
| spi_host_reset(spi_host); | |
| uint32_t reg = 0; | |
| reg = | |
| bitfield_field32_write(reg, SPI_HOST_CONFIGOPTS_CLKDIV_0_FIELD, divider); | |
| reg = bitfield_field32_write(reg, SPI_HOST_CONFIGOPTS_CSNIDLE_0_FIELD, | |
| config.chip_select.idle); | |
| reg = bitfield_field32_write(reg, SPI_HOST_CONFIGOPTS_CSNTRAIL_0_FIELD, | |
| config.chip_select.trail); | |
| reg = bitfield_field32_write(reg, SPI_HOST_CONFIGOPTS_CSNLEAD_0_FIELD, | |
| config.chip_select.lead); | |
| reg = bitfield_bit32_write(reg, SPI_HOST_CONFIGOPTS_FULLCYC_0_BIT, | |
| config.full_cycle); | |
| reg = bitfield_bit32_write(reg, SPI_HOST_CONFIGOPTS_CPHA_0_BIT, config.cpha); | |
| reg = bitfield_bit32_write(reg, SPI_HOST_CONFIGOPTS_CPOL_0_BIT, config.cpol); | |
| mmio_region_write32(spi_host->base_addr, SPI_HOST_CONFIGOPTS_REG_OFFSET, reg); | |
| spi_host_enable(spi_host, true); | |
| return kDifOk; | |
| } | |
| dif_result_t dif_spi_host_output_set_enabled(const dif_spi_host_t *spi_host, | |
| bool enabled) { | |
| if (spi_host == NULL) { | |
| return kDifBadArg; | |
| } | |
| uint32_t reg = | |
| mmio_region_read32(spi_host->base_addr, SPI_HOST_CONTROL_REG_OFFSET); | |
| mmio_region_write32( | |
| spi_host->base_addr, SPI_HOST_CONTROL_REG_OFFSET, | |
| bitfield_bit32_write(reg, SPI_HOST_CONTROL_OUTPUT_EN_BIT, enabled)); | |
| return kDifOk; | |
| } | |
| static void wait_ready(const dif_spi_host_t *spi_host) { | |
| bool ready; | |
| do { | |
| uint32_t reg = | |
| mmio_region_read32(spi_host->base_addr, SPI_HOST_STATUS_REG_OFFSET); | |
| ready = bitfield_bit32_read(reg, SPI_HOST_STATUS_READY_BIT); | |
| } while (!ready); | |
| } | |
| static void wait_tx_fifo(const dif_spi_host_t *spi_host) { | |
| uint32_t txqd; | |
| do { | |
| uint32_t reg = | |
| mmio_region_read32(spi_host->base_addr, SPI_HOST_STATUS_REG_OFFSET); | |
| txqd = bitfield_field32_read(reg, SPI_HOST_STATUS_TXQD_FIELD); | |
| } while (txqd == SPI_HOST_PARAM_TX_DEPTH); | |
| } | |
| static void wait_rx_fifo(const dif_spi_host_t *spi_host) { | |
| uint32_t rxqd; | |
| do { | |
| uint32_t reg = | |
| mmio_region_read32(spi_host->base_addr, SPI_HOST_STATUS_REG_OFFSET); | |
| rxqd = bitfield_field32_read(reg, SPI_HOST_STATUS_RXQD_FIELD); | |
| } while (rxqd == 0); | |
| } | |
| static inline void tx_fifo_write8(const dif_spi_host_t *spi_host, | |
| uintptr_t srcaddr) { | |
| uint8_t *src = (uint8_t *)srcaddr; | |
| wait_tx_fifo(spi_host); | |
| mmio_region_write8(spi_host->base_addr, SPI_HOST_TXDATA_REG_OFFSET, *src); | |
| } | |
| static inline void tx_fifo_write32(const dif_spi_host_t *spi_host, | |
| uintptr_t srcaddr) { | |
| wait_tx_fifo(spi_host); | |
| uint32_t val = read_32((const void *)srcaddr); | |
| mmio_region_write32(spi_host->base_addr, SPI_HOST_TXDATA_REG_OFFSET, val); | |
| } | |
| dif_result_t dif_spi_host_fifo_write(const dif_spi_host_t *spi_host, | |
| const void *src, uint16_t len) { | |
| uintptr_t ptr = (uintptr_t)src; | |
| if (spi_host == NULL || (src == NULL && len > 0)) { | |
| return kDifBadArg; | |
| } | |
| // If the pointer starts mis-aligned, write until we are aligned. | |
| while (misalignment32_of(ptr) && len > 0) { | |
| tx_fifo_write8(spi_host, ptr); | |
| ptr += 1; | |
| len -= 1; | |
| } | |
| // Write complete 32-bit words to the fifo. | |
| while (len > 3) { | |
| tx_fifo_write32(spi_host, ptr); | |
| ptr += 4; | |
| len -= 4; | |
| } | |
| // Clean up any leftover bytes. | |
| while (len > 0) { | |
| tx_fifo_write8(spi_host, ptr); | |
| ptr += 1; | |
| len -= 1; | |
| } | |
| return kDifOk; | |
| } | |
| typedef struct queue { | |
| int32_t length; | |
| uint8_t alignas(uint64_t) data[8]; | |
| } queue_t; | |
| static void enqueue_byte(queue_t *queue, uint8_t data) { | |
| queue->data[queue->length++] = data; | |
| } | |
| static void enqueue_word(queue_t *queue, uint32_t data) { | |
| if (queue->length % sizeof(uint32_t) == 0) { | |
| write_32(data, queue->data + queue->length); | |
| queue->length += 4; | |
| } else { | |
| for (size_t i = 0; i < sizeof(uint32_t); ++i) { | |
| enqueue_byte(queue, (uint8_t)data); | |
| data >>= 8; | |
| } | |
| } | |
| } | |
| static uint8_t dequeue_byte(queue_t *queue) { | |
| uint8_t val = queue->data[0]; | |
| uint64_t qword = read_64(queue->data); | |
| write_64(qword >> 8, queue->data); | |
| queue->length -= 1; | |
| return val; | |
| } | |
| static uint32_t dequeue_word(queue_t *queue) { | |
| uint32_t val = read_32(queue->data); | |
| write_32(read_32(queue->data + sizeof(uint32_t)), queue->data); | |
| queue->length -= 4; | |
| return val; | |
| } | |
| dif_result_t dif_spi_host_fifo_read(const dif_spi_host_t *spi_host, void *dst, | |
| uint16_t len) { | |
| if (spi_host == NULL || (dst == NULL && len > 0)) { | |
| return kDifBadArg; | |
| } | |
| uintptr_t ptr = (uintptr_t)dst; | |
| // We always have to read from the RXFIFO as a 32-bit word. We use a | |
| // two-word queue to handle destination and length mis-alignments. | |
| queue_t queue = {0}; | |
| // If the buffer is misaligned, write a byte at a time until we reach | |
| // alignment. | |
| while (misalignment32_of(ptr) && len > 0) { | |
| if (queue.length < 1) { | |
| wait_rx_fifo(spi_host); | |
| enqueue_word(&queue, mmio_region_read32(spi_host->base_addr, | |
| SPI_HOST_RXDATA_REG_OFFSET)); | |
| } | |
| uint8_t *p = (uint8_t *)ptr; | |
| *p = dequeue_byte(&queue); | |
| ptr += 1; | |
| len -= 1; | |
| } | |
| // While we can write complete words to memory, operate on 4 bytes at a time. | |
| while (len > 3) { | |
| if (queue.length < 4) { | |
| wait_rx_fifo(spi_host); | |
| enqueue_word(&queue, mmio_region_read32(spi_host->base_addr, | |
| SPI_HOST_RXDATA_REG_OFFSET)); | |
| } | |
| write_32(dequeue_word(&queue), (void *)ptr); | |
| ptr += 4; | |
| len -= 4; | |
| } | |
| // Finish up any left over buffer a byte at a time. | |
| while (len > 0) { | |
| if (queue.length < 1) { | |
| wait_rx_fifo(spi_host); | |
| enqueue_word(&queue, mmio_region_read32(spi_host->base_addr, | |
| SPI_HOST_RXDATA_REG_OFFSET)); | |
| } | |
| uint8_t *p = (uint8_t *)ptr; | |
| *p = dequeue_byte(&queue); | |
| ptr += 1; | |
| len -= 1; | |
| } | |
| return kDifOk; | |
| } | |
| static void write_command_reg(const dif_spi_host_t *spi_host, uint16_t length, | |
| dif_spi_host_width_t speed, | |
| dif_spi_host_direction_t direction, | |
| bool last_segment) { | |
| uint32_t reg = 0; | |
| reg = bitfield_field32_write(reg, SPI_HOST_COMMAND_LEN_FIELD, length - 1); | |
| reg = bitfield_field32_write(reg, SPI_HOST_COMMAND_SPEED_FIELD, speed); | |
| reg = | |
| bitfield_field32_write(reg, SPI_HOST_COMMAND_DIRECTION_FIELD, direction); | |
| reg = bitfield_bit32_write(reg, SPI_HOST_COMMAND_CSAAT_BIT, !last_segment); | |
| mmio_region_write32(spi_host->base_addr, SPI_HOST_COMMAND_REG_OFFSET, reg); | |
| } | |
| static void issue_opcode(const dif_spi_host_t *spi_host, | |
| dif_spi_host_segment_t *segment, bool last_segment) { | |
| wait_tx_fifo(spi_host); | |
| mmio_region_write8(spi_host->base_addr, SPI_HOST_TXDATA_REG_OFFSET, | |
| segment->opcode); | |
| write_command_reg(spi_host, 1, kDifSpiHostWidthStandard, | |
| kDifSpiHostDirectionTx, last_segment); | |
| } | |
| static void issue_address(const dif_spi_host_t *spi_host, | |
| dif_spi_host_segment_t *segment, bool last_segment) { | |
| wait_tx_fifo(spi_host); | |
| // The address appears on the wire in big-endian order. | |
| uint32_t address = bitfield_byteswap32(segment->address.address); | |
| uint32_t length; | |
| if (segment->address.mode == kDifSpiHostAddrMode4b) { | |
| length = 4; | |
| mmio_region_write32(spi_host->base_addr, SPI_HOST_TXDATA_REG_OFFSET, | |
| address); | |
| } else { | |
| length = 3; | |
| address >>= 8; | |
| mmio_region_write32(spi_host->base_addr, SPI_HOST_TXDATA_REG_OFFSET, | |
| address); | |
| } | |
| write_command_reg(spi_host, length, segment->address.width, | |
| kDifSpiHostDirectionTx, last_segment); | |
| } | |
| static void issue_dummy(const dif_spi_host_t *spi_host, | |
| dif_spi_host_segment_t *segment, bool last_segment) { | |
| write_command_reg(spi_host, segment->dummy.length, segment->dummy.width, | |
| kDifSpiHostDirectionDummy, last_segment); | |
| } | |
| static dif_result_t issue_data_phase(const dif_spi_host_t *spi_host, | |
| dif_spi_host_segment_t *segment, | |
| bool last_segment) { | |
| size_t length; | |
| dif_spi_host_width_t width; | |
| dif_spi_host_direction_t direction; | |
| switch (segment->type) { | |
| case kDifSpiHostSegmentTypeTx: | |
| width = segment->tx.width; | |
| length = segment->tx.length; | |
| direction = kDifSpiHostDirectionTx; | |
| spi_host_fifo_write_alias(spi_host, segment->tx.buf, segment->tx.length); | |
| break; | |
| case kDifSpiHostSegmentTypeBidirectional: | |
| width = segment->bidir.width; | |
| length = segment->bidir.length; | |
| direction = kDifSpiHostDirectionBidirectional; | |
| spi_host_fifo_write_alias(spi_host, segment->bidir.txbuf, | |
| segment->bidir.length); | |
| break; | |
| case kDifSpiHostSegmentTypeRx: | |
| width = segment->rx.width; | |
| length = segment->rx.length; | |
| direction = kDifSpiHostDirectionRx; | |
| break; | |
| default: | |
| // Programming error (within this file). We should never get here. | |
| // `issue_data_phase` should only get called for segment types which | |
| // represent a data transfer. | |
| return kDifBadArg; | |
| } | |
| write_command_reg(spi_host, length, width, direction, last_segment); | |
| return kDifOk; | |
| } | |
| dif_result_t dif_spi_host_transaction(const dif_spi_host_t *spi_host, | |
| uint32_t csid, | |
| dif_spi_host_segment_t *segments, | |
| size_t length) { | |
| // Write to chip select ID. | |
| mmio_region_write32(spi_host->base_addr, SPI_HOST_CSID_REG_OFFSET, csid); | |
| // For each segment, write the segment information to the | |
| // COMMAND register and transmit FIFO. | |
| for (size_t i = 0; i < length; ++i) { | |
| bool last_segment = i == length - 1; | |
| wait_ready(spi_host); | |
| dif_spi_host_segment_t *segment = &segments[i]; | |
| switch (segment->type) { | |
| case kDifSpiHostSegmentTypeOpcode: | |
| issue_opcode(spi_host, segment, last_segment); | |
| break; | |
| case kDifSpiHostSegmentTypeAddress: | |
| issue_address(spi_host, segment, last_segment); | |
| break; | |
| case kDifSpiHostSegmentTypeDummy: | |
| issue_dummy(spi_host, segment, last_segment); | |
| break; | |
| case kDifSpiHostSegmentTypeTx: | |
| case kDifSpiHostSegmentTypeRx: | |
| case kDifSpiHostSegmentTypeBidirectional: { | |
| dif_result_t error = issue_data_phase(spi_host, segment, last_segment); | |
| if (error != kDifOk) { | |
| return error; | |
| } | |
| break; | |
| } | |
| default: | |
| return kDifBadArg; | |
| } | |
| } | |
| // For each segment which receives data, read from the receive FIFO. | |
| for (size_t i = 0; i < length; ++i) { | |
| dif_spi_host_segment_t *segment = &segments[i]; | |
| switch (segment->type) { | |
| case kDifSpiHostSegmentTypeRx: | |
| spi_host_fifo_read_alias(spi_host, segment->rx.buf, segment->rx.length); | |
| break; | |
| case kDifSpiHostSegmentTypeBidirectional: | |
| spi_host_fifo_read_alias(spi_host, segment->bidir.rxbuf, | |
| segment->bidir.length); | |
| break; | |
| default: | |
| /* do nothing */; | |
| } | |
| } | |
| return kDifOk; | |
| } |