/* h316_rtc.c- BBN ARPAnet IMP/TIP Real Time Clock and Watch Dog Timer
   Based on the SIMH simulator package written by Robert M Supnik.

   Copyright (c) 2013 Robert Armstrong, bob@jfcl.com.

   Permission is hereby granted, free of charge, to any person obtaining a
   copy of this software and associated documentation files (the "Software"),
   to deal in the Software without restriction, including without limitation
   the rights to use, copy, modify, merge, publish, distribute, sublicense,
   and/or sell copies of the Software, and to permit persons to whom the
   Software is furnished to do so, subject to the following conditions:

   The above copyright notice and this permission notice shall be included in
   all copies or substantial portions of the Software.

   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
   ROBERT ARMSTRONG BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
   IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
   CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

   Except as contained in this notice, the name of Robert Armstrong shall not be
   used in advertising or otherwise to promote the sale, use or other dealings
   in this Software without prior written authorization from Robert Armstrong.

   rtc          real time clock
   wdt          watch dog timer

   21-May-13    RLA     New file

   OVERVIEW

   The IMP and TIP used a custom real time clock that was apparently created
   by BBN just for those devices.  The IMP/TIP RTC is NOT the same as the
   official Honeywell real time clock option, H316-12.  When emulating an IMP
   or TIP, this RTC device must be enabled and the standard simh H316 CLK
   device must be disabled.  

   The IMP and TIP also had a watch dog timer which, if ever allowed to time
   out, would cause a non-maskable interrupt via location 62(8) - this is the
   same trap location used by the memory lockout option, which the IMP/TIP
   lacked.  Not much is known about the WDT, and the current implementation is
   simply a place holder that doesn't do much.

   RTC state is maintained in a set of state variables:

        ENA             RTC is enabled
        COUNT           current count
        IEN             RTC interrupt enabled
        IRQ             RTC interrupt pending
        TPS             effective ticks per second
        WAIT            simulator time until the next tick

   WDT state is maintained in a set of state variables:

        COUNT           current countdown
        TMO             WDT timed out
        LIGHTS          last "set status lights"
        WAIT            simulator time until the next tick

   TODO

   Implement the WDT!!
*/
#ifdef VM_IMPTIP
#include "h316_defs.h"                  // H316 emulator definitions
#include "h316_imp.h"                   // ARPAnet IMP/TIP definitions

// Locals ...
uint32 rtc_interval = RTC_INTERVAL;     // RTC tick interval (in microseconds)
uint32 rtc_quantum  = RTC_QUANTUM;      // RTC update interval (in ticks)
uint32 rtc_tps      = 1000000UL / (RTC_INTERVAL * RTC_QUANTUM);
uint16 rtc_enabled  = 1;                // RTC enabled
uint32 rtc_count    = 0;                // current RTC count
uint32 wdt_delay    = WDT_DELAY;        // WDT timeout (in milliseconds, 0=none)
uint32 wdt_count    = 0;                // current WDT countdown
uint16 wdt_lights   = 0;                // last "set status lights" output

// Externals from other parts of simh ...
extern uint16 dev_ext_int, dev_ext_enb; // current IRQ and IEN bit vectors
extern int32 PC;                        // current PC (for debug messages)
extern int32 stop_inst;                 // needed by IOBADFNC()

// Forward declarations ...
int32  rtc_io      (int32 inst, int32 fnc, int32 dat, int32 dev);
t_stat rtc_service (UNIT *uptr);
t_stat rtc_reset   (DEVICE *dptr);
int32  wdt_io      (int32 inst, int32 fnc, int32 dat, int32 dev);
t_stat wdt_service (UNIT *uptr);
t_stat wdt_reset   (DEVICE *dptr);
t_stat rtc_set_interval (UNIT *uptr, int32 val, char *cptr, void *desc);
t_stat rtc_show_interval (FILE *st, UNIT *uptr, int32 val, void *desc);
t_stat rtc_set_quantum(UNIT *uptr, int32 val, char *cptr, void *desc);
t_stat rtc_show_quantum (FILE *st, UNIT *uptr, int32 val, void *desc);
t_stat wdt_set_delay (UNIT *uptr, int32 val, char *cptr, void *desc);
t_stat wdt_show_delay (FILE *st, UNIT *uptr, int32 val, void *desc);



