 /*
 *   DIVA PC ARM C source
 *
 *   DEV.C.MultiIO - Serial and parallel port emulation
 *
 *  Versions
 *  15-02-93  INH  Original
 *  15-03-93       Rewritten
 *  15-02-95       Version for Gemini only
 */

#include <stdio.h>
#include "os.h"
#include "swis.h"

#include "sys.h.stdtypes"
#include "sys.h.sys"
#include "sys.h.config"
#include "module.h.pc_io"


#define COM_IRQ   4
#define LPT_IRQ   7
#define LPT_FIRST  0x378
#define LPT_LAST   0x37B
#define COM_FIRST  0x3F8
#define COM_LAST   0x3FF

#define SWI(x) os_swix( x, &COM_SwiRegs )
#define regs(n) (COM_SwiRegs.r[n])

/* Globals ********************************************************* */

static os_regset COM_SwiRegs;



/* LPT port access handlers ********************************************* */

#define LPT_IRQENB 0x10
#define LPT_NACK   0x40

static FILE *LPT_file;
static bool  LPT_Open;

static BYTE *LPT_IObase;      /* LPT hardware itself - only in SVC mode!*/

static int   LPT_CtrlVal;
static bool  LPT_IntEdge;


/* On a PC, the LPT port will generate an INT on the _falling_ edge of
    (ENABLE & ACK), where ENABLE is the int enable bit (Ctrl port bit 4),
    and ACK is the inverse of the -ACK line (Status port bit 6).

   In other words, we have an interrupt when ENABLE is a 1 and -ACK goes
    from 0 to 1, and also when -ACK is 0 and ENABLE goes from 1 to 0. (Yes,
    that's an interrupt generated immediately after you've disabled them!).

   LPT_IntEdge is true when IRQ enable is 1 and -ACK is 0. An
   interrupt will be generated when LPT_IntEdge goes from true
   to false.

*/

/* ---------------------- */

static void LPT_IrqCheck ( int status_reg )
{

  if (    (LPT_CtrlVal & LPT_IRQENB) != 0
       && (status_reg  & LPT_NACK)   == 0 )
  {
    LPT_IntEdge = true;
  }
  else if ( LPT_IntEdge )
  {
    LPT_IntEdge = false;
    SYS_Interrupt ( LPT_IRQ );
  }
}

/* ---------------------- */

static void LPT_IrqHandler(void)
   /* Called on rising edge of -ACK when interrupts are enabled */
{
  LPT_IntEdge = false;
  SYS_Interrupt ( LPT_IRQ );
}

/* ---------------------- */

static int LPT_Rd8( int addr )
{
  switch ( addr & 3 )
  {
    case 0: /* Data port */
      return LPT_IObase [ 0 ];

    case 1: /* Status port */
      return LPT_IObase [ 4 ] | 7;  /* Caution! Usually !ERROR bit is 0 */

    case 2:
      return LPT_IObase [ 8 ];
  }

  return 0xFF;
}

/* -------------------------- */

static void LPT_Wr8( int addr, int data )
{
  switch ( addr & 3 )
  {
    case 0: /* Data port */
      LPT_IObase [ 0 ] = data;
      break;

    case 1: /* Status port: don't allow writes */
      break;

    case 2: /* Ctrl port */
      LPT_CtrlVal = data;
      LPT_IObase[8] = data;       /* Make ctrl port change */
      LPT_IrqCheck ( LPT_IObase[4] );
      break;
  }
}

/* -------------------------- */

static int LPT_Rd16( int addr )
{
  return LPT_Rd8(addr) + (LPT_Rd8(addr+1) << 8);  /* Undefined order! */
}


/* -------------------------- */

static void LPT_Wr16( int addr, int data )
{
  LPT_Wr8 (addr, data & 0xFF);
  LPT_Wr8 (addr+1, data >> 8);
}

/* ------------------------------------------------- */


