[PATCH v3 3/5] drivers: usb: host: efi: add efi io driver

Fabian Pflug <[email protected]>
Newsgroups org.infradead.lists.barebox
Message-ID <20260820-v2026-06-0-topic-efi_usb-v3-3-faf0b9267966@pengutronix.de>
The driver is based on [1] with one "host" controller per device.
The host controller will hopefully not have any sub-devices and only
have the one root device, which functions as the only device needed and
registered for.

[1] https://uefi.org/specs/UEFI/2.11/17_Protocols_USB_Support.html#usb-driver-model

Signed-off-by: Fabian Pflug <[email protected]>

---
v3:
- change return type when EFI_USB_ERR_STALL
- use goto in function on error instead of return. (Missed that...)
- lang table size of 0 is now not an error.
- failure in setting string descriptors are no errors.
- set host-hw-dev
- remove USB_HOST from Kconfig
v2:
- add missing free to string buffer
- use 16bit data type for string buffer
- include the first byte of string buffer
- renamed num_langs to table_size and made usage clearer
---
 drivers/usb/host/Kconfig           |  10 ++
 drivers/usb/host/Makefile          |   1 +
 drivers/usb/host/efi-io-protocol.c | 313 +++++++++++++++++++++++++++++++++++++
 efi/guid.c                         |   1 +
 include/efi/guid.h                 |   1 +
 5 files changed, 326 insertions(+)

diff --git a/drivers/usb/host/Kconfig b/drivers/usb/host/Kconfig
index 58f276cdb4..a2ddcb2bff 100644
--- a/drivers/usb/host/Kconfig
+++ b/drivers/usb/host/Kconfig
@@ -22,6 +22,16 @@ config USB_EHCI_ZYNQ
 	help
 	  Enable support for Zynq on-chip EHCI USB controller
 
+config USB_EFI_IO_PROTOCOL
+	bool "EFI USB I/O Protocol"
+	depends on EFI_PAYLOAD
+	help
+	  Enable support for usb devices, initialized by the UEFI BIOS.
+	  Use this with caution, as devices may afterwards have multiple drivers.
+	  For example can a USB-Stick be treated as an IO Blockdevice by UEFI and
+	  have a driver for it, but can also be initiated as a USB-Storage device,
+	  which could lead to errors in handling the USB-Device.
+
 config USB_OHCI
 	bool "OHCI driver"
 	depends on !MMU && HAS_DMA
diff --git a/drivers/usb/host/Makefile b/drivers/usb/host/Makefile
index cbddfbe923..4f77680fc7 100644
--- a/drivers/usb/host/Makefile
+++ b/drivers/usb/host/Makefile
@@ -6,3 +6,4 @@ obj-$(CONFIG_USB_EHCI_ZYNQ)	+= ehci-zynq.o
 obj-$(CONFIG_USB_OHCI)		+= ohci-hcd.o
 obj-$(CONFIG_USB_OHCI_AT91)	+= ohci-at91.o
 obj-$(CONFIG_USB_XHCI)		+= xhci.o xhci-mem.o xhci-ring.o
