/*
 * RTEMS driver for WD800x
 *
 *  Based on the 68360 Network Driver by:
 *    W. Eric Norum
 *    Saskatchewan Accelerator Laboratory
 *    University of Saskatchewan
 *    Saskatoon, Saskatchewan, CANADA
 *    eric@skatter.usask.ca
 */

#include <machine/rtems-bsd-kernel-space.h>

#include <bsp.h>
#include <libchip/wd80x3.h>

#include <stdio.h>
#include <stdarg.h>
#include <string.h>	/* memcpy, memset */
#include <errno.h>
#include <rtems/error.h>
#include <rtems/rtems_bsdnet.h>
#include <assert.h>

#include <sys/param.h>
#include <sys/mbuf.h>
#include <sys/socket.h>
#include <sys/sockio.h>

#include <net/if.h>

#include <netinet/in.h>
#include <netinet/if_ether.h>

#include <bsp/irq.h>

#define	ET_MINLEN 60		/* minimum message length */

/*
 * Number of WDs supported by this driver
 */
#define NWDDRIVER	1

/*
 * Default number of buffer descriptors set aside for this driver.
 * The number of transmit buffer descriptors has to be quite large
 * since a single frame often uses four or more buffer descriptors.
 */
#define RX_BUF_COUNT     15
#define TX_BUF_COUNT     4
#define TX_BD_PER_BUF    4

/*
 * RTEMS event used by interrupt handler to signal driver tasks.
 * This must not be any of the events used by the network task synchronization.
 */
#define INTERRUPT_EVENT	RTEMS_EVENT_1

/*
 * RTEMS event used to start transmit daemon.
 * This must not be the same as INTERRUPT_EVENT.
 */
#define START_TRANSMIT_EVENT	RTEMS_EVENT_2

/*
 * Receive buffer size -- Allow for a full ethernet packet including CRC
 */
#define RBUF_SIZE	1520

#if (MCLBYTES < RBUF_SIZE)
# error "Driver must have MCLBYTES > RBUF_SIZE"
#endif

/*
 * Per-device data
 */
struct wd_softc {
  struct arpcom			arpcom;
  struct mbuf			**rxMbuf;
  struct mbuf			**txMbuf;
  int				acceptBroadcast;
  int				rxBdCount;
  int				txBdCount;
  int				txBdHead;
  int				txBdTail;
  int				txBdActiveCount;
  rtems_id			rxDaemonTid;
  rtems_id			txDaemonTid;
  rtems_vector_number name;

  unsigned int 			port;
  unsigned char			*base;
  unsigned long			bpar;

  /*
   * Statistics
   */
  unsigned long	rxInterrupts;
  unsigned long	rxNotFirst;
  unsigned long	rxNotLast;
  unsigned long	rxGiant;
  unsigned long	rxNonOctet;
  unsigned long	rxRunt;
  unsigned long	rxBadCRC;
  unsigned long	rxOverrun;
  unsigned long	rxCollision;

  unsigned long	txInterrupts;
  unsigned long	txDeferred;
  unsigned long	txHeartbeat;
  unsigned long	txLateCollision;
  unsigned long	txRetryLimit;
  unsigned long	txUnderrun;
  unsigned long	txLostCarrier;
  unsigned long	txRawWait;
};

#define RO 0x10

#define SHATOT (8*1024)		/* size of shared memory */
#define SHAPAGE 256		/* shared memory information */
#define MAXSIZ 	1536		/*(MAXBUF - MESSH_SZ)*/
#define OUTPAGE ((SHATOT-(MAXSIZ+SHAPAGE-1))/SHAPAGE)

static volatile unsigned long overrun;
static volatile unsigned long resend;
static struct wd_softc wd_softc[NWDDRIVER];

/*
 * WD interrupt handler
 */
