dm: serial: Add driver model support for ns16550

Add driver model support so that ns16550 can support operation both with
and without driver model.

The driver needs a clock frequency so cannot stand alone unfortunately. The
clock frequency must be provided by a separate driver.

Signed-off-by: Simon Glass <sjg@chromium.org>
diff --git a/drivers/serial/Makefile b/drivers/serial/Makefile
index 4720e1d..5ae6416 100644
--- a/drivers/serial/Makefile
+++ b/drivers/serial/Makefile
@@ -9,6 +9,7 @@
 obj-y += serial-uclass.o
 else
 obj-y += serial.o
+obj-$(CONFIG_SYS_NS16550_SERIAL) += serial_ns16550.o
 endif
 
 obj-$(CONFIG_ALTERA_UART) += altera_uart.o
@@ -20,7 +21,6 @@
 obj-$(CONFIG_OPENCORES_YANU) += opencores_yanu.o
 obj-$(CONFIG_SYS_NS16550) += ns16550.o
 obj-$(CONFIG_S5P) += serial_s5p.o
-obj-$(CONFIG_SYS_NS16550_SERIAL) += serial_ns16550.o
 obj-$(CONFIG_IMX_SERIAL) += serial_imx.o
 obj-$(CONFIG_KS8695_SERIAL) += serial_ks8695.o
 obj-$(CONFIG_MAX3100_SERIAL) += serial_max3100.o
diff --git a/drivers/serial/ns16550.c b/drivers/serial/ns16550.c
index d54eba6..63a9ef6 100644
--- a/drivers/serial/ns16550.c
+++ b/drivers/serial/ns16550.c
@@ -5,17 +5,25 @@
  */
 
 #include <common.h>
+#include <dm.h>
+#include <errno.h>
+#include <fdtdec.h>
 #include <ns16550.h>
+#include <serial.h>
 #include <watchdog.h>
 #include <linux/types.h>
 #include <asm/io.h>
 
+DECLARE_GLOBAL_DATA_PTR;
+
 #define UART_LCRVAL UART_LCR_8N1		/* 8 data, 1 stop, no parity */
 #define UART_MCRVAL (UART_MCR_DTR | \
 		     UART_MCR_RTS)		/* RTS/DTR */
 #define UART_FCRVAL (UART_FCR_FIFO_EN |	\
 		     UART_FCR_RXSR |	\
 		     UART_FCR_TXSR)		/* Clear & enable FIFOs */
+
+#ifndef CONFIG_DM_SERIAL
 #ifdef CONFIG_SYS_NS16550_PORT_MAPPED
 #define serial_out(x, y)	outb(x, (ulong)y)
 #define serial_in(y)		inb((ulong)y)
@@ -29,6 +37,7 @@
 #define serial_out(x, y)	writeb(x, y)
 #define serial_in(y)		readb(y)
 #endif
+#endif /* !CONFIG_DM_SERIAL */
 
 #if defined(CONFIG_SOC_KEYSTONE)
 #define UART_REG_VAL_PWREMU_MGMT_UART_DISABLE   0
@@ -45,6 +54,58 @@
 #define CONFIG_SYS_NS16550_IER  0x00
 #endif /* CONFIG_SYS_NS16550_IER */
 
