/*  video.c
 *
 *  Milkymist video input driver for RTEMS
 *
 *  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.
 *
 *  COPYRIGHT (c) 2010, 2011 Sebastien Bourdeauducq
 */

#define RTEMS_STATUS_CHECKS_USE_PRINTK

#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <rtems.h>
#include <bsp.h>
#include <bsp/irq-generic.h>
#include <rtems/libio.h>
#include <rtems/status-checks.h>
#include <rtems/bspIo.h>
#include "../include/system_conf.h"
#include <bsp/milkymist_video.h>

#define DEVICE_NAME "/dev/video"
#define N_BUFFERS 3
#define FRAME_W 720
#define FRAME_H 288

static bool buffers_locked[N_BUFFERS];
static void *buffers[N_BUFFERS];
static int last_buffer;
static int current_buffer;

static rtems_isr frame_handler(rtems_vector_number n)
{
  int remaining_attempts;

  lm32_interrupt_ack(1 << MM_IRQ_VIDEOIN);
  
  last_buffer = current_buffer;
  
  /* get a new buffer */
  remaining_attempts = N_BUFFERS;
  do {
    current_buffer++;
    if(current_buffer == N_BUFFERS)
      current_buffer = 0;
    remaining_attempts--;
  } while(buffers_locked[current_buffer] && (remaining_attempts > 0));

  MM_WRITE(MM_BT656_BASE, (unsigned int)buffers[current_buffer]);
  
  if(buffers_locked[current_buffer])
    printk("Failed to find unlocked buffer\n");
}

static void i2c_delay(void)
{
  unsigned int i;

  for(i=0;i<1000;i++) __asm__("nop");
}

/* I2C bit-banging functions from http://en.wikipedia.org/wiki/I2c */
static unsigned int i2c_read_bit(void)
{
  unsigned int bit;

  /* Let the slave drive data */
  MM_WRITE(MM_BT656_I2C, 0);
  i2c_delay();
  MM_WRITE(MM_BT656_I2C, BT656_I2C_SDC);
  i2c_delay();
  bit = MM_READ(MM_BT656_I2C) & BT656_I2C_SDAIN;
  i2c_delay();
  MM_WRITE(MM_BT656_I2C, 0);
  return bit;
}

static void i2c_write_bit(unsigned int bit)
{
  if(bit) {
    MM_WRITE(MM_BT656_I2C, BT656_I2C_SDAOE|BT656_I2C_SDAOUT);
  } else {
    MM_WRITE(MM_BT656_I2C, BT656_I2C_SDAOE);
  }
  i2c_delay();
  MM_WRITE(MM_BT656_I2C, MM_READ(MM_BT656_I2C) | BT656_I2C_SDC);
  i2c_delay();
  MM_WRITE(MM_BT656_I2C, MM_READ(MM_BT656_I2C) & ~BT656_I2C_SDC);
}

static int i2c_started;

static void i2c_start_cond(void)
{
  if(i2c_started) {
    /* set SDA to 1 */
    MM_WRITE(MM_BT656_I2C, BT656_I2C_SDAOE|BT656_I2C_SDAOUT);
    i2c_delay();
    MM_WRITE(MM_BT656_I2C, MM_READ(MM_BT656_I2C) | BT656_I2C_SDC);
  }
  /* SCL is high, set SDA from 1 to 0 */
  MM_WRITE(MM_BT656_I2C, BT656_I2C_SDAOE|BT656_I2C_SDC);
  i2c_delay();
  MM_WRITE(MM_BT656_I2C, BT656_I2C_SDAOE);
  i2c_started = 1;
}

static void i2c_stop_cond(void)
{
  /* set SDA to 0 */
  MM_WRITE(MM_BT656_I2C, BT656_I2C_SDAOE);
  i2c_delay();
  /* Clock stretching */
  MM_WRITE(MM_BT656_I2C, BT656_I2C_SDAOE|BT656_I2C_SDC);
  /* SCL is high, set SDA from 0 to 1 */
  MM_WRITE(MM_BT656_I2C, BT656_I2C_SDC);
  i2c_delay();
  i2c_started = 0;
}

