#   Copyright (c) 2016, Xilinx, Inc.
#   All rights reserved.
# 
#   Redistribution and use in source and binary forms, with or without 
#   modification, are permitted provided that the following conditions are met:
#
#   1.  Redistributions of source code must retain the above copyright notice, 
#       this list of conditions and the following disclaimer.
#
#   2.  Redistributions in binary form must reproduce the above copyright 
#       notice, this list of conditions and the following disclaimer in the 
#       documentation and/or other materials provided with the distribution.
#
#   3.  Neither the name of the copyright holder nor the names of its 
#       contributors may be used to endorse or promote products derived from 
#       this software without specific prior written permission.
#
#   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
#   AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 
#   THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
#   PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 
#   CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
#   EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
#   PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
#   OR BUSINESS INTERRUPTION). HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
#   WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 
#   OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 
#   ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

__author__      = "Yun Rock Qu"
__copyright__   = "Copyright 2016, Xilinx"
__email__       = "pynq_support@xilinx.com"

from time import sleep
from pynq.iop import iop_const
from pynq.iop import DevMode
from pynq.iop import PMODA
from pynq.iop import PMODB

I2C_DELAY = .001

class Pmod_IIC(object):
    """This class controls the Pmod IIC pins.
    
    Note
    ----
    The index of the Pmod pins:
    upper row, from left to right: {vdd,gnd,3,2,1,0}.
    lower row, from left to right: {vdd,gnd,7,6,5,4}.
    
    Attributes
    ----------
    iop : _IOP
        The _IOP object returned from the DevMode.
    scl_pin : int
        The SCL pin number.
    sda_pin : int
        The SDA pin number.
    iic_addr : int
        The IIC device address.
    sr_addr : int
        The IIC device SR address (base address + 0x104).
    dtr_addr : int
        The IIC device DTR address (base address + 0x108).
    cr_addr : int
        The IIC device CR address (base address + 0x100).
    rfd_addr : int
        The IIC device RFD address (base address + 0x120).
    drr_addr : int
        The IIC device DRR address (base address + 0x10C).
    
    """
    def __init__(self, if_id, scl_pin, sda_pin, iic_addr): 
        """Return a new instance of a Pmod IIC object.
        
        Parameters
        ----------
        if_id : int
            The interface ID (1, 2) corresponding to (PMODA, PMODB).
        scl_pin : int
            The SCL pin number.
        sda_pin : int
            The SDA pin number.
        iic_addr : int
            The IIC device address.
            
        """
        if not if_id in [PMODA, PMODB]:
            raise ValueError("No such IOP for Pmod device.")
        if (scl_pin not in range(8)):
            raise ValueError("Valid SCL pin numbers are 0 - 7.")
        if (sda_pin not in range(8)):
            raise ValueError("Valid SDA pin numbers are 0 - 7.")
        
        switchconfig = []
        for i in range(8):
            if i == sda_pin:
                switchconfig.append(iop_const.PMOD_SWCFG_IIC0_SDA)
            elif i == scl_pin:
                switchconfig.append(iop_const.PMOD_SWCFG_IIC0_SCL)
            else:
                switchconfig.append(iop_const.PMOD_SWCFG_DIO0)
        
        self.iop = DevMode(if_id, switchconfig)
        self.iop.start()
        self.iop.load_switch_config(switchconfig)
        
        self.iic_addr = iic_addr

        # Useful IIC controller addresses
        self.sr_addr = iop_const.PMOD_XIIC_0_BASEADDR + \
                       iop_const.PMOD_XIIC_SR_REG_OFFSET

        self.dtr_addr = iop_const.PMOD_XIIC_0_BASEADDR + \
                        iop_const.PMOD_XIIC_DTR_REG_OFFSET

        self.cr_addr = iop_const.PMOD_XIIC_0_BASEADDR + \
                       iop_const.PMOD_XIIC_CR_REG_OFFSET
    
        self.rfd_addr = iop_const.PMOD_XIIC_0_BASEADDR + \
                        iop_const.PMOD_XIIC_RFD_REG_OFFSET

        self.drr_addr = iop_const.PMOD_XIIC_0_BASEADDR + \
                        iop_const.PMOD_XIIC_DRR_REG_OFFSET

    def _iic_enable(self):
        """This method enables the IIC drivers.
        
        The correct sequence to enable the drivers is:
        1. Disale the IIC core.
        2. Set the Rx FIFO depth to maximum.
        3. Reset the IIC core and flush the Tx FIFO.
        4. Enable the IIC core.
        
        Note
        ----
        This function is only required during initialization.
        
        Parameters
        ----------
        None
        
        Returns
        -------
        None
        
        """
        # Disable the IIC core
        self.iop.write_cmd(self.cr_addr, 0x00)
        # Set the Rx FIFO depth to maximum
        self.iop.write_cmd(self.rfd_addr, 0x0F)       
        # Reset the IIC core and flush the Tx FIFO
        self.iop.write_cmd(self.cr_addr, 0x02)
        # Enable the IIC core
        self.iop.write_cmd(self.cr_addr, 0x01)
        
        sleep(I2C_DELAY)
        
    def send(self, iic_bytes):
        """This method sends the command or data to the driver.
        
        Parameters
        ----------
        iic_bytes : list
            A list of 8-bit bytes to be sent to the driver.
            
        Returns
        -------
        None
        
        Raises
        ------
        RuntimeError
            Timeout when waiting for the FIFO to be empty.
            
        """
        # Enable IIC Core
        self._iic_enable()
        
        # Transmit 7-bit address and Write bit (with START)
        self.iop.write_cmd(self.dtr_addr, 0x100 | (self.iic_addr << 1))
        
        # Iteratively write into Tx FIFO, wait for it to be empty        
        for tx_cnt in range(len(iic_bytes)):
            timeout = 100
            
            # Construct the TX word
            if (tx_cnt == len(iic_bytes) - 1):
                tx_word = (0x200 | iic_bytes[tx_cnt])
            else:
                tx_word = iic_bytes[tx_cnt]
            
            # Write data
            self.iop.write_cmd(self.dtr_addr, tx_word)
            while ((timeout > 0) and \
                        ((self.iop.read_cmd(self.sr_addr) & 0x80) == 0x00)):
                timeout -= 1
            if (timeout == 0):
                raise RuntimeError("Timeout when writing IIC.")
                
        sleep(I2C_DELAY)

    def receive(self, num_bytes):
        """This method receives IIC bytes from the device.
        
        Parameters
        ----------
        num_bytes : int
            Number of bytes to be received from the device.
            
        Returns
        -------
        iic_bytes : list
            A list of 8-bit bytes received from the driver.
        
        Raises
        ------
        RuntimeError
            Timeout when waiting for the RX FIFO to fill.
            
        """

        # Reset the IIC core and flush the Tx FIFO
        self.iop.write_cmd(self.cr_addr, 0x02)

        # Set the Rx FIFO depth to one byte
        self.iop.write_cmd(self.rfd_addr, 0x0) 

        # Transmit 7-bit address and Read bit
        self.iop.write_cmd(self.dtr_addr, 0x101 | (self.iic_addr << 1))

        # Enable the IIC core
        cr_reg = 0x05
        if num_bytes == 1:
            cr_reg |= 0x10

        self.iop.write_cmd(self.cr_addr,cr_reg)
        sleep(I2C_DELAY)

        # Program IIC Core to read num_bytes bytes and issue STOP
        self.iop.write_cmd(self.dtr_addr, 0x200 + num_bytes)

        # Read num_bytes from RX FIFO
        iic_bytes = list()
        while(len(iic_bytes) < num_bytes):
 
            # Special condition for last two bytes
            if (num_bytes - len(iic_bytes)) == 1:
                self.iop.write_cmd(self.cr_addr,0x1)
            elif (num_bytes - len(iic_bytes)) == 2:
                self.iop.write_cmd(self.cr_addr, \
                                   self.iop.read_cmd(self.cr_addr) | 0x10)

            # Wait for data to be available in RX FIFO
            timeout = 100
            while(((self.iop.read_cmd(self.sr_addr) & 0x40) == 0x40) and \
                  (timeout > 0)):
                timeout -= 1

            if(timeout == 0):
                raise RuntimeError("Timeout when reading IIC.")

            # Read data 
            iic_bytes.append((self.iop.read_cmd(self.drr_addr) & 0xff))

        sleep(I2C_DELAY)
        return iic_bytes
        
