diff options
Diffstat (limited to 'src/soc/samsung/exynos5420/spi.c')
-rw-r--r-- | src/soc/samsung/exynos5420/spi.c | 411 |
1 files changed, 411 insertions, 0 deletions
diff --git a/src/soc/samsung/exynos5420/spi.c b/src/soc/samsung/exynos5420/spi.c new file mode 100644 index 0000000000..c6c08e925c --- /dev/null +++ b/src/soc/samsung/exynos5420/spi.c @@ -0,0 +1,411 @@ +/* + * This file is part of the coreboot project. + * + * Copyright (C) 2011 Samsung Electronics + * Copyright 2013 Google Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include <console/console.h> +#include <arch/io.h> +#include <stdlib.h> +#include <assert.h> +#include <spi_flash.h> + +#include "cpu.h" +#include "spi.h" + +#define EXYNOS_SPI_MAX_TRANSFER_BYTES (65535) + +#if defined(CONFIG_DEBUG_SPI) && CONFIG_DEBUG_SPI +# define DEBUG_SPI(x,...) printk(BIOS_DEBUG, "EXYNOS_SPI: " x) +#else +# define DEBUG_SPI(x,...) +#endif + +struct exynos_spi_slave { + struct spi_slave slave; + struct exynos_spi *regs; + unsigned int fifo_size; + uint8_t half_duplex; + uint8_t frame_header; /* header byte to detect in half-duplex mode. */ +}; + +/* TODO(hungte) Move the SPI param list to per-board configuration, probably + * Kconfig or mainboard.c */ +static struct exynos_spi_slave exynos_spi_slaves[3] = { + // SPI 0 + { + .slave = { .bus = 0, }, + .regs = (void *)EXYNOS5_SPI0_BASE, + }, + // SPI 1 + { + .slave = { .bus = 1, .rw = SPI_READ_FLAG, }, + .regs = (void *)EXYNOS5_SPI1_BASE, + .fifo_size = 64, + .half_duplex = 0, + }, + // SPI 2 + { + .slave = { .bus = 2, + .rw = SPI_READ_FLAG | SPI_WRITE_FLAG, }, + .regs = (void *)EXYNOS5_SPI2_BASE, + .fifo_size = 64, + .half_duplex = 1, + .frame_header = 0xec, + }, +}; + +static inline struct exynos_spi_slave *to_exynos_spi(struct spi_slave *slave) +{ + return container_of(slave, struct exynos_spi_slave, slave); +} + +void spi_init(void) +{ + printk(BIOS_INFO, "Exynos SPI driver initiated.\n"); +} + +struct spi_slave *spi_setup_slave(unsigned int bus, unsigned int cs) +{ + ASSERT(bus >= 0 && bus < 3); + return &(exynos_spi_slaves[bus].slave); +} + +int spi_cs_is_valid(unsigned int bus, unsigned int cs) +{ + return bus > 0 && bus < 3; +} + +void spi_cs_activate(struct spi_slave *slave) +{ + struct exynos_spi *regs = to_exynos_spi(slave)->regs; + // TODO(hungte) Add some delay if too many transactions happen at once. + clrbits_le32(®s->cs_reg, SPI_SLAVE_SIG_INACT); +} + +void spi_cs_deactivate(struct spi_slave *slave) +{ + struct exynos_spi *regs = to_exynos_spi(slave)->regs; + setbits_le32(®s->cs_reg, SPI_SLAVE_SIG_INACT); +} + +static inline void exynos_spi_soft_reset(struct exynos_spi *regs) +{ + /* The soft reset clears only FIFO and status register. + * All special function registers are not changed. */ + setbits_le32(®s->ch_cfg, SPI_CH_RST); + clrbits_le32(®s->ch_cfg, SPI_CH_RST); +} + +static inline void exynos_spi_flush_fifo(struct exynos_spi *regs) +{ + /* + * Flush spi tx, rx fifos and reset the SPI controller + * and clear rx/tx channel + */ + clrbits_le32(®s->ch_cfg, SPI_RX_CH_ON | SPI_TX_CH_ON); + clrbits_le32(®s->ch_cfg, SPI_CH_HS_EN); + exynos_spi_soft_reset(regs); + setbits_le32(®s->ch_cfg, SPI_RX_CH_ON | SPI_TX_CH_ON); +} + +static void exynos_spi_request_bytes(struct exynos_spi *regs, int count, + int width) +{ + uint32_t mode_word = SPI_MODE_CH_WIDTH_WORD | SPI_MODE_BUS_WIDTH_WORD, + swap_word = (SPI_TX_SWAP_EN | SPI_RX_SWAP_EN | + SPI_TX_BYTE_SWAP | SPI_RX_BYTE_SWAP | + SPI_TX_HWORD_SWAP | SPI_RX_HWORD_SWAP); + + /* For word address we need to swap bytes */ + if (width == sizeof(uint32_t)) { + setbits_le32(®s->mode_cfg, mode_word); + setbits_le32(®s->swap_cfg, swap_word); + count /= width; + } else { + /* Select byte access and clear the swap configuration */ + clrbits_le32(®s->mode_cfg, mode_word); + writel(0, ®s->swap_cfg); + } + + exynos_spi_soft_reset(regs); + + if (count) { + ASSERT(count < (1 << 16)); + writel(count | SPI_PACKET_CNT_EN, ®s->pkt_cnt); + } else { + writel(0, ®s->pkt_cnt); + } +} + +static int spi_rx_tx(struct spi_slave *slave, uint8_t *rxp, int rx_bytes, + const uint8_t *txp, int tx_bytes) +{ + struct exynos_spi_slave *espi = to_exynos_spi(slave); + struct exynos_spi *regs = espi->regs; + + int step; + int todo = MAX(rx_bytes, tx_bytes); + int wait_for_frame_header = espi->half_duplex; + + ASSERT(todo < EXYNOS_SPI_MAX_TRANSFER_BYTES); + + /* Select transfer mode. */ + if (espi->half_duplex) { + step = 1; + } else if ((rx_bytes | tx_bytes | (uintptr_t)rxp |(uintptr_t)txp) & 3) { + printk(BIOS_CRIT, "%s: WARNING: tranfer mode decreased to 1B\n", + __func__); + step = 1; + } else { + step = sizeof(uint32_t); + } + + exynos_spi_request_bytes(regs, espi->half_duplex ? 0 : todo, step); + + /* Note: Some device, like ChromeOS EC, tries to work in half-duplex + * mode and sends a large amount of data (larger than FIFO size). + * Printing lots of debug messages or doing extra delay in the loop + * below may cause rx buffer to overflow and getting unexpected data + * error. + */ + while (rx_bytes || tx_bytes) { + int temp; + uint32_t spi_sts = readl(®s->spi_sts); + int rx_lvl = (spi_sts >> SPI_RX_LVL_OFFSET) & SPI_FIFO_LVL_MASK, + tx_lvl = (spi_sts >> SPI_TX_LVL_OFFSET) & SPI_FIFO_LVL_MASK; + int min_tx = ((tx_bytes || !espi->half_duplex) ? + (espi->fifo_size / 2) : 1); + + // TODO(hungte) Abort if timeout happens in half-duplex mode. + + /* + * Don't completely fill the txfifo, since we don't want our + * rxfifo to overflow, and it may already contain data. + */ + while (tx_lvl < min_tx) { + if (tx_bytes) { + if (step == sizeof(uint32_t)) { + temp = *((uint32_t *)txp); + txp += sizeof(uint32_t); + } else { + temp = *txp++; + } + tx_bytes -= step; + } else { + temp = -1; + } + writel(temp, ®s->tx_data); + tx_lvl += step; + } + + while ((rx_lvl >= step) && rx_bytes) { + temp = readl(®s->rx_data); + rx_lvl -= step; + if (wait_for_frame_header) { + if ((temp & 0xff) == espi->frame_header) { + wait_for_frame_header = 0; + } + break; /* Restart the outer loop. */ + } + if (step == sizeof(uint32_t)) { + *((uint32_t *)rxp) = temp; + rxp += sizeof(uint32_t); + } else { + *rxp++ = temp; + } + rx_bytes -= step; + } + } + return 0; +} + +int spi_claim_bus(struct spi_slave *slave) +{ + struct exynos_spi_slave *espi = to_exynos_spi(slave); + struct exynos_spi *regs = espi->regs; + + exynos_spi_flush_fifo(regs); + + // Select Active High Clock, Format A (SCP 30.2.1.8). + clrbits_le32(®s->ch_cfg, SPI_CH_CPOL_L | SPI_CH_CPHA_B); + + // Set FeedBack Clock Selection. + writel(SPI_FB_DELAY_180, ®s->fb_clk); + + // HIGH speed is required for Tx/Rx to work in 50MHz (SCP 30.2.1.6). + if (espi->half_duplex) { + clrbits_le32(®s->ch_cfg, SPI_CH_HS_EN); + printk(BIOS_DEBUG, "%s: LOW speed.\n", __func__); + } else { + setbits_le32(®s->ch_cfg, SPI_CH_HS_EN); + printk(BIOS_DEBUG, "%s: HIGH speed.\n", __func__); + } + return 0; +} + +int spi_xfer(struct spi_slave *slave, const void *dout, unsigned int out_bytes, + void *din, unsigned int in_bytes) +{ + uint8_t *out_ptr = (uint8_t *)dout, *in_ptr = (uint8_t *)din; + int offset, todo, len; + int ret = 0; + + len = MAX(out_bytes, in_bytes); + + /* + * Exynos SPI limits each transfer to (2^16-1=65535) bytes. To keep + * things simple (especially for word-width transfer mode), allow a + * maximum of (2^16-4=65532) bytes. We could allow more in word mode, + * but the performance difference is small. + */ + spi_cs_activate(slave); + for (offset = 0; !ret && (offset < len); offset += todo) { + todo = min(len - offset, (1 << 16) - 4); + ret = spi_rx_tx(slave, in_ptr, MIN(in_bytes, todo), out_ptr, + MIN(out_bytes, todo)); + // Adjust remaining bytes and pointers. + if (in_bytes >= todo) { + in_bytes -= todo; + in_ptr += todo; + } else { + in_bytes = 0; + in_ptr = NULL; + } + if (out_bytes >= todo) { + out_bytes -= todo; + out_ptr += todo; + } else { + out_bytes = 0; + out_ptr = NULL; + } + } + spi_cs_deactivate(slave); + + return ret; +} + +static int exynos_spi_read(struct spi_slave *slave, void *dest, uint32_t len, + uint32_t off) +{ + struct exynos_spi *regs = to_exynos_spi(slave)->regs; + int rv; + + // TODO(hungte) Merge the "read address" command into spi_xfer calls + // (full-duplex mode). + + spi_cs_activate(slave); + + // Specify read address (in word-width mode). + ASSERT(off < (1 << 24)); + exynos_spi_request_bytes(regs, sizeof(off), sizeof(off)); + writel(htonl((SF_READ_DATA_CMD << 24) | off), ®s->tx_data); + while (!(readl(®s->spi_sts) & SPI_ST_TX_DONE)) { + /* Wait for TX done */ + } + + // Now, safe to transfer. + rv = spi_xfer(slave, NULL, 0, dest, len * 8); + spi_cs_deactivate(slave); + + return (rv == 0) ? len : -1; +} + +void spi_release_bus(struct spi_slave *slave) +{ + struct exynos_spi *regs = to_exynos_spi(slave)->regs; + /* Reset swap mode to make sure no one relying on default values (Ex, + * payload or kernel) will go wrong. */ + clrbits_le32(®s->mode_cfg, (SPI_MODE_CH_WIDTH_WORD | + SPI_MODE_BUS_WIDTH_WORD)); + writel(0, ®s->swap_cfg); + exynos_spi_flush_fifo(regs); +} + +// SPI as CBFS media. +struct exynos_spi_media { + struct spi_slave *slave; + struct cbfs_simple_buffer buffer; +}; + +static int exynos_spi_cbfs_open(struct cbfs_media *media) +{ + struct exynos_spi_media *spi = (struct exynos_spi_media*)media->context; + DEBUG_SPI("exynos_spi_cbfs_open\n"); + return spi_claim_bus(spi->slave); +} + +static int exynos_spi_cbfs_close(struct cbfs_media *media) +{ + struct exynos_spi_media *spi = (struct exynos_spi_media*)media->context; + DEBUG_SPI("exynos_spi_cbfs_close\n"); + spi_release_bus(spi->slave); + return 0; +} + +static size_t exynos_spi_cbfs_read(struct cbfs_media *media, void *dest, + size_t offset, size_t count) +{ + struct exynos_spi_media *spi = (struct exynos_spi_media*)media->context; + int bytes; + DEBUG_SPI("exynos_spi_cbfs_read(%u)\n", count); + bytes = exynos_spi_read(spi->slave, dest, count, offset); + return bytes; +} + +static void *exynos_spi_cbfs_map(struct cbfs_media *media, size_t offset, + size_t count) +{ + struct exynos_spi_media *spi = (struct exynos_spi_media*)media->context; + DEBUG_SPI("exynos_spi_cbfs_map\n"); + // exynos: spi_rx_tx may work in 4 byte-width-transmission mode and + // requires buffer memory address to be aligned. + if (count % 4) + count += 4 - (count % 4); + return cbfs_simple_buffer_map(&spi->buffer, media, offset, count); +} + +static void *exynos_spi_cbfs_unmap(struct cbfs_media *media, + const void *address) +{ + struct exynos_spi_media *spi = (struct exynos_spi_media*)media->context; + DEBUG_SPI("exynos_spi_cbfs_unmap\n"); + return cbfs_simple_buffer_unmap(&spi->buffer, address); +} + +int initialize_exynos_spi_cbfs_media(struct cbfs_media *media, + void *buffer_address, + size_t buffer_size) +{ + // TODO Replace static variable to support multiple streams. + static struct exynos_spi_media context; + static struct exynos_spi_slave *eslave = &exynos_spi_slaves[1]; + DEBUG_SPI("initialize_exynos_spi_cbfs_media\n"); + + context.slave = &eslave->slave; + context.buffer.allocated = context.buffer.last_allocate = 0; + context.buffer.buffer = buffer_address; + context.buffer.size = buffer_size; + media->context = (void*)&context; + media->open = exynos_spi_cbfs_open; + media->close = exynos_spi_cbfs_close; + media->read = exynos_spi_cbfs_read; + media->map = exynos_spi_cbfs_map; + media->unmap = exynos_spi_cbfs_unmap; + + return 0; +} |