From: Cai Yu <caiyu7372@gmail.com>
To: Marcel Holtmann <marcel@holtmann.org>,
Luiz Augusto von Dentz <luiz.dentz@gmail.com>
Cc: linux-bluetooth@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: [RFC PATCH 3/4] Bluetooth: hci_intel: add serdev support for the CcP controller
Date: Sat, 26 Sep 2026 07:40:42 +0800 [thread overview]
Message-ID: <20260925234043.707679-4-caiyu7372@gmail.com> (raw)
In-Reply-To: <20260925234043.707679-1-caiyu7372@gmail.com>
The Intel Bluetooth controller on the ThinkPad X1 Fold Gen1 (Lakefield,
ACPI INT33E4) is described as a serdev child of its LPSS UART: the port
has no tty device at all, so the line discipline path of this driver can
never be used and nothing binds to the controller today.
Add a serdev driver next to the platform driver, in the same way hci_bcm
supports controllers of both kinds:
- the ACPI id INT33E4 is matched in a separate table, so the platform
driver keeps its INT33E1/INT33E3 set;
- the probe power cycles the controller through the "reset" GPIO (it
does not keep the firmware across a power cycle, and this also clears
a controller left in an unknown state) and waits for the bootloader to
come up before the first command. On this board 500 ms of reset pulse
plus 2 s of boot delay are needed, otherwise the first command is lost;
- this UART is described with FlowControlHardware, so the serial core
enables CTS/RTS. The Intel handshake has to transmit freely before
the controller answers, so flow control is turned off;
- the serdev path uses a protocol struct with oper_speed = 0. The
generic baudrate change in hci_serdev.c runs before intel_setup(), but
the controller is in bootloader mode at that point: it only answers at
init_speed until the firmware has been downloaded, and it is not even
listening yet right after the reset pulse. Switching the host to
oper_speed there makes the setup fail with a -110 timeout on the
version read; intel_setup() changes the baudrate itself.
Tested on a Lenovo ThinkPad X1 Fold Gen1 (20RKA000CD): the controller
comes up as hci0, the Intel firmware is downloaded, the DDC parameters
are applied and the (sole) HOG keyboard connects.
Signed-off-by: Cai Yu <caiyu7372@gmail.com>
---
drivers/bluetooth/hci_intel.c | 132 +++++++++++++++++++++++++++++++++-
1 file changed, 131 insertions(+), 1 deletion(-)
diff --git a/drivers/bluetooth/hci_intel.c b/drivers/bluetooth/hci_intel.c
index d25d029..5926d1f 100644
--- a/drivers/bluetooth/hci_intel.c
+++ b/drivers/bluetooth/hci_intel.c
@@ -544,6 +544,14 @@ static int intel_setup(struct hci_uart *hu)
bt_dev_dbg(hdev, "");
+ /* ACPI describes the UART of this controller with FlowControlHardware,
+ * so the serial core enables CTS/RTS. The Intel handshake has to
+ * transmit freely at init_speed before the controller starts answering,
+ * otherwise the first commands time out; keep flow control off.
+ */
+ if (hu->serdev)
+ serdev_device_set_flow_control(hu->serdev, false);
+
hu->hdev->set_diag = btintel_set_diag;
hu->hdev->set_bdaddr = btintel_set_bdaddr;
@@ -1067,6 +1075,33 @@ static const struct hci_uart_proto intel_proto = {
.dequeue = intel_dequeue,
};
+/* Do not let the generic baudrate change in hci_serdev.c run for this path.
+ *
+ * It is executed before intel_setup() (and right after the controller has been
+ * power cycled by the probe), but the controller is still in bootloader mode at
+ * that point: it only answers at init_speed until the firmware has been
+ * downloaded, and it needs a few seconds after the reset pulse before it
+ * answers at all. Switching the host to oper_speed while the controller is
+ * silent leaves the two sides at different baudrates and the setup never
+ * recovers. intel_setup() changes the baudrate itself, once the bootloader is
+ * talking.
+ */
+static const struct hci_uart_proto intel_serdev_proto = {
+ .id = HCI_UART_INTEL,
+ .name = "Intel",
+ .manufacturer = 2,
+ .init_speed = 115200,
+ .oper_speed = 0,
+ .open = intel_open,
+ .close = intel_close,
+ .flush = intel_flush,
+ .setup = intel_setup,
+ .set_baudrate = intel_set_baudrate,
+ .recv = intel_recv,
+ .enqueue = intel_enqueue,
+ .dequeue = intel_dequeue,
+};
+
#ifdef CONFIG_ACPI
static const struct acpi_device_id intel_acpi_match[] = {
{ .id = "INT33E1" },
@@ -1074,6 +1109,16 @@ static const struct acpi_device_id intel_acpi_match[] = {
{ }
};
MODULE_DEVICE_TABLE(acpi, intel_acpi_match);
+
+/* Controllers which the firmware describes as a serdev child of their UART
+ * instead of as an LPSS platform device. The CcP controller (Lakefield,
+ * Jasper Lake) is one of them.
+ */
+static const struct acpi_device_id intel_serdev_acpi_match[] = {
+ { .id = "INT33E4" },
+ { }
+};
+MODULE_DEVICE_TABLE(acpi, intel_serdev_acpi_match);
#endif
static int intel_suspend_device(struct device *dev)
@@ -1217,6 +1262,67 @@ static struct platform_driver intel_driver = {
},
};
+#ifdef CONFIG_ACPI
+/* The controller does not keep the firmware across a power cycle, so it starts
+ * in bootloader mode every time; power cycling it here also makes sure a
+ * controller left in an unknown state by a previous boot cannot break the
+ * setup. Measured on the ThinkPad X1 Fold Gen1: 500 ms of reset pulse, then
+ * 2 s until the bootloader answers the first command.
+ */
+#define INTEL_RESET_PULSE_MS 500
+#define INTEL_BOOT_DELAY_MS 2000
+
+static int intel_serdev_probe(struct serdev_device *serdev)
+{
+ struct hci_uart *hu;
+ struct gpio_desc *reset;
+
+ hu = devm_kzalloc(&serdev->dev, sizeof(*hu), GFP_KERNEL);
+ if (!hu)
+ return -ENOMEM;
+
+ hu->serdev = serdev;
+
+ /* The port is not open yet (hci_uart_register_device() opens it), so
+ * only the ACPI properties and the reset GPIO can be used here.
+ */
+ if (devm_acpi_dev_add_driver_gpios(&serdev->dev, acpi_hci_intel_gpios))
+ dev_dbg(&serdev->dev, "No ACPI GPIO mapping table\n");
+
+ reset = devm_gpiod_get_optional(&serdev->dev, "reset", GPIOD_OUT_HIGH);
+ if (IS_ERR(reset))
+ return dev_err_probe(&serdev->dev, PTR_ERR(reset),
+ "Unable to retrieve reset gpio\n");
+
+ if (reset) {
+ gpiod_set_value_cansleep(reset, 0);
+ msleep(INTEL_RESET_PULSE_MS);
+ gpiod_set_value_cansleep(reset, 1);
+ msleep(INTEL_BOOT_DELAY_MS);
+ } else {
+ dev_warn(&serdev->dev, "No reset gpio, relying on the firmware state\n");
+ }
+
+ return hci_uart_register_device(hu, &intel_serdev_proto);
+}
+
+static void intel_serdev_remove(struct serdev_device *serdev)
+{
+ struct hci_uart *hu = serdev_device_get_drvdata(serdev);
+
+ hci_uart_unregister_device(hu);
+}
+
+static struct serdev_device_driver intel_serdev_driver = {
+ .probe = intel_serdev_probe,
+ .remove = intel_serdev_remove,
+ .driver = {
+ .name = "hci_uart_intel",
+ .acpi_match_table = ACPI_PTR(intel_serdev_acpi_match),
+ },
+};
+#endif
+
int __init intel_init(void)
{
int err;
@@ -1225,12 +1331,36 @@ int __init intel_init(void)
if (err)
return err;
+#ifdef CONFIG_ACPI
+ err = serdev_device_driver_register(&intel_serdev_driver);
+ if (err)
+ goto err_platform;
+
+ err = hci_uart_register_proto(&intel_proto);
+ if (err)
+ goto err_serdev;
+
+ return 0;
+
+err_serdev:
+ serdev_device_driver_unregister(&intel_serdev_driver);
+err_platform:
+ platform_driver_unregister(&intel_driver);
+ return err;
+#else
return hci_uart_register_proto(&intel_proto);
+#endif
}
int __exit intel_deinit(void)
{
+ hci_uart_unregister_proto(&intel_proto);
+
+#ifdef CONFIG_ACPI
+ serdev_device_driver_unregister(&intel_serdev_driver);
+#endif
+
platform_driver_unregister(&intel_driver);
- return hci_uart_unregister_proto(&intel_proto);
+ return 0;
}
next prev parent reply other threads:[~2026-09-25 23:40 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-25 23:40 [RFC PATCH 0/4] Bluetooth: hci_intel: support the CcP controller (X1 Fold Gen1) Cai Yu
2026-09-25 23:40 ` [RFC PATCH 1/4] Bluetooth: hci_intel: fix tty-only assumptions in the LPM paths Cai Yu
2026-09-26 1:11 ` Bluetooth: hci_intel: support the CcP controller (X1 Fold Gen1) bluez.test.bot
2026-09-25 23:40 ` [RFC PATCH 2/4] Bluetooth: hci_intel: add the CcP controller (hardware variant 0x14) Cai Yu
2026-09-25 23:40 ` Cai Yu [this message]
2026-09-25 23:40 ` [RFC PATCH 4/4] Bluetooth: hci_intel: download the CcP firmware at 921.6 kbaud Cai Yu
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=20260925234043.707679-4-caiyu7372@gmail.com \
--to=caiyu7372@gmail.com \
--cc=linux-bluetooth@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=luiz.dentz@gmail.com \
--cc=marcel@holtmann.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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.