/*
 *  This file contains the implementation of the function described in irq.h
 */

/*
 *  MPC5xx port sponsored by Defence Research and Development Canada - Suffield
 *  Copyright (C) 2004, Real-Time Systems Inc. (querbach@realtime.bc.ca)
 *
 *  Derived from libbsp/powerpc/mbx8xx/irq/irq.c:
 *
 *  Copyright (C) 1998, 1999 valette@crf.canon.fr
 *
 *  The license and distribution terms for this file may be
 *  found in the file LICENSE in this distribution or at
 *  http://www.rtems.org/license/LICENSE.
 */

#include <rtems.h>
#include <mpc5xx.h>
#include <libcpu/vectors.h>
#include <libcpu/raw_exception.h>
#include <libcpu/irq.h>
#include <bsp/irq.h>

/*
 * Convert an rtems_irq_number constant to an interrupt level
 * suitable for programming into an I/O device's interrupt level field.
 */
int CPU_irq_level_from_symbolic_name(const rtems_irq_number name)
{
  if (CPU_USIU_EXT_IRQ_0 <= name && name <= CPU_USIU_INT_IRQ_7)
    return (name - CPU_USIU_EXT_IRQ_0) / 2;

  if (CPU_UIMB_IRQ_8 <= name && name <= CPU_UIMB_IRQ_31)
    return 8 + (name - CPU_UIMB_IRQ_8);

  return 31;			/* reasonable default */
}

/*
 * default handler connected on each irq after bsp initialization
 */
static rtems_irq_connect_data		default_rtems_entry;

/*
 * location used to store initial tables used for interrupt
 * management.
 */
static rtems_irq_global_settings* 	internal_config;
static rtems_irq_connect_data*		rtems_hdl_tbl;

/*
 * Check if symbolic IRQ name is an USIU IRQ
 */
static inline int is_usiu_irq(const rtems_irq_number irqLine)
{
  return (((int) irqLine <= CPU_USIU_IRQ_MAX_OFFSET) &&
	  ((int) irqLine >= CPU_USIU_IRQ_MIN_OFFSET)
	 );
}

/*
 * Check if symbolic IRQ name is an UIMB IRQ
 */
static inline int is_uimb_irq(const rtems_irq_number irqLine)
{
  return (((int) irqLine <= CPU_UIMB_IRQ_MAX_OFFSET) &&
	  ((int) irqLine >= CPU_UIMB_IRQ_MIN_OFFSET)
	 );
}

/*
 * Check if symbolic IRQ name is a Processor IRQ
 */
static inline int is_proc_irq(const rtems_irq_number irqLine)
{
  return (((int) irqLine <= CPU_PROC_IRQ_MAX_OFFSET) &&
	  ((int) irqLine >= CPU_PROC_IRQ_MIN_OFFSET)
	 );
}


/*
 * Masks used to mask off the interrupts. For exmaple, for ILVL2, the
 * mask is used to mask off interrupts ILVL2, IRQ3, ILVL3, ... IRQ7
 * and ILVL7.
 *
 */
const static unsigned int USIU_IvectMask[CPU_USIU_IRQ_COUNT] =
{
  0,		 		/* external IRQ 0 */
  0xFFFFFFFF << 31, 		/* internal level 0 */
  0xFFFFFFFF << 30, 		/* external IRQ 1 */
  0xFFFFFFFF << 29, 		/* internal level 1 */
  0xFFFFFFFF << 28, 		/* external IRQ 2 */
  0xFFFFFFFF << 27, 		/* internal level 2 */
  0xFFFFFFFF << 26, 		/* external IRQ 3 */
  0xFFFFFFFF << 25, 		/* internal level 3 */
  0xFFFFFFFF << 24, 		/* external IRQ 4 */
  0xFFFFFFFF << 23, 		/* internal level 4 */
  0xFFFFFFFF << 22, 		/* external IRQ 5 */
  0xFFFFFFFF << 21, 		/* internal level 5 */
  0xFFFFFFFF << 20, 		/* external IRQ 6 */
  0xFFFFFFFF << 19, 		/* internal level 6 */
  0xFFFFFFFF << 18, 		/* external IRQ 7 */
  0xFFFFFFFF << 17 		/* internal level 7 */
};


/*
 * ------------------------ RTEMS Irq helper functions ----------------
 */

/*
 * Caution : this function assumes the variable "internal_config"
 * is already set and that the tables it contains are still valid
 * and accessible.
 */
static void compute_USIU_IvectMask_from_prio (void)
{
  /*
   * In theory this is feasible. No time to code it yet. See i386/shared/irq.c
   * for an example based on 8259 controller mask. The actual masks defined
   * correspond to the priorities defined for the USIU in irq_init.c.
   */
}

/*
 * This function check that the value given for the irq line
 * is valid.
 */
static int isValidInterrupt(int irq)
{
  if ( (irq < CPU_MIN_OFFSET) || (irq > CPU_MAX_OFFSET)
        || (irq == CPU_UIMB_INTERRUPT) )
    return 0;
  return 1;
}

