/**
 * @file
 *
 * @brief Console ESCI implementation.
 */

/*
 * Copyright (c) 2008-2012 embedded brains GmbH.  All rights reserved.
 *
 *  embedded brains GmbH
 *  Obere Lagerstr. 30
 *  82178 Puchheim
 *  Germany
 *  <rtems@embedded-brains.de>
 *
 * 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 <bsp/console-esci.h>

#include <bsp.h>
#include <bsp/fatal.h>
#include <bsp/irq.h>

#ifdef MPC55XX_HAS_ESCI

mpc55xx_esci_context mpc55xx_esci_devices [] = {
  {
    .regs = &ESCI_A,
    .irq = MPC55XX_IRQ_ESCI(0)
  }
  #ifdef ESCI_B
    , {
      .regs = &ESCI_B,
      .irq = MPC55XX_IRQ_ESCI(1)
    }
  #endif
  #ifdef ESCI_C
    , {
      .regs = &ESCI_C,
      .irq = MPC55XX_IRQ_ESCI(2)
    }
  #endif
  #ifdef ESCI_D
    , {
      .regs = &ESCI_D,
      .irq = MPC55XX_IRQ_ESCI(3)
    }
  #endif
};

static void mpc55xx_esci_poll_write(int minor, char c)
{
  mpc55xx_esci_context *self = console_generic_get_context(minor);
  const union ESCI_SR_tag clear_tdre = { .B = { .TDRE = 1 } };
  volatile struct ESCI_tag *regs = self->regs;
  rtems_interrupt_level level;
  bool done = false;
  bool wait_for_transmit_done = false;

  rtems_interrupt_disable(level);
  if (self->transmit_nest_level == 0) {
    union ESCI_CR1_tag cr1 = { .R = regs->CR1.R };

    if (cr1.B.TIE != 0) {
      cr1.B.TIE = 0;
      regs->CR1.R = cr1.R;
      wait_for_transmit_done = !self->transmit_in_progress;
      self->transmit_nest_level = 1;
    }
  } else {
    ++self->transmit_nest_level;
  }
  rtems_interrupt_enable(level);

  while (!done) {
    rtems_interrupt_disable(level);
    bool tx = self->transmit_in_progress;
    if (!tx || (tx && regs->SR.B.TDRE)) {
      regs->SR.R = clear_tdre.R;
      regs->DR.B.D = c;
      self->transmit_in_progress = true;
      done = true;
    }
    rtems_interrupt_enable(level);
  }

  done = false;
  while (!done) {
    rtems_interrupt_disable(level);
    if (wait_for_transmit_done) {
      if (regs->SR.B.TDRE) {
        regs->SR.R = clear_tdre.R;
        self->transmit_in_progress = false;
        done = true;
      }
    } else {
      done = true;
    }

    if (done && self->transmit_nest_level > 0) {
      --self->transmit_nest_level;

      if (self->transmit_nest_level == 0) {
        union ESCI_CR1_tag cr1 = { .R = regs->CR1.R };

        cr1.B.TIE = 1;
        regs->CR1.R = cr1.R;
      }
    }
    rtems_interrupt_enable(level);
  }
}

static inline void mpc55xx_esci_interrupts_clear_and_enable(
  mpc55xx_esci_context *self
)
{
  volatile struct ESCI_tag *regs = self->regs;
  union ESCI_CR1_tag cr1 = MPC55XX_ZERO_FLAGS;
  rtems_interrupt_level level;

  rtems_interrupt_disable(level);
  cr1.R = regs->CR1.R;
  cr1.B.RIE = 1;
  cr1.B.TIE = 1;
  regs->CR1.R = cr1.R;
  regs->SR.R = regs->SR.R;
  rtems_interrupt_enable(level);
}

static inline void mpc55xx_esci_interrupts_disable(mpc55xx_esci_context *self)
{
  volatile struct ESCI_tag *regs = self->regs;
  union ESCI_CR1_tag cr1 = MPC55XX_ZERO_FLAGS;
  rtems_interrupt_level level;

  rtems_interrupt_disable(level);
  cr1.R = regs->CR1.R;
  cr1.B.RIE = 0;
  cr1.B.TIE = 0;
  regs->CR1.R = cr1.R;
  rtems_interrupt_enable(level);
}

static void mpc55xx_esci_interrupt_handler(void *arg)
{
  mpc55xx_esci_context *self = arg;
  volatile struct ESCI_tag *regs = self->regs;
  union ESCI_SR_tag sr = MPC55XX_ZERO_FLAGS;
  union ESCI_SR_tag active = MPC55XX_ZERO_FLAGS;
  rtems_interrupt_level level;

  /* Status */
  sr.R = regs->SR.R;

  /* Receive data register full? */
  if (sr.B.RDRF != 0) {
    active.B.RDRF = 1;
  }

  /* Transmit data register empty? */
  if (sr.B.TDRE != 0) {
    active.B.TDRE = 1;
  }

  /* Clear flags */
  rtems_interrupt_disable(level);
  regs->SR.R = active.R;
  self->transmit_in_progress = false;
  rtems_interrupt_enable(level);

  /* Enqueue */
  if (active.B.RDRF != 0) {
    char c = regs->DR.B.D;
    rtems_termios_enqueue_raw_characters(self->tty, &c, 1);
  }

  /* Dequeue */
  if (active.B.TDRE != 0) {
    rtems_termios_dequeue_characters(self->tty, 1);
  }
}

