From: Rahul Pon <theflyingrahul@gmail.com>
To: "Hans de Goede" <hansg@kernel.org>,
"Ilpo Järvinen" <ilpo.jarvinen@linux.intel.com>
Cc: Bryan O'Donoghue <bryan.odonoghue@linaro.org>,
Rob Herring <robh@kernel.org>,
Krzysztof Kozlowski <krzk+dt@kernel.org>,
Conor Dooley <conor+dt@kernel.org>,
Guenter Roeck <linux@roeck-us.net>,
platform-driver-x86@vger.kernel.org, devicetree@vger.kernel.org,
linux-hwmon@vger.kernel.org, linux-arm-msm@vger.kernel.org,
linux-kernel@vger.kernel.org
Subject: [PATCH 2/2] platform: arm64: Add HP OmniBook 5 16 embedded controller driver
Date: Mon, 9 Nov 2026 16:19:14 +0530 [thread overview]
Message-ID: <20261109104914.1534-3-theflyingrahul@gmail.com> (raw)
In-Reply-To: <20261109104914.1534-1-theflyingrahul@gmail.com>
The embedded controller of the HP OmniBook 5 16 (Snapdragon X2) sits on
I2C at address 0x76 and takes commands through a mailbox: wait for an
idle status (0xef), write the command and sub-command (0xed), wait for
completion, then read the 32-byte reply (0xee). Its interrupt line
signals events, read from register 0x00 and acknowledged by writing 0
back, as HP's ACPI does.
Expose:
- the fan speed and the four thermistors HP's thermal zones use, through
hwmon;
- the two EC LEDs HP's mute LED WMI methods drive, as the mute and mic
mute LEDs;
- the keyboard backlight timeout (30 s, 3 min or always on) and the
current backlight level as sysfs attributes; the EC runs the backlight
itself, and its backlight events notify the level attribute;
- the F9 (mic mute) and F11 (programmable) hotkeys, which only the EC
reports, as input events, using HP's WMI key codes as hp-wmi maps them.
The command codes, event codes and reply formats are the ones HP's ACPI
methods use on this laptop.
Signed-off-by: Rahul Pon <theflyingrahul@gmail.com>
---
.../ABI/testing/sysfs-driver-hp-omnibook-5-ec | 19 +
MAINTAINERS | 8 +
drivers/platform/arm64/Kconfig | 17 +
drivers/platform/arm64/Makefile | 1 +
drivers/platform/arm64/hp-omnibook-5-ec.c | 417 ++++++++++++++++++
5 files changed, 462 insertions(+)
create mode 100644 Documentation/ABI/testing/sysfs-driver-hp-omnibook-5-ec
create mode 100644 drivers/platform/arm64/hp-omnibook-5-ec.c
diff --git a/Documentation/ABI/testing/sysfs-driver-hp-omnibook-5-ec b/Documentation/ABI/testing/sysfs-driver-hp-omnibook-5-ec
new file mode 100644
index 0000000000..4c6f5cb20e
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-driver-hp-omnibook-5-ec
@@ -0,0 +1,19 @@
+What: /sys/bus/i2c/devices/<bus>-0076/kbd_backlight_timeout
+Date: September 2026
+KernelVersion: 7.4
+Contact: Rahul Pon <theflyingrahul@gmail.com>
+Description:
+ (RW) How long the keyboard backlight of the HP OmniBook 5 16
+ stays on after the last key press, as the embedded controller
+ applies it: "30s", "3min" or "always". The Fn key still turns
+ the backlight on and off.
+
+What: /sys/bus/i2c/devices/<bus>-0076/kbd_backlight_level
+Date: September 2026
+KernelVersion: 7.4
+Contact: Rahul Pon <theflyingrahul@gmail.com>
+Description:
+ (RO) The keyboard backlight level of the HP OmniBook 5 16:
+ 0 (off), 1 or 2. Only the backlight key changes it, inside the
+ embedded controller. The attribute is notified when the EC
+ reports a change.
diff --git a/MAINTAINERS b/MAINTAINERS
index aa647f5d37..d043a2f8f6 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -12284,6 +12284,14 @@ L: platform-driver-x86@vger.kernel.org
S: Orphan
F: drivers/platform/x86/hp/tc1100-wmi.c
+HP OMNIBOOK 5 16 EMBEDDED CONTROLLER DRIVER
+M: Rahul Pon <theflyingrahul@gmail.com>
+L: platform-driver-x86@vger.kernel.org
+S: Maintained
+F: Documentation/ABI/testing/sysfs-driver-hp-omnibook-5-ec
+F: Documentation/devicetree/bindings/embedded-controller/hp,omnibook-5-16-bf1xxx-ec.yaml
+F: drivers/platform/arm64/hp-omnibook-5-ec.c
+
HP WMI HARDWARE MONITOR DRIVER
M: James Seo <james@equiv.tech>
L: linux-hwmon@vger.kernel.org
diff --git a/drivers/platform/arm64/Kconfig b/drivers/platform/arm64/Kconfig
index 3f879410b3..2b34f5ab1b 100644
--- a/drivers/platform/arm64/Kconfig
+++ b/drivers/platform/arm64/Kconfig
@@ -48,6 +48,23 @@ config EC_ASUS_GLYMUR
temperature sensors readout, fan monitoring, keyboard and EC event
reporting.
+config EC_HP_OMNIBOOK5
+ tristate "HP OmniBook 5 16 Embedded Controller driver"
+ depends on ARCH_QCOM || COMPILE_TEST
+ depends on I2C
+ depends on HWMON
+ depends on INPUT
+ select INPUT_SPARSEKMAP
+ select LEDS_CLASS
+ select NEW_LEDS
+ help
+ Driver for the Embedded Controller in the Snapdragon X2-based HP
+ OmniBook 5 16, which reports the fan speed, thermistor
+ temperatures and hotkeys, and drives the mute LEDs and the
+ keyboard backlight timeout.
+
+ Say M or Y here to include this support.
+
config EC_HUAWEI_GAOKUN
tristate "Huawei Matebook E Go Embedded Controller driver"
depends on ARCH_QCOM || COMPILE_TEST
diff --git a/drivers/platform/arm64/Makefile b/drivers/platform/arm64/Makefile
index 3bc3d4669c..6c72726eae 100644
--- a/drivers/platform/arm64/Makefile
+++ b/drivers/platform/arm64/Makefile
@@ -7,6 +7,7 @@
obj-$(CONFIG_EC_ACER_ASPIRE1) += acer-aspire1-ec.o
obj-$(CONFIG_EC_ASUS_GLYMUR) += asus-glymur-ec.o
+obj-$(CONFIG_EC_HP_OMNIBOOK5) += hp-omnibook-5-ec.o
obj-$(CONFIG_EC_HUAWEI_GAOKUN) += huawei-gaokun-ec.o
obj-$(CONFIG_EC_LENOVO_YOGA_C630) += lenovo-yoga-c630.o
obj-$(CONFIG_EC_LENOVO_THINKPAD_T14S) += lenovo-thinkpad-t14s.o
diff --git a/drivers/platform/arm64/hp-omnibook-5-ec.c b/drivers/platform/arm64/hp-omnibook-5-ec.c
new file mode 100644
index 0000000000..bfe20e12aa
--- /dev/null
+++ b/drivers/platform/arm64/hp-omnibook-5-ec.c
@@ -0,0 +1,417 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * HP OmniBook 5 16 (Snapdragon X2) embedded controller
+ *
+ * The EC takes commands through a mailbox: wait for an idle status, write
+ * the command, wait for it to complete, then read the reply. Command codes
+ * are the ones HP's ACPI tables use on this laptop.
+ */
+
+#include <linux/cleanup.h>
+#include <linux/delay.h>
+#include <linux/hwmon.h>
+#include <linux/i2c.h>
+#include <linux/input.h>
+#include <linux/input/sparse-keymap.h>
+#include <linux/interrupt.h>
+#include <linux/leds.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/string.h>
+#include <linux/sysfs.h>
+#include <linux/unaligned.h>
+
+/* The pending event, cleared by writing 0 (HP's IC10.INTR, field CMD5) */
+#define HPEC_EVENT 0x00
+#define HPEC_STATUS 0xef
+#define HPEC_REQUEST 0xed
+#define HPEC_REPLY 0xee
+#define HPEC_REPLY_LEN 32
+
+#define HPEC_STATUS_IDLE 0x00
+#define HPEC_STATUS_DONE 0xe0
+#define HPEC_POLL_TRIES 20
+#define HPEC_POLL_US 5000
+
+#define HPEC_OS_FLAG 0x01
+#define HPEC_OS_FLAG_LEDS 0x84
+#define HPEC_KBD 0x05
+#define HPEC_KBD_TIMEOUT_GET 0xfe
+#define HPEC_KBD_LEVEL_GET 0xff
+#define HPEC_LED 0x06
+#define HPEC_LED_GET 0xff
+#define HPEC_LED_ON 0x80
+#define HPEC_THERMISTOR 0x26
+#define HPEC_THERMISTOR_INVALID 0xff
+#define HPEC_FAN 0xbc
+#define HPEC_FAN_RPM 0x03
+
+/* The four thermistors HP's thermal zones enable, in their order */
+static const u8 hpec_thermistors[] = { 0x00, 0x07, 0x04, 0x0a };
+
+static const char * const hpec_thermistor_labels[] = {
+ "EC thermistor 1", "EC thermistor 2", "EC thermistor 3", "EC thermistor 4",
+};
+
+/* EC events, with HP's WMI hotkey codes from hp-wmi where they exist */
+#define HPEC_EVENT_HOT 0x0b
+#define HPEC_EVENT_KBD 0x0d
+#define HPEC_EVENT_KBD_KEY 0x1d
+
+static const struct key_entry hpec_keymap[] = {
+ { KE_IGNORE, 0x0d, }, /* backlight key, run by the EC */
+ { KE_KEY, 0x11, { KEY_MICMUTE } }, /* F9, WMI 0x270 */
+ { KE_IGNORE, 0x1d, }, /* backlight key, run by the EC */
+ { KE_KEY, 0x1e, { KEY_PROG2 } }, /* F11, WMI 0x21a8 */
+ { KE_IGNORE, 0x30, }, /* fan and thermistor notices */
+ { KE_IGNORE, 0x31, },
+ { KE_IGNORE, 0x32, },
+ { KE_IGNORE, 0x33, },
+ { KE_IGNORE, 0x34, },
+ { KE_IGNORE, 0x35, },
+ { KE_IGNORE, 0x36, },
+ { KE_IGNORE, 0x37, },
+ { KE_IGNORE, 0x38, },
+ { KE_IGNORE, 0x88, }, /* Fn lock, WMI 0x21ab */
+ { KE_END, 0 }
+};
+
+/* Keyboard backlight timeout, in EC value order */
+static const char * const hpec_kbd_timeouts[] = { "30s", "3min", "always" };
+
+/* EC LEDs 8 and 9, which HP's mute LED WMI methods drive */
+static const struct {
+ const char *name;
+ const char *trigger;
+ u8 index;
+} hpec_leds[] = {
+ { "platform::mute", "audio-mute", 0x08 },
+ { "platform::micmute", "audio-micmute", 0x09 },
+};
+
+struct hpec;
+
+struct hpec_led {
+ struct led_classdev cdev;
+ struct hpec *ec;
+ u8 index;
+};
+
+struct hpec {
+ struct i2c_client *client;
+ struct mutex lock; /* serializes mailbox commands */
+ struct hpec_led leds[ARRAY_SIZE(hpec_leds)];
+ struct input_dev *input;
+};
+
+static int hpec_wait(struct hpec *ec, u8 status)
+{
+ int i, ret;
+
+ for (i = 0; i < HPEC_POLL_TRIES; i++) {
+ ret = i2c_smbus_read_byte_data(ec->client, HPEC_STATUS);
+ if (ret < 0)
+ return ret;
+ if (ret == status)
+ return 0;
+ fsleep(HPEC_POLL_US);
+ }
+
+ return -ETIMEDOUT;
+}
+
+static int hpec_command(struct hpec *ec, u8 cmd, u8 sub, u8 reply[HPEC_REPLY_LEN])
+{
+ u8 request[] = { cmd, sub };
+ int ret;
+
+ guard(mutex)(&ec->lock);
+
+ ret = hpec_wait(ec, HPEC_STATUS_IDLE);
+ if (ret)
+ return ret;
+
+ ret = i2c_smbus_write_i2c_block_data(ec->client, HPEC_REQUEST,
+ sizeof(request), request);
+ if (ret)
+ return ret;
+
+ ret = hpec_wait(ec, HPEC_STATUS_DONE);
+ if (ret || !reply)
+ return ret;
+
+ ret = i2c_smbus_read_i2c_block_data(ec->client, HPEC_REPLY,
+ HPEC_REPLY_LEN, reply);
+ if (ret < 0)
+ return ret;
+
+ return ret == HPEC_REPLY_LEN ? 0 : -EIO;
+}
+
+static int hpec_hwmon_read(struct device *dev, enum hwmon_sensor_types type,
+ u32 attr, int channel, long *val)
+{
+ struct hpec *ec = dev_get_drvdata(dev);
+ u8 reply[HPEC_REPLY_LEN];
+ int ret;
+
+ switch (type) {
+ case hwmon_fan:
+ ret = hpec_command(ec, HPEC_FAN, HPEC_FAN_RPM, reply);
+ if (ret)
+ return ret;
+ *val = get_unaligned_le16(reply);
+ return 0;
+ case hwmon_temp:
+ ret = hpec_command(ec, HPEC_THERMISTOR, hpec_thermistors[channel], reply);
+ if (ret)
+ return ret;
+ if (reply[0] == HPEC_THERMISTOR_INVALID)
+ return -ENODATA;
+ *val = reply[0] * 1000;
+ return 0;
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static int hpec_hwmon_read_string(struct device *dev, enum hwmon_sensor_types type,
+ u32 attr, int channel, const char **str)
+{
+ if (type != hwmon_temp)
+ return -EOPNOTSUPP;
+
+ *str = hpec_thermistor_labels[channel];
+ return 0;
+}
+
+static umode_t hpec_hwmon_is_visible(const void *data, enum hwmon_sensor_types type,
+ u32 attr, int channel)
+{
+ return 0444;
+}
+
+static const struct hwmon_ops hpec_hwmon_ops = {
+ .is_visible = hpec_hwmon_is_visible,
+ .read = hpec_hwmon_read,
+ .read_string = hpec_hwmon_read_string,
+};
+
+static const struct hwmon_channel_info * const hpec_hwmon_info[] = {
+ HWMON_CHANNEL_INFO(chip, HWMON_C_REGISTER_TZ),
+ HWMON_CHANNEL_INFO(fan, HWMON_F_INPUT),
+ HWMON_CHANNEL_INFO(temp,
+ HWMON_T_INPUT | HWMON_T_LABEL,
+ HWMON_T_INPUT | HWMON_T_LABEL,
+ HWMON_T_INPUT | HWMON_T_LABEL,
+ HWMON_T_INPUT | HWMON_T_LABEL),
+ NULL
+};
+
+static const struct hwmon_chip_info hpec_hwmon_chip = {
+ .ops = &hpec_hwmon_ops,
+ .info = hpec_hwmon_info,
+};
+
+static enum led_brightness hpec_led_get(struct led_classdev *cdev)
+{
+ struct hpec_led *led = container_of(cdev, struct hpec_led, cdev);
+ u8 reply[HPEC_REPLY_LEN];
+
+ if (hpec_command(led->ec, HPEC_LED, HPEC_LED_GET, reply))
+ return LED_OFF;
+
+ return (reply[1] >> (led->index - hpec_leds[0].index)) & 1;
+}
+
+static int hpec_led_set(struct led_classdev *cdev, enum led_brightness value)
+{
+ struct hpec_led *led = container_of(cdev, struct hpec_led, cdev);
+ int ret;
+
+ /* As HP's method does before every change */
+ ret = hpec_command(led->ec, HPEC_OS_FLAG, HPEC_OS_FLAG_LEDS, NULL);
+ if (ret)
+ return ret;
+
+ return hpec_command(led->ec, HPEC_LED,
+ led->index | (value ? HPEC_LED_ON : 0), NULL);
+}
+
+static irqreturn_t hpec_irq(int irq, void *data)
+{
+ struct hpec *ec = data;
+ struct device *dev = &ec->client->dev;
+ int event;
+
+ scoped_guard(mutex, &ec->lock) {
+ event = i2c_smbus_read_byte_data(ec->client, HPEC_EVENT);
+ if (event < 0) {
+ dev_err_ratelimited(dev, "cannot read EC event: %d\n", event);
+ return IRQ_HANDLED;
+ }
+ i2c_smbus_write_byte_data(ec->client, HPEC_EVENT, 0);
+ }
+
+ if (!event)
+ return IRQ_HANDLED;
+
+ if (event == HPEC_EVENT_KBD || event == HPEC_EVENT_KBD_KEY)
+ sysfs_notify(&dev->kobj, NULL, "kbd_backlight_level");
+
+ if (event == HPEC_EVENT_HOT) {
+ dev_crit(dev, "EC reports a critical temperature\n");
+ return IRQ_HANDLED;
+ }
+
+ if (!sparse_keymap_report_event(ec->input, event, 1, true))
+ dev_info_ratelimited(dev, "unknown EC event %#x\n", event);
+
+ return IRQ_HANDLED;
+}
+
+static ssize_t kbd_backlight_timeout_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct hpec *ec = dev_get_drvdata(dev);
+ u8 reply[HPEC_REPLY_LEN];
+ int ret;
+
+ ret = hpec_command(ec, HPEC_KBD, HPEC_KBD_TIMEOUT_GET, reply);
+ if (ret)
+ return ret;
+
+ if (reply[0] >= ARRAY_SIZE(hpec_kbd_timeouts))
+ return sysfs_emit(buf, "%#x\n", reply[0]);
+
+ return sysfs_emit(buf, "%s\n", hpec_kbd_timeouts[reply[0]]);
+}
+
+static ssize_t kbd_backlight_timeout_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct hpec *ec = dev_get_drvdata(dev);
+ int i, ret;
+
+ i = sysfs_match_string(hpec_kbd_timeouts, buf);
+ if (i < 0)
+ return i;
+
+ ret = hpec_command(ec, HPEC_KBD, i, NULL);
+
+ return ret ?: count;
+}
+static DEVICE_ATTR_RW(kbd_backlight_timeout);
+
+/* The level is set only by the backlight key, inside the EC */
+static ssize_t kbd_backlight_level_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct hpec *ec = dev_get_drvdata(dev);
+ u8 reply[HPEC_REPLY_LEN];
+ int ret;
+
+ ret = hpec_command(ec, HPEC_KBD, HPEC_KBD_LEVEL_GET, reply);
+ if (ret)
+ return ret;
+
+ return sysfs_emit(buf, "%u\n", reply[0]);
+}
+static DEVICE_ATTR_RO(kbd_backlight_level);
+
+static struct attribute *hpec_attrs[] = {
+ &dev_attr_kbd_backlight_level.attr,
+ &dev_attr_kbd_backlight_timeout.attr,
+ NULL
+};
+ATTRIBUTE_GROUPS(hpec);
+
+static int hpec_probe(struct i2c_client *client)
+{
+ struct device *dev = &client->dev;
+ struct device *hwmon;
+ u8 reply[HPEC_REPLY_LEN];
+ struct hpec *ec;
+ unsigned int i;
+ int ret;
+
+ ec = devm_kzalloc(dev, sizeof(*ec), GFP_KERNEL);
+ if (!ec)
+ return -ENOMEM;
+
+ ec->client = client;
+ i2c_set_clientdata(client, ec);
+ ret = devm_mutex_init(dev, &ec->lock);
+ if (ret)
+ return ret;
+
+ ret = hpec_command(ec, HPEC_FAN, HPEC_FAN_RPM, reply);
+ if (ret)
+ return dev_err_probe(dev, ret, "EC does not answer\n");
+
+ ec->input = devm_input_allocate_device(dev);
+ if (!ec->input)
+ return -ENOMEM;
+
+ ec->input->name = "HP OmniBook 5 EC hotkeys";
+ ec->input->phys = "hp-omnibook-5-ec/input0";
+ ec->input->id.bustype = BUS_I2C;
+
+ ret = sparse_keymap_setup(ec->input, hpec_keymap, NULL);
+ if (ret)
+ return ret;
+
+ ret = input_register_device(ec->input);
+ if (ret)
+ return ret;
+
+ hwmon = devm_hwmon_device_register_with_info(dev, "hp_omnibook_ec", ec,
+ &hpec_hwmon_chip, NULL);
+ if (IS_ERR(hwmon))
+ return PTR_ERR(hwmon);
+
+ for (i = 0; i < ARRAY_SIZE(hpec_leds); i++) {
+ struct hpec_led *led = &ec->leds[i];
+
+ led->ec = ec;
+ led->index = hpec_leds[i].index;
+ led->cdev.name = hpec_leds[i].name;
+ led->cdev.default_trigger = hpec_leds[i].trigger;
+ led->cdev.max_brightness = 1;
+ led->cdev.brightness_get = hpec_led_get;
+ led->cdev.brightness_set_blocking = hpec_led_set;
+
+ ret = devm_led_classdev_register(dev, &led->cdev);
+ if (ret)
+ return ret;
+ }
+
+ if (client->irq > 0) {
+ ret = devm_request_threaded_irq(dev, client->irq, NULL, hpec_irq,
+ IRQF_ONESHOT, "hp-omnibook-5-ec", ec);
+ if (ret)
+ return dev_err_probe(dev, ret, "cannot request the EC interrupt\n");
+ }
+
+ return 0;
+}
+
+static const struct of_device_id hpec_of_match[] = {
+ { .compatible = "hp,omnibook-5-16-bf1xxx-ec" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, hpec_of_match);
+
+static struct i2c_driver hpec_driver = {
+ .driver = {
+ .name = "hp-omnibook-5-ec",
+ .of_match_table = hpec_of_match,
+ .dev_groups = hpec_groups,
+ },
+ .probe = hpec_probe,
+};
+module_i2c_driver(hpec_driver);
+
+MODULE_DESCRIPTION("HP OmniBook 5 16 embedded controller");
+MODULE_LICENSE("GPL");
--
2.53.0
prev parent reply other threads:[~2026-11-09 10:49 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-09 10:49 [PATCH 0/2] platform: arm64: HP OmniBook 5 16 embedded controller Rahul Pon
2026-11-09 10:49 ` [PATCH 1/2] dt-bindings: embedded-controller: Add HP OmniBook 5 16 EC Rahul Pon
2026-11-09 10:49 ` Rahul Pon [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261109104914.1534-3-theflyingrahul@gmail.com \
--to=theflyingrahul@gmail.com \
--cc=bryan.odonoghue@linaro.org \
--cc=conor+dt@kernel.org \
--cc=devicetree@vger.kernel.org \
--cc=hansg@kernel.org \
--cc=ilpo.jarvinen@linux.intel.com \
--cc=krzk+dt@kernel.org \
--cc=linux-arm-msm@vger.kernel.org \
--cc=linux-hwmon@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux@roeck-us.net \
--cc=platform-driver-x86@vger.kernel.org \
--cc=robh@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®