/* * SMBus driver for ACPI Embedded Controller (v0.1) * * Copyright (c) 2007 Alexey Starikovskiy * * 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. */ #include <linux/acpi.h> #include <linux/wait.h> #include <linux/slab.h> #include <linux/delay.h> #include <linux/module.h> #include <linux/interrupt.h> #include <linux/dmi.h> #include "sbshc.h" #define PREFIX "ACPI: " #define ACPI_SMB_HC_CLASS "smbus_host_ctl" #define ACPI_SMB_HC_DEVICE_NAME "ACPI SMBus HC" struct acpi_smb_hc { struct acpi_ec *ec; struct mutex lock; wait_queue_head_t wait; u8 offset; u8 query_bit; smbus_alarm_callback callback; void *context; }; static int acpi_smbus_hc_add(struct acpi_device *device); static int acpi_smbus_hc_remove(struct acpi_device *device); static const struct acpi_device_id sbs_device_ids[] = { {"ACPI0001", 0}, {"ACPI0005", 0}, {"", 0}, }; MODULE_DEVICE_TABLE(acpi, sbs_device_ids); static struct acpi_driver acpi_smb_hc_driver = { .name = "smbus_hc", .class = ACPI_SMB_HC_CLASS, .ids = sbs_device_ids, .ops = { .add = acpi_smbus_hc_add, .remove = acpi_smbus_hc_remove, }, }; union acpi_smb_status { u8 raw; struct { u8 status:5; u8 reserved:1; u8 alarm:1; u8 done:1; } fields; }; enum acpi_smb_status_codes { SMBUS_OK = 0, SMBUS_UNKNOWN_FAILURE = 0x07, SMBUS_DEVICE_ADDRESS_NACK = 0x10, SMBUS_DEVICE_ERROR = 0x11, SMBUS_DEVICE_COMMAND_ACCESS_DENIED = 0x12, SMBUS_UNKNOWN_ERROR = 0x13, SMBUS_DEVICE_ACCESS_DENIED = 0x17, SMBUS_TIMEOUT = 0x18, SMBUS_HOST_UNSUPPORTED_PROTOCOL = 0x19, SMBUS_BUSY = 0x1a, SMBUS_PEC_ERROR = 0x1f, }; enum acpi_smb_offset { ACPI_SMB_PROTOCOL = 0, /* protocol, PEC */ ACPI_SMB_STATUS = 1, /* status */ ACPI_SMB_ADDRESS = 2, /* address */ ACPI_SMB_COMMAND = 3, /* command */ ACPI_SMB_DATA = 4, /* 32 data registers */ ACPI_SMB_BLOCK_COUNT = 0x24, /* number of data bytes */ ACPI_SMB_ALARM_ADDRESS = 0x25, /* alarm address */ ACPI_SMB_ALARM_DATA = 0x26, /* 2 bytes alarm data */ }; static bool macbook; static inline int smb_hc_read(struct acpi_smb_hc *hc, u8 address, u8 *data) { return ec_read(hc->offset + address, data); } static inline int smb_hc_write(struct acpi_smb_hc *hc, u8 address, u8 data) { return ec_write(hc->offset + address, data); } static inline int smb_check_done(struct acpi_smb_hc *hc) { union acpi_smb_status status = {.raw = 0}; smb_hc_read(hc, ACPI_SMB_STATUS, &status.raw); return status.fields.done && (status.fields.status == SMBUS_OK); } static int wait_transaction_complete(struct acpi_smb_hc *hc, int timeout) { if (wait_event_timeout(hc->wait, smb_check_done(hc), msecs_to_jiffies(timeout))) return 0; /* * After the timeout happens, OS will try to check the status of SMbus. * If the status is what OS expected, it will be regarded as the bogus * timeout. */ if (smb_check_done(hc)) return 0; else return -ETIME; } static int acpi_smbus_transaction(struct acpi_smb_hc *hc, u8 protocol, u8 address, u8 command, u8 *data, u8 length) { int ret = -EFAULT, i; u8 temp, sz = 0; if (!hc) { printk(KERN_ERR PREFIX "host controller is not configured\n"); return ret; } mutex_lock(&hc->lock); if (macbook) udelay(5); if (smb_hc_read(hc, ACPI_SMB_PROTOCOL, &temp)) goto end; if (temp) { ret = -EBUSY; goto end; } smb_hc_write(hc, ACPI_SMB_COMMAND, command); if (!(protocol & 0x01)) { smb_hc_write(hc, ACPI_SMB_BLOCK_COUNT, length); for (i = 0; i < length; ++i) smb_hc_write(hc, ACPI_SMB_DATA + i, data[i]); } smb_hc_write(hc, ACPI_SMB_ADDRESS, address << 1); smb_hc_write(hc, ACPI_SMB_PROTOCOL, protocol); /* * Wait for completion. Save the status code, data size, * and data into the return package (if required by the protocol). */ ret = wait_transaction_complete(hc, 1000); if (ret || !(protocol & 0x01)) goto end; switch (protocol) { case SMBUS_RECEIVE_BYTE: case SMBUS_READ_BYTE: sz = 1; break; case SMBUS_READ_WORD: sz = 2; break; case SMBUS_READ_BLOCK: if (smb_hc_read(hc, ACPI_SMB_BLOCK_COUNT, &sz)) { ret = -EFAULT; goto end; } sz &= 0x1f; break; } for (i = 0; i < sz; ++i) smb_hc_read(hc, ACPI_SMB_DATA + i, &data[i]); end: mutex_unlock(&hc->lock); return ret; } int acpi_smbus_read(struct acpi_smb_hc *hc, u8 protocol, u8 address, u8 command, u8 *data) { return acpi_smbus_transaction(hc, protocol, address, command, data, 0); } EXPORT_SYMBOL_GPL(acpi_smbus_read); int acpi_smbus_write(struct acpi_smb_hc *hc, u8 protocol, u8 address, u8 command, u8 *data, u8 length) { return acpi_smbus_transaction(hc, protocol, address, command, data, length); } EXPORT_SYMBOL_GPL(acpi_smbus_write); int acpi_smbus_register_callback(struct acpi_smb_hc *hc, smbus_alarm_callback callback, void *context) { mutex_lock(&hc->lock); hc->callback = callback; hc->context = context; mutex_unlock(&hc->lock); return 0; } EXPORT_SYMBOL_GPL(acpi_smbus_register_callback); int acpi_smbus_unregister_callback(struct acpi_smb_hc *hc) { mutex_lock(&hc->lock); hc->callback = NULL; hc->context = NULL; mutex_unlock(&hc->lock); return 0; } EXPORT_SYMBOL_GPL(acpi_smbus_unregister_callback); static inline void acpi_smbus_callback(void *context) { struct acpi_smb_hc *hc = context; if (hc->callback) hc->callback(hc->context); } static int smbus_alarm(void *context) { struct acpi_smb_hc *hc = context; union acpi_smb_status status; u8 address; if (smb_hc_read(hc, ACPI_SMB_STATUS, &status.raw)) return 0; /* Check if it is only a completion notify */ if (status.fields.done) wake_up(&hc->wait); if (!status.fields.alarm) return 0; mutex_lock(&hc->lock); smb_hc_read(hc, ACPI_SMB_ALARM_ADDRESS, &address); status.fields.alarm = 0; smb_hc_write(hc, ACPI_SMB_STATUS, status.raw); /* We are only interested in events coming from known devices */ switch (address >> 1) { case ACPI_SBS_CHARGER: case ACPI_SBS_MANAGER: case ACPI_SBS_BATTERY: acpi_os_execute(OSL_NOTIFY_HANDLER, acpi_smbus_callback, hc); default:; } mutex_unlock(&hc->lock); return 0; } typedef int (*acpi_ec_query_func) (void *data); extern int acpi_ec_add_query_handler(struct acpi_ec *ec, u8 query_bit, acpi_handle handle, acpi_ec_query_func func, void *data); static int macbook_dmi_match(const struct dmi_system_id *d) { pr_debug("Detected MacBook, enabling workaround\n"); macbook = true; return 0; } static struct dmi_system_id acpi_smbus_dmi_table[] = { { macbook_dmi_match, "Apple MacBook", { DMI_MATCH(DMI_BOARD_VENDOR, "Apple"), DMI_MATCH(DMI_PRODUCT_NAME, "MacBook") }, }, { }, }; static int acpi_smbus_hc_add(struct acpi_device *device) { int status; unsigned long long val; struct acpi_smb_hc *hc; dmi_check_system(acpi_smbus_dmi_table); if (!device) return -EINVAL; status = acpi_evaluate_integer(device->handle, "_EC", NULL, &val); if (ACPI_FAILURE(status)) { printk(KERN_ERR PREFIX "error obtaining _EC.\n"); return -EIO; } strcpy(acpi_device_name(device), ACPI_SMB_HC_DEVICE_NAME); strcpy(acpi_device_class(device), ACPI_SMB_HC_CLASS); hc = kzalloc(sizeof(struct acpi_smb_hc), GFP_KERNEL); if (!hc) return -ENOMEM; mutex_init(&hc->lock); init_waitqueue_head(&hc->wait); hc->ec = acpi_driver_data(device->parent); hc->offset = (val >> 8) & 0xff; hc->query_bit = val & 0xff; device->driver_data = hc; acpi_ec_add_query_handler(hc->ec, hc->query_bit, NULL, smbus_alarm, hc); printk(KERN_INFO PREFIX "SBS HC: EC = 0x%p, offset = 0x%0x, query_bit = 0x%0x\n", hc->ec, hc->offset, hc->query_bit); return 0; } extern void acpi_ec_remove_query_handler(struct acpi_ec *ec, u8 query_bit); static int acpi_smbus_hc_remove(struct acpi_device *device) { struct acpi_smb_hc *hc; if (!device) return -EINVAL; hc = acpi_driver_data(device); acpi_ec_remove_query_handler(hc->ec, hc->query_bit); kfree(hc); device->driver_data = NULL; return 0; } module_acpi_driver(acpi_smb_hc_driver); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Alexey Starikovskiy"); MODULE_DESCRIPTION("ACPI SMBus HC driver");