+obj-$(CONFIG_USB_EFI_IO_PROTOCOL)	+= efi-io-protocol.o
diff --git a/drivers/usb/host/efi-io-protocol.c b/drivers/usb/host/efi-io-protocol.c
new file mode 100644
index 0000000000..cdba353e96
--- /dev/null
+++ b/drivers/usb/host/efi-io-protocol.c
@@ -0,0 +1,313 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <efi/payload.h>
+#include <efi/payload/init.h>
+#include <efi/payload/driver.h>
+#include <efi/protocol/usb.h>
+#include <efi/error.h>
+#include <linux/usb/usb.h>
+
+#include "../core/usb.h"
+
+struct efi_usb_io_priv {
+	struct efi_usb_io_protocol *protocol;
+	struct device *dev;
+	struct usb_host host;
+};
+
+#define usb_dev_to_efi_priv(ptr) \
+	container_of(ptr->host, struct efi_usb_io_priv, host)
+
+static int efi_usb_error_check(efi_status_t efiret, int status)
+{
+	if (efiret == EFI_DEVICE_ERROR) {
+		if (status & EFI_USB_ERR_TIMEOUT)
+			return -ETIMEDOUT;
+		if (status & EFI_USB_ERR_STALL)
+			return -EPIPE;
+		if (status & EFI_USB_ERR_NAK)
+			return -EPROTO;
+		if (status & EFI_USB_ERR_BUFFER)
+			return -EINVAL;
+		if (status & EFI_USB_ERR_NOTEXECUTE)
+			return -EIO;
+		if (status & EFI_USB_ERR_BABBLE)
+			return -EIO;
+		if (status & EFI_USB_ERR_CRC)
+			return -EIO;
+		if (status & EFI_USB_ERR_BITSTUFF)
+			return -EIO;
+		if (status & EFI_USB_ERR_SYSTEM)
+			return -EIO;
+	}
+
+	if (EFI_ERROR(efiret))
+		return -efi_errno(efiret);
+
+	return 0;
+}
+
+static int efi_usb_control_msg(struct usb_device *dev, unsigned long pipe,
+			       void *buffer, int length,
+			       struct devrequest *setup, int timeout)
+{
+	struct efi_usb_io_priv *priv = usb_dev_to_efi_priv(dev);
+	enum efi_usb_data_direction direction;
+	efi_status_t efiret;
+	efi_uintn_t efi_timeout = timeout;
+	u32 status;
+
+	if (usb_pipein(pipe))
+		direction = EFI_USB_DATA_IN;
+	else
+		direction = EFI_USB_DATA_OUT;
+	if (length == 0)
+		direction = EFI_USB_NO_DATA;
+
+	efiret = priv->protocol->control_transfer(priv->protocol, setup,
+						  direction, efi_timeout,
+						  buffer, length, &status);
+
+	dev->status = status; // dev-status is a long, status is u32
+	dev->act_len = length;
+
+	return efi_usb_error_check(efiret, status);
+}
+
+static int efi_usb_bulk_msg(struct usb_device *dev, unsigned long pipe,
+			    void *buffer, int length, int timeout)
+{
+	struct efi_usb_io_priv *priv = usb_dev_to_efi_priv(dev);
+	efi_uintn_t efi_length = length;
+	efi_status_t efiret;
+	u32 status;
+
+	u8 epnum = usb_pipeendpoint(pipe) | (usb_pipein(pipe) << 7);
+
+	efiret = priv->protocol->bulk_transfer(priv->protocol, epnum, buffer,
+					       &efi_length, timeout, &status);
+
+	dev->status = status;
+	dev->act_len = efi_length;
+
+	return efi_usb_error_check(efiret, status);
+}
+
+static int efi_usb_int_msg(struct usb_device *dev, unsigned long pipe,
+			   void *buffer, int length,
+			   int __always_unused interval)
+{
+	struct efi_usb_io_priv *priv = usb_dev_to_efi_priv(dev);
+	efi_status_t efiret;
+	u32 status;
+	efi_uintn_t efi_length = length;
+
+	u8 epnum = usb_pipeendpoint(pipe) | (usb_pipein(pipe) << 7);
+
+	efiret = priv->protocol->sync_interrupt_transfer(
+		priv->protocol, epnum, buffer, &efi_length, 100, &status);
+
+	dev->status = status;
+	dev->act_len = efi_length;
+
+	return efi_usb_error_check(efiret, status);
+}
+
+static int efi_get_usb_string(struct efi_usb_io_protocol *protocol, u16 lang_id,
+			      int index, char *buf, size_t size)
+{
+	wchar_t *efi_name;
+	efi_status_t efiret;
+	unsigned int u, idx;
+
+	memset(buf, 0, size);
+
+	if (!index)
+		return 0;
+
+	efiret = protocol->get_string_descriptor(protocol, lang_id, index,
+						 &efi_name);
+	if (EFI_ERROR(efiret))
+		return -efi_errno(efiret);
+
+	size--; /* leave room for trailing NULL char in output buffer */
+	for (idx = 0, u = 0;; u++) {
+		if (idx >= size)
+			break;
+		if (efi_name[u] & 0xff00) /* high byte */
+			buf[idx++] = '?'; /* non-ASCII character */
+		else if (efi_name[u])
+			buf[idx++] = efi_name[u] & 0xff;
+		else
+			break;
+	}
+	buf[idx] = 0;
+	BS->free_pool(efi_name);
+
+	return 0;
+}
+
+static int create_usb_device(struct efi_usb_io_priv *priv)
+{
+	struct usb_host *host = &priv->host;
+	struct usb_device *dev;
+	efi_status_t efiret;
+	struct usb_interface *interface;
+	int err;
+	u16 *lang_ids;
+	u16 table_size;
+
+	dev = usb_alloc_new_device();
+	dev->host = host;
+
+	dev_set_name(&dev->dev, "usb%d", dev->host->busnum);
+	dev->dev.id = DEVICE_ID_SINGLE;
+
+	efiret = priv->protocol->get_device_descriptor(priv->protocol,
+						       dev->descriptor);
+	if (EFI_ERROR(efiret)) {
+		err = -efi_errno(efiret);
+		goto out_err;
+	}
+
+	switch (dev->descriptor->bMaxPacketSize0) {
+	case 8:
+		dev->maxpacketsize = PACKET_SIZE_8;
+		break;
+	case 16:
+		dev->maxpacketsize = PACKET_SIZE_16;
+		break;
+	case 32:
+		dev->maxpacketsize = PACKET_SIZE_32;
+		break;
+	case 64:
+		dev->maxpacketsize = PACKET_SIZE_64;
+		break;
+	}
+
+	// There is only the possibility to access the current active configuration
+	// and not set the configuration.
+	// https://uefi.org/specs/UEFI/2.11/17_Protocols_USB_Support.html#efi-usb-io-protocol-usbgetconfigdescriptor
+	efiret = priv->protocol->get_config_descriptor(priv->protocol,
+						       &dev->config.desc);
+	if (EFI_ERROR(efiret)) {
+		err = -efi_errno(efiret);
+		goto out_err;
+	}
+
+	// UEFI has by definition only one interface per config
+	// https://uefi.org/specs/UEFI/2.11/17_Protocols_USB_Support.html#efi-usb-io-protocol-usbgetinterfacedescriptor
+	dev->config.no_of_if = 1;
+	interface = &dev->config.interface[0];
+
+	efiret = priv->protocol->get_interface_descriptor(priv->protocol,
+							  &interface->desc);
+	if (EFI_ERROR(efiret)) {
+		err = -efi_errno(efiret);
+		goto out_err;
+	}
+
+	interface->no_of_ep = interface->desc.bNumEndpoints;
+
+	if (interface->no_of_ep > USB_MAXENDPOINTS) {
+		err = -EPROTO;
+		goto out_err;
+	}
+
+	for (int i = 0; i < interface->no_of_ep; i++) {
+		efiret = priv->protocol->get_endpoint_descriptor(
+			priv->protocol, i, &interface->ep_desc[i]);
+		if (EFI_ERROR(efiret)) {
+			err = -efi_errno(efiret);
+			goto out_err;
+		}
+		usb_set_maxpacket_ep(dev, &interface->ep_desc[i]);
+	}
+
+	efiret = priv->protocol->get_supported_languages(
+		priv->protocol, &lang_ids, &table_size);
+	if (EFI_ERROR(efiret)) {
+		err = -efi_errno(efiret);
+		goto out_err;
+	}
+
+	dev->string_langid = lang_ids[0];
+	/* table size is given in bytes, not entries */
+	dev->have_langid = table_size >= sizeof(*lang_ids);
+
+	dev_info(&dev->dev, "new device: Mfr=%d, Product=%d, SerialNumber=%d\n",
+		 dev->descriptor->iManufacturer, dev->descriptor->iProduct,
+		 dev->descriptor->iSerialNumber);
+
+	if (dev->have_langid) {
+		efi_get_usb_string(priv->protocol, dev->string_langid,
+				   dev->descriptor->iManufacturer, dev->mf,
+				   sizeof(dev->mf));
+		efi_get_usb_string(priv->protocol, dev->string_langid,
+				   dev->descriptor->iProduct, dev->prod,
+				   sizeof(dev->prod));
+		efi_get_usb_string(priv->protocol, dev->string_langid,
+				   dev->descriptor->iSerialNumber, dev->serial,
+				   sizeof(dev->serial));
+	}
+
+	dev_info(&dev->dev, "Bus %03d Device %03d: ID %04x:%04x %s\n",
+		 dev->host->busnum, dev->devnum, dev->descriptor->idVendor,
+		 dev->descriptor->idProduct, dev->prod);
+
+	err = register_device(&dev->dev);
+	if (err) {
+		dev_err(&dev->dev, "Failed to register device: %pe\n",
+			ERR_PTR(err));
+		goto out_err;
+	}
+
+	// register as root device for host
+	host->root_dev = dev;
+
+	return 0;
+
+out_err:
+	dev_err(&dev->dev, "Failed to create UEFI-USB-IO device: %pe\n",
+		ERR_PTR(err));
+	usb_free_device(dev);
+	return err;
+}
+
+static int efi_usb_io_probe(struct efi_device *efidev)
+{
+	struct device *dev = &efidev->dev;
+	struct efi_usb_io_priv *priv;
+	struct usb_host *host;
+
+	priv = xzalloc(sizeof(*priv));
+
+	BS->handle_protocol(efidev->handle, &efi_usb_io_protocol_guid,
+			    (void **)&priv->protocol);
+	if (!priv->protocol)
+		return -ENODEV;
+
+	dev->priv = priv;
+	priv->dev = dev;
+
+	host->hw_dev = dev;
+
+	// EFI has one device per probe, which now needs its own host controller,
+	// since there is no shared host controller resource.
+
+	host = &priv->host;
+	host->submit_int_msg = efi_usb_int_msg;
+	host->submit_control_msg = efi_usb_control_msg;
+	host->submit_bulk_msg = efi_usb_bulk_msg;
+	usb_register_host(host);
+
+	return create_usb_device(priv);
+}
+
+static struct efi_driver efi_usb_io_driver = {
+	.driver = {
+		.name  = "efi-usb-io-protocol",
+	},
+	.probe = efi_usb_io_probe,
+	.guid = EFI_USB_IO_PROTOCOL_GUID,
+};
+device_efi_driver(efi_usb_io_driver);
diff --git a/efi/guid.c b/efi/guid.c
index 8853829d21..f438783fc1 100644
--- a/efi/guid.c
+++ b/efi/guid.c
@@ -15,6 +15,7 @@ efi_guid_t efi_null_guid = EFI_NULL_GUID;
 efi_guid_t efi_global_variable_guid = EFI_GLOBAL_VARIABLE_GUID;
 const efi_guid_t efi_guid_image_security_database = EFI_IMAGE_SECURITY_DATABASE_GUID;
 efi_guid_t efi_block_io_protocol_guid = EFI_BLOCK_IO_PROTOCOL_GUID;
+efi_guid_t efi_usb_io_protocol_guid = EFI_USB_IO_PROTOCOL_GUID;
 efi_guid_t efi_rng_protocol_guid = EFI_RNG_PROTOCOL_GUID;
 efi_guid_t efi_barebox_vendor_guid = EFI_BAREBOX_VENDOR_GUID;
 efi_guid_t efi_file_store_vars_guid = EFI_FILE_STORE_VARS_GUID;
diff --git a/include/efi/guid.h b/include/efi/guid.h
index 202300c74a..7d75842b04 100644
--- a/include/efi/guid.h
+++ b/include/efi/guid.h
@@ -27,6 +27,7 @@ extern efi_guid_t efi_null_guid;
 extern efi_guid_t efi_global_variable_guid;
 extern const efi_guid_t efi_guid_image_security_database;
 extern efi_guid_t efi_block_io_protocol_guid;
+extern efi_guid_t efi_usb_io_protocol_guid;
 extern efi_guid_t efi_rng_protocol_guid;
 extern efi_guid_t efi_barebox_vendor_guid;
 extern efi_guid_t efi_file_store_vars_guid;

-- 
2.47.3
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.