Provides a SMBus/I2C driver for the AMD CS5535, modeled after the scx200_acb driver. Signed-off-by: Ben Gardner <bgardner at wabtec.com> -------------- next part -------------- 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 at wabtec.com> + * + * 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 at nano-system.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; 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 at wabtec.com>"); +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