/*******************************************************************************
* File : pci_chr_drv_ctrlpln.c
*
* Description : Linux Device Driver to access SmartFusion2/IGLOO2 PCIe Endpoint
*
********************************************************************************/
#include <linux/kernel.h>
#include <linux/version.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/sched.h>
#include <linux/errno.h>
#include <asm/current.h>
#include <linux/pci_regs.h>
#include <linux/pci.h>
#include <linux/interrupt.h>
#include <linux/device.h>
#include <linux/kdev_t.h>
#include <linux/types.h>
#include <linux/delay.h>
#include "../inc/board.h"

#define DRV_NAME "MS_PCI_DRIVER"
#define PCI_DEV_NAME "MS_PCI_DEV"

#define PCI_BAR_0 0
#define PCI_BAR_1 1

#define LED_CONTROL 1
#define SRAM_WRITE 2
#define SRAM_READ 3
#define DIP_SWITCH 4
#define INTERRUPT_CTRL 6
#define INTERRUPT_CNTR 7
#define DSN 8

#define READ 0
#define CLEAR 0

/* Led Control Offset */
#define LED1_OFFSET 0x4C00
#define LED2_OFFSET 0x4C01
#define LED3_OFFSET 0x4C02
#define LED4_OFFSET 0x4C03
#define LED5_OFFSET 0x4C04
#define LED6_OFFSET 0x4C05
#define LED7_OFFSET 0x4C06
#define LED8_OFFSET 0x4C07

#define GPIO9_OFFSET 0x4C08
#define GPIO10_OFFSET 0x4C09
#define GPIO11_OFFSET 0x4C0A
#define GPIO12_OFFSET 0x4C0B

#define GPIO_LINE_OUTPUT 0x0005 
#define GPIO_LINE_INPUT 0x0003 

#ifdef SF2
#define LED_GLOW_REG_OFFSET 0x4C22 /* SF2 offset */
#else
#define LED_GLOW_REG_OFFSET 0x28 /* IGL2 Led Offset */
#endif

#ifdef SF2
#define DIP_SWITCH_STATUS_REGISTER 0x4C21 /* SF2 Dip Switch offset */
#else
#define DIP_SWITCH_STATUS_REGISTER 0x24 /* IGL2 Dip Switch Offset */
#endif

#define INTERRUPT_OFFSET 0x52

/* Device Serial Number Offset Value */
#define DSN_CTRL_OFFSET 0x28
#define DSN_CTRL_VALUE 0x100 /* Bit 8 set */
#define DSN_STATUS_OFFSET 0x24
#define DSN_STATUS_VALUE 0x4 /* Bit 4 set */

#define dsnOffset0 0x2010 
#define dsnOffset1 0x2011
#define dsnOffset2 0x2012
#define dsnOffset3 0x2013

#define SUCCESS 0
#define ENABLE 1
#define DISABLE 0

struct cdev *ms_pci_cdev = NULL;
dev_t ms_pci_dev;
static struct class *ms_pci_class = NULL;

static unsigned int wr_buf[32]; // 32 bytes buffer
static unsigned int rd_buf[32]; // 32 bytes buffer

unsigned long sramRcvdReadOffset = 0; /* Made global to control the sram read offset */
unsigned long dipSwitchStatusOffset = 0; /* Made global to control dip switch offset */
unsigned char sramReadFlag;
unsigned char dipSwitchReadFlag;
unsigned char interruptCntrFlag;
unsigned char dsnReadFlag;

static unsigned long intCounter;

void interrupt_control(struct pci_dev *pdev, unsigned char);
struct pci_dev *samplePdev;

/* Structure to hold driver specific resources */
struct ms_pci_resource {
	/* PCI BAR #0 */
	u32 bar0_base;
	u32 bar0_size;
	volatile void __iomem *base_addr0; // virtual address to represent PCI BAR #0

	/* PCI BAR #1 */
	u32 bar1_base;
	u32 bar1_size;
	volatile void __iomem *base_addr1; // virtual address to represent PCI BAR #1	 	
};

static struct ms_pci_resource * res; 

static struct pci_device_id ms_pci_tbl[] = {
	{ 0x11aa, 0x11aa, PCI_ANY_ID, PCI_ANY_ID, 0, 0, 0},
	{0,}
};
MODULE_DEVICE_TABLE(pci, ms_pci_tbl);