static void
wd8003Enet_interrupt_handler (void * unused)
{
  unsigned int tport;
  unsigned char status, status2;

  tport = wd_softc[0].port ;

  /*
   * Read status
   */
  inport_byte(tport+ISR, status);
  outport_byte(tport+IMR, 0x00);

  /*
   * Ring overwrite
   */

  if (status & MSK_OVW){
    outport_byte(tport+CMDR, MSK_STP + MSK_RD2);	/* stop 8390 */
    Wait_X_ms(2);
    outport_byte(tport+RBCR0, 0);			/* clear byte count */
    outport_byte(tport+RBCR1, 0);
    inport_byte(tport+ISR, status2);
    status |= (status2 & (MSK_PTX+MSK_TXE)) ;	/* TX status */
    outport_byte(tport+TCR, MSK_LOOP);		/* loopback mode */
    outport_byte(tport+CMDR, MSK_STA + MSK_RD2);	/* start */
    overrun = 1 ;
    if ((status & (MSK_PTX+MSK_TXE)) == 0)
	resend = 1;
  }

  /*
   * Frame received?
   */
  if (status & (MSK_PRX+MSK_RXE)) {
    outport_byte(tport+ISR, status & (MSK_PRX+MSK_RXE));
    wd_softc[0].rxInterrupts++;
    rtems_bsdnet_event_send (wd_softc[0].rxDaemonTid, INTERRUPT_EVENT);
  }

}

/*
 * Initialize the ethernet hardware
 */
static void
wd8003Enet_initialize_hardware (struct wd_softc *sc)
{
  int  i1, ultra;
  char cc1, cc2;
  unsigned char  temp;
  rtems_status_code status;
  unsigned int tport;
  unsigned char *hwaddr;

  tport = sc->port;

  /* address from board ROM */
  inport_byte(tport+0x04, temp);
  outport_byte(tport+0x04, temp & 0x7f);

  hwaddr = sc->arpcom.ac_enaddr;
  for (i1=cc2=0; i1<8; i1++) {
    inport_byte(tport + ADDROM + i1, cc1);
    cc2 += cc1;
    if (i1 < 6)
      hwaddr[i1] = cc1;
  }

  inport_byte(tport+0x04, temp);
  outport_byte(tport+0x04, temp | 0x80);	/* alternate registers */
  outport_byte(tport+W83CREG, MSK_RESET);	/* reset board, set buffer */
  outport_byte(tport+W83CREG, 0);
  outport_byte(tport+W83CREG, MSK_ENASH + (int)((sc->bpar>>13)&0x3f));

  outport_byte(tport+CMDR, MSK_PG0 + MSK_RD2);
  cc1 = MSK_BMS + MSK_FT10; /* configure 8 or 16 bits */

  inport_byte(tport+0x07, temp) ;

  ultra = ((temp & 0xf0) == 0x20 || (temp & 0xf0) == 0x40);
  if (ultra)
    cc1 = MSK_WTS + MSK_BMS + MSK_FT10;
  outport_byte(tport+DCR, cc1);
  outport_byte(tport+RBCR0, 0);
  outport_byte(tport+RBCR1, 0);
  outport_byte(tport+RCR, MSK_MON);	       	/* disable the rxer */
  outport_byte(tport+TCR, 0);			/* normal operation */
  outport_byte(tport+PSTOP, OUTPAGE);		/* init PSTOP */
  outport_byte(tport+PSTART, 0);	       	/* init PSTART */
  outport_byte(tport+BNRY, -1);			/* init BNRY */
  outport_byte(tport+ISR, -1);			/* clear IR's */
  outport_byte(tport+IMR, 0x15);	       	/* enable interrupt */

  outport_byte(tport+CMDR, MSK_PG1 + MSK_RD2);

  for (i1=0; i1<6; i1++)			/* initial physical addr */
    outport_byte(tport+PAR+i1, hwaddr[i1]);

  for (i1=0; i1<MARsize; i1++)			/* clear multicast */
    outport_byte(tport+MAR+i1, 0);
  outport_byte(tport+CURR, 0);			/* init current packet */

  outport_byte(tport+CMDR, MSK_PG0 + MSK_RD2);
  outport_byte(tport+CMDR, MSK_STA + MSK_RD2);	/* put 8390 on line */
  outport_byte(tport+RCR, MSK_AB);		/* MSK_AB accept broadcast */

  if (ultra) {
    inport_byte(tport+0x0c, temp);
    outport_byte(tport+0x0c, temp | 0x80);
    outport_byte(tport+0x05, 0x80);
    outport_byte(tport+0x06, 0x01);
  }

  /*
   * Set up interrupts
   */

  status = rtems_interrupt_handler_install(
    sc->name,
    "wd8003",
    RTEMS_INTERRUPT_UNIQUE,
    wd8003Enet_interrupt_handler,
    NULL
  );
  assert(status == RTEMS_SUCCESSFUL);
}

