/*
 * cel_voyager_platform.c - Celestica Voyager Platform Support.
 *
 * Copyright (C) 2017 Cumulus Networks, Inc.
 * Author: Scott Emery (scotte@cumulusnetworks.com)
 *
 * 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 program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 */

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <linux/err.h>
#include <linux/platform_device.h>
#include <linux/i2c/pca954x.h>
#include <linux/platform_data/pca953x.h>
#include <linux/platform_data/at24.h>
#include <linux/gpio.h>
#include <linux/cumulus-platform.h>

#include "platform_defs.h"
#include "platform_bitfield.h"

#define DRIVER_NAME	"cel_voyager_platform"
#define DRIVER_VERSION	"0.1"

/*
 * The platform has one CP2112 USB-to-I2C bridge, and possibly
 * other I2C buses that we don't care about.
 * The CP2112 has:
 *     - 2 pca9548 (8-port) I2C muxes (OK they're really switches), which go to
 *       the 12 QSFP28 front panel modules
 *     - a 24c64 (64k-bit) EEPROM.
 *
 * Because the pca9548s are connected in parallel, and their
 * down-stream devices (the QSFP28s) all have the same address,
 * they need to disable all ports when not used (the deselect_on_exit
 * option).
 */

enum {
	VGR_I2C_CP2112_BUS = -1,

	VGR_I2C_MUX1_BUS0 = 10,
	VGR_I2C_MUX1_BUS1,
	VGR_I2C_MUX1_BUS2,
	VGR_I2C_MUX1_BUS3,
	VGR_I2C_MUX1_BUS4,
	VGR_I2C_MUX1_BUS5,
	VGR_I2C_MUX1_BUS6,
	VGR_I2C_MUX1_BUS7,

	VGR_I2C_MUX2_BUS0 = 20,
	VGR_I2C_MUX2_BUS1,
	VGR_I2C_MUX2_BUS2,
	VGR_I2C_MUX2_BUS3,
	VGR_I2C_MUX2_BUS4,
	VGR_I2C_MUX2_BUS5,
	VGR_I2C_MUX2_BUS6,
	VGR_I2C_MUX2_BUS7,
};

/*
 * The list of I2C devices and their bus connections for this platform.
 *
 * Each entry is a bus number and a i2c_board_info.
 * The i2c_board_info specifies the device type, address,
 * and platform data depending on the device type.
 *
 * For muxes, we specify the bus number for each port,
 * and set the deselect_on_exit but (see comment above).
 *
 * For EEPROMs, including ones in the QSFP28 transceivers,
 * we specify the label, I2C address, size, and some flags.
 * All done in the magic mk*_eeprom() macros.  The label is
 * the string that ends up in /sys/class/eeprom_dev/eepromN/label,
 * which we use to identify them at user level.
 */

mk_pca9548(mux1, VGR_I2C_MUX1_BUS0, 1)
mk_pca9548(mux2, VGR_I2C_MUX2_BUS0, 1)

mk_port_eeprom(port1,  50, 256, AT24_FLAG_IRUGO)
mk_port_eeprom(port2,  50, 256, AT24_FLAG_IRUGO)
mk_port_eeprom(port3,  50, 256, AT24_FLAG_IRUGO)
mk_port_eeprom(port4,  50, 256, AT24_FLAG_IRUGO)
mk_port_eeprom(port5,  50, 256, AT24_FLAG_IRUGO)
mk_port_eeprom(port6,  50, 256, AT24_FLAG_IRUGO)
mk_port_eeprom(port7,  50, 256, AT24_FLAG_IRUGO)
mk_port_eeprom(port8,  50, 256, AT24_FLAG_IRUGO)
mk_port_eeprom(port9,  50, 256, AT24_FLAG_IRUGO)
mk_port_eeprom(port10, 50, 256, AT24_FLAG_IRUGO)
mk_port_eeprom(port11, 50, 256, AT24_FLAG_IRUGO)
mk_port_eeprom(port12, 50, 256, AT24_FLAG_IRUGO)

mk_eeprom(board, 53, 8192, AT24_FLAG_ADDR16 | AT24_FLAG_IRUGO)

static struct platform_i2c_device_info i2c_devices[] = {
	/* devices on CP2112 bus */
	mk_i2cdev(VGR_I2C_CP2112_BUS, "pca9548",  0x70, &mux1_platform_data),
	mk_i2cdev(VGR_I2C_CP2112_BUS, "pca9548",  0x71, &mux2_platform_data),
	mk_i2cdev(VGR_I2C_CP2112_BUS, "24c64",    0x53, &board_53_at24),
	mk_i2cdev(VGR_I2C_CP2112_BUS, "voy_qsfpcpld", 0x39, NULL),

	/* devices on pca9548 1 */
	mk_i2cdev(VGR_I2C_MUX1_BUS0, "24c02", 0x50, &port1_50_at24),
	mk_i2cdev(VGR_I2C_MUX1_BUS1, "24c02", 0x50, &port2_50_at24),
	mk_i2cdev(VGR_I2C_MUX1_BUS2, "24c02", 0x50, &port3_50_at24),
	mk_i2cdev(VGR_I2C_MUX1_BUS3, "24c02", 0x50, &port4_50_at24),
	mk_i2cdev(VGR_I2C_MUX1_BUS4, "24c02", 0x50, &port5_50_at24),
	mk_i2cdev(VGR_I2C_MUX1_BUS5, "24c02", 0x50, &port6_50_at24),
	mk_i2cdev(VGR_I2C_MUX1_BUS6, "24c02", 0x50, &port7_50_at24),
	mk_i2cdev(VGR_I2C_MUX1_BUS7, "24c02", 0x50, &port8_50_at24),

	/* devices on pca9548 2 */
	mk_i2cdev(VGR_I2C_MUX2_BUS0, "24c02", 0x50, &port9_50_at24),
	mk_i2cdev(VGR_I2C_MUX2_BUS1, "24c02", 0x50, &port10_50_at24),
	mk_i2cdev(VGR_I2C_MUX2_BUS2, "24c02", 0x50, &port11_50_at24),
	mk_i2cdev(VGR_I2C_MUX2_BUS3, "24c02", 0x50, &port12_50_at24),
};

/*
 * Utility functions for I2C
 */

