/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 *
 * Copyright (C) 2011 by Marcin Bukat
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ****************************************************************************/

#include "config.h"
#include "system.h"
#include "kernel.h"
#include "i2c-rk27xx.h"

/*  Driver for the rockchip rk27xx built-in I2C controller in master mode
    
    Both the i2c_read and i2c_write function take the following arguments:
    * slave, the address of the i2c slave device to read from / write to
    * address, optional sub-address in the i2c slave (unused if -1)
    * len, number of bytes to be transfered
    * data, pointer to data to be transfered
    A return value other than 0 indicates an error.
*/

static struct mutex i2c_mtx;

static bool i2c_stop(void)
{
    long timeout = current_tick + HZ/50;

    I2C_CONR |= (1<<4); /* NACK */
    I2C_LCMR |= (1<<2) | (1<<1); /* resume op, stop */

    while (I2C_LCMR & (1<<1))
        if (TIME_AFTER(current_tick, timeout))
            return false;

    return true;
}

static bool i2c_write_byte(uint8_t data, bool start)
{
    long timeout = current_tick + HZ/50;
    unsigned int isr_status;

    I2C_CONR = (1<<3) | (1<<2); /* master port enable, MTX mode, ACK enable */
    I2C_MTXR = data;

    if (start)
        I2C_LCMR = (1<<2) | (1<<0); /* resume op, start bit */
    else
        I2C_LCMR = (1<<2); /* resume op */

    /* wait for ACK from slave */
    do
    {
        isr_status = I2C_ISR;

        if (isr_status & (1<<7))
        {
            i2c_stop();
            I2C_ISR = 0;
            return false;
        }

        if (TIME_AFTER(current_tick, timeout))
            return false;

    } while ((isr_status & (1<<0)) == 0);

    /* clear status bit */
    I2C_ISR &= ~(1<<0);

    return true;
}

static bool i2c_read_byte(unsigned char *data)
{
    long timeout = current_tick + HZ/50;

    I2C_LCMR = (1<<2); /* resume op */

    while (!(I2C_ISR & (1<<1)))
        if (TIME_AFTER(current_tick, timeout))
            return false;

    *data = I2C_MRXR;

    /* clear status bit */
    I2C_ISR &= ~(1<<1);

    return true;
}



/* route i2c bus to internal codec or external bus
 * internal codec has 0x4e i2c slave address so
 * access to this address is routed to internal bus.
 * All other addresses are routed to external pads
 */
static void i2c_iomux(unsigned char slave)
{
    unsigned long muxa = SCU_IOMUXA_CON & ~(0x1f<<14);

    if ((slave & 0xfe) == (0x27<<1))
    {
        /* internal codec */
        SCU_IOMUXA_CON = (muxa | (1<<16) | (1<<14));
    }
    else
    {
        /* external I2C bus */
        SCU_IOMUXA_CON = (muxa | (1<<18));
    }
}

void i2c_init(void)
{
    mutex_init(&i2c_mtx);


    /* ungate i2c module clock */
    SCU_CLKCFG &= ~CLKCFG_I2C;

    I2C_OPR |= (1<<7); /* reset state machine */
    sleep(HZ/100);
    I2C_OPR &= ~((1<<7) | (1<<6)); /* clear ENABLE bit, deasert reset */

    /* set I2C divider to stay within allowed SCL freq limit
     * APBfreq = 50Mhz
     * I2C_div = (I2CCDVR[5:3] + 1) * 2^((I2CCDVR[2:0] + 1))
     * SCLfreq = APBfreq/(5*I2C_div)
     *
     * (5<<3) | (1<<0)    416.7 KHz (above spec)
     * (6<<3) | (1<<0)    357.1 kHz
     * (7<<3) | (1<<0)    312.4 kHz
     * (6<<3) | (2<<0)    178.6 kHz
     * (7<<3) | (2<<0)    156.3 kHz
     */
    I2C_OPR = (I2C_OPR & ~(0x3F)) | (6<<3) | (1<<0);

    I2C_IER = 0x00;

    I2C_OPR |= (1<<6); /* enable i2c core */

    /* turn off i2c module clock until we need to comunicate */
    SCU_CLKCFG |= CLKCFG_I2C;
}

int i2c_write(unsigned char slave, int address, int len,
              const unsigned char *data)
{
    int ret = 0;

    mutex_lock(&i2c_mtx);

    i2c_iomux(slave);

    /* ungate i2c clock */
    SCU_CLKCFG &= ~CLKCFG_I2C;

    /* clear all flags */
    I2C_ISR = 0x00;
    I2C_IER = 0x00;

    /* START */
    if (! i2c_write_byte(slave & ~1, true))
    {
        ret = 1;
        goto end;
    }

    if (address >= 0)
    {
        if (! i2c_write_byte(address, false))
        {
            ret = 2;
            goto end;
        }
    }

    /* write data */
    while (len--)
    {
        if (! i2c_write_byte(*data++, false))
        {
            ret = 4;
            goto end;
        }
    }

    /* STOP */
    if (! i2c_stop())
    {
        ret = 5;
        goto end;
    }

end:
    mutex_unlock(&i2c_mtx);
    SCU_CLKCFG |= CLKCFG_I2C;

    return ret;
}

int i2c_read(unsigned char slave, int address, int len, unsigned char *data)
{
    int ret = 0;

    mutex_lock(&i2c_mtx);

    i2c_iomux(slave);

    /* ungate i2c module clock */
    SCU_CLKCFG &= ~CLKCFG_I2C;

    /* clear all flags */
    I2C_ISR = 0x00;
    I2C_IER = 0x00;

    if (address >= 0)
    {
        /* START */
        if (! i2c_write_byte(slave & ~1, true))
        {
            ret = 1;
            goto end;
        }

        /* write address */
        if (! i2c_write_byte(address, false))
        {
            ret = 2;
            goto end;
        }
    }

    /* (repeated) START */
    if (! i2c_write_byte(slave | 1, true))
    {
        ret = 3;
        goto end;
    }

    I2C_CONR = (1<<2); /* master port enable, MRX mode, ACK enable */

    while (len)
    {
        if (! i2c_read_byte(data++))
        {
            ret = 4;
            goto end;
        }

        if (len == 1)
            I2C_CONR |= (1<<4); /* NACK */
        else
            I2C_CONR &= ~(1<<4); /* ACK */

        len--;
    }

   /* STOP */
    if (! i2c_stop())
    {
        ret = 5;
        goto end;
    }

end:
    mutex_unlock(&i2c_mtx);
    SCU_CLKCFG |= CLKCFG_I2C;

    return ret;
}