static bool FindLPTsupport()
{
  /* Initialise LPT & check signature in R0 */

  regs(0) = 0;
  if (SWI ( PCIO_LPTInit ) != 0 || (regs(0) != 0xAFE9) )
    return false;

  LPT_IObase = (BYTE *) regs(1);
  return true;
}

/* ------------------------------------------------- */

static bool ReserveLPT ( void )
{
  /* Open Parallel: to stop others using it */

  if ( LPT_file == NULL )
    LPT_file = fopen("PARALLEL:", "r+" );
  if ( LPT_file == NULL )
    return false;

  /* Set up PCIO LPT interrupts */

  os_swi1 ( PCIO_LPTSetCallback, (int) LPT_IrqHandler );

  LPT_Open = true;

  return true;
}

/* ------------------------------------------------- */

static bool ReleaseLPT ( void )
{
  /* De-register IRQ handler routine */
  os_swi1 ( PCIO_LPTSetCallback, 0 );

  if ( LPT_file != NULL )
    fclose ( LPT_file );

  LPT_file = NULL;
  LPT_Open = false;

  return true;
}





/* COM port handlers ********************************************** */


#define RBR 0   /* Receive buffer reg */
#define THR 0   /* Transmit holding reg */
#define IER 1   /* Interrupt enable reg */
#define IIR 2   /* Interrupt identification reg */
#define LCR 3   /* Line control reg */
#define MCR 4   /* Modem control reg */
#define LSR 5   /* Line status reg */
#define MSR 6   /* Modem status reg */
#define SCR 7   /* Scratch reg */
#define DLL 8   /* Baud rate divisor LSB */
#define DLM 9   /* Baud rate divisor MSB */
#define COM_MAXREGS 10

#define LCR_DLAB 0x80 /* Divisor Latch access bit in LCR */

static int DLAB_Regs[8] = { DLL, DLM, 2, 3, 4, 5, 6, 7 };

/* bits in Interrupt Enable reg */

#define IEN_RXRDY    1
#define IEN_TXRDY    2
#define IEN_LINESTAT 4
#define IEN_MDMSTAT  8

/* Values for COM_Status bits:

   bit 0: 1 if rx data available
   bit 1: 1 if an rx overrun has occurred
   bit 2: 1 if parity error
   bit 3: 1 if framing error
   bit 4: 1 if break detected
   bit 5: 1 if Tx buffer empty
   bit 6: 1 if Tx idle
   bit 8: Delta CTS
   bit 9: Delta DSR
   bit 10: Delta RI
   bit 11: Delta DCD
   bit 12: CTS state
   bit 13: DSR state
   bit 14: RI  state
   bit 15: DCD state
   bit 16: 1 if Tx empty bit has gone 0->1

   bits 0..7 form the Line Status Register, and bits 8..15 the
     Modem Status register
*/

#define SR_RXRDY   0x01
#define SR_RXOVR   0x02
#define SR_PARERR  0x04
#define SR_FRMERR  0x08
#define SR_BRKERR  0x10
#define SR_TXEMPT  0x20
#define SR_TXIDLE  0x40

#define SR_ERRORS  0x1E

#define SR_DELTAS  0x0F00
#define SR_MDMSTAT 0xF000

#define SR_TXINT   0x10000
/* TxInt is a Transmitter Empty Interrupt in-service bit; this
   is not (as the data books claim) the same as the TXEMPT bit,
   because clearing the TX interrupt (by reading the IIR register
   when the TX interrupt is the only active one) doesn't clear the
   TXEMPT bit. This bit is cleared when the THR is written with
   a byte or by reading the IIR, and set when TXEMPT goes from 0->1,
   or when TXEMPT is 1 and the IER is written with the TXINT bit
   set.
*/

/* Values in COM_Status which don't latch */

#define SR_NONSTICKYBITS (SR_RXRDY|SR_TXEMPT|SR_TXIDLE|SR_MDMSTAT)

