[RFC PATCH] picoxcell_fuse: add support for the picoXcell fuse block

Jamie Iles jamie at jamieiles.com
Mon Feb 7 10:26:30 EST 2011


Picochip picoXcell devices contain a fuse block that controls
aspects of the device such as disabling JTAG, disabling the ARM
memory controller, secure booting and storing secure keys. This
driver provides a character device to read and write the fuse
values and exports the fuse ranges to sysfs.

Platforms should add a struct picoxcell_fuse_map to the platform
device platform_data defining all of the fuse ranges and protection
bits.

Signed-off-by: Jamie Iles <jamie at jamieiles.com>
---
 drivers/misc/Kconfig                         |   10 +
 drivers/misc/Makefile                        |    1 +
 drivers/misc/picoxcell_fuse.c                |  780 ++++++++++++++++++++++++++
 include/linux/platform_data/picoxcell_fuse.h |  104 ++++
 4 files changed, 895 insertions(+), 0 deletions(-)
 create mode 100644 drivers/misc/picoxcell_fuse.c
 create mode 100644 include/linux/platform_data/picoxcell_fuse.h

diff --git a/drivers/misc/Kconfig b/drivers/misc/Kconfig
index b7d5ef2..b31bdc3 100644
--- a/drivers/misc/Kconfig
+++ b/drivers/misc/Kconfig
@@ -457,6 +457,16 @@ config PCH_PHUB
 	  To compile this driver as a module, choose M here: the module will
 	  be called pch_phub.
 
+config PICOXCELL_FUSE
+	tristate "Picochip picoXcell fuse driver"
+	depends on ARCH_PICOXCELL
+	help
+	  This driver enables a sysfs and character device interface to access
+	  the efuse block in picoXcell devices.
+
+	  To compile this driver as a module, choose M here: the module will be
+	  called picoxcell_fuse.
+
 source "drivers/misc/c2port/Kconfig"
 source "drivers/misc/eeprom/Kconfig"
 source "drivers/misc/cb710/Kconfig"
diff --git a/drivers/misc/Makefile b/drivers/misc/Makefile
index 98009cc..c096bbf 100644
--- a/drivers/misc/Makefile
+++ b/drivers/misc/Makefile
@@ -42,3 +42,4 @@ obj-$(CONFIG_ARM_CHARLCD)	+= arm-charlcd.o
 obj-$(CONFIG_PCH_PHUB)		+= pch_phub.o
 obj-y				+= ti-st/
 obj-$(CONFIG_AB8500_PWM)	+= ab8500-pwm.o
