Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 329e4e18 authored by Henrique de Moraes Holschuh's avatar Henrique de Moraes Holschuh Committed by Len Brown
Browse files

thinkpad-acpi: volume subdriver rewrite



I don't trust the coupled EC writes and SMI calls the current volume
control code does very much, although it is exactly what the IBM DSDTs
seem to do (they never do more than a single step though).

Change the driver to stop issuing SMIs, and just drive the EC directly
to the desired level (DSDTs seem to confirm this will work even on
very old models like the 570 and 600e/x).

We checkpoint directly to NVRAM (this can be turned off) at
suspend/shutdown/driver unload, which from what I can see in tbp,
should also work on every ThinkPad.

Signed-off-by: default avatarHenrique de Moraes Holschuh <hmh@hmh.eng.br>
Cc: Lorne Applebaum <lorne.applebaum@gmail.com>
Cc: Matthew Garrett <mjg@redhat.com>
Signed-off-by: default avatarLen Brown <len.brown@intel.com>
parent 5451a923
Loading
Loading
Loading
Loading
+17 −5
Original line number Original line Diff line number Diff line
@@ -1092,22 +1092,33 @@ WARNING:
    its level up and down at every change.
    its level up and down at every change.




Volume control -- /proc/acpi/ibm/volume
Volume control
---------------------------------------
--------------

procfs: /proc/acpi/ibm/volume


This feature allows volume control on ThinkPad models which don't have
This feature allows volume control on ThinkPad models with a digital
a hardware volume knob. The available commands are:
volume knob, as well as mute/unmute control.  The available commands are:


	echo up   >/proc/acpi/ibm/volume
	echo up   >/proc/acpi/ibm/volume
	echo down >/proc/acpi/ibm/volume
	echo down >/proc/acpi/ibm/volume
	echo mute >/proc/acpi/ibm/volume
	echo mute >/proc/acpi/ibm/volume
	echo 'level <level>' >/proc/acpi/ibm/volume
	echo 'level <level>' >/proc/acpi/ibm/volume


The <level> number range is 0 to 15 although not all of them may be
The <level> number range is 0 to 14 although not all of them may be
distinct. The unmute the volume after the mute command, use either the
distinct. The unmute the volume after the mute command, use either the
up or down command (the level command will not unmute the volume).
up or down command (the level command will not unmute the volume).
The current volume level and mute state is shown in the file.
The current volume level and mute state is shown in the file.


There are two strategies for volume control.  To select which one
should be used, use the volume_mode module parameter: volume_mode=1
selects EC mode, and volume_mode=3 selects EC mode with NVRAM backing
(so that volume/mute changes are remembered across shutdown/reboot).

The driver will operate in volume_mode=3 by default. If that does not
work well on your ThinkPad model, please report this to
ibm-acpi-devel@lists.sourceforge.net.

The ALSA mixer interface to this feature is still missing, but patches
The ALSA mixer interface to this feature is still missing, but patches
to add it exist.  That problem should be addressed in the not so
to add it exist.  That problem should be addressed in the not so
distant future.
distant future.
@@ -1376,6 +1387,7 @@ to enable more than one output class, just add their values.
	0x0008			HKEY event interface, hotkeys
	0x0008			HKEY event interface, hotkeys
	0x0010			Fan control
	0x0010			Fan control
	0x0020			Backlight brightness
	0x0020			Backlight brightness
	0x0040			Audio mixer/volume control