/* Values read from Interrupt Id register */

#define ID_LINEINT 6
#define ID_RXINT   4
#define ID_TXINT   2
#define ID_MDMINT  0
#define ID_NOINT   1

/* COM state variables --------------------- */

static bool  COM_Open;
static FILE *COM_file  = NULL;
static BYTE  COM_Regs[COM_MAXREGS];
static int   COM_ActiveInt;    /* Value of active interrupt (IEN_RXRDY etc),
                                   0 if none currently pending, or all
                                   disabled. */

static int   COM_Status;

/* -------------------------- */

/* Called whenever any of the status bits shown above change; also
   can be called to re-check the interrupt status.
*/

static void COM_SetStatus ( int bitmask, int newbits )
{
  int oldval = COM_Status;

  COM_Status = (COM_Status & ~bitmask) | newbits;

  /* Check for edge on TXEMPT bit; if so, set interrupt */

  if ( ~oldval & COM_Status & SR_TXEMPT )
    COM_Status |= SR_TXINT;

#ifdef DEBUG
  if ( oldval != COM_Status )
    SYS_trace("COM Status %X->%X", oldval, COM_Status );
#endif

  /* Now check interrupt status */

  oldval = COM_ActiveInt;

  COM_ActiveInt =   ( (COM_Status & SR_ERRORS) ? IEN_LINESTAT : 0 )
                  + ( (COM_Status & SR_RXRDY ) ? IEN_RXRDY    : 0 )
                  + ( (COM_Status & SR_TXINT ) ? IEN_TXRDY    : 0 )
                  + ( (COM_Status & SR_DELTAS) ? IEN_MDMSTAT  : 0 );

  COM_ActiveInt &= COM_Regs[IER];

#ifdef DEBUG
  if ( oldval != COM_ActiveInt )
    SYS_trace("COM Int %X->%X", oldval, COM_ActiveInt );
#endif

  /* Interrupt if these have just changed from inactive to active */

  if ( oldval == 0 && COM_ActiveInt != 0 )
  {

#ifdef DEBUG
    SYS_trace("Make COM irq, status %X, active %X",
                                        COM_Status, COM_ActiveInt );
#endif

    SYS_Interrupt ( COM_IRQ );
  }

}

/* -------------------- */

static int COM_ReadIIR (void)
{
  if      ( COM_ActiveInt & IEN_LINESTAT )
    return ID_LINEINT;
  else if ( COM_ActiveInt & IEN_RXRDY )
    return ID_RXINT;
  else if ( COM_ActiveInt & IEN_TXRDY )
  {
    COM_SetStatus( SR_TXINT, 0 );  /* Clear TXINT bit */
    return ID_TXINT;
  }
  else if ( COM_ActiveInt & IEN_MDMSTAT )
    return ID_MDMINT;

  return ID_NOINT;
}


/* This is called to generally update the serial port status
   bits, either before a read of a status register or
   from a callback from the PCIO module.

   The TxIdle/TxEmpty/RxReady amd modem-status bits are
   replaced with the new values, the error and modem 'delta'
   bits are ORred in.

   Note that the PCIO module keeps a set of 'pending' flags
   which indicate which latching bits have been set since the
   last time COM_GetStatus was called.

*/

static void COM_UpdateStatus ( void )
{
  SWI( PCIO_COMGetStatus );
  COM_SetStatus( SR_NONSTICKYBITS, regs(0) );
}

/* -------------------------- */

