| /* linux/drivers/spi/spi_s3c24xx.c |
| * |
| * Copyright (c) 2006 Ben Dooks |
| * Copyright (c) 2006 Simtec Electronics |
| * Ben Dooks <ben@simtec.co.uk> |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License version 2 as |
| * published by the Free Software Foundation. |
| * |
| */ |
| |
| #include <linux/init.h> |
| #include <linux/spinlock.h> |
| #include <linux/workqueue.h> |
| #include <linux/interrupt.h> |
| #include <linux/delay.h> |
| #include <linux/errno.h> |
| #include <linux/err.h> |
| #include <linux/clk.h> |
| #include <linux/platform_device.h> |
| |
| #include <linux/spi/spi.h> |
| #include <linux/spi/spi_bitbang.h> |
| |
| #include <asm/io.h> |
| #include <asm/dma.h> |
| #include <asm/hardware.h> |
| |
| #include <asm/arch/regs-gpio.h> |
| #include <asm/plat-s3c24xx/regs-spi.h> |
| #include <asm/arch/spi.h> |
| |
| struct s3c24xx_spi { |
| /* bitbang has to be first */ |
| struct spi_bitbang bitbang; |
| struct completion done; |
| |
| void __iomem *regs; |
| int irq; |
| int len; |
| int count; |
| |
| void (*set_cs)(struct s3c2410_spi_info *spi, |
| int cs, int pol); |
| |
| /* data buffers */ |
| const unsigned char *tx; |
| unsigned char *rx; |
| |
| struct clk *clk; |
| struct resource *ioarea; |
| struct spi_master *master; |
| struct spi_device *curdev; |
| struct device *dev; |
| struct s3c2410_spi_info *pdata; |
| }; |
| |
| #define SPCON_DEFAULT (S3C2410_SPCON_MSTR | S3C2410_SPCON_SMOD_INT) |
| #define SPPIN_DEFAULT (S3C2410_SPPIN_KEEP) |
| |
| static inline struct s3c24xx_spi *to_hw(struct spi_device *sdev) |
| { |
| return spi_master_get_devdata(sdev->master); |
| } |
| |
| static void s3c24xx_spi_gpiocs(struct s3c2410_spi_info *spi, int cs, int pol) |
| { |
| s3c2410_gpio_setpin(spi->pin_cs, pol); |
| } |
| |
| static void s3c24xx_spi_chipsel(struct spi_device *spi, int value) |
| { |
| struct s3c24xx_spi *hw = to_hw(spi); |
| unsigned int cspol = spi->mode & SPI_CS_HIGH ? 1 : 0; |
| unsigned int spcon; |
| |
| switch (value) { |
| case BITBANG_CS_INACTIVE: |
| hw->set_cs(hw->pdata, spi->chip_select, cspol^1); |
| break; |
| |
| case BITBANG_CS_ACTIVE: |
| spcon = readb(hw->regs + S3C2410_SPCON); |
| |
| if (spi->mode & SPI_CPHA) |
| spcon |= S3C2410_SPCON_CPHA_FMTB; |
| else |
| spcon &= ~S3C2410_SPCON_CPHA_FMTB; |
| |
| if (spi->mode & SPI_CPOL) |
| spcon |= S3C2410_SPCON_CPOL_HIGH; |
| else |
| spcon &= ~S3C2410_SPCON_CPOL_HIGH; |
| |
| spcon |= S3C2410_SPCON_ENSCK; |
| |
| /* write new configration */ |
| |
| writeb(spcon, hw->regs + S3C2410_SPCON); |
| hw->set_cs(hw->pdata, spi->chip_select, cspol); |
| |
| break; |
| } |
| } |
| |
| static int s3c24xx_spi_setupxfer(struct spi_device *spi, |
| struct spi_transfer *t) |
| { |
| struct s3c24xx_spi *hw = to_hw(spi); |
| unsigned int bpw; |
| unsigned int hz; |
| unsigned int div; |
| |
| bpw = t ? t->bits_per_word : spi->bits_per_word; |
| hz = t ? t->speed_hz : spi->max_speed_hz; |
| |
| if (bpw != 8) { |
| dev_err(&spi->dev, "invalid bits-per-word (%d)\n", bpw); |
| return -EINVAL; |
| } |
| |
| div = clk_get_rate(hw->clk) / hz; |
| |
| /* is clk = pclk / (2 * (pre+1)), or is it |
| * clk = (pclk * 2) / ( pre + 1) */ |
| |
| div /= 2; |
| |
| if (div > 0) |
| div -= 1; |
| |
| if (div > 255) |
| div = 255; |
| |
| dev_dbg(&spi->dev, "setting pre-scaler to %d (hz %d)\n", div, hz); |
| writeb(div, hw->regs + S3C2410_SPPRE); |
| |
| spin_lock(&hw->bitbang.lock); |
| if (!hw->bitbang.busy) { |
| hw->bitbang.chipselect(spi, BITBANG_CS_INACTIVE); |
| /* need to ndelay for 0.5 clocktick ? */ |
| } |
| spin_unlock(&hw->bitbang.lock); |
| |
| return 0; |
| } |
| |
| /* the spi->mode bits understood by this driver: */ |
| #define MODEBITS (SPI_CPOL | SPI_CPHA | SPI_CS_HIGH) |
| |
| static int s3c24xx_spi_setup(struct spi_device *spi) |
| { |
| int ret; |
| |
| if (!spi->bits_per_word) |
| spi->bits_per_word = 8; |
| |
| if (spi->mode & ~MODEBITS) { |
| dev_dbg(&spi->dev, "setup: unsupported mode bits %x\n", |
| spi->mode & ~MODEBITS); |
| return -EINVAL; |
| } |
| |
| ret = s3c24xx_spi_setupxfer(spi, NULL); |
| if (ret < 0) { |
| dev_err(&spi->dev, "setupxfer returned %d\n", ret); |
| return ret; |
| } |
| |
| dev_dbg(&spi->dev, "%s: mode %d, %u bpw, %d hz\n", |
| __func__, spi->mode, spi->bits_per_word, |
| spi->max_speed_hz); |
| |
| return 0; |
| } |
| |
| static inline unsigned int hw_txbyte(struct s3c24xx_spi *hw, int count) |
| { |
| return hw->tx ? hw->tx[count] : 0; |
| } |
| |
| static int s3c24xx_spi_txrx(struct spi_device *spi, struct spi_transfer *t) |
| { |
| struct s3c24xx_spi *hw = to_hw(spi); |
| |
| dev_dbg(&spi->dev, "txrx: tx %p, rx %p, len %d\n", |
| t->tx_buf, t->rx_buf, t->len); |
| |
| hw->tx = t->tx_buf; |
| hw->rx = t->rx_buf; |
| hw->len = t->len; |
| hw->count = 0; |
| |
| init_completion(&hw->done); |
| |
| /* send the first byte */ |
| writeb(hw_txbyte(hw, 0), hw->regs + S3C2410_SPTDAT); |
| |
| wait_for_completion(&hw->done); |
| |
| return hw->count; |
| } |
| |
| static irqreturn_t s3c24xx_spi_irq(int irq, void *dev) |
| { |
| struct s3c24xx_spi *hw = dev; |
| unsigned int spsta = readb(hw->regs + S3C2410_SPSTA); |
| unsigned int count = hw->count; |
| |
| if (spsta & S3C2410_SPSTA_DCOL) { |
| dev_dbg(hw->dev, "data-collision\n"); |
| complete(&hw->done); |
| goto irq_done; |
| } |
| |
| if (!(spsta & S3C2410_SPSTA_READY)) { |
| dev_dbg(hw->dev, "spi not ready for tx?\n"); |
| complete(&hw->done); |
| goto irq_done; |
| } |
| |
| hw->count++; |
| |
| if (hw->rx) |
| hw->rx[count] = readb(hw->regs + S3C2410_SPRDAT); |
| |
| count++; |
| |
| if (count < hw->len) |
| writeb(hw_txbyte(hw, count), hw->regs + S3C2410_SPTDAT); |
| else |
| complete(&hw->done); |
| |
| irq_done: |
| return IRQ_HANDLED; |
| } |
| |
| static int __init s3c24xx_spi_probe(struct platform_device *pdev) |
| { |
| struct s3c2410_spi_info *pdata; |
| struct s3c24xx_spi *hw; |
| struct spi_master *master; |
| struct resource *res; |
| int err = 0; |
| |
| master = spi_alloc_master(&pdev->dev, sizeof(struct s3c24xx_spi)); |
| if (master == NULL) { |
| dev_err(&pdev->dev, "No memory for spi_master\n"); |
| err = -ENOMEM; |
| goto err_nomem; |
| } |
| |
| hw = spi_master_get_devdata(master); |
| memset(hw, 0, sizeof(struct s3c24xx_spi)); |
| |
| hw->master = spi_master_get(master); |
| hw->pdata = pdata = pdev->dev.platform_data; |
| hw->dev = &pdev->dev; |
| |
| if (pdata == NULL) { |
| dev_err(&pdev->dev, "No platform data supplied\n"); |
| err = -ENOENT; |
| goto err_no_pdata; |
| } |
| |
| platform_set_drvdata(pdev, hw); |
| init_completion(&hw->done); |
| |
| /* setup the master state. */ |
| |
| master->num_chipselect = hw->pdata->num_cs; |
| |
| /* setup the state for the bitbang driver */ |
| |
| hw->bitbang.master = hw->master; |
| hw->bitbang.setup_transfer = s3c24xx_spi_setupxfer; |
| hw->bitbang.chipselect = s3c24xx_spi_chipsel; |
| hw->bitbang.txrx_bufs = s3c24xx_spi_txrx; |
| hw->bitbang.master->setup = s3c24xx_spi_setup; |
| |
| dev_dbg(hw->dev, "bitbang at %p\n", &hw->bitbang); |
| |
| /* find and map our resources */ |
| |
| res = platform_get_resource(pdev, IORESOURCE_MEM, 0); |
| if (res == NULL) { |
| dev_err(&pdev->dev, "Cannot get IORESOURCE_MEM\n"); |
| err = -ENOENT; |
| goto err_no_iores; |
| } |
| |
| hw->ioarea = request_mem_region(res->start, (res->end - res->start)+1, |
| pdev->name); |
| |
| if (hw->ioarea == NULL) { |
| dev_err(&pdev->dev, "Cannot reserve region\n"); |
| err = -ENXIO; |
| goto err_no_iores; |
| } |
| |
| hw->regs = ioremap(res->start, (res->end - res->start)+1); |
| if (hw->regs == NULL) { |
| dev_err(&pdev->dev, "Cannot map IO\n"); |
| err = -ENXIO; |
| goto err_no_iomap; |
| } |
| |
| hw->irq = platform_get_irq(pdev, 0); |
| if (hw->irq < 0) { |
| dev_err(&pdev->dev, "No IRQ specified\n"); |
| err = -ENOENT; |
| goto err_no_irq; |
| } |
| |
| err = request_irq(hw->irq, s3c24xx_spi_irq, 0, pdev->name, hw); |
| if (err) { |
| dev_err(&pdev->dev, "Cannot claim IRQ\n"); |
| goto err_no_irq; |
| } |
| |
| hw->clk = clk_get(&pdev->dev, "spi"); |
| if (IS_ERR(hw->clk)) { |
| dev_err(&pdev->dev, "No clock for device\n"); |
| err = PTR_ERR(hw->clk); |
| goto err_no_clk; |
| } |
| |
| /* for the moment, permanently enable the clock */ |
| |
| clk_enable(hw->clk); |
| |
| /* program defaults into the registers */ |
| |
| writeb(0xff, hw->regs + S3C2410_SPPRE); |
| writeb(SPPIN_DEFAULT, hw->regs + S3C2410_SPPIN); |
| writeb(SPCON_DEFAULT, hw->regs + S3C2410_SPCON); |
| |
| /* setup any gpio we can */ |
| |
| if (!pdata->set_cs) { |
| hw->set_cs = s3c24xx_spi_gpiocs; |
| |
| s3c2410_gpio_setpin(pdata->pin_cs, 1); |
| s3c2410_gpio_cfgpin(pdata->pin_cs, S3C2410_GPIO_OUTPUT); |
| } else |
| hw->set_cs = pdata->set_cs; |
| |
| /* register our spi controller */ |
| |
| err = spi_bitbang_start(&hw->bitbang); |
| if (err) { |
| dev_err(&pdev->dev, "Failed to register SPI master\n"); |
| goto err_register; |
| } |
| |
| return 0; |
| |
| err_register: |
| clk_disable(hw->clk); |
| clk_put(hw->clk); |
| |
| err_no_clk: |
| free_irq(hw->irq, hw); |
| |
| err_no_irq: |
| iounmap(hw->regs); |
| |
| err_no_iomap: |
| release_resource(hw->ioarea); |
| kfree(hw->ioarea); |
| |
| err_no_iores: |
| err_no_pdata: |
| spi_master_put(hw->master);; |
| |
| err_nomem: |
| return err; |
| } |
| |
| static int __exit s3c24xx_spi_remove(struct platform_device *dev) |
| { |
| struct s3c24xx_spi *hw = platform_get_drvdata(dev); |
| |
| platform_set_drvdata(dev, NULL); |
| |
| spi_unregister_master(hw->master); |
| |
| clk_disable(hw->clk); |
| clk_put(hw->clk); |
| |
| free_irq(hw->irq, hw); |
| iounmap(hw->regs); |
| |
| release_resource(hw->ioarea); |
| kfree(hw->ioarea); |
| |
| spi_master_put(hw->master); |
| return 0; |
| } |
| |
| |
| #ifdef CONFIG_PM |
| |
| static int s3c24xx_spi_suspend(struct platform_device *pdev, pm_message_t msg) |
| { |
| struct s3c24xx_spi *hw = platform_get_drvdata(pdev); |
| |
| clk_disable(hw->clk); |
| return 0; |
| } |
| |
| static int s3c24xx_spi_resume(struct platform_device *pdev) |
| { |
| struct s3c24xx_spi *hw = platform_get_drvdata(pdev); |
| |
| clk_enable(hw->clk); |
| return 0; |
| } |
| |
| #else |
| #define s3c24xx_spi_suspend NULL |
| #define s3c24xx_spi_resume NULL |
| #endif |
| |
| MODULE_ALIAS("platform:s3c2410-spi"); |
| static struct platform_driver s3c24xx_spidrv = { |
| .remove = __exit_p(s3c24xx_spi_remove), |
| .suspend = s3c24xx_spi_suspend, |
| .resume = s3c24xx_spi_resume, |
| .driver = { |
| .name = "s3c2410-spi", |
| .owner = THIS_MODULE, |
| }, |
| }; |
| |
| static int __init s3c24xx_spi_init(void) |
| { |
| return platform_driver_probe(&s3c24xx_spidrv, s3c24xx_spi_probe); |
| } |
| |
| static void __exit s3c24xx_spi_exit(void) |
| { |
| platform_driver_unregister(&s3c24xx_spidrv); |
| } |
| |
| module_init(s3c24xx_spi_init); |
| module_exit(s3c24xx_spi_exit); |
| |
| MODULE_DESCRIPTION("S3C24XX SPI Driver"); |
| MODULE_AUTHOR("Ben Dooks, <ben@simtec.co.uk>"); |
| MODULE_LICENSE("GPL"); |