/**
* interrupt_control() - Enable/Disable the interrupts
*
*  @pdev : platform device
*  @data : enable/disable value
*/
void interrupt_control(struct pci_dev *pdev, unsigned char data)
{
	if(data == ENABLE)
	{
		pci_read_config_byte(pdev, INTERRUPT_OFFSET, &data);

		data = *(&data) | 0x01;	/* Enable the first bit */	

		pci_write_config_byte(pdev, INTERRUPT_OFFSET, data);

		pr_info("pci write config success :: MSI Interrupt Enabled\n");

	}else if(data == DISABLE)
	{
		pci_read_config_byte(pdev, INTERRUPT_OFFSET, &data);
	
		data = *(&data) & 0xFE; /* Mask the first bit */

		pci_write_config_byte(pdev, INTERRUPT_OFFSET, data);

		pr_info("pci write config success :: MSI Interrupt Disabled\n");

		intCounter = CLEAR; /* Clear the interrupt Counter */

	}else
	{
		pr_info("Invalid Interrupt Data Flag recieved\n");
	}
}

/**
* ms_pci_interrupt() - PCIe interrupt Handler
*
*  @irq : PCIe interrupt number
*/
void ms_pci_interrupt(int irq)
{
	intCounter = intCounter + 1; /* To count interrupts */
	pr_info("intCounter = %ld\n", intCounter);

	*(unsigned int *)(res->base_addr0+0x04) = 0x01; // Clear the interrupt

	pr_info("irq cleared %d\n",irq);
}

/**
* ms_pci_probe() - Initialize char device Module
*
*  @pdev : Platform Device
*  @ent  : PCI Device ID List Entry 
*/

static int ms_pci_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{
	int i;

        pr_info("pci device found\n");

 	i = pci_enable_device(pdev);
	if (i)
		return i;

	pci_set_master(pdev);

	res = (struct ms_pci_resource *) kmalloc(sizeof(*res), GFP_KERNEL);
	if (res == NULL) {
		pr_info("Unable to allocate memory for PCI resource\n");
		return -ENOMEM;
	}

	/* PCI BAR 0# resources */

	res->bar0_base = pci_resource_start (pdev, PCI_BAR_0);
	pr_info("PCI BAR #0 BASE = 0x%x\n", res->bar0_base);

	if (!res->bar0_base || ((pci_resource_flags (pdev, PCI_BAR_0) & IORESOURCE_MEM) == 0)) {
                dev_err(&pdev->dev, "no I/O resource at PCI BAR #0\n");
        	goto err_out_free_res;
        }

	res->bar0_size = pci_resource_len (pdev, PCI_BAR_0);
	pr_info("PCI BAR #0 SIZE = 0x%x\n", res->bar0_size);

	if(request_mem_region(res->bar0_base, res->bar0_size, DRV_NAME) == NULL) {
		pr_info("PCI BAR #0 Space not available\n");
        	goto err_out_free_res;
	}

	res->base_addr0 = (void *)ioremap_nocache(res->bar0_base, res->bar0_size); 
	pr_info("base_addr0 = 0x%p\n", res->base_addr0);
	if(res->base_addr0 == NULL) {
		pr_info("Unable to map BAR #0 address");
        	goto err_out_free_reg;
	}
		
	 /* PCI BAR 1# resources */

        res->bar1_base = pci_resource_start (pdev, PCI_BAR_1);
        pr_info("PCI BAR #1 BASE = 0x%x\n", res->bar1_base);

        if (!res->bar1_base || ((pci_resource_flags (pdev, PCI_BAR_1) & IORESOURCE_MEM) == 0)) {
                dev_err(&pdev->dev, "no I/O resource at PCI BAR #1\n");
                goto err_out_free_res;
        }

        res->bar1_size = pci_resource_len (pdev, PCI_BAR_1);
        pr_info("PCI BAR #1 SIZE = 0x%x\n", res->bar1_size);

        if(request_mem_region(res->bar1_base, res->bar1_size, DRV_NAME) == NULL) {
                pr_info("PCI BAR #1 Space not available\n");
                goto err_out_free_res;
        }

        res->base_addr1 = (void *)ioremap_nocache(res->bar1_base, res->bar1_size);
        pr_info("base_addr1 = 0x%p\n", res->base_addr1);
        if(res->base_addr1 == NULL) {
                pr_info("Unable to map BAR #1 address");
                goto err_out_free_reg;
        }

	if(pci_enable_msi(pdev))
	{
		pr_info("Unable to enable MSI interrupt 0x%x\n",pdev->irq);
		goto err_out_free_reg;	
	}
		pr_info("MSI interrupt 0x%x\n",pdev->irq);

	if(request_irq(pdev->irq, (void *)ms_pci_interrupt, IRQF_SHARED, DRV_NAME, pdev)) {
		pr_info("unable to register irq 0x%x\n", pdev->irq);
        	goto err_out_free_irq;
	}

		samplePdev = pdev;
	return 0;

err_out_free_irq:
	free_irq(pdev->irq, pdev);
	pci_disable_msi(pdev);
	iounmap(res->base_addr0);
	iounmap(res->base_addr1);
err_out_free_reg:
	release_mem_region(res->bar0_base, res->bar0_size);
	release_mem_region(res->bar1_base, res->bar1_size);
err_out_free_res:
	kfree((const void *)res);	
	return -ENODEV;
}