static int mpc55xx_esci_set_attributes(int minor, const struct termios *t)
{
  mpc55xx_esci_context *self = console_generic_get_context(minor);
  volatile struct ESCI_tag *regs = self->regs;
  union ESCI_CR1_tag cr1 = { .R = regs->CR1.R };
  union ESCI_CR2_tag cr2 = MPC55XX_ZERO_FLAGS;
  rtems_termios_baud_t br = rtems_termios_baud_to_number(t->c_ospeed);

  /* Enable module */
  cr2.B.MDIS = 0;

  /* Interrupts */
  cr1.B.TCIE = 0;
  cr1.B.ILIE = 0;
  cr2.B.IEBERR = 0;
  cr2.B.ORIE = 0;
  cr2.B.NFIE = 0;
  cr2.B.FEIE = 0;
  cr2.B.PFIE = 0;

  /* Disable receiver wake-up standby */
  cr1.B.RWU = 0;

  /* Disable DMA channels */
  cr2.B.RXDMA = 0;
  cr2.B.TXDMA = 0;

  /* Idle line type */
  cr1.B.ILT = 0;

  /* Disable loops */
  cr1.B.LOOPS = 0;

  /* Enable or disable receiver */
  cr1.B.RE = (t->c_cflag & CREAD) ? 1 : 0;

  /* Enable transmitter */
  cr1.B.TE = 1;

  /* Baud rate */
  if (br > 0) {
    br = bsp_clock_speed / (16 * br);
    br = (br > 8191) ? 8191 : br;
  } else {
    br = 0;
  }
  cr1.B.SBR = br;

  /* Number of data bits */
  if ((t->c_cflag & CSIZE) != CS8) {
    return -1;
  }
  cr1.B.M = 0;

  /* Parity */
  cr1.B.PE = (t->c_cflag & PARENB) ? 1 : 0;
  cr1.B.PT = (t->c_cflag & PARODD) ? 1 : 0;

  /* Stop bits */
  if (t->c_cflag & CSTOPB ) {
    /* Two stop bits */
    return -1;
  }

  /* Disable LIN */
  regs->LCR.R = 0;

  /* Set control registers */
  regs->CR2.R = cr2.R;
  regs->CR1.R = cr1.R;

  return 0;
}

static int mpc55xx_esci_first_open(int major, int minor, void *arg)
{
  rtems_status_code sc = RTEMS_SUCCESSFUL;
  int rv = 0;
  mpc55xx_esci_context *self = console_generic_get_context(minor);
  struct rtems_termios_tty *tty = console_generic_get_tty_at_open(arg);

  self->tty = tty;

  rv = rtems_termios_set_initial_baud(tty, BSP_DEFAULT_BAUD_RATE);
  if (rv != 0) {
    bsp_fatal(MPC55XX_FATAL_CONSOLE_ESCI_BAUD);
  }

  rv = mpc55xx_esci_set_attributes(minor, &tty->termios);
  if (rv != 0) {
    bsp_fatal(MPC55XX_FATAL_CONSOLE_ESCI_ATTRIBUTES);
  }

  sc = mpc55xx_interrupt_handler_install(
    self->irq,
    "eSCI",
    RTEMS_INTERRUPT_UNIQUE,
    MPC55XX_INTC_DEFAULT_PRIORITY,
    mpc55xx_esci_interrupt_handler,
    self
  );
  if (sc != RTEMS_SUCCESSFUL) {
    bsp_fatal(MPC55XX_FATAL_CONSOLE_ESCI_IRQ_INSTALL);
  }

  mpc55xx_esci_interrupts_clear_and_enable(self);
  self->transmit_in_progress = false;

  return 0;
}

static int mpc55xx_esci_last_close(int major, int minor, void* arg)
{
  mpc55xx_esci_context *self = console_generic_get_context(minor);

  mpc55xx_esci_interrupts_disable(self);
  self->tty = NULL;

  return 0;
}

static int mpc55xx_esci_poll_read(int minor)
{
  mpc55xx_esci_context *self = console_generic_get_context(minor);
  volatile struct ESCI_tag *regs = self->regs;
  union ESCI_SR_tag sr = MPC55XX_ZERO_FLAGS;
  rtems_interrupt_level level;
  int c = -1;

  rtems_interrupt_disable(level);
  if (regs->SR.B.RDRF != 0) {
    /* Clear flag */
    sr.B.RDRF = 1;
    regs->SR.R = sr.R;

    /* Read */
    c = regs->DR.B.D;
  }
  rtems_interrupt_enable(level);

  return c;
}

static int mpc55xx_esci_write(int minor, const char *out, size_t n)
{
  if (n > 0) {
    mpc55xx_esci_context *self = console_generic_get_context(minor);

    self->regs->DR.B.D = out [0];
    self->transmit_in_progress = true;
  }

  return 0;
}

const console_generic_callbacks mpc55xx_esci_callbacks = {
  .termios_callbacks = {
    .firstOpen = mpc55xx_esci_first_open,
    .lastClose = mpc55xx_esci_last_close,
    .write = mpc55xx_esci_write,
    .setAttributes = mpc55xx_esci_set_attributes,
    .outputUsesInterrupts = TERMIOS_IRQ_DRIVEN
  },
  .poll_read = mpc55xx_esci_poll_read,
  .poll_write = mpc55xx_esci_poll_write
};

#endif /* MPC55XX_HAS_ESCI */
