From 1de38c425ad17eb0993c9eab6d50bb493ca2d8f9 Mon Sep 17 00:00:00 2001 From: Abel Vesa Date: Mon, 12 Oct 2026 12:36:59 +0000 Subject: [PATCH] drivers: add UnoQ CBUS control backend Add a dedicated UnoQ control backend for power and EDL control through FTDI CBUS pins. The existing FTDI GPIO backend uses normal bitbang GPIO and opens the FTDI interface through libftdi. That is not suitable for UnoQ because the CBUS controls share the FTDI device with the serial console; claiming or resetting the interface would disturb the console handled by the kernel FTDI serial driver. Use endpoint-zero SIO_SET_BITMODE vendor requests instead. This allows the CBUS bitmode/output state to be changed without detaching the serial driver. Document that the request uses one-based FTDI interface numbering, with interface A addressed as wIndex 1. Assisted-by: LLM Signed-off-by: Abel Vesa --- README | 9 ++ config-samples/sample14.yaml | 10 ++ device.h | 1 + device_parser.c | 5 + drivers/unoq.c | 243 +++++++++++++++++++++++++++++++++++ meson.build | 2 + schema.yaml | 33 +++++ 7 files changed, 303 insertions(+) create mode 100644 config-samples/sample14.yaml create mode 100644 drivers/unoq.c diff --git a/README b/README index e651165..0b91d93 100644 --- a/README +++ b/README @@ -184,6 +184,15 @@ target. fastboot_set_active: true fastboot_key_timeout: 2 + - board: unoq + console: /dev/serial/by-id/usb-FTDI_ALPACA-LITE_MTP_DEBUG_BOARD_FTAMC2D5-if00-port0 + name: Arduino UnoQ + unoq: + vendor: "0x0403" + product: "0x6015" + serial: DK0HENU2 + fastboot: 1ff52e59 + = Status messages The status messages that are used by the client fifo and the server's status diff --git a/config-samples/sample14.yaml b/config-samples/sample14.yaml new file mode 100644 index 0000000..8ed1077 --- /dev/null +++ b/config-samples/sample14.yaml @@ -0,0 +1,10 @@ +--- +devices: + - board: unoq + name: Arduino UnoQ + console: /dev/serial/by-id/usb-FTDI_ALPACA-LITE_MTP_DEBUG_BOARD_FTAMC2D5-if00-port0 + unoq: + vendor: "0x0403" + product: "0x6015" + serial: DK0HENU2 + fastboot: 1ff52e59 diff --git a/device.h b/device.h index 664f942..4ed947a 100644 --- a/device.h +++ b/device.h @@ -131,6 +131,7 @@ extern const struct control_ops external_ops; extern const struct control_ops qcomlt_dbg_ops; extern const struct control_ops laurent_ops; extern const struct control_ops pic32cx_ops; +extern const struct control_ops unoq_ops; extern const struct console_ops conmux_console_ops; extern const struct console_ops console_ops; diff --git a/device_parser.c b/device_parser.c index 62e5569..42e43cb 100644 --- a/device_parser.c +++ b/device_parser.c @@ -226,6 +226,11 @@ static void parse_board(struct device_parser *dp) if (dev->control_options) set_control_ops(dev, &laurent_ops); continue; + } else if (!strcmp(key, "unoq")) { + dev->control_options = unoq_ops.parse_options(dp); + if (dev->control_options) + set_control_ops(dev, &unoq_ops); + continue; } device_parser_expect(dp, YAML_SCALAR_EVENT, value, TOKEN_LENGTH); diff --git a/drivers/unoq.c b/drivers/unoq.c new file mode 100644 index 0000000..2be889c --- /dev/null +++ b/drivers/unoq.c @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2026 Qualcomm Technologies, Inc. and/or its subsidiaries + * All rights reserved. + * + * SPDX-License-Identifier: BSD-3-Clause + */ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "device.h" +#include "device_parser.h" + +#define TOKEN_LENGTH 16384 +#define PRE_RESET_DELAY_US 500000 +#define POWER_CYCLE_DELAY_US 2000000 + +struct unoq_options { + char *description; + char *vendor; + char *product; + char *serial; + unsigned int index; + char *devicenode; +}; + +struct unoq { + struct unoq_options *options; + struct ftdi_context *ftdi; + bool edl_asserted; + bool direct_usb; +}; + +static void unoq_set_bitmode(struct unoq *unoq, unsigned char bitmask) +{ + int ret; + + /* + * SIO_SET_BITMODE uses one-based FTDI interface numbers, so + * interface A is addressed with wIndex 1. Issue the request via + * endpoint zero instead of opening the interface with libftdi, as the + * UnoQ CBUS controls share the FTDI device with the serial console. + */ + ret = libusb_control_transfer(unoq->ftdi->usb_dev, + LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE, + 0x0b, bitmask | (BITMODE_CBUS << 8), 1, NULL, 0, 5000); + if (ret < 0) + errx(1, "failed to set UnoQ CBUS bitmode 0x%02x (%d)", + bitmask, ret); +} + +static void unoq_power_on(struct unoq *unoq) +{ + /* CBUS2 drives the open-drain VBUS_POWER_SWITCH_DISABLE_N line. + * Keep all CBUS pins as outputs; LOW releases each target signal. + */ + unoq_set_bitmode(unoq, 0xf4); + usleep(POWER_CYCLE_DELAY_US); + unoq_set_bitmode(unoq, 0xf0); +} + +static void unoq_boot_edl(struct unoq *unoq) +{ + unoq_set_bitmode(unoq, 0b01110100); + usleep(PRE_RESET_DELAY_US); + unoq_set_bitmode(unoq, 0b01110001); +} + +static void unoq_power_off(struct unoq *unoq) +{ + unoq_set_bitmode(unoq, 0xf4); +} + +void *unoq_parse_options(struct device_parser *dp) +{ + struct unoq_options *options; + char value[TOKEN_LENGTH]; + char key[TOKEN_LENGTH]; + + options = calloc(1, sizeof(*options)); + + if (device_parser_accept(dp, YAML_SCALAR_EVENT, value, TOKEN_LENGTH)) { + options->description = strdup(value); + return options; + } + + device_parser_accept(dp, YAML_MAPPING_START_EVENT, NULL, 0); + + while (device_parser_accept(dp, YAML_SCALAR_EVENT, key, TOKEN_LENGTH)) { + if (!device_parser_accept(dp, YAML_SCALAR_EVENT, value, TOKEN_LENGTH)) + errx(1, "%s: expected value for \"%s\"", __func__, key); + + if (!strcmp(key, "vendor")) + options->vendor = strdup(value); + else if (!strcmp(key, "product")) + options->product = strdup(value); + else if (!strcmp(key, "index")) + options->index = strtoul(value, NULL, 0); + else if (!strcmp(key, "serial")) + options->serial = strdup(value); + else if (!strcmp(key, "devicenode")) + options->devicenode = strdup(value); + else + errx(1, "%s: unknown option \"%s\"", __func__, key); + } + + device_parser_expect(dp, YAML_MAPPING_END_EVENT, NULL, 0); + + return options; +} + +static void *unoq_open(struct device *dev) +{ + struct unoq *unoq; + int ret; + + unoq = calloc(1, sizeof(*unoq)); + unoq->options = dev->control_options; + + if (!unoq->options->description) { + if (unoq->options->devicenode) + asprintf(&unoq->options->description, "d:%s", + unoq->options->devicenode); + else if (unoq->options->vendor && + unoq->options->product && + unoq->options->serial) + asprintf(&unoq->options->description, "s:%s:%s:%s", + unoq->options->vendor, + unoq->options->product, + unoq->options->serial); + else if (unoq->options->vendor && + unoq->options->product && + unoq->options->index > 0) + asprintf(&unoq->options->description, "i:%s:%s:%u", + unoq->options->vendor, + unoq->options->product, + unoq->options->index); + else if (unoq->options->vendor && unoq->options->product) + asprintf(&unoq->options->description, "i:%s:%s", + unoq->options->vendor, + unoq->options->product); + else + errx(1, "Incomplete UnoQ FTDI description properties"); + } + + unoq->ftdi = ftdi_new(); + if (!unoq->ftdi) + errx(1, "failed to allocate UnoQ FTDI context"); + + /* Serial-selected Bughoppers only need endpoint zero. Avoid the UART + * reset, baud-rate setup and interface claim performed by libftdi open. + */ + if (unoq->options->vendor && unoq->options->product && unoq->options->serial) { + struct ftdi_device_list *devices = NULL; + struct ftdi_device_list *entry; + char serial[256]; + + ret = ftdi_usb_find_all(unoq->ftdi, &devices, + strtoul(unoq->options->vendor, NULL, 0), + strtoul(unoq->options->product, NULL, 0)); + if (ret < 0) + errx(1, "failed to enumerate UnoQ USB devices (%d)", ret); + ret = LIBUSB_ERROR_NO_DEVICE; + for (entry = devices; entry; entry = entry->next) { + if (ftdi_usb_get_strings(unoq->ftdi, entry->dev, NULL, 0, + NULL, 0, serial, sizeof(serial)) < 0) + continue; + if (strcmp(serial, unoq->options->serial)) + continue; + ret = libusb_open(entry->dev, &unoq->ftdi->usb_dev); + break; + } + ftdi_list_free(&devices); + unoq->direct_usb = true; + } else { + ret = ftdi_usb_open_string(unoq->ftdi, unoq->options->description); + } + if (ret < 0) + errx(1, "failed to open UnoQ FTDI device '%s' (%d)", + unoq->options->description, ret); + + return unoq; +} + +static void unoq_close(struct device *dev) +{ + struct unoq *unoq = dev->cdb; + + if (!unoq) + return; + + if (unoq->ftdi) { + if (unoq->direct_usb) { + libusb_close(unoq->ftdi->usb_dev); + unoq->ftdi->usb_dev = NULL; + } else { + ftdi_usb_close(unoq->ftdi); + } + ftdi_free(unoq->ftdi); + } + + free(unoq); +} + +static int unoq_power(struct device *dev, bool on) +{ + struct unoq *unoq = dev->cdb; + + if (on) { + if (unoq->edl_asserted) + unoq_boot_edl(unoq); + else + unoq_power_on(unoq); + } else { + unoq_power_off(unoq); + } + + return 0; +} + +static void unoq_key(struct device *dev, int key, bool asserted) +{ + struct unoq *unoq = dev->cdb; + + if (key == DEVICE_KEY_EDL) + unoq->edl_asserted = asserted; +} + +const struct control_ops unoq_ops = { + .parse_options = unoq_parse_options, + .open = unoq_open, + .close = unoq_close, + .power = unoq_power, + .key = unoq_key, +}; diff --git a/meson.build b/meson.build index 06e532f..2e5314a 100644 --- a/meson.build +++ b/meson.build @@ -55,6 +55,7 @@ endif gpiod_dep = dependency('libgpiod', required: server_opt) cdbalib_deps = [dependency('libudev', required: server_opt), + dependency('libusb-1.0', required: server_opt), dependency('yaml-0.1', required: server_opt), gpiod_dep, ftdi_dep] @@ -76,6 +77,7 @@ drivers_srcs = ['drivers/alpaca.c', 'drivers/local-gpio.c', 'drivers/pic32cx.c', 'drivers/qcomlt_dbg.c', + 'drivers/unoq.c', ] if gpiod_dep.version().version_compare('>=2.0') diff --git a/schema.yaml b/schema.yaml index 6386b37..7a2b6a9 100644 --- a/schema.yaml +++ b/schema.yaml @@ -131,6 +131,39 @@ properties: description: PIC32CX board control device path $ref: "#/$defs/device_path" + unoq: + description: UnoQ board control over FTDI CBUS + oneOf: + - type: string + description: libftdi device description string + pattern: "^(d:/.*)|(i:0x[0-9a-fA-F]{4}:0x[0-9a-fA-F]{4}(:[0-9]+)?)|(s:0x[0-9a-fA-F]{4}:0x[0-9a-fA-F]{4}:[0-9a-zA-Z]+)$" + + - type: object + properties: + vendor: + type: string + pattern: "^0x[0-9a-fA-F]{4}$" + product: + type: string + pattern: "^0x[0-9a-fA-F]{4}$" + serial: + type: string + pattern: "^[0-9a-zA-Z]+$" + index: + type: integer + minimum: 0 + devicenode: + $ref: "#/$defs/device_path" + additionalProperties: false + + dependentRequired: + index: + - product + - vendor + serial: + - product + - vendor + users: description: User access allowance for the board type: array