static int init_i2c_devices(void)
{
	int cp2112_bus;
	int i;
	int ret;

	cp2112_bus = cumulus_i2c_find_adapter("CP2112 SMBus Bridge");
	if (cp2112_bus < 0) {
		pr_err(DRIVER_NAME ": could not find CP2112 adapter bus\n");
		ret = -ENODEV;
		goto err_exit;
	}

	for (i = 0; i < ARRAY_SIZE(i2c_devices); i++) {
		int bus = i2c_devices[i].bus;
		struct i2c_client *client;

		if (bus == VGR_I2C_CP2112_BUS)
			bus = cp2112_bus;
		client = cumulus_i2c_add_client(bus, &i2c_devices[i].board_info);
		if (IS_ERR(client)) {
			ret = PTR_ERR(client);
			goto err_exit;
		}
		i2c_devices[i].client = client;
	}
	return 0;

err_exit:
	return ret;
}

static void free_i2c_devices(void)
{
	int i;

	for (i = ARRAY_SIZE(i2c_devices); --i >= 0;) {
		struct i2c_client *c = i2c_devices[i].client;

		if (c) {
			i2c_devices[i].client = NULL;
			i2c_unregister_device(c);
		}
	}
}


/*
 * Module init and exit for all I2C devices
 */

static void free_all(void)
{
	free_i2c_devices();
}

static int __init celestica_voyager_i2c_init(void)
{
	int ret;

	ret = init_i2c_devices();
	if (ret)
		goto err_exit;
	return 0;

err_exit:
	free_all();
	return ret;
}

static void __exit celestica_voyager_i2c_exit(void)
{
	free_all();
}

/*
 * QSFP CPLD driver
 */

static inline int voy_qsfpcpld_read_reg(struct device *dev,
					int reg,
					int nregs,
					u32 *val)
{
	int ret;
	int retries = 17;
	int delay = 50;

	while (retries--) {
		ret = bf_i2c_read_reg(dev, reg, nregs, val);
		if (ret != -EAGAIN)
			break;
		usleep_range(delay*9/10, delay*11/10);
		delay = min(delay*2, 10000);
	}

	return ret;
}

static inline int voy_qsfpcpld_write_reg(struct device *dev,
					 int reg,
					 int nregs,
					 u32 val)
{
	int ret;
	int retries = 17;
	int delay = 50;

	while (retries--) {
		ret = bf_i2c_write_reg(dev, reg, nregs, val);
		if (ret != -EAGAIN)
			break;
		usleep_range(delay*9/10, delay*11/10);
		delay = min(delay*2, 10000);
	}

	return ret;
}

static const char * const cpld_rb[] = {
	"off", "red", "blue", "magenta"
};

mk_bf_ro(voy_qsfpcpld, cpld_revision,		0x01, 0, 6,  NULL, BF_DECIMAL)
mk_bf_ro(voy_qsfpcpld, cpld_released,		0x01, 6, 1,  NULL, 0)
mk_bf_ro(voy_qsfpcpld, cpld_subversion,		0x02, 0, 8,  NULL, BF_DECIMAL)
mk_bf_rw(voy_qsfpcpld, led_test_mode,		0x09, 0, 1,  NULL, 0)
mk_bf_rw(voy_qsfpcpld, led_test_color,		0x0a, 0, 2,  cpld_rb, BF_COMPLEMENT)
mk_bf_rw(voy_qsfpcpld, led_test_red,		0x0a, 0, 1,  NULL, BF_COMPLEMENT)
mk_bf_rw(voy_qsfpcpld, led_test_blue,		0x0a, 1, 1,  NULL, BF_COMPLEMENT)
mk_bf_rw(voy_qsfpcpld, qsfp_reset,		0x10, 0, 12, NULL, BF_COMPLEMENT)
mk_bf_rw(voy_qsfpcpld, qsfp_lpmode,		0x12, 0, 12, NULL, 0)
mk_bf_ro(voy_qsfpcpld, qsfp_interrupt,		0x14, 0, 12, NULL, BF_COMPLEMENT)
mk_bf_ro(voy_qsfpcpld, qsfp_present,		0x16, 0, 12, NULL, BF_COMPLEMENT)

struct attribute *qsfpcpld_attrs[] = {
	&voy_qsfpcpld_cpld_revision.attr,
	&voy_qsfpcpld_cpld_released.attr,
	&voy_qsfpcpld_cpld_subversion.attr,
	&voy_qsfpcpld_led_test_mode.attr,
	&voy_qsfpcpld_led_test_color.attr,
	&voy_qsfpcpld_led_test_red.attr,
	&voy_qsfpcpld_led_test_blue.attr,
	&voy_qsfpcpld_qsfp_reset.attr,
	&voy_qsfpcpld_qsfp_lpmode.attr,
	&voy_qsfpcpld_qsfp_interrupt.attr,
	&voy_qsfpcpld_qsfp_present.attr,
	NULL
};

static struct attribute_group qsfpcpld_attr_group = {
	.attrs = qsfpcpld_attrs,
};

static int qsfpcpld_probe(struct i2c_client *client,
			  const struct i2c_device_id *id)
{
	struct i2c_adapter *adapter = to_i2c_adapter(client->dev.parent);
	u32 revision;
	u32 subversion;
	int ret;

	if (!i2c_check_functionality(adapter, I2C_FUNC_SMBUS_BYTE_DATA)) {
		dev_err(&client->dev,
			"adapter does not support I2C_FUNC_SMBUS_BYTE_DATA\n");
		ret = -EINVAL;
		goto err;
	}

	/*
	 * Probe the hardware by reading the revision numbers.
	 */
	ret = voy_qsfpcpld_read_reg(&client->dev, 1, 1, &revision);
	if (ret < 0) {
		dev_err(&client->dev,
			"read CPLD revision register error %d\n", ret);
		goto err;
	}
	ret = voy_qsfpcpld_read_reg(&client->dev, 2, 1, &subversion);
	if (ret < 0) {
		dev_err(&client->dev,
			"read CPLD subversion register error %d\n", ret);
		goto err;
	}

	/*
	 * Create sysfs nodes.
	 */
	ret = sysfs_create_group(&client->dev.kobj, &qsfpcpld_attr_group);
	if (ret) {
		dev_err(&client->dev, "sysfs_create_group failed\n");
		goto err;
	}

	/*
	 * All clear from this point on
	 */
	dev_info(&client->dev,
		 "device created, QSFP CPLD rev %d %s subversion %d\n",
		 revision & 0x3f,
		 revision & 0x40 ? "released" : "unreleased",
		 subversion);

	return 0;

err:
	return ret;
}

static int qsfpcpld_remove(struct i2c_client *client)
{
	sysfs_remove_group(&client->dev.kobj, &qsfpcpld_attr_group);
	dev_info(&client->dev, "device removed\n");
	return 0;
}

static const struct i2c_device_id qsfpcpld_id[] = {
	{ "voy_qsfpcpld", 0 },
	{ }
};
MODULE_DEVICE_TABLE(i2c, qsfpcpld_id);