static unsigned int i2c_write(unsigned char byte)
{
  unsigned int bit;
  unsigned int ack;

  for(bit = 0; bit < 8; bit++) {
    i2c_write_bit(byte & 0x80);
    byte <<= 1;
  }
  ack = !i2c_read_bit();
  return ack;
}

static unsigned char i2c_read(int ack)
{
  unsigned char byte = 0;
  unsigned int bit;

  for(bit = 0; bit < 8; bit++) {
    byte <<= 1;
    byte |= i2c_read_bit();
  }
  i2c_write_bit(!ack);
  return byte;
}

static unsigned char read_reg(unsigned char addr)
{
  unsigned char r;

  i2c_start_cond();
  i2c_write(0x40);
  i2c_write(addr);
  i2c_start_cond();
  i2c_write(0x41);
  r = i2c_read(0);
  i2c_stop_cond();

  return r;
}

static void write_reg(unsigned char addr, unsigned char val)
{
  i2c_start_cond();
  i2c_write(0x40);
  i2c_write(addr);
  i2c_write(val);
  i2c_stop_cond();
}

static const char vreg_addr[] = {
  0x1d, 0xc3, 0xc4
};

static const char vreg_dat[] = {
  0x40, 0x05, 0x80
};

rtems_device_driver video_initialize(
  rtems_device_major_number major,
  rtems_device_minor_number minor,
  void *arg
)
{
  rtems_status_code sc;
  rtems_isr_entry dummy;
  int i;
  
  MM_WRITE(MM_BT656_I2C, BT656_I2C_SDC);

  sc = rtems_io_register_name(DEVICE_NAME, major, 0);
  RTEMS_CHECK_SC(sc, "create video input device");

  rtems_interrupt_catch(frame_handler, MM_IRQ_VIDEOIN, &dummy);
  bsp_interrupt_vector_enable(MM_IRQ_VIDEOIN);
  
  for(i=0;i<sizeof(vreg_addr);i++)
    write_reg(vreg_addr[i], vreg_dat[i]);

  return RTEMS_SUCCESSFUL;
}

rtems_device_driver video_open(
  rtems_device_major_number major,
  rtems_device_minor_number minor,
  void *arg
)
{
  int i;
  int status;

  for(i=0;i<N_BUFFERS;i++) {
    status = posix_memalign(&buffers[i], 32, 2*FRAME_W*FRAME_H);
    if(status != 0) {
      i--;
      while(i > 0) {
        free(buffers[i]);
        i--;
      }
      return RTEMS_UNSATISFIED;
    }
  }
  
  last_buffer = -1;
  current_buffer = 0;
  
  MM_WRITE(MM_BT656_BASE, (unsigned int)buffers[current_buffer]);
  MM_WRITE(MM_BT656_FILTERSTATUS, BT656_FILTER_FIELD1);
  
  return RTEMS_SUCCESSFUL;
}

rtems_device_driver video_close(
  rtems_device_major_number major,
  rtems_device_minor_number minor,
  void *arg
)
{
  int i;
  
  MM_WRITE(MM_BT656_FILTERSTATUS, 0);
  while(MM_READ(MM_BT656_FILTERSTATUS) & BT656_FILTER_INFRAME);
  for(i=0;i<N_BUFFERS;i++)
    free(buffers[i]);
  return RTEMS_SUCCESSFUL;
}

static void invalidate_caches(void)
{
  volatile char *flushbase = (char *)FMLBRG_FLUSH_BASE;
  int i, offset;

  offset = 0;
  for (i=0;i<FMLBRG_LINE_COUNT;i++) {
    flushbase[offset] = 0;
    offset += FMLBRG_LINE_LENGTH;
  }
  __asm__ volatile( /* Invalidate Level-1 data cache */
    "wcsr DCC, r0\n"
    "nop\n"
  );
}

