[PATCH 3/3] i386: CS5535 chip support - SMBus

From: Ben Gardner
Date: Wed Dec 07 2005 - 12:33:45 EST


Provides a SMBus/I2C driver for the AMD CS5535, modeled after the
scx200_acb driver.

Signed-off-by: Ben Gardner <bgardner@xxxxxxxxxx>
drivers/i2c/busses/Kconfig | 11
drivers/i2c/busses/Makefile | 1
drivers/i2c/busses/i2c-cs5535.c | 553 ++++++++++++++++++++++++++++++++++++++++
include/linux/i2c-id.h | 1
4 files changed, 566 insertions(+)

Index: linux-2.6.14/drivers/i2c/busses/Makefile
===================================================================
--- linux-2.6.14.orig/drivers/i2c/busses/Makefile
+++ linux-2.6.14/drivers/i2c/busses/Makefile
@@ -9,6 +9,7 @@ obj-$(CONFIG_I2C_AMD756) += i2c-amd756.o
obj-$(CONFIG_I2C_AMD756_S4882) += i2c-amd756-s4882.o
obj-$(CONFIG_I2C_AMD8111) += i2c-amd8111.o
obj-$(CONFIG_I2C_AU1550) += i2c-au1550.o
+obj-$(CONFIG_I2C_CS5535) += i2c-cs5535.o
obj-$(CONFIG_I2C_ELEKTOR) += i2c-elektor.o
obj-$(CONFIG_I2C_HYDRA) += i2c-hydra.o
obj-$(CONFIG_I2C_I801) += i2c-i801.o
Index: linux-2.6.14/drivers/i2c/busses/Kconfig
===================================================================
--- linux-2.6.14.orig/drivers/i2c/busses/Kconfig
+++ linux-2.6.14/drivers/i2c/busses/Kconfig
@@ -84,6 +84,17 @@ config I2C_AU1550
This driver can also be built as a module. If so, the module
will be called i2c-au1550.

