/* SPDX-License-Identifier: GPL-2.0-or-later */

#include <types.h>
#include <console/uart.h>
#include <device/mmio.h>
#include <boot/coreboot_tables.h>
#include <cpu/ti/am335x/uart.h>

#define EFR_ENHANCED_EN		(1 << 4)
#define FCR_FIFO_EN		(1 << 0)
#define MCR_TCR_TLR		(1 << 6)
#define SYSC_SOFTRESET		(1 << 1)
#define SYSS_RESETDONE		(1 << 0)

#define LSR_RXFIFOE		(1 << 0)
#define LSR_TXFIFOE		(1 << 5)

/*
 * Initialise the serial port with the given baudrate divisor. The settings
 * are always 8 data bits, no parity, 1 stop bit, no start bits.
 */
static void am335x_uart_init(struct am335x_uart *uart, uint16_t div)
{
	uint16_t lcr_orig, efr_orig, mcr_orig;

	/* reset the UART */
	write16(&uart->sysc, uart->sysc | SYSC_SOFTRESET);
	while (!(read16(&uart->syss) & SYSS_RESETDONE))
		;

	/* 1. switch to register config mode B */
	lcr_orig = read16(&uart->lcr);
	write16(&uart->lcr, 0xbf);

	/*
	 * 2. Set EFR ENHANCED_EN bit. To access this bit, registers must
	 * be in TCR_TLR submode, meaning EFR[4] = 1 and MCR[6] = 1.
	 */
	efr_orig = read16(&uart->efr);
	write16(&uart->efr, efr_orig | EFR_ENHANCED_EN);

	/* 3. Switch to register config mode A */
	write16(&uart->lcr, 0x80);

	/* 4. Enable register submode TCR_TLR to access the UARTi.UART_TLR */
	mcr_orig = read16(&uart->mcr);
	write16(&uart->mcr, mcr_orig | MCR_TCR_TLR);

	/* 5. Enable the FIFO. For now we'll ignore FIFO triggers and DMA */
	write16(&uart->fcr, FCR_FIFO_EN);

	/* 6. Switch to configuration mode B */
	write16(&uart->lcr, 0xbf);
	/* Skip steps 7 and 8 (setting up FIFO triggers for DMA) */

	/* 9. Restore original EFR value */
	write16(&uart->efr, efr_orig);

	/* 10. Switch to config mode A */
	write16(&uart->lcr, 0x80);

	/* 11. Restore original MCR value */
	write16(&uart->mcr, mcr_orig);

	/* 12. Restore original LCR value */
	write16(&uart->lcr, lcr_orig);

	/* Protocol, baud rate and interrupt settings */

	/* 1. Disable UART access to DLL and DLH registers */
	write16(&uart->mdr1, read16(&uart->mdr1) | 0x7);

	/* 2. Switch to config mode B */
	write16(&uart->lcr, 0xbf);

	/* 3. Enable access to IER[7:4] */
	write16(&uart->efr, efr_orig | EFR_ENHANCED_EN);

	/* 4. Switch to operational mode */
	write16(&uart->lcr, 0x0);

	/* 5. Clear IER */
	write16(&uart->ier, 0x0);

	/* 6. Switch to config mode B */
	write16(&uart->lcr, 0xbf);

	/* 7. Set dll and dlh to the desired values (table 19-25) */
	write16(&uart->dlh, (div >> 8));
	write16(&uart->dll, (div & 0xff));

	/* 8. Switch to operational mode to access ier */
	write16(&uart->lcr, 0x0);

	/* 9. Clear ier to disable all interrupts */
	write16(&uart->ier, 0x0);

	/* 10. Switch to config mode B */
	write16(&uart->lcr, 0xbf);

	/* 11. Restore efr */
	write16(&uart->efr, efr_orig);

	/* 12. Set protocol formatting 8n1 (8 bit data, no parity, 1 stop bit) */
	write16(&uart->lcr, 0x3);

	/* 13. Load the new UART mode */
	write16(&uart->mdr1, 0x0);
}

/*
 * Read a single byte from the serial port. Returns 1 on success, 0
 * otherwise. When the function is successful, the character read is
 * written into its argument c.
 */
static unsigned char am335x_uart_rx_byte(struct am335x_uart *uart)
{
	while (!(read16(&uart->lsr) & LSR_RXFIFOE));

	return read8(&uart->rhr);
}

/*
 * Output a single byte to the serial port.
 */
static void am335x_uart_tx_byte(struct am335x_uart *uart, unsigned char data)
{
	while (!(read16(&uart->lsr) & LSR_TXFIFOE));

	return write8(&uart->thr, data);
}

unsigned int uart_platform_refclk(void)
{
	return 48000000;
}

uintptr_t uart_platform_base(int idx)
{
	const unsigned int bases[] = {
		0x44e09000, 0x48022000, 0x48024000,
		0x481a6000, 0x481a8000, 0x481aa000
	};
	if (idx < ARRAY_SIZE(bases))
		return bases[idx];
	return 0;
}

void uart_init(int idx)
{
	struct am335x_uart *uart = uart_platform_baseptr(idx);
	uint16_t div = (uint16_t) uart_baudrate_divisor(
		get_uart_baudrate(), uart_platform_refclk(), 16);
	am335x_uart_init(uart, div);
}

unsigned char uart_rx_byte(int idx)
{
	struct am335x_uart *uart = uart_platform_baseptr(idx);
	return am335x_uart_rx_byte(uart);
}

void uart_tx_byte(int idx, unsigned char data)
{
	struct am335x_uart *uart = uart_platform_baseptr(idx);
	am335x_uart_tx_byte(uart, data);
}

void uart_tx_flush(int idx)
{
}

void uart_fill_lb(void *data)
{
	struct lb_serial serial;
	serial.type = LB_SERIAL_TYPE_MEMORY_MAPPED;
	serial.baseaddr = uart_platform_base(CONFIG_UART_FOR_CONSOLE);
	serial.baud = get_uart_baudrate();
	serial.regwidth = 2;
	serial.input_hertz = uart_platform_refclk();
	serial.uart_pci_addr = CONFIG_UART_PCI_ADDR;
	lb_add_serial(&serial, data);

	lb_add_console(LB_TAG_CONSOLE_SERIAL8250MEM, data);
}