mirror of
https://github.com/torvalds/linux.git
synced 2024-12-22 19:01:37 +00:00
4793f2ebff
Now that the SPDX tag is in all tty files, that identifies the license in a specific and legally-defined manner. So the extra GPL text wording can be removed as it is no longer needed at all. This is done on a quest to remove the 700+ different ways that files in the kernel describe the GPL license text. And there's unneeded stuff like the address (sometimes incorrect) for the FSF which is never needed. No copyright headers or other non-license-description text was removed. Cc: Jiri Slaby <jslaby@suse.com> Cc: Eric Anholt <eric@anholt.net> Cc: Stefan Wahren <stefan.wahren@i2se.com> Cc: Florian Fainelli <f.fainelli@gmail.com> Cc: Ray Jui <rjui@broadcom.com> Cc: Scott Branden <sbranden@broadcom.com> Cc: bcm-kernel-feedback-list@broadcom.com Cc: "James E.J. Bottomley" <jejb@parisc-linux.org> Cc: Helge Deller <deller@gmx.de> Cc: Joachim Eastwood <manabian@gmail.com> Cc: Matthias Brugger <matthias.bgg@gmail.com> Cc: Masahiro Yamada <yamada.masahiro@socionext.com> Cc: Tobias Klauser <tklauser@distanz.ch> Cc: Russell King <linux@armlinux.org.uk> Cc: Vineet Gupta <vgupta@synopsys.com> Cc: Richard Genoud <richard.genoud@gmail.com> Cc: Alexander Shiyan <shc_work@mail.ru> Cc: Baruch Siach <baruch@tkos.co.il> Cc: Pat Gefre <pfg@sgi.com> Cc: "Guilherme G. Piccoli" <gpiccoli@linux.vnet.ibm.com> Cc: Jason Wessel <jason.wessel@windriver.com> Cc: Vladimir Zapolskiy <vz@mleia.com> Cc: Sylvain Lemieux <slemieux.tyco@gmail.com> Cc: Carlo Caione <carlo@caione.org> Cc: Kevin Hilman <khilman@baylibre.com> Cc: Liviu Dudau <liviu.dudau@arm.com> Cc: Sudeep Holla <sudeep.holla@arm.com> Cc: Lorenzo Pieralisi <lorenzo.pieralisi@arm.com> Cc: Andy Gross <andy.gross@linaro.org> Cc: David Brown <david.brown@linaro.org> Cc: "Andreas Färber" <afaerber@suse.de> Cc: Benjamin Herrenschmidt <benh@kernel.crashing.org> Cc: Paul Mackerras <paulus@samba.org> Cc: Michael Ellerman <mpe@ellerman.id.au> Cc: Kevin Cernekee <cernekee@gmail.com> Cc: Laxman Dewangan <ldewangan@nvidia.com> Cc: Thierry Reding <thierry.reding@gmail.com> Cc: Jonathan Hunter <jonathanh@nvidia.com> Cc: Barry Song <baohua@kernel.org> Cc: Patrice Chotard <patrice.chotard@st.com> Cc: Maxime Coquelin <mcoquelin.stm32@gmail.com> Cc: Alexandre Torgue <alexandre.torgue@st.com> Cc: Chris Metcalf <cmetcalf@mellanox.com> Cc: Peter Korsgaard <jacmet@sunsite.dk> Cc: Timur Tabi <timur@tabi.org> Cc: Tony Prisk <linux@prisktech.co.nz> Cc: Michal Simek <michal.simek@xilinx.com> Cc: "Sören Brinkmann" <soren.brinkmann@xilinx.com> Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
1034 lines
26 KiB
C
1034 lines
26 KiB
C
// SPDX-License-Identifier: GPL-2.0+
|
|
/*
|
|
* NXP (Philips) SCC+++(SCN+++) serial driver
|
|
*
|
|
* Copyright (C) 2012 Alexander Shiyan <shc_work@mail.ru>
|
|
*
|
|
* Based on sc26xx.c, by Thomas Bogendörfer (tsbogend@alpha.franken.de)
|
|
*/
|
|
|
|
#if defined(CONFIG_SERIAL_SCCNXP_CONSOLE) && defined(CONFIG_MAGIC_SYSRQ)
|
|
#define SUPPORT_SYSRQ
|
|
#endif
|
|
|
|
#include <linux/clk.h>
|
|
#include <linux/err.h>
|
|
#include <linux/module.h>
|
|
#include <linux/device.h>
|
|
#include <linux/console.h>
|
|
#include <linux/serial_core.h>
|
|
#include <linux/serial.h>
|
|
#include <linux/io.h>
|
|
#include <linux/tty.h>
|
|
#include <linux/tty_flip.h>
|
|
#include <linux/spinlock.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/platform_data/serial-sccnxp.h>
|
|
#include <linux/regulator/consumer.h>
|
|
|
|
#define SCCNXP_NAME "uart-sccnxp"
|
|
#define SCCNXP_MAJOR 204
|
|
#define SCCNXP_MINOR 205
|
|
|
|
#define SCCNXP_MR_REG (0x00)
|
|
# define MR0_BAUD_NORMAL (0 << 0)
|
|
# define MR0_BAUD_EXT1 (1 << 0)
|
|
# define MR0_BAUD_EXT2 (5 << 0)
|
|
# define MR0_FIFO (1 << 3)
|
|
# define MR0_TXLVL (1 << 4)
|
|
# define MR1_BITS_5 (0 << 0)
|
|
# define MR1_BITS_6 (1 << 0)
|
|
# define MR1_BITS_7 (2 << 0)
|
|
# define MR1_BITS_8 (3 << 0)
|
|
# define MR1_PAR_EVN (0 << 2)
|
|
# define MR1_PAR_ODD (1 << 2)
|
|
# define MR1_PAR_NO (4 << 2)
|
|
# define MR2_STOP1 (7 << 0)
|
|
# define MR2_STOP2 (0xf << 0)
|
|
#define SCCNXP_SR_REG (0x01)
|
|
#define SCCNXP_CSR_REG SCCNXP_SR_REG
|
|
# define SR_RXRDY (1 << 0)
|
|
# define SR_FULL (1 << 1)
|
|
# define SR_TXRDY (1 << 2)
|
|
# define SR_TXEMT (1 << 3)
|
|
# define SR_OVR (1 << 4)
|
|
# define SR_PE (1 << 5)
|
|
# define SR_FE (1 << 6)
|
|
# define SR_BRK (1 << 7)
|
|
#define SCCNXP_CR_REG (0x02)
|
|
# define CR_RX_ENABLE (1 << 0)
|
|
# define CR_RX_DISABLE (1 << 1)
|
|
# define CR_TX_ENABLE (1 << 2)
|
|
# define CR_TX_DISABLE (1 << 3)
|
|
# define CR_CMD_MRPTR1 (0x01 << 4)
|
|
# define CR_CMD_RX_RESET (0x02 << 4)
|
|
# define CR_CMD_TX_RESET (0x03 << 4)
|
|
# define CR_CMD_STATUS_RESET (0x04 << 4)
|
|
# define CR_CMD_BREAK_RESET (0x05 << 4)
|
|
# define CR_CMD_START_BREAK (0x06 << 4)
|
|
# define CR_CMD_STOP_BREAK (0x07 << 4)
|
|
# define CR_CMD_MRPTR0 (0x0b << 4)
|
|
#define SCCNXP_RHR_REG (0x03)
|
|
#define SCCNXP_THR_REG SCCNXP_RHR_REG
|
|
#define SCCNXP_IPCR_REG (0x04)
|
|
#define SCCNXP_ACR_REG SCCNXP_IPCR_REG
|
|
# define ACR_BAUD0 (0 << 7)
|
|
# define ACR_BAUD1 (1 << 7)
|
|
# define ACR_TIMER_MODE (6 << 4)
|
|
#define SCCNXP_ISR_REG (0x05)
|
|
#define SCCNXP_IMR_REG SCCNXP_ISR_REG
|
|
# define IMR_TXRDY (1 << 0)
|
|
# define IMR_RXRDY (1 << 1)
|
|
# define ISR_TXRDY(x) (1 << ((x * 4) + 0))
|
|
# define ISR_RXRDY(x) (1 << ((x * 4) + 1))
|
|
#define SCCNXP_IPR_REG (0x0d)
|
|
#define SCCNXP_OPCR_REG SCCNXP_IPR_REG
|
|
#define SCCNXP_SOP_REG (0x0e)
|
|
#define SCCNXP_ROP_REG (0x0f)
|
|
|
|
/* Route helpers */
|
|
#define MCTRL_MASK(sig) (0xf << (sig))
|
|
#define MCTRL_IBIT(cfg, sig) ((((cfg) >> (sig)) & 0xf) - LINE_IP0)
|
|
#define MCTRL_OBIT(cfg, sig) ((((cfg) >> (sig)) & 0xf) - LINE_OP0)
|
|
|
|
#define SCCNXP_HAVE_IO 0x00000001
|
|
#define SCCNXP_HAVE_MR0 0x00000002
|
|
|
|
struct sccnxp_chip {
|
|
const char *name;
|
|
unsigned int nr;
|
|
unsigned long freq_min;
|
|
unsigned long freq_std;
|
|
unsigned long freq_max;
|
|
unsigned int flags;
|
|
unsigned int fifosize;
|
|
};
|
|
|
|
struct sccnxp_port {
|
|
struct uart_driver uart;
|
|
struct uart_port port[SCCNXP_MAX_UARTS];
|
|
bool opened[SCCNXP_MAX_UARTS];
|
|
|
|
int irq;
|
|
u8 imr;
|
|
|
|
struct sccnxp_chip *chip;
|
|
|
|
#ifdef CONFIG_SERIAL_SCCNXP_CONSOLE
|
|
struct console console;
|
|
#endif
|
|
|
|
spinlock_t lock;
|
|
|
|
bool poll;
|
|
struct timer_list timer;
|
|
|
|
struct sccnxp_pdata pdata;
|
|
|
|
struct regulator *regulator;
|
|
};
|
|
|
|
static const struct sccnxp_chip sc2681 = {
|
|
.name = "SC2681",
|
|
.nr = 2,
|
|
.freq_min = 1000000,
|
|
.freq_std = 3686400,
|
|
.freq_max = 4000000,
|
|
.flags = SCCNXP_HAVE_IO,
|
|
.fifosize = 3,
|
|
};
|
|
|
|
static const struct sccnxp_chip sc2691 = {
|
|
.name = "SC2691",
|
|
.nr = 1,
|
|
.freq_min = 1000000,
|
|
.freq_std = 3686400,
|
|
.freq_max = 4000000,
|
|
.flags = 0,
|
|
.fifosize = 3,
|
|
};
|
|
|
|
static const struct sccnxp_chip sc2692 = {
|
|
.name = "SC2692",
|
|
.nr = 2,
|
|
.freq_min = 1000000,
|
|
.freq_std = 3686400,
|
|
.freq_max = 4000000,
|
|
.flags = SCCNXP_HAVE_IO,
|
|
.fifosize = 3,
|
|
};
|
|
|
|
static const struct sccnxp_chip sc2891 = {
|
|
.name = "SC2891",
|
|
.nr = 1,
|
|
.freq_min = 100000,
|
|
.freq_std = 3686400,
|
|
.freq_max = 8000000,
|
|
.flags = SCCNXP_HAVE_IO | SCCNXP_HAVE_MR0,
|
|
.fifosize = 16,
|
|
};
|
|
|
|
static const struct sccnxp_chip sc2892 = {
|
|
.name = "SC2892",
|
|
.nr = 2,
|
|
.freq_min = 100000,
|
|
.freq_std = 3686400,
|
|
.freq_max = 8000000,
|
|
.flags = SCCNXP_HAVE_IO | SCCNXP_HAVE_MR0,
|
|
.fifosize = 16,
|
|
};
|
|
|
|
static const struct sccnxp_chip sc28202 = {
|
|
.name = "SC28202",
|
|
.nr = 2,
|
|
.freq_min = 1000000,
|
|
.freq_std = 14745600,
|
|
.freq_max = 50000000,
|
|
.flags = SCCNXP_HAVE_IO | SCCNXP_HAVE_MR0,
|
|
.fifosize = 256,
|
|
};
|
|
|
|
static const struct sccnxp_chip sc68681 = {
|
|
.name = "SC68681",
|
|
.nr = 2,
|
|
.freq_min = 1000000,
|
|
.freq_std = 3686400,
|
|
.freq_max = 4000000,
|
|
.flags = SCCNXP_HAVE_IO,
|
|
.fifosize = 3,
|
|
};
|
|
|
|
static const struct sccnxp_chip sc68692 = {
|
|
.name = "SC68692",
|
|
.nr = 2,
|
|
.freq_min = 1000000,
|
|
.freq_std = 3686400,
|
|
.freq_max = 4000000,
|
|
.flags = SCCNXP_HAVE_IO,
|
|
.fifosize = 3,
|
|
};
|
|
|
|
static inline u8 sccnxp_read(struct uart_port *port, u8 reg)
|
|
{
|
|
return readb(port->membase + (reg << port->regshift));
|
|
}
|
|
|
|
static inline void sccnxp_write(struct uart_port *port, u8 reg, u8 v)
|
|
{
|
|
writeb(v, port->membase + (reg << port->regshift));
|
|
}
|
|
|
|
static inline u8 sccnxp_port_read(struct uart_port *port, u8 reg)
|
|
{
|
|
return sccnxp_read(port, (port->line << 3) + reg);
|
|
}
|
|
|
|
static inline void sccnxp_port_write(struct uart_port *port, u8 reg, u8 v)
|
|
{
|
|
sccnxp_write(port, (port->line << 3) + reg, v);
|
|
}
|
|
|
|
static int sccnxp_update_best_err(int a, int b, int *besterr)
|
|
{
|
|
int err = abs(a - b);
|
|
|
|
if ((*besterr < 0) || (*besterr > err)) {
|
|
*besterr = err;
|
|
return 0;
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
static const struct {
|
|
u8 csr;
|
|
u8 acr;
|
|
u8 mr0;
|
|
int baud;
|
|
} baud_std[] = {
|
|
{ 0, ACR_BAUD0, MR0_BAUD_NORMAL, 50, },
|
|
{ 0, ACR_BAUD1, MR0_BAUD_NORMAL, 75, },
|
|
{ 1, ACR_BAUD0, MR0_BAUD_NORMAL, 110, },
|
|
{ 2, ACR_BAUD0, MR0_BAUD_NORMAL, 134, },
|
|
{ 3, ACR_BAUD1, MR0_BAUD_NORMAL, 150, },
|
|
{ 3, ACR_BAUD0, MR0_BAUD_NORMAL, 200, },
|
|
{ 4, ACR_BAUD0, MR0_BAUD_NORMAL, 300, },
|
|
{ 0, ACR_BAUD1, MR0_BAUD_EXT1, 450, },
|
|
{ 1, ACR_BAUD0, MR0_BAUD_EXT2, 880, },
|
|
{ 3, ACR_BAUD1, MR0_BAUD_EXT1, 900, },
|
|
{ 5, ACR_BAUD0, MR0_BAUD_NORMAL, 600, },
|
|
{ 7, ACR_BAUD0, MR0_BAUD_NORMAL, 1050, },
|
|
{ 2, ACR_BAUD0, MR0_BAUD_EXT2, 1076, },
|
|
{ 6, ACR_BAUD0, MR0_BAUD_NORMAL, 1200, },
|
|
{ 10, ACR_BAUD1, MR0_BAUD_NORMAL, 1800, },
|
|
{ 7, ACR_BAUD1, MR0_BAUD_NORMAL, 2000, },
|
|
{ 8, ACR_BAUD0, MR0_BAUD_NORMAL, 2400, },
|
|
{ 5, ACR_BAUD1, MR0_BAUD_EXT1, 3600, },
|
|
{ 9, ACR_BAUD0, MR0_BAUD_NORMAL, 4800, },
|
|
{ 10, ACR_BAUD0, MR0_BAUD_NORMAL, 7200, },
|
|
{ 11, ACR_BAUD0, MR0_BAUD_NORMAL, 9600, },
|
|
{ 8, ACR_BAUD0, MR0_BAUD_EXT1, 14400, },
|
|
{ 12, ACR_BAUD1, MR0_BAUD_NORMAL, 19200, },
|
|
{ 9, ACR_BAUD0, MR0_BAUD_EXT1, 28800, },
|
|
{ 12, ACR_BAUD0, MR0_BAUD_NORMAL, 38400, },
|
|
{ 11, ACR_BAUD0, MR0_BAUD_EXT1, 57600, },
|
|
{ 12, ACR_BAUD1, MR0_BAUD_EXT1, 115200, },
|
|
{ 12, ACR_BAUD0, MR0_BAUD_EXT1, 230400, },
|
|
{ 0, 0, 0, 0 }
|
|
};
|
|
|
|
static int sccnxp_set_baud(struct uart_port *port, int baud)
|
|
{
|
|
struct sccnxp_port *s = dev_get_drvdata(port->dev);
|
|
int div_std, tmp_baud, bestbaud = baud, besterr = -1;
|
|
struct sccnxp_chip *chip = s->chip;
|
|
u8 i, acr = 0, csr = 0, mr0 = 0;
|
|
|
|
/* Find best baud from table */
|
|
for (i = 0; baud_std[i].baud && besterr; i++) {
|
|
if (baud_std[i].mr0 && !(chip->flags & SCCNXP_HAVE_MR0))
|
|
continue;
|
|
div_std = DIV_ROUND_CLOSEST(chip->freq_std, baud_std[i].baud);
|
|
tmp_baud = DIV_ROUND_CLOSEST(port->uartclk, div_std);
|
|
if (!sccnxp_update_best_err(baud, tmp_baud, &besterr)) {
|
|
acr = baud_std[i].acr;
|
|
csr = baud_std[i].csr;
|
|
mr0 = baud_std[i].mr0;
|
|
bestbaud = tmp_baud;
|
|
}
|
|
}
|
|
|
|
if (chip->flags & SCCNXP_HAVE_MR0) {
|
|
/* Enable FIFO, set half level for TX */
|
|
mr0 |= MR0_FIFO | MR0_TXLVL;
|
|
/* Update MR0 */
|
|
sccnxp_port_write(port, SCCNXP_CR_REG, CR_CMD_MRPTR0);
|
|
sccnxp_port_write(port, SCCNXP_MR_REG, mr0);
|
|
}
|
|
|
|
sccnxp_port_write(port, SCCNXP_ACR_REG, acr | ACR_TIMER_MODE);
|
|
sccnxp_port_write(port, SCCNXP_CSR_REG, (csr << 4) | csr);
|
|
|
|
if (baud != bestbaud)
|
|
dev_dbg(port->dev, "Baudrate desired: %i, calculated: %i\n",
|
|
baud, bestbaud);
|
|
|
|
return bestbaud;
|
|
}
|
|
|
|
static void sccnxp_enable_irq(struct uart_port *port, int mask)
|
|
{
|
|
struct sccnxp_port *s = dev_get_drvdata(port->dev);
|
|
|
|
s->imr |= mask << (port->line * 4);
|
|
sccnxp_write(port, SCCNXP_IMR_REG, s->imr);
|
|
}
|
|
|
|
static void sccnxp_disable_irq(struct uart_port *port, int mask)
|
|
{
|
|
struct sccnxp_port *s = dev_get_drvdata(port->dev);
|
|
|
|
s->imr &= ~(mask << (port->line * 4));
|
|
sccnxp_write(port, SCCNXP_IMR_REG, s->imr);
|
|
}
|
|
|
|
static void sccnxp_set_bit(struct uart_port *port, int sig, int state)
|
|
{
|
|
u8 bitmask;
|
|
struct sccnxp_port *s = dev_get_drvdata(port->dev);
|
|
|
|
if (s->pdata.mctrl_cfg[port->line] & MCTRL_MASK(sig)) {
|
|
bitmask = 1 << MCTRL_OBIT(s->pdata.mctrl_cfg[port->line], sig);
|
|
if (state)
|
|
sccnxp_write(port, SCCNXP_SOP_REG, bitmask);
|
|
else
|
|
sccnxp_write(port, SCCNXP_ROP_REG, bitmask);
|
|
}
|
|
}
|
|
|
|
static void sccnxp_handle_rx(struct uart_port *port)
|
|
{
|
|
u8 sr;
|
|
unsigned int ch, flag;
|
|
|
|
for (;;) {
|
|
sr = sccnxp_port_read(port, SCCNXP_SR_REG);
|
|
if (!(sr & SR_RXRDY))
|
|
break;
|
|
sr &= SR_PE | SR_FE | SR_OVR | SR_BRK;
|
|
|
|
ch = sccnxp_port_read(port, SCCNXP_RHR_REG);
|
|
|
|
port->icount.rx++;
|
|
flag = TTY_NORMAL;
|
|
|
|
if (unlikely(sr)) {
|
|
if (sr & SR_BRK) {
|
|
port->icount.brk++;
|
|
sccnxp_port_write(port, SCCNXP_CR_REG,
|
|
CR_CMD_BREAK_RESET);
|
|
if (uart_handle_break(port))
|
|
continue;
|
|
} else if (sr & SR_PE)
|
|
port->icount.parity++;
|
|
else if (sr & SR_FE)
|
|
port->icount.frame++;
|
|
else if (sr & SR_OVR) {
|
|
port->icount.overrun++;
|
|
sccnxp_port_write(port, SCCNXP_CR_REG,
|
|
CR_CMD_STATUS_RESET);
|
|
}
|
|
|
|
sr &= port->read_status_mask;
|
|
if (sr & SR_BRK)
|
|
flag = TTY_BREAK;
|
|
else if (sr & SR_PE)
|
|
flag = TTY_PARITY;
|
|
else if (sr & SR_FE)
|
|
flag = TTY_FRAME;
|
|
else if (sr & SR_OVR)
|
|
flag = TTY_OVERRUN;
|
|
}
|
|
|
|
if (uart_handle_sysrq_char(port, ch))
|
|
continue;
|
|
|
|
if (sr & port->ignore_status_mask)
|
|
continue;
|
|
|
|
uart_insert_char(port, sr, SR_OVR, ch, flag);
|
|
}
|
|
|
|
tty_flip_buffer_push(&port->state->port);
|
|
}
|
|
|
|
static void sccnxp_handle_tx(struct uart_port *port)
|
|
{
|
|
u8 sr;
|
|
struct circ_buf *xmit = &port->state->xmit;
|
|
struct sccnxp_port *s = dev_get_drvdata(port->dev);
|
|
|
|
if (unlikely(port->x_char)) {
|
|
sccnxp_port_write(port, SCCNXP_THR_REG, port->x_char);
|
|
port->icount.tx++;
|
|
port->x_char = 0;
|
|
return;
|
|
}
|
|
|
|
if (uart_circ_empty(xmit) || uart_tx_stopped(port)) {
|
|
/* Disable TX if FIFO is empty */
|
|
if (sccnxp_port_read(port, SCCNXP_SR_REG) & SR_TXEMT) {
|
|
sccnxp_disable_irq(port, IMR_TXRDY);
|
|
|
|
/* Set direction to input */
|
|
if (s->chip->flags & SCCNXP_HAVE_IO)
|
|
sccnxp_set_bit(port, DIR_OP, 0);
|
|
}
|
|
return;
|
|
}
|
|
|
|
while (!uart_circ_empty(xmit)) {
|
|
sr = sccnxp_port_read(port, SCCNXP_SR_REG);
|
|
if (!(sr & SR_TXRDY))
|
|
break;
|
|
|
|
sccnxp_port_write(port, SCCNXP_THR_REG, xmit->buf[xmit->tail]);
|
|
xmit->tail = (xmit->tail + 1) & (UART_XMIT_SIZE - 1);
|
|
port->icount.tx++;
|
|
}
|
|
|
|
if (uart_circ_chars_pending(xmit) < WAKEUP_CHARS)
|
|
uart_write_wakeup(port);
|
|
}
|
|
|
|
static void sccnxp_handle_events(struct sccnxp_port *s)
|
|
{
|
|
int i;
|
|
u8 isr;
|
|
|
|
do {
|
|
isr = sccnxp_read(&s->port[0], SCCNXP_ISR_REG);
|
|
isr &= s->imr;
|
|
if (!isr)
|
|
break;
|
|
|
|
for (i = 0; i < s->uart.nr; i++) {
|
|
if (s->opened[i] && (isr & ISR_RXRDY(i)))
|
|
sccnxp_handle_rx(&s->port[i]);
|
|
if (s->opened[i] && (isr & ISR_TXRDY(i)))
|
|
sccnxp_handle_tx(&s->port[i]);
|
|
}
|
|
} while (1);
|
|
}
|
|
|
|
static void sccnxp_timer(struct timer_list *t)
|
|
{
|
|
struct sccnxp_port *s = from_timer(s, t, timer);
|
|
unsigned long flags;
|
|
|
|
spin_lock_irqsave(&s->lock, flags);
|
|
sccnxp_handle_events(s);
|
|
spin_unlock_irqrestore(&s->lock, flags);
|
|
|
|
mod_timer(&s->timer, jiffies + usecs_to_jiffies(s->pdata.poll_time_us));
|
|
}
|
|
|
|
static irqreturn_t sccnxp_ist(int irq, void *dev_id)
|
|
{
|
|
struct sccnxp_port *s = (struct sccnxp_port *)dev_id;
|
|
unsigned long flags;
|
|
|
|
spin_lock_irqsave(&s->lock, flags);
|
|
sccnxp_handle_events(s);
|
|
spin_unlock_irqrestore(&s->lock, flags);
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
static void sccnxp_start_tx(struct uart_port *port)
|
|
{
|
|
struct sccnxp_port *s = dev_get_drvdata(port->dev);
|
|
unsigned long flags;
|
|
|
|
spin_lock_irqsave(&s->lock, flags);
|
|
|
|
/* Set direction to output */
|
|
if (s->chip->flags & SCCNXP_HAVE_IO)
|
|
sccnxp_set_bit(port, DIR_OP, 1);
|
|
|
|
sccnxp_enable_irq(port, IMR_TXRDY);
|
|
|
|
spin_unlock_irqrestore(&s->lock, flags);
|
|
}
|
|
|
|
static void sccnxp_stop_tx(struct uart_port *port)
|
|
{
|
|
/* Do nothing */
|
|
}
|
|
|
|
static void sccnxp_stop_rx(struct uart_port *port)
|
|
{
|
|
struct sccnxp_port *s = dev_get_drvdata(port->dev);
|
|
unsigned long flags;
|
|
|
|
spin_lock_irqsave(&s->lock, flags);
|
|
sccnxp_port_write(port, SCCNXP_CR_REG, CR_RX_DISABLE);
|
|
spin_unlock_irqrestore(&s->lock, flags);
|
|
}
|
|
|
|
static unsigned int sccnxp_tx_empty(struct uart_port *port)
|
|
{
|
|
u8 val;
|
|
unsigned long flags;
|
|
struct sccnxp_port *s = dev_get_drvdata(port->dev);
|
|
|
|
spin_lock_irqsave(&s->lock, flags);
|
|
val = sccnxp_port_read(port, SCCNXP_SR_REG);
|
|
spin_unlock_irqrestore(&s->lock, flags);
|
|
|
|
return (val & SR_TXEMT) ? TIOCSER_TEMT : 0;
|
|
}
|
|
|
|
static void sccnxp_set_mctrl(struct uart_port *port, unsigned int mctrl)
|
|
{
|
|
struct sccnxp_port *s = dev_get_drvdata(port->dev);
|
|
unsigned long flags;
|
|
|
|
if (!(s->chip->flags & SCCNXP_HAVE_IO))
|
|
return;
|
|
|
|
spin_lock_irqsave(&s->lock, flags);
|
|
|
|
sccnxp_set_bit(port, DTR_OP, mctrl & TIOCM_DTR);
|
|
sccnxp_set_bit(port, RTS_OP, mctrl & TIOCM_RTS);
|
|
|
|
spin_unlock_irqrestore(&s->lock, flags);
|
|
}
|
|
|
|
static unsigned int sccnxp_get_mctrl(struct uart_port *port)
|
|
{
|
|
u8 bitmask, ipr;
|
|
unsigned long flags;
|
|
struct sccnxp_port *s = dev_get_drvdata(port->dev);
|
|
unsigned int mctrl = TIOCM_DSR | TIOCM_CTS | TIOCM_CAR;
|
|
|
|
if (!(s->chip->flags & SCCNXP_HAVE_IO))
|
|
return mctrl;
|
|
|
|
spin_lock_irqsave(&s->lock, flags);
|
|
|
|
ipr = ~sccnxp_read(port, SCCNXP_IPCR_REG);
|
|
|
|
if (s->pdata.mctrl_cfg[port->line] & MCTRL_MASK(DSR_IP)) {
|
|
bitmask = 1 << MCTRL_IBIT(s->pdata.mctrl_cfg[port->line],
|
|
DSR_IP);
|
|
mctrl &= ~TIOCM_DSR;
|
|
mctrl |= (ipr & bitmask) ? TIOCM_DSR : 0;
|
|
}
|
|
if (s->pdata.mctrl_cfg[port->line] & MCTRL_MASK(CTS_IP)) {
|
|
bitmask = 1 << MCTRL_IBIT(s->pdata.mctrl_cfg[port->line],
|
|
CTS_IP);
|
|
mctrl &= ~TIOCM_CTS;
|
|
mctrl |= (ipr & bitmask) ? TIOCM_CTS : 0;
|
|
}
|
|
if (s->pdata.mctrl_cfg[port->line] & MCTRL_MASK(DCD_IP)) {
|
|
bitmask = 1 << MCTRL_IBIT(s->pdata.mctrl_cfg[port->line],
|
|
DCD_IP);
|
|
mctrl &= ~TIOCM_CAR;
|
|
mctrl |= (ipr & bitmask) ? TIOCM_CAR : 0;
|
|
}
|
|
if (s->pdata.mctrl_cfg[port->line] & MCTRL_MASK(RNG_IP)) {
|
|
bitmask = 1 << MCTRL_IBIT(s->pdata.mctrl_cfg[port->line],
|
|
RNG_IP);
|
|
mctrl &= ~TIOCM_RNG;
|
|
mctrl |= (ipr & bitmask) ? TIOCM_RNG : 0;
|
|
}
|
|
|
|
spin_unlock_irqrestore(&s->lock, flags);
|
|
|
|
return mctrl;
|
|
}
|
|
|
|
static void sccnxp_break_ctl(struct uart_port *port, int break_state)
|
|
{
|
|
struct sccnxp_port *s = dev_get_drvdata(port->dev);
|
|
unsigned long flags;
|
|
|
|
spin_lock_irqsave(&s->lock, flags);
|
|
sccnxp_port_write(port, SCCNXP_CR_REG, break_state ?
|
|
CR_CMD_START_BREAK : CR_CMD_STOP_BREAK);
|
|
spin_unlock_irqrestore(&s->lock, flags);
|
|
}
|
|
|
|
static void sccnxp_set_termios(struct uart_port *port,
|
|
struct ktermios *termios, struct ktermios *old)
|
|
{
|
|
struct sccnxp_port *s = dev_get_drvdata(port->dev);
|
|
unsigned long flags;
|
|
u8 mr1, mr2;
|
|
int baud;
|
|
|
|
spin_lock_irqsave(&s->lock, flags);
|
|
|
|
/* Mask termios capabilities we don't support */
|
|
termios->c_cflag &= ~CMSPAR;
|
|
|
|
/* Disable RX & TX, reset break condition, status and FIFOs */
|
|
sccnxp_port_write(port, SCCNXP_CR_REG, CR_CMD_RX_RESET |
|
|
CR_RX_DISABLE | CR_TX_DISABLE);
|
|
sccnxp_port_write(port, SCCNXP_CR_REG, CR_CMD_TX_RESET);
|
|
sccnxp_port_write(port, SCCNXP_CR_REG, CR_CMD_STATUS_RESET);
|
|
sccnxp_port_write(port, SCCNXP_CR_REG, CR_CMD_BREAK_RESET);
|
|
|
|
/* Word size */
|
|
switch (termios->c_cflag & CSIZE) {
|
|
case CS5:
|
|
mr1 = MR1_BITS_5;
|
|
break;
|
|
case CS6:
|
|
mr1 = MR1_BITS_6;
|
|
break;
|
|
case CS7:
|
|
mr1 = MR1_BITS_7;
|
|
break;
|
|
case CS8:
|
|
default:
|
|
mr1 = MR1_BITS_8;
|
|
break;
|
|
}
|
|
|
|
/* Parity */
|
|
if (termios->c_cflag & PARENB) {
|
|
if (termios->c_cflag & PARODD)
|
|
mr1 |= MR1_PAR_ODD;
|
|
} else
|
|
mr1 |= MR1_PAR_NO;
|
|
|
|
/* Stop bits */
|
|
mr2 = (termios->c_cflag & CSTOPB) ? MR2_STOP2 : MR2_STOP1;
|
|
|
|
/* Update desired format */
|
|
sccnxp_port_write(port, SCCNXP_CR_REG, CR_CMD_MRPTR1);
|
|
sccnxp_port_write(port, SCCNXP_MR_REG, mr1);
|
|
sccnxp_port_write(port, SCCNXP_MR_REG, mr2);
|
|
|
|
/* Set read status mask */
|
|
port->read_status_mask = SR_OVR;
|
|
if (termios->c_iflag & INPCK)
|
|
port->read_status_mask |= SR_PE | SR_FE;
|
|
if (termios->c_iflag & (IGNBRK | BRKINT | PARMRK))
|
|
port->read_status_mask |= SR_BRK;
|
|
|
|
/* Set status ignore mask */
|
|
port->ignore_status_mask = 0;
|
|
if (termios->c_iflag & IGNBRK)
|
|
port->ignore_status_mask |= SR_BRK;
|
|
if (termios->c_iflag & IGNPAR)
|
|
port->ignore_status_mask |= SR_PE;
|
|
if (!(termios->c_cflag & CREAD))
|
|
port->ignore_status_mask |= SR_PE | SR_OVR | SR_FE | SR_BRK;
|
|
|
|
/* Setup baudrate */
|
|
baud = uart_get_baud_rate(port, termios, old, 50,
|
|
(s->chip->flags & SCCNXP_HAVE_MR0) ?
|
|
230400 : 38400);
|
|
baud = sccnxp_set_baud(port, baud);
|
|
|
|
/* Update timeout according to new baud rate */
|
|
uart_update_timeout(port, termios->c_cflag, baud);
|
|
|
|
/* Report actual baudrate back to core */
|
|
if (tty_termios_baud_rate(termios))
|
|
tty_termios_encode_baud_rate(termios, baud, baud);
|
|
|
|
/* Enable RX & TX */
|
|
sccnxp_port_write(port, SCCNXP_CR_REG, CR_RX_ENABLE | CR_TX_ENABLE);
|
|
|
|
spin_unlock_irqrestore(&s->lock, flags);
|
|
}
|
|
|
|
static int sccnxp_startup(struct uart_port *port)
|
|
{
|
|
struct sccnxp_port *s = dev_get_drvdata(port->dev);
|
|
unsigned long flags;
|
|
|
|
spin_lock_irqsave(&s->lock, flags);
|
|
|
|
if (s->chip->flags & SCCNXP_HAVE_IO) {
|
|
/* Outputs are controlled manually */
|
|
sccnxp_write(port, SCCNXP_OPCR_REG, 0);
|
|
}
|
|
|
|
/* Reset break condition, status and FIFOs */
|
|
sccnxp_port_write(port, SCCNXP_CR_REG, CR_CMD_RX_RESET);
|
|
sccnxp_port_write(port, SCCNXP_CR_REG, CR_CMD_TX_RESET);
|
|
sccnxp_port_write(port, SCCNXP_CR_REG, CR_CMD_STATUS_RESET);
|
|
sccnxp_port_write(port, SCCNXP_CR_REG, CR_CMD_BREAK_RESET);
|
|
|
|
/* Enable RX & TX */
|
|
sccnxp_port_write(port, SCCNXP_CR_REG, CR_RX_ENABLE | CR_TX_ENABLE);
|
|
|
|
/* Enable RX interrupt */
|
|
sccnxp_enable_irq(port, IMR_RXRDY);
|
|
|
|
s->opened[port->line] = 1;
|
|
|
|
spin_unlock_irqrestore(&s->lock, flags);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void sccnxp_shutdown(struct uart_port *port)
|
|
{
|
|
struct sccnxp_port *s = dev_get_drvdata(port->dev);
|
|
unsigned long flags;
|
|
|
|
spin_lock_irqsave(&s->lock, flags);
|
|
|
|
s->opened[port->line] = 0;
|
|
|
|
/* Disable interrupts */
|
|
sccnxp_disable_irq(port, IMR_TXRDY | IMR_RXRDY);
|
|
|
|
/* Disable TX & RX */
|
|
sccnxp_port_write(port, SCCNXP_CR_REG, CR_RX_DISABLE | CR_TX_DISABLE);
|
|
|
|
/* Leave direction to input */
|
|
if (s->chip->flags & SCCNXP_HAVE_IO)
|
|
sccnxp_set_bit(port, DIR_OP, 0);
|
|
|
|
spin_unlock_irqrestore(&s->lock, flags);
|
|
}
|
|
|
|
static const char *sccnxp_type(struct uart_port *port)
|
|
{
|
|
struct sccnxp_port *s = dev_get_drvdata(port->dev);
|
|
|
|
return (port->type == PORT_SC26XX) ? s->chip->name : NULL;
|
|
}
|
|
|
|
static void sccnxp_release_port(struct uart_port *port)
|
|
{
|
|
/* Do nothing */
|
|
}
|
|
|
|
static int sccnxp_request_port(struct uart_port *port)
|
|
{
|
|
/* Do nothing */
|
|
return 0;
|
|
}
|
|
|
|
static void sccnxp_config_port(struct uart_port *port, int flags)
|
|
{
|
|
if (flags & UART_CONFIG_TYPE)
|
|
port->type = PORT_SC26XX;
|
|
}
|
|
|
|
static int sccnxp_verify_port(struct uart_port *port, struct serial_struct *s)
|
|
{
|
|
if ((s->type == PORT_UNKNOWN) || (s->type == PORT_SC26XX))
|
|
return 0;
|
|
if (s->irq == port->irq)
|
|
return 0;
|
|
|
|
return -EINVAL;
|
|
}
|
|
|
|
static const struct uart_ops sccnxp_ops = {
|
|
.tx_empty = sccnxp_tx_empty,
|
|
.set_mctrl = sccnxp_set_mctrl,
|
|
.get_mctrl = sccnxp_get_mctrl,
|
|
.stop_tx = sccnxp_stop_tx,
|
|
.start_tx = sccnxp_start_tx,
|
|
.stop_rx = sccnxp_stop_rx,
|
|
.break_ctl = sccnxp_break_ctl,
|
|
.startup = sccnxp_startup,
|
|
.shutdown = sccnxp_shutdown,
|
|
.set_termios = sccnxp_set_termios,
|
|
.type = sccnxp_type,
|
|
.release_port = sccnxp_release_port,
|
|
.request_port = sccnxp_request_port,
|
|
.config_port = sccnxp_config_port,
|
|
.verify_port = sccnxp_verify_port,
|
|
};
|
|
|
|
#ifdef CONFIG_SERIAL_SCCNXP_CONSOLE
|
|
static void sccnxp_console_putchar(struct uart_port *port, int c)
|
|
{
|
|
int tryes = 100000;
|
|
|
|
while (tryes--) {
|
|
if (sccnxp_port_read(port, SCCNXP_SR_REG) & SR_TXRDY) {
|
|
sccnxp_port_write(port, SCCNXP_THR_REG, c);
|
|
break;
|
|
}
|
|
barrier();
|
|
}
|
|
}
|
|
|
|
static void sccnxp_console_write(struct console *co, const char *c, unsigned n)
|
|
{
|
|
struct sccnxp_port *s = (struct sccnxp_port *)co->data;
|
|
struct uart_port *port = &s->port[co->index];
|
|
unsigned long flags;
|
|
|
|
spin_lock_irqsave(&s->lock, flags);
|
|
uart_console_write(port, c, n, sccnxp_console_putchar);
|
|
spin_unlock_irqrestore(&s->lock, flags);
|
|
}
|
|
|
|
static int sccnxp_console_setup(struct console *co, char *options)
|
|
{
|
|
struct sccnxp_port *s = (struct sccnxp_port *)co->data;
|
|
struct uart_port *port = &s->port[(co->index > 0) ? co->index : 0];
|
|
int baud = 9600, bits = 8, parity = 'n', flow = 'n';
|
|
|
|
if (options)
|
|
uart_parse_options(options, &baud, &parity, &bits, &flow);
|
|
|
|
return uart_set_options(port, co, baud, parity, bits, flow);
|
|
}
|
|
#endif
|
|
|
|
static const struct platform_device_id sccnxp_id_table[] = {
|
|
{ .name = "sc2681", .driver_data = (kernel_ulong_t)&sc2681, },
|
|
{ .name = "sc2691", .driver_data = (kernel_ulong_t)&sc2691, },
|
|
{ .name = "sc2692", .driver_data = (kernel_ulong_t)&sc2692, },
|
|
{ .name = "sc2891", .driver_data = (kernel_ulong_t)&sc2891, },
|
|
{ .name = "sc2892", .driver_data = (kernel_ulong_t)&sc2892, },
|
|
{ .name = "sc28202", .driver_data = (kernel_ulong_t)&sc28202, },
|
|
{ .name = "sc68681", .driver_data = (kernel_ulong_t)&sc68681, },
|
|
{ .name = "sc68692", .driver_data = (kernel_ulong_t)&sc68692, },
|
|
{ }
|
|
};
|
|
MODULE_DEVICE_TABLE(platform, sccnxp_id_table);
|
|
|
|
static int sccnxp_probe(struct platform_device *pdev)
|
|
{
|
|
struct resource *res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
|
|
struct sccnxp_pdata *pdata = dev_get_platdata(&pdev->dev);
|
|
int i, ret, uartclk;
|
|
struct sccnxp_port *s;
|
|
void __iomem *membase;
|
|
struct clk *clk;
|
|
|
|
membase = devm_ioremap_resource(&pdev->dev, res);
|
|
if (IS_ERR(membase))
|
|
return PTR_ERR(membase);
|
|
|
|
s = devm_kzalloc(&pdev->dev, sizeof(struct sccnxp_port), GFP_KERNEL);
|
|
if (!s) {
|
|
dev_err(&pdev->dev, "Error allocating port structure\n");
|
|
return -ENOMEM;
|
|
}
|
|
platform_set_drvdata(pdev, s);
|
|
|
|
spin_lock_init(&s->lock);
|
|
|
|
s->chip = (struct sccnxp_chip *)pdev->id_entry->driver_data;
|
|
|
|
s->regulator = devm_regulator_get(&pdev->dev, "vcc");
|
|
if (!IS_ERR(s->regulator)) {
|
|
ret = regulator_enable(s->regulator);
|
|
if (ret) {
|
|
dev_err(&pdev->dev,
|
|
"Failed to enable regulator: %i\n", ret);
|
|
return ret;
|
|
}
|
|
} else if (PTR_ERR(s->regulator) == -EPROBE_DEFER)
|
|
return -EPROBE_DEFER;
|
|
|
|
clk = devm_clk_get(&pdev->dev, NULL);
|
|
if (IS_ERR(clk)) {
|
|
ret = PTR_ERR(clk);
|
|
if (ret == -EPROBE_DEFER)
|
|
goto err_out;
|
|
uartclk = 0;
|
|
} else {
|
|
ret = clk_prepare_enable(clk);
|
|
if (ret)
|
|
goto err_out;
|
|
|
|
ret = devm_add_action_or_reset(&pdev->dev,
|
|
(void(*)(void *))clk_disable_unprepare,
|
|
clk);
|
|
if (ret)
|
|
goto err_out;
|
|
|
|
uartclk = clk_get_rate(clk);
|
|
}
|
|
|
|
if (!uartclk) {
|
|
dev_notice(&pdev->dev, "Using default clock frequency\n");
|
|
uartclk = s->chip->freq_std;
|
|
}
|
|
|
|
/* Check input frequency */
|
|
if ((uartclk < s->chip->freq_min) || (uartclk > s->chip->freq_max)) {
|
|
dev_err(&pdev->dev, "Frequency out of bounds\n");
|
|
ret = -EINVAL;
|
|
goto err_out;
|
|
}
|
|
|
|
if (pdata)
|
|
memcpy(&s->pdata, pdata, sizeof(struct sccnxp_pdata));
|
|
|
|
if (s->pdata.poll_time_us) {
|
|
dev_info(&pdev->dev, "Using poll mode, resolution %u usecs\n",
|
|
s->pdata.poll_time_us);
|
|
s->poll = 1;
|
|
}
|
|
|
|
if (!s->poll) {
|
|
s->irq = platform_get_irq(pdev, 0);
|
|
if (s->irq < 0) {
|
|
dev_err(&pdev->dev, "Missing irq resource data\n");
|
|
ret = -ENXIO;
|
|
goto err_out;
|
|
}
|
|
}
|
|
|
|
s->uart.owner = THIS_MODULE;
|
|
s->uart.dev_name = "ttySC";
|
|
s->uart.major = SCCNXP_MAJOR;
|
|
s->uart.minor = SCCNXP_MINOR;
|
|
s->uart.nr = s->chip->nr;
|
|
#ifdef CONFIG_SERIAL_SCCNXP_CONSOLE
|
|
s->uart.cons = &s->console;
|
|
s->uart.cons->device = uart_console_device;
|
|
s->uart.cons->write = sccnxp_console_write;
|
|
s->uart.cons->setup = sccnxp_console_setup;
|
|
s->uart.cons->flags = CON_PRINTBUFFER;
|
|
s->uart.cons->index = -1;
|
|
s->uart.cons->data = s;
|
|
strcpy(s->uart.cons->name, "ttySC");
|
|
#endif
|
|
ret = uart_register_driver(&s->uart);
|
|
if (ret) {
|
|
dev_err(&pdev->dev, "Registering UART driver failed\n");
|
|
goto err_out;
|
|
}
|
|
|
|
for (i = 0; i < s->uart.nr; i++) {
|
|
s->port[i].line = i;
|
|
s->port[i].dev = &pdev->dev;
|
|
s->port[i].irq = s->irq;
|
|
s->port[i].type = PORT_SC26XX;
|
|
s->port[i].fifosize = s->chip->fifosize;
|
|
s->port[i].flags = UPF_SKIP_TEST | UPF_FIXED_TYPE;
|
|
s->port[i].iotype = UPIO_MEM;
|
|
s->port[i].mapbase = res->start;
|
|
s->port[i].membase = membase;
|
|
s->port[i].regshift = s->pdata.reg_shift;
|
|
s->port[i].uartclk = uartclk;
|
|
s->port[i].ops = &sccnxp_ops;
|
|
uart_add_one_port(&s->uart, &s->port[i]);
|
|
/* Set direction to input */
|
|
if (s->chip->flags & SCCNXP_HAVE_IO)
|
|
sccnxp_set_bit(&s->port[i], DIR_OP, 0);
|
|
}
|
|
|
|
/* Disable interrupts */
|
|
s->imr = 0;
|
|
sccnxp_write(&s->port[0], SCCNXP_IMR_REG, 0);
|
|
|
|
if (!s->poll) {
|
|
ret = devm_request_threaded_irq(&pdev->dev, s->irq, NULL,
|
|
sccnxp_ist,
|
|
IRQF_TRIGGER_FALLING |
|
|
IRQF_ONESHOT,
|
|
dev_name(&pdev->dev), s);
|
|
if (!ret)
|
|
return 0;
|
|
|
|
dev_err(&pdev->dev, "Unable to reguest IRQ %i\n", s->irq);
|
|
} else {
|
|
timer_setup(&s->timer, sccnxp_timer, 0);
|
|
mod_timer(&s->timer, jiffies +
|
|
usecs_to_jiffies(s->pdata.poll_time_us));
|
|
return 0;
|
|
}
|
|
|
|
uart_unregister_driver(&s->uart);
|
|
err_out:
|
|
if (!IS_ERR(s->regulator))
|
|
regulator_disable(s->regulator);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int sccnxp_remove(struct platform_device *pdev)
|
|
{
|
|
int i;
|
|
struct sccnxp_port *s = platform_get_drvdata(pdev);
|
|
|
|
if (!s->poll)
|
|
devm_free_irq(&pdev->dev, s->irq, s);
|
|
else
|
|
del_timer_sync(&s->timer);
|
|
|
|
for (i = 0; i < s->uart.nr; i++)
|
|
uart_remove_one_port(&s->uart, &s->port[i]);
|
|
|
|
uart_unregister_driver(&s->uart);
|
|
|
|
if (!IS_ERR(s->regulator))
|
|
return regulator_disable(s->regulator);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static struct platform_driver sccnxp_uart_driver = {
|
|
.driver = {
|
|
.name = SCCNXP_NAME,
|
|
},
|
|
.probe = sccnxp_probe,
|
|
.remove = sccnxp_remove,
|
|
.id_table = sccnxp_id_table,
|
|
};
|
|
module_platform_driver(sccnxp_uart_driver);
|
|
|
|
MODULE_LICENSE("GPL v2");
|
|
MODULE_AUTHOR("Alexander Shiyan <shc_work@mail.ru>");
|
|
MODULE_DESCRIPTION("SCCNXP serial driver");
|