This patch introduces the USB Billboard Driver. Its purpose is to display, via debugfs, basic information about connected Billboard devices. USB-C devices that support Alternate Modes (AUMs), such as DisplayPort and HDMI, can expose a simple USB 2 billboard device that describes the Alternate Modes the USB-C device supports. This enables users to see which Alternate Modes are supported by the USB-C device, even if the host system doesn't support them. All USB-C hosts support USB 2 devices. The AUM information is communicated through a 'Billboard Capability Descriptor' and one or more 'Billboard AUM Capability Descriptors'. The values described in the aforementioned descriptors are exposed by this driver via debugfs The driver will create a "billboards" directory within '/sys/kernel/debug/usb'. Each connected billboard device will have a corresponding file added to this "billboards" directory. Example: $ cat /sys/kernel/debug/usb/billboards/1-1:1.0 Billboard: iAddtionalInfoURL USB-C ADAPTOR bNumberOfAlternateOrUSB4Modes 1 bPreferredAlternateOrUSB4Modes 0 VCONNPower 1W bvdVersion v1.21 bAdditionalFailureInfo 0 bReserved 0 AUM-00: bwAlternateModesVdo 0x405 bmConfigured AUM configuration not attempted or exited wSVID 0xff01 bAlternateOrUSB4Mode 0x0 iAlternateOrUSB4ModeString Generic Link: https://www.usb.org/document-library/billboard-device-class-spec-revision-122-and-adopters-agreement Signed-off-by: Niklas Neronin <niklas.neronin@xxxxxxxxxxxxxxx> --- drivers/usb/common/Kconfig | 11 ++ drivers/usb/common/Makefile | 1 + drivers/usb/common/billboard.c | 334 +++++++++++++++++++++++++++++++++ 3 files changed, 346 insertions(+) create mode 100644 drivers/usb/common/billboard.c diff --git a/drivers/usb/common/Kconfig b/drivers/usb/common/Kconfig index b856622431a7..68f1154acf81 100644 --- a/drivers/usb/common/Kconfig +++ b/drivers/usb/common/Kconfig @@ -49,3 +49,14 @@ config USB_CONN_GPIO To compile the driver as a module, choose M here: the module will be called usb-conn-gpio.ko + +config USB_BILLBOARD + tristate "USB Billboard Driver" + depends on USB && DEBUG_FS + help + Say "y" to display, via debugfs, basic information about connected + USB Billboard devices. + + USB Billboard Devices communicate the Alternate Modes (AUMs) supported + by a Device Container to a host system. For example, the mode could be + for a DisplayPort, HDMI or any other type of data transfer. \ No newline at end of file diff --git a/drivers/usb/common/Makefile b/drivers/usb/common/Makefile index 8ac4d21ef5c8..19fba22185a4 100644 --- a/drivers/usb/common/Makefile +++ b/drivers/usb/common/Makefile @@ -11,3 +11,4 @@ usb-common-$(CONFIG_USB_LED_TRIG) += led.o obj-$(CONFIG_USB_CONN_GPIO) += usb-conn-gpio.o obj-$(CONFIG_USB_OTG_FSM) += usb-otg-fsm.o obj-$(CONFIG_USB_ULPI_BUS) += ulpi.o +obj-$(CONFIG_USB_BILLBOARD) += billboard.o diff --git a/drivers/usb/common/billboard.c b/drivers/usb/common/billboard.c new file mode 100644 index 000000000000..8be9765c39ba --- /dev/null +++ b/drivers/usb/common/billboard.c @@ -0,0 +1,334 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * USB Billboard Driver + * + * Copyright (C) 2023, Intel Corporation. + * Author: Niklas Neronin <niklas.neronin@xxxxxxxxxxxxxxx> + */ + +#define DRIVER_DESC "USB Billboard Driver" +#define DRIVER_AUTHOR "Niklas Neronin <niklas.neronin@xxxxxxxxxxxxxxx>" + +#include <linux/usb.h> +#include <linux/debugfs.h> + +#define USB_CAP_TYPE_BILLBOARD_AUM 0x0F +#define USB_CAP_TYPE_BILLBOARD 0x0D +#define MAX_NUM_ALT_OR_USB4_MODE 52 +#define USB_STRING_SIZE 256 +#define LPADD (-31) + +#define bb_dbg(billboard, ...) dev_dbg(&(billboard->interface)->dev, __VA_ARGS__) + +MODULE_DESCRIPTION(DRIVER_DESC); +MODULE_AUTHOR(DRIVER_AUTHOR); +MODULE_LICENSE("GPL"); + +static struct dentry *billboard_debug_root; +static struct usb_driver usb_billboard_driver; + +/* Structure to hold all of our device specific stuff */ +struct usb_billboard { + struct usb_device *udev; + struct usb_interface *interface; + struct usb_billboard_cap_descriptor *billboard_cap_desc; + struct usb_billboard_aum_cap_descriptor **aum_cap_descs; + unsigned char num_aums; +}; + +static void billboard_free(struct usb_billboard *billboard) +{ + if (!billboard) + return; + usb_put_dev(billboard->udev); + usb_put_intf(billboard->interface); + kfree(billboard->aum_cap_descs); + kfree(billboard); +} + +static struct usb_device_id billboard_id_table[] = { + { USB_DEVICE_INFO(0x11, 0, 0) }, + { } /* Terminating entry */ +}; +MODULE_DEVICE_TABLE(usb, billboard_id_table); + +/* Struct for Billboard Capability Descriptor */ +struct usb_billboard_cap_descriptor { + __u8 bLength; + __u8 bDescriptorType; + __u8 bDevCapabilityType; + __u8 iAddtionalInfoURL; + __u8 bNumberOfAlternateOrUSB4Modes; + __u8 bPreferredAlternateOrUSB4Modes; + __le16 VCONNPower; + __u8 bmConfigured[32]; + __u8 bvdVersion[2]; + __u8 bAdditionalFailureInfo; + __u8 bReserved; + DECLARE_FLEX_ARRAY(struct { + __le16 wSVID; + __u8 bAlternateOrUSB4Mode; + __u8 iAlternateOrUSB4ModeString; + }, aum) __packed; +} __packed; + +/* Struct for Billboard AUM Capability Descriptor */ +struct usb_billboard_aum_cap_descriptor { + __u8 bLength; + __u8 bDescriptorType; + __u8 bDevCapabilityType; + __u8 bIndex; + __le32 bwAlternateModesVdo; +} __packed; + +static int billboard_show(struct seq_file *s, void *unused) +{ + static const char *const power_table[] = {"1", "1.5", "2", "3", "4", "5", "6"}; + struct usb_billboard *billboard = s->private; + struct usb_billboard_cap_descriptor *billboard_cap = billboard->billboard_cap_desc; + unsigned char bitpair; + char usb_str[USB_STRING_SIZE]; + int vconn; + + seq_puts(s, "Billboard:\n"); + usb_string(billboard->udev, billboard_cap->iAddtionalInfoURL, usb_str, USB_STRING_SIZE); + seq_printf(s, "%*s %s\n", LPADD, "iAddtionalInfoURL", usb_str); + + seq_printf(s, "%*s %d\n", LPADD, "bNumberOfAlternateOrUSB4Modes", + billboard_cap->bNumberOfAlternateOrUSB4Modes); + seq_printf(s, "%*s %d\n", LPADD, "bPreferredAlternateOrUSB4Modes", + billboard_cap->bPreferredAlternateOrUSB4Modes); + + seq_printf(s, "%*s ", LPADD, "VCONNPower"); + vconn = le16_to_cpu(billboard_cap->VCONNPower); + if (vconn & (1 << 15)) + seq_puts(s, "Power not required\n"); + else if (vconn < 7) + seq_printf(s, "%sW\n", power_table[vconn]); + else + seq_puts(s, "Reserved\n"); + + seq_printf(s, "%*s v%x.%x\n", LPADD, "bvdVersion", + billboard_cap->bvdVersion[1], billboard_cap->bvdVersion[0]); + seq_printf(s, "%*s %d\n", LPADD, "bAdditionalFailureInfo", + billboard_cap->bAdditionalFailureInfo); + seq_printf(s, "%*s %d\n", LPADD, "bReserved", billboard_cap->bReserved); + + for (int i = 0; i < billboard->num_aums; i++) { + seq_printf(s, "\nAUM-%02d:\n", billboard->aum_cap_descs[i]->bIndex); + seq_printf(s, "%*s %#x\n", LPADD, "bwAlternateModesVdo", + billboard->aum_cap_descs[i]->bwAlternateModesVdo); + + /* In order, each bit pair in 'bmConfigured' describes a AUM configuration. */ + bitpair = (billboard_cap->bmConfigured[i / 4] >> (i % 4)) & 0x03; + seq_printf(s, "%*s ", LPADD, "bmConfigured"); + if (bitpair == 1) + seq_puts(s, "AUM configuration not attempted or exited\n"); + else if (bitpair == 2) + seq_puts(s, "AUM configuration attempted but unsuccessful and not entered\n"); + else if (bitpair == 3) + seq_puts(s, "AUM configuration successful\n"); + else + seq_puts(s, "Unspecified Error\n"); + + seq_printf(s, "%*s %#x\n", LPADD, "wSVID", + billboard_cap->aum[i].wSVID); + seq_printf(s, "%*s %#x\n", LPADD, "bAlternateOrUSB4Mode", + billboard_cap->aum[i].bAlternateOrUSB4Mode); + + usb_string(billboard->udev, billboard_cap->aum[i].iAlternateOrUSB4ModeString, + usb_str, USB_STRING_SIZE); + seq_printf(s, "%*s %s\n", LPADD, "iAlternateOrUSB4ModeString", usb_str); + } + return 0; +} +DEFINE_SHOW_ATTRIBUTE(billboard); + +static int get_billboard_capability_descriptor(struct usb_billboard *billboard, + struct usb_bos_descriptor *bos) +{ + struct usb_billboard_cap_descriptor *desc; + unsigned char *ptr = (unsigned char *)bos + bos->bLength; + + for (int i = 0; i < bos->bNumDeviceCaps; i++) { + desc = (struct usb_billboard_cap_descriptor *)ptr; + + if (desc->bDescriptorType != USB_DT_DEVICE_CAPABILITY) + goto skip_to_next_descriptor; + + if (desc->bDevCapabilityType != USB_CAP_TYPE_BILLBOARD) + goto skip_to_next_descriptor; + + if (desc->bLength < 48) { + bb_dbg(billboard, "incorrect billboard capability descriptor length"); + goto descriptor_error; + } + + if (!desc->bNumberOfAlternateOrUSB4Modes || + desc->bNumberOfAlternateOrUSB4Modes > MAX_NUM_ALT_OR_USB4_MODE) { + bb_dbg(billboard, "incorrect amount of AUMs"); + goto descriptor_error; + } + + if (desc->bLength != desc->bNumberOfAlternateOrUSB4Modes * 4 + 44) { + bb_dbg(billboard, "incorrect length of billboard AUM descriptors"); + goto descriptor_error; + } + + billboard->billboard_cap_desc = desc; + return 0; + +skip_to_next_descriptor: + ptr += desc->bLength; + } + +descriptor_error: + return -1; +} + +static int get_billboard_aum_capability_descriptors(struct usb_billboard *billboard, + struct usb_bos_descriptor *bos) +{ + struct usb_billboard_aum_cap_descriptor *desc; + unsigned char *ptr = (unsigned char *)bos + bos->bLength; + unsigned char total = 0; + + for (int i = 0; i < bos->bNumDeviceCaps; i++) { + desc = (struct usb_billboard_aum_cap_descriptor *)ptr; + + if (desc->bDescriptorType != USB_DT_DEVICE_CAPABILITY) + goto skip_to_next_descriptor; + + if (desc->bDevCapabilityType != USB_CAP_TYPE_BILLBOARD_AUM) + goto skip_to_next_descriptor; + + if (desc->bLength != 8) { + bb_dbg(billboard, "incorrect length of AUM capability descriptor"); + goto descriptor_error; + } + + if (desc->bIndex >= billboard->num_aums) { + bb_dbg(billboard, "incorrect index of AUM capability descriptor"); + goto descriptor_error; + } + + billboard->aum_cap_descs[desc->bIndex] = desc; + if (++total == billboard->num_aums) + return 0; + +skip_to_next_descriptor: + ptr += desc->bLength; + } + +descriptor_error: + return -1; +} + +static int probe(struct usb_interface *interface, const struct usb_device_id *id) +{ + struct usb_billboard *billboard; + struct usb_device *udev; + int ret = -ENODEV; + + udev = interface_to_usbdev(interface); + if (le16_to_cpu(udev->descriptor.bcdUSB) < 0x201 || !udev->bos) + return ret; + + billboard = kzalloc(sizeof(*billboard), GFP_KERNEL); + if (!billboard) { + ret = -ENOMEM; + goto err; + } + + billboard->udev = usb_get_dev(udev); + billboard->interface = usb_get_intf(interface); + + if (get_billboard_capability_descriptor(billboard, udev->bos->desc)) + goto err; + + billboard->num_aums = billboard->billboard_cap_desc->bNumberOfAlternateOrUSB4Modes; + + billboard->aum_cap_descs = kcalloc(billboard->num_aums, sizeof(*billboard->aum_cap_descs), + GFP_KERNEL); + if (!billboard->aum_cap_descs) { + ret = -ENOMEM; + goto err; + } + + if (get_billboard_aum_capability_descriptors(billboard, udev->bos->desc)) + goto err; + + usb_set_intfdata(interface, billboard); + + debugfs_create_file(dev_name(&interface->dev), 0444, billboard_debug_root, billboard, + &billboard_fops); + + dev_info(&interface->dev, "device successfully attached\n"); + return 0; + +err: + billboard_free(billboard); + return ret; +} + +static void disconnect(struct usb_interface *interface) +{ + debugfs_lookup_and_remove(dev_name(&(interface)->dev), billboard_debug_root); + billboard_free(usb_get_intfdata(interface)); +} + +static int suspend(struct usb_interface *interface, pm_message_t message) { return 0; } +static int resume(struct usb_interface *interface) { return 0; } + +static int reset_resume(struct usb_interface *interface) +{ + struct usb_billboard *billboard = usb_get_intfdata(interface); + struct usb_device *udev = interface_to_usbdev(interface); + + if (!udev->bos) + return -ENODEV; + + if (get_billboard_capability_descriptor(billboard, udev->bos->desc)) + return -ENODEV; + + if (billboard->num_aums != billboard->billboard_cap_desc->bNumberOfAlternateOrUSB4Modes) { + bb_dbg(billboard, "amount of AUMs changed"); + return -ENODEV; + } + + if (get_billboard_aum_capability_descriptors(billboard, udev->bos->desc)) + return -ENODEV; + + return 0; +} + +static struct usb_driver usb_billboard_driver = { + .name = "billboard", + .probe = probe, + .disconnect = disconnect, + .suspend = pm_ptr(suspend), + .resume = pm_ptr(resume), + .reset_resume = pm_ptr(reset_resume), + .id_table = billboard_id_table, + .supports_autosuspend = 1, +}; + +static int __init billboard_init(void) +{ + int ret; + + billboard_debug_root = debugfs_create_dir("billboards", usb_debug_root); + ret = usb_register(&usb_billboard_driver); + if (ret < 0) + debugfs_remove(billboard_debug_root); + + return ret; +} +module_init(billboard_init); + +static void __exit billboard_exit(void) +{ + usb_deregister(&usb_billboard_driver); + debugfs_remove(billboard_debug_root); +} +module_exit(billboard_exit); -- 2.43.0