/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * Copyright 2006 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

#pragma ident	"%Z%%M%	%I%	%E% SMI"

/*
 * tod driver module for OPL (implements a soft tod)
 */

#include <sys/types.h>
#include <sys/param.h>
#include <sys/sysmacros.h>
#include <sys/systm.h>
#include <sys/errno.h>
#include <sys/modctl.h>
#include <sys/autoconf.h>
#include <sys/debug.h>
#include <sys/clock.h>
#include <sys/cmn_err.h>
#include <sys/prom_plat.h>
#include <sys/cpuvar.h>
#include <sys/opl_module.h>

/*
 * Debug stuff
 */
#ifdef DEBUG
int todopl_debug = 0;
#define	TODOPL_DEBUG(args)  if (todopl_debug) cmn_err args
#else
#define	TODOPL_DEBUG(args)
#endif

#define	abs(x)	((x) < 0 ? -(x) : (x))

#define	TODOPL_SET_THRESHOLD	30

static timestruc_t	todopl_get(void);
static void		todopl_set(timestruc_t);
static uint_t		todopl_set_watchdog_timer(uint_t);
static uint_t		todopl_clear_watchdog_timer(void);
static void		todopl_set_power_alarm(timestruc_t);
static void		todopl_clear_power_alarm(void);
static uint64_t		todopl_get_cpufrequency(void);

/*
 * Module linkage information for the kernel.
 */
static struct modlmisc modlmisc = {
	&mod_miscops, "Soft tod module for OPL 1.11"
};

static struct modlinkage modlinkage = {
	MODREV_1, (void *)&modlmisc, NULL
};

/*
 * The TOD OPL logic description.
 *
 * The todopl driver uses promif functions prom_opl_get_tod() and
 * prom_opl_set_diff(). These functions call FJSV,get-tod and
 * FJSV,set-domain-time OBP client services.
 *
 * At the system boot or reboot:
 *
 *    FJSV,tod-get
 * OS  --------->   OBP     SCF I/F
 *                         ----------->  XSCF
 *                         <-----------
 *     <--------            time, diff
 *    time+diff, stick
 *
 * Note that on first powerup domain boot, diff is zero.
 *
 * When system updates the time via date(1m):
 *
 *   FJSV,set-domain-time
 * OS   --------->   OBP                      SRAM
 *      diff_delta        diff += diff_delta ------------->  XSCF
 *
 * diff_delta = new time -  current domain time (hrestime)
 *
 *
 * In theory, FJSV,get-tod and FJSV,set-domain-time should never fails.
 * But, if call to FJSV,get-tod fails on boot, the domain will be unable
 * to calculate "diff" properly and synchronization between Domain and
 * SP will be broken. In this particular case, we notify users that
 * "there is no time synchronization" and the logic will attempt to
 * resync with the SP whenever the OS tries to do a TOD update.
 * (e.g. via date(1m) or NTP).
 */

static	int enable_time_sync = 1;

int
_init(void)
{
	int64_t	stick;
	time_t	obp_time = 0;
	int64_t obp_stick;

	if (strcmp(tod_module_name, "todopl") == 0) {
		/*
		 * Get TOD time from OBP and adjust it.
		 */
		prom_opl_get_tod(&obp_time, &obp_stick);

		TODOPL_DEBUG((CE_NOTE, "todopl: OBP time 0x%lx stick 0x%lx\n",
			obp_time, obp_stick));

		if (obp_time != 0) {
			/*
			 * adjust OBP time by stick counts
			 */
			stick_timestamp(&stick);
			obp_time += ((stick - obp_stick) / system_clock_freq);

			TODOPL_DEBUG((CE_NOTE,
				"todopl: cpu stick 0x%lx sys_time 0x%lx\n",
				stick, obp_time));
		} else {
			/*
			 * A date of zero causes the root filesystem driver
			 * to try to set the date from the last shutdown.
			 */
			enable_time_sync = 0;
			cmn_err(CE_WARN, "Initial date is invalid.");
			cmn_err(CE_CONT, "Attempting to set the date and time "
				"based on the last shutdown.\n");
			cmn_err(CE_CONT, "The time could not be synchronized "
				"between Domain and Service Processor.\n");
			cmn_err(CE_CONT, "Please inspect the date and time and "
				"correct if necessary.\n");
		}

		hrestime.tv_sec = obp_time;

		/*
		 * Check that the date has not overflowed a 32-bit integer.
		 */
		if (TIMESPEC_OVERFLOW(&hrestime)) {
			cmn_err(CE_WARN, "Date overflow detected.");
			cmn_err(CE_CONT, "Attempting to set the date and time "
				"based on the last shutdown.\n");
			cmn_err(CE_CONT, "Please inspect the date and time and "
				"correct if necessary.\n");

			hrestime.tv_sec = (time_t)0;
		}

		tod_ops.tod_get = todopl_get;
		tod_ops.tod_set = todopl_set;
		tod_ops.tod_set_watchdog_timer = todopl_set_watchdog_timer;
		tod_ops.tod_clear_watchdog_timer = todopl_clear_watchdog_timer;
		tod_ops.tod_set_power_alarm = todopl_set_power_alarm;
		tod_ops.tod_clear_power_alarm = todopl_clear_power_alarm;
		tod_ops.tod_get_cpufrequency = todopl_get_cpufrequency;

		/*
		 * Flag warning if user tried to use hardware watchdog
		 */
		if (watchdog_enable) {
			cmn_err(CE_WARN, "Hardware watchdog unavailable");
		}
	}

	return (mod_install(&modlinkage));
}