static struct i2c_driver qsfpcpld_driver = {
	.driver = {
		.name = "voy_qsfpcpld",
		.owner = THIS_MODULE,
	},
	.probe = qsfpcpld_probe,
	.remove = qsfpcpld_remove,
	.id_table = qsfpcpld_id,
};

static int __init cel_voyager_qsfpcpld_init(void)
{
	pr_info(DRIVER_NAME ": loading QSFP CPLD driver\n");
	return i2c_add_driver(&qsfpcpld_driver);
}

static void cel_voyager_qsfpcpld_exit(void)
{
	i2c_del_driver(&qsfpcpld_driver);
	pr_info(DRIVER_NAME ": QSFP CPLD driver unloaded\n");
}

/*
 * SYSCPLD driver
 */
#define SYSCPLD_BASE                    0x100

#define SYSCPLD_MDIO_REG_CMD_STA        0x61
#define SYSCPLD_MDIO_CMD_START          0x40
#define SYSCPLD_MDIO_CMD_OP_ADDR        0x00
#define SYSCPLD_MDIO_CMD_OP_WRITE       0x10
#define SYSCPLD_MDIO_CMD_OP_READ        0x30
#define SYSCPLD_MDIO_CMD_OP_PI_READ     0x20
#define SYSCPLD_MDIO_CMD_AC400_SEL1     0x00
#define SYSCPLD_MDIO_CMD_AC400_SEL2     0x04
#define SYSCPLD_MDIO_CMD_BUSY           0x02
#define SYSCPLD_MDIO_CMD_READ_VALID     0x01

#define SYSCPLD_MDIO_REG_DEV_TYPE       0x62
#define SYSCPLD_MDIO_DEV_TYPE_MASK      0x1F
#define SYSCPLD_MDIO_DEV_TYPE_PMD       0x01
#define SYSCPLD_MDIO_DEV_TYPE_WIS       0x02
#define SYSCPLD_MDIO_DEV_TYPE_PCS       0x03
#define SYSCPLD_MDIO_DEV_TYPE_PHY       0x04
#define SYSCPLD_MDIO_DEV_TYPE_DTE       0x05

#define SYSCPLD_MDIO_REG_PHY_ADDR       0x63
#define SYSCPLD_MDIO_REG_ADDR_15_8      0x64
#define SYSCPLD_MDIO_REG_ADDR_7_0       0x65
#define SYSCPLD_MDIO_REG_WD_15_8        0x66
#define SYSCPLD_MDIO_REG_WD_7_0         0x67
#define SYSCPLD_MDIO_REG_RD_15_8        0x68
#define SYSCPLD_MDIO_REG_RD_7_0         0x69

#define SYSCPLD_PWR_CTRL_REG            0x10
#define SYSCPLD_PWR_CTRL_AC400_1        0x10
#define SYSCPLD_PWR_CTRL_AC400_2        0x20

#define SYSCPLD_AC400_RST_CTRL_REG      0x52
#define SYSCPLD_AC400_RST_CTRL_1        0x01
#define SYSCPLD_AC400_RST_CTRL_2        0x02

#define SYSCPLD_AC400_1_CTRL_REG        0x50
#define SYSCPLD_AC400_2_CTRL_REG        0x51
#define SYSCPLD_AC400_CTRL_RXLOS1       0x80
#define SYSCPLD_AC400_CTRL_RXLOS0       0x40
#define SYSCPLD_AC400_CTRL_GLB_ALRM     0x20
#define SYSCPLD_AC400_CTRL_MOD_ABS      0x10
#define SYSCPLD_AC400_CTRL_TX_DIS1      0x08
#define SYSCPLD_AC400_CTRL_TX_DIS0      0x04
#define SYSCPLD_AC400_CTRL_MOD_LOPWR    0x02
#define SYSCPLD_AC400_CTRL_PM_SYNC      0x01


static int syscpld_read_reg(struct device *dev,
			    int reg,
			    int nregs,
			    u32 *val)
{
	int nbits = nregs * 8;
	int bit;

	reg += SYSCPLD_BASE;
	*val = 0;
	for (bit = 0; bit < nbits; bit += 8, reg++)
		*val |= inb(reg) << bit;

	return 0;
}

static int syscpld_write_reg(struct device *dev,
			     int reg,
			     int nregs,
			     u32 val)
{
	reg += SYSCPLD_BASE;
	for (; nregs > 0; nregs--, reg++, val >>= 8)
		outb(val, reg);

	return 0;
}


/*
    MDIO access. Write address to mdio_[1|2]_address and then read or write
    mdio_[1|2]_data.
*/
struct syscpld_device_data {
	struct platform_device *device;
	u16 mdio_address[2];
	int last_unit;
	u16 last_mdio_address[2];
	u16 last_mdio_data[2];
};

static bool ac400_mdio_wait(u8 set, u8 clear, int min, int max, int reps)
{
	u8 val;

	do {
		val = inb(SYSCPLD_BASE + SYSCPLD_MDIO_REG_CMD_STA);
		if (!(val & clear) && (val & set) == set)
			break;
		usleep_range(min, max);
	}
	while (--reps);
	return(!reps);
}

static u16 syscpld_mdio_read(int unit,
			     struct syscpld_device_data *ddata,
			     u16 addr,
			     u16 *value)
{
	u8 cmd;

	/* Set the phy address */
	if (ddata->last_unit != unit) {
		outb(unit, SYSCPLD_BASE + SYSCPLD_MDIO_REG_PHY_ADDR);
		ddata->last_unit = unit;
	}
	/* Set the MDIO address */
	if (ddata->last_mdio_address[unit] != addr) {
		outb(addr >> 8, SYSCPLD_BASE + SYSCPLD_MDIO_REG_ADDR_15_8);
		outb(addr & 0xFF, SYSCPLD_BASE + SYSCPLD_MDIO_REG_ADDR_7_0);
		/* Issue the command to set the address */
		cmd = SYSCPLD_MDIO_CMD_START | SYSCPLD_MDIO_CMD_OP_ADDR |
		      (unit ? SYSCPLD_MDIO_CMD_AC400_SEL2 : SYSCPLD_MDIO_CMD_AC400_SEL1);
		outb(cmd, SYSCPLD_BASE + SYSCPLD_MDIO_REG_CMD_STA);
		if (ac400_mdio_wait(0, SYSCPLD_MDIO_CMD_START | SYSCPLD_MDIO_CMD_BUSY,
				    250, 500, 20))
			return(-EIO);
		ddata->last_mdio_address[unit] = addr;
	}
	/* Initiate the MDIO read */
	cmd = SYSCPLD_MDIO_CMD_START | SYSCPLD_MDIO_CMD_OP_READ |
	      (unit ? SYSCPLD_MDIO_CMD_AC400_SEL2 : SYSCPLD_MDIO_CMD_AC400_SEL1);
	outb(cmd, SYSCPLD_BASE + SYSCPLD_MDIO_REG_CMD_STA);
	if (ac400_mdio_wait(SYSCPLD_MDIO_CMD_READ_VALID,
			    SYSCPLD_MDIO_CMD_START | SYSCPLD_MDIO_CMD_BUSY,
			    250, 500, 20))
		return(-EIO);
	/* Get the read data and return it */
	*value = (inb(SYSCPLD_BASE + SYSCPLD_MDIO_REG_RD_15_8) << 8) |
		 inb(SYSCPLD_BASE + SYSCPLD_MDIO_REG_RD_7_0);
	return 0;
}

