/*
 * Copyright (C) 2008 Michael Brown <mbrown@fensystems.co.uk>.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include <limits.h>
#include <assert.h>
#include <unistd.h>
#include <gpxe/timer.h>
#include <gpxe/efi/efi.h>
#include <gpxe/efi/Protocol/Cpu.h>

/** @file
 *
 * gPXE timer API for EFI
 *
 */

/** Scale factor to apply to CPU timer 0
 *
 * The timer is scaled down in order to ensure that reasonable values
 * for "number of ticks" don't exceed the size of an unsigned long.
 */
#define EFI_TIMER0_SHIFT 12

/** Calibration time */
#define EFI_CALIBRATE_DELAY_MS 1

/** CPU protocol */
static EFI_CPU_ARCH_PROTOCOL *cpu_arch;
EFI_REQUIRE_PROTOCOL ( EFI_CPU_ARCH_PROTOCOL, &cpu_arch );

/**
 * Delay for a fixed number of microseconds
 *
 * @v usecs		Number of microseconds for which to delay
 */
static void efi_udelay ( unsigned long usecs ) {
	EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
	EFI_STATUS efirc;

	if ( ( efirc = bs->Stall ( usecs ) ) != 0 ) {
		DBG ( "EFI could not delay for %ldus: %s\n",
		      usecs, efi_strerror ( efirc ) );
		/* Probably screwed */
	}
}

/**
 * Get current system time in ticks
 *
 * @ret ticks		Current time, in ticks
 */
static unsigned long efi_currticks ( void ) {
	UINT64 time;
	EFI_STATUS efirc;

	/* Read CPU timer 0 (TSC) */
	if ( ( efirc = cpu_arch->GetTimerValue ( cpu_arch, 0, &time,
						 NULL ) ) != 0 ) {
		DBG ( "EFI could not read CPU timer: %s\n",
		      efi_strerror ( efirc ) );
		/* Probably screwed */
		return -1UL;
	}

	return ( time >> EFI_TIMER0_SHIFT );
}

/**
 * Get number of ticks per second
 *
 * @ret ticks_per_sec	Number of ticks per second
 */
static unsigned long efi_ticks_per_sec ( void ) {
	static unsigned long ticks_per_sec = 0;

	/* Calibrate timer, if necessary.  EFI does nominally provide
	 * the timer speed via the (optional) TimerPeriod parameter to
	 * the GetTimerValue() call, but it gets the speed slightly
	 * wrong.  By up to three orders of magnitude.  Not helpful.
	 */
	if ( ! ticks_per_sec ) {
		unsigned long start;
		unsigned long elapsed;

		DBG ( "Calibrating EFI timer with a %d ms delay\n",
		      EFI_CALIBRATE_DELAY_MS );
		start = currticks();
		mdelay ( EFI_CALIBRATE_DELAY_MS );
		elapsed = ( currticks() - start );
		ticks_per_sec = ( elapsed * ( 1000 / EFI_CALIBRATE_DELAY_MS ));
		DBG ( "EFI CPU timer calibrated at %ld ticks in %d ms (%ld "
		      "ticks/sec)\n", elapsed, EFI_CALIBRATE_DELAY_MS,
		      ticks_per_sec );
	}

	return ticks_per_sec;
}

PROVIDE_TIMER ( efi, udelay, efi_udelay );
PROVIDE_TIMER ( efi, currticks, efi_currticks );
PROVIDE_TIMER ( efi, ticks_per_sec, efi_ticks_per_sec );