+obj-$(CONFIG_PICOXCELL_FUSE)	+= picoxcell_fuse.o
diff --git a/drivers/misc/picoxcell_fuse.c b/drivers/misc/picoxcell_fuse.c
new file mode 100644
index 0000000..6c468ff
--- /dev/null
+++ b/drivers/misc/picoxcell_fuse.c
@@ -0,0 +1,780 @@
+/*
+ * Copyright (c) 2010 Picochip Ltd., Jamie Iles
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+#undef DEBUG
+#define pr_fmt(fmt) "picoxcell_fuse: " fmt
+#include <linux/clk.h>
+#include <linux/ctype.h>
+#include <linux/delay.h>
+#include <linux/fs.h>
+#include <linux/io.h>
+#include <linux/module.h>
+#include <linux/miscdevice.h>
+#include <linux/mutex.h>
+#include <linux/platform_device.h>
+#include <linux/slab.h>
+#include <linux/uaccess.h>
+#include <linux/platform_data/picoxcell_fuse.h>
+
+#define PICOXCELL_FUSE_PROG_TIME_USEC	20
+
+static int test_mode;
+module_param(test_mode, bool, 0600);
+MODULE_PARM_DESC(test_mode, "Enable test mode to allow prototyping without actually blowing fuses (0=use hardware, 1=use test mode)");
+
+/*
+ * A note on reading fuses: some of the fuses such as the keys and customer
+ * partitions have read once per boot bits and these allow each word in that
+ * region to be read once. Subsequent reads of the word will return undefined
+ * data. So if we do our reading bit by bit to cope with unaligned regions
+ * then we may not get valid data. To workaround this without leaking
+ * confidential data, when we do the first read of a word, cache that value
+ * and reuse it until another word is read. Also, provide a helper -
+ * clear_last_word() that we should call after we read a region so that the
+ * potentially confidential word is not left hanging around. This means that
+ * when reading a region, we can't skip around randomly but that's a fair
+ * restriction.
+ *
+ * Region may be read and written through sysfs. The fuses will be available
+ * in the fuses group of the platform device and may be written if the
+ * write_enable attribute is set to true. When reading and writing, the value
+ * should be formatted as a hexadecimal integer and the LSB's will go into the
+ * lowest byte addresses.
+ *
+ * Once blown, fuses changes do not become visible until power cycle and if
+ * they change behaviour of the system, this change will not happen until
+ * the next power cycle.  Note: SoC reset through the watchdog timer will
+ * *not* resample the fuses.
+ */
+struct picoxcell_fuse {
+	struct device			*dev;
+	struct miscdevice		miscdev;
+	struct picoxcell_fuse_map	*map;
+	struct attribute		**attrs;
+	struct attribute_group		attr_group;
+	union {
+		void			*mem;
+		void __iomem		*regs;
+	}				backing;
+	struct clk			*clk;
+	u32				last_word;
+	int				last_word_idx;
+	struct mutex			lock;
+	bool				write_enable;
+};
+
+static struct picoxcell_fuse picoxcell_fuse = {
+	.attr_group.name	= "fuses",
+};
+
+static int read_fuse_word(int idx)
+{
+	unsigned word_addr = (idx >> 5) * sizeof(u32);
+
+	return test_mode ? *(u32 *)(picoxcell_fuse.backing.mem + word_addr) :
+		readl(picoxcell_fuse.backing.regs + word_addr);
+}
+
+static int read_fuse(int idx)
+{
+	int word_idx = idx >> 5, bit = idx & 0x1f;
+	u32 val;
+
+	if (word_idx != picoxcell_fuse.last_word_idx) {
+		picoxcell_fuse.last_word = read_fuse_word(idx);
+		picoxcell_fuse.last_word_idx = word_idx;
+	}
+	val = picoxcell_fuse.last_word;
+
+	return !!(val & (1 << bit));
+}
+
+static void clear_last_word(void)
+{
+	picoxcell_fuse.last_word_idx = -1;
+	picoxcell_fuse.last_word = ~0LU;
+}
+
+/*
+ * Blow a single fuse. If there is a region protection last time program fuse
+ * then wire OR that with the global last time program fuse and only try
+ * blowing it if neither are programmed.
+ *
+ * This simply writes to a kmalloc()'d buffer allowing users to prototype
+ * before they actually commit to the efuses.
+ */
+static int blow_fuse_test_mode(int idx)
+{
+	u8 *p8 = ((u8 *)picoxcell_fuse.backing.mem) + idx / 8;
+
+	*p8 |= (1 << (idx % 8));
+
+	return 0;
+}
+
+#define PICOXCELL_FUSE_CTRL_REG_OFFSET			0x200
+#define		PICOXCELL_FUSE_CTRL_WRITE_BUSY		(1 << 0)
+#define		PICOXCELL_FUSE_CTRL_VDDQ_OE		(1 << 1)
+#define		PICOXCELL_FUSE_CTRL_VDDQ		(1 << 2)
+#define PICOXCELL_FUSE_WR_BIT_ADDRESS_REG_OFFSET	0x204
+#define PICOXCELL_FUSE_WR_PERFORM_REG_OFFSET		0x208
+#define		PICOXCELL_FUSE_WR_PERFORM		0x66757365 /* "fuse" */
+#define PICOXCELL_FUSE_WRITE_PAD_EN_REG_OFFSET		0x20c
+#define		PICOXCELL_FUSE_WRITE_PAD_EN_VALUE	0x656e626c /* "enbl" */
+#define PICOXCELL_FUSE_WRITE_PAD_REG_OFFSET		0x210
+#define		PICOXCELL_FUSE_WRITE_PAD_VALUE		0x56444451 /* "VDDQ" */
+
+static int blow_fuse_hardware(int idx)
+{
+	unsigned long control;
+
+	/*
+	 * The fuse macro has a maximum time of 1 second that the VDDQ time
+	 * can be applied for. This is long enough to blow all of the fuses
+	 * but we don't want to get interrupted for an unknown period of
+	 * time...
+	 */
+	local_irq_disable();
+
+	/* Tell the block which fuse to blow and activate the VDDQ voltage. */
+	writel(idx, picoxcell_fuse.backing.regs +
+	       PICOXCELL_FUSE_WR_BIT_ADDRESS_REG_OFFSET);
+	writel(PICOXCELL_FUSE_WRITE_PAD_EN_VALUE, picoxcell_fuse.backing.regs +
+	       PICOXCELL_FUSE_WRITE_PAD_EN_REG_OFFSET);
+	writel(PICOXCELL_FUSE_WRITE_PAD_VALUE, picoxcell_fuse.backing.regs +
+	       PICOXCELL_FUSE_WRITE_PAD_REG_OFFSET);
+
+	/* Give the external circuitry chance to take effect. */
+	udelay(picoxcell_fuse.map->vddq_rise_usec);
+
+	/* Start the fuse blowing process. */
+	writel(PICOXCELL_FUSE_WR_PERFORM, picoxcell_fuse.backing.regs +
+	       PICOXCELL_FUSE_WR_PERFORM_REG_OFFSET);
+
+	/* Wait for the operation to complete. */
+	do {
+		control = readl(picoxcell_fuse.backing.regs +
+				PICOXCELL_FUSE_CTRL_REG_OFFSET);
+	} while (control & PICOXCELL_FUSE_CTRL_WRITE_BUSY);
+
+	/* Disable VDDQ. */
+	writel(0, picoxcell_fuse.backing.regs +
+	       PICOXCELL_FUSE_WRITE_PAD_REG_OFFSET);
+	writel(0, picoxcell_fuse.backing.regs +
+	       PICOXCELL_FUSE_WRITE_PAD_EN_REG_OFFSET);
+	udelay(picoxcell_fuse.map->vddq_fall_usec);
+
+	local_irq_enable();
+
+	return 0;
+}
+
+static int blow_fuse(int idx, int ltp_idx)
+{
+	int ltp = read_fuse(picoxcell_fuse.map->ltp_fuse);
+
+	if (ltp_idx >= 0)
+		ltp |= read_fuse(ltp_idx);
+
+	if (ltp || !picoxcell_fuse.write_enable)
+		return -EPERM;
+
+	if (idx < 0 || idx >= picoxcell_fuse.map->nr_fuses) {
+		dev_dbg(picoxcell_fuse.dev, "attempt to blow invalid fuse (%d)\n",
+			idx);
+		return -EINVAL;
+	}
+
+	return test_mode ? blow_fuse_test_mode(idx) : blow_fuse_hardware(idx);
+}
+
+static const struct picoxcell_fuse_range *find_range(int fuse_idx)
+{
+	int i;
+
+	for (i = 0; i < picoxcell_fuse.map->nr_ranges; ++i)
+		if (fuse_idx >= picoxcell_fuse.map->ranges[i].start &&
+		    fuse_idx <= picoxcell_fuse.map->ranges[i].end)
+			return &picoxcell_fuse.map->ranges[i];
+
+	return NULL;
+}
+
+static ssize_t picoxcell_fuse_write(struct file *filp, const char __user *buf,
+				    size_t len, loff_t *off)
+{
+	ssize_t ret = 0;
+	int i, j;
+	loff_t pos = *off;
+
+	len = min_t(size_t, len, picoxcell_fuse.map->nr_fuses / 8 - pos);
+
+	if (mutex_lock_interruptible(&picoxcell_fuse.lock))
+		return -ERESTARTSYS;
+
+	if (!picoxcell_fuse.write_enable) {
+		ret = -EPERM;
+		goto out;
+	}
+
+	for (i = 0; i < len; ++i) {
+		u8 val = 0;
+
+		if (copy_from_user(&val, buf + i, 1)) {
+			ret = -EFAULT;
+			goto out;
+		}
+
+		for (j = 0; j < 8; ++j) {
+			int fuse_idx = (pos + i) * 8 + j;
+			const struct picoxcell_fuse_range *range =
+				find_range(fuse_idx);
+
+			/*
+			 * Fuse maps may be sparse and contain reserved holes.
+			 * As some ranges aren't aligned to a byte boundary we
+			 * can't treat this as an error so we just skip over
+			 * it and make sure we don't blow the reserved fuses.
+			 */
+			if (!range)
+				continue;
+
+			/*
+			 * Don't reprogram fuses that are already blown or
+			 * fuses that aren't blown and the user doesn't want
+			 * blown.
+			 */
+			if ((read_fuse(fuse_idx) && (val & (1 << j))) ||
+			    (!read_fuse(fuse_idx) && !(val & (1 << j))))
+				continue;
+
+			/* We can't transition from a 1 to a 0. */
+			if (read_fuse(fuse_idx) && !(val & (1 << j))) {
+				ret = -EIO;
+				goto out;
+			}
+
+			ret = blow_fuse(fuse_idx, range->last_time_prog);
+			if (ret)
+				goto out;
+		}
+
+	}
+
+	*off += len;
+
+out:
+	clear_last_word();
+	mutex_unlock(&picoxcell_fuse.lock);
+
+	return ret ?: len;
+}
+
+static ssize_t picoxcell_fuse_read(struct file *filp, char __user *buf,
+				   size_t len, loff_t *off)
+{
+	ssize_t ret = 0;
+	int i, j;
+	loff_t pos = *off;
+
+	len = min_t(size_t, len, picoxcell_fuse.map->nr_fuses / 8 - pos);
+
+	if (mutex_lock_interruptible(&picoxcell_fuse.lock))
+		return -ERESTARTSYS;
+
+	for (i = 0; i < len; ++i) {
+		u8 val = 0;
+
+		for (j = 0; j < 8; ++j)
+			val |= read_fuse((pos + i) * 8 + j) << j;
+
+		if (copy_to_user(buf + i, &val, 1)) {
+			ret = -EFAULT;
+			goto out;
+		}
+	}
+
+	*off += len;
+
+out:
+	clear_last_word();
+	mutex_unlock(&picoxcell_fuse.lock);
+
+	return ret ?: len;
+}
+
+static loff_t picoxcell_fuse_llseek(struct file *filp, loff_t offs, int origin)
+{
+	int ret = 0;
+	loff_t end;
+
+	if (mutex_lock_interruptible(&picoxcell_fuse.lock))
+		return -ERESTARTSYS;
+
+	switch (origin) {
+	case SEEK_CUR:
+		if (filp->f_pos + offs < 0 ||
+		    filp->f_pos + offs >= picoxcell_fuse.map->nr_fuses / 8)
+			ret = -EINVAL;
+		else
+			filp->f_pos += offs;
+
+	case SEEK_SET:
+		if (offs < 0 || offs >= picoxcell_fuse.map->nr_fuses / 8)
+			ret = -EINVAL;
+		else
+			filp->f_pos = offs;
+		break;
+
+	case SEEK_END:
+		end = picoxcell_fuse.map->nr_fuses / 8 - 1;
+		if (end + offs < 0 || end + offs >= end)
+			ret = -EINVAL;
+		else
+			filp->f_pos = end + offs;
+		break;
+
+	default:
+		ret = -EINVAL;
+	}
+
+	mutex_unlock(&picoxcell_fuse.lock);
+
+	return ret ?: filp->f_pos;
+}
+
+/*
+ * Check that we have a valid value to program. By valid value, we expect
+ * that the string is a hexadecimal number, prefixed with '0x', there are no
+ * non-whitespace characters after the end and that the value does not occupy
+ * more bits than there are in the region.
+ */
+static bool value_is_valid(const char *value, int start, int end)
+{
+	const char *p;
+	int bits = 0;
+
+	if (value[0] != '0' || value[1] != 'x')
+		return false;
+
+	p = value + strlen(value) - 1;
+	while (isspace(*p) && p >= value)
+		--p;
+
+	for (; p >= value + 2; --p) {
+		int v = hex_to_bin(*p);
+
+		if (v < 0)
+			return false;
+
+		if (p != value + 2) {
+			bits += 4;
+		} else {
+			if (v & (1 << 3))
+				bits += 4;
+			else if (v & (1 << 2))
+				bits += 3;
+			else if (v & (1 << 1))
+				bits += 2;
+			else if (v & (1 << 0))
+				bits += 1;
+		}
+	}
+
+	return bits > end - start + 1 ? false : true;
+}
+
+static inline struct picoxcell_fuse_range *
+to_picoxcell_fuse_range(struct device_attribute *attr)
+{
+	return attr ? container_of(attr, struct picoxcell_fuse_range, attr) :
+		NULL;
+}
+
+static ssize_t picoxcell_fuse_show(struct device *dev,
+				   struct device_attribute *attr, char *buf)
+{
+	struct picoxcell_fuse_range *range = to_picoxcell_fuse_range(attr);
+	int i, j, offs = (range->end - range->start) % 32;
+	u32 v;
+	ssize_t ret;
+
+	if (mutex_lock_interruptible(&picoxcell_fuse.lock))
+		return -ERESTARTSYS;
+
+	/*
+	 * Dump the value of a fuse range. Some fuses aren't aligned to a byte
+	 * boundary and may not be a multiple of 8 bits. For simplicity (and
+	 * the fact that this doesn't need to be lightning fast), just shift
+	 * the bits out one by one and output byte by byte.
+	 *
+	 * Start off by getting so that we can print the rest as 32 bit blocks.
+	 */
+	ret = sprintf(buf, "0x");
+	for (v = 0, i = range->end; i >= range->end - offs; --i) {
+		v <<= 1;
+		v |= read_fuse(i);
+	}
+	ret += sprintf(buf + ret, "%x", v);
+
+	for (; i >= range->start; i -= 32) {
+		v = 0;
+		for (j = i; j > i - 32; --j) {
+			v <<= 1;
+			v |= read_fuse(j);
+		}
+		ret += sprintf(buf + ret, "%08x", v);
+	}
+	ret += sprintf(buf + ret, "\n");
+
+	clear_last_word();
+	mutex_unlock(&picoxcell_fuse.lock);
+
+	return ret;
+}
+
+static ssize_t picoxcell_fuse_store(struct device *dev,
+				    struct device_attribute *attr,
+				    const char *buf, size_t len)
+{
+	struct picoxcell_fuse_range *range = to_picoxcell_fuse_range(attr);
+	const char *p;
+	int idx, i, err = 0;
+
+	if (!value_is_valid(buf, range->start, range->end))
+		return -EINVAL;
+
+	if (mutex_lock_interruptible(&picoxcell_fuse.lock))
+		return -ERESTARTSYS;
+
+	/*
+	 * Skip any whitespace and newlines after the value we're interested
+	 * in.
+	 */
+	p = buf + strlen(buf) - 1;
+	while (p >= buf && isspace(*p))
+		--p;
+
+	for (idx = range->start; p >= buf + 2; --p, idx += 4) {
+		int v = hex_to_bin(*p);
+
+		for (i = 0; i < 4; ++i) {
+			/*
+			 * Don't reprogram fuses that are already blown or
+			 * fuses that aren't blown and the user doesn't want
+			 * blown.
+			 */
+			if ((read_fuse(i + idx) && (v & (1 << i))) ||
+			    (!read_fuse(i + idx) && !(v & (1 << i))))
+				continue;
+
+			/* We can't transition from a 1 to a 0. */
+			if (read_fuse(i + idx) && !(v & (1 << i))) {
+				err = -EIO;
+				goto out;
+			}
+
+			err = blow_fuse(i + idx, range->last_time_prog);
+			if (err)
+				goto out;
+		}
+	}
+
+out:
+	mutex_unlock(&picoxcell_fuse.lock);
+
+	return err ?: len;
+}
+
+/*
+ * Show the estimated VDDQ active time in microseconds.  This is an estimate
+ * as due to the read-once-per-boot protection we can't reliably tell how many
+ * fuses have actually been blown.  Instead we provide the worst case where
+ * every fuse has been blown.
+ */
+static ssize_t vddq_show(struct device *dev, struct device_attribute *attr,
+			 char *buf)
+{
+	return sprintf(buf, "%d\n", picoxcell_fuse.map->nr_fuses *
+		       (PICOXCELL_FUSE_PROG_TIME_USEC +
+			picoxcell_fuse.map->vddq_rise_usec +
+			picoxcell_fuse.map->vddq_fall_usec));
+}
+static DEVICE_ATTR(vddq_time_usec, 0400, vddq_show, NULL);
+
+static ssize_t write_enable_show(struct device *dev,
+				 struct device_attribute *attr, char *buf)
+{
+	return sprintf(buf, "%s\n", picoxcell_fuse.write_enable ? "true" :
+		       "false");
+}
+
+static ssize_t write_enable_store(struct device *dev,
+				  struct device_attribute *attr,
+				  const char *buf, size_t len)
+{
+	int err = 0;
+
+	if (mutex_lock_interruptible(&picoxcell_fuse.lock))
+		return -ERESTARTSYS;
+
+	if (sysfs_streq(buf, "true"))
+		picoxcell_fuse.write_enable = true;
+	else if (sysfs_streq(buf, "false"))
+		picoxcell_fuse.write_enable = false;
+	else
+		err = -EINVAL;
+
+	mutex_unlock(&picoxcell_fuse.lock);
+
+	return err ?: len;
+}
+static DEVICE_ATTR(write_enable, 0600, write_enable_show, write_enable_store);
+
+static const struct attribute *control_attrs[] = {
+	&dev_attr_vddq_time_usec.attr,
+	&dev_attr_write_enable.attr,
+	NULL,
+};
+
+static void picoxcell_fuse_attrs_free(struct device *dev)
+{
+	struct attribute **attrs = picoxcell_fuse.attrs;
+
+	sysfs_remove_files(&dev->kobj, control_attrs);
+	sysfs_remove_group(&dev->kobj, &picoxcell_fuse.attr_group);
+
+	kfree(attrs);
+}
+
+static int picoxcell_fuse_attrs_create(struct device *dev)
+{
+	int i, err = -ENOMEM;
+	struct picoxcell_fuse_map *map = picoxcell_fuse.map;
+
+	picoxcell_fuse.attr_group.attrs =
+		kzalloc(sizeof(*picoxcell_fuse.attrs) *
+			(map->nr_ranges + 1), GFP_KERNEL);
+	if (!picoxcell_fuse.attr_group.attrs)
+		goto out;
+
+	for (i = 0; i < picoxcell_fuse.map->nr_ranges; ++i) {
+		map->ranges[i].attr = (struct device_attribute) {
+			.attr.name	= picoxcell_fuse.map->ranges[i].name,
+			.attr.mode	= 0600,
+			.show		= picoxcell_fuse_show,
+			.store		= picoxcell_fuse_store,
+		};
+		picoxcell_fuse.attr_group.attrs[i] = &map->ranges[i].attr.attr;
+	}
+
+	err = sysfs_create_group(&dev->kobj, &picoxcell_fuse.attr_group);
+	if (err)
+		goto out_free_attrs;
+
+	err = sysfs_create_files(&dev->kobj, control_attrs);
+	if (err)
+		goto out_free_attrs;
+
+	goto out;
+
+out_free_attrs:
+	picoxcell_fuse_attrs_free(dev);
+out:
+	return err;
+}
+
+static int picoxcell_fuse_open(struct inode *inode, struct file *filp)
+{
+	return 0;
+}
+
+static int picoxcell_fuse_release(struct inode *inode, struct file *filp)
+{
+	return 0;
+}
+
+static const struct file_operations picoxcell_fuse_fops = {
+	.open		= picoxcell_fuse_open,
+	.release	= picoxcell_fuse_release,
+	.write		= picoxcell_fuse_write,
+	.read		= picoxcell_fuse_read,
+	.llseek		= picoxcell_fuse_llseek,
+};
+
+static struct miscdevice picoxcell_fuse_miscdev = {
+	.minor		= MISC_DYNAMIC_MINOR,
+	.name		= "picoxcell_fuse",
+	.fops		= &picoxcell_fuse_fops,
+};
+
+static int picoxcell_fuse_test_mode_init(void)
+{
+	picoxcell_fuse.backing.mem = kzalloc(picoxcell_fuse.map->nr_fuses / 8,
+					     GFP_KERNEL);
+	return picoxcell_fuse.backing.mem ? 0 : -ENOMEM;
+}
+
+static void picoxcell_fuse_test_mode_cleanup(void)
+{
+	kfree(picoxcell_fuse.backing.mem);
+}
+
+static int picoxcell_fuse_hardware_init(struct platform_device *pdev)
+{
+	struct resource *iomem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
+
+	if (!iomem) {
+		dev_warn(&pdev->dev, "platform device has no io memory\n");
+		return -ENOENT;
+	}
+
+	if (!devm_request_region(&pdev->dev, iomem->start,
+				 resource_size(iomem), "picoxcell_fuse")) {
+		dev_warn(&pdev->dev, "no io memory\n");
+		return -ENOENT;
+	}
+
+	picoxcell_fuse.backing.regs = devm_ioremap(&pdev->dev, iomem->start,
+						   resource_size(iomem));
+	if (!picoxcell_fuse.backing.regs) {
+		dev_warn(&pdev->dev, "unable to remap io memory\n");
+		return -ENOMEM;
+	}
+
+	picoxcell_fuse.clk = clk_get(&pdev->dev, NULL);
+	if (IS_ERR(picoxcell_fuse.clk)) {
+		dev_warn(&pdev->dev, "no clk!\n");
+		return PTR_ERR(picoxcell_fuse.clk);
+	}
+
+	if (clk_enable(picoxcell_fuse.clk)) {
+		dev_warn(&pdev->dev, "unable to enable clk\n");
+		clk_put(picoxcell_fuse.clk);
+		return -EBUSY;
+	}
+
+	return 0;
+}
+
+static void picoxcell_fuse_hardware_cleanup(struct platform_device *pdev)
+{
+	clk_put(picoxcell_fuse.clk);
+}
+
+static int __devinit picoxcell_fuse_probe(struct platform_device *pdev)
+{
+	int err = -EINVAL;
+	struct picoxcell_fuse_map *map = pdev->dev.platform_data;
+
+	mutex_init(&picoxcell_fuse.lock);
+	picoxcell_fuse.last_word_idx = -1;
+	picoxcell_fuse.map = map;
+	picoxcell_fuse.write_enable = false;
+
+	if (!picoxcell_fuse.map) {
+		dev_err(&pdev->dev, "no fuse map supplied\n");
+		return -EINVAL;
+	}
+
+	if (map->nr_fuses * (map->vddq_rise_usec + map->vddq_fall_usec +
+			     PICOXCELL_FUSE_PROG_TIME_USEC) > NSEC_PER_SEC) {
+		dev_err(&pdev->dev, "VDDQ rise and fall time too large to allow all fuses to be blown.\n");
+		return -EINVAL;
+	}
+
+	err = test_mode ?  picoxcell_fuse_test_mode_init() :
+		picoxcell_fuse_hardware_init(pdev);
+	if (err)
+		goto out;
+
+	err = misc_register(&picoxcell_fuse_miscdev);
+	if (err)
+		goto out_unmap;
+
+	err = picoxcell_fuse_attrs_create(&pdev->dev);
+	if (err)
+		goto out_unregister;
+	picoxcell_fuse.dev = &pdev->dev;
+	goto out;
+
+out_unregister:
+	misc_deregister(&picoxcell_fuse_miscdev);
+out_unmap:
+	test_mode ? picoxcell_fuse_test_mode_cleanup() :
+		picoxcell_fuse_hardware_cleanup(pdev);
+out:
+	return err;
+}
+
+static int __devexit picoxcell_fuse_remove(struct platform_device *pdev)
+{
+	picoxcell_fuse_attrs_free(&pdev->dev);
+	misc_deregister(&picoxcell_fuse_miscdev);
+	test_mode ? picoxcell_fuse_test_mode_cleanup() :
+		picoxcell_fuse_hardware_cleanup(pdev);
+
+	return 0;
+}
+
+#ifdef CONFIG_PM
+static int picoxcell_fuse_suspend(struct device *dev)
+{
+	clk_disable(picoxcell_fuse.clk);
+
+	return 0;
+}
+
+static int picoxcell_fuse_resume(struct device *dev)
+{
+	return clk_enable(picoxcell_fuse.clk);
+}
+#else /* CONFIG_PM */
+#define picoxcell_fuse_suspend	NULL
+#define picoxcell_fuse_resume	NULL
+#endif /* CONFIG_PM */
+
+static const struct dev_pm_ops picoxcell_fuse_pm_ops = {
+	.suspend		= picoxcell_fuse_suspend,
+	.resume			= picoxcell_fuse_resume,
+};
+
+static struct platform_driver picoxcell_driver = {
+	.probe			= picoxcell_fuse_probe,
+	.remove			= __devexit_p(picoxcell_fuse_remove),
+	.driver			= {
+		.name		= "picoxcell-fuse",
+		.pm		= &picoxcell_fuse_pm_ops,
+	},
+};
+
+static int __init picoxcell_fuse_init(void)
+{
+	return platform_driver_register(&picoxcell_driver);
+}
+
+static void picoxcell_fuse_exit(void)
+{
+	platform_driver_unregister(&picoxcell_driver);
+}
+
+module_init(picoxcell_fuse_init);
+module_exit(picoxcell_fuse_exit);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Jamie Iles");
+MODULE_DESCRIPTION("Picochip picoXcell fuse block driver");
diff --git a/include/linux/platform_data/picoxcell_fuse.h b/include/linux/platform_data/picoxcell_fuse.h
new file mode 100644
index 0000000..84357e0
--- /dev/null
+++ b/include/linux/platform_data/picoxcell_fuse.h
@@ -0,0 +1,104 @@
+/*
+ * Copyright (c) 2010 Picochip Ltd., Jamie Iles
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+#ifndef __PICOXCELL_FUSE_H__
+#define __PICOXCELL_FUSE_H__
+
+#include <linux/device.h>
+#include <linux/types.h>
+
+/*
+ * A logical group of fuses. This could be a single fuse such as one to
+ * disable the memif_arm on a picoXcell device or a group of fuses to
+ * represent the serial number or a secure key.
+ */
+struct picoxcell_fuse_range {
+	const char		*name;
+	int			start;
+	int			end;
+
+	/*
+	 * Index of the read once per boot, jtag disable and last time program
+	 * fuses. If the read once per boot fuse is blown then this range will
+	 * only be able to be read once per boot with valid data. Some fuse
+	 * ranges will not have a read once per boot fuse so this will be -1.
+	 *
+	 * The jtag disable fuse prevents the range being read through the
+	 * JTAG interface and the last time program prevents the range from
+	 * being overwritten.
+	 */
+	int			read_once;
+	int			jtag_disable;
+	int			last_time_prog;
+
+	struct device_attribute	attr;
+};
+
+/*
+ * Define a fuse range with a given name, start and end fuse index.
+ */
+#define FUSE_RANGE(__name, __start, __end) { \
+		.name			= #__name, \
+		.start			= __start, \
+		.end			= __end, \
+		.read_once		= -1, \
+		.jtag_disable		= -1, \
+		.last_time_prog		= -1, \
+	}
+
+/*
+ * Define a fuse range with a given name, start and end fuse index. This range
+ * also has protection bits for read once per boot, jtag disable and last time
+ * program.
+ */
+#define FUSE_RANGE_PROTECTED(__name, __start, __end, __read_once, \
+			     __jtag_disable, __last_time) { \
+		.name			= #__name, \
+		.start			= __start, \
+		.end			= __end, \
+		.read_once		= __read_once, \
+		.jtag_disable		= __jtag_disable, \
+		.last_time_prog		= __last_time, \
+	}, \
+	FUSE_RANGE(__name ## _last_time_prog, __last_time, __last_time), \
+	FUSE_RANGE(__name ## _read_once, __read_once, __read_once), \
+	FUSE_RANGE(__name ## _jtag_disable, __jtag_disable, __jtag_disable)
+
+/*
+ * The fuse map to be embedded in the picoxcell-fuse platform device as
+ * platform data. The .ltp_fuse gives the global last time program fuse index.
+ * If this fuse is blown then no writes to any fuse will be allowed.
+ */
+struct picoxcell_fuse_map {
+	struct picoxcell_fuse_range	*ranges;
+	int				nr_ranges;
+	int				nr_fuses;
+	int				ltp_fuse;
+
+	/*
+	 * The VDDQ supply to the fuse block is external to the chip and is
+	 * controlled by an enable pin that controls an external transistor.
+	 * This switching will take some time to reach the correct voltage and
+	 * these times should be described here. To operate within spec, the
+	 * VDDQ voltage should only be applied for a maximum of 1 second in
+	 * the device's lifetime.
+	 */
+	unsigned			vddq_rise_usec;
+	unsigned			vddq_fall_usec;
+};
+
+#endif /* __PICOXCELL_FUSE_H__ */
-- 
1.7.4




More information about the linux-arm-kernel mailing list