static ssize_t syscpld_mdio_write(int unit,
				  struct syscpld_device_data *ddata,
				  u16 addr,
				  u16 value)
{

	int ret;
	u8 cmd;
	int reps;
	u16 rd_value;
	int delay;

	/* Set the phy address */
	if (ddata->last_unit != unit) {
		outb(unit, SYSCPLD_BASE + SYSCPLD_MDIO_REG_PHY_ADDR);
		ddata->last_unit = unit;
	}
	/* Set the MDIO address */
	if (ddata->last_mdio_address[unit] != addr) {
		outb(ddata->mdio_address[unit] >> 8, SYSCPLD_BASE + SYSCPLD_MDIO_REG_ADDR_15_8);
		outb(ddata->mdio_address[unit] & 0xFF, SYSCPLD_BASE + SYSCPLD_MDIO_REG_ADDR_7_0);
		/* Issue the command to set the address */
		cmd = SYSCPLD_MDIO_CMD_START | SYSCPLD_MDIO_CMD_OP_ADDR |
		      (unit ? SYSCPLD_MDIO_CMD_AC400_SEL2 : SYSCPLD_MDIO_CMD_AC400_SEL1);
		outb(cmd, SYSCPLD_BASE + SYSCPLD_MDIO_REG_CMD_STA);
		if (ac400_mdio_wait(0, SYSCPLD_MDIO_CMD_START | SYSCPLD_MDIO_CMD_BUSY,
				    250, 500, 20))
			return(-EIO);
		ddata->last_mdio_address[unit] = addr;
	}
	/* Set the write data */
	if (ddata->last_mdio_data[unit] != value) {
		outb(value >> 8, SYSCPLD_BASE + SYSCPLD_MDIO_REG_WD_15_8);
		outb(value & 0xFF, SYSCPLD_BASE + SYSCPLD_MDIO_REG_WD_7_0);
		ddata->last_mdio_data[unit] = value;
	}
	/* Initiate the MDIO write */
	cmd = SYSCPLD_MDIO_CMD_START | SYSCPLD_MDIO_CMD_OP_WRITE |
	      (unit ? SYSCPLD_MDIO_CMD_AC400_SEL2 : SYSCPLD_MDIO_CMD_AC400_SEL1);
	outb(cmd, SYSCPLD_BASE + SYSCPLD_MDIO_REG_CMD_STA);
	if (ac400_mdio_wait(0, SYSCPLD_MDIO_CMD_START | SYSCPLD_MDIO_CMD_BUSY,
			    250, 500, 20))
		return(-EIO);

	/* Wait for the write to complete */
	delay = 50;
	reps = 373;
	do {
		ret = syscpld_mdio_read(unit, ddata, 0xb050, &rd_value);
		if (ret)
			return ret;
		if (rd_value & 0xc000)
			break;
		usleep_range(delay, delay+250);
		delay = min(delay*2, 500000);
	}
	while (--reps);
	if (!reps || (rd_value & 0x4000))
		return -EIO;

	return 0;
}


static ssize_t syscpld_mdio_address_store(int unit,
					  struct device *dev,
					  struct device_attribute *dattr,
					  const char *buf,
					  size_t size)
{
	u32 newval;
	int ret;
	struct syscpld_device_data *ddata = dev_get_drvdata(dev);

	ret = kstrtou32(buf, 0, &newval);
	if (ret)
		return ret;
	if (newval > 0xFFFF)
		return -EINVAL;
	ddata->mdio_address[unit] = newval;
	return size;
}

static ssize_t syscpld_mdio_1_address_store(struct device *dev,
					    struct device_attribute *dattr,
					    const char *buf,
					    size_t size)
{
	return syscpld_mdio_address_store(0, dev, dattr, buf, size);
}

static ssize_t syscpld_mdio_2_address_store(struct device *dev,
					    struct device_attribute *dattr,
					    const char *buf,
					    size_t size)
{
	return syscpld_mdio_address_store(1, dev, dattr, buf, size);
}

static ssize_t syscpld_mdio_address_show(int unit,
					 struct device *dev,
					 struct device_attribute *dattr,
					 char *buf)
{
	struct syscpld_device_data *ddata = dev_get_drvdata(dev);
	return sprintf(buf, "0x%04x\n", ddata->mdio_address[unit]);
}

static ssize_t syscpld_mdio_1_address_show(struct device *dev,
					   struct device_attribute *dattr,
					   char *buf)
{
	return syscpld_mdio_address_show(0, dev, dattr, buf);
}

static ssize_t syscpld_mdio_2_address_show(struct device *dev,
					   struct device_attribute *dattr,
					   char *buf)
{
	return syscpld_mdio_address_show(1, dev, dattr, buf);
}

static struct device_attribute syscpld_mdio_1_address =
	__ATTR(mdio_1_address, S_IRUGO | S_IWUSR,
	       syscpld_mdio_1_address_show, syscpld_mdio_1_address_store);

static struct device_attribute syscpld_mdio_2_address =
	__ATTR(mdio_2_address, S_IRUGO | S_IWUSR,
	       syscpld_mdio_2_address_show, syscpld_mdio_2_address_store);

static ssize_t syscpld_mdio_data_store(int unit,
				       struct device *dev,
				       struct device_attribute *dattr,
				       const char *buf,
				       size_t size)
{

	u32 newval;
	int ret;
	struct syscpld_device_data *ddata = dev_get_drvdata(dev);

	ret = kstrtou32(buf, 0, &newval);
	if (ret)
		return ret;
	if (newval > 0xFFFF)
		return -EINVAL;

	ret = syscpld_mdio_write(unit, ddata, ddata->mdio_address[unit], newval);
	if (ret)
		return ret;
	return size;
}

