/*
 * tiumfwl.c: Texas Instruments UltraMedia firmware loader
 *
 * Copyright (C) 2006 Jochen Eisinger <jochen@penguin-breeder.org>
 *
 * Firmware is:
 * 	Copyright (C) 2002 Texas Instruments
 */

#include <linux/config.h>

#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/compiler.h>
#include <linux/slab.h>
#include <linux/delay.h>
#include <linux/init.h>
#include <linux/pci.h>
#include <linux/firmware.h>

#define DRV_MODULE_NAME		"tiumfwl"
#define PFX DRV_MODULE_NAME	": "
#define DRV_MODULE_VERSION	"1.10"
#define DRV_MODULE_RELDATE	"June 7, 2006"

static char version[] __devinitdata =
	DRV_MODULE_NAME ".c: v" DRV_MODULE_VERSION " (" DRV_MODULE_RELDATE ") "
	"Texas Instruments UltraMedia firmware loader\n";

static int key = 0;

module_param(key, int, 0);

MODULE_AUTHOR("Jochen Eisinger <jochen@penguin-breeder.org>");
MODULE_DESCRIPTION("Texas Instruments UltraMedia firmware loader");
MODULE_LICENSE("GPL");
MODULE_VERSION(DRV_MODULE_VERSION);

#define REG_DATA_ADDR	0x00
#define REG_LOADER_CTRL	0x04

#define FLG_ADDR_RST	0x08
#define FLG_DONE	0x04
#define FLG_PROGRAM	0x02
#define FLG_ERR		0x01

static struct pci_device_id tiumfwl_pci_tbl[] = {
	{ PCI_VENDOR_ID_TI, 0x8204,		/* PCI7510 */
	  PCI_ANY_ID, PCI_ANY_ID, 0, 0, 0UL },
	{ PCI_VENDOR_ID_TI, 0x8201,		/* PCI1620 */
	  PCI_ANY_ID, PCI_ANY_ID, 0, 0, 0UL },
	{ 0, }
};

MODULE_DEVICE_TABLE(pci, tiumfwl_pci_tbl);

static struct {
	unsigned short vendor;
	unsigned short device;
	int encrypted;
	char * fw;
} tiumfwl_fw_map[] = {
  { PCI_VENDOR_ID_TI, 0x8204, 1, "pci7510.bin" },
  { PCI_VENDOR_ID_TI, 0x8201, 1, "pci1620.bin" },
  { 0, }
};