static void set_format(int format)
{
  switch(format) {
    case VIDEO_FORMAT_CVBS6:
      write_reg(0x00, 0x00);
      write_reg(0xc3, 0x05);
      write_reg(0xc4, 0x80);
      break;
    case VIDEO_FORMAT_CVBS5:
      write_reg(0x00, 0x00);
      write_reg(0xc3, 0x0d);
      write_reg(0xc4, 0x80);
      break;
    case VIDEO_FORMAT_CVBS4:
      write_reg(0x00, 0x00);
      write_reg(0xc3, 0x04);
      write_reg(0xc4, 0x80);
      break;
    case VIDEO_FORMAT_SVIDEO:
      write_reg(0x00, 0x06);
      write_reg(0xc3, 0xd5);
      write_reg(0xc4, 0x80);
      break;
    case VIDEO_FORMAT_COMPONENT:
      write_reg(0x00, 0x09);
      write_reg(0xc3, 0x45);
      write_reg(0xc4, 0x8d);
      break;
  }
}

rtems_device_driver video_control(
  rtems_device_major_number major,
  rtems_device_minor_number minor,
  void *arg
)
{
  rtems_libio_ioctl_args_t *args = arg;
  unsigned int *a = (unsigned int *)args->buffer;
  rtems_status_code sc;

  switch (args->command) {
    case VIDEO_BUFFER_LOCK:
      if (last_buffer == -1) {
        *a = 0;
      } else {
        bsp_interrupt_vector_disable(MM_IRQ_VIDEOIN);
        if(*a) invalidate_caches();
        *a = (unsigned int)buffers[last_buffer];
        buffers_locked[last_buffer] = true;
        bsp_interrupt_vector_enable(MM_IRQ_VIDEOIN);
      }
      sc = RTEMS_SUCCESSFUL;
      break;
    case VIDEO_BUFFER_UNLOCK: {
      int i;
      for(i=0;i<N_BUFFERS;i++) {
        if ((unsigned int)buffers[i] == (unsigned int)a) {
          buffers_locked[i] = false;
          break;
        }
      }
      sc = RTEMS_SUCCESSFUL;
      break;
    }
    
    case VIDEO_SET_BRIGHTNESS:
      write_reg(0x0a, (unsigned int)a);
      sc = RTEMS_SUCCESSFUL;
      break;
    case VIDEO_GET_BRIGHTNESS:
      *a = read_reg(0x0a);
      sc = RTEMS_SUCCESSFUL;
      break;
    case VIDEO_SET_CONTRAST:
      write_reg(0x08, (unsigned int)a);
      sc = RTEMS_SUCCESSFUL;
      break;
    case VIDEO_GET_CONTRAST:
      *a = read_reg(0x08);
      sc = RTEMS_SUCCESSFUL;
      break;
    case VIDEO_SET_HUE:
      write_reg(0x0b, (unsigned int)a);
      sc = RTEMS_SUCCESSFUL;
      break;
    case VIDEO_GET_HUE:
      *a = read_reg(0x0b);
      sc = RTEMS_SUCCESSFUL;
      break;
    
    case VIDEO_GET_SIGNAL:
      *a = read_reg(0x10);
      sc = RTEMS_SUCCESSFUL;
      break;
    
    case VIDEO_SET_REGISTER:
      write_reg(((unsigned int)a & 0xffff0000) >> 16,
        (unsigned int)a & 0x0000ffff);
      sc = RTEMS_SUCCESSFUL;
      break;
    case VIDEO_GET_REGISTER:
      *a = read_reg(*a);
      sc = RTEMS_SUCCESSFUL;
      break;
    
    case VIDEO_SET_FORMAT:
      set_format((int)a);
      sc = RTEMS_SUCCESSFUL;
      break;
    
    default:
      sc = RTEMS_UNSATISFIED;
      break;
  }

  if (sc == RTEMS_SUCCESSFUL)
    args->ioctl_return = 0;
  else
    args->ioctl_return = -1;

  return sc;
}