static ssize_t syscpld_mdio_1_data_store(struct device *dev,
					 struct device_attribute *dattr,
					 const char *buf,
					 size_t size)
{
	return syscpld_mdio_data_store(0, dev, dattr, buf, size);
}

static ssize_t syscpld_mdio_2_data_store(struct device *dev,
					 struct device_attribute *dattr,
					 const char *buf,
					 size_t size)
{
	return syscpld_mdio_data_store(1, dev, dattr, buf, size);
}

static ssize_t syscpld_mdio_data_show(int unit,
				      struct device *dev,
				      struct device_attribute *dattr,
				      char *buf)
{
	u16 value;
	int ret;
	struct syscpld_device_data *ddata = dev_get_drvdata(dev);

	ret = syscpld_mdio_read(unit, ddata, ddata->mdio_address[unit], &value);
	if (ret) {
		return 0;
	}
	return sprintf(buf, "0x%04x\n", value);
}

static ssize_t syscpld_mdio_1_data_show(struct device *dev,
					struct device_attribute *dattr,
					char *buf)
{
	return syscpld_mdio_data_show(0, dev, dattr, buf);

}

static ssize_t syscpld_mdio_2_data_show(struct device *dev,
					struct device_attribute *dattr,
					char *buf)
{
	return syscpld_mdio_data_show(1, dev, dattr, buf);

}

static struct device_attribute syscpld_mdio_1_data =
	__ATTR(mdio_1_data, S_IRUGO | S_IWUSR,
	       syscpld_mdio_1_data_show, syscpld_mdio_1_data_store);

static struct device_attribute syscpld_mdio_2_data =
	__ATTR(mdio_2_data, S_IRUGO | S_IWUSR,
	       syscpld_mdio_2_data_show, syscpld_mdio_2_data_store);


static const char * const syscpld_led_system_values[] = {
	PLATFORM_LED_OFF,
	PLATFORM_LED_RED,
	PLATFORM_LED_GREEN,
	PLATFORM_LED_YELLOW,
	PLATFORM_LED_BLUE,
	"magenta",
	"cyan",
	"white",
	PLATFORM_LED_OFF,
	PLATFORM_LED_RED_BLINKING,
	PLATFORM_LED_GREEN_BLINKING,
	PLATFORM_LED_YELLOW_BLINKING,
	PLATFORM_LED_BLUE_BLINKING,
	"magenta_blinking",
	"cyan_blinking",
	"white_blinking",
};

static const char * const syscpld_rov_values[] = {
	"1.2000",
	"1.1750",
	"1.1500",
	"1.2500",
	"1.1000",
	"1.0750",
	"1.0500",
	"1.0250",
	"1.0000",
	"0.9250",
	"0.9500",
	"0.9250",
	"0.9000",
	"0.8750",
	"0.8500",
	"0.8250",
};