////////////////////////////////////////////////////////////////////////////////
//////////////////   R T C   D A T A    S T R U C T U R E S   //////////////////
////////////////////////////////////////////////////////////////////////////////

// RTC device information block ...
DIB rtc_dib = { RTC, 1, IOBUS, IOBUS, INT_V_RTC, INT_V_NONE, &rtc_io, 0 };

// RTC unit data block (we have only one unit!) ...
UNIT rtc_unit = { UDATA (&rtc_service, 0, 0), (RTC_INTERVAL*RTC_QUANTUM) };

// RTC device registers (for "EXAMINE RTC STATE") ...
REG rtc_reg[] = {
  { FLDATA (ENA, rtc_enabled, 0) },
  { DRDATA (COUNT, rtc_count, 16), PV_LEFT },
  { FLDATA (IEN, dev_ext_enb, INT_V_RTC-INT_V_EXTD) },
  { FLDATA (IRQ, dev_ext_int, INT_V_RTC-INT_V_EXTD) },
  { DRDATA (TPS, rtc_tps, 32), PV_LEFT },
  { DRDATA (WAIT, rtc_unit.wait, 24), REG_NZ + PV_LEFT },
  { NULL }
};

// RTC device modifiers (for "SET/SHOW RTC xxx") ...
MTAB rtc_mod[] = {
  { MTAB_XTD|MTAB_VDV, 0, "INTERVAL", "INTERVAL", &rtc_set_interval, &rtc_show_interval, NULL },
  { MTAB_XTD|MTAB_VDV, 0, "QUANTUM",   "QUANTUM", &rtc_set_quantum,   &rtc_show_quantum, NULL },
  { 0 }
};

// RTC debugging flags (for "SET RTC DEBUG=xxx") ...
DEBTAB rtc_debug[] = {
  {"WARN",  IMP_DBG_WARN},
  {"IO",    IMP_DBG_IOT},
  {0}
};

// And finally tie it all together ...
DEVICE rtc_dev = {
  "RTC", &rtc_unit, rtc_reg, rtc_mod,
  1, 0, 0, 0, 0, 0,
  NULL, NULL, &rtc_reset, NULL, NULL, NULL,
  &rtc_dib, DEV_DIS|DEV_DISABLE|DEV_DEBUG, 0, rtc_debug, NULL, NULL
};



////////////////////////////////////////////////////////////////////////////////
//////////////////   W D T   D A T A    S T R U C T U R E S   //////////////////
////////////////////////////////////////////////////////////////////////////////

// WDT device information block ...
DIB wdt_dib = { WDT, 1, IOBUS, IOBUS, INT_V_NONE, INT_V_NONE, &wdt_io, 0 };

// WDT unit data block (it has only one) ...
UNIT wdt_unit = { UDATA (&wdt_service, 0, 0), 1000 };

// WDT device registers (for "EXAMINE WDT STATE") ...
REG wdt_reg[] = {
  { DRDATA (COUNT, wdt_count, 16), PV_LEFT },
  { DRDATA (WAIT, wdt_unit.wait, 24), REG_NZ | PV_LEFT },
  { ORDATA (LIGHTS, wdt_lights, 16), REG_RO | PV_LEFT },
  { NULL }
};

// WDT device modifiers (for "SET/SHOW WDT xxx") ...
MTAB wdt_mod[] = {
  { MTAB_XTD | MTAB_VDV, 0, "DELAY", "DELAY", &wdt_set_delay, &wdt_show_delay, NULL },
  { 0 }
};

// WDT debugging flags (for "SET WDT DEBUG=xxx") ...
DEBTAB wdt_debug[] = {
  {"WARN",   IMP_DBG_WARN},
  {"IO",     IMP_DBG_IOT},
  {"LIGHTS", WDT_DBG_LIGHTS},
  {0}
};

// And summarize ...
DEVICE wdt_dev = {
  "WDT", &wdt_unit, wdt_reg, wdt_mod,
  1, 0, 0, 0, 0, 0,
  NULL, NULL, &wdt_reset, NULL, NULL, NULL,
  &wdt_dib, DEV_DIS|DEV_DISABLE|DEV_DEBUG, 0, wdt_debug, NULL, NULL
};



