mirror of https://github.com/Qortal/Brooklyn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
302 lines
7.1 KiB
302 lines
7.1 KiB
// SPDX-License-Identifier: GPL-2.0-only |
|
/* |
|
* SMBus driver for ACPI Embedded Controller (v0.1) |
|
* |
|
* Copyright (c) 2007 Alexey Starikovskiy |
|
*/ |
|
|
|
#define pr_fmt(fmt) "ACPI: " fmt |
|
|
|
#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 "sbshc.h" |
|
|
|
#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; |
|
bool done; |
|
}; |
|
|
|
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 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 int wait_transaction_complete(struct acpi_smb_hc *hc, int timeout) |
|
{ |
|
if (wait_event_timeout(hc->wait, hc->done, msecs_to_jiffies(timeout))) |
|
return 0; |
|
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) { |
|
pr_err("host controller is not configured\n"); |
|
return ret; |
|
} |
|
|
|
mutex_lock(&hc->lock); |
|
hc->done = false; |
|
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); |
|
acpi_os_wait_events_complete(); |
|
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 && status.fields.status == SMBUS_OK) { |
|
hc->done = true; |
|
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); |
|
} |
|
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 acpi_smbus_hc_add(struct acpi_device *device) |
|
{ |
|
int status; |
|
unsigned long long val; |
|
struct acpi_smb_hc *hc; |
|
|
|
if (!device) |
|
return -EINVAL; |
|
|
|
status = acpi_evaluate_integer(device->handle, "_EC", NULL, &val); |
|
if (ACPI_FAILURE(status)) { |
|
pr_err("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); |
|
dev_info(&device->dev, "SBS HC: offset = 0x%0x, query_bit = 0x%0x\n", |
|
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); |
|
acpi_os_wait_events_complete(); |
|
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");
|
|
|