mk_bf_ro(syscpld, board_revision,          0x00, 0, 3, NULL, BF_DECIMAL)
mk_bf_ro(syscpld, cpld_revision,           0x01, 0, 6, NULL, BF_DECIMAL)
mk_bf_ro(syscpld, cpld_released,           0x01, 6, 1, NULL, 0)
mk_bf_ro(syscpld, cpld_subversion,         0x02, 0, 8, NULL, BF_DECIMAL)
mk_bf_ro(syscpld, come_board_rev,          0x03, 0, 3, NULL, BF_DECIMAL)
mk_bf_rw(syscpld, eeprom_wp_bmc_phy,       0x04, 6, 1, NULL, 0)
mk_bf_rw(syscpld, eeprom_wp_gbe_phy,       0x04, 5, 1, NULL, 0)
mk_bf_rw(syscpld, eeprom_wp_come_phy,      0x04, 4, 1, NULL, 0)
mk_bf_rw(syscpld, eeprom_wp_board_id,      0x04, 3, 1, NULL, 0)
mk_bf_rw(syscpld, eeprom_wp_sys_cpld_spi,  0x04, 2, 1, NULL, 0)
mk_bf_rw(syscpld, eeprom_wp_cpld_sec_boot, 0x04, 1, 1, NULL, 0)
mk_bf_rw(syscpld, eeprom_wp_cpld_boot_fl0, 0x04, 0, 1, NULL, 0)
mk_bf_rw(syscpld, system_led2,             0x06, 4, 4, syscpld_led_system_values, 0)
mk_bf_rw(syscpld, system_led1,             0x06, 0, 4, syscpld_led_system_values, 0)
mk_bf_ro(syscpld, lm75_tover,              0x0b, 0, 1, NULL, 0)
mk_bf_rw(syscpld, hw_shutdown_recorded,    0x0c, 0, 8, NULL, 0)
mk_bf_rw(syscpld, pwr_ac400_2,             0x10, 5, 1, NULL, 0)
mk_bf_rw(syscpld, pwr_ac400_1,             0x10, 4, 1, NULL, 0)
mk_bf_rw(syscpld, pwr_cyc_come,            0x10, 3, 1, NULL, BF_COMPLEMENT)
mk_bf_rw(syscpld, pwr_come,                0x10, 2, 1, NULL, 0)
mk_bf_rw(syscpld, pwr_cyc_main,            0x10, 1, 1, NULL, BF_COMPLEMENT)
mk_bf_rw(syscpld, pwr_main,                0x10, 0, 1, NULL, 0)
mk_bf_rw(syscpld, reset_bmc,               0x11, 5, 1, NULL, BF_COMPLEMENT)
mk_bf_rw(syscpld, reset_come,              0x11, 4, 1, NULL, BF_COMPLEMENT)
mk_bf_rw(syscpld, reset_thawk_pcie_mask,   0x11, 3, 1, NULL, 0)
mk_bf_rw(syscpld, reset_thawk_sys_mask,    0x11, 2, 1, NULL, 0)
mk_bf_rw(syscpld, reset_thawk_pcie,        0x11, 1, 1, NULL, BF_COMPLEMENT)
mk_bf_rw(syscpld, reset_thawk_sys,         0x11, 0, 1, NULL, BF_COMPLEMENT)
mk_bf_ro(syscpld, suspend_status,          0x12, 3, 1, NULL, BF_COMPLEMENT)
mk_bf_ro(syscpld, suspend_s5_status,       0x12, 2, 1, NULL, BF_COMPLEMENT)
mk_bf_ro(syscpld, suspend_s4_status,       0x12, 1, 1, NULL, BF_COMPLEMENT)
mk_bf_ro(syscpld, suspend_s3_status,       0x12, 0, 1, NULL, BF_COMPLEMENT)
mk_bf_rw(syscpld, reset_timing_card,       0x14, 7, 1, NULL, BF_COMPLEMENT)
mk_bf_rw(syscpld, reset_fan_card,          0x14, 6, 1, NULL, BF_COMPLEMENT)
mk_bf_rw(syscpld, reset_gbe_phy,           0x14, 5, 1, NULL, BF_COMPLEMENT)
mk_bf_rw(syscpld, reset_bmc_phy,           0x14, 4, 1, NULL, BF_COMPLEMENT)
mk_bf_rw(syscpld, reset_come_phy,          0x14, 3, 1, NULL, BF_COMPLEMENT)
mk_bf_rw(syscpld, reset_5387_sw,           0x14, 2, 1, NULL, BF_COMPLEMENT)
mk_bf_rw(syscpld, reset_usb_hub,           0x14, 1, 1, NULL, BF_COMPLEMENT)
mk_bf_rw(syscpld, reset_cp2112,            0x14, 0, 1, NULL, BF_COMPLEMENT)
mk_bf_rw(syscpld, reset_xc7a200t,          0x15, 1, 1, NULL, BF_COMPLEMENT)
mk_bf_rw(syscpld, reset_eeprom_i2c_mux,    0x15, 0, 1, NULL, BF_COMPLEMENT)
mk_bf_rw(syscpld, bmc_reset_type,          0x22, 0, 8, NULL, 0)
mk_bf_rw(syscpld, uart_mux,                0x30, 2, 2, NULL, 0)
mk_bf_rw(syscpld, thawk_uart_mux,          0x30, 0, 2, NULL, 0)
mk_bf_ro(syscpld, thawk_rov,               0x40, 0, 4, syscpld_rov_values, 0)
mk_bf_ro(syscpld, ir3584_over_temp,        0x41, 3, 1, NULL, 0)
mk_bf_ro(syscpld, ir3584_power_ok,         0x41, 2, 1, NULL, 0)
mk_bf_ro(syscpld, ir3581_over_temp,        0x41, 1, 1, NULL, 0)
mk_bf_ro(syscpld, ir3581_power_ok,         0x41, 0, 1, NULL, 0)
mk_bf_ro(syscpld, psu_pwr2_ac_ok,          0x42, 7, 1, NULL, 0)
mk_bf_ro(syscpld, psu_pwr2_present,        0x42, 6, 1, NULL, BF_COMPLEMENT)
mk_bf_ro(syscpld, psu_pwr2_dc_ok,          0x42, 5, 1, NULL, 0)
mk_bf_ro(syscpld, psu_pwr2_alarm,          0x42, 4, 1, NULL, 0)
mk_bf_ro(syscpld, psu_pwr1_ac_ok,          0x42, 3, 1, NULL, 0)
mk_bf_ro(syscpld, psu_pwr1_present,        0x42, 2, 1, NULL, BF_COMPLEMENT)
mk_bf_ro(syscpld, psu_pwr1_dc_ok,          0x42, 1, 1, NULL, 0)
mk_bf_ro(syscpld, psu_pwr1_alarm,          0x42, 0, 1, NULL, 0)
mk_bf_ro(syscpld, ac400_1_rxlos1,          0x50, 7, 1, NULL, 0)
mk_bf_ro(syscpld, ac400_1_rxlos0,          0x50, 6, 1, NULL, 0)
mk_bf_ro(syscpld, ac400_1_glb_alarm,       0x50, 5, 1, NULL, BF_COMPLEMENT)
mk_bf_ro(syscpld, ac400_1_mod_absent,      0x50, 4, 1, NULL, 0)
mk_bf_rw(syscpld, ac400_1_tx_disable1,     0x50, 3, 1, NULL, 0)
mk_bf_rw(syscpld, ac400_1_tx_disable0,     0x50, 2, 1, NULL, 0)
mk_bf_rw(syscpld, ac400_1_mod_lopwr,       0x50, 1, 1, NULL, 0)
mk_bf_rw(syscpld, ac400_1_pm_sync,         0x50, 0, 1, NULL, 0)
mk_bf_ro(syscpld, ac400_2_rxlos1,          0x51, 7, 1, NULL, 0)
mk_bf_ro(syscpld, ac400_2_rxlos0,          0x51, 6, 1, NULL, 0)
mk_bf_ro(syscpld, ac400_2_glb_alarm,       0x51, 5, 1, NULL, BF_COMPLEMENT)
mk_bf_ro(syscpld, ac400_2_mod_absent,      0x51, 4, 1, NULL, 0)
mk_bf_rw(syscpld, ac400_2_tx_disable1,     0x51, 3, 1, NULL, 0)
mk_bf_rw(syscpld, ac400_2_tx_disable0,     0x51, 2, 1, NULL, 0)
mk_bf_rw(syscpld, ac400_2_mod_lopwr,       0x51, 1, 1, NULL, 0)
mk_bf_rw(syscpld, ac400_2_pm_sync,         0x51, 0, 1, NULL, 0)
mk_bf_rw(syscpld, reset_ac400_2,           0x52, 1, 1, NULL, BF_COMPLEMENT)
mk_bf_rw(syscpld, reset_ac400_1,           0x52, 0, 1, NULL, BF_COMPLEMENT)
mk_bf_rw(syscpld, mdio_clock_prescale,     0x60, 0, 8, NULL, 0)
mk_bf_rw(syscpld, mdio_command_status,     0x61, 0, 8, NULL, 0)
mk_bf_rw(syscpld, mdio_device_type,        0x62, 0, 5, NULL, 0)
mk_bf_rw(syscpld, mdio_phy_addr,           0x63, 0, 5, NULL, 0)
mk_bf_rw(syscpld, mdio_reg_addr_15_8,      0x64, 0, 8, NULL, 0)
mk_bf_rw(syscpld, mdio_reg_addr_7_0,       0x65, 0, 8, NULL, 0)
mk_bf_rw(syscpld, mdio_wr_data_15_8,       0x66, 0, 8, NULL, 0)
mk_bf_rw(syscpld, mdio_wr_data_7_0,        0x67, 0, 8, NULL, 0)
mk_bf_ro(syscpld, mdio_rd_data_15_8,       0x68, 0, 8, NULL, 0)
mk_bf_ro(syscpld, mdio_rd_data_7_0,        0x69, 0, 8, NULL, 0)
mk_bf_rw(syscpld, reset_qsfp_2_mux,        0x78, 2, 1, NULL, BF_COMPLEMENT)
mk_bf_rw(syscpld, reset_qsfp_1_mux,        0x78, 1, 1, NULL, BF_COMPLEMENT)
mk_bf_rw(syscpld, reset_qsfp_cpld,         0x78, 0, 1, NULL, BF_COMPLEMENT)
mk_bf_ro(syscpld, qsfp_cpld_int,           0x79, 0, 1, NULL, 0)

