blob: d27bbcaa69cc4a95168be2458cc4d42cfde47721 [file] [log] [blame]
// 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;
}