Skip to content
Snippets Groups Projects
spi-atmel.c 42.3 KiB
Newer Older
  • Learn to ignore specific revisions
  • 	struct spi_master	*master = (struct spi_master *)data;
    	struct atmel_spi	*as = spi_master_get_devdata(master);
    	struct spi_message	*msg;
    	struct spi_transfer	*xfer;
    
    	dev_vdbg(master->dev.parent, "atmel_spi_tasklet_func\n");
    
    	atmel_spi_lock(as);
    
    	xfer = as->current_transfer;
    
    	if (xfer == NULL)
    		/* already been there */
    		goto tasklet_out;
    
    	msg = list_entry(as->queue.next, struct spi_message, queue);
    
    	if (as->current_remaining_bytes == 0) {
    		if (as->done_status < 0) {
    			/* error happened (overrun) */
    			if (atmel_spi_use_dma(as, xfer))
    				atmel_spi_stop_dma(as);
    		} else {
    			/* only update length if no error */
    			msg->actual_length += xfer->len;
    		}
    
    		if (atmel_spi_use_dma(as, xfer))
    			if (!msg->is_dma_mapped)
    				atmel_spi_dma_unmap_xfer(master, xfer);
    
    		if (xfer->delay_usecs)
    			udelay(xfer->delay_usecs);
    
    		if (atmel_spi_xfer_is_last(msg, xfer) || as->done_status < 0) {
    			/* report completed (or erroneous) message */
    			atmel_spi_msg_done(master, as, msg, xfer->cs_change);
    		} else {
    			if (xfer->cs_change) {
    				cs_deactivate(as, msg->spi);
    				udelay(1);
    				cs_activate(as, msg->spi);
    			}
    
    			/*
    			 * Not done yet. Submit the next transfer.
    			 *
    			 * FIXME handle protocol options for xfer
    			 */
    			atmel_spi_dma_next_xfer(master, msg);
    		}
    	} else {
    		/*
    		 * Keep going, we still have data to send in
    		 * the current transfer.
    		 */
    		atmel_spi_dma_next_xfer(master, msg);
    	}
    
    tasklet_out:
    	atmel_spi_unlock(as);
    }
    
    /* Interrupt
     *
     * No need for locking in this Interrupt handler: done_status is the
     * only information modified. What we need is the update of this field
     * before tasklet runs. This is ensured by using barrier.
     */
    static irqreturn_t
    atmel_spi_pio_interrupt(int irq, void *dev_id)
    {
    	struct spi_master	*master = dev_id;
    	struct atmel_spi	*as = spi_master_get_devdata(master);
    	u32			status, pending, imr;
    	struct spi_transfer	*xfer;
    	int			ret = IRQ_NONE;
    
    	imr = spi_readl(as, IMR);
    	status = spi_readl(as, SR);
    	pending = status & imr;
    
    	if (pending & SPI_BIT(OVRES)) {
    		ret = IRQ_HANDLED;
    		spi_writel(as, IDR, SPI_BIT(OVRES));
    		dev_warn(master->dev.parent, "overrun\n");
    
    		/*
    		 * When we get an overrun, we disregard the current
    		 * transfer. Data will not be copied back from any
    		 * bounce buffer and msg->actual_len will not be
    		 * updated with the last xfer.
    		 *
    		 * We will also not process any remaning transfers in
    		 * the message.
    		 *
    		 * All actions are done in tasklet with done_status indication
    		 */
    		as->done_status = -EIO;
    		smp_wmb();
    
    		/* Clear any overrun happening while cleaning up */
    		spi_readl(as, SR);
    
    		tasklet_schedule(&as->tasklet);
    
    	} else if (pending & SPI_BIT(RDRF)) {
    		atmel_spi_lock(as);
    
    		if (as->current_remaining_bytes) {
    			ret = IRQ_HANDLED;
    			xfer = as->current_transfer;
    			atmel_spi_pump_pio_data(as, xfer);
    			if (!as->current_remaining_bytes) {
    				/* no more data to xfer, kick tasklet */
    				spi_writel(as, IDR, pending);
    				tasklet_schedule(&as->tasklet);
    			}
    		}
    
    		atmel_spi_unlock(as);
    	} else {
    		WARN_ONCE(pending, "IRQ not handled, pending = %x\n", pending);
    		ret = IRQ_HANDLED;
    		spi_writel(as, IDR, pending);
    	}
    
    	return ret;
    
    }
    
    static irqreturn_t
    
    atmel_spi_pdc_interrupt(int irq, void *dev_id)
    
    {
    	struct spi_master	*master = dev_id;
    	struct atmel_spi	*as = spi_master_get_devdata(master);
    	struct spi_message	*msg;
    	struct spi_transfer	*xfer;
    	u32			status, pending, imr;
    	int			ret = IRQ_NONE;
    
    
    
    	xfer = as->current_transfer;
    	msg = list_entry(as->queue.next, struct spi_message, queue);
    
    	imr = spi_readl(as, IMR);
    	status = spi_readl(as, SR);
    	pending = status & imr;
    
    	if (pending & SPI_BIT(OVRES)) {
    		int timeout;
    
    		ret = IRQ_HANDLED;
    
    
    		spi_writel(as, IDR, (SPI_BIT(RXBUFF) | SPI_BIT(ENDRX)
    
    				     | SPI_BIT(OVRES)));
    
    		/*
    		 * When we get an overrun, we disregard the current
    		 * transfer. Data will not be copied back from any
    		 * bounce buffer and msg->actual_len will not be
    		 * updated with the last xfer.
    		 *
    		 * We will also not process any remaning transfers in
    		 * the message.
    		 *
    		 * First, stop the transfer and unmap the DMA buffers.
    		 */
    		spi_writel(as, PTCR, SPI_BIT(RXTDIS) | SPI_BIT(TXTDIS));
    		if (!msg->is_dma_mapped)
    			atmel_spi_dma_unmap_xfer(master, xfer);
    
    		/* REVISIT: udelay in irq is unfriendly */
    		if (xfer->delay_usecs)
    			udelay(xfer->delay_usecs);
    
    
    		dev_warn(master->dev.parent, "overrun (%u/%u remaining)\n",
    
    			 spi_readl(as, TCR), spi_readl(as, RCR));
    
    		/*
    		 * Clean up DMA registers and make sure the data
    		 * registers are empty.
    		 */
    		spi_writel(as, RNCR, 0);
    		spi_writel(as, TNCR, 0);
    		spi_writel(as, RCR, 0);
    		spi_writel(as, TCR, 0);
    		for (timeout = 1000; timeout; timeout--)
    			if (spi_readl(as, SR) & SPI_BIT(TXEMPTY))
    				break;
    		if (!timeout)
    
    			dev_warn(master->dev.parent,
    
    				 "timeout waiting for TXEMPTY");
    		while (spi_readl(as, SR) & SPI_BIT(RDRF))
    			spi_readl(as, RDR);
    
    		/* Clear any overrun happening while cleaning up */
    		spi_readl(as, SR);
    
    
    		as->done_status = -EIO;
    		atmel_spi_msg_done(master, as, msg, 0);
    
    	} else if (pending & (SPI_BIT(RXBUFF) | SPI_BIT(ENDRX))) {
    
    		ret = IRQ_HANDLED;
    
    		spi_writel(as, IDR, pending);
    
    
    		if (as->current_remaining_bytes == 0) {
    
    			msg->actual_length += xfer->len;
    
    			if (!msg->is_dma_mapped)
    				atmel_spi_dma_unmap_xfer(master, xfer);
    
    			/* REVISIT: udelay in irq is unfriendly */
    			if (xfer->delay_usecs)
    				udelay(xfer->delay_usecs);
    
    
    			if (atmel_spi_xfer_is_last(msg, xfer)) {
    
    				/* report completed message */
    
    				atmel_spi_msg_done(master, as, msg,
    
    			} else {
    				if (xfer->cs_change) {
    
    					cs_deactivate(as, msg->spi);
    
    					udelay(1);
    
    					cs_activate(as, msg->spi);
    
    				}
    
    				/*
    				 * Not done yet. Submit the next transfer.
    				 *
    				 * FIXME handle protocol options for xfer
    				 */
    
    				atmel_spi_pdc_next_xfer(master, msg);
    
    			}
    		} else {
    			/*
    			 * Keep going, we still have data to send in
    			 * the current transfer.
    			 */
    
    			atmel_spi_pdc_next_xfer(master, msg);
    
    
    	return ret;
    }
    
    static int atmel_spi_setup(struct spi_device *spi)
    {
    	struct atmel_spi	*as;
    
    	struct atmel_spi_device	*asd;
    
    	u32			scbr, csr;
    	unsigned int		bits = spi->bits_per_word;
    
    	unsigned int		npcs_pin;
    	int			ret;
    
    	as = spi_master_get_devdata(spi->master);
    
    	if (as->stopping)
    		return -ESHUTDOWN;
    
    	if (spi->chip_select > spi->master->num_chipselect) {
    		dev_dbg(&spi->dev,
    				"setup: invalid chipselect %u (%u defined)\n",
    				spi->chip_select, spi->master->num_chipselect);
    		return -EINVAL;
    	}
    
    	if (bits < 8 || bits > 16) {
    		dev_dbg(&spi->dev,
    				"setup: invalid bits_per_word %u (8 to 16)\n",
    				bits);
    		return -EINVAL;
    	}
    
    
    	/* see notes above re chipselect */
    
    			&& spi->chip_select == 0
    			&& (spi->mode & SPI_CS_HIGH)) {
    		dev_dbg(&spi->dev, "setup: can't be active-high\n");
    		return -EINVAL;
    	}
    
    
    	/* v1 chips start out at half the peripheral bus speed. */
    
    	bus_hz = clk_get_rate(as->clk);
    
    	if (spi->max_speed_hz) {
    
    		/*
    		 * Calculate the lowest divider that satisfies the
    		 * constraint, assuming div32/fdiv/mbz == 0.
    		 */
    		scbr = DIV_ROUND_UP(bus_hz, spi->max_speed_hz);
    
    		/*
    		 * If the resulting divider doesn't fit into the
    		 * register bitfield, we can't satisfy the constraint.
    		 */
    
    		if (scbr >= (1 << SPI_SCBR_SIZE)) {
    
    David Brownell's avatar
    David Brownell committed
    			dev_dbg(&spi->dev,
    				"setup: %d Hz too slow, scbr %u; min %ld Hz\n",
    				spi->max_speed_hz, scbr, bus_hz/255);
    
    			return -EINVAL;
    		}
    	} else
    
    		/* speed zero means "as slow as possible" */
    
    		scbr = 0xff;
    
    	csr = SPI_BF(SCBR, scbr) | SPI_BF(BITS, bits - 8);
    	if (spi->mode & SPI_CPOL)
    		csr |= SPI_BIT(CPOL);
    	if (!(spi->mode & SPI_CPHA))
    		csr |= SPI_BIT(NCPHA);
    
    
    	/* DLYBS is mostly irrelevant since we manage chipselect using GPIOs.
    	 *
    	 * DLYBCT would add delays between words, slowing down transfers.
    	 * It could potentially be useful to cope with DMA bottlenecks, but
    	 * in those cases it's probably best to just use a lower bitrate.
    	 */
    	csr |= SPI_BF(DLYBS, 0);
    	csr |= SPI_BF(DLYBCT, 0);
    
    
    	/* chipselect must have been muxed as GPIO (e.g. in board setup) */
    	npcs_pin = (unsigned int)spi->controller_data;
    
    
    	if (gpio_is_valid(spi->cs_gpio))
    		npcs_pin = spi->cs_gpio;
    
    
    	asd = spi->controller_state;
    	if (!asd) {
    		asd = kzalloc(sizeof(struct atmel_spi_device), GFP_KERNEL);
    		if (!asd)
    			return -ENOMEM;
    
    
    		ret = gpio_request(npcs_pin, dev_name(&spi->dev));
    
    			return ret;
    
    		}
    
    		asd->npcs_pin = npcs_pin;
    		spi->controller_state = asd;
    
    		gpio_direction_output(npcs_pin, !(spi->mode & SPI_CS_HIGH));
    
    		if (as->stay == spi)
    			as->stay = NULL;
    		cs_deactivate(as, spi);
    
    	dev_dbg(&spi->dev,
    		"setup: %lu Hz bpw %u mode 0x%x -> csr%d %08x\n",
    
    		bus_hz / scbr, bits, spi->mode, spi->chip_select, csr);
    
    		spi_writel(as, CSR0 + 4 * spi->chip_select, csr);
    
    
    	return 0;
    }
    
    static int atmel_spi_transfer(struct spi_device *spi, struct spi_message *msg)
    {
    	struct atmel_spi	*as;
    	struct spi_transfer	*xfer;
    
    	struct device		*controller = spi->master->dev.parent;
    
    	u8			bits;
    	struct atmel_spi_device	*asd;
    
    
    	as = spi_master_get_devdata(spi->master);
    
    	dev_dbg(controller, "new message %p submitted for %s\n",
    
    	if (unlikely(list_empty(&msg->transfers)))
    
    		return -EINVAL;
    
    	if (as->stopping)
    		return -ESHUTDOWN;
    
    	list_for_each_entry(xfer, &msg->transfers, transfer_list) {
    
    		if (!(xfer->tx_buf || xfer->rx_buf) && xfer->len) {
    
    			dev_dbg(&spi->dev, "missing rx or tx buf\n");
    			return -EINVAL;
    		}
    
    
    		if (xfer->bits_per_word) {
    			asd = spi->controller_state;
    			bits = (asd->csr >> 4) & 0xf;
    			if (bits != xfer->bits_per_word - 8) {
    				dev_dbg(&spi->dev, "you can't yet change "
    
    					 "bits_per_word in transfers\n");
    
    		if (xfer->bits_per_word > 8) {
    			if (xfer->len % 2) {
    				dev_dbg(&spi->dev, "buffer len should be 16 bits aligned\n");
    				return -EINVAL;
    			}
    		}
    
    
    		/* FIXME implement these protocol options!! */
    
    		if (xfer->speed_hz < spi->max_speed_hz) {
    			dev_dbg(&spi->dev, "can't change speed in transfer\n");
    
    			return -ENOPROTOOPT;
    		}
    
    
    David Brownell's avatar
    David Brownell committed
    		/*
    		 * DMA map early, for performance (empties dcache ASAP) and
    
    		 * better fault reporting.
    
    		if ((!msg->is_dma_mapped) && (atmel_spi_use_dma(as, xfer)
    			|| as->use_pdc)) {
    
    David Brownell's avatar
    David Brownell committed
    			if (atmel_spi_dma_map_xfer(as, xfer) < 0)
    				return -ENOMEM;
    		}
    
    	list_for_each_entry(xfer, &msg->transfers, transfer_list) {
    		dev_dbg(controller,
    			"  xfer %p: len %u tx %p/%08x rx %p/%08x\n",
    			xfer, xfer->len,
    			xfer->tx_buf, xfer->tx_dma,
    			xfer->rx_buf, xfer->rx_dma);
    	}
    
    
    	msg->status = -EINPROGRESS;
    	msg->actual_length = 0;
    
    
    	list_add_tail(&msg->queue, &as->queue);
    	if (!as->current_transfer)
    		atmel_spi_next_message(spi->master);
    
    static void atmel_spi_cleanup(struct spi_device *spi)
    
    	struct atmel_spi	*as = spi_master_get_devdata(spi->master);
    
    	struct atmel_spi_device	*asd = spi->controller_state;
    
    	unsigned		gpio = (unsigned) spi->controller_data;
    
    
    	if (as->stay == spi) {
    		as->stay = NULL;
    		cs_deactivate(as, spi);
    	}
    
    	spi->controller_state = NULL;
    
    static inline unsigned int atmel_get_version(struct atmel_spi *as)
    {
    	return spi_readl(as, VERSION) & 0x00000fff;
    }
    
    static void atmel_get_caps(struct atmel_spi *as)
    {
    	unsigned int version;
    
    	version = atmel_get_version(as);
    	dev_info(&as->pdev->dev, "version: 0x%x\n", version);
    
    	as->caps.is_spi2 = version > 0x121;
    	as->caps.has_wdrbt = version >= 0x210;
    	as->caps.has_dma_support = version >= 0x212;
    }
    
    
    /*-------------------------------------------------------------------------*/
    
    
    static int atmel_spi_probe(struct platform_device *pdev)
    
    {
    	struct resource		*regs;
    	int			irq;
    	struct clk		*clk;
    	int			ret;
    	struct spi_master	*master;
    	struct atmel_spi	*as;
    
    	regs = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    	if (!regs)
    		return -ENXIO;
    
    	irq = platform_get_irq(pdev, 0);
    	if (irq < 0)
    		return irq;
    
    	clk = clk_get(&pdev->dev, "spi_clk");
    	if (IS_ERR(clk))
    		return PTR_ERR(clk);
    
    	/* setup spi core then atmel-specific driver state */
    	ret = -ENOMEM;
    	master = spi_alloc_master(&pdev->dev, sizeof *as);
    	if (!master)
    		goto out_free;
    
    
    	/* the spi->mode bits understood by this driver: */
    	master->mode_bits = SPI_CPOL | SPI_CPHA | SPI_CS_HIGH;
    
    
    	master->dev.of_node = pdev->dev.of_node;
    
    	master->bus_num = pdev->id;
    
    	master->num_chipselect = master->dev.of_node ? 0 : 4;
    
    	master->setup = atmel_spi_setup;
    	master->transfer = atmel_spi_transfer;
    	master->cleanup = atmel_spi_cleanup;
    	platform_set_drvdata(pdev, master);
    
    	as = spi_master_get_devdata(master);
    
    
    David Brownell's avatar
    David Brownell committed
    	/*
    	 * Scratch buffer is used for throwaway rx and tx data.
    	 * It's coherent to minimize dcache pollution.
    	 */
    
    	as->buffer = dma_alloc_coherent(&pdev->dev, BUFFER_SIZE,
    					&as->buffer_dma, GFP_KERNEL);
    	if (!as->buffer)
    		goto out_free;
    
    	spin_lock_init(&as->lock);
    	INIT_LIST_HEAD(&as->queue);
    
    	as->pdev = pdev;
    
    	as->regs = ioremap(regs->start, resource_size(regs));
    
    	if (!as->regs)
    		goto out_free_buffer;
    
    	as->phybase = regs->start;
    
    	as->irq = irq;
    	as->clk = clk;
    
    
    	as->use_dma = false;
    	as->use_pdc = false;
    	if (as->caps.has_dma_support) {
    		if (atmel_spi_configure_dma(as) == 0)
    			as->use_dma = true;
    	} else {
    		as->use_pdc = true;
    	}
    
    	if (as->caps.has_dma_support && !as->use_dma)
    		dev_info(&pdev->dev, "Atmel SPI Controller using PIO only\n");
    
    	if (as->use_pdc) {
    		ret = request_irq(irq, atmel_spi_pdc_interrupt, 0,
    					dev_name(&pdev->dev), master);
    	} else {
    		tasklet_init(&as->tasklet, atmel_spi_tasklet_func,
    					(unsigned long)master);
    
    		ret = request_irq(irq, atmel_spi_pio_interrupt, 0,
    					dev_name(&pdev->dev), master);
    	}
    
    	if (ret)
    		goto out_unmap_regs;
    
    	/* Initialize the hardware */
    	clk_enable(clk);
    	spi_writel(as, CR, SPI_BIT(SWRST));
    
    	spi_writel(as, CR, SPI_BIT(SWRST)); /* AT91SAM9263 Rev B workaround */
    
    	if (as->caps.has_wdrbt) {
    		spi_writel(as, MR, SPI_BIT(WDRBT) | SPI_BIT(MODFDIS)
    				| SPI_BIT(MSTR));
    	} else {
    		spi_writel(as, MR, SPI_BIT(MSTR) | SPI_BIT(MODFDIS));
    	}
    
    
    	if (as->use_pdc)
    		spi_writel(as, PTCR, SPI_BIT(RXTDIS) | SPI_BIT(TXTDIS));
    
    	spi_writel(as, CR, SPI_BIT(SPIEN));
    
    	/* go! */
    	dev_info(&pdev->dev, "Atmel SPI Controller at 0x%08lx (irq %d)\n",
    			(unsigned long)regs->start, irq);
    
    	ret = spi_register_master(master);
    	if (ret)
    
    		goto out_free_dma;
    
    out_free_dma:
    	if (as->use_dma)
    		atmel_spi_release_dma(as);
    
    
    	spi_writel(as, CR, SPI_BIT(SWRST));
    
    	spi_writel(as, CR, SPI_BIT(SWRST)); /* AT91SAM9263 Rev B workaround */
    
    	clk_disable(clk);
    	free_irq(irq, master);
    out_unmap_regs:
    	iounmap(as->regs);
    out_free_buffer:
    
    	if (!as->use_pdc)
    		tasklet_kill(&as->tasklet);
    
    	dma_free_coherent(&pdev->dev, BUFFER_SIZE, as->buffer,
    			as->buffer_dma);
    out_free:
    	clk_put(clk);
    	spi_master_put(master);
    	return ret;
    }
    
    
    static int atmel_spi_remove(struct platform_device *pdev)
    
    {
    	struct spi_master	*master = platform_get_drvdata(pdev);
    	struct atmel_spi	*as = spi_master_get_devdata(master);
    	struct spi_message	*msg;
    
    	struct spi_transfer	*xfer;
    
    
    	/* reset the hardware and block queue progress */
    	spin_lock_irq(&as->lock);
    	as->stopping = 1;
    
    	if (as->use_dma) {
    		atmel_spi_stop_dma(as);
    		atmel_spi_release_dma(as);
    	}
    
    
    	spi_writel(as, CR, SPI_BIT(SWRST));
    
    	spi_writel(as, CR, SPI_BIT(SWRST)); /* AT91SAM9263 Rev B workaround */
    
    	spi_readl(as, SR);
    	spin_unlock_irq(&as->lock);
    
    	/* Terminate remaining queued transfers */
    	list_for_each_entry(msg, &as->queue, queue) {
    
    		list_for_each_entry(xfer, &msg->transfers, transfer_list) {
    
    			if (!msg->is_dma_mapped
    				&& (atmel_spi_use_dma(as, xfer)
    					|| as->use_pdc))
    
    				atmel_spi_dma_unmap_xfer(master, xfer);
    		}
    
    		msg->status = -ESHUTDOWN;
    		msg->complete(msg->context);
    	}
    
    
    	if (!as->use_pdc)
    		tasklet_kill(&as->tasklet);
    
    	dma_free_coherent(&pdev->dev, BUFFER_SIZE, as->buffer,
    			as->buffer_dma);
    
    	clk_disable(as->clk);
    	clk_put(as->clk);
    	free_irq(as->irq, master);
    	iounmap(as->regs);
    
    	spi_unregister_master(master);
    
    	return 0;
    }
    
    #ifdef	CONFIG_PM
    
    static int atmel_spi_suspend(struct platform_device *pdev, pm_message_t mesg)
    {
    	struct spi_master	*master = platform_get_drvdata(pdev);
    	struct atmel_spi	*as = spi_master_get_devdata(master);
    
    	clk_disable(as->clk);
    	return 0;
    }
    
    static int atmel_spi_resume(struct platform_device *pdev)
    {
    	struct spi_master	*master = platform_get_drvdata(pdev);
    	struct atmel_spi	*as = spi_master_get_devdata(master);
    
    	clk_enable(as->clk);
    	return 0;
    }
    
    #else
    #define	atmel_spi_suspend	NULL
    #define	atmel_spi_resume	NULL
    #endif
    
    
    #if defined(CONFIG_OF)
    static const struct of_device_id atmel_spi_dt_ids[] = {
    	{ .compatible = "atmel,at91rm9200-spi" },
    	{ /* sentinel */ }
    };
    
    MODULE_DEVICE_TABLE(of, atmel_spi_dt_ids);
    #endif
    
    
    static struct platform_driver atmel_spi_driver = {
    	.driver		= {
    		.name	= "atmel_spi",
    		.owner	= THIS_MODULE,
    
    		.of_match_table	= of_match_ptr(atmel_spi_dt_ids),
    
    	},
    	.suspend	= atmel_spi_suspend,
    	.resume		= atmel_spi_resume,
    
    	.probe		= atmel_spi_probe,
    
    module_platform_driver(atmel_spi_driver);
    
    
    MODULE_DESCRIPTION("Atmel AT32/AT91 SPI Controller driver");
    
    MODULE_AUTHOR("Haavard Skinnemoen (Atmel)");
    
    MODULE_LICENSE("GPL");
    
    MODULE_ALIAS("platform:atmel_spi");