/**
* ms_pci_remove() - Uninitialize the PCIe device
*
*  @pdev : Platform Device
*/
static void ms_pci_remove(struct pci_dev *pdev)
{
 	pci_clear_master(pdev);
	pci_disable_device(pdev);
	free_irq(pdev->irq, pdev);
	pci_disable_msi(pdev);
	iounmap(res->base_addr0);
	iounmap(res->base_addr1);
	release_mem_region(res->bar0_base, res->bar0_size);
	release_mem_region(res->bar1_base, res->bar1_size);
	kfree((const void *)res);	
}

static struct pci_driver ms_pci_driver = {
        .name           = DRV_NAME,
        .probe          = ms_pci_probe,
        .remove         = ms_pci_remove,
        .id_table       = ms_pci_tbl,
};

/**
* char_dev_open() - Open Char Device
*
*  @inode : Device Driver Information
*  @file  : Pointer to the file struct
*/
static int char_dev_open(struct inode *inode,
			    struct file  *file)
{
	printk(KERN_INFO "open operation invoked \n");
	return SUCCESS;
}

/**
* char_dev_release() - Release Char Device
*
*  @inode : Device Driver Information
*  @file  : Pointer to the file struct
*/

static int char_dev_release(struct inode *inode,
		            struct file *file)
{
	return SUCCESS;
}

/**
* char_dev_write() - Copy buffer from user and write to PCIe driver
*
*  @file : Pointer to the file struct
*  @buf  : Write buffer
*  @lbuf : size of the buffer write
*  @ppos : Position of buffer
*/

