From ee8ef31fe9769d663c6d041813d58796ffd40db8 Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Tue, 25 Aug 2026 20:30:00 +0100 Subject: [PATCH 1/3] firehose: allow selecting the reset mode The firehose power command accepts more than a plain reset: the device can also be rebooted straight back into EDL or powered off. Only "reset" was ever emitted in qdl, so callers that want the device to come back up in EDL, which is useful when flashing repeatedly to avoid a manual power cycle, had no way to ask for it. Add enum qdl_reset_mode and make firehose_reset() take it as an argument, mapping the mode onto the value attribute. All existing callers pass QDL_RESET_NORMAL, preserving current behaviour. Changing the signature is safe for both consumers: qdl links the common sources through the qdl_common static library and the nbdkit plugin recompiles those same sources into its shared module. Neither exposes the function across a stable ABI. Signed-off-by: Christopher Obbard --- include/qdl.h | 9 ++++++++- src/firehose.c | 25 ++++++++++++++++++++----- src/nbdkit-qdl-plugin.c | 2 +- src/qdl.c | 2 +- 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/include/qdl.h b/include/qdl.h index 3fb24380..67e3daa7 100644 --- a/include/qdl.h +++ b/include/qdl.h @@ -72,6 +72,13 @@ enum qdl_skipblock_mode { QDL_SKIPBLOCK_SHA256, }; +enum qdl_reset_mode { + QDL_RESET_NORMAL, + QDL_RESET_TO_EDL, + /* Just powers the device off. */ + QDL_RESET_POWER_OFF, +}; + struct qdl_device { enum QDL_DEVICE_TYPE dev_type; int fd; @@ -191,7 +198,7 @@ int firehose_read_buf(struct qdl_device *qdl, struct firehose_op *read_op, void /* Block-level entry points used by the nbdkit plugin */ int firehose_open(struct qdl_device *qdl, enum qdl_storage_type storage); -int firehose_reset(struct qdl_device *qdl); +int firehose_reset(struct qdl_device *qdl, enum qdl_reset_mode mode); int firehose_getsize(struct qdl_device *qdl, int lun, size_t *sector_size, size_t *num_sectors); ssize_t firehose_pread(struct qdl_device *qdl, int lun, size_t sector_offset, diff --git a/src/firehose.c b/src/firehose.c index 77ab3777..46e789b0 100644 --- a/src/firehose.c +++ b/src/firehose.c @@ -1551,8 +1551,23 @@ static int firehose_set_bootable(struct qdl_device *qdl, int part) return 0; } -int firehose_reset(struct qdl_device *qdl) +static const char *firehose_reset_mode_str(enum qdl_reset_mode mode) { + switch (mode) { + case QDL_RESET_NORMAL: + return "reset"; + case QDL_RESET_TO_EDL: + return "reset_to_edl"; + case QDL_RESET_POWER_OFF: + return "off"; + } + + return "reset"; +} + +int firehose_reset(struct qdl_device *qdl, enum qdl_reset_mode mode) +{ + const char *mode_name = firehose_reset_mode_str(mode); xmlNode *root; xmlNode *node; xmlDoc *doc; @@ -1563,7 +1578,7 @@ int firehose_reset(struct qdl_device *qdl) xmlDocSetRootElement(doc, root); node = xmlNewChild(root, NULL, (xmlChar *)"power", NULL); - xml_setpropf(node, "value", "reset"); + xml_setpropf(node, "value", mode_name); xml_setpropf(node, "DelayInSeconds", "10"); // Add a delay to prevent reboot fail ret = firehose_write(qdl, doc); @@ -1573,7 +1588,7 @@ int firehose_reset(struct qdl_device *qdl) ret = firehose_read(qdl, 5000, firehose_generic_parser, NULL); if (ret < 0) - ux_err("failed to request device reset\n"); + ux_err("failed to request device reset (mode=%s)\n", mode_name); /* drain any remaining log messages for reset */ else firehose_read(qdl, 1000, firehose_generic_parser, NULL); @@ -1679,7 +1694,7 @@ int firehose_provision(struct qdl_device *qdl, struct ufs_provisioning *ufs, boo ux_info("UFS provisioning failed\n"); if (!skip_reset) - firehose_reset(qdl); + firehose_reset(qdl, QDL_RESET_NORMAL); return ret; @@ -1785,7 +1800,7 @@ static int firehose_execute_ops(struct qdl_device *qdl, struct list_head *ops) firehose_set_bootable(qdl, op->partition); break; case FIREHOSE_OP_RESET: - ret = firehose_reset(qdl); + ret = firehose_reset(qdl, QDL_RESET_NORMAL); if (ret < 0) return ret; break; diff --git a/src/nbdkit-qdl-plugin.c b/src/nbdkit-qdl-plugin.c index 533f436a..805c9107 100644 --- a/src/nbdkit-qdl-plugin.c +++ b/src/nbdkit-qdl-plugin.c @@ -128,7 +128,7 @@ static void qdl_plugin_unload(void) if (!dev) return; - firehose_reset(dev); + firehose_reset(dev, QDL_RESET_NORMAL); qdl_close(dev); qdl_deinit(dev); dev = NULL; diff --git a/src/qdl.c b/src/qdl.c index 18d72876..d45d4cb4 100644 --- a/src/qdl.c +++ b/src/qdl.c @@ -755,7 +755,7 @@ static int qdl_reset_run(struct qdl_device *qdl) ret = sahara_device_reset(qdl); if (ret == 1) { ux_info("falling back to Firehose reset\n"); - ret = firehose_reset(qdl); + ret = firehose_reset(qdl, QDL_RESET_NORMAL); } return ret; From 8f52721a78e0127910f5cf4ea2500c1adff12f0a Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Tue, 25 Aug 2026 20:30:00 +0100 Subject: [PATCH 2/3] qdl: add a firehose-reset subcommand The reset modes the previous patch made available are not reachable from the command line: the "reset" subcommand goes through Sahara and only falls back to a plain Firehose reset; the "reset" flashing verb only runs at the end of a flash. Neither can leave the device in EDL or powered off. Add "qdl firehose-reset [] ". Since the command needs a programmer running on the target, the programmer is taken as an optional leading argument and uploaded through sahara_run() exactly as a flashing run does. sahara_run() skips the upload on its own when the target already answers in firehose, so passing the programmer is harmless and can be left out entirely when one is known to be up. Keeping this separate from "reset" also makes each mode testable on its own, without reflashing the board first, so a mode that a given programmer rejects can be identified directly. Tested on RB3gen2 with a UFS target, uploading prog_firehose_ddr.elf from a freshly booted EDL device: Sahara transfers the programmer, the programmer logs "bsp_target_reset_edl()" in response to , acknowledges it and the device re-enumerates in EDL. Argument handling was checked separately - an unknown power mode is rejected, an optional programmer binary is handled correctly too. Signed-off-by: Christopher Obbard --- src/qdl.c | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/src/qdl.c b/src/qdl.c index d45d4cb4..dfccfb51 100644 --- a/src/qdl.c +++ b/src/qdl.c @@ -475,6 +475,7 @@ static void print_usage(FILE *out) fprintf(out, " %s list\n", __progname); fprintf(out, " %s chipinfo\n", __progname); fprintf(out, " %s reset\n", __progname); + fprintf(out, " %s firehose-reset [] \n", __progname); fprintf(out, " %s ramdump [--debug] [-o ] [,...]\n", __progname); fprintf(out, " %s ks [-p | --serial=T] -s ...\n", __progname); fprintf(out, " %s flash ([::specifier] | [::])\n", __progname); @@ -507,6 +508,8 @@ static void print_usage(FILE *out) fprintf(out, " \t\tnumber S, the number of sectors to follow L, or partition by \"name\"\n"); fprintf(out, " \t\tpath where ramdump should stored\n"); fprintf(out, " \toptional glob-pattern to select which segments to ramdump\n"); + fprintf(out, " \t\tstate to leave the device in: (firehose-reset);\n"); + fprintf(out, " \t\t may be omitted when a programmer is already running\n"); fprintf(out, " \tSahara device node, e.g. /dev/mhi0_QAIC_SAHARA (ks);\n"); fprintf(out, " \tomit to use the selected device backend (ks)\n"); fprintf(out, " \t\tmap a Sahara image id to a host file, repeatable (ks)\n"); @@ -761,6 +764,92 @@ static int qdl_reset_run(struct qdl_device *qdl) return ret; } +static int decode_reset_mode(const char *name, enum qdl_reset_mode *out) +{ + if (!strcmp(name, "system")) { + *out = QDL_RESET_NORMAL; + return 0; + } + + if (!strcmp(name, "edl")) { + *out = QDL_RESET_TO_EDL; + return 0; + } + + if (!strcmp(name, "off")) { + *out = QDL_RESET_POWER_OFF; + return 0; + } + + return -1; +} + +/* + * Firehose reset ("firehose-reset") subcommand. + * + * Sends a single Firehose command, which the "reset" subcommand + * cannot express: that one resets the device from EDL over Sahara and only + * falls back to Firehose to perform a plain reset. Rebooting straight back + * into EDL, or powering the device off, is only available through Firehose. + * + * The command therefore needs a programmer running on the target. A device + * sitting in EDL has none, so the programmer is taken as an optional leading + * argument and uploaded exactly like a flashing run does; sahara_run() skips + * the upload by itself when the target answers in Firehose, so passing it is + * harmless when a previous --skip-reset run left one behind, and it can be + * omitted entirely when the programmer is known to be up. + */ +static int qdl_firehose_reset(int argc, char **argv) +{ + struct sahara_image sahara_images[MAPPING_SZ] = {}; + enum QDL_DEVICE_TYPE qdl_dev_type = QDL_DEVICE_AUTO; + enum qdl_reset_mode mode; + struct qdl_device *qdl; + char *programmer = NULL; + char *serial = NULL; + int ret; + int opt; + + while ((opt = getopt_long(argc, argv, "dvS:h", qdl_common_options, NULL)) != -1) { + ret = qdl_common_opt(opt, &serial, &qdl_dev_type); + if (ret == QDL_OPT_EXIT_OK) + return 0; + if (ret == QDL_OPT_EXIT_FAIL) + return 1; + } + + if (argc - optind == 2) + programmer = argv[optind++]; + + if (optind + 1 != argc) { + print_usage(stderr); + return 1; + } + + if (decode_reset_mode(argv[optind], &mode) < 0) + errx(1, "unknown reset mode \"%s\" (expected edl|off|system)", argv[optind]); + + ux_init(); + + if (programmer && decode_programmer(programmer, sahara_images) < 0) + return 1; + + qdl = qdl_session_open(qdl_dev_type, serial); + if (!qdl) { + sahara_images_free(sahara_images, MAPPING_SZ); + return 1; + } + + ret = sahara_run(qdl, sahara_images, NULL, NULL) < 0 ? 1 : 0; + if (!ret) + ret = firehose_reset(qdl, mode) < 0 ? 1 : 0; + + qdl_session_close(qdl); + sahara_images_free(sahara_images, MAPPING_SZ); + + return ret; +} + /* * Sahara kickstart ("ks") subcommand. * @@ -1570,6 +1659,8 @@ int main(int argc, char **argv) return qdl_sahara_cmd(argc - i, argv + i, sahara_chipinfo); if (!strcmp(argv[i], "reset")) return qdl_sahara_cmd(argc - i, argv + i, qdl_reset_run); + if (!strcmp(argv[i], "firehose-reset")) + return qdl_firehose_reset(argc - i, argv + i); if (!strcmp(argv[i], "ks")) return qdl_ks(argc - i, argv + i); if (!strcmp(argv[i], "create-zip")) From c59bdb6b94e53a68dfcfaba0a3302541826406e6 Mon Sep 17 00:00:00 2001 From: Christopher Obbard Date: Tue, 25 Aug 2026 20:30:00 +0100 Subject: [PATCH 3/3] firehose: drain the programmer banner before resetting "firehose-reset" sends as the only command of the session, immediately after Sahara hands control to a programmer that has just started. The programmer is still printing its startup banner at that point and is not reading its input endpoint, so the write times out after a second; firehose_write() then drains the backlog itself and retries; the retry is what the programmer answers. The result is a wasted second and a duplicate document on the wire on every invocation, reproducible on an emmc target: FIREHOSE WRITE: LOG: INFO: Binary build date: ... [ ~20 more banner lines ] FIREHOSE WRITE: LOG: INFO: Calling handler for power The flashing path never hits this because the configure exchange in firehose_detect_and_configure() consumes the banner first. Export firehose_drain() and call it from the subcommand when it uploaded the programmer itself so the first write is the one that lands. It is skipped when no programmer was passed, where the session is already past its startup output. Tested on RB3gen2: the banner is consumed before the write, the single that follows is acknowledged and the device comes back in EDL. Signed-off-by: Christopher Obbard --- include/qdl.h | 1 + src/firehose.c | 15 +++++++++++++++ src/qdl.c | 10 +++++++++- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/include/qdl.h b/include/qdl.h index 67e3daa7..3c20798c 100644 --- a/include/qdl.h +++ b/include/qdl.h @@ -199,6 +199,7 @@ int firehose_read_buf(struct qdl_device *qdl, struct firehose_op *read_op, void /* Block-level entry points used by the nbdkit plugin */ int firehose_open(struct qdl_device *qdl, enum qdl_storage_type storage); int firehose_reset(struct qdl_device *qdl, enum qdl_reset_mode mode); +void firehose_drain(struct qdl_device *qdl, unsigned int timeout_ms); int firehose_getsize(struct qdl_device *qdl, int lun, size_t *sector_size, size_t *num_sectors); ssize_t firehose_pread(struct qdl_device *qdl, int lun, size_t sector_offset, diff --git a/src/firehose.c b/src/firehose.c index 46e789b0..77d89bd6 100644 --- a/src/firehose.c +++ b/src/firehose.c @@ -1551,6 +1551,21 @@ static int firehose_set_bootable(struct qdl_device *qdl, int part) return 0; } +/* + * Consume whatever the programmer has queued up, for callers that send a + * command as the first thing after the programmer starts. A programmer that + * is still printing its startup banner does not read its input endpoint, so + * the first write times out and is only retried a second later, once + * firehose_write() has drained the backlog itself. + * + * firehose_detect_and_configure() gets this for free from the configure + * exchange; callers that send nothing else need to drain explicitly. + */ +void firehose_drain(struct qdl_device *qdl, unsigned int timeout_ms) +{ + firehose_read(qdl, timeout_ms, firehose_generic_parser, NULL); +} + static const char *firehose_reset_mode_str(enum qdl_reset_mode mode) { switch (mode) { diff --git a/src/qdl.c b/src/qdl.c index dfccfb51..4aba2798 100644 --- a/src/qdl.c +++ b/src/qdl.c @@ -841,8 +841,16 @@ static int qdl_firehose_reset(int argc, char **argv) } ret = sahara_run(qdl, sahara_images, NULL, NULL) < 0 ? 1 : 0; - if (!ret) + if (!ret) { + /* + * is the only command sent here, so nothing else + * consumes the banner of a programmer that just started. + */ + if (programmer) + firehose_drain(qdl, 1000); + ret = firehose_reset(qdl, mode) < 0 ? 1 : 0; + } qdl_session_close(qdl); sahara_images_free(sahara_images, MAPPING_SZ);