////////////////////////////////////////////////////////////////////////////////
//////////   R T C   I / O   A N D   S E R V I C E   R O U T I N E S  //////////
////////////////////////////////////////////////////////////////////////////////

// Set and clear the RTC IRQ and IEN ...
#define SET_RTC_IRQ()  SET_EXT_INT((1u << (rtc_dib.inum - INT_V_EXTD)))
#define CLR_RTC_IRQ()  CLR_EXT_INT((1u << (rtc_dib.inum - INT_V_EXTD)))
#define CLR_RTC_IEN()  CLR_EXT_ENB((1u << (rtc_dib.inum - INT_V_EXTD)))

// RTC IO routine ...
int32 rtc_io (int32 inst, int32 fnc, int32 dat, int32 dev)
{
  switch (inst) {
    case ioOCP:
      if (fnc == 010) {
        // CLKOFF - turn the RTC off ...
        sim_cancel(&rtc_unit);  rtc_enabled = 0;  CLR_RTC_IRQ();
        sim_debug(IMP_DBG_IOT, &rtc_dev, "disabled (PC=%06o)\n", PC-1);
        return dat;
      } else if (fnc == 000) {
        // CLKON - turn the RTC on ...
        rtc_enabled = 1;  CLR_RTC_IRQ();
        if (sim_is_active(&rtc_unit) == 0)
          sim_activate (&rtc_unit, sim_rtc_init (rtc_unit.wait));
        sim_debug(IMP_DBG_IOT, &rtc_dev, "enabled (PC=%06o)\n", PC-1);
        return dat;
      }
      break;

    case ioINA:
      if ((fnc == 010) || (fnc == 000)) {
        // RDCLOK - return the current count
        sim_debug(IMP_DBG_IOT, &rtc_dev, "read clock (PC=%06o, RTC=%06o)\n", PC-1, (rtc_count & DMASK));
        return IOSKIP((rtc_count & DMASK));
      }
      break;
  }

  sim_debug(IMP_DBG_WARN, &rtc_dev, "UNIMPLEMENTED I/O (PC=%06o, instruction=%o, function=%02o)\n", PC-1, inst, fnc);
  return IOBADFNC(dat);
}

// RTC unit service ...
t_stat rtc_service (UNIT *uptr)
{
  //   Add the current quantum to the clock register and, if the clock register
  // has overflowed, request an interrupt.  The real hardware interrupts when
  // there is a carry out of the low byte (in other words, every 256 clocks).
  // Note that we can't simply check the low byte for zero to detect overflows
  // because of the quantum.  Since we aren't necessarily incrementing by 1, we
  // may never see a value of exactly zero.  We'll have to be more clever.
  uint8 rtc_high = HIBYTE(rtc_count);
  rtc_count = (rtc_count + rtc_quantum) & DMASK;
  if (HIBYTE(rtc_count) != rtc_high) {
    sim_debug(IMP_DBG_IOT, &rtc_dev, "interrupt request\n");
    SET_RTC_IRQ();
  }
  mi_tx_service(rtc_quantum);
  uptr->wait = sim_rtc_calb (rtc_tps);                  /* recalibrate */
  sim_activate_after (uptr, 1000000/rtc_tps);           /* reactivate unit */
  return SCPE_OK;
}



////////////////////////////////////////////////////////////////////////////////
//////////   W D T   I / O   A N D   S E R V I C E   R O U T I N E S  //////////
////////////////////////////////////////////////////////////////////////////////

// WDT IO routine ...
int32 wdt_io (int32 inst, int32 fnc, int32 dat, int32 dev)
{
  if ((inst == ioOCP) && (fnc == 0)) {
    // Reset WDT ...
    sim_debug(IMP_DBG_IOT, &wdt_dev, "reset (PC=%06o)\n", PC-1);
    return dat;
  } else if ((inst == ioOTA) && (fnc == 0)) {
    // Set status lights ...
      if (wdt_lights != dat) {
        sim_debug(WDT_DBG_LIGHTS, &wdt_dev, "changed to %06o\n", dat);
      }
    sim_debug(IMP_DBG_IOT, &wdt_dev, "set status lights (PC=%06o, LIGHTS=%06o)\n", PC-1, dat);
    wdt_lights = dat;  return dat;
  }

  sim_debug(IMP_DBG_WARN, &wdt_dev, "UNIMPLEMENTED I/O (PC=%06o, instruction=%o, function=%02o)\n", PC-1, inst, fnc);
  return IOBADFNC(dat);
}

