[v2] misc: eeprom: add driver for Cypress FRAM
diff mbox

Message ID 4b6664bedcb7bddfb6eed6c4b14a11a853197522.1413287343.git.jiri.prchal@aksignal.cz
State New, archived
Headers show

Commit Message

Prchal Ji?í Oct. 16, 2014, 7:25 a.m. UTC
This patch adds driver for Cypress FRAMs on SPI bus, such as FM25V05, FM25V10
etc.
Reworked from at25 driver:
- simplified writing since data are written without erasing and waiting to
finish write cycle
- add reading device ID and choose size and addr len from it
- add serial number reading and exporting it to sysfs

Signed-off-by: Jiri Prchal <jiri.prchal@aksignal.cz>
---
v2: changed upon Varka Bhadram coments

 drivers/misc/eeprom/Kconfig  |  11 +
 drivers/misc/eeprom/Makefile |   1 +
 drivers/misc/eeprom/fm25.c   | 499 +++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 511 insertions(+)

--
1.9.1

--
To unsubscribe from this list: send the line "unsubscribe linux-spi" in
the body of a message to majordomo@vger.kernel.org
More majordomo info at  http://vger.kernel.org/majordomo-info.html

Patch
diff mbox

diff --git a/drivers/misc/eeprom/Kconfig b/drivers/misc/eeprom/Kconfig
index 9536852f..aee6a73 100644
--- a/drivers/misc/eeprom/Kconfig
+++ b/drivers/misc/eeprom/Kconfig
@@ -38,6 +38,17 @@  config EEPROM_AT25
 	  This driver can also be built as a module.  If so, the module
 	  will be called at25.

+config FRAM_FM25
+	tristate "SPI Cypress FRAM"
+	depends on SPI && SYSFS
+	help
+	  Enable this driver to get read/write support to SPI FRAMs,
+	  after you configure the board init code to know about each fram
+	  on your target board.
+
+	  This driver can also be built as a module.  If so, the module
+	  will be called fm25.
+
 config EEPROM_LEGACY
 	tristate "Old I2C EEPROM reader"
 	depends on I2C && SYSFS
diff --git a/drivers/misc/eeprom/Makefile b/drivers/misc/eeprom/Makefile
index 9507aec..6738752 100644
--- a/drivers/misc/eeprom/Makefile
+++ b/drivers/misc/eeprom/Makefile
@@ -1,5 +1,6 @@ 
 obj-$(CONFIG_EEPROM_AT24)	+= at24.o
 obj-$(CONFIG_EEPROM_AT25)	+= at25.o
+obj-$(CONFIG_FRAM_FM25)		+= fm25.o
 obj-$(CONFIG_EEPROM_LEGACY)	+= eeprom.o
 obj-$(CONFIG_EEPROM_MAX6875)	+= max6875.o
 obj-$(CONFIG_EEPROM_93CX6)	+= eeprom_93cx6.o
