0365ba7fb1
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>
317 lines
7.9 KiB
C
317 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);
|