static ssize_t char_dev_write(struct file *file,
		            const char *buf,
			    size_t lbuf,
			    loff_t *ppos)
{
	int nbytes; /* Number of bytes written */
	unsigned int *ptr0;
	unsigned int *ptr1; 
	unsigned long sramRcvdData;
	unsigned long sramRcvdWriteOffset;
	int cnt;

	if(lbuf >= 256) {
		pr_info("write buffer size too large\n");
		return -1;
	}
		
	nbytes = copy_from_user( (char *)(wr_buf), //+ *ppos, /* to */
					buf, /* from */ 
					lbuf ); /* how many bytes */

	for(cnt = 0; cnt < 32; cnt++)
	pr_info("0x%x", wr_buf[cnt]); 
	
	ptr0 = (unsigned int *)(res->base_addr0);  
	pr_info("\nptr0 = %p\n", ptr0);

	ptr1 = (unsigned int *)(res->base_addr1);  
	pr_info("ptr1 = %p\n", ptr1);

	if(wr_buf[0] == LED_CONTROL) /* LED Access */
	{

#ifdef SF2
		*(ptr0 + LED1_OFFSET) = GPIO_LINE_OUTPUT;
		*(ptr0 + LED2_OFFSET) = GPIO_LINE_OUTPUT;
		*(ptr0 + LED3_OFFSET) = GPIO_LINE_OUTPUT;
		*(ptr0 + LED4_OFFSET) = GPIO_LINE_OUTPUT;
		*(ptr0 + LED5_OFFSET) = GPIO_LINE_OUTPUT;
		*(ptr0 + LED6_OFFSET) = GPIO_LINE_OUTPUT;
		*(ptr0 + LED7_OFFSET) = GPIO_LINE_OUTPUT;
		*(ptr0 + LED8_OFFSET) = GPIO_LINE_OUTPUT;
#endif

		for(cnt=0; cnt<4; cnt++)
		{
			*(ptr0 + LED_GLOW_REG_OFFSET) = wr_buf[cnt+1];
		}
		pr_info("LED : 0x%x Data[24-31] : 0x%x Data[16-23] : 0x%x Data[8-15] : 0x%x Data[0-7] : 0x%x\n", wr_buf[0],wr_buf[1], \
                wr_buf[2],wr_buf[3],wr_buf[4]);

	}else if(wr_buf[0] == SRAM_WRITE) /* SRAM Write Access */
	{
	         sramRcvdData = (wr_buf[2] << 24); /* 24 - 31*/
		 sramRcvdData = ((wr_buf[3] << 16) | sramRcvdData); /* 16 -23 */
		 sramRcvdData = ((wr_buf[4] << 8 ) | sramRcvdData); /* 8 -15 */
		 sramRcvdData = (wr_buf[5] | sramRcvdData);  /* 0 -7 */

		 sramRcvdWriteOffset = wr_buf[6];

		*(ptr1+sramRcvdWriteOffset) = sramRcvdData;

		 pr_info(" sram data wrote : 0x%x\n", *(ptr1+sramRcvdWriteOffset));

		pr_info("SRAM : 0x%x Write : 0x%x Data[24-31] : 0x%x Data[16-23] :0x%x Data[8-15] : 0x%x Data[0-7]: 0x%x Write Offset : 0x%x\n", \
		wr_buf[0],wr_buf[1],wr_buf[2],wr_buf[3],wr_buf[4],wr_buf[5],wr_buf[6]);

	}else if(wr_buf[0] == SRAM_READ) /* SRAM Read Access */
	{
		if (wr_buf[1] == READ)
		{
			pr_info("wr_buf[2] = 0x%x\n",wr_buf[2]);
			sramRcvdReadOffset = wr_buf[2];	
			sramReadFlag = ENABLE;
		}else
		{
			pr_info("Invalid Read Command Recieved\n");
		}

	}else if(wr_buf[0] == DIP_SWITCH) /* DIP switch Access */
	{

#ifdef SF2
		*(ptr0 + GPIO9_OFFSET)  = GPIO_LINE_INPUT; /* Make GPIO9 Line Input */
                *(ptr0 + GPIO10_OFFSET) = GPIO_LINE_INPUT; /* Make GPIO10 Line Input */
                *(ptr0 + GPIO11_OFFSET) = GPIO_LINE_INPUT; /* Make GPIO11 Line Input */
                *(ptr0 + GPIO12_OFFSET) = GPIO_LINE_INPUT; /* Make GPIO12 Line Input */
#endif

		dipSwitchStatusOffset = DIP_SWITCH_STATUS_REGISTER; /* Dip Switch Status Register */ 

		dipSwitchReadFlag = ENABLE;
		
	}else if(wr_buf[0] == INTERRUPT_CTRL)
	{	
		if(wr_buf[1] == ENABLE)
		{
			pr_info("Enable Interrupts\n");
			interrupt_control(samplePdev,ENABLE);
		
		}else if(wr_buf[1] == DISABLE)
		{
			pr_info("Disable interrupts\n");
			interrupt_control(samplePdev,DISABLE);
		}else
		{	
			pr_info("Invalid Interrupt Control rcvd\n");
		}
	}
	else if(wr_buf[0] == INTERRUPT_CNTR)
	{
		interruptCntrFlag = ENABLE;
	}else if(wr_buf[0] == DSN)
	{
		*(ptr0 + DSN_CTRL_OFFSET) =  (*(ptr0 + DSN_CTRL_OFFSET) | 0x1FF); /* Enable(1) on 8th bit of 0xA0 BAR0 address */
		dsnReadFlag = ENABLE;
	}
	else
	{
		pr_info("Invalid access command recieved\n");
	}

	return nbytes;
}

/**
* char_dev_read() - Queue and copy buffer to user
*
*  @file : Pointer to the file struct
*  @buf  : recieved buffer
*  @lbuf : size of the buffer
*  @ppos : Position in buffer
*  
*  Returns:
*  Bytes Successfully Read
*  
*/