static int CPU_irq_enable_at_uimb(const rtems_irq_number irqLine)
{
  if (!is_uimb_irq(irqLine))
    return 1;
  return 0;
}

static int CPU_irq_disable_at_uimb(const rtems_irq_number irqLine)
{
  if (!is_uimb_irq(irqLine))
    return 1;
  return 0;
}

static int CPU_irq_enable_at_usiu(const rtems_irq_number irqLine)
{
  int usiu_irq_index;

  if (!is_usiu_irq(irqLine))
    return 1;

  usiu_irq_index = ((int) (irqLine) - CPU_USIU_IRQ_MIN_OFFSET);
  ppc_cached_irq_mask |= (1 << (31-usiu_irq_index));
  usiu.simask = ppc_cached_irq_mask;

  return 0;
}

static int CPU_irq_disable_at_usiu(const rtems_irq_number irqLine)
{
  int usiu_irq_index;

  if (!is_usiu_irq(irqLine))
    return 1;

  usiu_irq_index = ((int) (irqLine) - CPU_USIU_IRQ_MIN_OFFSET);
  ppc_cached_irq_mask &= ~(1 << (31-usiu_irq_index));
  usiu.simask = ppc_cached_irq_mask;

  return 0;
}

/*
 * --------------- RTEMS Single Irq Handler Mngt Routines ----------------
 */

int CPU_install_rtems_irq_handler	(const rtems_irq_connect_data* irq)
{
    rtems_interrupt_level       level;

    if (!isValidInterrupt(irq->name)) {
      return 0;
    }
    /*
     * Check if default handler is actually connected. If not issue an error.
     * You must first get the current handler via CPU_get_current_idt_entry
     * and then disconnect it using CPU_delete_idt_entry.
     * RATIONALE : to always have the same transition by forcing the user
     * to get the previous handler before accepting to disconnect.
     */
    if (rtems_hdl_tbl[irq->name].hdl != default_rtems_entry.hdl) {
      return 0;
    }

    rtems_interrupt_disable(level);

    /*
     * store the data provided by user
     */
    rtems_hdl_tbl[irq->name] = *irq;

    if (is_uimb_irq(irq->name)) {
      /*
       * Enable interrupt at UIMB level
       */
      CPU_irq_enable_at_uimb (irq->name);
    }

    if (is_usiu_irq(irq->name)) {
      /*
       * Enable interrupt at USIU level
       */
      CPU_irq_enable_at_usiu (irq->name);
    }

    if (is_proc_irq(irq->name)) {
      /*
       * Should Enable exception at processor level but not needed.  Will restore
       * EE flags at the end of the routine anyway.
       */
    }
    /*
     * Enable interrupt on device
     */
	if (irq->on)
    	irq->on(irq);

    rtems_interrupt_enable(level);

    return 1;
}


int CPU_get_current_rtems_irq_handler	(rtems_irq_connect_data* irq)
{
     if (!isValidInterrupt(irq->name)) {
       return 0;
     }
     *irq = rtems_hdl_tbl[irq->name];
     return 1;
}

int CPU_remove_rtems_irq_handler  (const rtems_irq_connect_data* irq)
{
    rtems_interrupt_level       level;

    if (!isValidInterrupt(irq->name)) {
      return 0;
    }
    /*
     * Check if default handler is actually connected. If not issue an error.
     * You must first get the current handler via CPU_get_current_idt_entry
     * and then disconnect it using CPU_delete_idt_entry.
     * RATIONALE : to always have the same transition by forcing the user
     * to get the previous handler before accepting to disconnect.
     */
    if (rtems_hdl_tbl[irq->name].hdl != irq->hdl) {
      return 0;
    }
    rtems_interrupt_disable(level);

    /*
     * Disable interrupt on device
     */
	if (irq->off)
    	irq->off(irq);

    if (is_uimb_irq(irq->name)) {
      /*
       * disable interrupt at UIMB level
       */
      CPU_irq_disable_at_uimb (irq->name);
    }
    if (is_usiu_irq(irq->name)) {
      /*
       * disable interrupt at USIU level
       */
      CPU_irq_disable_at_usiu (irq->name);
    }
    if (is_proc_irq(irq->name)) {
      /*
       * disable exception at processor level
       */
    }

    /*
     * restore the default irq value
     */
    rtems_hdl_tbl[irq->name] = default_rtems_entry;

    rtems_interrupt_enable(level);

    return 1;
}

/*
 * ---------------- RTEMS Global Irq Handler Mngt Routines ----------------
 */