static int tiumfwl_load_firmware(struct pci_dev *pdev)
{
	int i;
	const struct firmware *fw_entry = NULL;
	u8 *fw_ptr;
	long fw_len;
	int io_base;
	unsigned int state;

	for (i=0; tiumfwl_fw_map[i].vendor; i++)
		if ((tiumfwl_fw_map[i].vendor == pdev->vendor) &&
			(tiumfwl_fw_map[i].device == pdev->device))
			break;

	if (request_firmware(&fw_entry, tiumfwl_fw_map[i].fw, &pdev->dev)) {
		printk(KERN_ERR PFX "%s: request_firmware() failed\n",
			pci_name(pdev));
		return -1;
	}

	fw_ptr = (u8 *)fw_entry->data;
	fw_len = fw_entry->size;

	if (tiumfwl_fw_map[i].encrypted)
		for (i=0; i<fw_entry->size; i++)
			fw_ptr[i] ^= (u8)key;

	io_base = pci_resource_start(pdev, 0);
	
	if (((fw_entry->size >= 0x5000) && (fw_entry->size < 0x8000)) || 
		(fw_entry->size > 0xF000)) {
		printk(KERN_ERR PFX 
			"%s: invalid firmware size\n", pci_name(pdev));
		goto err_out;
	}

	/* start firmware upload sequence */
	outl_p(0, io_base + REG_LOADER_CTRL);
	outl_p(FLG_ADDR_RST | FLG_PROGRAM, io_base + REG_LOADER_CTRL);
	
	/* clear ERR flag */
	inl_p(io_base + REG_LOADER_CTRL);

	/* set start address */
	outl_p(0, io_base + REG_DATA_ADDR);
	state = inl_p(io_base + REG_LOADER_CTRL);


	if (state & FLG_ERR) {
		for (i=0; i<5; i++) {
			outl_p(0, io_base + REG_LOADER_CTRL);
			outl_p(FLG_ADDR_RST | FLG_PROGRAM, 
					io_base + REG_LOADER_CTRL);
			outl_p(0, io_base + REG_DATA_ADDR);
			state = inl_p(io_base + REG_LOADER_CTRL);
			if (!(state & FLG_ERR))
				break; 
		}

		if (state & FLG_ERR) {
			printk(KERN_ERR PFX 
				"%s: failed to initiate firmware upload\n",
				pci_name(pdev));
			goto err_out;
		}
	}

	while (fw_len > 0) {
		if ((fw_ptr - (u8 *)fw_entry->data) == 0x5000) {
			fw_len -= 0x3000;
			fw_ptr += 0x3000;

			outl_p(0, io_base + REG_LOADER_CTRL);
			outl_p(FLG_ADDR_RST | FLG_PROGRAM, 
					io_base + REG_LOADER_CTRL);
			
			/* clear ERR flag */
			inl_p(io_base + REG_LOADER_CTRL);

			/* set start address */
			outl_p(0x8000, io_base + REG_DATA_ADDR);
			state = inl_p(io_base + REG_LOADER_CTRL);

			if (state & FLG_ERR) {
				for (i=0; i<5; i++) {
					outl_p(0, io_base + REG_LOADER_CTRL);
					outl_p(FLG_ADDR_RST | FLG_PROGRAM, 
						io_base + REG_LOADER_CTRL);
					outl_p(0, io_base + REG_DATA_ADDR);
					state = inl_p(io_base +
							REG_LOADER_CTRL);
					if (!(state & FLG_ERR))
						break; 
				}

				if (state & FLG_ERR) {
					printk(KERN_ERR PFX 
						"%s: failed to skip to next "
						"firmware location\n",
						pci_name(pdev));
					goto err_out;
				}
			}
		}

		/* upload a byte */
		outb_p(*(fw_ptr), io_base + REG_DATA_ADDR);
		state = inl_p(io_base + REG_LOADER_CTRL);
		if (state & FLG_ERR) {
			printk(KERN_ERR PFX 
				"%s: failed to write data at "
				"address 0x%04x\n",
				pci_name(pdev),
				(fw_ptr - (u8 *)fw_entry->data));
			goto err_out;
		}
		fw_ptr++;
		fw_len--;
	}

	/* set done bit */
	outl_p(FLG_DONE, io_base + REG_LOADER_CTRL);

	release_firmware(fw_entry);

	printk(KERN_INFO PFX "%s: firmware successfully loaded\n", 
		pci_name(pdev));

	return 0;

err_out:
	release_firmware(fw_entry);

	return -1;
}

static int __devinit tiumfwl_probe(struct pci_dev *pdev,
				  const struct pci_device_id *ent)
{
	static int tiumfwl_version_printed = 0;
	int err;

	if (tiumfwl_version_printed++ == 0)
		printk(KERN_INFO "%s", version);

	err = pci_enable_device(pdev);
	if (err) {
		printk(KERN_ERR PFX "Cannot enable PCI device, "
		       "aborting.\n");
		return err;
	}

	err = pci_request_regions(pdev, DRV_MODULE_NAME);
	if (err) {
		printk(KERN_ERR PFX "Cannot obtain PCI resources, "
		       "aborting.\n");
		return err;
	}

	err = tiumfwl_load_firmware(pdev);
	if (err) {
		printk(KERN_ERR PFX "Failed to load firmware, aborting.\n");
		return err;
	}

	return 0;
}

static void __devexit tiumfwl_release(struct pci_dev *pdev)
{
	pci_release_regions(pdev);
	pci_disable_device(pdev);
}

static struct pci_driver tiumfwl_driver = {
	.name		= DRV_MODULE_NAME,
	.id_table	= tiumfwl_pci_tbl,
	.probe		= tiumfwl_probe,
	.remove		= tiumfwl_release,
};

static int __init tiumfwl_init(void)
{
	return pci_module_init(&tiumfwl_driver);
}

static void __exit tiumfwl_cleanup(void)
{
	pci_unregister_driver(&tiumfwl_driver);
}

module_init(tiumfwl_init);
module_exit(tiumfwl_cleanup);