// WDT unit service ...
t_stat wdt_service (UNIT *uptr)
{
  return SCPE_OK;
}



////////////////////////////////////////////////////////////////////////////////
///////////////   D E V I C E   A C T I O N   C O M M A N D S   ////////////////
////////////////////////////////////////////////////////////////////////////////

// RTC reset routine ...
t_stat rtc_reset (DEVICE *dptr)
{
  //   Clear the interrupt enable and any pending interrupts, reset the count
  // and enable the clock.  At least I assume that's what a reset does - the
  // docs aren't too specific on this point...
  rtc_enabled = 1;  rtc_count = 0;
  CLR_RTC_IRQ();  CLR_RTC_IEN();
  sim_cancel (&rtc_unit);
  sim_register_clock_unit ((dptr->flags & DEV_DIS) ? NULL : &rtc_unit);
  return SCPE_OK;
}

// WDT reset routine ...
t_stat wdt_reset (DEVICE *dptr)
{
  // Clear the WDT countdown and turn off all the lights ...
  wdt_count = 0;  wdt_lights = 0;
  sim_cancel (&wdt_unit);
  return SCPE_OK;
}



////////////////////////////////////////////////////////////////////////////////
/////////   D E V I C E   S E T   A N D   S H O W   C O M M A N D S   //////////
////////////////////////////////////////////////////////////////////////////////

// Set/Show RTC interval ...
t_stat rtc_set_interval (UNIT *uptr, int32 val, char *cptr, void *desc)
{
  uint32 newint, newtps;  t_stat ret;
  if (cptr == NULL) return SCPE_ARG;
  newint = get_uint (cptr, 10, 1000000, &ret);
  if (ret != SCPE_OK) return ret;
  if (newint == 0) return SCPE_ARG;
  newtps = 1000000UL / (newint * rtc_quantum);
  if ((newtps == 0) || (newtps >= 100000)) return SCPE_ARG;
  rtc_interval = newint;  rtc_tps = newtps;
  uptr->wait = sim_rtc_calb (rtc_tps);
  return SCPE_OK;
}

t_stat rtc_show_interval (FILE *st, UNIT *uptr, int32 val, void *desc)
{
  fprintf(st,"interval=%d (us)", rtc_interval);
  return SCPE_OK;
}

// Set/Show RTC quantum ...
t_stat rtc_set_quantum (UNIT *uptr, int32 val, char *cptr, void *desc)
{
  uint32 newquant, newtps;  t_stat ret;
  if (cptr == NULL) return SCPE_ARG;
  newquant = get_uint (cptr, 10, 1000000, &ret);
  if (ret != SCPE_OK) return ret;
  if (newquant == 0) return SCPE_ARG;
  newtps = 1000000UL / (rtc_interval * newquant);
  if ((newtps == 0) || (newtps >= 100000)) return SCPE_ARG;
  rtc_quantum = newquant;  rtc_tps = newtps;
  uptr->wait = sim_rtc_calb (rtc_tps);
  return SCPE_OK;
}

t_stat rtc_show_quantum (FILE *st, UNIT *uptr, int32 val, void *desc)
{
  fprintf(st,"quantum=%d (ticks)", rtc_quantum);
  return SCPE_OK;
}

// Set/Show WDT delay ...
t_stat wdt_set_delay (UNIT *uptr, int32 val, char *cptr, void *desc)
{
  uint32 newint;  t_stat ret;
  if (cptr == NULL) return SCPE_ARG;
  newint = get_uint (cptr, 10, 65535, &ret);
  if (ret != SCPE_OK) return ret;
  if (newint != 0) {
    fprintf(stderr,"WDT - timeout not yet implemented\n");
    return SCPE_IERR;
  }
  wdt_delay = newint;
// TBA add calculations here???
  return SCPE_OK;
}

t_stat wdt_show_delay (FILE *st, UNIT *uptr, int32 val, void *desc)
{
  if (wdt_delay > 0)
    fprintf(st,"delay=%d (ms)", wdt_delay);
  else
    fprintf(st,"no timeout");
  return SCPE_OK;
}

#endif // #ifdef VM_IMPTIP from the very top