int CPU_rtems_irq_mngt_set	(rtems_irq_global_settings* config)
{
    int                    i;
    rtems_interrupt_level  level;

   /*
    * Store various code accelerators
    */
    internal_config 		= config;
    default_rtems_entry 	= config->defaultEntry;
    rtems_hdl_tbl 		= config->irqHdlTbl;

    rtems_interrupt_disable(level);

    /*
     * Start with UIMB IRQ
     */
    for (i = CPU_UIMB_IRQ_MIN_OFFSET; i <= CPU_UIMB_IRQ_MAX_OFFSET ; i++) {
      if (rtems_hdl_tbl[i].hdl != default_rtems_entry.hdl) {
	CPU_irq_enable_at_uimb (i);
	if (rtems_hdl_tbl[i].on)
		rtems_hdl_tbl[i].on(&rtems_hdl_tbl[i]);
      }
      else {
	if (rtems_hdl_tbl[i].off)
		rtems_hdl_tbl[i].off(&rtems_hdl_tbl[i]);
	CPU_irq_disable_at_uimb (i);
      }
    }

    /*
     * Continue with USIU IRQ
     * Set up internal tables used by rtems interrupt prologue
     */
    compute_USIU_IvectMask_from_prio ();

    for (i = CPU_USIU_IRQ_MIN_OFFSET; i <= CPU_USIU_IRQ_MAX_OFFSET ; i++) {
      if (rtems_hdl_tbl[i].hdl != default_rtems_entry.hdl) {
	CPU_irq_enable_at_usiu (i);
	if (rtems_hdl_tbl[i].on)
		rtems_hdl_tbl[i].on(&rtems_hdl_tbl[i]);
      }
      else {
	if (rtems_hdl_tbl[i].off)
		rtems_hdl_tbl[i].off(&rtems_hdl_tbl[i]);
	CPU_irq_disable_at_usiu (i);
       }
    }

    /*
     * Enable all UIMB interrupt lines, then enable at USIU.
     */
    imb.uimb.umcr |= UIMB_UMCR_IRQMUX(3);
    CPU_irq_enable_at_usiu (CPU_UIMB_INTERRUPT);

    /*
     * finish with Processor exceptions handled like IRQ
     */
    for (i = CPU_PROC_IRQ_MIN_OFFSET; i <= CPU_PROC_IRQ_MAX_OFFSET; i++) {
      if (rtems_hdl_tbl[i].hdl != default_rtems_entry.hdl) {
	if (rtems_hdl_tbl[i].on)
		rtems_hdl_tbl[i].on(&rtems_hdl_tbl[i]);
      }
      else {
	if (rtems_hdl_tbl[i].off)
		rtems_hdl_tbl[i].off(&rtems_hdl_tbl[i]);
      }
    }
    rtems_interrupt_enable(level);
    return 1;
}

int CPU_rtems_irq_mngt_get(rtems_irq_global_settings** config)
{
    *config = internal_config;
    return 0;
}


/*
 * High level IRQ handler called from shared_raw_irq_code_entry
 */
void C_dispatch_irq_handler (MPC5XX_Interrupt_frame *frame, unsigned int excNum)
{
  register unsigned int irq;
  register unsigned uimbIntr;                 /* boolean */
  register unsigned oldMask;		      /* old siu pic masks */
  register unsigned msr;
  register unsigned new_msr;

  /*
   * Handle decrementer interrupt
   */
  if (excNum == ASM_DEC_VECTOR) {
    _CPU_MSR_GET(msr);
    new_msr = msr | MSR_EE;
    _CPU_MSR_SET(new_msr);

    rtems_hdl_tbl[CPU_DECREMENTER].hdl(rtems_hdl_tbl[CPU_DECREMENTER].handle);

    _CPU_MSR_SET(msr);
    return;
  }

  /*
   * Handle external interrupt generated by USIU on PPC core
   */
  while ((ppc_cached_irq_mask & usiu.sipend) != 0) {
    irq = (usiu.sivec >> 26);
    uimbIntr = (irq == CPU_UIMB_INTERRUPT);
    /*
     * Disable the interrupt of the same and lower priority.
     */
    oldMask = ppc_cached_irq_mask;
    ppc_cached_irq_mask = oldMask & USIU_IvectMask[irq];
    usiu.simask = ppc_cached_irq_mask;
    /*
     * Acknowledge current interrupt. This has no effect on internal level
     * interrupts.
     */
    usiu.sipend = (1 << (31 - irq));

    if (uimbIntr)  {
      /*
       * Look at the bits set in the UIMB interrupt-pending register.  The
       * highest-order set bit indicates the handler we will run.
       *
       * Unfortunately, we can't easily mask individual UIMB interrupts
       * unless they use USIU levels 0 to 6, so we must mask all low-level
       * (level > 7) UIMB interrupts while we service any interrupt.
       */
      int uipend = imb.uimb.uipend << 8;

      if (uipend == 0) {	/* spurious interrupt?  use last vector */
        irq = CPU_UIMB_IRQ_MAX_OFFSET;
      }
      else {
        irq = CPU_UIMB_IRQ_MIN_OFFSET;
        for ( ; (uipend & 0x8000000) == 0; uipend <<= 1) {
          irq++;
        }
      }
    }
    _CPU_MSR_GET(msr);
    new_msr = msr | MSR_EE;
    _CPU_MSR_SET(new_msr);

    rtems_hdl_tbl[irq].hdl(rtems_hdl_tbl[irq].handle);

    _CPU_MSR_SET(msr);

    ppc_cached_irq_mask = oldMask;
    usiu.simask = ppc_cached_irq_mask;
  }
}
