Contributors: 1
Author Tokens Token Proportion Commits Commit Proportion
Gerhard Engleder 1074 100.00% 1 100.00%
Total 1074 1


// SPDX-License-Identifier: GPL-2.0
/*
 * Copyright (C) 2025 KEBA Industrial Automation GmbH
 *
 * Driver for KEBA UART FPGA IP core
 */

#include <linux/auxiliary_bus.h>
#include <linux/device.h>
#include <linux/io.h>
#include <linux/misc/keba.h>
#include <linux/module.h>

#include "8250.h"

#define KUART "kuart"

/* flags */
#define KUART_RS485		BIT(0)
#define KUART_USE_CAPABILITY	BIT(1)

/* registers */
#define KUART_VERSION		0x0000
#define KUART_REVISION		0x0001
#define KUART_CAPABILITY	0x0002
#define KUART_CONTROL		0x0004
#define KUART_BASE		0x000C
#define KUART_REGSHIFT		2
#define KUART_CLK		1843200

/* mode flags */
enum kuart_mode {
	KUART_MODE_NONE = 0,
	KUART_MODE_RS485,
	KUART_MODE_RS422,
	KUART_MODE_RS232
};

/* capability flags */
#define KUART_CAPABILITY_NONE	BIT(KUART_MODE_NONE)
#define KUART_CAPABILITY_RS485	BIT(KUART_MODE_RS485)
#define KUART_CAPABILITY_RS422	BIT(KUART_MODE_RS422)
#define KUART_CAPABILITY_RS232	BIT(KUART_MODE_RS232)
#define KUART_CAPABILITY_MASK	GENMASK(3, 0)

/* Additional Control Register DTR line configuration */
#define UART_ACR_DTRLC_MASK		0x18
#define UART_ACR_DTRLC_COMPAT		0x00
#define UART_ACR_DTRLC_ENABLE_LOW	0x10

struct kuart {
	struct keba_uart_auxdev *auxdev;
	void __iomem *base;
	unsigned int line;

	unsigned int flags;
	u8 capability;
	enum kuart_mode mode;
};

static void kuart_set_phy_mode(struct kuart *kuart, enum kuart_mode mode)
{
	iowrite8(mode, kuart->base + KUART_CONTROL);
}

static void kuart_enhanced_mode(struct uart_8250_port *up, bool enable)
{
	u8 lcr, efr;

	/* backup LCR register */
	lcr = serial_in(up, UART_LCR);

	/* enable 650 compatible register set (EFR, ...) */
	serial_out(up, UART_LCR, UART_LCR_CONF_MODE_B);

	/* enable/disable enhanced mode with indexed control registers */
	efr = serial_in(up, UART_EFR);
	if (enable)
		efr |= UART_EFR_ECB;
	else
		efr &= ~UART_EFR_ECB;
	serial_out(up, UART_EFR, efr);

	/* disable 650 compatible register set, restore LCR */
	serial_out(up, UART_LCR, lcr);
}

static void kuart_dtr_line_config(struct uart_8250_port *up, u8 dtrlc)
{
	u8 acr;

	/* set index register to 0 to access ACR register */
	serial_out(up, UART_SCR, UART_ACR);

	/* set value register to 0x10 writing DTR mode (1,0) */
	acr = serial_in(up, UART_LSR);
	acr &= ~UART_ACR_DTRLC_MASK;
	acr |= dtrlc;
	serial_out(up, UART_LSR, acr);
}

static int kuart_rs485_config(struct uart_port *port, struct ktermios *termios,
			      struct serial_rs485 *rs485)
{
	struct uart_8250_port *up = up_to_u8250p(port);
	struct kuart *kuart = port->private_data;
	enum kuart_mode mode;
	u8 dtrlc;

	if (rs485->flags & SER_RS485_ENABLED) {
		if (rs485->flags & SER_RS485_MODE_RS422)
			mode = KUART_MODE_RS422;
		else
			mode = KUART_MODE_RS485;
	} else {
		mode = KUART_MODE_RS232;
	}

	if (mode == kuart->mode)
		return 0;

	if (kuart->flags & KUART_USE_CAPABILITY) {
		/* deactivate physical interface, break before make */
		kuart_set_phy_mode(kuart, KUART_MODE_NONE);
	}

	if (mode == KUART_MODE_RS485) {
		/*
		 * Set DTR line configuration of 95x UART to DTR mode (1,0).
		 * In this mode the DTR pin drives the active-low enable pin of
		 * an external RS485 buffer. The DTR pin will be forced low
		 * whenever the transmitter is not empty, otherwise DTR pin is
		 * high.
		 */
		dtrlc = UART_ACR_DTRLC_ENABLE_LOW;
	} else {
		/*
		 * Set DTR line configuration of 95x UART to DTR mode (0,0).
		 * In this mode the DTR pin is compatible with 16C450, 16C550,
		 * 16C650 and 16c670 (i.e. normal).
		 */
		dtrlc = UART_ACR_DTRLC_COMPAT;
	}

	kuart_enhanced_mode(up, true);
	kuart_dtr_line_config(up, dtrlc);
	kuart_enhanced_mode(up, false);

