diff options
author | Jean Delvare <jdelvare@suse.de> | 2018-10-07 19:05:00 +0200 |
---|---|---|
committer | Greg Kroah-Hartman <gregkh@linuxfoundation.org> | 2018-10-15 20:51:46 +0200 |
commit | 3b7584a296c4fac45e59ab3a264880e1a2fedbfe (patch) | |
tree | fb5ed1191a06f96be8af59354916978bac371c4e /drivers/misc/eeprom | |
parent | 5fe9f6ccbb7bc82a92e5d32b3eb68eebb3ea4a93 (diff) |
eeprom: New ee1004 driver for DDR4 memory
The EEPROMs which hold the SPD data on DDR4 memory modules are no
longer standard AT24C02-compatible EEPROMs. They are 512-byte EEPROMs
which use only 1 I2C address for data access. You need to switch
between the lower page and the upper page of data by sending commands
on the SMBus.
Signed-off-by: Jean Delvare <jdelvare@suse.de>
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Diffstat (limited to 'drivers/misc/eeprom')
-rw-r--r-- | drivers/misc/eeprom/Kconfig | 11 | ||||
-rw-r--r-- | drivers/misc/eeprom/Makefile | 1 | ||||
-rw-r--r-- | drivers/misc/eeprom/ee1004.c | 281 |
3 files changed, 293 insertions, 0 deletions
diff --git a/drivers/misc/eeprom/Kconfig b/drivers/misc/eeprom/Kconfig index 68a1ac929917..fe7a1d27a017 100644 --- a/drivers/misc/eeprom/Kconfig +++ b/drivers/misc/eeprom/Kconfig @@ -111,4 +111,15 @@ config EEPROM_IDT_89HPESX This driver can also be built as a module. If so, the module will be called idt_89hpesx. +config EEPROM_EE1004 + tristate "SPD EEPROMs on DDR4 memory modules" + depends on I2C && SYSFS + help + Enable this driver to get read support to SPD EEPROMs following + the JEDEC EE1004 standard. These are typically found on DDR4 + SDRAM memory modules. + + This driver can also be built as a module. If so, the module + will be called ee1004. + endmenu diff --git a/drivers/misc/eeprom/Makefile b/drivers/misc/eeprom/Makefile index 2aab60ef3e3e..a9b4b6579b75 100644 --- a/drivers/misc/eeprom/Makefile +++ b/drivers/misc/eeprom/Makefile @@ -7,3 +7,4 @@ obj-$(CONFIG_EEPROM_93CX6) += eeprom_93cx6.o obj-$(CONFIG_EEPROM_93XX46) += eeprom_93xx46.o obj-$(CONFIG_EEPROM_DIGSY_MTC_CFG) += digsy_mtc_eeprom.o obj-$(CONFIG_EEPROM_IDT_89HPESX) += idt_89hpesx.o +obj-$(CONFIG_EEPROM_EE1004) += ee1004.o diff --git a/drivers/misc/eeprom/ee1004.c b/drivers/misc/eeprom/ee1004.c new file mode 100644 index 000000000000..276c1690ea1b --- /dev/null +++ b/drivers/misc/eeprom/ee1004.c @@ -0,0 +1,281 @@ +/* + * ee1004 - driver for DDR4 SPD EEPROMs + * + * Copyright (C) 2017 Jean Delvare + * + * Based on the at24 driver: + * Copyright (C) 2005-2007 David Brownell + * Copyright (C) 2008 Wolfram Sang, Pengutronix + * + * 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/i2c.h> +#include <linux/init.h> +#include <linux/kernel.h> +#include <linux/mod_devicetable.h> +#include <linux/module.h> +#include <linux/mutex.h> + +/* + * DDR4 memory modules use special EEPROMs following the Jedec EE1004 + * specification. These are 512-byte EEPROMs using a single I2C address + * in the 0x50-0x57 range for data. One of two 256-byte page is selected + * by writing a command to I2C address 0x36 or 0x37 on the same I2C bus. + * + * Therefore we need to request these 2 additional addresses, and serialize + * access to all such EEPROMs with a single mutex. + * + * We assume it is safe to read up to 32 bytes at once from these EEPROMs. + * We use SMBus access even if I2C is available, these EEPROMs are small + * enough, and reading from them infrequent enough, that we favor simplicity + * over performance. + */ + +#define EE1004_ADDR_SET_PAGE 0x36 +#define EE1004_EEPROM_SIZE 512 +#define EE1004_PAGE_SIZE 256 +#define EE1004_PAGE_SHIFT 8 + +/* + * Mutex protects ee1004_set_page and ee1004_dev_count, and must be held + * from page selection to end of read. + */ +static DEFINE_MUTEX(ee1004_bus_lock); +static struct i2c_client *ee1004_set_page[2]; +static unsigned int ee1004_dev_count; +static int ee1004_current_page; + +static const struct i2c_device_id ee1004_ids[] = { + { "ee1004", 0 }, + { } +}; +MODULE_DEVICE_TABLE(i2c, ee1004_ids); + +/*-------------------------------------------------------------------------*/ + +static ssize_t ee1004_eeprom_read(struct i2c_client *client, char *buf, + unsigned int offset, size_t count) +{ + int status; + + if (count > I2C_SMBUS_BLOCK_MAX) + count = I2C_SMBUS_BLOCK_MAX; + /* Can't cross page boundaries */ + if (unlikely(offset + count > EE1004_PAGE_SIZE)) + count = EE1004_PAGE_SIZE - offset; + + status = i2c_smbus_read_i2c_block_data_or_emulated(client, offset, + count, buf); + dev_dbg(&client->dev, "read %zu@%d --> %d\n", count, offset, status); + + return status; +} + +static ssize_t ee1004_read(struct file *filp, struct kobject *kobj, + struct bin_attribute *bin_attr, + char *buf, loff_t off, size_t count) +{ + struct device *dev = kobj_to_dev(kobj); + struct i2c_client *client = to_i2c_client(dev); + size_t requested = count; + int page; + + if (unlikely(!count)) + return count; + + page = off >> EE1004_PAGE_SHIFT; + if (unlikely(page > 1)) + return 0; + off &= (1 << EE1004_PAGE_SHIFT) - 1; + + /* + * Read data from chip, protecting against concurrent access to + * other EE1004 SPD EEPROMs on the same adapter. + */ + mutex_lock(&ee1004_bus_lock); + + while (count) { + int status; + + /* Select page */ + if (page != ee1004_current_page) { + /* Data is ignored */ + status = i2c_smbus_write_byte(ee1004_set_page[page], + 0x00); + if (status < 0) { + dev_err(dev, "Failed to select page %d (%d)\n", + page, status); + mutex_unlock(&ee1004_bus_lock); + return status; + } + dev_dbg(dev, "Selected page %d\n", page); + ee1004_current_page = page; + } + + status = ee1004_eeprom_read(client, buf, off, count); + if (status < 0) { + mutex_unlock(&ee1004_bus_lock); + return status; + } + buf += status; + off += status; + count -= status; + + if (off == EE1004_PAGE_SIZE) { + page++; + off = 0; + } + } + + mutex_unlock(&ee1004_bus_lock); + + return requested; +} + +static const struct bin_attribute eeprom_attr = { + .attr = { + .name = "eeprom", + .mode = 0444, + }, + .size = EE1004_EEPROM_SIZE, + .read = ee1004_read, +}; + +static int ee1004_probe(struct i2c_client *client, + const struct i2c_device_id *id) +{ + int err, cnr = 0; + const char *slow = NULL; + + /* Make sure we can operate on this adapter */ + if (!i2c_check_functionality(client->adapter, + I2C_FUNC_SMBUS_READ_BYTE | + I2C_FUNC_SMBUS_READ_I2C_BLOCK)) { + if (i2c_check_functionality(client->adapter, + I2C_FUNC_SMBUS_READ_BYTE | + I2C_FUNC_SMBUS_READ_WORD_DATA)) + slow = "word"; + else if (i2c_check_functionality(client->adapter, + I2C_FUNC_SMBUS_READ_BYTE | + I2C_FUNC_SMBUS_READ_BYTE_DATA)) + slow = "byte"; + else + return -EPFNOSUPPORT; + } + + /* Use 2 dummy devices for page select command */ + mutex_lock(&ee1004_bus_lock); + if (++ee1004_dev_count == 1) { + for (cnr = 0; cnr < 2; cnr++) { + ee1004_set_page[cnr] = i2c_new_dummy(client->adapter, + EE1004_ADDR_SET_PAGE + cnr); + if (!ee1004_set_page[cnr]) { + dev_err(&client->dev, + "address 0x%02x unavailable\n", + EE1004_ADDR_SET_PAGE + cnr); + err = -EADDRINUSE; + goto err_clients; + } + } + } else if (i2c_adapter_id(client->adapter) != + i2c_adapter_id(ee1004_set_page[0]->adapter)) { + dev_err(&client->dev, + "Driver only supports devices on a single I2C bus\n"); + err = -EOPNOTSUPP; + goto err_clients; + } + + /* Remember current page to avoid unneeded page select */ + err = i2c_smbus_read_byte(ee1004_set_page[0]); + if (err == -ENXIO) { + /* Nack means page 1 is selected */ + ee1004_current_page = 1; + } else if (err < 0) { + /* Anything else is a real error, bail out */ + goto err_clients; + } else { + /* Ack means page 0 is selected, returned value meaningless */ + ee1004_current_page = 0; + } + dev_dbg(&client->dev, "Currently selected page: %d\n", + ee1004_current_page); + mutex_unlock(&ee1004_bus_lock); + + /* Create the sysfs eeprom file */ + err = sysfs_create_bin_file(&client->dev.kobj, &eeprom_attr); + if (err) + goto err_clients_lock; + + dev_info(&client->dev, + "%u byte EE1004-compliant SPD EEPROM, read-only\n", + EE1004_EEPROM_SIZE); + if (slow) + dev_notice(&client->dev, + "Falling back to %s reads, performance will suffer\n", + slow); + + return 0; + + err_clients_lock: + mutex_lock(&ee1004_bus_lock); + err_clients: + if (--ee1004_dev_count == 0) { + for (cnr--; cnr >= 0; cnr--) { + i2c_unregister_device(ee1004_set_page[cnr]); + ee1004_set_page[cnr] = NULL; + } + } + mutex_unlock(&ee1004_bus_lock); + + return err; +} + +static int ee1004_remove(struct i2c_client *client) +{ + int i; + + sysfs_remove_bin_file(&client->dev.kobj, &eeprom_attr); + + /* Remove page select clients if this is the last device */ + mutex_lock(&ee1004_bus_lock); + if (--ee1004_dev_count == 0) { + for (i = 0; i < 2; i++) { + i2c_unregister_device(ee1004_set_page[i]); + ee1004_set_page[i] = NULL; + } + } + mutex_unlock(&ee1004_bus_lock); + + return 0; +} + +/*-------------------------------------------------------------------------*/ + +static struct i2c_driver ee1004_driver = { + .driver = { + .name = "ee1004", + }, + .probe = ee1004_probe, + .remove = ee1004_remove, + .id_table = ee1004_ids, +}; + +static int __init ee1004_init(void) +{ + return i2c_add_driver(&ee1004_driver); +} +module_init(ee1004_init); + +static void __exit ee1004_exit(void) +{ + i2c_del_driver(&ee1004_driver); +} +module_exit(ee1004_exit); + +MODULE_DESCRIPTION("Driver for EE1004-compliant DDR4 SPD EEPROMs"); +MODULE_AUTHOR("Jean Delvare"); +MODULE_LICENSE("GPL"); |