/**
 * @file op_rtc.c
 * Setup and handling of RTC interrupts
 *
 * @remark Copyright 2002 OProfile authors
 * @remark Read the file COPYING
 *
 * @author Bob Montgomery
 * @author Philippe Elie
 * @author John Levon
 */

#include <linux/ioport.h>
#include <linux/mc146818rtc.h>
#include <asm/ptrace.h>

#include "oprofile.h"
#include "op_arch.h"
#include "op_util.h"

#define RTC_IO_PORTS 2

/* not in 2.2 */
#ifndef RTC_IRQ
#define RTC_IRQ 8
#endif

/* ---------------- RTC handler ------------------ */

static void do_rtc_interrupt(int irq, void * dev_id, struct pt_regs * regs)
{
	uint cpu = op_cpu_id();
	unsigned char intr_flags;
	unsigned long flags;

	int usermode = user_mode(regs);
	if ((sysctl.ctr[0].kernel && usermode)
		|| (sysctl.ctr[0].user && !usermode))
		return;

	lock_rtc(flags);

	/* read and ack the interrupt */
	intr_flags = CMOS_READ(RTC_INTR_FLAGS);
	/* Is this my type of interrupt? */
	if (intr_flags & RTC_PF) {
		op_do_profile(cpu, instruction_pointer(regs), IRQ_ENABLED(regs), 0);
	}

	unlock_rtc(flags);

	return;
}

static int rtc_setup(void)
{
	unsigned char tmp_control;
	unsigned long flags;
	unsigned char tmp_freq_select;
	unsigned long target;
	unsigned int exp, freq;

	lock_rtc(flags);

	/* disable periodic interrupts */
	tmp_control = CMOS_READ(RTC_CONTROL);
	tmp_control &= ~RTC_PIE;
	CMOS_WRITE(tmp_control, RTC_CONTROL);
	CMOS_READ(RTC_INTR_FLAGS);

	/* Set the frequency for periodic interrupts by finding the
	 * closest power of two within the allowed range.
	 */

	target = sysctl.ctr[0].count;

	exp = 0;
	while (target > (1 << exp) + ((1 << exp) >> 1))
		exp++;
	freq = 16 - exp;

	tmp_freq_select = CMOS_READ(RTC_FREQ_SELECT);
	tmp_freq_select = (tmp_freq_select & 0xf0) | freq;
	CMOS_WRITE(tmp_freq_select, RTC_FREQ_SELECT);

	/* Update /proc with the actual frequency. */
	sysctl_parms.ctr[0].count = sysctl.ctr[0].count = 1 << exp;

	unlock_rtc(flags);
	return 0;
}

static void rtc_start(void)
{
	unsigned char tmp_control;
	unsigned long flags;

	lock_rtc(flags);

	/* Enable periodic interrupts */
	tmp_control = CMOS_READ(RTC_CONTROL);
	tmp_control |= RTC_PIE;
	CMOS_WRITE(tmp_control, RTC_CONTROL);

	/* read the flags register to start interrupts */
	CMOS_READ(RTC_INTR_FLAGS);

	unlock_rtc(flags);
}

static void rtc_stop(void)
{
	unsigned char tmp_control;
	unsigned long flags;

	lock_rtc(flags);

	/* disable periodic interrupts */
	tmp_control = CMOS_READ(RTC_CONTROL);
	tmp_control &= ~RTC_PIE;
	CMOS_WRITE(tmp_control, RTC_CONTROL);
	CMOS_READ(RTC_INTR_FLAGS);

	unlock_rtc(flags);
}

static void rtc_start_cpu(uint cpu)
{
	rtc_start();
}

static void rtc_stop_cpu(uint cpu)
{
	rtc_stop();
}

static int rtc_check_params(void)
{
	int target = sysctl.ctr[0].count;

	if (check_range(target, OP_MIN_RTC_COUNT, OP_MAX_RTC_COUNT,
		"RTC value %d is out of range (%d-%d)\n"))
		return -EINVAL;

	return 0;
}

static int rtc_init(void)
{
	 /* request_region returns 0 on **failure** */
	if (!request_region_check(RTC_PORT(0), RTC_IO_PORTS, "oprofile")) {
		printk(KERN_ERR "oprofile: can't get RTC I/O Ports\n");
		return -EBUSY;
	}

	/* request_irq returns 0 on **success** */
	if (request_irq(RTC_IRQ, do_rtc_interrupt,
			SA_INTERRUPT, "oprofile", NULL)) {
		printk(KERN_ERR "oprofile: IRQ%d busy \n", RTC_IRQ);
		release_region(RTC_PORT(0), RTC_IO_PORTS);
		return -EBUSY;
	}
	return 0;
}

static void rtc_deinit(void)
{
	free_irq(RTC_IRQ, NULL);
	release_region(RTC_PORT(0), RTC_IO_PORTS);
}

static int rtc_add_sysctls(ctl_table * next)
{
	*next = ((ctl_table) { 1, "rtc_value", &sysctl_parms.ctr[0].count, sizeof(int), 0600, NULL, lproc_dointvec, NULL, });
	return 0;
}

static void rtc_remove_sysctls(ctl_table * next)
{
	/* nothing to do */
}

struct op_int_operations op_rtc_ops = {
	init: rtc_init,
	deinit: rtc_deinit,
	add_sysctls: rtc_add_sysctls,
	remove_sysctls: rtc_remove_sysctls,
	check_params: rtc_check_params,
	setup: rtc_setup,
	start: rtc_start,
	stop: rtc_stop,
	start_cpu: rtc_start_cpu,
	stop_cpu: rtc_stop_cpu,
};