There is also a kernel build option to enable more debugging
There is also a kernel build option to enable more debugging
information, which may be necessary to debug driver problems.
information, which may be necessary to debug driver problems.
+282 −58
Original line number Original line Diff line number Diff line
@@ -231,6 +231,7 @@ enum tpacpi_hkey_event_t {
#define TPACPI_DBG_HKEY		0x0008
#define TPACPI_DBG_HKEY		0x0008
#define TPACPI_DBG_FAN		0x0010
#define TPACPI_DBG_FAN		0x0010
#define TPACPI_DBG_BRGHT	0x0020
#define TPACPI_DBG_BRGHT	0x0020
#define TPACPI_DBG_MIXER	0x0040


#define onoff(status, bit) ((status) & (1 << (bit)) ? "on" : "off")
#define onoff(status, bit) ((status) & (1 << (bit)) ? "on" : "off")
#define enabled(status, bit) ((status) & (1 << (bit)) ? "enabled" : "disabled")
#define enabled(status, bit) ((status) & (1 << (bit)) ? "enabled" : "disabled")
@@ -6375,21 +6376,260 @@ static struct ibm_struct brightness_driver_data = {
 * Volume subdriver
 * 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.
 */

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
};

static enum tpacpi_volume_access_mode volume_mode =
	TPACPI_VOL_MODE_MAX;


/*
 * 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)
{
	u8 lec = 0;
	u8 b_nvram;
	const u8 ec_mask = TP_EC_AUDIO_LVL_MSK | TP_EC_AUDIO_MUTESW_MSK;

	if (volume_mode != TPACPI_VOL_MODE_ECNVRAM)
		return;

	vdbg_printk(TPACPI_DBG_MIXER,
		"trying to checkpoint mixer state to NVRAM...\n");

	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 {
		vdbg_printk(TPACPI_DBG_MIXER,
			   "NVRAM mixer status already is 0x%02x (0x%02x)\n",
			   (unsigned int) lec, (unsigned int) b_nvram);
	}

unlock:
	mutex_unlock(&volume_mutex);
}

static int volume_get_status_ec(u8 *status)
{
	u8 s;

	if (!acpi_ec_read(TP_EC_AUDIO, &s))
		return -EIO;

	*status = s;

	dbg_printk(TPACPI_DBG_MIXER, "status 0x%02x\n", s);

	return 0;
}

static int volume_get_status(u8 *status)
{
	return volume_get_status_ec(status);
}

static int volume_set_status_ec(const u8 status)
{
	if (!acpi_ec_write(TP_EC_AUDIO, status))
		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_suspend(pm_message_t state)
{
	tpacpi_volume_checkpoint_nvram();
}

static void volume_shutdown(void)
{
	tpacpi_volume_checkpoint_nvram();
}

static void volume_exit(void)
{
	tpacpi_volume_checkpoint_nvram();
}

static int __init volume_init(struct ibm_init_struct *iibm)
{
	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_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,
			"volume is supported\n");

	return 0;
}


static int volume_read(char *p)
static int volume_read(char *p)
{
{
	int len = 0;
	int len = 0;
	u8 level;
	u8 status;


	if (!acpi_ec_read(volume_offset, &level)) {
	if (volume_get_status(&status) < 0) {
		len += sprintf(p + len, "level:\t\tunreadable\n");
		len += sprintf(p + len, "level:\t\tunreadable\n");
	} else {
	} else {
		len += sprintf(p + len, "level:\t\t%d\n", level & 0xf);
		len += sprintf(p + len, "level:\t\t%d\n",
		len += sprintf(p + len, "mute:\t\t%s\n", onoff(level, 6));
				status & TP_EC_AUDIO_LVL_MSK);
		len += sprintf(p + len, "mute:\t\t%s\n",
				onoff(status, TP_EC_AUDIO_MUTESW));
		len += sprintf(p + len, "commands:\tup, down, mute\n");
		len += sprintf(p + len, "commands:\tup, down, mute\n");
		len += sprintf(p + len, "commands:\tlevel <level>"
		len += sprintf(p + len, "commands:\tlevel <level>"
			       " (<level> is 0-15)\n");
			       " (<level> is 0-%d)\n", TP_EC_VOLUME_MAX);
	}
	}


	return len;
	return len;
@@ -6397,77 +6637,55 @@ static int volume_read(char *p)


static int volume_write(char *buf)
static int volume_write(char *buf)
{
{
	int cmos_cmd, inc, i;
	u8 s;
	u8 level, mute;
	u8 new_level, new_mute;
	int new_level, new_mute;
	int l;
	char *cmd;
	char *cmd;
	int rc;


	while ((cmd = next_cmd(&buf))) {
	rc = volume_get_status(&s);
		if (!acpi_ec_read(volume_offset, &level))
	if (rc < 0)
			return -EIO;
		return rc;
		new_mute = mute = level & 0x40;

		new_level = level = level & 0xf;
	new_level = s & TP_EC_AUDIO_LVL_MSK;
	new_mute  = s & TP_EC_AUDIO_MUTESW_MSK;


	while ((cmd = next_cmd(&buf))) {
		if (strlencmp(cmd, "up") == 0) {
		if (strlencmp(cmd, "up") == 0) {
			if (mute)
			if (new_mute)
				new_mute = 0;
				new_mute = 0;
			else
			else if (new_level < TP_EC_VOLUME_MAX)
				new_level = level == 15 ? 15 : level + 1;
				new_level++;
		} else if (strlencmp(cmd, "down") == 0) {
		} else if (strlencmp(cmd, "down") == 0) {
			if (mute)
			if (new_mute)
				new_mute = 0;
				new_mute = 0;
			else
			else if (new_level > 0)
				new_level = level == 0 ? 0 : level - 1;
				new_level--;
		} else if (sscanf(cmd, "level %d", &new_level) == 1 &&
		} else if (sscanf(cmd, "level %u", &l) == 1 &&
			   new_level >= 0 && new_level <= 15) {
			   l >= 0 && l <= TP_EC_VOLUME_MAX) {
			/* new_level set */
				new_level = l;
		} else if (strlencmp(cmd, "mute") == 0) {
		} else if (strlencmp(cmd, "mute") == 0) {
			new_mute = 0x40;
			new_mute = TP_EC_AUDIO_MUTESW_MSK;
		} else
		} else
			return -EINVAL;
			return -EINVAL;

		if (new_level != level) {
			/* mute doesn't change */

			cmos_cmd = (new_level > level) ?
					TP_CMOS_VOLUME_UP : TP_CMOS_VOLUME_DOWN;
			inc = new_level > level ? 1 : -1;

			if (mute && (issue_thinkpad_cmos_command(cmos_cmd) ||
				     !acpi_ec_write(volume_offset, level)))
				return -EIO;

			for (i = level; i != new_level; i += inc)
				if (issue_thinkpad_cmos_command(cmos_cmd) ||
				    !acpi_ec_write(volume_offset, i + inc))
					return -EIO;

			if (mute &&
			    (issue_thinkpad_cmos_command(TP_CMOS_VOLUME_MUTE) ||
			     !acpi_ec_write(volume_offset, new_level + mute))) {
				return -EIO;
			}
	}
	}


		if (new_mute != mute) {
	tpacpi_disclose_usertask("procfs volume",
			/* level doesn't change */
				"%smute and set level to %d\n",
				new_mute ? "" : "un", new_level);


			cmos_cmd = (new_mute) ?
	rc = volume_set_status(new_mute | new_level);
				   TP_CMOS_VOLUME_MUTE : TP_CMOS_VOLUME_UP;


			if (issue_thinkpad_cmos_command(cmos_cmd) ||
	return (rc == -EINTR) ? -ERESTARTSYS : rc;
			    !acpi_ec_write(volume_offset, level + new_mute))
				return -EIO;
		}
	}

	return 0;
}
}