static void
wd_rxDaemon (void *arg)
{
  unsigned int tport;
  struct ether_header *eh;
  struct wd_softc *dp = (struct wd_softc *)&wd_softc[0];
  struct ifnet *ifp = &dp->arpcom.ac_if;
  struct mbuf *m;
  unsigned int i2;
  unsigned int len;
  volatile unsigned char start, next, current;
  unsigned char *shp, *temp;
  unsigned short *real_short_ptr;
  rtems_event_set events;

  tport = wd_softc[0].port ;

  for (;;){

    rtems_bsdnet_event_receive (INTERRUPT_EVENT,
				RTEMS_WAIT|RTEMS_EVENT_ANY,
				RTEMS_NO_TIMEOUT,
				&events);

    for (;;){
      inport_byte(tport+BNRY, start);

      outport_byte(tport+CMDR, MSK_PG1 + MSK_RD2);
      inport_byte(tport+CURR, current);
      outport_byte(tport+CMDR, MSK_PG0 + MSK_RD2);

      start += 1;
      if (start >= OUTPAGE){
	start = 0;
      }

      if (current == start)
	break;

      /* real_short_ptr avoids cast on lvalue which gcc no longer allows */
      shp = dp->base + 1 + (SHAPAGE * start);
      next = *shp++;
      real_short_ptr = (unsigned short *)shp;
      len = *(real_short_ptr)++ - 4;

      if (next >= OUTPAGE){
	next = 0;
      }

      MGETHDR (m, M_WAIT, MT_DATA);
      MCLGET (m, M_WAIT);
      m->m_pkthdr.rcvif = ifp;

      temp = (unsigned char *) m->m_data;
      m->m_len = m->m_pkthdr.len = len - sizeof(struct ether_header);

      if ((i2 = (OUTPAGE - start) * SHAPAGE - 4) < len){
	memcpy(temp, shp, i2);
	len -= i2;
	temp += i2;
	shp = dp->base;
      }
      memcpy(temp, shp, len);

      eh = mtod (m, struct ether_header *);
      m->m_data += sizeof(struct ether_header);
      ether_input (ifp, eh, m);

      outport_byte(tport+BNRY, next-1);
    }

  /*
   * Ring overwrite
   */
    if (overrun){
      outport_byte(tport+ISR, MSK_OVW);		/* reset IR */
      outport_byte(tport+TCR, 0);		/* out of loopback */
      if (resend  == 1)
	outport_byte(tport+CMDR, MSK_TXP + MSK_RD2);	/* resend */
      resend = 0;
      overrun = 0;
    }

    outport_byte(tport+IMR, 0x15);  /* re-enable IT rx */
  }
}

