linux/drivers/i2c/busses/i2c-pmac-smu.c
Benjamin Herrenschmidt 0365ba7fb1 [PATCH] ppc64: SMU driver update & i2c support
The SMU is the "system controller" chip used by Apple recent G5 machines
including the iMac G5.  It drives things like fans, i2c busses, real time
clock, etc...

The current kernel contains a very crude driver that doesn't do much more
than reading the real time clock synchronously.  This is a completely
rewritten driver that provides interrupt based command queuing, a userland
interface, and an i2c/smbus driver for accessing the devices hanging off
the SMU i2c busses like temperature sensors.  This driver is a basic block
for upcoming work on thermal control for those machines, among others.

Signed-off-by: Benjamin Herrenschmidt <benh@kernel.crashing.org>
Cc: Jean Delvare <khali@linux-fr.org>
Cc: Greg KH <greg@kroah.com>
Signed-off-by: Andrew Morton <akpm@osdl.org>
Signed-off-by: Linus Torvalds <torvalds@osdl.org>
2005-09-22 22:17:35 -07:00

316 lines
7.9 KiB
C

/*
i2c Support for Apple SMU Controller
Copyright (c) 2005 Benjamin Herrenschmidt, IBM Corp.
<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 program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
#include <linux/config.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/i2c.h>
#include <linux/init.h>
#include <linux/completion.h>
#include <linux/device.h>
#include <asm/prom.h>
#include <asm/of_device.h>
#include <asm/smu.h>
static int probe;
MODULE_AUTHOR("Benjamin Herrenschmidt <benh@kernel.crashing.org>");
MODULE_DESCRIPTION("I2C driver for Apple's SMU");
MODULE_LICENSE("GPL");
module_param(probe, bool, 0);
/* Physical interface */
struct smu_iface
{
struct i2c_adapter adapter;
struct completion complete;
u32 busid;
};
static void smu_i2c_done(struct smu_i2c_cmd *cmd, void *misc)
{
struct smu_iface *iface = misc;
complete(&iface->complete);
}
/*
* SMBUS-type transfer entrypoint
*/
static s32 smu_smbus_xfer( struct i2c_adapter* adap,
u16 addr,
unsigned short flags,
char read_write,
u8 command,
int size,
union i2c_smbus_data* data)
{
struct smu_iface *iface = i2c_get_adapdata(adap);
struct smu_i2c_cmd cmd;
int rc = 0;
int read = (read_write == I2C_SMBUS_READ);
cmd.info.bus = iface->busid;
cmd.info.devaddr = (addr << 1) | (read ? 0x01 : 0x00);
/* Prepare datas & select mode */
switch (size) {
case I2C_SMBUS_QUICK:
cmd.info.type = SMU_I2C_TRANSFER_SIMPLE;
cmd.info.datalen = 0;
break;
case I2C_SMBUS_BYTE:
cmd.info.type = SMU_I2C_TRANSFER_SIMPLE;
cmd.info.datalen = 1;
if (!read)
cmd.info.data[0] = data->byte;
break;
case I2C_SMBUS_BYTE_DATA:
cmd.info.type = SMU_I2C_TRANSFER_STDSUB;
cmd.info.datalen = 1;
cmd.info.sublen = 1;
cmd.info.subaddr[0] = command;
cmd.info.subaddr[1] = 0;
cmd.info.subaddr[2] = 0;
if (!read)
cmd.info.data[0] = data->byte;
break;
case I2C_SMBUS_WORD_DATA:
cmd.info.type = SMU_I2C_TRANSFER_STDSUB;
cmd.info.datalen = 2;
cmd.info.sublen = 1;
cmd.info.subaddr[0] = command;
cmd.info.subaddr[1] = 0;
cmd.info.subaddr[2] = 0;
if (!read) {
cmd.info.data[0] = data->byte & 0xff;
cmd.info.data[1] = (data->byte >> 8) & 0xff;
}
break;
/* Note that these are broken vs. the expected smbus API where
* on reads, the lenght is actually returned from the function,
* but I think the current API makes no sense and I don't want
* any driver that I haven't verified for correctness to go
* anywhere near a pmac i2c bus anyway ...
*/
case I2C_SMBUS_BLOCK_DATA:
cmd.info.type = SMU_I2C_TRANSFER_STDSUB;
cmd.info.datalen = data->block[0] + 1;
if (cmd.info.datalen > 6)
return -EINVAL;
if (!read)
memcpy(cmd.info.data, data->block, cmd.info.datalen);
cmd.info.sublen = 1;
cmd.info.subaddr[0] = command;
cmd.info.subaddr[1] = 0;
cmd.info.subaddr[2] = 0;
break;
case I2C_SMBUS_I2C_BLOCK_DATA:
cmd.info.type = SMU_I2C_TRANSFER_STDSUB;
cmd.info.datalen = data->block[0];
if (cmd.info.datalen > 7)
return -EINVAL;
if (!read)
memcpy(cmd.info.data, &data->block[1],
cmd.info.datalen);
cmd.info.sublen = 1;
cmd.info.subaddr[0] = command;
cmd.info.subaddr[1] = 0;
cmd.info.subaddr[2] = 0;
break;
default:
return -EINVAL;
}
/* Turn a standardsub read into a combined mode access */
if (read_write == I2C_SMBUS_READ &&
cmd.info.type == SMU_I2C_TRANSFER_STDSUB)
cmd.info.type = SMU_I2C_TRANSFER_COMBINED;
/* Finish filling command and submit it */
cmd.done = smu_i2c_done;
cmd.misc = iface;
rc = smu_queue_i2c(&cmd);
if (rc < 0)
return rc;
wait_for_completion(&iface->complete);
rc = cmd.status;
if (!read || rc < 0)
return rc;
switch (size) {
case I2C_SMBUS_BYTE:
case I2C_SMBUS_BYTE_DATA:
data->byte = cmd.info.data[0];
break;
case I2C_SMBUS_WORD_DATA:
data->word = ((u16)cmd.info.data[1]) << 8;
data->word |= cmd.info.data[0];
break;
/* Note that these are broken vs. the expected smbus API where
* on reads, the lenght is actually returned from the function,
* but I think the current API makes no sense and I don't want
* any driver that I haven't verified for correctness to go
* anywhere near a pmac i2c bus anyway ...
*/
case I2C_SMBUS_BLOCK_DATA:
case I2C_SMBUS_I2C_BLOCK_DATA:
memcpy(&data->block[0], cmd.info.data, cmd.info.datalen);
break;
}
return rc;
}
static u32
smu_smbus_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;
}
/* For now, we only handle combined mode (smbus) */
static struct i2c_algorithm smu_algorithm = {
.smbus_xfer = smu_smbus_xfer,
.functionality = smu_smbus_func,
};
static int create_iface(struct device_node *np, struct device *dev)
{
struct smu_iface* iface;
u32 *reg, busid;
int rc;
reg = (u32 *)get_property(np, "reg", NULL);
if (reg == NULL) {
printk(KERN_ERR "i2c-pmac-smu: can't find bus number !\n");
return -ENXIO;
}
busid = *reg;
iface = kmalloc(sizeof(struct smu_iface), GFP_KERNEL);
if (iface == NULL) {
printk(KERN_ERR "i2c-pmac-smu: can't allocate inteface !\n");
return -ENOMEM;
}
memset(iface, 0, sizeof(struct smu_iface));
init_completion(&iface->complete);
iface->busid = busid;
dev_set_drvdata(dev, iface);
sprintf(iface->adapter.name, "smu-i2c-%02x", busid);
iface->adapter.algo = &smu_algorithm;
iface->adapter.algo_data = NULL;
iface->adapter.client_register = NULL;
iface->adapter.client_unregister = NULL;
i2c_set_adapdata(&iface->adapter, iface);
iface->adapter.dev.parent = dev;
rc = i2c_add_adapter(&iface->adapter);
if (rc) {
printk(KERN_ERR "i2c-pamc-smu.c: Adapter %s registration "
"failed\n", iface->adapter.name);
i2c_set_adapdata(&iface->adapter, NULL);
}
if (probe) {
unsigned char addr;
printk("Probe: ");
for (addr = 0x00; addr <= 0x7f; addr++) {
if (i2c_smbus_xfer(&iface->adapter,addr,
0,0,0,I2C_SMBUS_QUICK,NULL) >= 0)
printk("%02x ", addr);
}
printk("\n");
}
printk(KERN_INFO "SMU i2c bus %x registered\n", busid);
return 0;
}
static int dispose_iface(struct device *dev)
{
struct smu_iface *iface = dev_get_drvdata(dev);
int rc;
rc = i2c_del_adapter(&iface->adapter);
i2c_set_adapdata(&iface->adapter, NULL);
/* We aren't that prepared to deal with this... */
if (rc)
printk("i2c-pmac-smu.c: Failed to remove bus %s !\n",
iface->adapter.name);
dev_set_drvdata(dev, NULL);
kfree(iface);
return 0;
}
static int create_iface_of_platform(struct of_device* dev,
const struct of_device_id *match)
{
return create_iface(dev->node, &dev->dev);
}
static int dispose_iface_of_platform(struct of_device* dev)
{
return dispose_iface(&dev->dev);
}
static struct of_device_id i2c_smu_match[] =
{
{
.compatible = "smu-i2c",
},
{},
};
static struct of_platform_driver i2c_smu_of_platform_driver =
{
.name = "i2c-smu",
.match_table = i2c_smu_match,
.probe = create_iface_of_platform,
.remove = dispose_iface_of_platform
};
static int __init i2c_pmac_smu_init(void)
{
of_register_driver(&i2c_smu_of_platform_driver);
return 0;
}
static void __exit i2c_pmac_smu_cleanup(void)
{
of_unregister_driver(&i2c_smu_of_platform_driver);
}
module_init(i2c_pmac_smu_init);
module_exit(i2c_pmac_smu_cleanup);