/* SPDX-License-Identifier: GPL-2.0-only */

/*
 * Derived from Cavium's BSD-3 Clause OCTEONTX-SDK-6.2.0.
 */

#include <device/mmio.h>
#include <console/uart.h>
#include <delay.h>
#include <endian.h>
#include <stdint.h>
#include <soc/clock.h>
#include <soc/uart.h>
#include <assert.h>
#include <soc/addressmap.h>
#include <drivers/uart/pl011.h>

union cn81xx_uart_ctl {
	u64 u;
	struct {
		u64 uctl_rst		: 1;
		u64 uaa_rst		: 1;
		u64			: 2;
		u64 csclk_en		: 1;
		u64			: 19;
		u64 h_clkdiv_sel	: 3;
		u64			: 1;
		u64 h_clkdiv_rst	: 1;
		u64 h_clk_byp_sel	: 1;
		u64 h_clk_en		: 1;
		u64			: 33;
	} s;
};

struct cn81xx_uart {
	struct pl011_uart pl011;
	union cn81xx_uart_ctl uctl_ctl;
	u8 rsvd4[0x8];
	u64 uctl_spare0;
	u8 rsvd5[0xe0];
	u64 uctl_spare1;
};

#define UART_IBRD_BAUD_DIVINT_SHIFT		0
#define UART_IBRD_BAUD_DIVINT_MASK		0xffff

#define UART_FBRD_BAUD_DIVFRAC_SHIFT		0
#define UART_FBRD_BAUD_DIVFRAC_MASK		0x3f


check_member(cn81xx_uart, uctl_ctl, 0x1000);
check_member(cn81xx_uart, uctl_spare1, 0x10f8);

#define UART_SCLK_DIV 3

/**
 * Returns the current UART HCLK divider
 *
 * @param reg      The H_CLKDIV_SEL value
 * @return         The HCLK divider
 */
static size_t uart_sclk_divisor(const size_t reg)
{
	static const u8 div[] = {1, 2, 4, 6, 8, 16, 24, 32};

	assert(reg < ARRAY_SIZE(div));

	return div[reg];
}

/**
 * Returns the current UART HCLK
 *
 * @param uart     The UART to operate on
 * @return         The HCLK in Hz
 */
static size_t uart_hclk(struct cn81xx_uart *uart)
{
	union cn81xx_uart_ctl ctl;
	const uint64_t sclk = thunderx_get_io_clock();

	ctl.u = read64(&uart->uctl_ctl);
	return sclk / uart_sclk_divisor(ctl.s.h_clkdiv_sel);
}

unsigned int uart_platform_refclk(void)
{
	struct cn81xx_uart *uart =
	    (struct cn81xx_uart *)CONFIG_CONSOLE_SERIAL_UART_ADDRESS;

	if (!uart)
		return 0;

	return uart_hclk(uart);
}

uintptr_t uart_platform_base(int idx)
{
	return CONFIG_CONSOLE_SERIAL_UART_ADDRESS;
}

/**
 * Waits given count if HCLK cycles
 *
 * @param uart     The UART to operate on
 * @param hclks    The number of HCLK cycles to wait
 */
static void uart_wait_hclk(struct cn81xx_uart *uart, const size_t hclks)
{
	const size_t hclk = uart_hclk(uart);
	const size_t delay = (hclks * 1000000ULL) / hclk;
	udelay(MAX(delay, 1));
}

/**
 * Returns the UART state.
 *
 * @param bus     The UART to operate on
 * @return        Boolean: True if UART is enabled
 */
int uart_is_enabled(const size_t bus)
{
	struct cn81xx_uart *uart = (struct cn81xx_uart *)UAAx_PF_BAR0(bus);
	union cn81xx_uart_ctl ctl;

	assert(uart);
	if (!uart)
		return 0;

	ctl.u = read64(&uart->uctl_ctl);
	return !!ctl.s.csclk_en;
}

/**
 * Setup UART with desired BAUD rate in 8N1, no parity mode.
 *
 * @param bus          The UART to operate on
 * @param baudrate     baudrate to set up
 *
 * @return             Boolean: True on error
 */