	if (kuart->flags & KUART_USE_CAPABILITY) {
		/* activate selected physical interface */
		kuart_set_phy_mode(kuart, mode);
	}

	kuart->mode = mode;

	return 0;
}

static int kuart_probe(struct auxiliary_device *auxdev,
		       const struct auxiliary_device_id *id)
{
	struct device *dev = &auxdev->dev;
	struct uart_8250_port uart = {};
	struct resource res;
	struct kuart *kuart;
	int retval;

	kuart = devm_kzalloc(dev, sizeof(*kuart), GFP_KERNEL);
	if (!kuart)
		return -ENOMEM;
	kuart->auxdev = container_of(auxdev, struct keba_uart_auxdev, auxdev);
	kuart->flags = id->driver_data;
	auxiliary_set_drvdata(auxdev, kuart);

	/*
	 * map only memory in front of UART registers, UART registers will be
	 * mapped by serial port
	 */
	res = kuart->auxdev->io;
	res.end = res.start + KUART_BASE - 1;
	kuart->base = devm_ioremap_resource(dev, &res);
	if (IS_ERR(kuart->base))
		return PTR_ERR(kuart->base);

	if (kuart->flags & KUART_USE_CAPABILITY) {
		/*
		 * supported modes are read from capability register, at least
		 * one mode other than none must be supported
		 */
		kuart->capability = ioread8(kuart->base + KUART_CAPABILITY) &
				    KUART_CAPABILITY_MASK;
		if ((kuart->capability & ~KUART_CAPABILITY_NONE) == 0)
			return -EIO;
	}

	spin_lock_init(&uart.port.lock);
	uart.port.dev = dev;
	uart.port.mapbase = kuart->auxdev->io.start + KUART_BASE;
	uart.port.irq = kuart->auxdev->irq;
	uart.port.uartclk = KUART_CLK;
	uart.port.private_data = kuart;

	/* 8 bit registers are 32 bit aligned => shift register offset */
	uart.port.iotype = UPIO_MEM32;
	uart.port.regshift = KUART_REGSHIFT;

	/*
	 * UART mixes 16550, 16750 and 16C950 (for RS485) standard => auto
	 * configuration works best
	 */
	uart.port.flags = UPF_SKIP_TEST | UPF_BOOT_AUTOCONF | UPF_IOREMAP;

	/*
	 * UART supports RS485, RS422 and RS232 with switching of physical
	 * interface
	 */
	uart.port.rs485_config = kuart_rs485_config;
	if (kuart->flags & KUART_RS485) {
		uart.port.rs485_supported.flags = SER_RS485_ENABLED |
						  SER_RS485_RTS_ON_SEND;
		uart.port.rs485.flags = SER_RS485_ENABLED |
					SER_RS485_RTS_ON_SEND;
	}
	if (kuart->flags & KUART_USE_CAPABILITY) {
		/* default mode priority is RS485 > RS422 > RS232 */
		if (kuart->capability & KUART_CAPABILITY_RS422) {
			uart.port.rs485_supported.flags |= SER_RS485_ENABLED |
							   SER_RS485_RTS_ON_SEND |
							   SER_RS485_MODE_RS422;
			uart.port.rs485.flags = SER_RS485_ENABLED |
						SER_RS485_RTS_ON_SEND |
						SER_RS485_MODE_RS422;
		}
		if (kuart->capability & KUART_CAPABILITY_RS485) {
			uart.port.rs485_supported.flags |= SER_RS485_ENABLED |
							   SER_RS485_RTS_ON_SEND;
			uart.port.rs485.flags = SER_RS485_ENABLED |
						SER_RS485_RTS_ON_SEND;
		}
	}

	retval = serial8250_register_8250_port(&uart);
	if (retval < 0) {
		dev_err(&auxdev->dev, "UART registration failed!\n");
		return retval;
	}
	kuart->line = retval;

	return 0;
}

static void kuart_remove(struct auxiliary_device *auxdev)
{
	struct kuart *kuart = auxiliary_get_drvdata(auxdev);

	if (kuart->flags & KUART_USE_CAPABILITY)
		kuart_set_phy_mode(kuart, KUART_MODE_NONE);

	serial8250_unregister_port(kuart->line);
}

static const struct auxiliary_device_id kuart_devtype_aux[] = {
	{ .name = "keba.rs485-uart", .driver_data = KUART_RS485 },
	{ .name = "keba.rs232-uart", .driver_data = 0 },
	{ .name = "keba.uart", .driver_data = KUART_USE_CAPABILITY },
	{}
};
MODULE_DEVICE_TABLE(auxiliary, kuart_devtype_aux);

static struct auxiliary_driver kuart_driver_aux = {
	.name = KUART,
	.id_table = kuart_devtype_aux,
	.probe  = kuart_probe,
	.remove = kuart_remove,
};
module_auxiliary_driver(kuart_driver_aux);

MODULE_AUTHOR("Gerhard Engleder <eg@keba.com>");
MODULE_DESCRIPTION("KEBA 8250 serial port driver");
MODULE_LICENSE("GPL");