2019-05-27 06:55:01 +00:00
|
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
2006-05-20 22:00:15 +00:00
|
|
|
/*
|
2010-10-12 10:18:31 +00:00
|
|
|
* Freescale SPI controller driver.
|
2006-05-20 22:00:15 +00:00
|
|
|
*
|
|
|
|
* Maintainer: Kumar Gala
|
|
|
|
*
|
|
|
|
* Copyright (C) 2006 Polycom, Inc.
|
2010-10-12 10:18:31 +00:00
|
|
|
* Copyright 2010 Freescale Semiconductor, Inc.
|
2006-05-20 22:00:15 +00:00
|
|
|
*
|
2009-10-12 16:49:27 +00:00
|
|
|
* CPM SPI and QE buffer descriptors mode support:
|
|
|
|
* Copyright (c) 2009 MontaVista Software, Inc.
|
|
|
|
* Author: Anton Vorontsov <avorontsov@ru.mvista.com>
|
|
|
|
*
|
2013-02-15 15:52:26 +00:00
|
|
|
* GRLIB support:
|
|
|
|
* Copyright (c) 2012 Aeroflex Gaisler AB.
|
|
|
|
* Author: Andreas Larsson <andreas@gaisler.com>
|
2006-05-20 22:00:15 +00:00
|
|
|
*/
|
|
|
|
#include <linux/delay.h>
|
2009-10-12 16:49:27 +00:00
|
|
|
#include <linux/dma-mapping.h>
|
2014-09-29 02:57:06 +00:00
|
|
|
#include <linux/fsl_devices.h>
|
2019-08-04 00:35:39 +00:00
|
|
|
#include <linux/gpio/consumer.h>
|
2014-09-29 02:57:06 +00:00
|
|
|
#include <linux/interrupt.h>
|
|
|
|
#include <linux/irq.h>
|
|
|
|
#include <linux/kernel.h>
|
2009-10-12 16:49:27 +00:00
|
|
|
#include <linux/mm.h>
|
2014-09-29 02:57:06 +00:00
|
|
|
#include <linux/module.h>
|
2009-10-12 16:49:27 +00:00
|
|
|
#include <linux/mutex.h>
|
2009-03-31 22:24:37 +00:00
|
|
|
#include <linux/of.h>
|
2013-02-15 15:52:21 +00:00
|
|
|
#include <linux/of_address.h>
|
|
|
|
#include <linux/of_irq.h>
|
2014-09-29 02:57:06 +00:00
|
|
|
#include <linux/of_platform.h>
|
|
|
|
#include <linux/platform_device.h>
|
|
|
|
#include <linux/spi/spi.h>
|
|
|
|
#include <linux/spi/spi_bitbang.h>
|
|
|
|
#include <linux/types.h>
|
2006-05-20 22:00:15 +00:00
|
|
|
|
2019-03-06 10:32:05 +00:00
|
|
|
#ifdef CONFIG_FSL_SOC
|
|
|
|
#include <sysdev/fsl_soc.h>
|
|
|
|
#endif
|
|
|
|
|
|
|
|
/* Specific to the MPC8306/MPC8309 */
|
|
|
|
#define IMMR_SPI_CS_OFFSET 0x14c
|
|
|
|
#define SPI_BOOT_SEL_BIT 0x80000000
|
|
|
|
|
2011-06-06 07:16:30 +00:00
|
|
|
#include "spi-fsl-lib.h"
|
2013-02-15 15:52:21 +00:00
|
|
|
#include "spi-fsl-cpm.h"
|
|
|
|
#include "spi-fsl-spi.h"
|
2006-05-20 22:00:15 +00:00
|
|
|
|
2013-02-15 15:52:24 +00:00
|
|
|
#define TYPE_FSL 0
|
2013-02-15 15:52:26 +00:00
|
|
|
#define TYPE_GRLIB 1
|
2013-02-15 15:52:24 +00:00
|
|
|
|
|
|
|
struct fsl_spi_match_data {
|
|
|
|
int type;
|
|
|
|
};
|
|
|
|
|
|
|
|
static struct fsl_spi_match_data of_fsl_spi_fsl_config = {
|
|
|
|
.type = TYPE_FSL,
|
|
|
|
};
|
|
|
|
|
2013-02-15 15:52:26 +00:00
|
|
|
static struct fsl_spi_match_data of_fsl_spi_grlib_config = {
|
|
|
|
.type = TYPE_GRLIB,
|
|
|
|
};
|
|
|
|
|
2014-06-03 12:03:59 +00:00
|
|
|
static const struct of_device_id of_fsl_spi_match[] = {
|
2013-02-15 15:52:24 +00:00
|
|
|
{
|
|
|
|
.compatible = "fsl,spi",
|
|
|
|
.data = &of_fsl_spi_fsl_config,
|
|
|
|
},
|
2013-02-15 15:52:26 +00:00
|
|
|
{
|
|
|
|
.compatible = "aeroflexgaisler,spictrl",
|
|
|
|
.data = &of_fsl_spi_grlib_config,
|
|
|
|
},
|
2013-02-15 15:52:24 +00:00
|
|
|
{}
|
|
|
|
};
|
|
|
|
MODULE_DEVICE_TABLE(of, of_fsl_spi_match);
|
|
|
|
|
|
|
|
static int fsl_spi_get_type(struct device *dev)
|
|
|
|
{
|
|
|
|
const struct of_device_id *match;
|
|
|
|
|
|
|
|
if (dev->of_node) {
|
|
|
|
match = of_match_node(of_fsl_spi_match, dev->of_node);
|
|
|
|
if (match && match->data)
|
|
|
|
return ((struct fsl_spi_match_data *)match->data)->type;
|
|
|
|
}
|
|
|
|
return TYPE_FSL;
|
|
|
|
}
|
|
|
|
|
2010-10-12 10:18:31 +00:00
|
|
|
static void fsl_spi_change_mode(struct spi_device *spi)
|
2009-10-12 16:49:24 +00:00
|
|
|
{
|
|
|
|
struct mpc8xxx_spi *mspi = spi_master_get_devdata(spi->master);
|
|
|
|
struct spi_mpc8xxx_cs *cs = spi->controller_state;
|
2020-06-22 16:26:11 +00:00
|
|
|
struct fsl_spi_reg __iomem *reg_base = mspi->reg_base;
|
2010-10-12 10:18:31 +00:00
|
|
|
__be32 __iomem *mode = ®_base->mode;
|
2009-10-12 16:49:24 +00:00
|
|
|
unsigned long flags;
|
|
|
|
|
|
|
|
if (cs->hw_mode == mpc8xxx_spi_read_reg(mode))
|
|
|
|
return;
|
|
|
|
|
|
|
|
/* Turn off IRQs locally to minimize time that SPI is disabled. */
|
|
|
|
local_irq_save(flags);
|
|
|
|
|
|
|
|
/* Turn off SPI unit prior changing mode */
|
|
|
|
mpc8xxx_spi_write_reg(mode, cs->hw_mode & ~SPMODE_ENABLE);
|
|
|
|
|
2009-10-12 16:49:27 +00:00
|
|
|
/* When in CPM mode, we need to reinit tx and rx. */
|
|
|
|
if (mspi->flags & SPI_CPM_MODE) {
|
2013-02-15 15:52:21 +00:00
|
|
|
fsl_spi_cpm_reinit_txrx(mspi);
|
2009-10-12 16:49:27 +00:00
|
|
|
}
|
2010-05-22 08:18:02 +00:00
|
|
|
mpc8xxx_spi_write_reg(mode, cs->hw_mode);
|
2009-10-12 16:49:24 +00:00
|
|
|
local_irq_restore(flags);
|
|
|
|
}
|
|
|
|
|
2010-10-12 10:18:31 +00:00
|
|
|
static void fsl_spi_chipselect(struct spi_device *spi, int value)
|
2006-05-20 22:00:15 +00:00
|
|
|
{
|
2009-06-18 23:49:08 +00:00
|
|
|
struct mpc8xxx_spi *mpc8xxx_spi = spi_master_get_devdata(spi->master);
|
2012-03-30 15:05:30 +00:00
|
|
|
struct fsl_spi_platform_data *pdata;
|
2009-06-18 23:49:08 +00:00
|
|
|
struct spi_mpc8xxx_cs *cs = spi->controller_state;
|
2006-05-20 22:00:15 +00:00
|
|
|
|
2012-03-30 15:05:30 +00:00
|
|
|
pdata = spi->dev.parent->parent->platform_data;
|
|
|
|
|
2006-05-20 22:00:15 +00:00
|
|
|
if (value == BITBANG_CS_INACTIVE) {
|
2009-03-31 22:24:36 +00:00
|
|
|
if (pdata->cs_control)
|
2021-01-14 13:09:37 +00:00
|
|
|
pdata->cs_control(spi, false);
|
2006-05-20 22:00:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (value == BITBANG_CS_ACTIVE) {
|
2009-06-18 23:49:08 +00:00
|
|
|
mpc8xxx_spi->rx_shift = cs->rx_shift;
|
|
|
|
mpc8xxx_spi->tx_shift = cs->tx_shift;
|
|
|
|
mpc8xxx_spi->get_rx = cs->get_rx;
|
|
|
|
mpc8xxx_spi->get_tx = cs->get_tx;
|
2008-05-12 21:02:30 +00:00
|
|
|
|
2010-10-12 10:18:31 +00:00
|
|
|
fsl_spi_change_mode(spi);
|
2009-10-12 16:49:24 +00:00
|
|
|
|
2009-03-31 22:24:36 +00:00
|
|
|
if (pdata->cs_control)
|
2021-01-14 13:09:37 +00:00
|
|
|
pdata->cs_control(spi, true);
|
2006-05-20 22:00:15 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-02-15 15:52:23 +00:00
|
|
|
static void fsl_spi_qe_cpu_set_shifts(u32 *rx_shift, u32 *tx_shift,
|
|
|
|
int bits_per_word, int msb_first)
|
|
|
|
{
|
|
|
|
*rx_shift = 0;
|
|
|
|
*tx_shift = 0;
|
|
|
|
if (msb_first) {
|
|
|
|
if (bits_per_word <= 8) {
|
|
|
|
*rx_shift = 16;
|
|
|
|
*tx_shift = 24;
|
|
|
|
} else if (bits_per_word <= 16) {
|
|
|
|
*rx_shift = 16;
|
|
|
|
*tx_shift = 16;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (bits_per_word <= 8)
|
|
|
|
*rx_shift = 8;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-02-15 15:52:26 +00:00
|
|
|
static void fsl_spi_grlib_set_shifts(u32 *rx_shift, u32 *tx_shift,
|
|
|
|
int bits_per_word, int msb_first)
|
|
|
|
{
|
|
|
|
*rx_shift = 0;
|
|
|
|
*tx_shift = 0;
|
|
|
|
if (bits_per_word <= 16) {
|
|
|
|
if (msb_first) {
|
|
|
|
*rx_shift = 16; /* LSB in bit 16 */
|
|
|
|
*tx_shift = 32 - bits_per_word; /* MSB in bit 31 */
|
|
|
|
} else {
|
|
|
|
*rx_shift = 16 - bits_per_word; /* MSB in bit 15 */
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-10-12 10:18:31 +00:00
|
|
|
static int mspi_apply_cpu_mode_quirks(struct spi_mpc8xxx_cs *cs,
|
|
|
|
struct spi_device *spi,
|
|
|
|
struct mpc8xxx_spi *mpc8xxx_spi,
|
|
|
|
int bits_per_word)
|
2006-05-20 22:00:15 +00:00
|
|
|
{
|
2008-05-12 21:02:30 +00:00
|
|
|
cs->rx_shift = 0;
|
|
|
|
cs->tx_shift = 0;
|
2006-05-20 22:00:15 +00:00
|
|
|
if (bits_per_word <= 8) {
|
2009-06-18 23:49:08 +00:00
|
|
|
cs->get_rx = mpc8xxx_spi_rx_buf_u8;
|
|
|
|
cs->get_tx = mpc8xxx_spi_tx_buf_u8;
|
2006-05-20 22:00:15 +00:00
|
|
|
} else if (bits_per_word <= 16) {
|
2009-06-18 23:49:08 +00:00
|
|
|
cs->get_rx = mpc8xxx_spi_rx_buf_u16;
|
|
|
|
cs->get_tx = mpc8xxx_spi_tx_buf_u16;
|
2006-05-20 22:00:15 +00:00
|
|
|
} else if (bits_per_word <= 32) {
|
2009-06-18 23:49:08 +00:00
|
|
|
cs->get_rx = mpc8xxx_spi_rx_buf_u32;
|
|
|
|
cs->get_tx = mpc8xxx_spi_tx_buf_u32;
|
2006-05-20 22:00:15 +00:00
|
|
|
} else
|
|
|
|
return -EINVAL;
|
|
|
|
|
2013-02-15 15:52:23 +00:00
|
|
|
if (mpc8xxx_spi->set_shifts)
|
|
|
|
mpc8xxx_spi->set_shifts(&cs->rx_shift, &cs->tx_shift,
|
|
|
|
bits_per_word,
|
|
|
|
!(spi->mode & SPI_LSB_FIRST));
|
|
|
|
|
2009-06-18 23:49:08 +00:00
|
|
|
mpc8xxx_spi->rx_shift = cs->rx_shift;
|
|
|
|
mpc8xxx_spi->tx_shift = cs->tx_shift;
|
|
|
|
mpc8xxx_spi->get_rx = cs->get_rx;
|
|
|
|
mpc8xxx_spi->get_tx = cs->get_tx;
|
2006-05-20 22:00:15 +00:00
|
|
|
|
2010-05-14 09:14:26 +00:00
|
|
|
return bits_per_word;
|
|
|
|
}
|
|
|
|
|
2010-10-12 10:18:31 +00:00
|
|
|
static int mspi_apply_qe_mode_quirks(struct spi_mpc8xxx_cs *cs,
|
|
|
|
struct spi_device *spi,
|
|
|
|
int bits_per_word)
|
2010-05-14 09:14:26 +00:00
|
|
|
{
|
|
|
|
/* QE uses Little Endian for words > 8
|
|
|
|
* so transform all words > 8 into 8 bits
|
|
|
|
* Unfortnatly that doesn't work for LSB so
|
|
|
|
* reject these for now */
|
|
|
|
/* Note: 32 bits word, LSB works iff
|
|
|
|
* tfcr/rfcr is set to CPMFCR_GBL */
|
|
|
|
if (spi->mode & SPI_LSB_FIRST &&
|
|
|
|
bits_per_word > 8)
|
|
|
|
return -EINVAL;
|
|
|
|
if (bits_per_word > 8)
|
|
|
|
return 8; /* pretend its 8 bits */
|
|
|
|
return bits_per_word;
|
|
|
|
}
|
|
|
|
|
2010-10-12 10:18:31 +00:00
|
|
|
static int fsl_spi_setup_transfer(struct spi_device *spi,
|
|
|
|
struct spi_transfer *t)
|
2010-05-14 09:14:26 +00:00
|
|
|
{
|
|
|
|
struct mpc8xxx_spi *mpc8xxx_spi;
|
2010-10-12 10:18:31 +00:00
|
|
|
int bits_per_word = 0;
|
2010-05-14 09:14:26 +00:00
|
|
|
u8 pm;
|
2010-10-12 10:18:31 +00:00
|
|
|
u32 hz = 0;
|
2010-05-14 09:14:26 +00:00
|
|
|
struct spi_mpc8xxx_cs *cs = spi->controller_state;
|
|
|
|
|
|
|
|
mpc8xxx_spi = spi_master_get_devdata(spi->master);
|
|
|
|
|
|
|
|
if (t) {
|
|
|
|
bits_per_word = t->bits_per_word;
|
|
|
|
hz = t->speed_hz;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* spi_transfer level calls that work per-word */
|
|
|
|
if (!bits_per_word)
|
|
|
|
bits_per_word = spi->bits_per_word;
|
|
|
|
|
|
|
|
if (!hz)
|
|
|
|
hz = spi->max_speed_hz;
|
|
|
|
|
|
|
|
if (!(mpc8xxx_spi->flags & SPI_CPM_MODE))
|
|
|
|
bits_per_word = mspi_apply_cpu_mode_quirks(cs, spi,
|
|
|
|
mpc8xxx_spi,
|
|
|
|
bits_per_word);
|
|
|
|
else if (mpc8xxx_spi->flags & SPI_QE)
|
|
|
|
bits_per_word = mspi_apply_qe_mode_quirks(cs, spi,
|
|
|
|
bits_per_word);
|
|
|
|
|
|
|
|
if (bits_per_word < 0)
|
|
|
|
return bits_per_word;
|
|
|
|
|
2006-05-20 22:00:15 +00:00
|
|
|
if (bits_per_word == 32)
|
|
|
|
bits_per_word = 0;
|
|
|
|
else
|
|
|
|
bits_per_word = bits_per_word - 1;
|
|
|
|
|
2007-07-31 07:38:41 +00:00
|
|
|
/* mask out bits we are going to set */
|
2008-05-12 21:02:30 +00:00
|
|
|
cs->hw_mode &= ~(SPMODE_LEN(0xF) | SPMODE_DIV16
|
|
|
|
| SPMODE_PM(0xF));
|
|
|
|
|
|
|
|
cs->hw_mode |= SPMODE_LEN(bits_per_word);
|
|
|
|
|
2009-06-18 23:49:08 +00:00
|
|
|
if ((mpc8xxx_spi->spibrg / hz) > 64) {
|
2008-09-13 09:33:14 +00:00
|
|
|
cs->hw_mode |= SPMODE_DIV16;
|
spi: Correct SPI clock frequency setting in spi_mpc8xxx
Correct SPI clock frequency division factor rounding, preventing clock rates
higher than the maximum specified clock frequency being used.
When specifying spi-max-frequency = <10000000> in the device tree,
the resulting frequency was 11.1 MHz, with spibrg being 133333332.
According to the freescale data sheet [1], the spi clock rate is
spiclk = spibrg / (4 * (pm+1))
The existing code calculated
pm = mpc8xxx_spi->spibrg / (hz * 4); pm--;
resulting in pm = (int) (3.3333) - 1 = 2,
resulting in spiclk = 133333332/(4*(2+1)) = 11111111
With the fix,
pm = (mpc8xxx_spi->spibrg - 1) / (hz * 4) + 1; pm--;
resulting in pm = (int) (4.3333) - 1 = 3,
resulting in spiclk = 133333332/(4*(3+1)) = 8333333
Without the fix, for every desired SPI frequency that
is not exactly derivable from spibrg, pm will be too
small due to rounding down, resulting in a too high SPI clock,
so we need a pm which is one higher.
For values that are exactly derivable, spibrg will
be dividable by (hz*4) without remainder, and
(int) ((spibrg-1)/(hz*4)) will be one lower than
(int) (spibrg)/(hz*4), which is compensated by adding 1.
For these values, the fixed version calculates the same pm
as the unfixed version.
For all values that are not exactly derivable,
spibrg will be not dividable by (hz*4) without
remainder, and (int) ((spibrg-1)/(hz*4)) will be
the same as (int) (spibrg)/(hz*4), and the calculated pm will
be one higher than calculated by the unfixed version.
References:
[1] http://www.freescale.com/files/32bit/doc/ref_manual/MPC8315ERM.pdf,
page 22-10 -> 1398
Signed-off-by: Ernst Schwab <eschwab@online.de>
Signed-off-by: Grant Likely <grant.likely@secretlab.ca>
2010-02-17 04:02:57 +00:00
|
|
|
pm = (mpc8xxx_spi->spibrg - 1) / (hz * 64) + 1;
|
2017-01-13 12:50:21 +00:00
|
|
|
WARN_ONCE(pm > 16,
|
|
|
|
"%s: Requested speed is too low: %d Hz. Will use %d Hz instead.\n",
|
|
|
|
dev_name(&spi->dev), hz, mpc8xxx_spi->spibrg / 1024);
|
2009-06-18 23:49:01 +00:00
|
|
|
if (pm > 16)
|
2008-09-13 09:33:14 +00:00
|
|
|
pm = 16;
|
2010-10-12 10:18:31 +00:00
|
|
|
} else {
|
spi: Correct SPI clock frequency setting in spi_mpc8xxx
Correct SPI clock frequency division factor rounding, preventing clock rates
higher than the maximum specified clock frequency being used.
When specifying spi-max-frequency = <10000000> in the device tree,
the resulting frequency was 11.1 MHz, with spibrg being 133333332.
According to the freescale data sheet [1], the spi clock rate is
spiclk = spibrg / (4 * (pm+1))
The existing code calculated
pm = mpc8xxx_spi->spibrg / (hz * 4); pm--;
resulting in pm = (int) (3.3333) - 1 = 2,
resulting in spiclk = 133333332/(4*(2+1)) = 11111111
With the fix,
pm = (mpc8xxx_spi->spibrg - 1) / (hz * 4) + 1; pm--;
resulting in pm = (int) (4.3333) - 1 = 3,
resulting in spiclk = 133333332/(4*(3+1)) = 8333333
Without the fix, for every desired SPI frequency that
is not exactly derivable from spibrg, pm will be too
small due to rounding down, resulting in a too high SPI clock,
so we need a pm which is one higher.
For values that are exactly derivable, spibrg will
be dividable by (hz*4) without remainder, and
(int) ((spibrg-1)/(hz*4)) will be one lower than
(int) (spibrg)/(hz*4), which is compensated by adding 1.
For these values, the fixed version calculates the same pm
as the unfixed version.
For all values that are not exactly derivable,
spibrg will be not dividable by (hz*4) without
remainder, and (int) ((spibrg-1)/(hz*4)) will be
the same as (int) (spibrg)/(hz*4), and the calculated pm will
be one higher than calculated by the unfixed version.
References:
[1] http://www.freescale.com/files/32bit/doc/ref_manual/MPC8315ERM.pdf,
page 22-10 -> 1398
Signed-off-by: Ernst Schwab <eschwab@online.de>
Signed-off-by: Grant Likely <grant.likely@secretlab.ca>
2010-02-17 04:02:57 +00:00
|
|
|
pm = (mpc8xxx_spi->spibrg - 1) / (hz * 4) + 1;
|
2010-10-12 10:18:31 +00:00
|
|
|
}
|
2008-07-24 04:29:52 +00:00
|
|
|
if (pm)
|
|
|
|
pm--;
|
|
|
|
|
|
|
|
cs->hw_mode |= SPMODE_PM(pm);
|
2009-10-12 16:49:24 +00:00
|
|
|
|
2010-10-12 10:18:31 +00:00
|
|
|
fsl_spi_change_mode(spi);
|
2008-05-12 21:02:30 +00:00
|
|
|
return 0;
|
|
|
|
}
|
2006-05-20 22:00:15 +00:00
|
|
|
|
2010-10-12 10:18:31 +00:00
|
|
|
static int fsl_spi_cpu_bufs(struct mpc8xxx_spi *mspi,
|
2009-10-12 16:49:27 +00:00
|
|
|
struct spi_transfer *t, unsigned int len)
|
|
|
|
{
|
|
|
|
u32 word;
|
2020-06-22 16:26:11 +00:00
|
|
|
struct fsl_spi_reg __iomem *reg_base = mspi->reg_base;
|
2009-10-12 16:49:27 +00:00
|
|
|
|
|
|
|
mspi->count = len;
|
|
|
|
|
|
|
|
/* enable rx ints */
|
2010-10-12 10:18:31 +00:00
|
|
|
mpc8xxx_spi_write_reg(®_base->mask, SPIM_NE);
|
2009-10-12 16:49:27 +00:00
|
|
|
|
|
|
|
/* transmit word */
|
|
|
|
word = mspi->get_tx(mspi);
|
2010-10-12 10:18:31 +00:00
|
|
|
mpc8xxx_spi_write_reg(®_base->transmit, word);
|
2009-10-12 16:49:27 +00:00
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2010-10-12 10:18:31 +00:00
|
|
|
static int fsl_spi_bufs(struct spi_device *spi, struct spi_transfer *t,
|
2009-10-12 16:49:27 +00:00
|
|
|
bool is_dma_mapped)
|
|
|
|
{
|
|
|
|
struct mpc8xxx_spi *mpc8xxx_spi = spi_master_get_devdata(spi->master);
|
2020-06-22 16:26:11 +00:00
|
|
|
struct fsl_spi_reg __iomem *reg_base;
|
2009-10-12 16:49:27 +00:00
|
|
|
unsigned int len = t->len;
|
|
|
|
u8 bits_per_word;
|
|
|
|
int ret;
|
2008-05-12 21:02:30 +00:00
|
|
|
|
2010-10-12 10:18:31 +00:00
|
|
|
reg_base = mpc8xxx_spi->reg_base;
|
2008-05-12 21:02:30 +00:00
|
|
|
bits_per_word = spi->bits_per_word;
|
|
|
|
if (t->bits_per_word)
|
|
|
|
bits_per_word = t->bits_per_word;
|
2009-10-12 16:49:27 +00:00
|
|
|
|
2008-09-13 09:33:15 +00:00
|
|
|
if (bits_per_word > 8) {
|
|
|
|
/* invalid length? */
|
|
|
|
if (len & 1)
|
|
|
|
return -EINVAL;
|
2008-05-12 21:02:30 +00:00
|
|
|
len /= 2;
|
2008-09-13 09:33:15 +00:00
|
|
|
}
|
|
|
|
if (bits_per_word > 16) {
|
|
|
|
/* invalid length? */
|
|
|
|
if (len & 1)
|
|
|
|
return -EINVAL;
|
2008-05-12 21:02:30 +00:00
|
|
|
len /= 2;
|
2008-09-13 09:33:15 +00:00
|
|
|
}
|
|
|
|
|
2009-10-12 16:49:27 +00:00
|
|
|
mpc8xxx_spi->tx = t->tx_buf;
|
|
|
|
mpc8xxx_spi->rx = t->rx_buf;
|
2008-05-12 21:02:30 +00:00
|
|
|
|
2013-11-14 22:32:02 +00:00
|
|
|
reinit_completion(&mpc8xxx_spi->done);
|
2008-05-12 21:02:30 +00:00
|
|
|
|
2009-10-12 16:49:27 +00:00
|
|
|
if (mpc8xxx_spi->flags & SPI_CPM_MODE)
|
2010-10-12 10:18:31 +00:00
|
|
|
ret = fsl_spi_cpm_bufs(mpc8xxx_spi, t, is_dma_mapped);
|
2009-10-12 16:49:27 +00:00
|
|
|
else
|
2010-10-12 10:18:31 +00:00
|
|
|
ret = fsl_spi_cpu_bufs(mpc8xxx_spi, t, len);
|
2009-10-12 16:49:27 +00:00
|
|
|
if (ret)
|
|
|
|
return ret;
|
2008-05-12 21:02:30 +00:00
|
|
|
|
2009-06-18 23:49:08 +00:00
|
|
|
wait_for_completion(&mpc8xxx_spi->done);
|
2008-05-12 21:02:30 +00:00
|
|
|
|
|
|
|
/* disable rx ints */
|
2010-10-12 10:18:31 +00:00
|
|
|
mpc8xxx_spi_write_reg(®_base->mask, 0);
|
2008-05-12 21:02:30 +00:00
|
|
|
|
2009-10-12 16:49:27 +00:00
|
|
|
if (mpc8xxx_spi->flags & SPI_CPM_MODE)
|
2010-10-12 10:18:31 +00:00
|
|
|
fsl_spi_cpm_bufs_complete(mpc8xxx_spi);
|
2009-10-12 16:49:27 +00:00
|
|
|
|
2009-06-18 23:49:08 +00:00
|
|
|
return mpc8xxx_spi->count;
|
2008-05-12 21:02:30 +00:00
|
|
|
}
|
|
|
|
|
2014-12-03 06:56:17 +00:00
|
|
|
static int fsl_spi_do_one_msg(struct spi_master *master,
|
|
|
|
struct spi_message *m)
|
2008-05-12 21:02:30 +00:00
|
|
|
{
|
2019-03-27 14:30:52 +00:00
|
|
|
struct mpc8xxx_spi *mpc8xxx_spi = spi_master_get_devdata(master);
|
2009-06-18 23:49:06 +00:00
|
|
|
struct spi_device *spi = m->spi;
|
2014-01-31 12:44:59 +00:00
|
|
|
struct spi_transfer *t, *first;
|
2009-06-18 23:49:06 +00:00
|
|
|
unsigned int cs_change;
|
|
|
|
const int nsecs = 50;
|
2019-03-27 14:30:51 +00:00
|
|
|
int status, last_bpw;
|
2009-06-18 23:49:06 +00:00
|
|
|
|
2019-03-27 14:30:52 +00:00
|
|
|
/*
|
|
|
|
* In CPU mode, optimize large byte transfers to use larger
|
|
|
|
* bits_per_word values to reduce number of interrupts taken.
|
|
|
|
*/
|
|
|
|
if (!(mpc8xxx_spi->flags & SPI_CPM_MODE)) {
|
|
|
|
list_for_each_entry(t, &m->transfers, transfer_list) {
|
|
|
|
if (t->len < 256 || t->bits_per_word != 8)
|
|
|
|
continue;
|
|
|
|
if ((t->len & 3) == 0)
|
|
|
|
t->bits_per_word = 32;
|
|
|
|
else if ((t->len & 1) == 0)
|
|
|
|
t->bits_per_word = 16;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-01-31 12:44:59 +00:00
|
|
|
/* Don't allow changes if CS is active */
|
2019-03-27 14:30:51 +00:00
|
|
|
cs_change = 1;
|
2009-06-18 23:49:06 +00:00
|
|
|
list_for_each_entry(t, &m->transfers, transfer_list) {
|
2019-03-27 14:30:51 +00:00
|
|
|
if (cs_change)
|
|
|
|
first = t;
|
|
|
|
cs_change = t->cs_change;
|
2019-03-27 14:30:51 +00:00
|
|
|
if (first->speed_hz != t->speed_hz) {
|
2014-01-31 12:44:59 +00:00
|
|
|
dev_err(&spi->dev,
|
2019-03-27 14:30:51 +00:00
|
|
|
"speed_hz cannot change while CS is active\n");
|
2014-12-04 13:15:47 +00:00
|
|
|
return -EINVAL;
|
2014-01-31 12:44:59 +00:00
|
|
|
}
|
|
|
|
}
|
2009-06-18 23:49:06 +00:00
|
|
|
|
2019-03-27 14:30:51 +00:00
|
|
|
last_bpw = -1;
|
2014-01-31 12:44:59 +00:00
|
|
|
cs_change = 1;
|
|
|
|
status = -EINVAL;
|
|
|
|
list_for_each_entry(t, &m->transfers, transfer_list) {
|
2019-03-27 14:30:51 +00:00
|
|
|
if (cs_change || last_bpw != t->bits_per_word)
|
2019-03-27 14:30:50 +00:00
|
|
|
status = fsl_spi_setup_transfer(spi, t);
|
|
|
|
if (status < 0)
|
|
|
|
break;
|
2019-03-27 14:30:51 +00:00
|
|
|
last_bpw = t->bits_per_word;
|
2008-05-12 21:02:30 +00:00
|
|
|
|
2009-06-18 23:49:06 +00:00
|
|
|
if (cs_change) {
|
2010-10-12 10:18:31 +00:00
|
|
|
fsl_spi_chipselect(spi, BITBANG_CS_ACTIVE);
|
2009-06-18 23:49:06 +00:00
|
|
|
ndelay(nsecs);
|
|
|
|
}
|
|
|
|
cs_change = t->cs_change;
|
|
|
|
if (t->len)
|
2010-10-12 10:18:31 +00:00
|
|
|
status = fsl_spi_bufs(spi, t, m->is_dma_mapped);
|
2009-06-18 23:49:06 +00:00
|
|
|
if (status) {
|
|
|
|
status = -EMSGSIZE;
|
|
|
|
break;
|
2008-05-12 21:02:30 +00:00
|
|
|
}
|
2009-06-18 23:49:06 +00:00
|
|
|
m->actual_length += t->len;
|
2008-05-12 21:02:30 +00:00
|
|
|
|
2019-09-26 10:51:37 +00:00
|
|
|
spi_transfer_delay_exec(t);
|
2008-05-12 21:02:30 +00:00
|
|
|
|
2009-06-18 23:49:06 +00:00
|
|
|
if (cs_change) {
|
2008-05-12 21:02:30 +00:00
|
|
|
ndelay(nsecs);
|
2010-10-12 10:18:31 +00:00
|
|
|
fsl_spi_chipselect(spi, BITBANG_CS_INACTIVE);
|
2009-06-18 23:49:06 +00:00
|
|
|
ndelay(nsecs);
|
2008-05-12 21:02:30 +00:00
|
|
|
}
|
2009-06-18 23:49:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
m->status = status;
|
|
|
|
|
|
|
|
if (status || !cs_change) {
|
|
|
|
ndelay(nsecs);
|
2010-10-12 10:18:31 +00:00
|
|
|
fsl_spi_chipselect(spi, BITBANG_CS_INACTIVE);
|
2009-06-18 23:49:06 +00:00
|
|
|
}
|
|
|
|
|
2010-10-12 10:18:31 +00:00
|
|
|
fsl_spi_setup_transfer(spi, NULL);
|
2019-05-22 11:00:36 +00:00
|
|
|
spi_finalize_current_message(master);
|
2014-12-03 06:56:17 +00:00
|
|
|
return 0;
|
2006-05-20 22:00:15 +00:00
|
|
|
}
|
|
|
|
|
2010-10-12 10:18:31 +00:00
|
|
|
static int fsl_spi_setup(struct spi_device *spi)
|
2006-05-20 22:00:15 +00:00
|
|
|
{
|
2009-06-18 23:49:08 +00:00
|
|
|
struct mpc8xxx_spi *mpc8xxx_spi;
|
2020-06-22 16:26:11 +00:00
|
|
|
struct fsl_spi_reg __iomem *reg_base;
|
spi: Cleanup on failure of initial setup
Commit c7299fea6769 ("spi: Fix spi device unregister flow") changed the
SPI core's behavior if the ->setup() hook returns an error upon adding
an spi_device: Before, the ->cleanup() hook was invoked to free any
allocations that were made by ->setup(). With the commit, that's no
longer the case, so the ->setup() hook is expected to free the
allocations itself.
I've identified 5 drivers which depend on the old behavior and am fixing
them up hereinafter: spi-bitbang.c spi-fsl-spi.c spi-omap-uwire.c
spi-omap2-mcspi.c spi-pxa2xx.c
Importantly, ->setup() is not only invoked on spi_device *addition*:
It may subsequently be called to *change* SPI parameters. If changing
these SPI parameters fails, freeing memory allocations would be wrong.
That should only be done if the spi_device is finally destroyed.
I am therefore using a bool "initial_setup" in 4 of the affected drivers
to differentiate between the invocation on *adding* the spi_device and
any subsequent invocations: spi-bitbang.c spi-fsl-spi.c spi-omap-uwire.c
spi-omap2-mcspi.c
In spi-pxa2xx.c, it seems the ->setup() hook can only fail on spi_device
addition, not any subsequent calls. It therefore doesn't need the bool.
It's worth noting that 5 other drivers already perform a cleanup if the
->setup() hook fails. Before c7299fea6769, they caused a double-free
if ->setup() failed on spi_device addition. Since the commit, they're
fine. These drivers are: spi-mpc512x-psc.c spi-pl022.c spi-s3c64xx.c
spi-st-ssc4.c spi-tegra114.c
(spi-pxa2xx.c also already performs a cleanup, but only in one of
several error paths.)
Fixes: c7299fea6769 ("spi: Fix spi device unregister flow")
Signed-off-by: Lukas Wunner <lukas@wunner.de>
Cc: Saravana Kannan <saravanak@google.com>
Acked-by: Andy Shevchenko <andriy.shevchenko@linux.intel.com> # pxa2xx
Link: https://lore.kernel.org/r/f76a0599469f265b69c371538794101fa37b5536.1622149321.git.lukas@wunner.de
Signed-off-by: Mark Brown <broonie@kernel.org>
2021-05-27 21:10:56 +00:00
|
|
|
bool initial_setup = false;
|
2006-05-20 22:00:15 +00:00
|
|
|
int retval;
|
2008-05-12 21:02:30 +00:00
|
|
|
u32 hw_mode;
|
2014-08-31 04:44:09 +00:00
|
|
|
struct spi_mpc8xxx_cs *cs = spi_get_ctldata(spi);
|
2006-05-20 22:00:15 +00:00
|
|
|
|
|
|
|
if (!spi->max_speed_hz)
|
|
|
|
return -EINVAL;
|
|
|
|
|
2008-05-12 21:02:30 +00:00
|
|
|
if (!cs) {
|
2014-08-31 04:44:09 +00:00
|
|
|
cs = kzalloc(sizeof(*cs), GFP_KERNEL);
|
2008-05-12 21:02:30 +00:00
|
|
|
if (!cs)
|
|
|
|
return -ENOMEM;
|
2014-08-31 04:44:09 +00:00
|
|
|
spi_set_ctldata(spi, cs);
|
spi: Cleanup on failure of initial setup
Commit c7299fea6769 ("spi: Fix spi device unregister flow") changed the
SPI core's behavior if the ->setup() hook returns an error upon adding
an spi_device: Before, the ->cleanup() hook was invoked to free any
allocations that were made by ->setup(). With the commit, that's no
longer the case, so the ->setup() hook is expected to free the
allocations itself.
I've identified 5 drivers which depend on the old behavior and am fixing
them up hereinafter: spi-bitbang.c spi-fsl-spi.c spi-omap-uwire.c
spi-omap2-mcspi.c spi-pxa2xx.c
Importantly, ->setup() is not only invoked on spi_device *addition*:
It may subsequently be called to *change* SPI parameters. If changing
these SPI parameters fails, freeing memory allocations would be wrong.
That should only be done if the spi_device is finally destroyed.
I am therefore using a bool "initial_setup" in 4 of the affected drivers
to differentiate between the invocation on *adding* the spi_device and
any subsequent invocations: spi-bitbang.c spi-fsl-spi.c spi-omap-uwire.c
spi-omap2-mcspi.c
In spi-pxa2xx.c, it seems the ->setup() hook can only fail on spi_device
addition, not any subsequent calls. It therefore doesn't need the bool.
It's worth noting that 5 other drivers already perform a cleanup if the
->setup() hook fails. Before c7299fea6769, they caused a double-free
if ->setup() failed on spi_device addition. Since the commit, they're
fine. These drivers are: spi-mpc512x-psc.c spi-pl022.c spi-s3c64xx.c
spi-st-ssc4.c spi-tegra114.c
(spi-pxa2xx.c also already performs a cleanup, but only in one of
several error paths.)
Fixes: c7299fea6769 ("spi: Fix spi device unregister flow")
Signed-off-by: Lukas Wunner <lukas@wunner.de>
Cc: Saravana Kannan <saravanak@google.com>
Acked-by: Andy Shevchenko <andriy.shevchenko@linux.intel.com> # pxa2xx
Link: https://lore.kernel.org/r/f76a0599469f265b69c371538794101fa37b5536.1622149321.git.lukas@wunner.de
Signed-off-by: Mark Brown <broonie@kernel.org>
2021-05-27 21:10:56 +00:00
|
|
|
initial_setup = true;
|
2008-05-12 21:02:30 +00:00
|
|
|
}
|
2009-06-18 23:49:08 +00:00
|
|
|
mpc8xxx_spi = spi_master_get_devdata(spi->master);
|
2006-05-20 22:00:15 +00:00
|
|
|
|
2010-10-12 10:18:31 +00:00
|
|
|
reg_base = mpc8xxx_spi->reg_base;
|
|
|
|
|
2010-03-16 10:47:56 +00:00
|
|
|
hw_mode = cs->hw_mode; /* Save original settings */
|
2010-10-12 10:18:31 +00:00
|
|
|
cs->hw_mode = mpc8xxx_spi_read_reg(®_base->mode);
|
2008-05-12 21:02:30 +00:00
|
|
|
/* mask out bits we are going to set */
|
|
|
|
cs->hw_mode &= ~(SPMODE_CP_BEGIN_EDGECLK | SPMODE_CI_INACTIVEHIGH
|
|
|
|
| SPMODE_REV | SPMODE_LOOP);
|
|
|
|
|
|
|
|
if (spi->mode & SPI_CPHA)
|
|
|
|
cs->hw_mode |= SPMODE_CP_BEGIN_EDGECLK;
|
|
|
|
if (spi->mode & SPI_CPOL)
|
|
|
|
cs->hw_mode |= SPMODE_CI_INACTIVEHIGH;
|
|
|
|
if (!(spi->mode & SPI_LSB_FIRST))
|
|
|
|
cs->hw_mode |= SPMODE_REV;
|
|
|
|
if (spi->mode & SPI_LOOP)
|
|
|
|
cs->hw_mode |= SPMODE_LOOP;
|
|
|
|
|
2010-10-12 10:18:31 +00:00
|
|
|
retval = fsl_spi_setup_transfer(spi, NULL);
|
2008-05-12 21:02:30 +00:00
|
|
|
if (retval < 0) {
|
|
|
|
cs->hw_mode = hw_mode; /* Restore settings */
|
spi: Cleanup on failure of initial setup
Commit c7299fea6769 ("spi: Fix spi device unregister flow") changed the
SPI core's behavior if the ->setup() hook returns an error upon adding
an spi_device: Before, the ->cleanup() hook was invoked to free any
allocations that were made by ->setup(). With the commit, that's no
longer the case, so the ->setup() hook is expected to free the
allocations itself.
I've identified 5 drivers which depend on the old behavior and am fixing
them up hereinafter: spi-bitbang.c spi-fsl-spi.c spi-omap-uwire.c
spi-omap2-mcspi.c spi-pxa2xx.c
Importantly, ->setup() is not only invoked on spi_device *addition*:
It may subsequently be called to *change* SPI parameters. If changing
these SPI parameters fails, freeing memory allocations would be wrong.
That should only be done if the spi_device is finally destroyed.
I am therefore using a bool "initial_setup" in 4 of the affected drivers
to differentiate between the invocation on *adding* the spi_device and
any subsequent invocations: spi-bitbang.c spi-fsl-spi.c spi-omap-uwire.c
spi-omap2-mcspi.c
In spi-pxa2xx.c, it seems the ->setup() hook can only fail on spi_device
addition, not any subsequent calls. It therefore doesn't need the bool.
It's worth noting that 5 other drivers already perform a cleanup if the
->setup() hook fails. Before c7299fea6769, they caused a double-free
if ->setup() failed on spi_device addition. Since the commit, they're
fine. These drivers are: spi-mpc512x-psc.c spi-pl022.c spi-s3c64xx.c
spi-st-ssc4.c spi-tegra114.c
(spi-pxa2xx.c also already performs a cleanup, but only in one of
several error paths.)
Fixes: c7299fea6769 ("spi: Fix spi device unregister flow")
Signed-off-by: Lukas Wunner <lukas@wunner.de>
Cc: Saravana Kannan <saravanak@google.com>
Acked-by: Andy Shevchenko <andriy.shevchenko@linux.intel.com> # pxa2xx
Link: https://lore.kernel.org/r/f76a0599469f265b69c371538794101fa37b5536.1622149321.git.lukas@wunner.de
Signed-off-by: Mark Brown <broonie@kernel.org>
2021-05-27 21:10:56 +00:00
|
|
|
if (initial_setup)
|
|
|
|
kfree(cs);
|
2006-05-20 22:00:15 +00:00
|
|
|
return retval;
|
2008-05-12 21:02:30 +00:00
|
|
|
}
|
2013-02-15 15:52:22 +00:00
|
|
|
|
|
|
|
/* Initialize chipselect - might be active for SPI_CS_HIGH mode */
|
|
|
|
fsl_spi_chipselect(spi, BITBANG_CS_INACTIVE);
|
|
|
|
|
2006-05-20 22:00:15 +00:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2013-02-15 15:52:27 +00:00
|
|
|
static void fsl_spi_cleanup(struct spi_device *spi)
|
|
|
|
{
|
2014-08-31 04:44:09 +00:00
|
|
|
struct spi_mpc8xxx_cs *cs = spi_get_ctldata(spi);
|
2013-02-15 15:52:27 +00:00
|
|
|
|
2014-08-31 04:44:09 +00:00
|
|
|
kfree(cs);
|
|
|
|
spi_set_ctldata(spi, NULL);
|
2013-02-15 15:52:27 +00:00
|
|
|
}
|
|
|
|
|
2010-10-12 10:18:31 +00:00
|
|
|
static void fsl_spi_cpu_irq(struct mpc8xxx_spi *mspi, u32 events)
|
2009-10-12 16:49:27 +00:00
|
|
|
{
|
2020-06-22 16:26:11 +00:00
|
|
|
struct fsl_spi_reg __iomem *reg_base = mspi->reg_base;
|
2010-10-12 10:18:31 +00:00
|
|
|
|
2009-10-12 16:49:27 +00:00
|
|
|
/* We need handle RX first */
|
|
|
|
if (events & SPIE_NE) {
|
2010-10-12 10:18:31 +00:00
|
|
|
u32 rx_data = mpc8xxx_spi_read_reg(®_base->receive);
|
2009-10-12 16:49:27 +00:00
|
|
|
|
|
|
|
if (mspi->rx)
|
|
|
|
mspi->get_rx(rx_data, mspi);
|
2006-05-20 22:00:15 +00:00
|
|
|
}
|
|
|
|
|
2009-10-12 16:49:27 +00:00
|
|
|
if ((events & SPIE_NF) == 0)
|
2006-05-20 22:00:15 +00:00
|
|
|
/* spin until TX is done */
|
2009-10-12 16:49:27 +00:00
|
|
|
while (((events =
|
2010-10-12 10:18:31 +00:00
|
|
|
mpc8xxx_spi_read_reg(®_base->event)) &
|
2006-05-20 22:00:15 +00:00
|
|
|
SPIE_NF) == 0)
|
2009-06-18 23:49:05 +00:00
|
|
|
cpu_relax();
|
2006-05-20 22:00:15 +00:00
|
|
|
|
2009-10-12 16:49:27 +00:00
|
|
|
/* Clear the events */
|
2010-10-12 10:18:31 +00:00
|
|
|
mpc8xxx_spi_write_reg(®_base->event, events);
|
2009-10-12 16:49:27 +00:00
|
|
|
|
|
|
|
mspi->count -= 1;
|
|
|
|
if (mspi->count) {
|
|
|
|
u32 word = mspi->get_tx(mspi);
|
|
|
|
|
2010-10-12 10:18:31 +00:00
|
|
|
mpc8xxx_spi_write_reg(®_base->transmit, word);
|
2006-05-20 22:00:15 +00:00
|
|
|
} else {
|
2009-10-12 16:49:27 +00:00
|
|
|
complete(&mspi->done);
|
2006-05-20 22:00:15 +00:00
|
|
|
}
|
2009-10-12 16:49:27 +00:00
|
|
|
}
|
2006-05-20 22:00:15 +00:00
|
|
|
|
2010-10-12 10:18:31 +00:00
|
|
|
static irqreturn_t fsl_spi_irq(s32 irq, void *context_data)
|
2009-10-12 16:49:27 +00:00
|
|
|
{
|
|
|
|
struct mpc8xxx_spi *mspi = context_data;
|
|
|
|
irqreturn_t ret = IRQ_NONE;
|
|
|
|
u32 events;
|
2020-06-22 16:26:11 +00:00
|
|
|
struct fsl_spi_reg __iomem *reg_base = mspi->reg_base;
|
2009-10-12 16:49:27 +00:00
|
|
|
|
|
|
|
/* Get interrupt events(tx/rx) */
|
2010-10-12 10:18:31 +00:00
|
|
|
events = mpc8xxx_spi_read_reg(®_base->event);
|
2009-10-12 16:49:27 +00:00
|
|
|
if (events)
|
|
|
|
ret = IRQ_HANDLED;
|
|
|
|
|
|
|
|
dev_dbg(mspi->dev, "%s: events %x\n", __func__, events);
|
|
|
|
|
|
|
|
if (mspi->flags & SPI_CPM_MODE)
|
2010-10-12 10:18:31 +00:00
|
|
|
fsl_spi_cpm_irq(mspi, events);
|
2009-10-12 16:49:27 +00:00
|
|
|
else
|
2010-10-12 10:18:31 +00:00
|
|
|
fsl_spi_cpu_irq(mspi, events);
|
2006-05-20 22:00:15 +00:00
|
|
|
|
|
|
|
return ret;
|
|
|
|
}
|
2009-10-12 16:49:27 +00:00
|
|
|
|
2013-02-15 15:52:26 +00:00
|
|
|
static void fsl_spi_grlib_cs_control(struct spi_device *spi, bool on)
|
|
|
|
{
|
|
|
|
struct mpc8xxx_spi *mpc8xxx_spi = spi_master_get_devdata(spi->master);
|
2020-06-22 16:26:11 +00:00
|
|
|
struct fsl_spi_reg __iomem *reg_base = mpc8xxx_spi->reg_base;
|
2013-02-15 15:52:26 +00:00
|
|
|
u32 slvsel;
|
|
|
|
u16 cs = spi->chip_select;
|
|
|
|
|
2019-08-04 00:35:39 +00:00
|
|
|
if (spi->cs_gpiod) {
|
|
|
|
gpiod_set_value(spi->cs_gpiod, on);
|
2013-02-15 15:52:27 +00:00
|
|
|
} else if (cs < mpc8xxx_spi->native_chipselects) {
|
|
|
|
slvsel = mpc8xxx_spi_read_reg(®_base->slvsel);
|
|
|
|
slvsel = on ? (slvsel | (1 << cs)) : (slvsel & ~(1 << cs));
|
|
|
|
mpc8xxx_spi_write_reg(®_base->slvsel, slvsel);
|
|
|
|
}
|
2013-02-15 15:52:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static void fsl_spi_grlib_probe(struct device *dev)
|
|
|
|
{
|
2013-07-30 07:58:59 +00:00
|
|
|
struct fsl_spi_platform_data *pdata = dev_get_platdata(dev);
|
2013-02-15 15:52:26 +00:00
|
|
|
struct spi_master *master = dev_get_drvdata(dev);
|
|
|
|
struct mpc8xxx_spi *mpc8xxx_spi = spi_master_get_devdata(master);
|
2020-06-22 16:26:11 +00:00
|
|
|
struct fsl_spi_reg __iomem *reg_base = mpc8xxx_spi->reg_base;
|
2013-02-15 15:52:26 +00:00
|
|
|
int mbits;
|
|
|
|
u32 capabilities;
|
|
|
|
|
|
|
|
capabilities = mpc8xxx_spi_read_reg(®_base->cap);
|
|
|
|
|
|
|
|
mpc8xxx_spi->set_shifts = fsl_spi_grlib_set_shifts;
|
|
|
|
mbits = SPCAP_MAXWLEN(capabilities);
|
|
|
|
if (mbits)
|
|
|
|
mpc8xxx_spi->max_bits_per_word = mbits + 1;
|
|
|
|
|
2013-02-15 15:52:27 +00:00
|
|
|
mpc8xxx_spi->native_chipselects = 0;
|
2013-02-15 15:52:26 +00:00
|
|
|
if (SPCAP_SSEN(capabilities)) {
|
2013-02-15 15:52:27 +00:00
|
|
|
mpc8xxx_spi->native_chipselects = SPCAP_SSSZ(capabilities);
|
2013-02-15 15:52:26 +00:00
|
|
|
mpc8xxx_spi_write_reg(®_base->slvsel, 0xffffffff);
|
|
|
|
}
|
2013-02-15 15:52:27 +00:00
|
|
|
master->num_chipselect = mpc8xxx_spi->native_chipselects;
|
2013-02-15 15:52:26 +00:00
|
|
|
pdata->cs_control = fsl_spi_grlib_cs_control;
|
|
|
|
}
|
|
|
|
|
2020-04-07 12:28:55 +00:00
|
|
|
static struct spi_master *fsl_spi_probe(struct device *dev,
|
2010-10-12 10:18:31 +00:00
|
|
|
struct resource *mem, unsigned int irq)
|
2006-05-20 22:00:15 +00:00
|
|
|
{
|
2013-07-30 07:58:59 +00:00
|
|
|
struct fsl_spi_platform_data *pdata = dev_get_platdata(dev);
|
2006-05-20 22:00:15 +00:00
|
|
|
struct spi_master *master;
|
2009-06-18 23:49:08 +00:00
|
|
|
struct mpc8xxx_spi *mpc8xxx_spi;
|
2020-06-22 16:26:11 +00:00
|
|
|
struct fsl_spi_reg __iomem *reg_base;
|
2006-05-20 22:00:15 +00:00
|
|
|
u32 regval;
|
|
|
|
int ret = 0;
|
|
|
|
|
2009-06-18 23:49:08 +00:00
|
|
|
master = spi_alloc_master(dev, sizeof(struct mpc8xxx_spi));
|
2006-05-20 22:00:15 +00:00
|
|
|
if (master == NULL) {
|
|
|
|
ret = -ENOMEM;
|
|
|
|
goto err;
|
|
|
|
}
|
|
|
|
|
2009-03-31 22:24:37 +00:00
|
|
|
dev_set_drvdata(dev, master);
|
2006-05-20 22:00:15 +00:00
|
|
|
|
2014-12-03 06:56:17 +00:00
|
|
|
mpc8xxx_spi_probe(dev, mem, irq);
|
2009-06-17 23:26:04 +00:00
|
|
|
|
2010-10-12 10:18:31 +00:00
|
|
|
master->setup = fsl_spi_setup;
|
2013-02-15 15:52:27 +00:00
|
|
|
master->cleanup = fsl_spi_cleanup;
|
2014-12-03 06:56:17 +00:00
|
|
|
master->transfer_one_message = fsl_spi_do_one_msg;
|
2019-11-28 08:37:16 +00:00
|
|
|
master->use_gpio_descriptors = true;
|
2009-06-18 23:49:08 +00:00
|
|
|
|
|
|
|
mpc8xxx_spi = spi_master_get_devdata(master);
|
2013-02-15 15:52:25 +00:00
|
|
|
mpc8xxx_spi->max_bits_per_word = 32;
|
2013-02-15 15:52:24 +00:00
|
|
|
mpc8xxx_spi->type = fsl_spi_get_type(dev);
|
2009-06-18 23:49:08 +00:00
|
|
|
|
2010-10-12 10:18:31 +00:00
|
|
|
ret = fsl_spi_cpm_init(mpc8xxx_spi);
|
2009-10-12 16:49:27 +00:00
|
|
|
if (ret)
|
|
|
|
goto err_cpm_init;
|
|
|
|
|
2015-08-26 19:21:50 +00:00
|
|
|
mpc8xxx_spi->reg_base = devm_ioremap_resource(dev, mem);
|
2015-08-30 10:35:51 +00:00
|
|
|
if (IS_ERR(mpc8xxx_spi->reg_base)) {
|
|
|
|
ret = PTR_ERR(mpc8xxx_spi->reg_base);
|
2015-08-26 19:21:50 +00:00
|
|
|
goto err_probe;
|
2013-02-15 15:52:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (mpc8xxx_spi->type == TYPE_GRLIB)
|
|
|
|
fsl_spi_grlib_probe(dev);
|
|
|
|
|
2014-02-13 11:05:38 +00:00
|
|
|
master->bits_per_word_mask =
|
|
|
|
(SPI_BPW_RANGE_MASK(4, 16) | SPI_BPW_MASK(32)) &
|
|
|
|
SPI_BPW_RANGE_MASK(1, mpc8xxx_spi->max_bits_per_word);
|
|
|
|
|
2013-02-15 15:52:23 +00:00
|
|
|
if (mpc8xxx_spi->flags & SPI_QE_CPU_MODE)
|
|
|
|
mpc8xxx_spi->set_shifts = fsl_spi_qe_cpu_set_shifts;
|
|
|
|
|
|
|
|
if (mpc8xxx_spi->set_shifts)
|
|
|
|
/* 8 bits per word and MSB first */
|
|
|
|
mpc8xxx_spi->set_shifts(&mpc8xxx_spi->rx_shift,
|
|
|
|
&mpc8xxx_spi->tx_shift, 8, 1);
|
2007-07-17 11:04:12 +00:00
|
|
|
|
2006-05-20 22:00:15 +00:00
|
|
|
/* Register for SPI Interrupt */
|
2015-08-26 19:21:50 +00:00
|
|
|
ret = devm_request_irq(dev, mpc8xxx_spi->irq, fsl_spi_irq,
|
|
|
|
0, "fsl_spi", mpc8xxx_spi);
|
2006-05-20 22:00:15 +00:00
|
|
|
|
|
|
|
if (ret != 0)
|
2015-08-26 19:21:50 +00:00
|
|
|
goto err_probe;
|
2006-05-20 22:00:15 +00:00
|
|
|
|
2010-10-12 10:18:31 +00:00
|
|
|
reg_base = mpc8xxx_spi->reg_base;
|
2006-05-20 22:00:15 +00:00
|
|
|
|
|
|
|
/* SPI controller initializations */
|
2010-10-12 10:18:31 +00:00
|
|
|
mpc8xxx_spi_write_reg(®_base->mode, 0);
|
|
|
|
mpc8xxx_spi_write_reg(®_base->mask, 0);
|
|
|
|
mpc8xxx_spi_write_reg(®_base->command, 0);
|
|
|
|
mpc8xxx_spi_write_reg(®_base->event, 0xffffffff);
|
2006-05-20 22:00:15 +00:00
|
|
|
|
|
|
|
/* Enable SPI interface */
|
|
|
|
regval = pdata->initial_spmode | SPMODE_INIT_VAL | SPMODE_ENABLE;
|
2013-02-15 15:52:25 +00:00
|
|
|
if (mpc8xxx_spi->max_bits_per_word < 8) {
|
|
|
|
regval &= ~SPMODE_LEN(0xF);
|
|
|
|
regval |= SPMODE_LEN(mpc8xxx_spi->max_bits_per_word - 1);
|
|
|
|
}
|
2009-10-12 16:49:25 +00:00
|
|
|
if (mpc8xxx_spi->flags & SPI_QE_CPU_MODE)
|
2007-07-17 11:04:12 +00:00
|
|
|
regval |= SPMODE_OP;
|
|
|
|
|
2010-10-12 10:18:31 +00:00
|
|
|
mpc8xxx_spi_write_reg(®_base->mode, regval);
|
2008-05-12 21:02:30 +00:00
|
|
|
|
2015-08-26 19:21:50 +00:00
|
|
|
ret = devm_spi_register_master(dev, master);
|
2008-05-12 21:02:30 +00:00
|
|
|
if (ret < 0)
|
2015-08-26 19:21:50 +00:00
|
|
|
goto err_probe;
|
2006-05-20 22:00:15 +00:00
|
|
|
|
2010-10-12 10:18:31 +00:00
|
|
|
dev_info(dev, "at 0x%p (irq = %d), %s mode\n", reg_base,
|
2009-10-12 16:49:25 +00:00
|
|
|
mpc8xxx_spi->irq, mpc8xxx_spi_strmode(mpc8xxx_spi->flags));
|
2006-05-20 22:00:15 +00:00
|
|
|
|
2009-03-31 22:24:37 +00:00
|
|
|
return master;
|
2006-05-20 22:00:15 +00:00
|
|
|
|
2015-08-26 19:21:50 +00:00
|
|
|
err_probe:
|
2010-10-12 10:18:31 +00:00
|
|
|
fsl_spi_cpm_free(mpc8xxx_spi);
|
2009-10-12 16:49:27 +00:00
|
|
|
err_cpm_init:
|
2006-05-20 22:00:15 +00:00
|
|
|
spi_master_put(master);
|
|
|
|
err:
|
2009-03-31 22:24:37 +00:00
|
|
|
return ERR_PTR(ret);
|
2006-05-20 22:00:15 +00:00
|
|
|
}
|
|
|
|
|
2010-10-12 10:18:31 +00:00
|
|
|
static void fsl_spi_cs_control(struct spi_device *spi, bool on)
|
2009-03-31 22:24:37 +00:00
|
|
|
{
|
2019-08-04 00:35:39 +00:00
|
|
|
if (spi->cs_gpiod) {
|
|
|
|
gpiod_set_value(spi->cs_gpiod, on);
|
2019-03-06 10:32:05 +00:00
|
|
|
} else {
|
2019-08-04 00:35:39 +00:00
|
|
|
struct device *dev = spi->dev.parent->parent;
|
|
|
|
struct fsl_spi_platform_data *pdata = dev_get_platdata(dev);
|
|
|
|
struct mpc8xxx_spi_probe_info *pinfo = to_of_pinfo(pdata);
|
|
|
|
|
|
|
|
if (WARN_ON_ONCE(!pinfo->immr_spi_cs))
|
2019-03-06 10:32:05 +00:00
|
|
|
return;
|
2021-01-30 14:35:45 +00:00
|
|
|
iowrite32be(on ? 0 : SPI_BOOT_SEL_BIT, pinfo->immr_spi_cs);
|
2019-03-06 10:32:05 +00:00
|
|
|
}
|
2009-03-31 22:24:37 +00:00
|
|
|
}
|
|
|
|
|
2012-12-07 16:57:14 +00:00
|
|
|
static int of_fsl_spi_probe(struct platform_device *ofdev)
|
2009-03-31 22:24:37 +00:00
|
|
|
{
|
|
|
|
struct device *dev = &ofdev->dev;
|
2010-04-13 23:12:29 +00:00
|
|
|
struct device_node *np = ofdev->dev.of_node;
|
2009-03-31 22:24:37 +00:00
|
|
|
struct spi_master *master;
|
|
|
|
struct resource mem;
|
2020-01-14 16:02:40 +00:00
|
|
|
int irq, type;
|
|
|
|
int ret;
|
2021-04-01 14:03:50 +00:00
|
|
|
bool spisel_boot = false;
|
|
|
|
#if IS_ENABLED(CONFIG_FSL_SOC)
|
|
|
|
struct mpc8xxx_spi_probe_info *pinfo = NULL;
|
|
|
|
#endif
|
|
|
|
|
2009-03-31 22:24:37 +00:00
|
|
|
|
2011-02-23 04:02:43 +00:00
|
|
|
ret = of_mpc8xxx_spi_probe(ofdev);
|
2010-10-12 10:18:31 +00:00
|
|
|
if (ret)
|
|
|
|
return ret;
|
2009-03-31 22:24:37 +00:00
|
|
|
|
2013-02-15 15:52:26 +00:00
|
|
|
type = fsl_spi_get_type(&ofdev->dev);
|
|
|
|
if (type == TYPE_FSL) {
|
2019-08-04 00:35:39 +00:00
|
|
|
struct fsl_spi_platform_data *pdata = dev_get_platdata(dev);
|
|
|
|
#if IS_ENABLED(CONFIG_FSL_SOC)
|
2021-04-01 14:03:50 +00:00
|
|
|
pinfo = to_of_pinfo(pdata);
|
2019-08-04 00:35:39 +00:00
|
|
|
|
2020-11-27 15:29:47 +00:00
|
|
|
spisel_boot = of_property_read_bool(np, "fsl,spisel_boot");
|
2019-08-04 00:35:39 +00:00
|
|
|
if (spisel_boot) {
|
|
|
|
pinfo->immr_spi_cs = ioremap(get_immrbase() + IMMR_SPI_CS_OFFSET, 4);
|
2020-01-14 16:02:40 +00:00
|
|
|
if (!pinfo->immr_spi_cs)
|
|
|
|
return -ENOMEM;
|
2019-08-04 00:35:39 +00:00
|
|
|
}
|
|
|
|
#endif
|
2019-11-28 08:37:18 +00:00
|
|
|
/*
|
|
|
|
* Handle the case where we have one hardwired (always selected)
|
|
|
|
* device on the first "chipselect". Else we let the core code
|
|
|
|
* handle any GPIOs or native chip selects and assign the
|
|
|
|
* appropriate callback for dealing with the CS lines. This isn't
|
|
|
|
* supported on the GRLIB variant.
|
|
|
|
*/
|
|
|
|
ret = gpiod_count(dev, "cs");
|
2020-11-27 15:29:47 +00:00
|
|
|
if (ret < 0)
|
|
|
|
ret = 0;
|
|
|
|
if (ret == 0 && !spisel_boot) {
|
2019-11-28 08:37:18 +00:00
|
|
|
pdata->max_chipselect = 1;
|
2020-11-27 15:29:47 +00:00
|
|
|
} else {
|
|
|
|
pdata->max_chipselect = ret + spisel_boot;
|
2019-11-28 08:37:18 +00:00
|
|
|
pdata->cs_control = fsl_spi_cs_control;
|
2020-11-27 15:29:47 +00:00
|
|
|
}
|
2013-02-15 15:52:26 +00:00
|
|
|
}
|
2009-03-31 22:24:37 +00:00
|
|
|
|
|
|
|
ret = of_address_to_resource(np, 0, &mem);
|
|
|
|
if (ret)
|
2021-04-01 14:03:50 +00:00
|
|
|
goto unmap_out;
|
2009-03-31 22:24:37 +00:00
|
|
|
|
2019-12-12 17:47:24 +00:00
|
|
|
irq = platform_get_irq(ofdev, 0);
|
2021-04-01 14:03:50 +00:00
|
|
|
if (irq < 0) {
|
|
|
|
ret = irq;
|
|
|
|
goto unmap_out;
|
|
|
|
}
|
2009-03-31 22:24:37 +00:00
|
|
|
|
2013-02-15 15:52:21 +00:00
|
|
|
master = fsl_spi_probe(dev, &mem, irq);
|
2009-03-31 22:24:37 +00:00
|
|
|
|
2020-01-14 16:02:40 +00:00
|
|
|
return PTR_ERR_OR_ZERO(master);
|
2021-04-01 14:03:50 +00:00
|
|
|
|
|
|
|
unmap_out:
|
|
|
|
#if IS_ENABLED(CONFIG_FSL_SOC)
|
|
|
|
if (spisel_boot)
|
|
|
|
iounmap(pinfo->immr_spi_cs);
|
|
|
|
#endif
|
|
|
|
return ret;
|
2009-03-31 22:24:37 +00:00
|
|
|
}
|
|
|
|
|
2012-12-07 16:57:14 +00:00
|
|
|
static int of_fsl_spi_remove(struct platform_device *ofdev)
|
2009-03-31 22:24:37 +00:00
|
|
|
{
|
2013-05-23 10:20:40 +00:00
|
|
|
struct spi_master *master = platform_get_drvdata(ofdev);
|
2013-02-15 15:52:26 +00:00
|
|
|
struct mpc8xxx_spi *mpc8xxx_spi = spi_master_get_devdata(master);
|
2009-03-31 22:24:37 +00:00
|
|
|
|
2015-08-26 19:21:53 +00:00
|
|
|
fsl_spi_cpm_free(mpc8xxx_spi);
|
2009-03-31 22:24:37 +00:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2011-02-23 04:02:43 +00:00
|
|
|
static struct platform_driver of_fsl_spi_driver = {
|
2010-04-13 23:13:02 +00:00
|
|
|
.driver = {
|
2010-10-12 10:18:31 +00:00
|
|
|
.name = "fsl_spi",
|
|
|
|
.of_match_table = of_fsl_spi_match,
|
2010-04-13 23:13:02 +00:00
|
|
|
},
|
2010-10-12 10:18:31 +00:00
|
|
|
.probe = of_fsl_spi_probe,
|
2012-12-07 16:57:14 +00:00
|
|
|
.remove = of_fsl_spi_remove,
|
2009-03-31 22:24:37 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
#ifdef CONFIG_MPC832x_RDB
|
|
|
|
/*
|
2010-10-12 10:18:31 +00:00
|
|
|
* XXX XXX XXX
|
2009-03-31 22:24:37 +00:00
|
|
|
* This is "legacy" platform driver, was used by the MPC8323E-RDB boards
|
|
|
|
* only. The driver should go away soon, since newer MPC8323E-RDB's device
|
|
|
|
* tree can work with OpenFirmware driver. But for now we support old trees
|
|
|
|
* as well.
|
|
|
|
*/
|
2012-12-07 16:57:14 +00:00
|
|
|
static int plat_mpc8xxx_spi_probe(struct platform_device *pdev)
|
2009-03-31 22:24:37 +00:00
|
|
|
{
|
|
|
|
struct resource *mem;
|
2010-01-20 20:49:44 +00:00
|
|
|
int irq;
|
2009-03-31 22:24:37 +00:00
|
|
|
struct spi_master *master;
|
|
|
|
|
2013-07-30 07:58:59 +00:00
|
|
|
if (!dev_get_platdata(&pdev->dev))
|
2009-03-31 22:24:37 +00:00
|
|
|
return -EINVAL;
|
|
|
|
|
|
|
|
mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
|
|
|
|
if (!mem)
|
|
|
|
return -EINVAL;
|
|
|
|
|
|
|
|
irq = platform_get_irq(pdev, 0);
|
2010-01-20 20:49:44 +00:00
|
|
|
if (irq <= 0)
|
2009-03-31 22:24:37 +00:00
|
|
|
return -EINVAL;
|
|
|
|
|
2010-10-12 10:18:31 +00:00
|
|
|
master = fsl_spi_probe(&pdev->dev, mem, irq);
|
2013-07-15 01:50:32 +00:00
|
|
|
return PTR_ERR_OR_ZERO(master);
|
2009-03-31 22:24:37 +00:00
|
|
|
}
|
|
|
|
|
2012-12-07 16:57:14 +00:00
|
|
|
static int plat_mpc8xxx_spi_remove(struct platform_device *pdev)
|
2009-03-31 22:24:37 +00:00
|
|
|
{
|
2015-08-26 19:21:53 +00:00
|
|
|
struct spi_master *master = platform_get_drvdata(pdev);
|
|
|
|
struct mpc8xxx_spi *mpc8xxx_spi = spi_master_get_devdata(master);
|
|
|
|
|
|
|
|
fsl_spi_cpm_free(mpc8xxx_spi);
|
|
|
|
|
|
|
|
return 0;
|
2009-03-31 22:24:37 +00:00
|
|
|
}
|
|
|
|
|
2009-06-18 23:49:08 +00:00
|
|
|
MODULE_ALIAS("platform:mpc8xxx_spi");
|
|
|
|
static struct platform_driver mpc8xxx_spi_driver = {
|
|
|
|
.probe = plat_mpc8xxx_spi_probe,
|
2012-12-07 16:57:14 +00:00
|
|
|
.remove = plat_mpc8xxx_spi_remove,
|
2006-05-20 22:00:15 +00:00
|
|
|
.driver = {
|
2009-06-18 23:49:08 +00:00
|
|
|
.name = "mpc8xxx_spi",
|
2006-05-20 22:00:15 +00:00
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2009-03-31 22:24:37 +00:00
|
|
|
static bool legacy_driver_failed;
|
|
|
|
|
|
|
|
static void __init legacy_driver_register(void)
|
|
|
|
{
|
2009-06-18 23:49:08 +00:00
|
|
|
legacy_driver_failed = platform_driver_register(&mpc8xxx_spi_driver);
|
2009-03-31 22:24:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
static void __exit legacy_driver_unregister(void)
|
|
|
|
{
|
|
|
|
if (legacy_driver_failed)
|
|
|
|
return;
|
2009-06-18 23:49:08 +00:00
|
|
|
platform_driver_unregister(&mpc8xxx_spi_driver);
|
2009-03-31 22:24:37 +00:00
|
|
|
}
|
|
|
|
#else
|
|
|
|
static void __init legacy_driver_register(void) {}
|
|
|
|
static void __exit legacy_driver_unregister(void) {}
|
|
|
|
#endif /* CONFIG_MPC832x_RDB */
|
|
|
|
|
2010-10-12 10:18:31 +00:00
|
|
|
static int __init fsl_spi_init(void)
|
2006-05-20 22:00:15 +00:00
|
|
|
{
|
2009-03-31 22:24:37 +00:00
|
|
|
legacy_driver_register();
|
2011-02-23 04:02:43 +00:00
|
|
|
return platform_driver_register(&of_fsl_spi_driver);
|
2006-05-20 22:00:15 +00:00
|
|
|
}
|
2010-10-12 10:18:31 +00:00
|
|
|
module_init(fsl_spi_init);
|
2006-05-20 22:00:15 +00:00
|
|
|
|
2010-10-12 10:18:31 +00:00
|
|
|
static void __exit fsl_spi_exit(void)
|
2006-05-20 22:00:15 +00:00
|
|
|
{
|
2011-02-23 04:02:43 +00:00
|
|
|
platform_driver_unregister(&of_fsl_spi_driver);
|
2009-03-31 22:24:37 +00:00
|
|
|
legacy_driver_unregister();
|
2006-05-20 22:00:15 +00:00
|
|
|
}
|
2010-10-12 10:18:31 +00:00
|
|
|
module_exit(fsl_spi_exit);
|
2006-05-20 22:00:15 +00:00
|
|
|
|
|
|
|
MODULE_AUTHOR("Kumar Gala");
|
2010-10-12 10:18:31 +00:00
|
|
|
MODULE_DESCRIPTION("Simple Freescale SPI Driver");
|
2006-05-20 22:00:15 +00:00
|
|
|
MODULE_LICENSE("GPL");
|