int uart_setup(const size_t bus, int baudrate)
{
	union cn81xx_uart_ctl ctl;
	struct cn81xx_uart *uart = (struct cn81xx_uart *)UAAx_PF_BAR0(bus);

	assert(uart);
	if (!uart)
		return 1;

	/* 1.2.1 Initialization Sequence (Power-On/Hard/Cold Reset) */
	/* 1. Wait for IOI reset (srst_n) to deassert. */

	/**
	 * 2. Assert all resets:
	 * a. UAA reset: UCTL_CTL[UAA_RST] = 1
	 * b. UCTL reset: UCTL_CTL[UCTL_RST] = 1
	 */
	ctl.u = read64(&uart->uctl_ctl);
	ctl.s.uctl_rst = 1;
	ctl.s.uaa_rst = 1;
	write64(&uart->uctl_ctl, ctl.u);

	/**
	 * 3. Configure the HCLK:
	 * a. Reset the clock dividers: UCTL_CTL[H_CLKDIV_RST] = 1.
	 * b. Select the HCLK frequency
	 * i. UCTL_CTL[H_CLKDIV] = desired value,
	 * ii. UCTL_CTL[H_CLKDIV_EN] = 1 to enable the HCLK.
	 * iii. Readback UCTL_CTL to ensure the values take effect.
	 * c. Deassert the HCLK clock divider reset: UCTL_CTL[H_CLKDIV_RST] = 0.
	 */
	ctl.u = read64(&uart->uctl_ctl);
	ctl.s.h_clkdiv_sel = UART_SCLK_DIV;
	write64(&uart->uctl_ctl, ctl.u);

	ctl.u = read64(&uart->uctl_ctl);
	ctl.s.h_clk_byp_sel = 0;
	write64(&uart->uctl_ctl, ctl.u);

	ctl.u = read64(&uart->uctl_ctl);
	ctl.s.h_clk_en = 1;
	write64(&uart->uctl_ctl, ctl.u);

	ctl.u = read64(&uart->uctl_ctl);
	ctl.s.h_clkdiv_rst = 0;
	write64(&uart->uctl_ctl, ctl.u);

	/**
	 * 4. Wait 20 HCLK cycles from step 3 for HCLK to start and async fifo
	 * to properly reset.
	 */
	uart_wait_hclk(uart, 20 + 1);

	/**
	 * 5. Deassert UCTL and UAHC resets:
	 *  a. UCTL_CTL[UCTL_RST] = 0
	 * b. Wait 10 HCLK cycles.
	 * c. UCTL_CTL[UAHC_RST] = 0
	 * d. You will have to wait 10 HCLK cycles before accessing any
	 * HCLK-only registers.
	 */
	ctl.u = read64(&uart->uctl_ctl);
	ctl.s.uctl_rst = 0;
	write64(&uart->uctl_ctl, ctl.u);

	uart_wait_hclk(uart, 10 + 1);

	ctl.u = read64(&uart->uctl_ctl);
	ctl.s.uaa_rst = 0;
	write64(&uart->uctl_ctl, ctl.u);

	uart_wait_hclk(uart, 10 + 1);

	/**
	 * 6. Enable conditional SCLK of UCTL by writing
	 * UCTL_CTL[CSCLK_EN] = 1.
	 */
	ctl.u = read64(&uart->uctl_ctl);
	ctl.s.csclk_en = 1;
	write64(&uart->uctl_ctl, ctl.u);

	/**
	 * Exit here if the UART is not going to be used in coreboot.
	 * The previous initialization steps are sufficient to make the Linux
	 * kernel not panic.
	 */
	if (!baudrate)
		return 0;

	/**
	 * 7. Initialize the integer and fractional baud rate divider registers
	 * UARTIBRD and UARTFBRD as follows:
	 * a. Baud Rate Divisor = UARTCLK/(16xBaud Rate) = BRDI + BRDF
	 * b. The fractional register BRDF, m is calculated as
	 * integer(BRDF x 64 + 0.5)
	 * Example calculation:
	 * If the required baud rate is 230400 and hclk = 4MHz then:
	 * Baud Rate Divisor = (4x10^6)/(16x230400) = 1.085
	 * This means BRDI = 1 and BRDF = 0.085.
	 * Therefore, fractional part, BRDF = integer((0.085x64)+0.5) = 5
	 * Generated baud rate divider = 1+5/64 = 1.078
	 */
	u64 divisor = thunderx_get_io_clock() /
		(baudrate * 16 * uart_sclk_divisor(UART_SCLK_DIV) / 64);
	write32(&uart->pl011.ibrd, divisor >> 6);
	write32(&uart->pl011.fbrd, divisor & UART_FBRD_BAUD_DIVFRAC_MASK);

	/**
	 * 8. Program the line control register UAA(0..1)_LCR_H and the control
	 * register UAA(0..1)_CR
	 */
	/* 8-bits, FIFO enable */
	write32(&uart->pl011.lcr_h, PL011_UARTLCR_H_WLEN_8 |
				    PL011_UARTLCR_H_FEN);
	/* RX/TX enable, UART enable */
	write32(&uart->pl011.cr, PL011_UARTCR_RXE | PL011_UARTCR_TXE |
				 PL011_UARTCR_UARTEN);

	return 0;
}