+#ifdef CONFIG_DM_SERIAL
+static void ns16550_writeb(NS16550_t port, int offset, int value)
+{
+	struct ns16550_platdata *plat = port->plat;
+	unsigned char *addr;
+
+	offset *= 1 << plat->reg_shift;
+	addr = plat->base + offset;
+	/*
+	 * As far as we know it doesn't make sense to support selection of
+	 * these options at run-time, so use the existing CONFIG options.
+	 */
+#ifdef CONFIG_SYS_NS16550_PORT_MAPPED
+	outb(value, addr);
+#elif defined(CONFIG_SYS_NS16550_MEM32) && !defined(CONFIG_SYS_BIG_ENDIAN)
+	out_le32(addr, value);
+#elif defined(CONFIG_SYS_NS16550_MEM32) && defined(CONFIG_SYS_BIG_ENDIAN)
+	out_be32(addr, value);
+#elif defined(CONFIG_SYS_BIG_ENDIAN)
+	writeb(value, addr + (1 << plat->reg_shift) - 1);
+#else
+	writeb(value, addr);
+#endif
+}
+
+static int ns16550_readb(NS16550_t port, int offset)
+{
+	struct ns16550_platdata *plat = port->plat;
+	unsigned char *addr;
+
+	offset *= 1 << plat->reg_shift;
+	addr = plat->base + offset;
+#ifdef CONFIG_SYS_NS16550_PORT_MAPPED
+	return inb(addr);
+#elif defined(CONFIG_SYS_NS16550_MEM32) && !defined(CONFIG_SYS_BIG_ENDIAN)
+	return in_le32(addr);
+#elif defined(CONFIG_SYS_NS16550_MEM32) && defined(CONFIG_SYS_BIG_ENDIAN)
+	return in_be32(addr);
+#elif defined(CONFIG_SYS_BIG_ENDIAN)
+	return readb(addr + (1 << plat->reg_shift) - 1);
+#else
+	return readb(addr);
+#endif
+}
+
+/* We can clean these up once everything is moved to driver model */
+#define serial_out(value, addr)	\
+	ns16550_writeb(com_port, addr - (unsigned char *)com_port, value)
+#define serial_in(addr) \
+	ns16550_readb(com_port, addr - (unsigned char *)com_port)
+#endif
+
 int ns16550_calc_divisor(NS16550_t port, int clock, int baudrate)
 {
 	const unsigned int mode_x_div = 16;
@@ -79,7 +140,8 @@
 	 */
 	if ((serial_in(&com_port->lsr) & (UART_LSR_TEMT | UART_LSR_THRE))
 	     == UART_LSR_THRE) {
-		NS16550_setbrg(com_port, baud_divisor);
+		if (baud_divisor != -1)
+			NS16550_setbrg(com_port, baud_divisor);
 		serial_out(0, &com_port->mdr1);
 	}
 #endif
@@ -95,7 +157,8 @@
 	NS16550_setbrg(com_port, 0);
 	serial_out(UART_MCRVAL, &com_port->mcr);
 	serial_out(UART_FCRVAL, &com_port->fcr);
-	NS16550_setbrg(com_port, baud_divisor);
+	if (baud_divisor != -1)
+		NS16550_setbrg(com_port, baud_divisor);
 #if defined(CONFIG_OMAP) || \
 	defined(CONFIG_AM33XX) || defined(CONFIG_SOC_DA8XX) || \
 	defined(CONFIG_TI81XX) || defined(CONFIG_AM43XX)
@@ -154,3 +217,92 @@
 }
 
 #endif /* CONFIG_NS16550_MIN_FUNCTIONS */
+
+#ifdef CONFIG_DM_SERIAL
+static int ns16550_serial_putc(struct udevice *dev, const char ch)
+{
+	struct NS16550 *const com_port = dev_get_priv(dev);
+
+	if (!(serial_in(&com_port->lsr) & UART_LSR_THRE))
+		return -EAGAIN;
+	serial_out(ch, &com_port->thr);
+
+	/*
+	 * Call watchdog_reset() upon newline. This is done here in putc
+	 * since the environment code uses a single puts() to print the complete
+	 * environment upon "printenv". So we can't put this watchdog call
+	 * in puts().
+	 */
+	if (ch == '\n')
+		WATCHDOG_RESET();
+
+	return 0;
+}
+
+static int ns16550_serial_pending(struct udevice *dev, bool input)
+{
+	struct NS16550 *const com_port = dev_get_priv(dev);
+
+	if (input)
+		return serial_in(&com_port->lsr) & UART_LSR_DR ? 1 : 0;
+	else
+		return serial_in(&com_port->lsr) & UART_LSR_THRE ? 0 : 1;
+}
+
+static int ns16550_serial_getc(struct udevice *dev)
+{
+	struct NS16550 *const com_port = dev_get_priv(dev);
+
+	if (!serial_in(&com_port->lsr) & UART_LSR_DR)
+		return -EAGAIN;
+
+	return serial_in(&com_port->rbr);
+}
+
+static int ns16550_serial_setbrg(struct udevice *dev, int baudrate)
+{
+	struct NS16550 *const com_port = dev_get_priv(dev);
+	struct ns16550_platdata *plat = com_port->plat;
+	int clock_divisor;
+
+	clock_divisor = ns16550_calc_divisor(com_port, plat->clock, baudrate);
+
+	NS16550_setbrg(com_port, clock_divisor);
+
+	return 0;
+}
+
+int ns16550_serial_probe(struct udevice *dev)
+{
+	struct NS16550 *const com_port = dev_get_priv(dev);
+
+	NS16550_init(com_port, -1);
+
+	return 0;
+}
+
+int ns16550_serial_ofdata_to_platdata(struct udevice *dev)
+{
+	struct NS16550 *const com_port = dev_get_priv(dev);
+	struct ns16550_platdata *plat = dev->platdata;
+	fdt_addr_t addr;
+
+	addr = fdtdec_get_addr(gd->fdt_blob, dev->of_offset, "reg");
+	if (addr == FDT_ADDR_T_NONE)
+		return -EINVAL;
+
+	plat->base = (unsigned char *)addr;
+	plat->reg_shift = fdtdec_get_int(gd->fdt_blob, dev->of_offset,
+					 "reg-shift", 1);
+	com_port->plat = plat;
+
+	return 0;
+}
+
+const struct dm_serial_ops ns16550_serial_ops = {
+	.putc = ns16550_serial_putc,
+	.pending = ns16550_serial_pending,
+	.getc = ns16550_serial_getc,
+	.setbrg = ns16550_serial_setbrg,
+};
+#endif /* CONFIG_DM_SERIAL */