| /* |
| * This file is subject to the terms and conditions of the GNU General Public |
| * License. See the file "COPYING" in the main directory of this archive |
| * for more details. |
| * |
| * Copyright (C) 2011, 2012 Cavium, Inc. |
| */ |
| |
| #include <linux/platform_device.h> |
| #include <linux/interrupt.h> |
| #include <linux/spi/spi.h> |
| #include <linux/module.h> |
| #include <linux/delay.h> |
| #include <linux/init.h> |
| #include <linux/io.h> |
| #include <linux/of.h> |
| |
| #include <asm/octeon/octeon.h> |
| #include <asm/octeon/cvmx-mpi-defs.h> |
| |
| #define OCTEON_SPI_CFG 0 |
| #define OCTEON_SPI_STS 0x08 |
| #define OCTEON_SPI_TX 0x10 |
| #define OCTEON_SPI_DAT0 0x80 |
| |
| #define OCTEON_SPI_MAX_BYTES 9 |
| |
| #define OCTEON_SPI_MAX_CLOCK_HZ 16000000 |
| |
| struct octeon_spi { |
| u64 register_base; |
| u64 last_cfg; |
| u64 cs_enax; |
| }; |
| |
| struct octeon_spi_setup { |
| u32 max_speed_hz; |
| u8 chip_select; |
| u8 mode; |
| u8 bits_per_word; |
| }; |
| |
| static void octeon_spi_wait_ready(struct octeon_spi *p) |
| { |
| union cvmx_mpi_sts mpi_sts; |
| unsigned int loops = 0; |
| |
| do { |
| if (loops++) |
| __delay(500); |
| mpi_sts.u64 = cvmx_read_csr(p->register_base + OCTEON_SPI_STS); |
| } while (mpi_sts.s.busy); |
| } |
| |
| static int octeon_spi_do_transfer(struct octeon_spi *p, |
| struct spi_message *msg, |
| struct spi_transfer *xfer, |
| bool last_xfer) |
| { |
| union cvmx_mpi_cfg mpi_cfg; |
| union cvmx_mpi_tx mpi_tx; |
| unsigned int clkdiv; |
| unsigned int speed_hz; |
| int mode; |
| bool cpha, cpol; |
| const u8 *tx_buf; |
| u8 *rx_buf; |
| int len; |
| int i; |
| |
| struct octeon_spi_setup *msg_setup = spi_get_ctldata(msg->spi); |
| |
| speed_hz = msg_setup->max_speed_hz; |
| mode = msg_setup->mode; |
| cpha = mode & SPI_CPHA; |
| cpol = mode & SPI_CPOL; |
| |
| if (xfer->speed_hz) |
| speed_hz = xfer->speed_hz; |
| |
| if (speed_hz > OCTEON_SPI_MAX_CLOCK_HZ) |
| speed_hz = OCTEON_SPI_MAX_CLOCK_HZ; |
| |
| clkdiv = octeon_get_io_clock_rate() / (2 * speed_hz); |
| |
| mpi_cfg.u64 = 0; |
| |
| mpi_cfg.s.clkdiv = clkdiv; |
| mpi_cfg.s.cshi = (mode & SPI_CS_HIGH) ? 1 : 0; |
| mpi_cfg.s.lsbfirst = (mode & SPI_LSB_FIRST) ? 1 : 0; |
| mpi_cfg.s.wireor = (mode & SPI_3WIRE) ? 1 : 0; |
| mpi_cfg.s.idlelo = cpha != cpol; |
| mpi_cfg.s.cslate = cpha ? 1 : 0; |
| mpi_cfg.s.enable = 1; |
| |
| if (msg_setup->chip_select < 4) |
| p->cs_enax |= 1ull << (12 + msg_setup->chip_select); |
| mpi_cfg.u64 |= p->cs_enax; |
| |
| if (mpi_cfg.u64 != p->last_cfg) { |
| p->last_cfg = mpi_cfg.u64; |
| cvmx_write_csr(p->register_base + OCTEON_SPI_CFG, mpi_cfg.u64); |
| } |
| tx_buf = xfer->tx_buf; |
| rx_buf = xfer->rx_buf; |
| len = xfer->len; |
| while (len > OCTEON_SPI_MAX_BYTES) { |
| for (i = 0; i < OCTEON_SPI_MAX_BYTES; i++) { |
| u8 d; |
| if (tx_buf) |
| d = *tx_buf++; |
| else |
| d = 0; |
| cvmx_write_csr(p->register_base + OCTEON_SPI_DAT0 + (8 * i), d); |
| } |
| mpi_tx.u64 = 0; |
| mpi_tx.s.csid = msg_setup->chip_select; |
| mpi_tx.s.leavecs = 1; |
| mpi_tx.s.txnum = tx_buf ? OCTEON_SPI_MAX_BYTES : 0; |
| mpi_tx.s.totnum = OCTEON_SPI_MAX_BYTES; |
| cvmx_write_csr(p->register_base + OCTEON_SPI_TX, mpi_tx.u64); |
| |
| octeon_spi_wait_ready(p); |
| if (rx_buf) |
| for (i = 0; i < OCTEON_SPI_MAX_BYTES; i++) { |
| u64 v = cvmx_read_csr(p->register_base + OCTEON_SPI_DAT0 + (8 * i)); |
| *rx_buf++ = (u8)v; |
| } |
| len -= OCTEON_SPI_MAX_BYTES; |
| } |
| |
| for (i = 0; i < len; i++) { |
| u8 d; |
| if (tx_buf) |
| d = *tx_buf++; |
| else |
| d = 0; |
| cvmx_write_csr(p->register_base + OCTEON_SPI_DAT0 + (8 * i), d); |
| } |
| |
| mpi_tx.u64 = 0; |
| mpi_tx.s.csid = msg_setup->chip_select; |
| if (last_xfer) |
| mpi_tx.s.leavecs = xfer->cs_change; |
| else |
| mpi_tx.s.leavecs = !xfer->cs_change; |
| mpi_tx.s.txnum = tx_buf ? len : 0; |
| mpi_tx.s.totnum = len; |
| cvmx_write_csr(p->register_base + OCTEON_SPI_TX, mpi_tx.u64); |
| |
| octeon_spi_wait_ready(p); |
| if (rx_buf) |
| for (i = 0; i < len; i++) { |
| u64 v = cvmx_read_csr(p->register_base + OCTEON_SPI_DAT0 + (8 * i)); |
| *rx_buf++ = (u8)v; |
| } |
| |
| if (xfer->delay_usecs) |
| udelay(xfer->delay_usecs); |
| |
| return xfer->len; |
| } |
| |
| static int octeon_spi_transfer_one_message(struct spi_master *master, |
| struct spi_message *msg) |
| { |
| struct octeon_spi *p = spi_master_get_devdata(master); |
| unsigned int total_len = 0; |
| int status = 0; |
| struct spi_transfer *xfer; |
| |
| /* |
| * We better have set the configuration via a call to .setup |
| * before we get here. |
| */ |
| if (spi_get_ctldata(msg->spi) == NULL) { |
| status = -EINVAL; |
| goto err; |
| } |
| |
| list_for_each_entry(xfer, &msg->transfers, transfer_list) { |
| bool last_xfer = &xfer->transfer_list == msg->transfers.prev; |
| int r = octeon_spi_do_transfer(p, msg, xfer, last_xfer); |
| if (r < 0) { |
| status = r; |
| goto err; |
| } |
| total_len += r; |
| } |
| err: |
| msg->status = status; |
| msg->actual_length = total_len; |
| spi_finalize_current_message(master); |
| return status; |
| } |
| |
| static struct octeon_spi_setup *octeon_spi_new_setup(struct spi_device *spi) |
| { |
| struct octeon_spi_setup *setup = kzalloc(sizeof(*setup), GFP_KERNEL); |
| if (!setup) |
| return NULL; |
| |
| setup->max_speed_hz = spi->max_speed_hz; |
| setup->chip_select = spi->chip_select; |
| setup->mode = spi->mode; |
| setup->bits_per_word = spi->bits_per_word; |
| return setup; |
| } |
| |
| static int octeon_spi_setup(struct spi_device *spi) |
| { |
| struct octeon_spi_setup *new_setup; |
| struct octeon_spi_setup *old_setup = spi_get_ctldata(spi); |
| |
| new_setup = octeon_spi_new_setup(spi); |
| if (!new_setup) |
| return -ENOMEM; |
| |
| spi_set_ctldata(spi, new_setup); |
| kfree(old_setup); |
| |
| return 0; |
| } |
| |
| static void octeon_spi_cleanup(struct spi_device *spi) |
| { |
| struct octeon_spi_setup *old_setup = spi_get_ctldata(spi); |
| spi_set_ctldata(spi, NULL); |
| kfree(old_setup); |
| } |
| |
| static int octeon_spi_probe(struct platform_device *pdev) |
| { |
| struct resource *res_mem; |
| struct spi_master *master; |
| struct octeon_spi *p; |
| int err = -ENOENT; |
| |
| master = spi_alloc_master(&pdev->dev, sizeof(struct octeon_spi)); |
| if (!master) |
| return -ENOMEM; |
| p = spi_master_get_devdata(master); |
| platform_set_drvdata(pdev, master); |
| |
| res_mem = platform_get_resource(pdev, IORESOURCE_MEM, 0); |
| |
| if (res_mem == NULL) { |
| dev_err(&pdev->dev, "found no memory resource\n"); |
| err = -ENXIO; |
| goto fail; |
| } |
| if (!devm_request_mem_region(&pdev->dev, res_mem->start, |
| resource_size(res_mem), res_mem->name)) { |
| dev_err(&pdev->dev, "request_mem_region failed\n"); |
| goto fail; |
| } |
| p->register_base = (u64)devm_ioremap(&pdev->dev, res_mem->start, |
| resource_size(res_mem)); |
| |
| master->num_chipselect = 4; |
| master->mode_bits = SPI_CPHA | |
| SPI_CPOL | |
| SPI_CS_HIGH | |
| SPI_LSB_FIRST | |
| SPI_3WIRE; |
| |
| master->setup = octeon_spi_setup; |
| master->cleanup = octeon_spi_cleanup; |
| master->transfer_one_message = octeon_spi_transfer_one_message; |
| master->bits_per_word_mask = SPI_BPW_MASK(8); |
| |
| master->dev.of_node = pdev->dev.of_node; |
| err = devm_spi_register_master(&pdev->dev, master); |
| if (err) { |
| dev_err(&pdev->dev, "register master failed: %d\n", err); |
| goto fail; |
| } |
| |
| dev_info(&pdev->dev, "OCTEON SPI bus driver\n"); |
| |
| return 0; |
| fail: |
| spi_master_put(master); |
| return err; |
| } |
| |
| static int octeon_spi_remove(struct platform_device *pdev) |
| { |
| struct spi_master *master = platform_get_drvdata(pdev); |
| struct octeon_spi *p = spi_master_get_devdata(master); |
| u64 register_base = p->register_base; |
| |
| /* Clear the CSENA* and put everything in a known state. */ |
| cvmx_write_csr(register_base + OCTEON_SPI_CFG, 0); |
| |
| return 0; |
| } |
| |
| static struct of_device_id octeon_spi_match[] = { |
| { .compatible = "cavium,octeon-3010-spi", }, |
| {}, |
| }; |
| MODULE_DEVICE_TABLE(of, octeon_spi_match); |
| |
| static struct platform_driver octeon_spi_driver = { |
| .driver = { |
| .name = "spi-octeon", |
| .owner = THIS_MODULE, |
| .of_match_table = octeon_spi_match, |
| }, |
| .probe = octeon_spi_probe, |
| .remove = octeon_spi_remove, |
| }; |
| |
| module_platform_driver(octeon_spi_driver); |
| |
| MODULE_DESCRIPTION("Cavium, Inc. OCTEON SPI bus driver"); |
| MODULE_AUTHOR("David Daney"); |
| MODULE_LICENSE("GPL"); |