diff --git a/drivers/misc/eeprom/fm25.c b/drivers/misc/eeprom/fm25.c
new file mode 100644
index 0000000..c30c523
--- /dev/null
+++ b/drivers/misc/eeprom/fm25.c
@@ -0,0 +1,499 @@ 
+/*
+ * fm25.c -- support SPI FRAMs, such as Cypress FM25 models
+ *
+ * Copyright (C) 2014 Jiri Prchal
+ *
+ * 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.
+ */
+
+#include <linux/kernel.h>
+#include <linux/init.h>
+#include <linux/module.h>
+#include <linux/slab.h>
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/sched.h>
+
+#include <linux/spi/spi.h>
+#include <linux/spi/eeprom.h>
+#include <linux/of.h>
+
+struct fm25_data {
+	struct spi_device	*spi;
+	struct memory_accessor	mem;
+	struct mutex		lock;
+	struct spi_eeprom	chip;
+	struct bin_attribute	bin;
+	unsigned		addrlen;
+	int			has_sernum;
+};
+
+#define	FM25_WREN	0x06		/* latch the write enable */
+#define	FM25_WRDI	0x04		/* reset the write enable */
+#define	FM25_RDSR	0x05		/* read status register */
+#define	FM25_WRSR	0x01		/* write status register */
+#define	FM25_READ	0x03		/* read byte(s) */
+#define	FM25_WRITE	0x02		/* write byte(s)/sector */
+#define	FM25_SLEEP	0xb9		/* enter sleep mode */
+#define	FM25_RDID	0x9f		/* read device ID */
+#define	FM25_RDSN	0xc3		/* read S/N */
+
+#define	FM25_SR_WEN	0x02		/* write enable (latched) */
+#define	FM25_SR_BP0	0x04		/* BP for software writeprotect */
+#define	FM25_SR_BP1	0x08
+#define	FM25_SR_WPEN	0x80		/* writeprotect enable */
+
+#define	FM25_ID_LEN	9		/* ID lenght */
+#define	FM25_SN_LEN	8		/* serial number lenght */
+
+#define	FM25_MAXADDRLEN	3		/* 24 bit addresses */
+
+#define	io_limit	PAGE_SIZE	/* bytes */
+
+static ssize_t
+fm25_data_read(
+	struct fm25_data	*fm25,
+	char			*buf,
+	unsigned		offset,
+	size_t			count
+)
+{
+	u8			command[FM25_MAXADDRLEN + 1];
+	u8			*cp;
+	ssize_t			status;
+	struct spi_transfer	t[2];
+	struct spi_message	m;
+	u8			instr;
+
+	if (unlikely(offset >= fm25->bin.size))
+		return 0;
+	if ((offset + count) > fm25->bin.size)
+		count = fm25->bin.size - offset;
+	if (unlikely(!count))
+		return count;
+
+	cp = command;
+
+	instr = FM25_READ;
+	*cp++ = instr;
+
+	/* 8/16/24-bit address is written MSB first */
+	switch (fm25->addrlen) {
+	default:	/* case 3 */
+		*cp++ = offset >> 16;
+	case 2:
+		*cp++ = offset >> 8;
+	case 1:
+	case 0:	/* can't happen: for better codegen */
+		*cp++ = offset >> 0;
+	}
+
+	spi_message_init(&m);
+	memset(t, 0, sizeof t);
+
+	t[0].tx_buf = command;
+	t[0].len = fm25->addrlen + 1;
+	spi_message_add_tail(&t[0], &m);
+
+	t[1].rx_buf = buf;
+	t[1].len = count;
+	spi_message_add_tail(&t[1], &m);
+
+	mutex_lock(&fm25->lock);
+
+	/* Read it all at once.
+	 *
+	 * REVISIT that's potentially a problem with large chips, if
+	 * other devices on the bus need to be accessed regularly or
+	 * this chip is clocked very slowly
+	 */
+	status = spi_sync(fm25->spi, &m);
+	dev_dbg(&fm25->spi->dev,
+		"read %Zd bytes at %d --> %d\n",
+		count, offset, (int) status);
+
+	mutex_unlock(&fm25->lock);
+	return status ? status : count;
+}
+
+static ssize_t
+fm25_id_read(struct fm25_data *fm25, char *buf)
+{
+	u8			command = FM25_RDID;
+	ssize_t			status;
+	struct spi_transfer	t[2];
+	struct spi_message	m;
+
+	spi_message_init(&m);
+	memset(t, 0, sizeof t);
+
+	t[0].tx_buf = &command;
+	t[0].len = 1;
+	spi_message_add_tail(&t[0], &m);
+
+	t[1].rx_buf = buf;
+	t[1].len = FM25_ID_LEN;
+	spi_message_add_tail(&t[1], &m);
+
+	mutex_lock(&fm25->lock);
+
+	status = spi_sync(fm25->spi, &m);
+	dev_dbg(&fm25->spi->dev,
+		"read %Zd bytes of ID --> %d\n",
+	 FM25_ID_LEN, (int) status);
+
+	mutex_unlock(&fm25->lock);
+	return status ? status : FM25_ID_LEN;
+}
+
+static ssize_t
+fm25_sernum_read(struct fm25_data *fm25, char *buf)
+{
+	u8			command = FM25_RDSN;
+	ssize_t			status;
+	struct spi_transfer	t[2];
+	struct spi_message	m;
+
+	spi_message_init(&m);
+	memset(t, 0, sizeof t);
+
+	t[0].tx_buf = &command;
+	t[0].len = 1;
+	spi_message_add_tail(&t[0], &m);
+
+	t[1].rx_buf = buf;
+	t[1].len = FM25_SN_LEN;
+	spi_message_add_tail(&t[1], &m);
+
+	mutex_lock(&fm25->lock);
+
+	status = spi_sync(fm25->spi, &m);
+	dev_dbg(&fm25->spi->dev,
+		"read %Zd bytes of serial number --> %d\n",
+		FM25_SN_LEN, (int) status);
+
+	mutex_unlock(&fm25->lock);
+	return status ? status : FM25_SN_LEN;
+}
+
+static ssize_t
+sernum_show(struct device *dev, struct device_attribute *attr, char *buf)
+{
+	char			binbuf[FM25_SN_LEN];
+	struct fm25_data	*fm25;
+	int			i;
+	char			*pbuf = buf;
+
+	fm25 = dev_get_drvdata(dev);
+	fm25_sernum_read(fm25, binbuf);
+	for (i = 0; i < FM25_SN_LEN; i++)
+		pbuf += sprintf(pbuf, "%02x ", binbuf[i]);
+	sprintf(--pbuf, "\n");
+	return (3 * i);
+}
+static const DEVICE_ATTR_RO(sernum);
+
+static ssize_t
+fm25_bin_read(struct file *filp, struct kobject *kobj,
+	      struct bin_attribute *bin_attr,
+	      char *buf, loff_t off, size_t count)
+{
+	struct device		*dev;
+	struct fm25_data	*fm25;
+
+	dev = container_of(kobj, struct device, kobj);
+	fm25 = dev_get_drvdata(dev);
+
+	return fm25_data_read(fm25, buf, off, count);
+}
+
+
+static ssize_t
+fm25_data_write(struct fm25_data *fm25, const char *buf, loff_t off,
+	      size_t count)
+{
+	ssize_t			status = 0;
+	unsigned		written = 0;
+	unsigned		buf_size;
+	u8			*bounce;
+
+	if (unlikely(off >= fm25->bin.size))
+		return -EFBIG;
+	if ((off + count) > fm25->bin.size)
+		count = fm25->bin.size - off;
+	if (unlikely(!count))
+		return count;
+
+	/* Temp buffer starts with command and address */
+	buf_size = io_limit;
+	bounce = kmalloc(buf_size + fm25->addrlen + 1, GFP_KERNEL);
+	if (!bounce)
+		return -ENOMEM;
+
+	/* For write, rollover is within the page ... so we write at
+	 * most one page, then manually roll over to the next page.
+	 */
+	mutex_lock(&fm25->lock);
+	do {
+		unsigned	segment;
+		unsigned	offset = (unsigned) off;
+		u8		*cp = bounce;
+		u8		instr;
+
+		*cp = FM25_WREN;
+		status = spi_write(fm25->spi, cp, 1);
+		if (status < 0) {
+			dev_dbg(&fm25->spi->dev, "WREN --> %d\n",
+					(int) status);
+			break;
+		}
+
+		instr = FM25_WRITE;
+		*cp++ = instr;
+
+		/* 8/16/24-bit address is written MSB first */
+		switch (fm25->addrlen) {
+		default:	/* case 3 */
+			*cp++ = offset >> 16;
+		case 2:
+			*cp++ = offset >> 8;
+		case 1:
+		case 0:	/* can't happen: for better codegen */
+			*cp++ = offset >> 0;
+		}
+
+		/* Write as much of a page as we can */
+		segment = buf_size - (offset % buf_size);
+		if (segment > count)
+			segment = count;
+		memcpy(cp, buf, segment);
+		status = spi_write(fm25->spi, bounce,
+				segment + fm25->addrlen + 1);
+		dev_dbg(&fm25->spi->dev,
+				"write %u bytes at %u --> %d\n",
+				segment, offset, (int) status);
+		if (status < 0)
+			break;
+
+		/* REVISIT this should detect (or prevent) failed writes
+		 * to readonly sections of the EEPROM...
+		 */
+
+		off += segment;
+		buf += segment;
+		count -= segment;
+		written += segment;
+
+	} while (count > 0);
+
+	mutex_unlock(&fm25->lock);
+
+	kfree(bounce);
+	return written ? written : status;
+}
+
+static ssize_t
+fm25_bin_write(struct file *filp, struct kobject *kobj,
+	       struct bin_attribute *bin_attr,
+	       char *buf, loff_t off, size_t count)
+{
+	struct device		*dev;
+	struct fm25_data	*fm25;
+
+	dev = container_of(kobj, struct device, kobj);
+	fm25 = dev_get_drvdata(dev);
+
+	return fm25_data_write(fm25, buf, off, count);
+}
+
+/*-------------------------------------------------------------------------*/
+
+/* Let in-kernel code access the eeprom data. */
+
+static ssize_t fm25_mem_read(struct memory_accessor *mem, char *buf,
+			     off_t offset, size_t count)
+{
+  struct fm25_data *fm25 = container_of(mem, struct fm25_data, mem);
+
+  return fm25_data_read(fm25, buf, offset, count);
+}
+
+static ssize_t fm25_mem_write(struct memory_accessor *mem, const char *buf,
+			  off_t offset, size_t count)
+{
+	struct fm25_data *fm25 = container_of(mem, struct fm25_data, mem);
+
+	return fm25_data_write(fm25, buf, offset, count);
+}
+
+/*-------------------------------------------------------------------------*/
+
+static int fm25_np_to_chip(struct device *dev,
+			   struct device_node *np,
+			   struct spi_eeprom *chip)
+{
+	memset(chip, 0, sizeof(*chip));
+	strncpy(chip->name, np->name, sizeof(chip->name));
+
+	if (of_find_property(np, "read-only", NULL))
+		chip->flags |= EE_READONLY;
+	return 0;
+}
+
+static int fm25_probe(struct spi_device *spi)
+{
+	struct fm25_data	*fm25 = NULL;
+	struct spi_eeprom	chip;
+	struct device_node	*np = spi->dev.of_node;
+	int			err;
+	char			id[FM25_ID_LEN];
+
+	/* Chip description */
+	if (!spi->dev.platform_data) {
+		if (np) {
+			err = fm25_np_to_chip(&spi->dev, np, &chip);
+			if (err)
+				return err;
+		} else {
+			dev_err(&spi->dev, "Error: no chip description\n");
+			return -ENODEV;
+		}
+	} else
+		chip = *(struct spi_eeprom *)spi->dev.platform_data;
+
+	fm25 = devm_kzalloc(&spi->dev, sizeof(*fm25), GFP_KERNEL);
+	if (!fm25)
+		return -ENOMEM;
+
+	mutex_init(&fm25->lock);
+	fm25->chip = chip;
+	fm25->spi = spi_dev_get(spi);
+	spi_set_drvdata(spi, fm25);
+
+	/* Get ID of chip */
+	fm25_id_read(fm25, id);
+	if (id[6] != 0xc2) {
+		dev_err(&spi->dev, "Error: no Cypress FRAM (id %02x)\n", id[6]);
+		return -ENODEV;
+	}
+	/* set size found in ID */
+	switch (id[7]) {
+	case 0x21:
+		fm25->chip.byte_len = 16 * 1024;
+		break;
+	case 0x22:
+		fm25->chip.byte_len = 32 * 1024;
+		break;
+	case 0x23:
+		fm25->chip.byte_len = 64 * 1024;
+		break;
+	case 0x24:
+		fm25->chip.byte_len = 128 * 1024;
+		break;
+	case 0x25:
+		fm25->chip.byte_len = 256 * 1024;
+		break;
+	default:
+		dev_err(&spi->dev, "Error: unsupported size (id %02x)\n", id[7]);
+		return -ENODEV;
+		break;
+	}
+
+	if (fm25->chip.byte_len > 64 * 1024) {
+		fm25->addrlen = 3;
+		fm25->chip.flags |= EE_ADDR3;
+	}
+	else {
+		fm25->addrlen = 2;
+		fm25->chip.flags |= EE_ADDR2;
+	}
+
+	if (id[8])
+		fm25->has_sernum = 1;
+	else
+		fm25->has_sernum = 0;
+
+	fm25->chip.page_size = PAGE_SIZE;
+
+	/* Export the EEPROM bytes through sysfs, since that's convenient.
+	 * And maybe to other kernel code; it might hold a board's Ethernet
+	 * address, or board-specific calibration data generated on the
+	 * manufacturing floor.
+	 *
+	 * Default to root-only access to the data; EEPROMs often hold data
+	 * that's sensitive for read and/or write, like ethernet addresses,
+	 * security codes, board-specific manufacturing calibrations, etc.
+	 */
+	sysfs_bin_attr_init(&fm25->bin);
+	fm25->bin.attr.name = "fram";
+	fm25->bin.attr.mode = S_IRUGO;
+	fm25->bin.read = fm25_bin_read;
+	fm25->mem.read = fm25_mem_read;
+
+	fm25->bin.size = fm25->chip.byte_len;
+	if (!(chip.flags & EE_READONLY)) {
+		fm25->bin.write = fm25_bin_write;
+		fm25->bin.attr.mode |= S_IWUSR | S_IWGRP;
+		fm25->mem.write = fm25_mem_write;
+	}
+
+	err = sysfs_create_bin_file(&spi->dev.kobj, &fm25->bin);
+	if (err)
+		return err;
+
+	/* Export the FM25 serial number */
+	if (fm25->has_sernum) {
+		err = device_create_file(&spi->dev, &dev_attr_sernum);
+		if (err)
+			return err;
+	}
+
+	if (chip.setup)
+		chip.setup(&fm25->mem, chip.context);
+
+	dev_info(&spi->dev, "%Zd %s %s fram%s\n",
+		(fm25->bin.size < 1024)
+			? fm25->bin.size
+			: (fm25->bin.size / 1024),
+		(fm25->bin.size < 1024) ? "Byte" : "KByte",
+		fm25->chip.name,
+		(chip.flags & EE_READONLY) ? " (readonly)" : "");
+	return 0;
+}
+
+static int fm25_remove(struct spi_device *spi)
+{
+	struct fm25_data *fm25 = spi_get_drvdata(spi);
+
+	sysfs_remove_bin_file(&spi->dev.kobj, &fm25->bin);
+	if (fm25->has_sernum)
+		device_remove_file(&spi->dev, &dev_attr_sernum);
+	return 0;
+}
+
+/*-------------------------------------------------------------------------*/
+
+static const struct of_device_id fm25_of_match[] = {
+	{ .compatible = "cypress,fm25", },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, fm25_of_match);
+
+static struct spi_driver fm25_driver = {
+	.driver = {
+		.name		= "fm25",
+		.of_match_table = fm25_of_match,
+	},
+	.probe		= fm25_probe,
+	.remove		= fm25_remove,
+};
+
+module_spi_driver(fm25_driver);
+
+MODULE_DESCRIPTION("Driver for Cypress SPI FRAMs");
+MODULE_AUTHOR("Jiri Prchal");
+MODULE_LICENSE("GPL");
+MODULE_ALIAS("spi:fram");