/* Temporary attributes to initialze an AC400 module */
static ssize_t syscpld_ac400_init_store(int unit,
					struct device *dev,
					struct device_attribute *dattr,
					const char *buf,
					size_t size)
{
	int reps;
	char alarmStr[512];
	struct syscpld_device_data *ddata = dev_get_drvdata(dev);

	if (!unit) {
		/* Power on the AC400 and assert reset, low power, and txdis */
		syscpld_pwr_ac400_1_store(dev, dattr, "1", strlen("1"));
		syscpld_reset_ac400_1_store(dev, dattr, "1", strlen("1"));
		syscpld_ac400_1_mod_lopwr_store(dev, dattr, "1", strlen("1"));
		syscpld_ac400_1_tx_disable0_store(dev, dattr, "1", strlen("1"));
		syscpld_ac400_1_tx_disable1_store(dev, dattr, "1", strlen("1"));

		/* Take the module out of reset */
		syscpld_reset_ac400_1_store(dev, dattr, "0", strlen("0"));

		/* Wait until global alarm is asserted (reset complete) */
		reps = 75;
		do {
			syscpld_ac400_1_glb_alarm_show(dev, dattr, alarmStr);
			if (!strcmp(alarmStr, "1\n"))
				break;
			msleep(200);
		}
		while (--reps);
		if (!reps)
			return -EIO;
	}
	else {
		/* Power on the AC400 and assert reset, low power, and txdis */
		syscpld_pwr_ac400_2_store(dev, dattr, "1", strlen("1"));
		syscpld_reset_ac400_2_store(dev, dattr, "1", strlen("1"));
		syscpld_ac400_2_mod_lopwr_store(dev, dattr, "1", strlen("1"));
		syscpld_ac400_2_tx_disable0_store(dev, dattr, "1", strlen("1"));
		syscpld_ac400_2_tx_disable1_store(dev, dattr, "1", strlen("1"));

		/* Take the module out of reset */
		syscpld_reset_ac400_2_store(dev, dattr, "0", strlen("0"));

		/* Wait until global alarm is asserted (reset complete) */
		reps = 75;
		do {
			syscpld_ac400_2_glb_alarm_show(dev, dattr, alarmStr);
			if (!strcmp(alarmStr, "1\n"))
				break;
			msleep(200);
		}
		while (--reps);
		if (!reps)
			return -EIO;
	}
	/* Clear out the cached values */
	ddata->mdio_address[unit] = 0;
	ddata->last_unit = -1;
	ddata->last_mdio_address[unit] = 0;
	ddata->last_mdio_data[unit] = 0;

	/* Set the MDIO Device Type*/
	syscpld_mdio_device_type_store(dev, dattr, "1", strlen("1"));
	return size;
}

static ssize_t syscpld_ac400_1_init_store(struct device *dev,
					  struct device_attribute *dattr,
					  const char *buf,
					  size_t size)
{
	return syscpld_ac400_init_store(0, dev, dattr, buf, size);
}

static ssize_t syscpld_ac400_2_init_store(struct device *dev,
					  struct device_attribute *dattr,
					  const char *buf,
					  size_t size)
{
	return syscpld_ac400_init_store(1, dev, dattr, buf, size);
}

static struct device_attribute syscpld_ac400_1_init =
	__ATTR(ac400_1_init, S_IWUSR,
	       NULL, syscpld_ac400_1_init_store);

static struct device_attribute syscpld_ac400_2_init =
	__ATTR(ac400_2_init, S_IWUSR,
	       NULL, syscpld_ac400_2_init_store);

struct attribute *syscpld_attrs[] = {
	&syscpld_board_revision.attr,
	&syscpld_cpld_revision.attr,
	&syscpld_cpld_released.attr,
	&syscpld_cpld_subversion.attr,
	&syscpld_come_board_rev.attr,
	&syscpld_eeprom_wp_bmc_phy.attr,
	&syscpld_eeprom_wp_gbe_phy.attr,
	&syscpld_eeprom_wp_come_phy.attr,
	&syscpld_eeprom_wp_board_id.attr,
	&syscpld_eeprom_wp_sys_cpld_spi.attr,
	&syscpld_eeprom_wp_cpld_sec_boot.attr,
	&syscpld_eeprom_wp_cpld_boot_fl0.attr,
	&syscpld_system_led2.attr,
	&syscpld_system_led1.attr,
	&syscpld_lm75_tover.attr,
	&syscpld_hw_shutdown_recorded.attr,
	&syscpld_pwr_ac400_2.attr,
	&syscpld_pwr_ac400_1.attr,
	&syscpld_pwr_cyc_come.attr,
	&syscpld_pwr_come.attr,
	&syscpld_pwr_cyc_main.attr,
	&syscpld_pwr_main.attr,
	&syscpld_reset_bmc.attr,
	&syscpld_reset_come.attr,
	&syscpld_reset_thawk_pcie_mask.attr,
	&syscpld_reset_thawk_sys_mask.attr,
	&syscpld_reset_thawk_pcie.attr,
	&syscpld_reset_thawk_sys.attr,
	&syscpld_suspend_status.attr,
	&syscpld_suspend_s5_status.attr,
	&syscpld_suspend_s4_status.attr,
	&syscpld_suspend_s3_status.attr,
	&syscpld_reset_timing_card.attr,
	&syscpld_reset_fan_card.attr,
	&syscpld_reset_gbe_phy.attr,
	&syscpld_reset_bmc_phy.attr,
	&syscpld_reset_come_phy.attr,
	&syscpld_reset_5387_sw.attr,
	&syscpld_reset_usb_hub.attr,
	&syscpld_reset_cp2112.attr,
	&syscpld_reset_xc7a200t.attr,
	&syscpld_reset_eeprom_i2c_mux.attr,
	&syscpld_bmc_reset_type.attr,
	&syscpld_uart_mux.attr,
	&syscpld_thawk_uart_mux.attr,
	&syscpld_thawk_rov.attr,
	&syscpld_ir3584_over_temp.attr,
	&syscpld_ir3584_power_ok.attr,
	&syscpld_ir3581_over_temp.attr,
	&syscpld_ir3581_power_ok.attr,
	&syscpld_psu_pwr2_ac_ok.attr,
	&syscpld_psu_pwr2_present.attr,
	&syscpld_psu_pwr2_dc_ok.attr,
	&syscpld_psu_pwr2_alarm.attr,
	&syscpld_psu_pwr1_ac_ok.attr,
	&syscpld_psu_pwr1_present.attr,
	&syscpld_psu_pwr1_dc_ok.attr,
	&syscpld_psu_pwr1_alarm.attr,
	&syscpld_ac400_1_rxlos1.attr,
	&syscpld_ac400_1_rxlos0.attr,
	&syscpld_ac400_1_glb_alarm.attr,
	&syscpld_ac400_1_mod_absent.attr,
	&syscpld_ac400_1_tx_disable1.attr,
	&syscpld_ac400_1_tx_disable0.attr,
	&syscpld_ac400_1_mod_lopwr.attr,
	&syscpld_ac400_1_pm_sync.attr,
	&syscpld_ac400_2_rxlos1.attr,
	&syscpld_ac400_2_rxlos0.attr,
	&syscpld_ac400_2_glb_alarm.attr,
	&syscpld_ac400_2_mod_absent.attr,
	&syscpld_ac400_2_tx_disable1.attr,
	&syscpld_ac400_2_tx_disable0.attr,
	&syscpld_ac400_2_mod_lopwr.attr,
	&syscpld_ac400_2_pm_sync.attr,
	&syscpld_reset_ac400_2.attr,
	&syscpld_reset_ac400_1.attr,
	&syscpld_mdio_clock_prescale.attr,
	&syscpld_mdio_command_status.attr,
	&syscpld_mdio_device_type.attr,
	&syscpld_mdio_phy_addr.attr,
	&syscpld_mdio_reg_addr_15_8.attr,
	&syscpld_mdio_reg_addr_7_0.attr,
	&syscpld_mdio_wr_data_15_8.attr,
	&syscpld_mdio_wr_data_7_0.attr,
	&syscpld_mdio_rd_data_15_8.attr,
	&syscpld_mdio_rd_data_7_0.attr,
	&syscpld_reset_qsfp_2_mux.attr,
	&syscpld_reset_qsfp_1_mux.attr,
	&syscpld_reset_qsfp_cpld.attr,
	&syscpld_qsfp_cpld_int.attr,
	&syscpld_mdio_1_address.attr,
	&syscpld_mdio_2_address.attr,
	&syscpld_mdio_1_data.attr,
	&syscpld_mdio_2_data.attr,
	&syscpld_ac400_1_init.attr,
	&syscpld_ac400_2_init.attr,
	NULL
};