static ssize_t char_dev_read(struct file *file,
                            char *buf,
                            size_t lbuf,
                            loff_t *ppos)
{
        int nbytes; /* number of bytes actually read */
        unsigned int * readPtr0;
        unsigned int * readPtr1;

        readPtr0 = (unsigned int *)(res->base_addr0); /* PCI BAR0 Addr */
        readPtr1 = (unsigned int *)(res->base_addr1); /* PCI BAR1 Addr */
	
	pr_info("sramRcvdReadOffset = 0x%lx\n",sramRcvdReadOffset);
	pr_info("dipSwitchStatusOffset= 0x%lx\n",dipSwitchStatusOffset);
	pr_info("intCounter= %ld\n",intCounter);

	if(sramReadFlag == ENABLE)
	{
		rd_buf[0] = *(readPtr1+sramRcvdReadOffset);	/* SRAM Read from PCI BAR 1 */
		sramReadFlag = DISABLE;
	}
	else if(dipSwitchReadFlag == ENABLE)
	{
		rd_buf[0] = *(readPtr0+dipSwitchStatusOffset); /* DIP Switch Status Register */
		pr_info("rd_buf[0] = 0x%x\n", rd_buf[0]);
		dipSwitchReadFlag = DISABLE;
	}else if(interruptCntrFlag == ENABLE)
	{
		rd_buf[0] = intCounter;   /* Assign Interrupt Counter Value to Read Buffer */
		interruptCntrFlag = DISABLE;  
	}else if(dsnReadFlag == ENABLE)
	{
		int status;
		
		msleep(100);

		status = *(readPtr0+DSN_STATUS_OFFSET);
		status = (status >> 4) || (status & 0x1);

		pr_info("DSN status Bit = 0x%x\n",status);

		if (status == 1)
		{
			rd_buf[0] = *(readPtr1+dsnOffset0); /* 31:0 */
			rd_buf[1] = *(readPtr1+dsnOffset1); /* 63:32 */
			rd_buf[2] = *(readPtr1+dsnOffset2); /* 95:64 */
			rd_buf[3] = *(readPtr1+dsnOffset3); /* 127:96 */
			dsnReadFlag = DISABLE;
		}else
		{
			pr_info("Device Serial Number status bit is not set\n");	
		}
	}
	else
	{
		pr_info("Do Nothing\n");
	}

        nbytes =  copy_to_user( buf, /* to */
                               (char *)rd_buf, // + *ppos, /* from */
                               lbuf); /* how many bytes */
        return nbytes;
}

static struct file_operations char_dev_fops = {
	.owner = THIS_MODULE,
	.write = char_dev_write,
	.read = char_dev_read,
	.open = char_dev_open,
	.release = char_dev_release,
};

/**
*  pcidrv_init() - Initialize PCIe Module
*
*  Registers Platform Driver
*/

static __init int pcidrv_init(void)
{
	int ret,count=1;

	pr_info("Module Inserted \n");
	
	if(pci_register_driver(&ms_pci_driver)){
		pr_info("unable to register pci driver \n");
		return -1;
	}

	if (alloc_chrdev_region (&ms_pci_dev, 0, count, PCI_DEV_NAME) < 0) {
            printk (KERN_ERR "failed to reserve major/minor range\n");
            return -1;
    	}

	printk("MS_PCI_DEV:  Got Major %d\n", MAJOR(ms_pci_dev));

        if (!(ms_pci_cdev = cdev_alloc ())) {
            printk (KERN_ERR "cdev_alloc() failed\n");
            unregister_chrdev_region (ms_pci_dev, count);
            return -1;
 	}
	cdev_init(ms_pci_cdev,&char_dev_fops);

	ret=cdev_add(ms_pci_cdev,ms_pci_dev,count);
	if( ret < 0 ) {
		printk(KERN_INFO "Error registering device driver\n");
	        cdev_del (ms_pci_cdev);
                unregister_chrdev_region (ms_pci_dev, count); 	
		return -1;
	}

         ms_pci_class = class_create (THIS_MODULE, "VIRTUAL");
         device_create (ms_pci_class, NULL, ms_pci_dev,"%s",PCI_DEV_NAME);

	printk("Successfully created /dev/MS_PCI_DEV\n");

	printk(KERN_INFO"\nDevice Registered: %s\n",PCI_DEV_NAME);
	printk (KERN_INFO "Major number = %d, Minor number = %d\n", MAJOR (ms_pci_dev),MINOR (ms_pci_dev));

	return 0;
}

/**
* pcidrv_cleanup() - Uninitialize the PCIe Module
*
*  Unregister the Platform Driver
*/

static __exit void  pcidrv_cleanup(void)
{
	pr_info("Module Deleted \n");
	pci_unregister_driver (&ms_pci_driver);
        device_destroy (ms_pci_class, ms_pci_dev);
        class_destroy (ms_pci_class);
 	cdev_del(ms_pci_cdev);
	unregister_chrdev_region(ms_pci_dev,1);
	printk(KERN_INFO "\n Driver unregistered \n");
}

module_init(pcidrv_init);
module_exit(pcidrv_cleanup);

MODULE_AUTHOR("Microsemi");
MODULE_DESCRIPTION("PCI device driver");
MODULE_LICENSE("GPL");