static void
sendpacket (struct ifnet *ifp, struct mbuf *m)
{
	struct wd_softc *dp = ifp->if_softc;
	struct mbuf *n;
	unsigned int len, tport;
	uint8_t *shp, txReady;

	tport = dp->port;

  /*
   * Waiting for Transmitter ready
   */
  inport_byte(tport+CMDR, txReady);
  while(txReady & MSK_TXP)
    inport_byte(tport+CMDR, txReady);

  len = 0;
  shp = dp->base + (SHAPAGE * OUTPAGE);

  n = m;

  for (;;){
    len += m->m_len;
    memcpy(shp, (char *)m->m_data, m->m_len);
    shp += m->m_len ;
    if ((m = m->m_next) == NULL)
      break;
  }

  m_freem(n);

  if (len < ET_MINLEN) len = ET_MINLEN;
  outport_byte(tport+TBCR0, len);
  outport_byte(tport+TBCR1, (len >> 8) );
  outport_byte(tport+TPSR, OUTPAGE);
  outport_byte(tport+CMDR, MSK_TXP + MSK_RD2);
}

/*
 * Driver transmit daemon
 */
static void
wd_txDaemon (void *arg)
{
	struct wd_softc *sc = (struct wd_softc *)arg;
	struct ifnet *ifp = &sc->arpcom.ac_if;
	struct mbuf *m;
	rtems_event_set events;

	for (;;) {
		/*
		 * Wait for packet
		 */
		rtems_bsdnet_event_receive (START_TRANSMIT_EVENT, RTEMS_EVENT_ANY | RTEMS_WAIT, RTEMS_NO_TIMEOUT, &events);

		/*
		 * Send packets till queue is empty
		 */
		for (;;) {
			/*
			 * Get the next mbuf chain to transmit.
			 */
			IF_DEQUEUE(&ifp->if_snd, m);
			if (!m)
				break;
			sendpacket (ifp, m);
		}
		ifp->if_flags &= ~IFF_OACTIVE;
	}
}

/*
 * Send packet (caller provides header).
 */
static void
wd_start (struct ifnet *ifp)
{
	struct wd_softc *sc = ifp->if_softc;

	rtems_bsdnet_event_send (sc->txDaemonTid, START_TRANSMIT_EVENT);
	ifp->if_flags |= IFF_OACTIVE;
}

/*
 * Initialize and start the device
 */
static void
wd_init (void *arg)
{
  struct wd_softc *sc = arg;
  struct ifnet *ifp = &sc->arpcom.ac_if;

  if (sc->txDaemonTid == 0) {

    /*
     * Set up WD hardware
     */
    wd8003Enet_initialize_hardware (sc);

    /*
     * Start driver tasks
     */
    sc->txDaemonTid = rtems_bsdnet_newproc ("SCtx", 4096, wd_txDaemon, sc);
    sc->rxDaemonTid = rtems_bsdnet_newproc ("SCrx", 4096, wd_rxDaemon, sc);
  }

  /*
   * Tell the world that we're running.
   */
  ifp->if_flags |= IFF_RUNNING;

}

/*
 * Stop the device
 */
static void
wd_stop (struct wd_softc *sc)
{
  unsigned int tport;
  unsigned char temp;
  struct ifnet *ifp = &sc->arpcom.ac_if;

  ifp->if_flags &= ~IFF_RUNNING;

  /*
   * Stop the transmitter
   */
  tport=wd_softc[0].port ;
  inport_byte(tport+0x04,temp);
  outport_byte(tport+0x04, temp & 0x7f);
  outport_byte(tport + CMDR, MSK_STP + MSK_RD2);

}

/*
 * Show interface statistics
 */
static void
wd_stats (struct wd_softc *sc)
{
	printf ("      Rx Interrupts:%-8lu", sc->rxInterrupts);
	printf ("       Not First:%-8lu", sc->rxNotFirst);
	printf ("        Not Last:%-8lu\n", sc->rxNotLast);
	printf ("              Giant:%-8lu", sc->rxGiant);
	printf ("            Runt:%-8lu", sc->rxRunt);
	printf ("       Non-octet:%-8lu\n", sc->rxNonOctet);
	printf ("            Bad CRC:%-8lu", sc->rxBadCRC);
	printf ("         Overrun:%-8lu", sc->rxOverrun);
	printf ("       Collision:%-8lu\n", sc->rxCollision);

	printf ("      Tx Interrupts:%-8lu", sc->txInterrupts);
	printf ("        Deferred:%-8lu", sc->txDeferred);
	printf (" Missed Hearbeat:%-8lu\n", sc->txHeartbeat);
	printf ("         No Carrier:%-8lu", sc->txLostCarrier);
	printf ("Retransmit Limit:%-8lu", sc->txRetryLimit);
	printf ("  Late Collision:%-8lu\n", sc->txLateCollision);
	printf ("           Underrun:%-8lu", sc->txUnderrun);
	printf (" Raw output wait:%-8lu\n", sc->txRawWait);
}

