|
@@ -21,8 +21,8 @@
|
|
|
* 02110-1301, USA.
|
|
|
*/
|
|
|
|
|
|
-#define TPACPI_VERSION "0.23"
|
|
|
-#define TPACPI_SYSFS_VERSION 0x020500
|
|
|
+#define TPACPI_VERSION "0.24"
|
|
|
+#define TPACPI_SYSFS_VERSION 0x020700
|
|
|
|
|
|
/*
|
|
|
* Changelog:
|
|
@@ -61,6 +61,7 @@
|
|
|
|
|
|
#include <linux/nvram.h>
|
|
|
#include <linux/proc_fs.h>
|
|
|
+#include <linux/seq_file.h>
|
|
|
#include <linux/sysfs.h>
|
|
|
#include <linux/backlight.h>
|
|
|
#include <linux/fb.h>
|
|
@@ -76,6 +77,10 @@
|
|
|
#include <linux/jiffies.h>
|
|
|
#include <linux/workqueue.h>
|
|
|
|
|
|
+#include <sound/core.h>
|
|
|
+#include <sound/control.h>
|
|
|
+#include <sound/initval.h>
|
|
|
+
|
|
|
#include <acpi/acpi_drivers.h>
|
|
|
|
|
|
#include <linux/pci_ids.h>
|
|
@@ -231,6 +236,7 @@ enum tpacpi_hkey_event_t {
|
|
|
#define TPACPI_DBG_HKEY 0x0008
|
|
|
#define TPACPI_DBG_FAN 0x0010
|
|
|
#define TPACPI_DBG_BRGHT 0x0020
|
|
|
+#define TPACPI_DBG_MIXER 0x0040
|
|
|
|
|
|
#define onoff(status, bit) ((status) & (1 << (bit)) ? "on" : "off")
|
|
|
#define enabled(status, bit) ((status) & (1 << (bit)) ? "enabled" : "disabled")
|
|
@@ -256,7 +262,7 @@ struct tp_acpi_drv_struct {
|
|
|
struct ibm_struct {
|
|
|
char *name;
|
|
|
|
|
|
- int (*read) (char *);
|
|
|
+ int (*read) (struct seq_file *);
|
|
|
int (*write) (char *);
|
|
|
void (*exit) (void);
|
|
|
void (*resume) (void);
|
|
@@ -298,6 +304,7 @@ static struct {
|
|
|
u32 fan_ctrl_status_undef:1;
|
|
|
u32 second_fan:1;
|
|
|
u32 beep_needs_two_args:1;
|
|
|
+ u32 mixer_no_level_control:1;
|
|
|
u32 input_device_registered:1;
|
|
|
u32 platform_drv_registered:1;
|
|
|
u32 platform_drv_attrs_registered:1;
|
|
@@ -309,6 +316,7 @@ static struct {
|
|
|
|
|
|
static struct {
|
|
|
u16 hotkey_mask_ff:1;
|
|
|
+ u16 volume_ctrl_forbidden:1;
|
|
|
} tp_warned;
|
|
|
|
|
|
struct thinkpad_id_data {
|
|
@@ -425,6 +433,12 @@ static void tpacpi_log_usertask(const char * const what)
|
|
|
.ec = TPACPI_MATCH_ANY, \
|
|
|
.quirks = (__quirk) }
|
|
|
|
|
|
+#define TPACPI_QEC_LNV(__id1, __id2, __quirk) \
|
|
|
+ { .vendor = PCI_VENDOR_ID_LENOVO, \
|
|
|
+ .bios = TPACPI_MATCH_ANY, \
|
|
|
+ .ec = TPID(__id1, __id2), \
|
|
|
+ .quirks = (__quirk) }
|
|
|
+
|
|
|
struct tpacpi_quirk {
|
|
|
unsigned int vendor;
|
|
|
u16 bios;
|
|
@@ -776,36 +790,25 @@ static int __init register_tpacpi_subdriver(struct ibm_struct *ibm)
|
|
|
****************************************************************************
|
|
|
****************************************************************************/
|
|
|
|
|
|
-static int dispatch_procfs_read(char *page, char **start, off_t off,
|
|
|
- int count, int *eof, void *data)
|
|
|
+static int dispatch_proc_show(struct seq_file *m, void *v)
|
|
|
{
|
|
|
- struct ibm_struct *ibm = data;
|
|
|
- int len;
|
|
|
+ struct ibm_struct *ibm = m->private;
|
|
|
|
|
|
if (!ibm || !ibm->read)
|
|
|
return -EINVAL;
|
|
|
+ return ibm->read(m);
|
|
|
+}
|
|
|
|
|
|
- len = ibm->read(page);
|
|
|
- if (len < 0)
|
|
|
- return len;
|
|
|
-
|
|
|
- if (len <= off + count)
|
|
|
- *eof = 1;
|
|
|
- *start = page + off;
|
|
|
- len -= off;
|
|
|
- if (len > count)
|
|
|
- len = count;
|
|
|
- if (len < 0)
|
|
|
- len = 0;
|
|
|
-
|
|
|
- return len;
|
|
|
+static int dispatch_proc_open(struct inode *inode, struct file *file)
|
|
|
+{
|
|
|
+ return single_open(file, dispatch_proc_show, PDE(inode)->data);
|
|
|
}
|
|
|
|
|
|
-static int dispatch_procfs_write(struct file *file,
|
|
|
+static ssize_t dispatch_proc_write(struct file *file,
|
|
|
const char __user *userbuf,
|
|
|
- unsigned long count, void *data)
|
|
|
+ size_t count, loff_t *pos)
|
|
|
{
|
|
|
- struct ibm_struct *ibm = data;
|
|
|
+ struct ibm_struct *ibm = PDE(file->f_path.dentry->d_inode)->data;
|
|
|
char *kernbuf;
|
|
|
int ret;
|
|
|
|
|
@@ -834,6 +837,15 @@ static int dispatch_procfs_write(struct file *file,
|
|
|
return ret;
|
|
|
}
|
|
|
|
|
|
+static const struct file_operations dispatch_proc_fops = {
|
|
|
+ .owner = THIS_MODULE,
|
|
|
+ .open = dispatch_proc_open,
|
|
|
+ .read = seq_read,
|
|
|
+ .llseek = seq_lseek,
|
|
|
+ .release = single_release,
|
|
|
+ .write = dispatch_proc_write,
|
|
|
+};
|
|
|
+
|
|
|
static char *next_cmd(char **cmds)
|
|
|
{
|
|
|
char *start = *cmds;
|
|
@@ -1261,6 +1273,7 @@ static int __init tpacpi_new_rfkill(const enum tpacpi_rfk_id id,
|
|
|
struct tpacpi_rfk *atp_rfk;
|
|
|
int res;
|
|
|
bool sw_state = false;
|
|
|
+ bool hw_state;
|
|
|
int sw_status;
|
|
|
|
|
|
BUG_ON(id >= TPACPI_RFK_SW_MAX || tpacpi_rfkill_switches[id]);
|
|
@@ -1295,7 +1308,8 @@ static int __init tpacpi_new_rfkill(const enum tpacpi_rfk_id id,
|
|
|
rfkill_init_sw_state(atp_rfk->rfkill, sw_state);
|
|
|
}
|
|
|
}
|
|
|
- rfkill_set_hw_state(atp_rfk->rfkill, tpacpi_rfk_check_hwblock_state());
|
|
|
+ hw_state = tpacpi_rfk_check_hwblock_state();
|
|
|
+ rfkill_set_hw_state(atp_rfk->rfkill, hw_state);
|
|
|
|
|
|
res = rfkill_register(atp_rfk->rfkill);
|
|
|
if (res < 0) {
|
|
@@ -1308,6 +1322,9 @@ static int __init tpacpi_new_rfkill(const enum tpacpi_rfk_id id,
|
|
|
}
|
|
|
|
|
|
tpacpi_rfkill_switches[id] = atp_rfk;
|
|
|
+
|
|
|
+ printk(TPACPI_INFO "rfkill switch %s: radio is %sblocked\n",
|
|
|
+ name, (sw_state || hw_state) ? "" : "un");
|
|
|
return 0;
|
|
|
}
|
|
|
|
|
@@ -1380,12 +1397,10 @@ static ssize_t tpacpi_rfk_sysfs_enable_store(const enum tpacpi_rfk_id id,
|
|
|
}
|
|
|
|
|
|
/* procfs -------------------------------------------------------------- */
|
|
|
-static int tpacpi_rfk_procfs_read(const enum tpacpi_rfk_id id, char *p)
|
|
|
+static int tpacpi_rfk_procfs_read(const enum tpacpi_rfk_id id, struct seq_file *m)
|
|
|
{
|
|
|
- int len = 0;
|
|
|
-
|
|
|
if (id >= TPACPI_RFK_SW_MAX)
|
|
|
- len += sprintf(p + len, "status:\t\tnot supported\n");
|
|
|
+ seq_printf(m, "status:\t\tnot supported\n");
|
|
|
else {
|
|
|
int status;
|
|
|
|
|
@@ -1399,13 +1414,13 @@ static int tpacpi_rfk_procfs_read(const enum tpacpi_rfk_id id, char *p)
|
|
|
return status;
|
|
|
}
|
|
|
|
|
|
- len += sprintf(p + len, "status:\t\t%s\n",
|
|
|
+ seq_printf(m, "status:\t\t%s\n",
|
|
|
(status == TPACPI_RFK_RADIO_ON) ?
|
|
|
"enabled" : "disabled");
|
|
|
- len += sprintf(p + len, "commands:\tenable, disable\n");
|
|
|
+ seq_printf(m, "commands:\tenable, disable\n");
|
|
|
}
|
|
|
|
|
|
- return len;
|
|
|
+ return 0;
|
|
|
}
|
|
|
|
|
|
static int tpacpi_rfk_procfs_write(const enum tpacpi_rfk_id id, char *buf)
|
|
@@ -1776,7 +1791,7 @@ static const struct tpacpi_quirk tpacpi_bios_version_qtable[] __initconst = {
|
|
|
|
|
|
TPV_QL1('7', '9', 'E', '3', '5', '0'), /* T60/p */
|
|
|
TPV_QL1('7', 'C', 'D', '2', '2', '2'), /* R60, R60i */
|
|
|
- TPV_QL0('7', 'E', 'D', '0'), /* R60e, R60i */
|
|
|
+ TPV_QL1('7', 'E', 'D', '0', '1', '5'), /* R60e, R60i */
|
|
|
|
|
|
/* BIOS FW BIOS VERS EC FW EC VERS */
|
|
|
TPV_QI2('1', 'W', '9', '0', '1', 'V', '2', '8'), /* R50e (1) */
|
|
@@ -1792,8 +1807,8 @@ static const struct tpacpi_quirk tpacpi_bios_version_qtable[] __initconst = {
|
|
|
TPV_QI1('7', '4', '6', '4', '2', '7'), /* X41 (0) */
|
|
|
TPV_QI1('7', '5', '6', '0', '2', '0'), /* X41t (0) */
|
|
|
|
|
|
- TPV_QL0('7', 'B', 'D', '7'), /* X60/s */
|
|
|
- TPV_QL0('7', 'J', '3', '0'), /* X60t */
|
|
|
+ TPV_QL1('7', 'B', 'D', '7', '4', '0'), /* X60/s */
|
|
|
+ TPV_QL1('7', 'J', '3', '0', '1', '3'), /* X60t */
|
|
|
|
|
|
/* (0) - older versions lack DMI EC fw string and functionality */
|
|
|
/* (1) - older versions known to lack functionality */
|
|
@@ -1883,14 +1898,11 @@ static int __init thinkpad_acpi_driver_init(struct ibm_init_struct *iibm)
|
|
|
return 0;
|
|
|
}
|
|
|
|
|
|
-static int thinkpad_acpi_driver_read(char *p)
|
|
|
+static int thinkpad_acpi_driver_read(struct seq_file *m)
|
|
|
{
|
|
|
- int len = 0;
|
|
|
-
|
|
|
- len += sprintf(p + len, "driver:\t\t%s\n", TPACPI_DESC);
|
|
|
- len += sprintf(p + len, "version:\t%s\n", TPACPI_VERSION);
|
|
|
-
|
|
|
- return len;
|
|
|
+ seq_printf(m, "driver:\t\t%s\n", TPACPI_DESC);
|
|
|
+ seq_printf(m, "version:\t%s\n", TPACPI_VERSION);
|
|
|
+ return 0;
|
|
|
}
|
|
|
|
|
|
static struct ibm_struct thinkpad_acpi_driver_data = {
|
|
@@ -2186,7 +2198,8 @@ static int hotkey_mask_set(u32 mask)
|
|
|
fwmask, hotkey_acpi_mask);
|
|
|
}
|
|
|
|
|
|
- hotkey_mask_warn_incomplete_mask();
|
|
|
+ if (tpacpi_lifecycle != TPACPI_LIFE_EXITING)
|
|
|
+ hotkey_mask_warn_incomplete_mask();
|
|
|
|
|
|
return rc;
|
|
|
}
|
|
@@ -3182,6 +3195,8 @@ static int __init hotkey_init(struct ibm_init_struct *iibm)
|
|
|
int res, i;
|
|
|
int status;
|
|
|
int hkeyv;
|
|
|
+ bool radiosw_state = false;
|
|
|
+ bool tabletsw_state = false;
|
|
|
|
|
|
unsigned long quirks;
|
|
|
|
|
@@ -3287,6 +3302,7 @@ static int __init hotkey_init(struct ibm_init_struct *iibm)
|
|
|
#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES
|
|
|
if (dbg_wlswemul) {
|
|
|
tp_features.hotkey_wlsw = 1;
|
|
|
+ radiosw_state = !!tpacpi_wlsw_emulstate;
|
|
|
printk(TPACPI_INFO
|
|
|
"radio switch emulation enabled\n");
|
|
|
} else
|
|
@@ -3294,6 +3310,7 @@ static int __init hotkey_init(struct ibm_init_struct *iibm)
|
|
|
/* Not all thinkpads have a hardware radio switch */
|
|
|
if (acpi_evalf(hkey_handle, &status, "WLSW", "qd")) {
|
|
|
tp_features.hotkey_wlsw = 1;
|
|
|
+ radiosw_state = !!status;
|
|
|
printk(TPACPI_INFO
|
|
|
"radio switch found; radios are %s\n",
|
|
|
enabled(status, 0));
|
|
@@ -3305,11 +3322,11 @@ static int __init hotkey_init(struct ibm_init_struct *iibm)
|
|
|
/* For X41t, X60t, X61t Tablets... */
|
|
|
if (!res && acpi_evalf(hkey_handle, &status, "MHKG", "qd")) {
|
|
|
tp_features.hotkey_tablet = 1;
|
|
|
+ tabletsw_state = !!(status & TP_HOTKEY_TABLET_MASK);
|
|
|
printk(TPACPI_INFO
|
|
|
"possible tablet mode switch found; "
|
|
|
"ThinkPad in %s mode\n",
|
|
|
- (status & TP_HOTKEY_TABLET_MASK)?
|
|
|
- "tablet" : "laptop");
|
|
|
+ (tabletsw_state) ? "tablet" : "laptop");
|
|
|
res = add_to_attr_set(hotkey_dev_attributes,
|
|
|
&dev_attr_hotkey_tablet_mode.attr);
|
|
|
}
|
|
@@ -3344,16 +3361,14 @@ static int __init hotkey_init(struct ibm_init_struct *iibm)
|
|
|
TPACPI_HOTKEY_MAP_SIZE);
|
|
|
}
|
|
|
|
|
|
- set_bit(EV_KEY, tpacpi_inputdev->evbit);
|
|
|
- set_bit(EV_MSC, tpacpi_inputdev->evbit);
|
|
|
- set_bit(MSC_SCAN, tpacpi_inputdev->mscbit);
|
|
|
+ input_set_capability(tpacpi_inputdev, EV_MSC, MSC_SCAN);
|
|
|
tpacpi_inputdev->keycodesize = TPACPI_HOTKEY_MAP_TYPESIZE;
|
|
|
tpacpi_inputdev->keycodemax = TPACPI_HOTKEY_MAP_LEN;
|
|
|
tpacpi_inputdev->keycode = hotkey_keycode_map;
|
|
|
for (i = 0; i < TPACPI_HOTKEY_MAP_LEN; i++) {
|
|
|
if (hotkey_keycode_map[i] != KEY_RESERVED) {
|
|
|
- set_bit(hotkey_keycode_map[i],
|
|
|
- tpacpi_inputdev->keybit);
|
|
|
+ input_set_capability(tpacpi_inputdev, EV_KEY,
|
|
|
+ hotkey_keycode_map[i]);
|
|
|
} else {
|
|
|
if (i < sizeof(hotkey_reserved_mask)*8)
|
|
|
hotkey_reserved_mask |= 1 << i;
|
|
@@ -3361,12 +3376,14 @@ static int __init hotkey_init(struct ibm_init_struct *iibm)
|
|
|
}
|
|
|
|
|
|
if (tp_features.hotkey_wlsw) {
|
|
|
- set_bit(EV_SW, tpacpi_inputdev->evbit);
|
|
|
- set_bit(SW_RFKILL_ALL, tpacpi_inputdev->swbit);
|
|
|
+ input_set_capability(tpacpi_inputdev, EV_SW, SW_RFKILL_ALL);
|
|
|
+ input_report_switch(tpacpi_inputdev,
|
|
|
+ SW_RFKILL_ALL, radiosw_state);
|
|
|
}
|
|
|
if (tp_features.hotkey_tablet) {
|
|
|
- set_bit(EV_SW, tpacpi_inputdev->evbit);
|
|
|
- set_bit(SW_TABLET_MODE, tpacpi_inputdev->swbit);
|
|
|
+ input_set_capability(tpacpi_inputdev, EV_SW, SW_TABLET_MODE);
|
|
|
+ input_report_switch(tpacpi_inputdev,
|
|
|
+ SW_TABLET_MODE, tabletsw_state);
|
|
|
}
|
|
|
|
|
|
/* Do not issue duplicate brightness change events to
|
|
@@ -3433,8 +3450,6 @@ static int __init hotkey_init(struct ibm_init_struct *iibm)
|
|
|
tpacpi_inputdev->close = &hotkey_inputdev_close;
|
|
|
|
|
|
hotkey_poll_setup_safe(true);
|
|
|
- tpacpi_send_radiosw_update();
|
|
|
- tpacpi_input_send_tabletsw();
|
|
|
|
|
|
return 0;
|
|
|
|
|
@@ -3542,49 +3557,57 @@ static bool hotkey_notify_usrevent(const u32 hkey,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+static void thermal_dump_all_sensors(void);
|
|
|
+
|
|
|
static bool hotkey_notify_thermal(const u32 hkey,
|
|
|
bool *send_acpi_ev,
|
|
|
bool *ignore_acpi_ev)
|
|
|
{
|
|
|
+ bool known = true;
|
|
|
+
|
|
|
/* 0x6000-0x6FFF: thermal alarms */
|
|
|
*send_acpi_ev = true;
|
|
|
*ignore_acpi_ev = false;
|
|
|
|
|
|
switch (hkey) {
|
|
|
+ case TP_HKEY_EV_THM_TABLE_CHANGED:
|
|
|
+ printk(TPACPI_INFO
|
|
|
+ "EC reports that Thermal Table has changed\n");
|
|
|
+ /* recommended action: do nothing, we don't have
|
|
|
+ * Lenovo ATM information */
|
|
|
+ return true;
|
|
|
case TP_HKEY_EV_ALARM_BAT_HOT:
|
|
|
printk(TPACPI_CRIT
|
|
|
"THERMAL ALARM: battery is too hot!\n");
|
|
|
/* recommended action: warn user through gui */
|
|
|
- return true;
|
|
|
+ break;
|
|
|
case TP_HKEY_EV_ALARM_BAT_XHOT:
|
|
|
printk(TPACPI_ALERT
|
|
|
"THERMAL EMERGENCY: battery is extremely hot!\n");
|
|
|
/* recommended action: immediate sleep/hibernate */
|
|
|
- return true;
|
|
|
+ break;
|
|
|
case TP_HKEY_EV_ALARM_SENSOR_HOT:
|
|
|
printk(TPACPI_CRIT
|
|
|
"THERMAL ALARM: "
|
|
|
"a sensor reports something is too hot!\n");
|
|
|
/* recommended action: warn user through gui, that */
|
|
|
/* some internal component is too hot */
|
|
|
- return true;
|
|
|
+ break;
|
|
|
case TP_HKEY_EV_ALARM_SENSOR_XHOT:
|
|
|
printk(TPACPI_ALERT
|
|
|
"THERMAL EMERGENCY: "
|
|
|
"a sensor reports something is extremely hot!\n");
|
|
|
/* recommended action: immediate sleep/hibernate */
|
|
|
- return true;
|
|
|
- case TP_HKEY_EV_THM_TABLE_CHANGED:
|
|
|
- printk(TPACPI_INFO
|
|
|
- "EC reports that Thermal Table has changed\n");
|
|
|
- /* recommended action: do nothing, we don't have
|
|
|
- * Lenovo ATM information */
|
|
|
- return true;
|
|
|
+ break;
|
|
|
default:
|
|
|
printk(TPACPI_ALERT
|
|
|
"THERMAL ALERT: unknown thermal alarm received\n");
|
|
|
- return false;
|
|
|
+ known = false;
|
|
|
}
|
|
|
+
|
|
|
+ thermal_dump_all_sensors();
|
|
|
+
|
|
|
+ return known;
|
|
|
}
|
|
|
|
|
|
static void hotkey_notify(struct ibm_struct *ibm, u32 event)
|
|
@@ -3727,14 +3750,13 @@ static void hotkey_resume(void)
|
|
|
}
|
|
|
|
|
|
/* procfs -------------------------------------------------------------- */
|
|
|
-static int hotkey_read(char *p)
|
|
|
+static int hotkey_read(struct seq_file *m)
|
|
|
{
|
|
|
int res, status;
|
|
|
- int len = 0;
|
|
|
|
|
|
if (!tp_features.hotkey) {
|
|
|
- len += sprintf(p + len, "status:\t\tnot supported\n");
|
|
|
- return len;
|
|
|
+ seq_printf(m, "status:\t\tnot supported\n");
|
|
|
+ return 0;
|
|
|
}
|
|
|
|
|
|
if (mutex_lock_killable(&hotkey_mutex))
|
|
@@ -3746,17 +3768,16 @@ static int hotkey_read(char *p)
|
|
|
if (res)
|
|
|
return res;
|
|
|
|
|
|
- len += sprintf(p + len, "status:\t\t%s\n", enabled(status, 0));
|
|
|
+ seq_printf(m, "status:\t\t%s\n", enabled(status, 0));
|
|
|
if (hotkey_all_mask) {
|
|
|
- len += sprintf(p + len, "mask:\t\t0x%08x\n", hotkey_user_mask);
|
|
|
- len += sprintf(p + len,
|
|
|
- "commands:\tenable, disable, reset, <mask>\n");
|
|
|
+ seq_printf(m, "mask:\t\t0x%08x\n", hotkey_user_mask);
|
|
|
+ seq_printf(m, "commands:\tenable, disable, reset, <mask>\n");
|
|
|
} else {
|
|
|
- len += sprintf(p + len, "mask:\t\tnot supported\n");
|
|
|
- len += sprintf(p + len, "commands:\tenable, disable, reset\n");
|
|
|
+ seq_printf(m, "mask:\t\tnot supported\n");
|
|
|
+ seq_printf(m, "commands:\tenable, disable, reset\n");
|
|
|
}
|
|
|
|
|
|
- return len;
|
|
|
+ return 0;
|
|
|
}
|
|
|
|
|
|
static void hotkey_enabledisable_warn(bool enable)
|
|
@@ -3863,15 +3884,6 @@ enum {
|
|
|
|
|
|
#define TPACPI_RFK_BLUETOOTH_SW_NAME "tpacpi_bluetooth_sw"
|
|
|
|
|
|
-static void bluetooth_suspend(pm_message_t state)
|
|
|
-{
|
|
|
- /* Try to make sure radio will resume powered off */
|
|
|
- if (!acpi_evalf(NULL, NULL, "\\BLTH", "vd",
|
|
|
- TP_ACPI_BLTH_PWR_OFF_ON_RESUME))
|
|
|
- vdbg_printk(TPACPI_DBG_RFKILL,
|
|
|
- "bluetooth power down on resume request failed\n");
|
|
|
-}
|
|
|
-
|
|
|
static int bluetooth_get_status(void)
|
|
|
{
|
|
|
int status;
|
|
@@ -3905,10 +3917,9 @@ static int bluetooth_set_status(enum tpacpi_rfkill_state state)
|
|
|
#endif
|
|
|
|
|
|
/* We make sure to keep TP_ACPI_BLUETOOTH_RESUMECTRL off */
|
|
|
+ status = TP_ACPI_BLUETOOTH_RESUMECTRL;
|
|
|
if (state == TPACPI_RFK_RADIO_ON)
|
|
|
- status = TP_ACPI_BLUETOOTH_RADIOSSW;
|
|
|
- else
|
|
|
- status = 0;
|
|
|
+ status |= TP_ACPI_BLUETOOTH_RADIOSSW;
|
|
|
|
|
|
if (!acpi_evalf(hkey_handle, NULL, "SBDC", "vd", status))
|
|
|
return -EIO;
|
|
@@ -4032,9 +4043,9 @@ static int __init bluetooth_init(struct ibm_init_struct *iibm)
|
|
|
}
|
|
|
|
|
|
/* procfs -------------------------------------------------------------- */
|
|
|
-static int bluetooth_read(char *p)
|
|
|
+static int bluetooth_read(struct seq_file *m)
|
|
|
{
|
|
|
- return tpacpi_rfk_procfs_read(TPACPI_RFK_BLUETOOTH_SW_ID, p);
|
|
|
+ return tpacpi_rfk_procfs_read(TPACPI_RFK_BLUETOOTH_SW_ID, m);
|
|
|
}
|
|
|
|
|
|
static int bluetooth_write(char *buf)
|
|
@@ -4047,7 +4058,6 @@ static struct ibm_struct bluetooth_driver_data = {
|
|
|
.read = bluetooth_read,
|
|
|
.write = bluetooth_write,
|
|
|
.exit = bluetooth_exit,
|
|
|
- .suspend = bluetooth_suspend,
|
|
|
.shutdown = bluetooth_shutdown,
|
|
|
};
|
|
|
|
|
@@ -4065,15 +4075,6 @@ enum {
|
|
|
|
|
|
#define TPACPI_RFK_WWAN_SW_NAME "tpacpi_wwan_sw"
|
|
|
|
|
|
-static void wan_suspend(pm_message_t state)
|
|
|
-{
|
|
|
- /* Try to make sure radio will resume powered off */
|
|
|
- if (!acpi_evalf(NULL, NULL, "\\WGSV", "qvd",
|
|
|
- TP_ACPI_WGSV_PWR_OFF_ON_RESUME))
|
|
|
- vdbg_printk(TPACPI_DBG_RFKILL,
|
|
|
- "WWAN power down on resume request failed\n");
|
|
|
-}
|
|
|
-
|
|
|
static int wan_get_status(void)
|
|
|
{
|
|
|
int status;
|
|
@@ -4106,11 +4107,10 @@ static int wan_set_status(enum tpacpi_rfkill_state state)
|
|
|
}
|
|
|
#endif
|
|
|
|
|
|
- /* We make sure to keep TP_ACPI_WANCARD_RESUMECTRL off */
|
|
|
+ /* We make sure to set TP_ACPI_WANCARD_RESUMECTRL */
|
|
|
+ status = TP_ACPI_WANCARD_RESUMECTRL;
|
|
|
if (state == TPACPI_RFK_RADIO_ON)
|
|
|
- status = TP_ACPI_WANCARD_RADIOSSW;
|
|
|
- else
|
|
|
- status = 0;
|
|
|
+ status |= TP_ACPI_WANCARD_RADIOSSW;
|
|
|
|
|
|
if (!acpi_evalf(hkey_handle, NULL, "SWAN", "vd", status))
|
|
|
return -EIO;
|
|
@@ -4233,9 +4233,9 @@ static int __init wan_init(struct ibm_init_struct *iibm)
|
|
|
}
|
|
|
|
|
|
/* procfs -------------------------------------------------------------- */
|
|
|
-static int wan_read(char *p)
|
|
|
+static int wan_read(struct seq_file *m)
|
|
|
{
|
|
|
- return tpacpi_rfk_procfs_read(TPACPI_RFK_WWAN_SW_ID, p);
|
|
|
+ return tpacpi_rfk_procfs_read(TPACPI_RFK_WWAN_SW_ID, m);
|
|
|
}
|
|
|
|
|
|
static int wan_write(char *buf)
|
|
@@ -4248,7 +4248,6 @@ static struct ibm_struct wan_driver_data = {
|
|
|
.read = wan_read,
|
|
|
.write = wan_write,
|
|
|
.exit = wan_exit,
|
|
|
- .suspend = wan_suspend,
|
|
|
.shutdown = wan_shutdown,
|
|
|
};
|
|
|
|
|
@@ -4611,14 +4610,13 @@ static int video_expand_toggle(void)
|
|
|
/* not reached */
|
|
|
}
|
|
|
|
|
|
-static int video_read(char *p)
|
|
|
+static int video_read(struct seq_file *m)
|
|
|
{
|
|
|
int status, autosw;
|
|
|
- int len = 0;
|
|
|
|
|
|
if (video_supported == TPACPI_VIDEO_NONE) {
|
|
|
- len += sprintf(p + len, "status:\t\tnot supported\n");
|
|
|
- return len;
|
|
|
+ seq_printf(m, "status:\t\tnot supported\n");
|
|
|
+ return 0;
|
|
|
}
|
|
|
|
|
|
status = video_outputsw_get();
|
|
@@ -4629,20 +4627,20 @@ static int video_read(char *p)
|
|
|
if (autosw < 0)
|
|
|
return autosw;
|
|
|
|
|
|
- len += sprintf(p + len, "status:\t\tsupported\n");
|
|
|
- len += sprintf(p + len, "lcd:\t\t%s\n", enabled(status, 0));
|
|
|
- len += sprintf(p + len, "crt:\t\t%s\n", enabled(status, 1));
|
|
|
+ seq_printf(m, "status:\t\tsupported\n");
|
|
|
+ seq_printf(m, "lcd:\t\t%s\n", enabled(status, 0));
|
|
|
+ seq_printf(m, "crt:\t\t%s\n", enabled(status, 1));
|
|
|
if (video_supported == TPACPI_VIDEO_NEW)
|
|
|
- len += sprintf(p + len, "dvi:\t\t%s\n", enabled(status, 3));
|
|
|
- len += sprintf(p + len, "auto:\t\t%s\n", enabled(autosw, 0));
|
|
|
- len += sprintf(p + len, "commands:\tlcd_enable, lcd_disable\n");
|
|
|
- len += sprintf(p + len, "commands:\tcrt_enable, crt_disable\n");
|
|
|
+ seq_printf(m, "dvi:\t\t%s\n", enabled(status, 3));
|
|
|
+ seq_printf(m, "auto:\t\t%s\n", enabled(autosw, 0));
|
|
|
+ seq_printf(m, "commands:\tlcd_enable, lcd_disable\n");
|
|
|
+ seq_printf(m, "commands:\tcrt_enable, crt_disable\n");
|
|
|
if (video_supported == TPACPI_VIDEO_NEW)
|
|
|
- len += sprintf(p + len, "commands:\tdvi_enable, dvi_disable\n");
|
|
|
- len += sprintf(p + len, "commands:\tauto_enable, auto_disable\n");
|
|
|
- len += sprintf(p + len, "commands:\tvideo_switch, expand_toggle\n");
|
|
|
+ seq_printf(m, "commands:\tdvi_enable, dvi_disable\n");
|
|
|
+ seq_printf(m, "commands:\tauto_enable, auto_disable\n");
|
|
|
+ seq_printf(m, "commands:\tvideo_switch, expand_toggle\n");
|
|
|
|
|
|
- return len;
|
|
|
+ return 0;
|
|
|
}
|
|
|
|
|
|
static int video_write(char *buf)
|
|
@@ -4834,25 +4832,24 @@ static void light_exit(void)
|
|
|
flush_workqueue(tpacpi_wq);
|
|
|
}
|
|
|
|
|
|
-static int light_read(char *p)
|
|
|
+static int light_read(struct seq_file *m)
|
|
|
{
|
|
|
- int len = 0;
|
|
|
int status;
|
|
|
|
|
|
if (!tp_features.light) {
|
|
|
- len += sprintf(p + len, "status:\t\tnot supported\n");
|
|
|
+ seq_printf(m, "status:\t\tnot supported\n");
|
|
|
} else if (!tp_features.light_status) {
|
|
|
- len += sprintf(p + len, "status:\t\tunknown\n");
|
|
|
- len += sprintf(p + len, "commands:\ton, off\n");
|
|
|
+ seq_printf(m, "status:\t\tunknown\n");
|
|
|
+ seq_printf(m, "commands:\ton, off\n");
|
|
|
} else {
|
|
|
status = light_get_status();
|
|
|
if (status < 0)
|
|
|
return status;
|
|
|
- len += sprintf(p + len, "status:\t\t%s\n", onoff(status, 0));
|
|
|
- len += sprintf(p + len, "commands:\ton, off\n");
|
|
|
+ seq_printf(m, "status:\t\t%s\n", onoff(status, 0));
|
|
|
+ seq_printf(m, "commands:\ton, off\n");
|
|
|
}
|
|
|
|
|
|
- return len;
|
|
|
+ return 0;
|
|
|
}
|
|
|
|
|
|
static int light_write(char *buf)
|
|
@@ -4930,20 +4927,18 @@ static void cmos_exit(void)
|
|
|
device_remove_file(&tpacpi_pdev->dev, &dev_attr_cmos_command);
|
|
|
}
|
|
|
|
|
|
-static int cmos_read(char *p)
|
|
|
+static int cmos_read(struct seq_file *m)
|
|
|
{
|
|
|
- int len = 0;
|
|
|
-
|
|
|
/* cmos not supported on 570, 600e/x, 770e, 770x, A21e, A2xm/p,
|
|
|
R30, R31, T20-22, X20-21 */
|
|
|
if (!cmos_handle)
|
|
|
- len += sprintf(p + len, "status:\t\tnot supported\n");
|
|
|
+ seq_printf(m, "status:\t\tnot supported\n");
|
|
|
else {
|
|
|
- len += sprintf(p + len, "status:\t\tsupported\n");
|
|
|
- len += sprintf(p + len, "commands:\t<cmd> (<cmd> is 0-21)\n");
|
|
|
+ seq_printf(m, "status:\t\tsupported\n");
|
|
|
+ seq_printf(m, "commands:\t<cmd> (<cmd> is 0-21)\n");
|
|
|
}
|
|
|
|
|
|
- return len;
|
|
|
+ return 0;
|
|
|
}
|
|
|
|
|
|
static int cmos_write(char *buf)
|
|
@@ -5318,15 +5313,13 @@ static int __init led_init(struct ibm_init_struct *iibm)
|
|
|
((s) == TPACPI_LED_OFF ? "off" : \
|
|
|
((s) == TPACPI_LED_ON ? "on" : "blinking"))
|
|
|
|
|
|
-static int led_read(char *p)
|
|
|
+static int led_read(struct seq_file *m)
|
|
|
{
|
|
|
- int len = 0;
|
|
|
-
|
|
|
if (!led_supported) {
|
|
|
- len += sprintf(p + len, "status:\t\tnot supported\n");
|
|
|
- return len;
|
|
|
+ seq_printf(m, "status:\t\tnot supported\n");
|
|
|
+ return 0;
|
|
|
}
|
|
|
- len += sprintf(p + len, "status:\t\tsupported\n");
|
|
|
+ seq_printf(m, "status:\t\tsupported\n");
|
|
|
|
|
|
if (led_supported == TPACPI_LED_570) {
|
|
|
/* 570 */
|
|
@@ -5335,15 +5328,15 @@ static int led_read(char *p)
|
|
|
status = led_get_status(i);
|
|
|
if (status < 0)
|
|
|
return -EIO;
|
|
|
- len += sprintf(p + len, "%d:\t\t%s\n",
|
|
|
+ seq_printf(m, "%d:\t\t%s\n",
|
|
|
i, str_led_status(status));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- len += sprintf(p + len, "commands:\t"
|
|
|
+ seq_printf(m, "commands:\t"
|
|
|
"<led> on, <led> off, <led> blink (<led> is 0-15)\n");
|
|
|
|
|
|
- return len;
|
|
|
+ return 0;
|
|
|
}
|
|
|
|
|
|
static int led_write(char *buf)
|
|
@@ -5416,18 +5409,16 @@ static int __init beep_init(struct ibm_init_struct *iibm)
|
|
|
return (beep_handle)? 0 : 1;
|
|
|
}
|
|
|
|
|
|
-static int beep_read(char *p)
|
|
|
+static int beep_read(struct seq_file *m)
|
|
|
{
|
|
|
- int len = 0;
|
|
|
-
|
|
|
if (!beep_handle)
|
|
|
- len += sprintf(p + len, "status:\t\tnot supported\n");
|
|
|
+ seq_printf(m, "status:\t\tnot supported\n");
|
|
|
else {
|
|
|
- len += sprintf(p + len, "status:\t\tsupported\n");
|
|
|
- len += sprintf(p + len, "commands:\t<cmd> (<cmd> is 0-17)\n");
|
|
|
+ seq_printf(m, "status:\t\tsupported\n");
|
|
|
+ seq_printf(m, "commands:\t<cmd> (<cmd> is 0-17)\n");
|
|
|
}
|
|
|
|
|
|
- return len;
|
|
|
+ return 0;
|
|
|
}
|
|
|
|
|
|
static int beep_write(char *buf)
|
|
@@ -5480,8 +5471,11 @@ enum { /* TPACPI_THERMAL_TPEC_* */
|
|
|
TP_EC_THERMAL_TMP0 = 0x78, /* ACPI EC regs TMP 0..7 */
|
|
|
TP_EC_THERMAL_TMP8 = 0xC0, /* ACPI EC regs TMP 8..15 */
|
|
|
TP_EC_THERMAL_TMP_NA = -128, /* ACPI EC sensor not available */
|
|
|
+
|
|
|
+ TPACPI_THERMAL_SENSOR_NA = -128000, /* Sensor not available */
|
|
|
};
|
|
|
|
|
|
+
|
|
|
#define TPACPI_MAX_THERMAL_SENSORS 16 /* Max thermal sensors supported */
|
|
|
struct ibm_thermal_sensors_struct {
|
|
|
s32 temp[TPACPI_MAX_THERMAL_SENSORS];
|
|
@@ -5571,6 +5565,28 @@ static int thermal_get_sensors(struct ibm_thermal_sensors_struct *s)
|
|
|
return n;
|
|
|
}
|
|
|
|
|
|
+static void thermal_dump_all_sensors(void)
|
|
|
+{
|
|
|
+ int n, i;
|
|
|
+ struct ibm_thermal_sensors_struct t;
|
|
|
+
|
|
|
+ n = thermal_get_sensors(&t);
|
|
|
+ if (n <= 0)
|
|
|
+ return;
|
|
|
+
|
|
|
+ printk(TPACPI_NOTICE
|
|
|
+ "temperatures (Celsius):");
|
|
|
+
|
|
|
+ for (i = 0; i < n; i++) {
|
|
|
+ if (t.temp[i] != TPACPI_THERMAL_SENSOR_NA)
|
|
|
+ printk(KERN_CONT " %d", (int)(t.temp[i] / 1000));
|
|
|
+ else
|
|
|
+ printk(KERN_CONT " N/A");
|
|
|
+ }
|
|
|
+
|
|
|
+ printk(KERN_CONT "\n");
|
|
|
+}
|
|
|
+
|
|
|
/* sysfs temp##_input -------------------------------------------------- */
|
|
|
|
|
|
static ssize_t thermal_temp_input_show(struct device *dev,
|
|
@@ -5586,7 +5602,7 @@ static ssize_t thermal_temp_input_show(struct device *dev,
|
|
|
res = thermal_get_sensor(idx, &value);
|
|
|
if (res)
|
|
|
return res;
|
|
|
- if (value == TP_EC_THERMAL_TMP_NA * 1000)
|
|
|
+ if (value == TPACPI_THERMAL_SENSOR_NA)
|
|
|
return -ENXIO;
|
|
|
|
|
|
return snprintf(buf, PAGE_SIZE, "%d\n", value);
|
|
@@ -5763,9 +5779,8 @@ static void thermal_exit(void)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-static int thermal_read(char *p)
|
|
|
+static int thermal_read(struct seq_file *m)
|
|
|
{
|
|
|
- int len = 0;
|
|
|
int n, i;
|
|
|
struct ibm_thermal_sensors_struct t;
|
|
|
|
|
@@ -5773,16 +5788,16 @@ static int thermal_read(char *p)
|
|
|
if (unlikely(n < 0))
|
|
|
return n;
|
|
|
|
|
|
- len += sprintf(p + len, "temperatures:\t");
|
|
|
+ seq_printf(m, "temperatures:\t");
|
|
|
|
|
|
if (n > 0) {
|
|
|
for (i = 0; i < (n - 1); i++)
|
|
|
- len += sprintf(p + len, "%d ", t.temp[i] / 1000);
|
|
|
- len += sprintf(p + len, "%d\n", t.temp[i] / 1000);
|
|
|
+ seq_printf(m, "%d ", t.temp[i] / 1000);
|
|
|
+ seq_printf(m, "%d\n", t.temp[i] / 1000);
|
|
|
} else
|
|
|
- len += sprintf(p + len, "not supported\n");
|
|
|
+ seq_printf(m, "not supported\n");
|
|
|
|
|
|
- return len;
|
|
|
+ return 0;
|
|
|
}
|
|
|
|
|
|
static struct ibm_struct thermal_driver_data = {
|
|
@@ -5797,39 +5812,38 @@ static struct ibm_struct thermal_driver_data = {
|
|
|
|
|
|
static u8 ecdump_regs[256];
|
|
|
|
|
|
-static int ecdump_read(char *p)
|
|
|
+static int ecdump_read(struct seq_file *m)
|
|
|
{
|
|
|
- int len = 0;
|
|
|
int i, j;
|
|
|
u8 v;
|
|
|
|
|
|
- len += sprintf(p + len, "EC "
|
|
|
+ seq_printf(m, "EC "
|
|
|
" +00 +01 +02 +03 +04 +05 +06 +07"
|
|
|
" +08 +09 +0a +0b +0c +0d +0e +0f\n");
|
|
|
for (i = 0; i < 256; i += 16) {
|
|
|
- len += sprintf(p + len, "EC 0x%02x:", i);
|
|
|
+ seq_printf(m, "EC 0x%02x:", i);
|
|
|
for (j = 0; j < 16; j++) {
|
|
|
if (!acpi_ec_read(i + j, &v))
|
|
|
break;
|
|
|
if (v != ecdump_regs[i + j])
|
|
|
- len += sprintf(p + len, " *%02x", v);
|
|
|
+ seq_printf(m, " *%02x", v);
|
|
|
else
|
|
|
- len += sprintf(p + len, " %02x", v);
|
|
|
+ seq_printf(m, " %02x", v);
|
|
|
ecdump_regs[i + j] = v;
|
|
|
}
|
|
|
- len += sprintf(p + len, "\n");
|
|
|
+ seq_putc(m, '\n');
|
|
|
if (j != 16)
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
/* These are way too dangerous to advertise openly... */
|
|
|
#if 0
|
|
|
- len += sprintf(p + len, "commands:\t0x<offset> 0x<value>"
|
|
|
+ seq_printf(m, "commands:\t0x<offset> 0x<value>"
|
|
|
" (<offset> is 00-ff, <value> is 00-ff)\n");
|
|
|
- len += sprintf(p + len, "commands:\t0x<offset> <value> "
|
|
|
+ seq_printf(m, "commands:\t0x<offset> <value> "
|
|
|
" (<offset> is 00-ff, <value> is 0-255)\n");
|
|
|
#endif
|
|
|
- return len;
|
|
|
+ return 0;
|
|
|
}
|
|
|
|
|
|
static int ecdump_write(char *buf)
|
|
@@ -6092,6 +6106,12 @@ static int brightness_get(struct backlight_device *bd)
|
|
|
return status & TP_EC_BACKLIGHT_LVLMSK;
|
|
|
}
|
|
|
|
|
|
+static void tpacpi_brightness_notify_change(void)
|
|
|
+{
|
|
|
+ backlight_force_update(ibm_backlight_device,
|
|
|
+ BACKLIGHT_UPDATE_HOTKEY);
|
|
|
+}
|
|
|
+
|
|
|
static struct backlight_ops ibm_backlight_data = {
|
|
|
.get_brightness = brightness_get,
|
|
|
.update_status = brightness_update_status,
|
|
@@ -6120,8 +6140,8 @@ static const struct tpacpi_quirk brightness_quirk_table[] __initconst = {
|
|
|
|
|
|
/* Models with Intel Extreme Graphics 2 */
|
|
|
TPACPI_Q_IBM('1', 'U', TPACPI_BRGHT_Q_NOEC),
|
|
|
- TPACPI_Q_IBM('1', 'V', TPACPI_BRGHT_Q_ASK|TPACPI_BRGHT_Q_NOEC),
|
|
|
- TPACPI_Q_IBM('1', 'W', TPACPI_BRGHT_Q_ASK|TPACPI_BRGHT_Q_NOEC),
|
|
|
+ TPACPI_Q_IBM('1', 'V', TPACPI_BRGHT_Q_ASK|TPACPI_BRGHT_Q_EC),
|
|
|
+ TPACPI_Q_IBM('1', 'W', TPACPI_BRGHT_Q_ASK|TPACPI_BRGHT_Q_EC),
|
|
|
|
|
|
/* Models with Intel GMA900 */
|
|
|
TPACPI_Q_IBM('7', '0', TPACPI_BRGHT_Q_NOEC), /* T43, R52 */
|
|
@@ -6246,6 +6266,12 @@ static int __init brightness_init(struct ibm_init_struct *iibm)
|
|
|
ibm_backlight_device->props.brightness = b & TP_EC_BACKLIGHT_LVLMSK;
|
|
|
backlight_update_status(ibm_backlight_device);
|
|
|
|
|
|
+ vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_BRGHT,
|
|
|
+ "brightness: registering brightness hotkeys "
|
|
|
+ "as change notification\n");
|
|
|
+ tpacpi_hotkey_driver_mask_set(hotkey_driver_mask
|
|
|
+ | TP_ACPI_HKEY_BRGHTUP_MASK
|
|
|
+ | TP_ACPI_HKEY_BRGHTDWN_MASK);;
|
|
|
return 0;
|
|
|
}
|
|
|
|
|
@@ -6270,23 +6296,22 @@ static void brightness_exit(void)
|
|
|
tpacpi_brightness_checkpoint_nvram();
|
|
|
}
|
|
|
|
|
|
-static int brightness_read(char *p)
|
|
|
+static int brightness_read(struct seq_file *m)
|
|
|
{
|
|
|
- int len = 0;
|
|
|
int level;
|
|
|
|
|
|
level = brightness_get(NULL);
|
|
|
if (level < 0) {
|
|
|
- len += sprintf(p + len, "level:\t\tunreadable\n");
|
|
|
+ seq_printf(m, "level:\t\tunreadable\n");
|
|
|
} else {
|
|
|
- len += sprintf(p + len, "level:\t\t%d\n", level);
|
|
|
- len += sprintf(p + len, "commands:\tup, down\n");
|
|
|
- len += sprintf(p + len, "commands:\tlevel <level>"
|
|
|
+ seq_printf(m, "level:\t\t%d\n", level);
|
|
|
+ seq_printf(m, "commands:\tup, down\n");
|
|
|
+ seq_printf(m, "commands:\tlevel <level>"
|
|
|
" (<level> is 0-%d)\n",
|
|
|
(tp_features.bright_16levels) ? 15 : 7);
|
|
|
}
|
|
|
|
|
|
- return len;
|
|
|
+ return 0;
|
|
|
}
|
|
|
|
|
|
static int brightness_write(char *buf)
|
|
@@ -6322,6 +6347,9 @@ static int brightness_write(char *buf)
|
|
|
* Doing it this way makes the syscall restartable in case of EINTR
|
|
|
*/
|
|
|
rc = brightness_set(level);
|
|
|
+ if (!rc && ibm_backlight_device)
|
|
|
+ backlight_force_update(ibm_backlight_device,
|
|
|
+ BACKLIGHT_UPDATE_SYSFS);
|
|
|
return (rc == -EINTR)? -ERESTARTSYS : rc;
|
|
|
}
|
|
|
|
|
@@ -6338,99 +6366,654 @@ static struct ibm_struct brightness_driver_data = {
|
|
|
* Volume subdriver
|
|
|
*/
|
|
|
|
|
|
-static int volume_offset = 0x30;
|
|
|
+/*
|
|
|
+ * IBM ThinkPads have a simple volume controller with MUTE gating.
|
|
|
+ * Very early Lenovo ThinkPads follow the IBM ThinkPad spec.
|
|
|
+ *
|
|
|
+ * Since the *61 series (and probably also the later *60 series), Lenovo
|
|
|
+ * ThinkPads only implement the MUTE gate.
|
|
|
+ *
|
|
|
+ * EC register 0x30
|
|
|
+ * Bit 6: MUTE (1 mutes sound)
|
|
|
+ * Bit 3-0: Volume
|
|
|
+ * Other bits should be zero as far as we know.
|
|
|
+ *
|
|
|
+ * This is also stored in CMOS NVRAM, byte 0x60, bit 6 (MUTE), and
|
|
|
+ * bits 3-0 (volume). Other bits in NVRAM may have other functions,
|
|
|
+ * such as bit 7 which is used to detect repeated presses of MUTE,
|
|
|
+ * and we leave them unchanged.
|
|
|
+ */
|
|
|
+
|
|
|
+#define TPACPI_ALSA_DRVNAME "ThinkPad EC"
|
|
|
+#define TPACPI_ALSA_SHRTNAME "ThinkPad Console Audio Control"
|
|
|
+#define TPACPI_ALSA_MIXERNAME TPACPI_ALSA_SHRTNAME
|
|
|
+
|
|
|
+static int alsa_index = SNDRV_DEFAULT_IDX1;
|
|
|
+static char *alsa_id = "ThinkPadEC";
|
|
|
+static int alsa_enable = SNDRV_DEFAULT_ENABLE1;
|
|
|
+
|
|
|
+struct tpacpi_alsa_data {
|
|
|
+ struct snd_card *card;
|
|
|
+ struct snd_ctl_elem_id *ctl_mute_id;
|
|
|
+ struct snd_ctl_elem_id *ctl_vol_id;
|
|
|
+};
|
|
|
+
|
|
|
+static struct snd_card *alsa_card;
|
|
|
+
|
|
|
+enum {
|
|
|
+ TP_EC_AUDIO = 0x30,
|
|
|
+
|
|
|
+ /* TP_EC_AUDIO bits */
|
|
|
+ TP_EC_AUDIO_MUTESW = 6,
|
|
|
+
|
|
|
+ /* TP_EC_AUDIO bitmasks */
|
|
|
+ TP_EC_AUDIO_LVL_MSK = 0x0F,
|
|
|
+ TP_EC_AUDIO_MUTESW_MSK = (1 << TP_EC_AUDIO_MUTESW),
|
|
|
+
|
|
|
+ /* Maximum volume */
|
|
|
+ TP_EC_VOLUME_MAX = 14,
|
|
|
+};
|
|
|
+
|
|
|
+enum tpacpi_volume_access_mode {
|
|
|
+ TPACPI_VOL_MODE_AUTO = 0, /* Not implemented yet */
|
|
|
+ TPACPI_VOL_MODE_EC, /* Pure EC control */
|
|
|
+ TPACPI_VOL_MODE_UCMS_STEP, /* UCMS step-based control: N/A */
|
|
|
+ TPACPI_VOL_MODE_ECNVRAM, /* EC control w/ NVRAM store */
|
|
|
+ TPACPI_VOL_MODE_MAX
|
|
|
+};
|
|
|
+
|
|
|
+enum tpacpi_volume_capabilities {
|
|
|
+ TPACPI_VOL_CAP_AUTO = 0, /* Use white/blacklist */
|
|
|
+ TPACPI_VOL_CAP_VOLMUTE, /* Output vol and mute */
|
|
|
+ TPACPI_VOL_CAP_MUTEONLY, /* Output mute only */
|
|
|
+ TPACPI_VOL_CAP_MAX
|
|
|
+};
|
|
|
+
|
|
|
+static enum tpacpi_volume_access_mode volume_mode =
|
|
|
+ TPACPI_VOL_MODE_MAX;
|
|
|
+
|
|
|
+static enum tpacpi_volume_capabilities volume_capabilities;
|
|
|
+static int volume_control_allowed;
|
|
|
|
|
|
-static int volume_read(char *p)
|
|
|
+/*
|
|
|
+ * Used to syncronize writers to TP_EC_AUDIO and
|
|
|
+ * TP_NVRAM_ADDR_MIXER, as we need to do read-modify-write
|
|
|
+ */
|
|
|
+static struct mutex volume_mutex;
|
|
|
+
|
|
|
+static void tpacpi_volume_checkpoint_nvram(void)
|
|
|
{
|
|
|
- int len = 0;
|
|
|
- u8 level;
|
|
|
+ u8 lec = 0;
|
|
|
+ u8 b_nvram;
|
|
|
+ u8 ec_mask;
|
|
|
+
|
|
|
+ if (volume_mode != TPACPI_VOL_MODE_ECNVRAM)
|
|
|
+ return;
|
|
|
+ if (!volume_control_allowed)
|
|
|
+ return;
|
|
|
+
|
|
|
+ vdbg_printk(TPACPI_DBG_MIXER,
|
|
|
+ "trying to checkpoint mixer state to NVRAM...\n");
|
|
|
|
|
|
- if (!acpi_ec_read(volume_offset, &level)) {
|
|
|
- len += sprintf(p + len, "level:\t\tunreadable\n");
|
|
|
+ if (tp_features.mixer_no_level_control)
|
|
|
+ ec_mask = TP_EC_AUDIO_MUTESW_MSK;
|
|
|
+ else
|
|
|
+ ec_mask = TP_EC_AUDIO_MUTESW_MSK | TP_EC_AUDIO_LVL_MSK;
|
|
|
+
|
|
|
+ if (mutex_lock_killable(&volume_mutex) < 0)
|
|
|
+ return;
|
|
|
+
|
|
|
+ if (unlikely(!acpi_ec_read(TP_EC_AUDIO, &lec)))
|
|
|
+ goto unlock;
|
|
|
+ lec &= ec_mask;
|
|
|
+ b_nvram = nvram_read_byte(TP_NVRAM_ADDR_MIXER);
|
|
|
+
|
|
|
+ if (lec != (b_nvram & ec_mask)) {
|
|
|
+ /* NVRAM needs update */
|
|
|
+ b_nvram &= ~ec_mask;
|
|
|
+ b_nvram |= lec;
|
|
|
+ nvram_write_byte(b_nvram, TP_NVRAM_ADDR_MIXER);
|
|
|
+ dbg_printk(TPACPI_DBG_MIXER,
|
|
|
+ "updated NVRAM mixer status to 0x%02x (0x%02x)\n",
|
|
|
+ (unsigned int) lec, (unsigned int) b_nvram);
|
|
|
} else {
|
|
|
- len += sprintf(p + len, "level:\t\t%d\n", level & 0xf);
|
|
|
- len += sprintf(p + len, "mute:\t\t%s\n", onoff(level, 6));
|
|
|
- len += sprintf(p + len, "commands:\tup, down, mute\n");
|
|
|
- len += sprintf(p + len, "commands:\tlevel <level>"
|
|
|
- " (<level> is 0-15)\n");
|
|
|
+ vdbg_printk(TPACPI_DBG_MIXER,
|
|
|
+ "NVRAM mixer status already is 0x%02x (0x%02x)\n",
|
|
|
+ (unsigned int) lec, (unsigned int) b_nvram);
|
|
|
}
|
|
|
|
|
|
- return len;
|
|
|
+unlock:
|
|
|
+ mutex_unlock(&volume_mutex);
|
|
|
}
|
|
|
|
|
|
-static int volume_write(char *buf)
|
|
|
+static int volume_get_status_ec(u8 *status)
|
|
|
{
|
|
|
- int cmos_cmd, inc, i;
|
|
|
- u8 level, mute;
|
|
|
- int new_level, new_mute;
|
|
|
- char *cmd;
|
|
|
+ u8 s;
|
|
|
|
|
|
- while ((cmd = next_cmd(&buf))) {
|
|
|
- if (!acpi_ec_read(volume_offset, &level))
|
|
|
- return -EIO;
|
|
|
- new_mute = mute = level & 0x40;
|
|
|
- new_level = level = level & 0xf;
|
|
|
+ if (!acpi_ec_read(TP_EC_AUDIO, &s))
|
|
|
+ return -EIO;
|
|
|
|
|
|
- if (strlencmp(cmd, "up") == 0) {
|
|
|
- if (mute)
|
|
|
- new_mute = 0;
|
|
|
- else
|
|
|
- new_level = level == 15 ? 15 : level + 1;
|
|
|
- } else if (strlencmp(cmd, "down") == 0) {
|
|
|
- if (mute)
|
|
|
- new_mute = 0;
|
|
|
- else
|
|
|
- new_level = level == 0 ? 0 : level - 1;
|
|
|
- } else if (sscanf(cmd, "level %d", &new_level) == 1 &&
|
|
|
- new_level >= 0 && new_level <= 15) {
|
|
|
- /* new_level set */
|
|
|
- } else if (strlencmp(cmd, "mute") == 0) {
|
|
|
- new_mute = 0x40;
|
|
|
- } else
|
|
|
- return -EINVAL;
|
|
|
+ *status = s;
|
|
|
|
|
|
- if (new_level != level) {
|
|
|
- /* mute doesn't change */
|
|
|
+ dbg_printk(TPACPI_DBG_MIXER, "status 0x%02x\n", s);
|
|
|
|
|
|
- cmos_cmd = (new_level > level) ?
|
|
|
- TP_CMOS_VOLUME_UP : TP_CMOS_VOLUME_DOWN;
|
|
|
- inc = new_level > level ? 1 : -1;
|
|
|
+ return 0;
|
|
|
+}
|
|
|
|
|
|
- if (mute && (issue_thinkpad_cmos_command(cmos_cmd) ||
|
|
|
- !acpi_ec_write(volume_offset, level)))
|
|
|
- return -EIO;
|
|
|
+static int volume_get_status(u8 *status)
|
|
|
+{
|
|
|
+ return volume_get_status_ec(status);
|
|
|
+}
|
|
|
|
|
|
- for (i = level; i != new_level; i += inc)
|
|
|
- if (issue_thinkpad_cmos_command(cmos_cmd) ||
|
|
|
- !acpi_ec_write(volume_offset, i + inc))
|
|
|
- return -EIO;
|
|
|
+static int volume_set_status_ec(const u8 status)
|
|
|
+{
|
|
|
+ if (!acpi_ec_write(TP_EC_AUDIO, status))
|
|
|
+ return -EIO;
|
|
|
|
|
|
- if (mute &&
|
|
|
- (issue_thinkpad_cmos_command(TP_CMOS_VOLUME_MUTE) ||
|
|
|
- !acpi_ec_write(volume_offset, new_level + mute))) {
|
|
|
- return -EIO;
|
|
|
- }
|
|
|
+ dbg_printk(TPACPI_DBG_MIXER, "set EC mixer to 0x%02x\n", status);
|
|
|
+
|
|
|
+ return 0;
|
|
|
+}
|
|
|
+
|
|
|
+static int volume_set_status(const u8 status)
|
|
|
+{
|
|
|
+ return volume_set_status_ec(status);
|
|
|
+}
|
|
|
+
|
|
|
+static int volume_set_mute_ec(const bool mute)
|
|
|
+{
|
|
|
+ int rc;
|
|
|
+ u8 s, n;
|
|
|
+
|
|
|
+ if (mutex_lock_killable(&volume_mutex) < 0)
|
|
|
+ return -EINTR;
|
|
|
+
|
|
|
+ rc = volume_get_status_ec(&s);
|
|
|
+ if (rc)
|
|
|
+ goto unlock;
|
|
|
+
|
|
|
+ n = (mute) ? s | TP_EC_AUDIO_MUTESW_MSK :
|
|
|
+ s & ~TP_EC_AUDIO_MUTESW_MSK;
|
|
|
+
|
|
|
+ if (n != s)
|
|
|
+ rc = volume_set_status_ec(n);
|
|
|
+
|
|
|
+unlock:
|
|
|
+ mutex_unlock(&volume_mutex);
|
|
|
+ return rc;
|
|
|
+}
|
|
|
+
|
|
|
+static int volume_set_mute(const bool mute)
|
|
|
+{
|
|
|
+ dbg_printk(TPACPI_DBG_MIXER, "trying to %smute\n",
|
|
|
+ (mute) ? "" : "un");
|
|
|
+ return volume_set_mute_ec(mute);
|
|
|
+}
|
|
|
+
|
|
|
+static int volume_set_volume_ec(const u8 vol)
|
|
|
+{
|
|
|
+ int rc;
|
|
|
+ u8 s, n;
|
|
|
+
|
|
|
+ if (vol > TP_EC_VOLUME_MAX)
|
|
|
+ return -EINVAL;
|
|
|
+
|
|
|
+ if (mutex_lock_killable(&volume_mutex) < 0)
|
|
|
+ return -EINTR;
|
|
|
+
|
|
|
+ rc = volume_get_status_ec(&s);
|
|
|
+ if (rc)
|
|
|
+ goto unlock;
|
|
|
+
|
|
|
+ n = (s & ~TP_EC_AUDIO_LVL_MSK) | vol;
|
|
|
+
|
|
|
+ if (n != s)
|
|
|
+ rc = volume_set_status_ec(n);
|
|
|
+
|
|
|
+unlock:
|
|
|
+ mutex_unlock(&volume_mutex);
|
|
|
+ return rc;
|
|
|
+}
|
|
|
+
|
|
|
+static int volume_set_volume(const u8 vol)
|
|
|
+{
|
|
|
+ dbg_printk(TPACPI_DBG_MIXER,
|
|
|
+ "trying to set volume level to %hu\n", vol);
|
|
|
+ return volume_set_volume_ec(vol);
|
|
|
+}
|
|
|
+
|
|
|
+static void volume_alsa_notify_change(void)
|
|
|
+{
|
|
|
+ struct tpacpi_alsa_data *d;
|
|
|
+
|
|
|
+ if (alsa_card && alsa_card->private_data) {
|
|
|
+ d = alsa_card->private_data;
|
|
|
+ if (d->ctl_mute_id)
|
|
|
+ snd_ctl_notify(alsa_card,
|
|
|
+ SNDRV_CTL_EVENT_MASK_VALUE,
|
|
|
+ d->ctl_mute_id);
|
|
|
+ if (d->ctl_vol_id)
|
|
|
+ snd_ctl_notify(alsa_card,
|
|
|
+ SNDRV_CTL_EVENT_MASK_VALUE,
|
|
|
+ d->ctl_vol_id);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+static int volume_alsa_vol_info(struct snd_kcontrol *kcontrol,
|
|
|
+ struct snd_ctl_elem_info *uinfo)
|
|
|
+{
|
|
|
+ uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
|
|
|
+ uinfo->count = 1;
|
|
|
+ uinfo->value.integer.min = 0;
|
|
|
+ uinfo->value.integer.max = TP_EC_VOLUME_MAX;
|
|
|
+ return 0;
|
|
|
+}
|
|
|
+
|
|
|
+static int volume_alsa_vol_get(struct snd_kcontrol *kcontrol,
|
|
|
+ struct snd_ctl_elem_value *ucontrol)
|
|
|
+{
|
|
|
+ u8 s;
|
|
|
+ int rc;
|
|
|
+
|
|
|
+ rc = volume_get_status(&s);
|
|
|
+ if (rc < 0)
|
|
|
+ return rc;
|
|
|
+
|
|
|
+ ucontrol->value.integer.value[0] = s & TP_EC_AUDIO_LVL_MSK;
|
|
|
+ return 0;
|
|
|
+}
|
|
|
+
|
|
|
+static int volume_alsa_vol_put(struct snd_kcontrol *kcontrol,
|
|
|
+ struct snd_ctl_elem_value *ucontrol)
|
|
|
+{
|
|
|
+ return volume_set_volume(ucontrol->value.integer.value[0]);
|
|
|
+}
|
|
|
+
|
|
|
+#define volume_alsa_mute_info snd_ctl_boolean_mono_info
|
|
|
+
|
|
|
+static int volume_alsa_mute_get(struct snd_kcontrol *kcontrol,
|
|
|
+ struct snd_ctl_elem_value *ucontrol)
|
|
|
+{
|
|
|
+ u8 s;
|
|
|
+ int rc;
|
|
|
+
|
|
|
+ rc = volume_get_status(&s);
|
|
|
+ if (rc < 0)
|
|
|
+ return rc;
|
|
|
+
|
|
|
+ ucontrol->value.integer.value[0] =
|
|
|
+ (s & TP_EC_AUDIO_MUTESW_MSK) ? 0 : 1;
|
|
|
+ return 0;
|
|
|
+}
|
|
|
+
|
|
|
+static int volume_alsa_mute_put(struct snd_kcontrol *kcontrol,
|
|
|
+ struct snd_ctl_elem_value *ucontrol)
|
|
|
+{
|
|
|
+ return volume_set_mute(!ucontrol->value.integer.value[0]);
|
|
|
+}
|
|
|
+
|
|
|
+static struct snd_kcontrol_new volume_alsa_control_vol __devinitdata = {
|
|
|
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
|
|
|
+ .name = "Console Playback Volume",
|
|
|
+ .index = 0,
|
|
|
+ .access = SNDRV_CTL_ELEM_ACCESS_READ,
|
|
|
+ .info = volume_alsa_vol_info,
|
|
|
+ .get = volume_alsa_vol_get,
|
|
|
+};
|
|
|
+
|
|
|
+static struct snd_kcontrol_new volume_alsa_control_mute __devinitdata = {
|
|
|
+ .iface = SNDRV_CTL_ELEM_IFACE_MIXER,
|
|
|
+ .name = "Console Playback Switch",
|
|
|
+ .index = 0,
|
|
|
+ .access = SNDRV_CTL_ELEM_ACCESS_READ,
|
|
|
+ .info = volume_alsa_mute_info,
|
|
|
+ .get = volume_alsa_mute_get,
|
|
|
+};
|
|
|
+
|
|
|
+static void volume_suspend(pm_message_t state)
|
|
|
+{
|
|
|
+ tpacpi_volume_checkpoint_nvram();
|
|
|
+}
|
|
|
+
|
|
|
+static void volume_resume(void)
|
|
|
+{
|
|
|
+ volume_alsa_notify_change();
|
|
|
+}
|
|
|
+
|
|
|
+static void volume_shutdown(void)
|
|
|
+{
|
|
|
+ tpacpi_volume_checkpoint_nvram();
|
|
|
+}
|
|
|
+
|
|
|
+static void volume_exit(void)
|
|
|
+{
|
|
|
+ if (alsa_card) {
|
|
|
+ snd_card_free(alsa_card);
|
|
|
+ alsa_card = NULL;
|
|
|
+ }
|
|
|
+
|
|
|
+ tpacpi_volume_checkpoint_nvram();
|
|
|
+}
|
|
|
+
|
|
|
+static int __init volume_create_alsa_mixer(void)
|
|
|
+{
|
|
|
+ struct snd_card *card;
|
|
|
+ struct tpacpi_alsa_data *data;
|
|
|
+ struct snd_kcontrol *ctl_vol;
|
|
|
+ struct snd_kcontrol *ctl_mute;
|
|
|
+ int rc;
|
|
|
+
|
|
|
+ rc = snd_card_create(alsa_index, alsa_id, THIS_MODULE,
|
|
|
+ sizeof(struct tpacpi_alsa_data), &card);
|
|
|
+ if (rc < 0)
|
|
|
+ return rc;
|
|
|
+ if (!card)
|
|
|
+ return -ENOMEM;
|
|
|
+
|
|
|
+ BUG_ON(!card->private_data);
|
|
|
+ data = card->private_data;
|
|
|
+ data->card = card;
|
|
|
+
|
|
|
+ strlcpy(card->driver, TPACPI_ALSA_DRVNAME,
|
|
|
+ sizeof(card->driver));
|
|
|
+ strlcpy(card->shortname, TPACPI_ALSA_SHRTNAME,
|
|
|
+ sizeof(card->shortname));
|
|
|
+ snprintf(card->mixername, sizeof(card->mixername), "ThinkPad EC %s",
|
|
|
+ (thinkpad_id.ec_version_str) ?
|
|
|
+ thinkpad_id.ec_version_str : "(unknown)");
|
|
|
+ snprintf(card->longname, sizeof(card->longname),
|
|
|
+ "%s at EC reg 0x%02x, fw %s", card->shortname, TP_EC_AUDIO,
|
|
|
+ (thinkpad_id.ec_version_str) ?
|
|
|
+ thinkpad_id.ec_version_str : "unknown");
|
|
|
+
|
|
|
+ if (volume_control_allowed) {
|
|
|
+ volume_alsa_control_vol.put = volume_alsa_vol_put;
|
|
|
+ volume_alsa_control_vol.access =
|
|
|
+ SNDRV_CTL_ELEM_ACCESS_READWRITE;
|
|
|
+
|
|
|
+ volume_alsa_control_mute.put = volume_alsa_mute_put;
|
|
|
+ volume_alsa_control_mute.access =
|
|
|
+ SNDRV_CTL_ELEM_ACCESS_READWRITE;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!tp_features.mixer_no_level_control) {
|
|
|
+ ctl_vol = snd_ctl_new1(&volume_alsa_control_vol, NULL);
|
|
|
+ rc = snd_ctl_add(card, ctl_vol);
|
|
|
+ if (rc < 0) {
|
|
|
+ printk(TPACPI_ERR
|
|
|
+ "Failed to create ALSA volume control\n");
|
|
|
+ goto err_out;
|
|
|
}
|
|
|
+ data->ctl_vol_id = &ctl_vol->id;
|
|
|
+ }
|
|
|
|
|
|
- if (new_mute != mute) {
|
|
|
- /* level doesn't change */
|
|
|
+ ctl_mute = snd_ctl_new1(&volume_alsa_control_mute, NULL);
|
|
|
+ rc = snd_ctl_add(card, ctl_mute);
|
|
|
+ if (rc < 0) {
|
|
|
+ printk(TPACPI_ERR "Failed to create ALSA mute control\n");
|
|
|
+ goto err_out;
|
|
|
+ }
|
|
|
+ data->ctl_mute_id = &ctl_mute->id;
|
|
|
|
|
|
- cmos_cmd = (new_mute) ?
|
|
|
- TP_CMOS_VOLUME_MUTE : TP_CMOS_VOLUME_UP;
|
|
|
+ snd_card_set_dev(card, &tpacpi_pdev->dev);
|
|
|
+ rc = snd_card_register(card);
|
|
|
|
|
|
- if (issue_thinkpad_cmos_command(cmos_cmd) ||
|
|
|
- !acpi_ec_write(volume_offset, level + new_mute))
|
|
|
- return -EIO;
|
|
|
+err_out:
|
|
|
+ if (rc < 0) {
|
|
|
+ snd_card_free(card);
|
|
|
+ card = NULL;
|
|
|
+ }
|
|
|
+
|
|
|
+ alsa_card = card;
|
|
|
+ return rc;
|
|
|
+}
|
|
|
+
|
|
|
+#define TPACPI_VOL_Q_MUTEONLY 0x0001 /* Mute-only control available */
|
|
|
+#define TPACPI_VOL_Q_LEVEL 0x0002 /* Volume control available */
|
|
|
+
|
|
|
+static const struct tpacpi_quirk volume_quirk_table[] __initconst = {
|
|
|
+ /* Whitelist volume level on all IBM by default */
|
|
|
+ { .vendor = PCI_VENDOR_ID_IBM,
|
|
|
+ .bios = TPACPI_MATCH_ANY,
|
|
|
+ .ec = TPACPI_MATCH_ANY,
|
|
|
+ .quirks = TPACPI_VOL_Q_LEVEL },
|
|
|
+
|
|
|
+ /* Lenovo models with volume control (needs confirmation) */
|
|
|
+ TPACPI_QEC_LNV('7', 'C', TPACPI_VOL_Q_LEVEL), /* R60/i */
|
|
|
+ TPACPI_QEC_LNV('7', 'E', TPACPI_VOL_Q_LEVEL), /* R60e/i */
|
|
|
+ TPACPI_QEC_LNV('7', '9', TPACPI_VOL_Q_LEVEL), /* T60/p */
|
|
|
+ TPACPI_QEC_LNV('7', 'B', TPACPI_VOL_Q_LEVEL), /* X60/s */
|
|
|
+ TPACPI_QEC_LNV('7', 'J', TPACPI_VOL_Q_LEVEL), /* X60t */
|
|
|
+ TPACPI_QEC_LNV('7', '7', TPACPI_VOL_Q_LEVEL), /* Z60 */
|
|
|
+ TPACPI_QEC_LNV('7', 'F', TPACPI_VOL_Q_LEVEL), /* Z61 */
|
|
|
+
|
|
|
+ /* Whitelist mute-only on all Lenovo by default */
|
|
|
+ { .vendor = PCI_VENDOR_ID_LENOVO,
|
|
|
+ .bios = TPACPI_MATCH_ANY,
|
|
|
+ .ec = TPACPI_MATCH_ANY,
|
|
|
+ .quirks = TPACPI_VOL_Q_MUTEONLY }
|
|
|
+};
|
|
|
+
|
|
|
+static int __init volume_init(struct ibm_init_struct *iibm)
|
|
|
+{
|
|
|
+ unsigned long quirks;
|
|
|
+ int rc;
|
|
|
+
|
|
|
+ vdbg_printk(TPACPI_DBG_INIT, "initializing volume subdriver\n");
|
|
|
+
|
|
|
+ mutex_init(&volume_mutex);
|
|
|
+
|
|
|
+ /*
|
|
|
+ * Check for module parameter bogosity, note that we
|
|
|
+ * init volume_mode to TPACPI_VOL_MODE_MAX in order to be
|
|
|
+ * able to detect "unspecified"
|
|
|
+ */
|
|
|
+ if (volume_mode > TPACPI_VOL_MODE_MAX)
|
|
|
+ return -EINVAL;
|
|
|
+
|
|
|
+ if (volume_mode == TPACPI_VOL_MODE_UCMS_STEP) {
|
|
|
+ printk(TPACPI_ERR
|
|
|
+ "UCMS step volume mode not implemented, "
|
|
|
+ "please contact %s\n", TPACPI_MAIL);
|
|
|
+ return 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (volume_capabilities >= TPACPI_VOL_CAP_MAX)
|
|
|
+ return -EINVAL;
|
|
|
+
|
|
|
+ /*
|
|
|
+ * The ALSA mixer is our primary interface.
|
|
|
+ * When disabled, don't install the subdriver at all
|
|
|
+ */
|
|
|
+ if (!alsa_enable) {
|
|
|
+ vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_MIXER,
|
|
|
+ "ALSA mixer disabled by parameter, "
|
|
|
+ "not loading volume subdriver...\n");
|
|
|
+ return 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ quirks = tpacpi_check_quirks(volume_quirk_table,
|
|
|
+ ARRAY_SIZE(volume_quirk_table));
|
|
|
+
|
|
|
+ switch (volume_capabilities) {
|
|
|
+ case TPACPI_VOL_CAP_AUTO:
|
|
|
+ if (quirks & TPACPI_VOL_Q_MUTEONLY)
|
|
|
+ tp_features.mixer_no_level_control = 1;
|
|
|
+ else if (quirks & TPACPI_VOL_Q_LEVEL)
|
|
|
+ tp_features.mixer_no_level_control = 0;
|
|
|
+ else
|
|
|
+ return 1; /* no mixer */
|
|
|
+ break;
|
|
|
+ case TPACPI_VOL_CAP_VOLMUTE:
|
|
|
+ tp_features.mixer_no_level_control = 0;
|
|
|
+ break;
|
|
|
+ case TPACPI_VOL_CAP_MUTEONLY:
|
|
|
+ tp_features.mixer_no_level_control = 1;
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ return 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (volume_capabilities != TPACPI_VOL_CAP_AUTO)
|
|
|
+ dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_MIXER,
|
|
|
+ "using user-supplied volume_capabilities=%d\n",
|
|
|
+ volume_capabilities);
|
|
|
+
|
|
|
+ if (volume_mode == TPACPI_VOL_MODE_AUTO ||
|
|
|
+ volume_mode == TPACPI_VOL_MODE_MAX) {
|
|
|
+ volume_mode = TPACPI_VOL_MODE_ECNVRAM;
|
|
|
+
|
|
|
+ dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_MIXER,
|
|
|
+ "driver auto-selected volume_mode=%d\n",
|
|
|
+ volume_mode);
|
|
|
+ } else {
|
|
|
+ dbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_MIXER,
|
|
|
+ "using user-supplied volume_mode=%d\n",
|
|
|
+ volume_mode);
|
|
|
+ }
|
|
|
+
|
|
|
+ vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_MIXER,
|
|
|
+ "mute is supported, volume control is %s\n",
|
|
|
+ str_supported(!tp_features.mixer_no_level_control));
|
|
|
+
|
|
|
+ rc = volume_create_alsa_mixer();
|
|
|
+ if (rc) {
|
|
|
+ printk(TPACPI_ERR
|
|
|
+ "Could not create the ALSA mixer interface\n");
|
|
|
+ return rc;
|
|
|
+ }
|
|
|
+
|
|
|
+ printk(TPACPI_INFO
|
|
|
+ "Console audio control enabled, mode: %s\n",
|
|
|
+ (volume_control_allowed) ?
|
|
|
+ "override (read/write)" :
|
|
|
+ "monitor (read only)");
|
|
|
+
|
|
|
+ vdbg_printk(TPACPI_DBG_INIT | TPACPI_DBG_MIXER,
|
|
|
+ "registering volume hotkeys as change notification\n");
|
|
|
+ tpacpi_hotkey_driver_mask_set(hotkey_driver_mask
|
|
|
+ | TP_ACPI_HKEY_VOLUP_MASK
|
|
|
+ | TP_ACPI_HKEY_VOLDWN_MASK
|
|
|
+ | TP_ACPI_HKEY_MUTE_MASK);
|
|
|
+
|
|
|
+ return 0;
|
|
|
+}
|
|
|
+
|
|
|
+static int volume_read(struct seq_file *m)
|
|
|
+{
|
|
|
+ u8 status;
|
|
|
+
|
|
|
+ if (volume_get_status(&status) < 0) {
|
|
|
+ seq_printf(m, "level:\t\tunreadable\n");
|
|
|
+ } else {
|
|
|
+ if (tp_features.mixer_no_level_control)
|
|
|
+ seq_printf(m, "level:\t\tunsupported\n");
|
|
|
+ else
|
|
|
+ seq_printf(m, "level:\t\t%d\n",
|
|
|
+ status & TP_EC_AUDIO_LVL_MSK);
|
|
|
+
|
|
|
+ seq_printf(m, "mute:\t\t%s\n",
|
|
|
+ onoff(status, TP_EC_AUDIO_MUTESW));
|
|
|
+
|
|
|
+ if (volume_control_allowed) {
|
|
|
+ seq_printf(m, "commands:\tunmute, mute\n");
|
|
|
+ if (!tp_features.mixer_no_level_control) {
|
|
|
+ seq_printf(m,
|
|
|
+ "commands:\tup, down\n");
|
|
|
+ seq_printf(m,
|
|
|
+ "commands:\tlevel <level>"
|
|
|
+ " (<level> is 0-%d)\n",
|
|
|
+ TP_EC_VOLUME_MAX);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return 0;
|
|
|
}
|
|
|
|
|
|
+static int volume_write(char *buf)
|
|
|
+{
|
|
|
+ u8 s;
|
|
|
+ u8 new_level, new_mute;
|
|
|
+ int l;
|
|
|
+ char *cmd;
|
|
|
+ int rc;
|
|
|
+
|
|
|
+ /*
|
|
|
+ * We do allow volume control at driver startup, so that the
|
|
|
+ * user can set initial state through the volume=... parameter hack.
|
|
|
+ */
|
|
|
+ if (!volume_control_allowed && tpacpi_lifecycle != TPACPI_LIFE_INIT) {
|
|
|
+ if (unlikely(!tp_warned.volume_ctrl_forbidden)) {
|
|
|
+ tp_warned.volume_ctrl_forbidden = 1;
|
|
|
+ printk(TPACPI_NOTICE
|
|
|
+ "Console audio control in monitor mode, "
|
|
|
+ "changes are not allowed.\n");
|
|
|
+ printk(TPACPI_NOTICE
|
|
|
+ "Use the volume_control=1 module parameter "
|
|
|
+ "to enable volume control\n");
|
|
|
+ }
|
|
|
+ return -EPERM;
|
|
|
+ }
|
|
|
+
|
|
|
+ rc = volume_get_status(&s);
|
|
|
+ if (rc < 0)
|
|
|
+ return rc;
|
|
|
+
|
|
|
+ new_level = s & TP_EC_AUDIO_LVL_MSK;
|
|
|
+ new_mute = s & TP_EC_AUDIO_MUTESW_MSK;
|
|
|
+
|
|
|
+ while ((cmd = next_cmd(&buf))) {
|
|
|
+ if (!tp_features.mixer_no_level_control) {
|
|
|
+ if (strlencmp(cmd, "up") == 0) {
|
|
|
+ if (new_mute)
|
|
|
+ new_mute = 0;
|
|
|
+ else if (new_level < TP_EC_VOLUME_MAX)
|
|
|
+ new_level++;
|
|
|
+ continue;
|
|
|
+ } else if (strlencmp(cmd, "down") == 0) {
|
|
|
+ if (new_mute)
|
|
|
+ new_mute = 0;
|
|
|
+ else if (new_level > 0)
|
|
|
+ new_level--;
|
|
|
+ continue;
|
|
|
+ } else if (sscanf(cmd, "level %u", &l) == 1 &&
|
|
|
+ l >= 0 && l <= TP_EC_VOLUME_MAX) {
|
|
|
+ new_level = l;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (strlencmp(cmd, "mute") == 0)
|
|
|
+ new_mute = TP_EC_AUDIO_MUTESW_MSK;
|
|
|
+ else if (strlencmp(cmd, "unmute") == 0)
|
|
|
+ new_mute = 0;
|
|
|
+ else
|
|
|
+ return -EINVAL;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (tp_features.mixer_no_level_control) {
|
|
|
+ tpacpi_disclose_usertask("procfs volume", "%smute\n",
|
|
|
+ new_mute ? "" : "un");
|
|
|
+ rc = volume_set_mute(!!new_mute);
|
|
|
+ } else {
|
|
|
+ tpacpi_disclose_usertask("procfs volume",
|
|
|
+ "%smute and set level to %d\n",
|
|
|
+ new_mute ? "" : "un", new_level);
|
|
|
+ rc = volume_set_status(new_mute | new_level);
|
|
|
+ }
|
|
|
+ volume_alsa_notify_change();
|
|
|
+
|
|
|
+ return (rc == -EINTR) ? -ERESTARTSYS : rc;
|
|
|
+}
|
|
|
+
|
|
|
static struct ibm_struct volume_driver_data = {
|
|
|
.name = "volume",
|
|
|
.read = volume_read,
|
|
|
.write = volume_write,
|
|
|
+ .exit = volume_exit,
|
|
|
+ .suspend = volume_suspend,
|
|
|
+ .resume = volume_resume,
|
|
|
+ .shutdown = volume_shutdown,
|
|
|
};
|
|
|
|
|
|
/*************************************************************************
|
|
@@ -7507,9 +8090,8 @@ static void fan_resume(void)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-static int fan_read(char *p)
|
|
|
+static int fan_read(struct seq_file *m)
|
|
|
{
|
|
|
- int len = 0;
|
|
|
int rc;
|
|
|
u8 status;
|
|
|
unsigned int speed = 0;
|
|
@@ -7521,7 +8103,7 @@ static int fan_read(char *p)
|
|
|
if (rc < 0)
|
|
|
return rc;
|
|
|
|
|
|
- len += sprintf(p + len, "status:\t\t%s\n"
|
|
|
+ seq_printf(m, "status:\t\t%s\n"
|
|
|
"level:\t\t%d\n",
|
|
|
(status != 0) ? "enabled" : "disabled", status);
|
|
|
break;
|
|
@@ -7532,54 +8114,54 @@ static int fan_read(char *p)
|
|
|
if (rc < 0)
|
|
|
return rc;
|
|
|
|
|
|
- len += sprintf(p + len, "status:\t\t%s\n",
|
|
|
+ seq_printf(m, "status:\t\t%s\n",
|
|
|
(status != 0) ? "enabled" : "disabled");
|
|
|
|
|
|
rc = fan_get_speed(&speed);
|
|
|
if (rc < 0)
|
|
|
return rc;
|
|
|
|
|
|
- len += sprintf(p + len, "speed:\t\t%d\n", speed);
|
|
|
+ seq_printf(m, "speed:\t\t%d\n", speed);
|
|
|
|
|
|
if (status & TP_EC_FAN_FULLSPEED)
|
|
|
/* Disengaged mode takes precedence */
|
|
|
- len += sprintf(p + len, "level:\t\tdisengaged\n");
|
|
|
+ seq_printf(m, "level:\t\tdisengaged\n");
|
|
|
else if (status & TP_EC_FAN_AUTO)
|
|
|
- len += sprintf(p + len, "level:\t\tauto\n");
|
|
|
+ seq_printf(m, "level:\t\tauto\n");
|
|
|
else
|
|
|
- len += sprintf(p + len, "level:\t\t%d\n", status);
|
|
|
+ seq_printf(m, "level:\t\t%d\n", status);
|
|
|
break;
|
|
|
|
|
|
case TPACPI_FAN_NONE:
|
|
|
default:
|
|
|
- len += sprintf(p + len, "status:\t\tnot supported\n");
|
|
|
+ seq_printf(m, "status:\t\tnot supported\n");
|
|
|
}
|
|
|
|
|
|
if (fan_control_commands & TPACPI_FAN_CMD_LEVEL) {
|
|
|
- len += sprintf(p + len, "commands:\tlevel <level>");
|
|
|
+ seq_printf(m, "commands:\tlevel <level>");
|
|
|
|
|
|
switch (fan_control_access_mode) {
|
|
|
case TPACPI_FAN_WR_ACPI_SFAN:
|
|
|
- len += sprintf(p + len, " (<level> is 0-7)\n");
|
|
|
+ seq_printf(m, " (<level> is 0-7)\n");
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
- len += sprintf(p + len, " (<level> is 0-7, "
|
|
|
+ seq_printf(m, " (<level> is 0-7, "
|
|
|
"auto, disengaged, full-speed)\n");
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (fan_control_commands & TPACPI_FAN_CMD_ENABLE)
|
|
|
- len += sprintf(p + len, "commands:\tenable, disable\n"
|
|
|
+ seq_printf(m, "commands:\tenable, disable\n"
|
|
|
"commands:\twatchdog <timeout> (<timeout> "
|
|
|
"is 0 (off), 1-120 (seconds))\n");
|
|
|
|
|
|
if (fan_control_commands & TPACPI_FAN_CMD_SPEED)
|
|
|
- len += sprintf(p + len, "commands:\tspeed <speed>"
|
|
|
+ seq_printf(m, "commands:\tspeed <speed>"
|
|
|
" (<speed> is 0-65535)\n");
|
|
|
|
|
|
- return len;
|
|
|
+ return 0;
|
|
|
}
|
|
|
|
|
|
static int fan_write_cmd_level(const char *cmd, int *rc)
|
|
@@ -7721,10 +8303,23 @@ static struct ibm_struct fan_driver_data = {
|
|
|
*/
|
|
|
static void tpacpi_driver_event(const unsigned int hkey_event)
|
|
|
{
|
|
|
+ if (ibm_backlight_device) {
|
|
|
+ switch (hkey_event) {
|
|
|
+ case TP_HKEY_EV_BRGHT_UP:
|
|
|
+ case TP_HKEY_EV_BRGHT_DOWN:
|
|
|
+ tpacpi_brightness_notify_change();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (alsa_card) {
|
|
|
+ switch (hkey_event) {
|
|
|
+ case TP_HKEY_EV_VOL_UP:
|
|
|
+ case TP_HKEY_EV_VOL_DOWN:
|
|
|
+ case TP_HKEY_EV_VOL_MUTE:
|
|
|
+ volume_alsa_notify_change();
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-
|
|
|
-
|
|
|
static void hotkey_driver_event(const unsigned int scancode)
|
|
|
{
|
|
|
tpacpi_driver_event(TP_HKEY_EV_HOTKEY_BASE + scancode);
|
|
@@ -7853,19 +8448,19 @@ static int __init ibm_init(struct ibm_init_struct *iibm)
|
|
|
"%s installed\n", ibm->name);
|
|
|
|
|
|
if (ibm->read) {
|
|
|
- entry = create_proc_entry(ibm->name,
|
|
|
- S_IFREG | S_IRUGO | S_IWUSR,
|
|
|
- proc_dir);
|
|
|
+ mode_t mode;
|
|
|
+
|
|
|
+ mode = S_IRUGO;
|
|
|
+ if (ibm->write)
|
|
|
+ mode |= S_IWUSR;
|
|
|
+ entry = proc_create_data(ibm->name, mode, proc_dir,
|
|
|
+ &dispatch_proc_fops, ibm);
|
|
|
if (!entry) {
|
|
|
printk(TPACPI_ERR "unable to create proc entry %s\n",
|
|
|
ibm->name);
|
|
|
ret = -ENODEV;
|
|
|
goto err_out;
|
|
|
}
|
|
|
- entry->data = ibm;
|
|
|
- entry->read_proc = &dispatch_procfs_read;
|
|
|
- if (ibm->write)
|
|
|
- entry->write_proc = &dispatch_procfs_write;
|
|
|
ibm->flags.proc_created = 1;
|
|
|
}
|
|
|
|
|
@@ -8077,6 +8672,7 @@ static struct ibm_init_struct ibms_init[] __initdata = {
|
|
|
.data = &brightness_driver_data,
|
|
|
},
|
|
|
{
|
|
|
+ .init = volume_init,
|
|
|
.data = &volume_driver_data,
|
|
|
},
|
|
|
{
|
|
@@ -8112,36 +8708,59 @@ static int __init set_ibm_param(const char *val, struct kernel_param *kp)
|
|
|
return -EINVAL;
|
|
|
}
|
|
|
|
|
|
-module_param(experimental, int, 0);
|
|
|
+module_param(experimental, int, 0444);
|
|
|
MODULE_PARM_DESC(experimental,
|
|
|
"Enables experimental features when non-zero");
|
|
|
|
|
|
module_param_named(debug, dbg_level, uint, 0);
|
|
|
MODULE_PARM_DESC(debug, "Sets debug level bit-mask");
|
|
|
|
|
|
-module_param(force_load, bool, 0);
|
|
|
+module_param(force_load, bool, 0444);
|
|
|
MODULE_PARM_DESC(force_load,
|
|
|
"Attempts to load the driver even on a "
|
|
|
"mis-identified ThinkPad when true");
|
|
|
|
|
|
-module_param_named(fan_control, fan_control_allowed, bool, 0);
|
|
|
+module_param_named(fan_control, fan_control_allowed, bool, 0444);
|
|
|
MODULE_PARM_DESC(fan_control,
|
|
|
"Enables setting fan parameters features when true");
|
|
|
|
|
|
-module_param_named(brightness_mode, brightness_mode, uint, 0);
|
|
|
+module_param_named(brightness_mode, brightness_mode, uint, 0444);
|
|
|
MODULE_PARM_DESC(brightness_mode,
|
|
|
"Selects brightness control strategy: "
|
|
|
"0=auto, 1=EC, 2=UCMS, 3=EC+NVRAM");
|
|
|
|
|
|
-module_param(brightness_enable, uint, 0);
|
|
|
+module_param(brightness_enable, uint, 0444);
|
|
|
MODULE_PARM_DESC(brightness_enable,
|
|
|
"Enables backlight control when 1, disables when 0");
|
|
|
|
|
|
-module_param(hotkey_report_mode, uint, 0);
|
|
|
+module_param(hotkey_report_mode, uint, 0444);
|
|
|
MODULE_PARM_DESC(hotkey_report_mode,
|
|
|
"used for backwards compatibility with userspace, "
|
|
|
"see documentation");
|
|
|
|
|
|
+module_param_named(volume_mode, volume_mode, uint, 0444);
|
|
|
+MODULE_PARM_DESC(volume_mode,
|
|
|
+ "Selects volume control strategy: "
|
|
|
+ "0=auto, 1=EC, 2=N/A, 3=EC+NVRAM");
|
|
|
+
|
|
|
+module_param_named(volume_capabilities, volume_capabilities, uint, 0444);
|
|
|
+MODULE_PARM_DESC(volume_capabilities,
|
|
|
+ "Selects the mixer capabilites: "
|
|
|
+ "0=auto, 1=volume and mute, 2=mute only");
|
|
|
+
|
|
|
+module_param_named(volume_control, volume_control_allowed, bool, 0444);
|
|
|
+MODULE_PARM_DESC(volume_control,
|
|
|
+ "Enables software override for the console audio "
|
|
|
+ "control when true");
|
|
|
+
|
|
|
+/* ALSA module API parameters */
|
|
|
+module_param_named(index, alsa_index, int, 0444);
|
|
|
+MODULE_PARM_DESC(index, "ALSA index for the ACPI EC Mixer");
|
|
|
+module_param_named(id, alsa_id, charp, 0444);
|
|
|
+MODULE_PARM_DESC(id, "ALSA id for the ACPI EC Mixer");
|
|
|
+module_param_named(enable, alsa_enable, bool, 0444);
|
|
|
+MODULE_PARM_DESC(enable, "Enable the ALSA interface for the ACPI EC Mixer");
|
|
|
+
|
|
|
#define TPACPI_PARAM(feature) \
|
|
|
module_param_call(feature, set_ibm_param, NULL, NULL, 0); \
|
|
|
MODULE_PARM_DESC(feature, "Simulates thinkpad-acpi procfs command " \
|
|
@@ -8160,25 +8779,25 @@ TPACPI_PARAM(volume);
|
|
|
TPACPI_PARAM(fan);
|
|
|
|
|
|
#ifdef CONFIG_THINKPAD_ACPI_DEBUGFACILITIES
|
|
|
-module_param(dbg_wlswemul, uint, 0);
|
|
|
+module_param(dbg_wlswemul, uint, 0444);
|
|
|
MODULE_PARM_DESC(dbg_wlswemul, "Enables WLSW emulation");
|
|
|
module_param_named(wlsw_state, tpacpi_wlsw_emulstate, bool, 0);
|
|
|
MODULE_PARM_DESC(wlsw_state,
|
|
|
"Initial state of the emulated WLSW switch");
|
|
|
|
|
|
-module_param(dbg_bluetoothemul, uint, 0);
|
|
|
+module_param(dbg_bluetoothemul, uint, 0444);
|
|
|
MODULE_PARM_DESC(dbg_bluetoothemul, "Enables bluetooth switch emulation");
|
|
|
module_param_named(bluetooth_state, tpacpi_bluetooth_emulstate, bool, 0);
|
|
|
MODULE_PARM_DESC(bluetooth_state,
|
|
|
"Initial state of the emulated bluetooth switch");
|
|
|
|
|
|
-module_param(dbg_wwanemul, uint, 0);
|
|
|
+module_param(dbg_wwanemul, uint, 0444);
|
|
|
MODULE_PARM_DESC(dbg_wwanemul, "Enables WWAN switch emulation");
|
|
|
module_param_named(wwan_state, tpacpi_wwan_emulstate, bool, 0);
|
|
|
MODULE_PARM_DESC(wwan_state,
|
|
|
"Initial state of the emulated WWAN switch");
|
|
|
|
|
|
-module_param(dbg_uwbemul, uint, 0);
|
|
|
+module_param(dbg_uwbemul, uint, 0444);
|
|
|
MODULE_PARM_DESC(dbg_uwbemul, "Enables UWB switch emulation");
|
|
|
module_param_named(uwb_state, tpacpi_uwb_emulstate, bool, 0);
|
|
|
MODULE_PARM_DESC(uwb_state,
|
|
@@ -8371,6 +8990,7 @@ static int __init thinkpad_acpi_module_init(void)
|
|
|
PCI_VENDOR_ID_IBM;
|
|
|
tpacpi_inputdev->id.product = TPACPI_HKEY_INPUT_PRODUCT;
|
|
|
tpacpi_inputdev->id.version = TPACPI_HKEY_INPUT_VERSION;
|
|
|
+ tpacpi_inputdev->dev.parent = &tpacpi_pdev->dev;
|
|
|
}
|
|
|
for (i = 0; i < ARRAY_SIZE(ibms_init); i++) {
|
|
|
ret = ibm_init(&ibms_init[i]);
|