| /* |
| * arch/ppc/platforms/pmac_low_i2c.c |
| * |
| * Copyright (C) 2003 Ben. Herrenschmidt (benh@kernel.crashing.org) |
| * |
| * 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 file contains some low-level i2c access routines that |
| * need to be used by various bits of the PowerMac platform code |
| * at times where the real asynchronous & interrupt driven driver |
| * cannot be used. The API borrows some semantics from the darwin |
| * driver in order to ease the implementation of the platform |
| * properties parser |
| */ |
| |
| #include <linux/config.h> |
| #include <linux/types.h> |
| #include <linux/delay.h> |
| #include <linux/sched.h> |
| #include <linux/init.h> |
| #include <linux/module.h> |
| #include <linux/adb.h> |
| #include <linux/pmu.h> |
| #include <asm/keylargo.h> |
| #include <asm/uninorth.h> |
| #include <asm/io.h> |
| #include <asm/prom.h> |
| #include <asm/machdep.h> |
| #include <asm/pmac_low_i2c.h> |
| |
| #define MAX_LOW_I2C_HOST 4 |
| |
| #if 1 |
| #define DBG(x...) do {\ |
| printk(KERN_DEBUG "KW:" x); \ |
| } while(0) |
| #else |
| #define DBGG(x...) |
| #endif |
| |
| struct low_i2c_host; |
| |
| typedef int (*low_i2c_func_t)(struct low_i2c_host *host, u8 addr, u8 sub, u8 *data, int len); |
| |
| struct low_i2c_host |
| { |
| struct device_node *np; /* OF device node */ |
| struct semaphore mutex; /* Access mutex for use by i2c-keywest */ |
| low_i2c_func_t func; /* Access function */ |
| int is_open : 1; /* Poor man's access control */ |
| int mode; /* Current mode */ |
| int channel; /* Current channel */ |
| int num_channels; /* Number of channels */ |
| void __iomem * base; /* For keywest-i2c, base address */ |
| int bsteps; /* And register stepping */ |
| int speed; /* And speed */ |
| }; |
| |
| static struct low_i2c_host low_i2c_hosts[MAX_LOW_I2C_HOST]; |
| |
| /* No locking is necessary on allocation, we are running way before |
| * anything can race with us |
| */ |
| static struct low_i2c_host *find_low_i2c_host(struct device_node *np) |
| { |
| int i; |
| |
| for (i = 0; i < MAX_LOW_I2C_HOST; i++) |
| if (low_i2c_hosts[i].np == np) |
| return &low_i2c_hosts[i]; |
| return NULL; |
| } |
| |
| /* |
| * |
| * i2c-keywest implementation (UniNorth, U2, U3, Keylargo's) |
| * |
| */ |
| |
| /* |
| * Keywest i2c definitions borrowed from drivers/i2c/i2c-keywest.h, |
| * should be moved somewhere in include/asm-ppc/ |
| */ |
| /* Register indices */ |
| typedef enum { |
| reg_mode = 0, |
| reg_control, |
| reg_status, |
| reg_isr, |
| reg_ier, |
| reg_addr, |
| reg_subaddr, |
| reg_data |
| } reg_t; |
| |
| |
| /* Mode register */ |
| #define KW_I2C_MODE_100KHZ 0x00 |
| #define KW_I2C_MODE_50KHZ 0x01 |
| #define KW_I2C_MODE_25KHZ 0x02 |
| #define KW_I2C_MODE_DUMB 0x00 |
| #define KW_I2C_MODE_STANDARD 0x04 |
| #define KW_I2C_MODE_STANDARDSUB 0x08 |
| #define KW_I2C_MODE_COMBINED 0x0C |
| #define KW_I2C_MODE_MODE_MASK 0x0C |
| #define KW_I2C_MODE_CHAN_MASK 0xF0 |
| |
| /* Control register */ |
| #define KW_I2C_CTL_AAK 0x01 |
| #define KW_I2C_CTL_XADDR 0x02 |
| #define KW_I2C_CTL_STOP 0x04 |
| #define KW_I2C_CTL_START 0x08 |
| |
| /* Status register */ |
| #define KW_I2C_STAT_BUSY 0x01 |
| #define KW_I2C_STAT_LAST_AAK 0x02 |
| #define KW_I2C_STAT_LAST_RW 0x04 |
| #define KW_I2C_STAT_SDA 0x08 |
| #define KW_I2C_STAT_SCL 0x10 |
| |
| /* IER & ISR registers */ |
| #define KW_I2C_IRQ_DATA 0x01 |
| #define KW_I2C_IRQ_ADDR 0x02 |
| #define KW_I2C_IRQ_STOP 0x04 |
| #define KW_I2C_IRQ_START 0x08 |
| #define KW_I2C_IRQ_MASK 0x0F |
| |
| /* State machine states */ |
| enum { |
| state_idle, |
| state_addr, |
| state_read, |
| state_write, |
| state_stop, |
| state_dead |
| }; |
| |
| #define WRONG_STATE(name) do {\ |
| printk(KERN_DEBUG "KW: wrong state. Got %s, state: %s (isr: %02x)\n", \ |
| name, __kw_state_names[state], isr); \ |
| } while(0) |
| |
| static const char *__kw_state_names[] = { |
| "state_idle", |
| "state_addr", |
| "state_read", |
| "state_write", |
| "state_stop", |
| "state_dead" |
| }; |
| |
| static inline u8 __kw_read_reg(struct low_i2c_host *host, reg_t reg) |
| { |
| return in_8(host->base + (((unsigned)reg) << host->bsteps)); |
| } |
| |
| static inline void __kw_write_reg(struct low_i2c_host *host, reg_t reg, u8 val) |
| { |
| out_8(host->base + (((unsigned)reg) << host->bsteps), val); |
| (void)__kw_read_reg(host, reg_subaddr); |
| } |
| |
| #define kw_write_reg(reg, val) __kw_write_reg(host, reg, val) |
| #define kw_read_reg(reg) __kw_read_reg(host, reg) |
| |
| |
| /* Don't schedule, the g5 fan controller is too |
| * timing sensitive |
| */ |
| static u8 kw_wait_interrupt(struct low_i2c_host* host) |
| { |
| int i; |
| u8 isr; |
| |
| for (i = 0; i < 200000; i++) { |
| isr = kw_read_reg(reg_isr) & KW_I2C_IRQ_MASK; |
| if (isr != 0) |
| return isr; |
| udelay(1); |
| } |
| return isr; |
| } |
| |
| static int kw_handle_interrupt(struct low_i2c_host *host, int state, int rw, int *rc, u8 **data, int *len, u8 isr) |
| { |
| u8 ack; |
| |
| if (isr == 0) { |
| if (state != state_stop) { |
| DBG("KW: Timeout !\n"); |
| *rc = -EIO; |
| goto stop; |
| } |
| if (state == state_stop) { |
| ack = kw_read_reg(reg_status); |
| if (!(ack & KW_I2C_STAT_BUSY)) { |
| state = state_idle; |
| kw_write_reg(reg_ier, 0x00); |
| } |
| } |
| return state; |
| } |
| |
| if (isr & KW_I2C_IRQ_ADDR) { |
| ack = kw_read_reg(reg_status); |
| if (state != state_addr) { |
| kw_write_reg(reg_isr, KW_I2C_IRQ_ADDR); |
| WRONG_STATE("KW_I2C_IRQ_ADDR"); |
| *rc = -EIO; |
| goto stop; |
| } |
| if ((ack & KW_I2C_STAT_LAST_AAK) == 0) { |
| *rc = -ENODEV; |
| DBG("KW: NAK on address\n"); |
| return state_stop; |
| } else { |
| if (rw) { |
| state = state_read; |
| if (*len > 1) |
| kw_write_reg(reg_control, KW_I2C_CTL_AAK); |
| } else { |
| state = state_write; |
| kw_write_reg(reg_data, **data); |
| (*data)++; (*len)--; |
| } |
| } |
| kw_write_reg(reg_isr, KW_I2C_IRQ_ADDR); |
| } |
| |
| if (isr & KW_I2C_IRQ_DATA) { |
| if (state == state_read) { |
| **data = kw_read_reg(reg_data); |
| (*data)++; (*len)--; |
| kw_write_reg(reg_isr, KW_I2C_IRQ_DATA); |
| if ((*len) == 0) |
| state = state_stop; |
| else if ((*len) == 1) |
| kw_write_reg(reg_control, 0); |
| } else if (state == state_write) { |
| ack = kw_read_reg(reg_status); |
| if ((ack & KW_I2C_STAT_LAST_AAK) == 0) { |
| DBG("KW: nack on data write\n"); |
| *rc = -EIO; |
| goto stop; |
| } else if (*len) { |
| kw_write_reg(reg_data, **data); |
| (*data)++; (*len)--; |
| } else { |
| kw_write_reg(reg_control, KW_I2C_CTL_STOP); |
| state = state_stop; |
| *rc = 0; |
| } |
| kw_write_reg(reg_isr, KW_I2C_IRQ_DATA); |
| } else { |
| kw_write_reg(reg_isr, KW_I2C_IRQ_DATA); |
| WRONG_STATE("KW_I2C_IRQ_DATA"); |
| if (state != state_stop) { |
| *rc = -EIO; |
| goto stop; |
| } |
| } |
| } |
| |
| if (isr & KW_I2C_IRQ_STOP) { |
| kw_write_reg(reg_isr, KW_I2C_IRQ_STOP); |
| if (state != state_stop) { |
| WRONG_STATE("KW_I2C_IRQ_STOP"); |
| *rc = -EIO; |
| } |
| return state_idle; |
| } |
| |
| if (isr & KW_I2C_IRQ_START) |
| kw_write_reg(reg_isr, KW_I2C_IRQ_START); |
| |
| return state; |
| |
| stop: |
| kw_write_reg(reg_control, KW_I2C_CTL_STOP); |
| return state_stop; |
| } |
| |
| static int keywest_low_i2c_func(struct low_i2c_host *host, u8 addr, u8 subaddr, u8 *data, int len) |
| { |
| u8 mode_reg = host->speed; |
| int state = state_addr; |
| int rc = 0; |
| |
| /* Setup mode & subaddress if any */ |
| switch(host->mode) { |
| case pmac_low_i2c_mode_dumb: |
| printk(KERN_ERR "low_i2c: Dumb mode not supported !\n"); |
| return -EINVAL; |
| case pmac_low_i2c_mode_std: |
| mode_reg |= KW_I2C_MODE_STANDARD; |
| break; |
| case pmac_low_i2c_mode_stdsub: |
| mode_reg |= KW_I2C_MODE_STANDARDSUB; |
| kw_write_reg(reg_subaddr, subaddr); |
| break; |
| case pmac_low_i2c_mode_combined: |
| mode_reg |= KW_I2C_MODE_COMBINED; |
| kw_write_reg(reg_subaddr, subaddr); |
| break; |
| } |
| |
| /* Setup channel & clear pending irqs */ |
| kw_write_reg(reg_isr, kw_read_reg(reg_isr)); |
| kw_write_reg(reg_mode, mode_reg | (host->channel << 4)); |
| kw_write_reg(reg_status, 0); |
| |
| /* Set up address and r/w bit */ |
| kw_write_reg(reg_addr, addr); |
| |
| /* Start sending address & disable interrupt*/ |
| kw_write_reg(reg_ier, 0 /*KW_I2C_IRQ_MASK*/); |
| kw_write_reg(reg_control, KW_I2C_CTL_XADDR); |
| |
| /* State machine, to turn into an interrupt handler */ |
| while(state != state_idle) { |
| u8 isr = kw_wait_interrupt(host); |
| state = kw_handle_interrupt(host, state, addr & 1, &rc, &data, &len, isr); |
| } |
| |
| return rc; |
| } |
| |
| static void keywest_low_i2c_add(struct device_node *np) |
| { |
| struct low_i2c_host *host = find_low_i2c_host(NULL); |
| unsigned long *psteps, *prate, steps, aoffset = 0; |
| struct device_node *parent; |
| |
| if (host == NULL) { |
| printk(KERN_ERR "low_i2c: Can't allocate host for %s\n", |
| np->full_name); |
| return; |
| } |
| memset(host, 0, sizeof(*host)); |
| |
| init_MUTEX(&host->mutex); |
| host->np = of_node_get(np); |
| psteps = (unsigned long *)get_property(np, "AAPL,address-step", NULL); |
| steps = psteps ? (*psteps) : 0x10; |
| for (host->bsteps = 0; (steps & 0x01) == 0; host->bsteps++) |
| steps >>= 1; |
| parent = of_get_parent(np); |
| host->num_channels = 1; |
| if (parent && parent->name[0] == 'u') { |
| host->num_channels = 2; |
| aoffset = 3; |
| } |
| /* Select interface rate */ |
| host->speed = KW_I2C_MODE_100KHZ; |
| prate = (unsigned long *)get_property(np, "AAPL,i2c-rate", NULL); |
| if (prate) switch(*prate) { |
| case 100: |
| host->speed = KW_I2C_MODE_100KHZ; |
| break; |
| case 50: |
| host->speed = KW_I2C_MODE_50KHZ; |
| break; |
| case 25: |
| host->speed = KW_I2C_MODE_25KHZ; |
| break; |
| } |
| host->mode = pmac_low_i2c_mode_std; |
| host->base = ioremap(np->addrs[0].address + aoffset, |
| np->addrs[0].size); |
| host->func = keywest_low_i2c_func; |
| } |
| |
| /* |
| * |
| * PMU implementation |
| * |
| */ |
| |
| |
| #ifdef CONFIG_ADB_PMU |
| |
| static int pmu_low_i2c_func(struct low_i2c_host *host, u8 addr, u8 sub, u8 *data, int len) |
| { |
| // TODO |
| return -ENODEV; |
| } |
| |
| static void pmu_low_i2c_add(struct device_node *np) |
| { |
| struct low_i2c_host *host = find_low_i2c_host(NULL); |
| |
| if (host == NULL) { |
| printk(KERN_ERR "low_i2c: Can't allocate host for %s\n", |
| np->full_name); |
| return; |
| } |
| memset(host, 0, sizeof(*host)); |
| |
| init_MUTEX(&host->mutex); |
| host->np = of_node_get(np); |
| host->num_channels = 3; |
| host->mode = pmac_low_i2c_mode_std; |
| host->func = pmu_low_i2c_func; |
| } |
| |
| #endif /* CONFIG_ADB_PMU */ |
| |
| void __init pmac_init_low_i2c(void) |
| { |
| struct device_node *np; |
| |
| /* Probe keywest-i2c busses */ |
| np = of_find_compatible_node(NULL, "i2c", "keywest-i2c"); |
| while(np) { |
| keywest_low_i2c_add(np); |
| np = of_find_compatible_node(np, "i2c", "keywest-i2c"); |
| } |
| |
| #ifdef CONFIG_ADB_PMU |
| /* Probe PMU busses */ |
| np = of_find_node_by_name(NULL, "via-pmu"); |
| if (np) |
| pmu_low_i2c_add(np); |
| #endif /* CONFIG_ADB_PMU */ |
| |
| /* TODO: Add CUDA support as well */ |
| } |
| |
| int pmac_low_i2c_lock(struct device_node *np) |
| { |
| struct low_i2c_host *host = find_low_i2c_host(np); |
| |
| if (!host) |
| return -ENODEV; |
| down(&host->mutex); |
| return 0; |
| } |
| EXPORT_SYMBOL(pmac_low_i2c_lock); |
| |
| int pmac_low_i2c_unlock(struct device_node *np) |
| { |
| struct low_i2c_host *host = find_low_i2c_host(np); |
| |
| if (!host) |
| return -ENODEV; |
| up(&host->mutex); |
| return 0; |
| } |
| EXPORT_SYMBOL(pmac_low_i2c_unlock); |
| |
| |
| int pmac_low_i2c_open(struct device_node *np, int channel) |
| { |
| struct low_i2c_host *host = find_low_i2c_host(np); |
| |
| if (!host) |
| return -ENODEV; |
| |
| if (channel >= host->num_channels) |
| return -EINVAL; |
| |
| down(&host->mutex); |
| host->is_open = 1; |
| host->channel = channel; |
| |
| return 0; |
| } |
| EXPORT_SYMBOL(pmac_low_i2c_open); |
| |
| int pmac_low_i2c_close(struct device_node *np) |
| { |
| struct low_i2c_host *host = find_low_i2c_host(np); |
| |
| if (!host) |
| return -ENODEV; |
| |
| host->is_open = 0; |
| up(&host->mutex); |
| |
| return 0; |
| } |
| EXPORT_SYMBOL(pmac_low_i2c_close); |
| |
| int pmac_low_i2c_setmode(struct device_node *np, int mode) |
| { |
| struct low_i2c_host *host = find_low_i2c_host(np); |
| |
| if (!host) |
| return -ENODEV; |
| WARN_ON(!host->is_open); |
| host->mode = mode; |
| |
| return 0; |
| } |
| EXPORT_SYMBOL(pmac_low_i2c_setmode); |
| |
| int pmac_low_i2c_xfer(struct device_node *np, u8 addrdir, u8 subaddr, u8 *data, int len) |
| { |
| struct low_i2c_host *host = find_low_i2c_host(np); |
| |
| if (!host) |
| return -ENODEV; |
| WARN_ON(!host->is_open); |
| |
| return host->func(host, addrdir, subaddr, data, len); |
| } |
| EXPORT_SYMBOL(pmac_low_i2c_xfer); |
| |