static struct ibm_struct volume_driver_data = {
static struct ibm_struct volume_driver_data = {
	.name = "volume",
	.name = "volume",
	.read = volume_read,
	.read = volume_read,
	.write = volume_write,
	.write = volume_write,
	.exit = volume_exit,
	.suspend = volume_suspend,
	.shutdown = volume_shutdown,
};
};


/*************************************************************************
/*************************************************************************
@@ -8121,6 +8339,7 @@ static struct ibm_init_struct ibms_init[] __initdata = {
		.data = &brightness_driver_data,
		.data = &brightness_driver_data,
	},
	},
	{
	{
		.init = volume_init,
		.data = &volume_driver_data,
		.data = &volume_driver_data,
	},
	},
	{
	{
@@ -8186,6 +8405,11 @@ MODULE_PARM_DESC(hotkey_report_mode,
		 "used for backwards compatibility with userspace, "
		 "used for backwards compatibility with userspace, "
		 "see documentation");
		 "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");

#define TPACPI_PARAM(feature) \
#define TPACPI_PARAM(feature) \
	module_param_call(feature, set_ibm_param, NULL, NULL, 0); \
	module_param_call(feature, set_ibm_param, NULL, NULL, 0); \
	MODULE_PARM_DESC(feature, "Simulates thinkpad-acpi procfs command " \
	MODULE_PARM_DESC(feature, "Simulates thinkpad-acpi procfs command " \