/*
 * Driver ioctl handler
 */
static int
wd_ioctl (struct ifnet *ifp, ioctl_command_t command, caddr_t data)
{
	struct wd_softc *sc = ifp->if_softc;
	int error = 0;

	switch (command) {
	case SIOCGIFADDR:
	case SIOCSIFADDR:
		ether_ioctl (ifp, command, data);
		break;

	case SIOCSIFFLAGS:
		switch (ifp->if_flags & (IFF_UP | IFF_RUNNING)) {
		case IFF_RUNNING:
			wd_stop (sc);
			break;

		case IFF_UP:
			wd_init (sc);
			break;

		case IFF_UP | IFF_RUNNING:
			wd_stop (sc);
			wd_init (sc);
			break;

		default:
			break;
		}
		break;

	case SIO_RTEMS_SHOW_STATS:
		wd_stats (sc);
		break;

	/*
	 * FIXME: All sorts of multicast commands need to be added here!
	 */
	default:
		error = EINVAL;
		break;
	}
	return error;
}

/*
 * Attach an WD driver to the system
 */
int
rtems_wd_driver_attach (struct rtems_bsdnet_ifconfig *config, int attach)
{
	struct wd_softc *sc;
	struct ifnet *ifp;
	int mtu;
	int i;

	/*
	 * Find a free driver
	 */
	for (i = 0 ; i < NWDDRIVER ; i++) {
		sc = &wd_softc[i];
		ifp = &sc->arpcom.ac_if;
		if (ifp->if_softc == NULL)
			break;
	}
	if (i >= NWDDRIVER) {
		printf ("Too many WD drivers.\n");
		return 0;
	}

	/*
	 * Process options
	 */
	if (config->hardware_address) {
	  memcpy (sc->arpcom.ac_enaddr, config->hardware_address,
		  ETHER_ADDR_LEN);
	}
	else {
	  memset (sc->arpcom.ac_enaddr, 0x08,ETHER_ADDR_LEN);
	}
	if (config->mtu)
		mtu = config->mtu;
	else
		mtu = ETHERMTU;

	if (config->irno)
		sc->name = config->irno;
	else
		sc->name = 5;

	if (config->port)
		sc->port = config->port;
	else
		sc->port = 0x240;

	if (config->bpar) {
		sc->bpar = config->bpar;
		sc->base = (unsigned char*) config->bpar;
	}
	else {
		sc->bpar = 0xD0000;
		sc->base = (unsigned char*) 0xD0000;
	}

	sc->acceptBroadcast = !config->ignore_broadcast;

	/*
	 * Set up network interface values
	 */
	ifp->if_softc = sc;
	ifp->if_unit = i + 1;
	ifp->if_name = "wd";
	ifp->if_mtu = mtu;
	ifp->if_init = wd_init;
	ifp->if_ioctl = wd_ioctl;
	ifp->if_start = wd_start;
	ifp->if_output = ether_output;
	ifp->if_flags = IFF_BROADCAST | IFF_SIMPLEX;
	if (ifp->if_snd.ifq_maxlen == 0)
		ifp->if_snd.ifq_maxlen = ifqmaxlen;

	/*
	 * init some variables
	 */
	overrun = 0;
	resend = 0;

 	/*
	 * Attach the interface
	 */
	if_attach (ifp);
	ether_ifattach (ifp);
	return 1;
};