+config I2C_CS5535
+ tristate "AMD CS5535 SMBus (Geode GX)"
+ depends on I2C && CS5535 && EXPERIMENTAL
+ help
+ Enable the use of the SMB controller on the CS5535 companion device.
+
+ If you don't know what to do here, say N.
+
+ This support is also available as a module. If so, the module
+ will be called i2c-cs5535.
+
config I2C_ELEKTOR
tristate "Elektor ISA card"
depends on I2C && ISA && BROKEN_ON_SMP
Index: linux-2.6.14/drivers/i2c/busses/i2c-cs5535.c
===================================================================
--- /dev/null
+++ linux-2.6.14/drivers/i2c/busses/i2c-cs5535.c
@@ -0,0 +1,553 @@
+/* linux/drivers/i2c/i2c-cs5535.c
+ *
+ * Copyright (c) 2005 Ben Gardner <bgardner@xxxxxxxxxx>
+ *
+ * AMD CS5535 SMB support - mostly identical to
+ * National Semiconductor SCx200 ACCESS.bus support
+ * except for the detection routine.
+ *
+ * Based on scx200_acb.c which is:
+ * Copyright (c) 2001,2002 Christer Weinigel <wingel@xxxxxxxxxxxxxxx>
+ *
+ * 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; version 2 of the License.
+ *
+ * TODO: the Access.Bus logic should be put in a separate file, as it could
+ * be shared with the scx200.
+ */
+
+#include <linux/config.h>
+#include <linux/module.h>
+#include <linux/errno.h>
+#include <linux/kernel.h>
+#include <linux/init.h>
+#include <linux/i2c.h>
+#include <linux/smp_lock.h>
+#include <linux/pci.h>
+#include <linux/delay.h>
+#include <linux/sched.h>
+#include <linux/interrupt.h>
+#include <asm/msr.h>
+#include <asm/io.h>
+
+#define NAME "cs5535_smb"
+
+MODULE_AUTHOR("Ben Gardner <bgardner@xxxxxxxxxx>");
+MODULE_DESCRIPTION("AMD CS5535 SMB Driver");
+MODULE_LICENSE("GPL");
+
+/* Needed to see if the cs5535 is present */
+extern u32 cs5535_gpio_base;
+
+#define MSR_LBAR_SMB 0x5140000B
+#define SMB_IO_SIZE 8
+
+#undef DEBUG
+
+#ifdef DEBUG
+#define DBG(x...) printk(KERN_DEBUG NAME ": " x)
+#else
+#define DBG(x...)
+#endif
+
+/* The hardware supports interrupt driven mode too, but I haven't
+ implemented that. */
+#define POLL_TIMEOUT (HZ)
+
+enum cs5535_smb_state {
+ state_idle,
+ state_address,
+ state_command,
+ state_repeat_start,
+ state_quick,
+ state_read,
+ state_write,
+};
+
+static const char *cs5535_smb_state_name[] = {
+ "idle",
+ "address",
+ "command",
+ "repeat_start",
+ "quick",
+ "read",
+ "write",
+};
+
+/* Physical interface */
+struct cs5535_smb_iface {
+ struct i2c_adapter adapter;
+ u32 base;
+ struct semaphore sem;
+
+ /* State machine data */
+ enum cs5535_smb_state state;
+ int result;
+ u8 address_byte;
+ u8 command;
+ u8 *ptr;
+ char needs_reset;
+ u32 len;
+};
+
+
+/* Register Definitions */
+#define SMBSDA (iface->base + 0)
+#define SMBST (iface->base + 1)
+#define SMBST_SDAST 0x40 /* SDA Status */
+#define SMBST_BER 0x20
+#define SMBST_NEGACK 0x10 /* Negative Acknowledge */
+#define SMBST_STASTR 0x08 /* Stall After Start */
+#define SMBST_MASTER 0x02
+#define SMBCST (iface->base + 2)
+#define SMBCST_BB 0x02
+#define SMBCTL1 (iface->base + 3)
+#define SMBCTL1_STASTRE 0x80
+#define SMBCTL1_NMINTE 0x40
+#define SMBCTL1_ACK 0x10
+#define SMBCTL1_INTEN 0x04
+#define SMBCTL1_STOP 0x02
+#define SMBCTL1_START 0x01
+#define SMBADDR (iface->base + 4)
+#define SMBCTL2 (iface->base + 5)
+#define SMBCTL2_ENABLE 0x01
+
+/************************************************************************/
+
+static u8 smb_inb(u32 p)
+{
+ u8 ch = inb(p);
+ udelay(5);
+ return ch;
+}
+
+static void smb_outb(u8 ch, u32 p)
+{
+ outb(ch, p);
+ udelay(5);
+}
+
+static void cs5535_smb_machine(struct cs5535_smb_iface *iface, u8 status)
+{
+ const char *errmsg;
+
+ DBG("state %s, status = 0x%02x\n",
+ cs5535_smb_state_name[iface->state], status);
+
+ if (status & SMBST_BER) {
+ errmsg = "bus error";
+ goto error;
+ }
+ if (!(status & SMBST_MASTER)) {
+ errmsg = "not master";
+ goto error;
+ }
+ if (status & SMBST_NEGACK) {
+ DBG("negative acknowledge in state %s\n",
+ cs5535_smb_state_name[iface->state]);
+
+ iface->state = state_idle;
+ iface->result = -ENXIO;
+
+ smb_outb(smb_inb(SMBCTL1) | SMBCTL1_STOP, SMBCTL1);
+ smb_outb(SMBST_STASTR | SMBST_NEGACK, SMBST);
+ smb_outb(0, SMBST); /* Status Reg bug workaround */
+ return;
+ }
+
+ switch (iface->state) {
+ case state_idle:
+ dev_warn(&iface->adapter.dev, "interrupt in idle state\n");
+ break;
+
+ case state_address:
+ /* Do a pointer write first */
+ smb_outb(iface->address_byte & ~1, SMBSDA);
+
+ iface->state = state_command;
+ break;
+
+ case state_command:
+ smb_outb(iface->command, SMBSDA);
+
+ if (iface->address_byte & 1)
+ iface->state = state_repeat_start;
+ else
+ iface->state = state_write;
+ break;
+
+ case state_repeat_start:
+ smb_outb(smb_inb(SMBCTL1) | SMBCTL1_START, SMBCTL1);
+ /* fallthrough */
+
+ case state_quick:
+ if (iface->address_byte & 1) {
+ if (iface->len == 1)
+ smb_outb(smb_inb(SMBCTL1) | SMBCTL1_ACK,
+ SMBCTL1);
+ else
+ smb_outb(smb_inb(SMBCTL1) & ~SMBCTL1_ACK,
+ SMBCTL1);
+ smb_outb(iface->address_byte, SMBSDA);
+
+ iface->state = state_read;
+ } else {
+ smb_outb(iface->address_byte, SMBSDA);
+
+ iface->state = state_write;
+ }
+ break;
+
+ case state_read:
+ /* Set ACK if receiving the last byte */
+ if (iface->len == 1)
+ smb_outb(smb_inb(SMBCTL1) | SMBCTL1_ACK, SMBCTL1);
+ else
+ smb_outb(smb_inb(SMBCTL1) & ~SMBCTL1_ACK, SMBCTL1);
+
+ *iface->ptr++ = smb_inb(SMBSDA);
+ --iface->len;
+
+ if (iface->len == 0) {
+ iface->result = 0;
+ iface->state = state_idle;
+ smb_outb(smb_inb(SMBCTL1) | SMBCTL1_STOP, SMBCTL1);
+ }
+
+ break;
+
+ case state_write:
+ if (iface->len == 0) {
+ iface->result = 0;
+ iface->state = state_idle;
+ smb_outb(smb_inb(SMBCTL1) | SMBCTL1_STOP, SMBCTL1);
+ break;
+ }
+
+ smb_outb(*iface->ptr++, SMBSDA);
+ --iface->len;
+
+ break;
+ }
+
+ return;
+
+error:
+ dev_err(&iface->adapter.dev, "%s in state %s\n", errmsg,
+ cs5535_smb_state_name[iface->state]);
+
+ iface->state = state_idle;
+ iface->result = -EIO;
+ iface->needs_reset = 1;
+}
+
+static void cs5535_smb_timeout(struct cs5535_smb_iface *iface)
+{
+ dev_err(&iface->adapter.dev, "timeout in state %s\n",
+ cs5535_smb_state_name[iface->state]);
+
+ iface->state = state_idle;
+ iface->result = -EIO;
+ iface->needs_reset = 1;
+}
+
+static void cs5535_smb_poll(struct cs5535_smb_iface *iface)
+{
+ u8 status = 0;
+ unsigned long timeout;
+
+ timeout = jiffies + POLL_TIMEOUT;
+ while (time_before(jiffies, timeout)) {
+
+ /* The i2c bus takes 9us per bit, 10 bits per transaction.
+ * This amounts to ~100us per char. Since that time is quite
+ * small and we can wait longer, we'll just yield.
+ */
+ yield();
+
+ status = smb_inb(SMBST);
+ if ((status & (SMBST_SDAST | SMBST_BER | SMBST_NEGACK)) != 0) {
+ cs5535_smb_machine(iface, status);
+ return;
+ }
+ }
+
+ cs5535_smb_timeout(iface);
+}
+
+static void cs5535_smb_reset(struct cs5535_smb_iface *iface)
+{
+ /* Disable the ACCESS.bus device and Configure the SCL
+ * frequency: 16 clock cycles */
+
+ smb_outb(0x70, SMBCTL2); /* clock time is 4.7 us */
+ /* interrupt mode */
+ smb_outb(SMBCTL1_INTEN, SMBCTL1);
+ /* Disable slave address */
+ smb_outb(0, SMBADDR);
+ /* Enable the ACCESS.bus device */
+ smb_outb(smb_inb(SMBCTL2) | SMBCTL2_ENABLE, SMBCTL2);
+ /* Free STALL after START */
+ smb_outb(smb_inb(SMBCTL1) & ~(SMBCTL1_STASTRE | SMBCTL1_NMINTE), SMBCTL1);
+ /* Send a STOP */
+ smb_outb(smb_inb(SMBCTL1) | SMBCTL1_STOP, SMBCTL1);
+ /* Clear BER, NEGACK and STASTR bits */
+ smb_outb(SMBST_BER | SMBST_NEGACK | SMBST_STASTR, SMBST);
+ smb_outb(0, SMBST); /* Status Reg bug workaround */
+
+ /* Clear BB bit */
+ smb_outb(smb_inb(SMBCST) | SMBCST_BB, SMBCST);
+}
+
+static s32 cs5535_smb_smbus_xfer(struct i2c_adapter *adapter,
+ u16 address, unsigned short flags,
+ char rw, u8 command, int size,
+ union i2c_smbus_data *data)
+{
+ struct cs5535_smb_iface *iface = i2c_get_adapdata(adapter);
+ int len;
+ u8 *buffer;
+ u16 cur_word;
+ int rc;
+
+ switch (size) {
+ case I2C_SMBUS_QUICK:
+ len = 0;
+ buffer = NULL;
+ break;
+
+ case I2C_SMBUS_BYTE:
+ if (rw == I2C_SMBUS_READ) {
+ len = 1;
+ buffer = &data->byte;
+ } else {
+ len = 1;
+ buffer = &command;
+ }
+ break;
+
+ case I2C_SMBUS_BYTE_DATA:
+ len = 1;
+ buffer = &data->byte;
+ break;
+
+ case I2C_SMBUS_WORD_DATA:
+ len = 2;
+ cur_word = cpu_to_le16(data->word);
+ buffer = (u8 *)&cur_word;
+ break;
+
+ case I2C_SMBUS_BLOCK_DATA:
+ len = data->block[0];
+ buffer = &data->block[1];
+ break;
+
+ default:
+ return -EINVAL;
+ }
+
+ DBG("size=%d, address=0x%x, command=0x%x, len=%d, read=%d\n",
+ size, address, command, len, rw == I2C_SMBUS_READ);
+
+ if (!len && rw == I2C_SMBUS_READ) {
+ dev_warn(&adapter->dev, "zero length read\n");
+ return -EINVAL;
+ }
+
+ if (len && !buffer) {
+ dev_warn(&adapter->dev, "nonzero length but no buffer\n");
+ return -EFAULT;
+ }
+
+ down(&iface->sem);
+
+ iface->address_byte = address << 1;
+ if (rw == I2C_SMBUS_READ)
+ iface->address_byte |= 1;
+ iface->command = command;
+ iface->ptr = buffer;
+ iface->len = len;
+ iface->result = -EINVAL;
+ iface->needs_reset = 0;
+
+ smb_outb(smb_inb(SMBCTL1) | SMBCTL1_START, SMBCTL1);
+
+ if (size == I2C_SMBUS_QUICK || size == I2C_SMBUS_BYTE)
+ iface->state = state_quick;
+ else
+ iface->state = state_address;
+
+ while (iface->state != state_idle)
+ cs5535_smb_poll(iface);
+
+ if (iface->needs_reset)
+ cs5535_smb_reset(iface);
+
+ rc = iface->result;
+
+ up(&iface->sem);
+
+ if (rc == 0 && size == I2C_SMBUS_WORD_DATA && rw == I2C_SMBUS_READ)
+ data->word = le16_to_cpu(cur_word);
+
+#ifdef DEBUG
+ DBG(": transfer done, result: %d", rc);
+ if (buffer) {
+ int i;
+ printk(" data:");
+ for (i = 0; i < len; ++i)
+ printk(" %02x", buffer[i]);
+ }
+ printk("\n");
+#endif
+
+ return rc;
+}
+
+static u32 cs5535_smb_func(struct i2c_adapter *adapter)
+{
+ return I2C_FUNC_SMBUS_QUICK | I2C_FUNC_SMBUS_BYTE |
+ I2C_FUNC_SMBUS_BYTE_DATA | I2C_FUNC_SMBUS_WORD_DATA |
+ I2C_FUNC_SMBUS_BLOCK_DATA;
+}
+
+static struct i2c_algorithm cs5535_smb_algorithm = {
+ .smbus_xfer = cs5535_smb_smbus_xfer,
+ .functionality = cs5535_smb_func,
+};
+static struct cs5535_smb_iface *cs5535_iface;
+
+static int cs5535_smb_probe(struct cs5535_smb_iface *iface)
+{
+ u8 val;
+
+ cs5535_smb_reset(iface);
+
+ /* Disable the ACCESS.bus device and Configure the SCL
+ * frequency: 16 clock cycles */
+ smb_outb(0x70, SMBCTL2);
+ if (smb_inb(SMBCTL2) != 0x70) {
+ DBG("SMBCTL2 readback failed\n");
+ return -ENXIO;
+ }
+
+ smb_outb(smb_inb(SMBCTL1) | SMBCTL1_NMINTE, SMBCTL1);
+
+ val = smb_inb(SMBCTL1);
+ if (val) {
+ DBG("disabled, but SMBCTL1=0x%02x\n", val);
+ return -ENXIO;
+ }
+
+ smb_outb(smb_inb(SMBCTL2) | SMBCTL2_ENABLE, SMBCTL2);
+
+ smb_outb(smb_inb(SMBCTL1) | SMBCTL1_NMINTE, SMBCTL1);
+
+ val = smb_inb(SMBCTL1);
+ if ((val & SMBCTL1_NMINTE) != SMBCTL1_NMINTE) {
+ DBG("enabled, but NMINTE won't be set, SMBCTL1=0x%02x\n", val);
+ return -ENXIO;
+ }
+
+ return 0;
+}
+
+static int __init cs5535_smb_create(int base, int index)
+{
+ struct cs5535_smb_iface *iface;
+ struct i2c_adapter *adapter;
+ int rc = 0;
+
+ iface = kzalloc(sizeof(*iface), GFP_KERNEL);
+ if (!iface) {
+ printk(KERN_ERR NAME ": can't allocate memory\n");
+ rc = -ENOMEM;
+ goto errout;
+ }
+
+ adapter = &iface->adapter;
+ i2c_set_adapdata(adapter, iface);
+ snprintf(adapter->name, I2C_NAME_SIZE, "CS5535 SMB%d", index);
+ adapter->owner = THIS_MODULE;
+ adapter->id = I2C_HW_SMBUS_CS5535;
+ adapter->algo = &cs5535_smb_algorithm;
+ adapter->class = I2C_CLASS_HWMON;
+
+ init_MUTEX(&iface->sem);
+
+ iface->base = base;
+ if (request_region(iface->base, SMB_IO_SIZE, adapter->name) == 0) {
+ printk(KERN_ERR NAME ": request_region(%d) failed\n",
+ iface->base);
+ rc = -EBUSY;
+ goto errout;
+ }
+
+ rc = cs5535_smb_probe(iface);
+ if (rc) {
+ dev_warn(&adapter->dev, "probe failed\n");
+ goto errout;
+ }
+
+ cs5535_smb_reset(iface);
+
+ if (i2c_add_adapter(adapter) < 0) {
+ dev_err(&adapter->dev, "failed to register\n");
+ rc = -ENODEV;
+ goto errout;
+ }
+
+ cs5535_iface = iface;
+
+ return 0;
+
+errout:
+ if (iface) {
+ if (iface->base)
+ release_region(iface->base, SMB_IO_SIZE);
+ kfree(iface);
+ }
+ return rc;
+}
+
+static int __init cs5535_smb_init(void)
+{
+ u32 low32, high32;
+ u32 smb_base;
+
+ pr_debug(NAME ": AMD CS5535 SMB Driver\n");
+
+ if (cs5535_gpio_base == 0) {
+ printk(KERN_WARNING NAME ": CS5535 GPIO not present\n");
+ return -ENODEV;
+ }
+
+ /* Grab & reserve the SMB I/O range */
+ rdmsr(MSR_LBAR_SMB, low32, high32);
+
+ /* Check the mask and whether SMB is enabled */
+ if (high32 != 0x0000F001) {
+ /* TODO: enable SMB IO mappings via LBAR? */
+ printk(KERN_WARNING NAME ": SMBus not enabled\n");
+ return -ENODEV;
+ }
+
+ /* SMBus IO size is 8 bytes */
+ smb_base = low32 & 0x0000FFF8;
+
+ return cs5535_smb_create(smb_base, 0);
+}
+
+static void __exit cs5535_smb_cleanup(void)
+{
+ if (cs5535_iface != NULL) {
+ release_region(cs5535_iface->base, SMB_IO_SIZE);
+ i2c_del_adapter(&cs5535_iface->adapter);
+ kfree(cs5535_iface);
+ }
+}
+
+module_init(cs5535_smb_init);
+module_exit(cs5535_smb_cleanup);
+
Index: linux-2.6.14/include/linux/i2c-id.h
===================================================================
--- linux-2.6.14.orig/include/linux/i2c-id.h
+++ linux-2.6.14/include/linux/i2c-id.h
@@ -256,6 +256,7 @@
#define I2C_HW_SMBUS_OV518 0x04000f /* OV518(+) USB 1.1 webcam ICs */
#define I2C_HW_SMBUS_OV519 0x040010 /* OV519 USB 1.1 webcam IC */
#define I2C_HW_SMBUS_OVFX2 0x040011 /* Cypress/OmniVision FX2 webcam */
+#define I2C_HW_SMBUS_CS5535 0x040012

/* --- ISA pseudo-adapter */
#define I2C_HW_ISA 0x050000