static struct attribute_group syscpld_attr_group = {
	.attrs = syscpld_attrs,
};

static int syscpld_probe(struct platform_device *dev)
{
	u8 boardrev;
	u8 cpldrev;
	int ret;

	boardrev = 0;
	cpldrev = 0;

	/*
	 * Probe the hardware by reading the revision numbers.
	 * OK, This is not really a good test.
	 */
	boardrev = inb(SYSCPLD_BASE + 0);
	cpldrev = inb(SYSCPLD_BASE + 1);

	/*
	 * Create sysfs nodes.
	 */
	ret = sysfs_create_group(&dev->dev.kobj, &syscpld_attr_group);
	if (ret) {
		dev_err(&dev->dev, "sysfs_create_group failed\n");
		return ret;
	}

	/*
	 * All clear from this point on
	 */
	dev_info(&dev->dev,
		 "device created, board rev %d system CPLD rev %d %s\n",
		 boardrev & 0x07,
		 cpldrev & 0x3f,
		 cpldrev & 0x40 ? "released" : "unreleased");

	return 0;
}

static int syscpld_remove(struct platform_device *dev)
{
	sysfs_remove_group(&dev->dev.kobj, &syscpld_attr_group);
	dev_info(&dev->dev, "device removed\n");
	return 0;
}

static struct platform_driver syscpld_driver = {
	.driver = {
		.name = "syscpld",
		.owner = THIS_MODULE,
	},
	.probe = syscpld_probe,
	.remove = syscpld_remove,
};

static struct syscpld_device_data syscpld_ddata = {
	.last_unit = -1,
};

static int __init cel_voyager_syscpld_init(void)
{
	int ret;
	struct platform_device *syscpld_device;

	pr_info(DRIVER_NAME ": loading system CPLD driver\n");

	ret = platform_driver_register(&syscpld_driver);
	if (ret)
		return ret;

	syscpld_device = platform_device_alloc("syscpld", -1);
	if (!syscpld_device) {
		platform_driver_unregister(&syscpld_driver);
		return -ENOMEM;
	}
	syscpld_ddata.device = syscpld_device;
	dev_set_drvdata(&syscpld_device->dev, &syscpld_ddata);
	ret = platform_device_add(syscpld_device);
	if (ret) {
		platform_device_put(syscpld_device);
		platform_driver_unregister(&syscpld_driver);
		return ret;
	}

	return 0;
}

static void cel_voyager_syscpld_exit(void)
{
	struct platform_device *dev = syscpld_ddata.device;

	syscpld_ddata.device = NULL;
	platform_device_unregister(dev);
	platform_driver_unregister(&syscpld_driver);
	pr_info(DRIVER_NAME ": system CPLD driver unloaded\n");
}

/*
 * Module init and exit
 */

static int __init celestica_voyager_init(void)
{
	int ret;

	/*
	 * Initialize the QSFP CPLD driver first, because it's an I2C
	 * device and will get instantiated along with the other
	 * I2C devices in celestica_voyager_i2c_init().
	 */
	ret = cel_voyager_qsfpcpld_init();
	if (ret) {
		pr_err(DRIVER_NAME ": QSFP CPLD initialization failed\n");
		return ret;
	}

	ret = celestica_voyager_i2c_init();
	if (ret) {
		pr_err(DRIVER_NAME ": I2C initialization failed\n");
		cel_voyager_qsfpcpld_exit();
		return ret;
	}

	ret = cel_voyager_syscpld_init();
	if (ret) {
		pr_err(DRIVER_NAME ": CPLD initialization failed\n");
		celestica_voyager_i2c_exit();
		cel_voyager_qsfpcpld_exit();
		return ret;
	}

	pr_info(DRIVER_NAME ": version " DRIVER_VERSION
		" successfully loaded\n");
	return 0;
}

static void __exit celestica_voyager_exit(void)
{
	cel_voyager_syscpld_exit();
	celestica_voyager_i2c_exit();
	cel_voyager_qsfpcpld_exit();
	pr_info(DRIVER_NAME " driver unloaded\n");
}

module_init(celestica_voyager_init);
module_exit(celestica_voyager_exit);

MODULE_AUTHOR("Scott Emery (scotte@cumulusnetworks.com)");
MODULE_DESCRIPTION("Celestica Voyager Platform Support");
MODULE_VERSION(DRIVER_VERSION);
MODULE_LICENSE("GPL");
