512 lines
11 KiB
C
512 lines
11 KiB
C
/*
|
|
* 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);
|
|
|