static int COM_Rd8( int addr )
{
  int tmp;

  if ( COM_Regs[LCR] & LCR_DLAB ) /* Divisor latch access bit */
    addr = DLAB_Regs[addr & 7 ];
  else
    addr &= 7;

  switch ( addr )
  {
    case RBR: /* Receive buffer register */
      COM_SetStatus ( SR_RXRDY, 0 );     /* Clear 'ready' interrupt */

      SWI ( PCIO_COMReceive );  /* Get Rx character */

#ifdef DEBUG
      SYS_trace("RdCh %Xh", regs(1) );
#endif
      return regs(1);

    case IIR: /* Int ID register: possibly clears Tx int as well */
      tmp = COM_ReadIIR();
#ifdef DEBUG
      SYS_trace("ReadIIR = %X", tmp );
#endif
      return tmp;


    case LSR: /* Line status register ---------- */
      COM_UpdateStatus();
      tmp = COM_Status & 0xFF;
      COM_SetStatus ( SR_ERRORS, 0 );
#ifdef DEBUG
      SYS_trace("RdStatus %Xh", tmp);
#endif
      return tmp;

    case MSR: /* Modem status register ---------- */
      COM_UpdateStatus();
      tmp = (COM_Status >> 8 ) & 0xFF;
      COM_SetStatus ( SR_DELTAS, 0 );
#ifdef DEBUG
      SYS_trace("RdModem %Xh", tmp);
#endif
      return tmp;

    default:
      break;
  }

  return COM_Regs[addr];
}

/* -------------------------- */

static void COM_Wr8( int addr, int data )
{

  if ( COM_Regs[LCR] & LCR_DLAB ) /* Divisor latch access bit */
    addr = DLAB_Regs[addr & 7 ];
  else
    addr &= 7;

  COM_Regs[addr] = data;

  switch ( addr )
  {
    case DLL:
    case DLM:
      os_swi1 (PCIO_COMSetRate, (COM_Regs[DLM] << 8) + COM_Regs[DLL] );
      break;

    case THR: /* TX buffer register */
#ifdef DEBUG
      SYS_trace("WrCh %Xh", data );
#endif
      COM_SetStatus ( SR_TXINT | SR_TXEMPT | SR_TXIDLE, 0 );
      os_swi1 ( PCIO_COMTransmit, data );
      break;

    case IER: /* Interrupt Enable register: recheck active ints, etc */
#ifdef DEBUG
      SYS_trace("IER := %Xh", data );
#endif
      /* Note: Although slightly strange, the code below seems to best
         fit the observed behaviour: a Tx empty interrupt can be caused
         merely by writing to the Int Enable register with the TXRDY bit
         set, provided that TxEmpty is true at the time. The interrupt
         can be cleared by reading IIR, or writing THR */

      if ( (data & IEN_TXRDY) != 0 && (COM_Status & SR_TXEMPT) != 0 )
        COM_SetStatus ( SR_TXINT, SR_TXINT ); /* Set Tx int */
      else
        COM_SetStatus( 0, 0);              /* Recheck status anyway */
      break;

    case LCR: /* Line Ctrl register ------------- */
      os_swi1 ( PCIO_COMSetByteFormat, data & 0x7F);
      break;

    case MCR: /* Modem Ctrl register ------------ */
      COM_Regs[MCR] &= 0x1F;  /* bits 5..7 should read back zero */

      if ( data & 0x10 )
        SYS_trace("Unsupported: 8250 Loop mode"); /* !! */

      os_swi1 ( PCIO_COMSetModem, data & 3 ); /* Only set DTR/RTS */
      break;
  }

}

/* -------------------------- */

static int COM_Rd16( int addr )
{
  return COM_Rd8(addr) + (COM_Rd8(addr+1) << 8);  /* Don't define order! */
}

/* -------------------------- */

static void COM_Wr16( int addr, int data )
{
  COM_Wr8 (addr, data & 0xFF);
  COM_Wr8 (addr+1, data >> 8);
}

/* -------------------------- */



static bool FindCOMsupport()
{
  /* Initialise COM & check signature in R0 */

  regs(0) = 0;
  if (SWI ( PCIO_COMInit ) != 0 || (regs(0) != 0xAFE4) )
    return false;

  return true;
}


/* ------------------------------------------------- */