int
_fini(void)
{
	if (strcmp(tod_module_name, "todopl") == 0)
		return (EBUSY);
	else
		return (mod_remove(&modlinkage));
}

int
_info(struct modinfo *modinfop)
{
	return (mod_info(&modlinkage, modinfop));
}


/*
 * OPL tod_get is simplified to return hrestime
 * Must be called with tod_lock held.
 */
static timestruc_t
todopl_get(void)
{
	ASSERT(MUTEX_HELD(&tod_lock));
	return (hrestime);
}

/*
 * Must be called with tod_lock held.
 *
 * When running NTP, tod_set is called at least once per second in order
 * to update the hardware clock. To minimize pressure on SP, we want only
 * to record significant time changes on the SP (when date(1M) is run).
 * We have 30 seconds threshold requirement before recording the time change.
 */
/* ARGSUSED */
static void
todopl_set(timestruc_t ts)
{
	ASSERT(MUTEX_HELD(&tod_lock));

	if (abs(ts.tv_sec - hrestime.tv_sec) > TODOPL_SET_THRESHOLD) {
		/*
		 * Send time difference to SP
		 */
		if (enable_time_sync)
			prom_opl_set_diff(ts.tv_sec - hrestime.tv_sec);
		else {
			/*
			 * We did not get a successful initial time
			 * update/sync from the SP via OBP during boot.
			 * Try again here.
			 */
			time_t  obp_time = 0;
			int64_t obp_stick;
			int64_t stick;

			prom_opl_get_tod(&obp_time, &obp_stick);

			if (obp_time != 0) {
				/*
				 * adjust OBP time by stick counts
				 */
				stick_timestamp(&stick);
				obp_time += ((stick - obp_stick) /
					system_clock_freq);

				/*
				 * Sync up by computing the diff using the
				 * newly acquired SP/OBP reference time
				 */
				prom_opl_set_diff(ts.tv_sec - obp_time);

				enable_time_sync = 1;
			}
		}
		TODOPL_DEBUG((CE_NOTE, "todopl_set: new domain time 0x%lx\n",
			ts.tv_sec));
	}
}

/*
 * No watchdog function.
 */
/* ARGSUSED */
static uint_t
todopl_set_watchdog_timer(uint_t timeoutval)
{
	ASSERT(MUTEX_HELD(&tod_lock));
	return (0);
}

/*
 * No watchdog function
 */
static uint_t
todopl_clear_watchdog_timer(void)
{
	ASSERT(MUTEX_HELD(&tod_lock));
	return (0);
}

/*
 * Null function.
 */
/* ARGSUSED */
static void
todopl_set_power_alarm(timestruc_t ts)
{
	ASSERT(MUTEX_HELD(&tod_lock));
}

/*
 * Null function
 */
static void
todopl_clear_power_alarm()
{
	ASSERT(MUTEX_HELD(&tod_lock));
}

/*
 * Get clock freq from the cpunode.  This function is only called
 * when use_stick = 0, otherwise, system_clock_freq gets used instead.
 */
uint64_t
todopl_get_cpufrequency(void)
{
	return (cpunodes[CPU->cpu_id].clock_freq);
}