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

#include <arch/io.h>
#include <commonlib/helpers.h>
#include <cpu/x86/tsc.h>
#include <delay.h>
#include <pc80/i8254.h>

/* Initialize i8254 timers */

void setup_i8254(void)
{
	/* Timer 0 (taken from biosemu) */
	outb(TIMER0_SEL | WORD_ACCESS | MODE3 | BINARY_COUNT, TIMER_MODE_PORT);
	outb(0x00, TIMER0_PORT);
	outb(0x00, TIMER0_PORT);

	/* Timer 1 */
	outb(TIMER1_SEL | LOBYTE_ACCESS | MODE3 | BINARY_COUNT,
	     TIMER_MODE_PORT);
	outb(0x12, TIMER1_PORT);
}

#define CLOCK_TICK_RATE	1193180U /* Underlying HZ */

/* ------ Calibrate the TSC -------
 * Too much 64-bit arithmetic here to do this cleanly in C, and for
 * accuracy's sake we want to keep the overhead on the CTC speaker (channel 2)
 * output busy loop as low as possible. We avoid reading the CTC registers
 * directly because of the awkward 8-bit access mechanism of the 82C54
 * device.
 */

#define CALIBRATE_INTERVAL ((2*CLOCK_TICK_RATE)/1000) /* 2ms */
#define CALIBRATE_DIVISOR  (2*1000) /* 2ms / 2000 == 1usec */

unsigned long calibrate_tsc_with_pit(void)
{
	/* Set the Gate high, disable speaker */
	outb((inb(0x61) & ~0x02) | 0x01, 0x61);

	/*
	 * Now let's take care of CTC channel 2
	 *
	 * Set the Gate high, program CTC channel 2 for mode 0,
	 * (interrupt on terminal count mode), binary count,
	 * load 5 * LATCH count, (LSB and MSB) to begin countdown.
	 */
	outb(0xb0, 0x43);	/* binary, mode 0, LSB/MSB, Ch 2 */

	outb(CALIBRATE_INTERVAL	& 0xff, 0x42);	/* LSB of count */
	outb(CALIBRATE_INTERVAL	>> 8, 0x42);	/* MSB of count */

	{
		tsc_t start;
		tsc_t end;
		unsigned long count;

		start = rdtsc();
		count = 0;
		do {
			count++;
		} while ((inb(0x61) & 0x20) == 0);
		end = rdtsc();

		/* Error: ECTCNEVERSET */
		if (count <= 1)
			goto bad_ctc;

		/* 64-bit subtract - gcc just messes up with long longs */
		__asm__("subl %2,%0\n\t"
			"sbbl %3,%1"
			: "=a" (end.lo), "=d" (end.hi)
			: "g" (start.lo), "g" (start.hi),
			 "0" (end.lo), "1" (end.hi));

		/* Error: ECPUTOOFAST */
		if (end.hi)
			goto bad_ctc;

		/* Error: ECPUTOOSLOW */
		if (end.lo <= CALIBRATE_DIVISOR)
			goto bad_ctc;

		return DIV_ROUND_UP(end.lo, CALIBRATE_DIVISOR);
	}

	/*
	 * The CTC wasn't reliable: we got a hit on the very first read,
	 * or the CPU was so fast/slow that the quotient wouldn't fit in
	 * 32 bits..
	 */
bad_ctc:
	return 0;
}

#if CONFIG(UNKNOWN_TSC_RATE)
static u32 timer_tsc;

unsigned long tsc_freq_mhz(void)
{
	if (timer_tsc > 0)
		return timer_tsc;

	timer_tsc = calibrate_tsc_with_pit();

	/* Set some semi-ridiculous rate if approximation fails. */
	if (timer_tsc == 0)
		timer_tsc = 5000;

	return timer_tsc;
}
#endif

void beep(unsigned int frequency_hz, unsigned int duration_msec)
{
	unsigned int count = CLOCK_TICK_RATE / frequency_hz;

	/* Set command for counter 2, 2 byte write, mode 3 */
	outb(TIMER2_SEL | WORD_ACCESS | MODE3, TIMER_MODE_PORT);

	/* Select desired Hz */
	outb(count & 0xff, TIMER2_PORT);
	outb((count >> 8) & 0xff, TIMER2_PORT);

	/* Switch on the speaker */
	outb(inb(PPC_PORTB) | (PPCB_T2GATE | PPCB_SPKR), PPC_PORTB);

	/* Block for specified milliseconds */
	mdelay(duration_msec);

	/* Switch off the speaker */
	outb(inb(PPC_PORTB) & ~(PPCB_T2GATE | PPCB_SPKR), PPC_PORTB);
}