static bool ReserveCOM ( void )
{
  if ( COM_file == NULL )
    COM_file = fopen("SERIAL:", "r+" );
  if ( COM_file == NULL )
    return false;

  /* Set callback routine */
  os_swi1 ( PCIO_COMSetCallback, (int) COM_UpdateStatus );
  COM_Open = true;

  return true;
}

/* ------------------------------------------------- */

static bool ReleaseCOM ( void )
{
  /* De-register callback routine */
  os_swi1 ( PCIO_COMSetCallback, 0 );

  /* Release claim on serial port */
  if ( COM_file != NULL )
    fclose ( COM_file );
  COM_file = NULL;

  COM_Open = false;
  return true;
}


/* Event handling routines ******************************************** */


static bool MUL_IRQpoll ( void )
{
  COM_UpdateStatus (); /* Ensure up-to-date */
  return false;
}

/* MUL_OpenDevices() ------------------------------------------------- */

static bool MUL_OpenDevices()
{
  if ( CFG.DirectParallel && !LPT_Open && ReserveLPT() )
  {
    SYS_registerIO ( LPT_FIRST, LPT_LAST, LPT_Rd8, LPT_Rd16,
                       LPT_Wr8, LPT_Wr16, 0 );
  }

  if ( CFG.DirectSerial && !COM_Open && ReserveCOM() )
  {
    SYS_registerIO ( COM_FIRST, COM_LAST, COM_Rd8, COM_Rd16,
                     COM_Wr8, COM_Wr16, 0 );
  }

  return false;
}

/* MUL_CloseDevices() ---------------------------------------------- */

static bool MUL_CloseDevices()
{

  if ( LPT_Open )
  {
    ReleaseLPT();
    SYS_removeIO ( LPT_FIRST, LPT_LAST );
  }

  if ( COM_Open )
  {
    ReleaseCOM();
    SYS_removeIO ( COM_FIRST, COM_LAST );
  }

  return false;
}

/* SetConfig routines ***************************************************** */

static bool Registered = false;

static bool MUL_SetConfig(void)
{
  /* Check Parallel capabilities ------------------ */

  if ( CFG.DirectParallel )
  {
    if ( !SYS_IntAvailable(LPT_IRQ) )
    {
      SYS_error( false, "cenoparall" );
      CFG.DirectParallel = false;
    }
    else if ( !FindLPTsupport() )
    {
      SYS_error( false, "senopciop" );
      CFG.DirectParallel = false;
    }
  }

  /* Check Serial capabilities ------------------ */

  if ( CFG.DirectSerial )
  {
    if ( !SYS_IntAvailable(COM_IRQ) )
    {
      SYS_error( false, "cenoserial" );
      CFG.DirectSerial = false;
    }
    else if ( !FindCOMsupport() )
    {
      SYS_error( false, "senopcios" );
      CFG.DirectSerial = false;
    }
  }

  if ( ! (CFG.DirectSerial || CFG.DirectParallel) )
    return false;

  /* Register for events ------------------------ */

  if ( !Registered )
  {
    Registered = true;
    SYS_registerEvent ( SYS_Shutdown,   MUL_CloseDevices, 0 );
    SYS_registerEvent ( SYS_PollChain,  MUL_IRQpoll,      0 );
    SYS_registerEvent ( SYS_StartFE,    MUL_OpenDevices,  0 );
    SYS_registerEvent ( SYS_StartWinFE, MUL_OpenDevices,  0 );
    SYS_registerEvent ( SYS_StopWinFE,  MUL_CloseDevices, 0 );
    SYS_registerEvent ( SYS_StopFE,     MUL_CloseDevices, 0 );
  }

  return false;
}

/* Initialisation routines ************************************* */

bool MUL_Init()
{
  Registered = false;

  LPT_file = COM_file = NULL;
  LPT_Open = COM_Open = false;

  /* Register events */
  SYS_registerEvent ( SYS_SetConfig,  MUL_SetConfig, 0 );


  return true;
}

/* ************************************* */



