summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--MAINTAINERS9
-rw-r--r--arch/Kconfig10
-rw-r--r--arch/arm/lib/setjmp.S12
-rw-r--r--arch/arm/lib/setjmp_aarch64.S10
-rw-r--r--arch/riscv/lib/setjmp.S11
-rw-r--r--arch/sandbox/cpu/Makefile11
-rw-r--r--arch/sandbox/cpu/initjmp.c175
-rw-r--r--cmd/Kconfig17
-rw-r--r--cmd/Makefile2
-rw-r--r--cmd/spawn.c187
-rw-r--r--common/cyclic.c3
-rw-r--r--configs/imx93_11x11_evk_defconfig2
-rw-r--r--configs/qemu-riscv32_defconfig2
-rw-r--r--configs/qemu-riscv32_smode_defconfig2
-rw-r--r--configs/qemu-riscv64_defconfig2
-rw-r--r--configs/qemu-riscv64_smode_defconfig2
-rw-r--r--configs/qemu_arm64_defconfig2
-rw-r--r--configs/qemu_arm_defconfig2
-rw-r--r--configs/sandbox64_defconfig2
-rw-r--r--configs/sandbox_defconfig2
-rw-r--r--doc/api/index.rst2
-rw-r--r--doc/api/setjmp.rst20
-rw-r--r--doc/api/uthread.rst19
-rw-r--r--drivers/usb/host/usb-uclass.c190
-rw-r--r--include/setjmp.h32
-rw-r--r--include/u-boot/schedule.h3
-rw-r--r--include/uthread.h183
-rw-r--r--lib/Kconfig21
-rw-r--r--lib/Makefile2
-rw-r--r--lib/time.c9
-rw-r--r--lib/uthread.c165
-rw-r--r--test/boot/bootdev.c11
-rw-r--r--test/boot/bootflow.c2
-rw-r--r--test/cmd/Makefile1
-rw-r--r--test/cmd/spawn.c32
-rw-r--r--test/lib/Makefile2
-rw-r--r--test/lib/initjmp.c73
-rw-r--r--test/lib/uthread.c146
38 files changed, 1316 insertions, 62 deletions
diff --git a/MAINTAINERS b/MAINTAINERS
index 9d5af7ebca4..ebbb0b6e8d0 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1825,6 +1825,15 @@ T: git https://source.denx.de/u-boot/custodians/u-boot-usb.git topic-xhci
F: drivers/usb/host/xhci*
F: include/usb/xhci.h
+UTHREAD
+M: Jerome Forissier <jerome.forissier@linaro.org>
+S: Maintained
+F: cmd/spawn.c
+F: include/uthread.h
+F: lib/uthread.c
+F: test/cmd/spawn.c
+F: test/lib/uthread.c
+
UUID testing
M: Abdellatif El Khlifi <abdellatif.elkhlifi@arm.com>
S: Maintained
diff --git a/arch/Kconfig b/arch/Kconfig
index 35b19f9bfdc..ea33d07c086 100644
--- a/arch/Kconfig
+++ b/arch/Kconfig
@@ -13,6 +13,13 @@ config HAVE_SETJMP
help
The architecture supports setjmp() and longjmp().
+config HAVE_INITJMP
+ bool
+ depends on HAVE_SETJMP
+ help
+ The architecture supports initjmp(), a non-standard companion to
+ setjmp() and longjmp().
+
config SUPPORT_BIG_ENDIAN
bool
@@ -88,6 +95,7 @@ config ARC
config ARM
bool "ARM architecture"
select HAVE_SETJMP
+ select HAVE_INITJMP
select ARCH_SUPPORTS_LTO
select CREATE_ARCH_SYMLINK
select HAVE_PRIVATE_LIBGCC if !ARM64
@@ -145,6 +153,7 @@ config RISCV
bool "RISC-V architecture"
select CREATE_ARCH_SYMLINK
select HAVE_SETJMP
+ select HAVE_INITJMP
select SUPPORT_ACPI
select SUPPORT_LITTLE_ENDIAN
select SUPPORT_OF_CONTROL
@@ -171,6 +180,7 @@ config RISCV
config SANDBOX
bool "Sandbox"
select HAVE_SETJMP
+ select HAVE_INITJMP
select ARCH_SUPPORTS_LTO
select BOARD_LATE_INIT
select BZIP2
diff --git a/arch/arm/lib/setjmp.S b/arch/arm/lib/setjmp.S
index 2f041aeef01..81bef578719 100644
--- a/arch/arm/lib/setjmp.S
+++ b/arch/arm/lib/setjmp.S
@@ -34,3 +34,15 @@ ENTRY(longjmp)
ret lr
ENDPROC(longjmp)
.popsection
+
+.pushsection .text.initjmp, "ax"
+ENTRY(initjmp)
+ stm a1, {v1-v8}
+ /* a2: entry point address, a3: stack base, a4: stack size */
+ add a3, a3, a4
+ str a3, [a1, #32] /* where setjmp would save sp */
+ str a2, [a1, #36] /* where setjmp would save lr */
+ mov a1, #0
+ ret lr
+ENDPROC(initjmp)
+.popsection
diff --git a/arch/arm/lib/setjmp_aarch64.S b/arch/arm/lib/setjmp_aarch64.S
index 1b8d000eb48..01193ccc426 100644
--- a/arch/arm/lib/setjmp_aarch64.S
+++ b/arch/arm/lib/setjmp_aarch64.S
@@ -39,3 +39,13 @@ ENTRY(longjmp)
ret
ENDPROC(longjmp)
.popsection
+
+.pushsection .text.initjmp, "ax"
+ENTRY(initjmp)
+ /* x1: entry point address, x2: stack base, x3: stack size */
+ add x2, x2, x3
+ stp x1, x2, [x0,#88]
+ mov x0, #0
+ ret
+ENDPROC(initjmp)
+.popsection
diff --git a/arch/riscv/lib/setjmp.S b/arch/riscv/lib/setjmp.S
index 99d6195827e..9e1f3d5749b 100644
--- a/arch/riscv/lib/setjmp.S
+++ b/arch/riscv/lib/setjmp.S
@@ -59,3 +59,14 @@ ENTRY(longjmp)
ret
ENDPROC(longjmp)
.popsection
+
+.pushsection .text.initjmp, "ax"
+ENTRY(initjmp)
+ /* a1: entry point address, a2: stack base, a3: stack size */
+ add a2, a2, a3
+ STORE_IDX(a1, 12)
+ STORE_IDX(a2, 13)
+ li a0, 0
+ ret
+ENDPROC(initjmp)
+.popsection
diff --git a/arch/sandbox/cpu/Makefile b/arch/sandbox/cpu/Makefile
index bfcdc335d32..038ad78accc 100644
--- a/arch/sandbox/cpu/Makefile
+++ b/arch/sandbox/cpu/Makefile
@@ -5,7 +5,7 @@
# (C) Copyright 2000-2003
# Wolfgang Denk, DENX Software Engineering, wd@denx.de.
-obj-y := cache.o cpu.o state.o
+obj-y := cache.o cpu.o state.o initjmp.o
extra-y := start.o os.o
extra-$(CONFIG_SANDBOX_SDL) += sdl.o
obj-$(CONFIG_XPL_BUILD) += spl.o
@@ -29,6 +29,15 @@ cmd_cc_eth-raw-os.o = $(CC) $(filter-out -nostdinc, \
$(obj)/eth-raw-os.o: $(src)/eth-raw-os.c FORCE
$(call if_changed_dep,cc_eth-raw-os.o)
+# initjmp.c is build in the system environment, so needs standard includes
+# CFLAGS_REMOVE_initjmp.o cannot be used to drop header include path
+quiet_cmd_cc_initjmp.o = CC $(quiet_modtag) $@
+cmd_cc_initjmp.o = $(CC) $(filter-out -nostdinc, \
+ $(patsubst -I%,-idirafter%,$(c_flags))) -c -o $@ $<
+
+$(obj)/initjmp.o: $(src)/initjmp.c FORCE
+ $(call if_changed_dep,cc_initjmp.o)
+
# sdl.c fails to build with -fshort-wchar using musl
cmd_cc_sdl.o = $(CC) $(filter-out -nostdinc -fshort-wchar, \
$(patsubst -I%,-idirafter%,$(c_flags))) -fno-lto -c -o $@ $<
diff --git a/arch/sandbox/cpu/initjmp.c b/arch/sandbox/cpu/initjmp.c
new file mode 100644
index 00000000000..6e72d32cb4b
--- /dev/null
+++ b/arch/sandbox/cpu/initjmp.c
@@ -0,0 +1,175 @@
+// SPDX-License-Identifier: LGPL-2.1-or-later
+/*
+ * An implementation of initjmp() in C, that plays well with the system's
+ * setjmp() and longjmp() functions.
+ * Taken verbatim from arch/sandbox/os/setjmp.c in the barebox project.
+ * Modified so that initjmp() accepts a stack_size argument.
+ *
+ * Copyright (C) 2006 Anthony Liguori <anthony@codemonkey.ws>
+ * Copyright (C) 2011 Kevin Wolf <kwolf@redhat.com>
+ * Copyright (C) 2012 Alex Barcelo <abarcelo@ac.upc.edu>
+ * Copyright (C) 2021 Ahmad Fatoum, Pengutronix
+ * Copyright (C) 2025 Linaro Ltd.
+ * This file is partly based on pth_mctx.c, from the GNU Portable Threads
+ * Copyright (c) 1999-2006 Ralf S. Engelschall <rse@engelschall.com>
+ */
+
+/* XXX Is there a nicer way to disable glibc's stack check for longjmp? */
+#ifdef _FORTIFY_SOURCE
+#undef _FORTIFY_SOURCE
+#endif
+
+#include <pthread.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <setjmp.h>
+#include <signal.h>
+
+typedef sigjmp_buf _jmp_buf __attribute__((aligned((16))));
+_Static_assert(sizeof(_jmp_buf) <= 512, "sigjmp_buf size exceeds expectation");
+
+/*
+ * Information for the signal handler (trampoline)
+ */
+static struct {
+ _jmp_buf *reenter;
+ void (*entry)(void);
+ volatile sig_atomic_t called;
+} tr_state;
+
+/*
+ * "boot" function
+ * This is what starts the coroutine, is called from the trampoline
+ * (from the signal handler when it is not signal handling, read ahead
+ * for more information).
+ */
+static void __attribute__((noinline, noreturn))
+coroutine_bootstrap(void (*entry)(void))
+{
+ for (;;)
+ entry();
+}
+
+/*
+ * This is used as the signal handler. This is called with the brand new stack
+ * (thanks to sigaltstack). We have to return, given that this is a signal
+ * handler and the sigmask and some other things are changed.
+ */
+static void coroutine_trampoline(int signal)
+{
+ /* Get the thread specific information */
+ tr_state.called = 1;
+
+ /*
+ * Here we have to do a bit of a ping pong between the caller, given that
+ * this is a signal handler and we have to do a return "soon". Then the
+ * caller can reestablish everything and do a siglongjmp here again.
+ */
+ if (!sigsetjmp(*tr_state.reenter, 0)) {
+ return;
+ }
+
+ /*
+ * Ok, the caller has siglongjmp'ed back to us, so now prepare
+ * us for the real machine state switching. We have to jump
+ * into another function here to get a new stack context for
+ * the auto variables (which have to be auto-variables
+ * because the start of the thread happens later). Else with
+ * PIC (i.e. Position Independent Code which is used when PTH
+ * is built as a shared library) most platforms would
+ * horrible core dump as experience showed.
+ */
+ coroutine_bootstrap(tr_state.entry);
+}
+
+int __attribute__((weak)) initjmp(_jmp_buf jmp, void (*func)(void),
+ void *stack_base, size_t stack_size)
+{
+ struct sigaction sa;
+ struct sigaction osa;
+ stack_t ss;
+ stack_t oss;
+ sigset_t sigs;
+ sigset_t osigs;
+
+ /* The way to manipulate stack is with the sigaltstack function. We
+ * prepare a stack, with it delivering a signal to ourselves and then
+ * put sigsetjmp/siglongjmp where needed.
+ * This has been done keeping coroutine-ucontext (from the QEMU project)
+ * as a model and with the pth ideas (GNU Portable Threads).
+ * See coroutine-ucontext for the basics of the coroutines and see
+ * pth_mctx.c (from the pth project) for the
+ * sigaltstack way of manipulating stacks.
+ */
+
+ tr_state.entry = func;
+ tr_state.reenter = (void *)jmp;
+
+ /*
+ * Preserve the SIGUSR2 signal state, block SIGUSR2,
+ * and establish our signal handler. The signal will
+ * later transfer control onto the signal stack.
+ */
+ sigemptyset(&sigs);
+ sigaddset(&sigs, SIGUSR2);
+ pthread_sigmask(SIG_BLOCK, &sigs, &osigs);
+ sa.sa_handler = coroutine_trampoline;
+ sigfillset(&sa.sa_mask);
+ sa.sa_flags = SA_ONSTACK;
+ if (sigaction(SIGUSR2, &sa, &osa) != 0) {
+ return -1;
+ }
+
+ /*
+ * Set the new stack.
+ */
+ ss.ss_sp = stack_base;
+ ss.ss_size = stack_size;
+ ss.ss_flags = 0;
+ if (sigaltstack(&ss, &oss) < 0) {
+ return -1;
+ }
+
+ /*
+ * Now transfer control onto the signal stack and set it up.
+ * It will return immediately via "return" after the sigsetjmp()
+ * was performed. Be careful here with race conditions. The
+ * signal can be delivered the first time sigsuspend() is
+ * called.
+ */
+ tr_state.called = 0;
+ pthread_kill(pthread_self(), SIGUSR2);
+ sigfillset(&sigs);
+ sigdelset(&sigs, SIGUSR2);
+ while (!tr_state.called) {
+ sigsuspend(&sigs);
+ }
+
+ /*
+ * Inform the system that we are back off the signal stack by
+ * removing the alternative signal stack. Be careful here: It
+ * first has to be disabled, before it can be removed.
+ */
+ sigaltstack(NULL, &ss);
+ ss.ss_flags = SS_DISABLE;
+ if (sigaltstack(&ss, NULL) < 0) {
+ return -1;
+ }
+ sigaltstack(NULL, &ss);
+ if (!(oss.ss_flags & SS_DISABLE)) {
+ sigaltstack(&oss, NULL);
+ }
+
+ /*
+ * Restore the old SIGUSR2 signal handler and mask
+ */
+ sigaction(SIGUSR2, &osa, NULL);
+ pthread_sigmask(SIG_SETMASK, &osigs, NULL);
+
+ /*
+ * jmp can now be used to enter the trampoline again, but not as a
+ * signal handler. Instead it's longjmp'd to directly.
+ */
+ return 0;
+}
+
diff --git a/cmd/Kconfig b/cmd/Kconfig
index c2ce519d1e3..2d31abcef73 100644
--- a/cmd/Kconfig
+++ b/cmd/Kconfig
@@ -3081,4 +3081,21 @@ config CMD_MESON
help
Enable useful commands for the Meson Soc family developed by Amlogic Inc.
+config CMD_SPAWN
+ bool "spawn and wait commands"
+ depends on UTHREAD
+ help
+ spawn runs a command in the background and sets the job_id environment
+ variable. wait is used to suspend the shell execution until one or more
+ jobs are complete.
+
+config CMD_SPAWN_NUM_JOBS
+ int "Maximum number of simultaneous jobs for spawn"
+ default 16
+ help
+ Job identifiers are in the range 1..CMD_SPAWN_NUM_JOBS. In other words
+ there can be no more that CMD_SPAWN_NUM_JOBS running simultaneously.
+ When a jobs exits, its identifier is available to be re-used by the next
+ spawn command.
+
endif
diff --git a/cmd/Makefile b/cmd/Makefile
index 8f0cee8d714..80cf70b7fe8 100644
--- a/cmd/Makefile
+++ b/cmd/Makefile
@@ -239,6 +239,8 @@ obj-$(CONFIG_CMD_SCP03) += scp03.o
obj-$(CONFIG_HUSH_SELECTABLE) += cli.o
+obj-$(CONFIG_CMD_SPAWN) += spawn.o
+
obj-$(CONFIG_ARM) += arm/
obj-$(CONFIG_RISCV) += riscv/
obj-$(CONFIG_SANDBOX) += sandbox/
diff --git a/cmd/spawn.c b/cmd/spawn.c
new file mode 100644
index 00000000000..eddbcb792b3
--- /dev/null
+++ b/cmd/spawn.c
@@ -0,0 +1,187 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright (c) 2011 The Chromium OS Authors.
+ */
+
+#include <command.h>
+#include <console.h>
+#include <malloc.h>
+#include <vsprintf.h>
+#include <uthread.h>
+
+/* Spawn arguments and job index */
+struct spa {
+ int argc;
+ char **argv;
+ unsigned int job_idx;
+};
+
+/*
+ * uthread group identifiers for each running job
+ * 0: job slot available, != 0: uthread group id
+ * Note that job[0] is job_id 1, job[1] is job_id 2 etc.
+ */
+static unsigned int job[CONFIG_CMD_SPAWN_NUM_JOBS];
+/* Return values of the commands run as jobs */
+static enum command_ret_t job_ret[CONFIG_CMD_SPAWN_NUM_JOBS];
+
+static void spa_free(struct spa *spa)
+{
+ int i;
+
+ if (!spa)
+ return;
+
+ for (i = 0; i < spa->argc; i++)
+ free(spa->argv[i]);
+ free(spa->argv);
+ free(spa);
+}
+
+static struct spa *spa_create(int argc, char *const argv[])
+{
+ struct spa *spa;
+ int i;
+
+ spa = calloc(1, sizeof(*spa));
+ if (!spa)
+ return NULL;
+ spa->argc = argc;
+ spa->argv = malloc(argc * sizeof(char *));
+ if (!spa->argv)
+ goto err;
+ for (i = 0; i < argc; i++) {
+ spa->argv[i] = strdup(argv[i]);
+ if (!spa->argv[i])
+ goto err;
+ }
+ return spa;
+err:
+ spa_free(spa);
+ return NULL;
+}
+
+static void spawn_thread(void *arg)
+{
+ struct spa *spa = (struct spa *)arg;
+ ulong cycles = 0;
+ int repeatable = 0;
+
+ job_ret[spa->job_idx] = cmd_process(0, spa->argc, spa->argv,
+ &repeatable, &cycles);
+ spa_free(spa);
+}
+
+static unsigned int next_job_id(void)
+{
+ int i;
+
+ for (i = 0; i < CONFIG_CMD_SPAWN_NUM_JOBS; i++)
+ if (!job[i])
+ return i + 1;
+
+ /* No job available */
+ return 0;
+}
+
+static void refresh_jobs(void)
+{
+ int i;
+
+ for (i = 0; i < CONFIG_CMD_SPAWN_NUM_JOBS; i++)
+ if (job[i] && uthread_grp_done(job[i]))
+ job[i] = 0;
+
+}
+
+static int do_spawn(struct cmd_tbl *cmdtp, int flag, int argc,
+ char *const argv[])
+{
+ unsigned int id;
+ unsigned int idx;
+ struct spa *spa;
+ int ret;
+
+ if (argc == 1)
+ return CMD_RET_USAGE;
+
+ spa = spa_create(argc - 1, argv + 1);
+ if (!spa)
+ return CMD_RET_FAILURE;
+
+ refresh_jobs();
+
+ id = next_job_id();
+ if (!id)
+ return CMD_RET_FAILURE;
+ idx = id - 1;
+
+ job[idx] = uthread_grp_new_id();
+
+ ret = uthread_create(NULL, spawn_thread, spa, 0, job[idx]);
+ if (ret) {
+ job[idx] = 0;
+ return CMD_RET_FAILURE;
+ }
+
+ ret = env_set_ulong("job_id", id);
+ if (ret)
+ return CMD_RET_FAILURE;
+
+ return CMD_RET_SUCCESS;
+}
+
+U_BOOT_CMD(spawn, CONFIG_SYS_MAXARGS, 0, do_spawn,
+ "run commands and summarize execution time",
+ "command [args...]\n");
+
+static enum command_ret_t wait_job(unsigned int idx)
+{
+ int prev = disable_ctrlc(false);
+
+ while (!uthread_grp_done(job[idx])) {
+ if (ctrlc()) {
+ puts("<INTERRUPT>\n");
+ disable_ctrlc(prev);
+ return CMD_RET_FAILURE;
+ }
+ uthread_schedule();
+ }
+
+ job[idx] = 0;
+ disable_ctrlc(prev);
+
+ return job_ret[idx];
+}
+
+static int do_wait(struct cmd_tbl *cmdtp, int flag, int argc,
+ char *const argv[])
+{
+ enum command_ret_t ret = CMD_RET_SUCCESS;
+ unsigned long id;
+ unsigned int idx;
+ int i;
+
+ if (argc == 1) {
+ for (i = 0; i < CONFIG_CMD_SPAWN_NUM_JOBS; i++)
+ if (job[i])
+ ret = wait_job(i);
+ } else {
+ for (i = 1; i < argc; i++) {
+ id = dectoul(argv[i], NULL);
+ if (id < 0 || id > CONFIG_CMD_SPAWN_NUM_JOBS)
+ return CMD_RET_USAGE;
+ idx = (int)id - 1;
+ ret = wait_job(idx);
+ }
+ }
+
+ return ret;
+}
+
+U_BOOT_CMD(wait, CONFIG_SYS_MAXARGS, 0, do_wait,
+ "wait for one or more jobs to complete",
+ "[job_id ...]\n"
+ " - Wait until all specified jobs have exited and return the\n"
+ " exit status of the last job waited for. When no job_id is\n"
+ " given, wait for all the background jobs.\n");
diff --git a/common/cyclic.c b/common/cyclic.c
index fad071a39c6..b695f092f52 100644
--- a/common/cyclic.c
+++ b/common/cyclic.c
@@ -16,6 +16,7 @@
#include <linux/list.h>
#include <asm/global_data.h>
#include <u-boot/schedule.h>
+#include <uthread.h>
DECLARE_GLOBAL_DATA_PTR;
@@ -100,6 +101,8 @@ void schedule(void)
*/
if (gd)
cyclic_run();
+
+ uthread_schedule();
}
int cyclic_unregister_all(void)
diff --git a/configs/imx93_11x11_evk_defconfig b/configs/imx93_11x11_evk_defconfig
index 59c4da7fa2b..d03895ed6ea 100644
--- a/configs/imx93_11x11_evk_defconfig
+++ b/configs/imx93_11x11_evk_defconfig
@@ -129,3 +129,5 @@ CONFIG_ULP_WATCHDOG=y
CONFIG_WDT=y
CONFIG_LZO=y
CONFIG_BZIP2=y
+CONFIG_UTHREAD=y
+CONFIG_CMD_SPAWN=y
diff --git a/configs/qemu-riscv32_defconfig b/configs/qemu-riscv32_defconfig
index 6f8f9827611..b9f28873c15 100644
--- a/configs/qemu-riscv32_defconfig
+++ b/configs/qemu-riscv32_defconfig
@@ -16,8 +16,10 @@ CONFIG_DISPLAY_BOARDINFO=y
CONFIG_CMD_BOOTEFI_SELFTEST=y
CONFIG_CMD_NVEDIT_EFI=y
# CONFIG_CMD_MII is not set
+CONFIG_CMD_SPAWN=y
CONFIG_SYS_RELOC_GD_ENV_ADDR=y
CONFIG_DM_MTD=y
CONFIG_FLASH_SHOW_PROGRESS=0
CONFIG_SYS_MAX_FLASH_BANKS=2
+CONFIG_UTHREAD=y
CONFIG_UNIT_TEST=y
diff --git a/configs/qemu-riscv32_smode_defconfig b/configs/qemu-riscv32_smode_defconfig
index 1d57b68086a..cd89571e40c 100644
--- a/configs/qemu-riscv32_smode_defconfig
+++ b/configs/qemu-riscv32_smode_defconfig
@@ -17,8 +17,10 @@ CONFIG_DISPLAY_BOARDINFO=y
CONFIG_CMD_BOOTEFI_SELFTEST=y
CONFIG_CMD_NVEDIT_EFI=y
# CONFIG_CMD_MII is not set
+CONFIG_CMD_SPAWN=y
CONFIG_SYS_RELOC_GD_ENV_ADDR=y
CONFIG_DM_MTD=y
CONFIG_FLASH_SHOW_PROGRESS=0
CONFIG_SYS_MAX_FLASH_BANKS=2
+CONFIG_UTHREAD=y
CONFIG_UNIT_TEST=y
diff --git a/configs/qemu-riscv64_defconfig b/configs/qemu-riscv64_defconfig
index e00050db4e1..c67fb9a3352 100644
--- a/configs/qemu-riscv64_defconfig
+++ b/configs/qemu-riscv64_defconfig
@@ -16,8 +16,10 @@ CONFIG_DISPLAY_BOARDINFO=y
CONFIG_CMD_BOOTEFI_SELFTEST=y
CONFIG_CMD_NVEDIT_EFI=y
# CONFIG_CMD_MII is not set
+CONFIG_CMD_SPAWN=y
CONFIG_SYS_RELOC_GD_ENV_ADDR=y
CONFIG_DM_MTD=y
CONFIG_FLASH_SHOW_PROGRESS=0
CONFIG_SYS_MAX_FLASH_BANKS=2
+CONFIG_UTHREAD=y
CONFIG_UNIT_TEST=y
diff --git a/configs/qemu-riscv64_smode_defconfig b/configs/qemu-riscv64_smode_defconfig
index a424f978294..d28e9fbeceb 100644
--- a/configs/qemu-riscv64_smode_defconfig
+++ b/configs/qemu-riscv64_smode_defconfig
@@ -19,8 +19,10 @@ CONFIG_DISPLAY_BOARDINFO=y
CONFIG_CMD_BOOTEFI_SELFTEST=y
CONFIG_CMD_NVEDIT_EFI=y
# CONFIG_CMD_MII is not set
+CONFIG_CMD_SPAWN=y
CONFIG_SYS_RELOC_GD_ENV_ADDR=y
CONFIG_DM_MTD=y
CONFIG_FLASH_SHOW_PROGRESS=0
CONFIG_SYS_MAX_FLASH_BANKS=2
+CONFIG_UTHREAD=y
CONFIG_UNIT_TEST=y
diff --git a/configs/qemu_arm64_defconfig b/configs/qemu_arm64_defconfig
index cd9a4f2ae26..b1371d4258f 100644
--- a/configs/qemu_arm64_defconfig
+++ b/configs/qemu_arm64_defconfig
@@ -37,6 +37,7 @@ CONFIG_CMD_PCI=y
CONFIG_CMD_EFIDEBUG=y
CONFIG_CMD_TPM=y
CONFIG_CMD_MTDPARTS=y
+CONFIG_CMD_SPAWN=y
CONFIG_ENV_IS_IN_FLASH=y
CONFIG_SCSI_AHCI=y
CONFIG_AHCI_PCI=y
@@ -75,4 +76,5 @@ CONFIG_MBEDTLS_LIB=y
CONFIG_TPM=y
CONFIG_TPM_PCR_ALLOCATE=y
CONFIG_GENERATE_SMBIOS_TABLE_VERBOSE=y
+CONFIG_UTHREAD=y
CONFIG_UNIT_TEST=y
diff --git a/configs/qemu_arm_defconfig b/configs/qemu_arm_defconfig
index 2d642f86ba6..97d2f2f2e49 100644
--- a/configs/qemu_arm_defconfig
+++ b/configs/qemu_arm_defconfig
@@ -35,6 +35,7 @@ CONFIG_CMD_MTD=y
CONFIG_CMD_PCI=y
CONFIG_CMD_TPM=y
CONFIG_CMD_MTDPARTS=y
+CONFIG_CMD_SPAWN=y
CONFIG_ENV_IS_IN_FLASH=y
CONFIG_SCSI_AHCI=y
CONFIG_AHCI_PCI=y
@@ -67,4 +68,5 @@ CONFIG_TPM2_MMIO=y
CONFIG_USB_EHCI_HCD=y
CONFIG_USB_EHCI_PCI=y
CONFIG_TPM=y
+CONFIG_UTHREAD=y
CONFIG_UNIT_TEST=y
diff --git a/configs/sandbox64_defconfig b/configs/sandbox64_defconfig
index 42c40077823..23df4c3b635 100644
--- a/configs/sandbox64_defconfig
+++ b/configs/sandbox64_defconfig
@@ -99,6 +99,7 @@ CONFIG_CMD_CBFS=y
CONFIG_CMD_CRAMFS=y
CONFIG_CMD_EXT4_WRITE=y
CONFIG_CMD_MTDPARTS=y
+CONFIG_CMD_SPAWN=y
CONFIG_MAC_PARTITION=y
CONFIG_AMIGA_PARTITION=y
CONFIG_OF_CONTROL=y
@@ -275,6 +276,7 @@ CONFIG_TPM=y
CONFIG_ERRNO_STR=y
CONFIG_GETOPT=y
CONFIG_TEST_FDTDEC=y
+CONFIG_UTHREAD=y
CONFIG_UNIT_TEST=y
CONFIG_UT_TIME=y
CONFIG_UT_DM=y
diff --git a/configs/sandbox_defconfig b/configs/sandbox_defconfig
index 87f21fdbd12..c4b1b8114d6 100644
--- a/configs/sandbox_defconfig
+++ b/configs/sandbox_defconfig
@@ -143,6 +143,7 @@ CONFIG_CMD_EXT4_WRITE=y
CONFIG_CMD_SQUASHFS=y
CONFIG_CMD_MTDPARTS=y
CONFIG_CMD_STACKPROTECTOR_TEST=y
+CONFIG_CMD_SPAWN=y
CONFIG_MAC_PARTITION=y
CONFIG_OF_CONTROL=y
CONFIG_OF_LIVE=y
@@ -364,6 +365,7 @@ CONFIG_TPM=y
CONFIG_ERRNO_STR=y
CONFIG_GETOPT=y
CONFIG_TEST_FDTDEC=y
+CONFIG_UTHREAD=y
CONFIG_UNIT_TEST=y
CONFIG_UT_TIME=y
CONFIG_UT_DM=y
diff --git a/doc/api/index.rst b/doc/api/index.rst
index a108718ea99..506843ed74a 100644
--- a/doc/api/index.rst
+++ b/doc/api/index.rst
@@ -25,6 +25,8 @@ U-Boot API documentation
rng
sandbox
serial
+ setjmp
sysreset
timer
unicode
+ uthread
diff --git a/doc/api/setjmp.rst b/doc/api/setjmp.rst
new file mode 100644
index 00000000000..c30e51c2b55
--- /dev/null
+++ b/doc/api/setjmp.rst
@@ -0,0 +1,20 @@
+.. SPDX-License-Identifier: GPL-2.0-or-later
+
+Long jump API
+=============
+
+.. kernel-doc:: include/setjmp.h
+ :doc: Overview
+
+.. kernel-doc:: include/setjmp.h
+ :internal:
+
+Example
+-------
+
+Here is an example showing how to use the a long jump functions and
+initjmp() in particular:
+
+.. literalinclude:: ../../test/lib/initjmp.c
+ :language: c
+ :linenos:
diff --git a/doc/api/uthread.rst b/doc/api/uthread.rst
new file mode 100644
index 00000000000..8b25cc1ff80
--- /dev/null
+++ b/doc/api/uthread.rst
@@ -0,0 +1,19 @@
+.. SPDX-License-Identifier: GPL-2.0-or-later
+
+Uthread API
+===========
+
+.. kernel-doc:: include/uthread.h
+ :doc: Overview
+
+.. kernel-doc:: include/uthread.h
+ :internal:
+
+Example
+-------
+
+Here is an example of how to use this API:
+
+.. literalinclude:: ../../test/lib/uthread.c
+ :language: c
+ :linenos:
diff --git a/drivers/usb/host/usb-uclass.c b/drivers/usb/host/usb-uclass.c
index bfec303e7af..7247245a702 100644
--- a/drivers/usb/host/usb-uclass.c
+++ b/drivers/usb/host/usb-uclass.c
@@ -9,6 +9,7 @@
#define LOG_CATEGORY UCLASS_USB
#include <bootdev.h>
+#include <uthread.h>
#include <dm.h>
#include <errno.h>
#include <log.h>
@@ -17,6 +18,7 @@
#include <dm/device-internal.h>
#include <dm/lists.h>
#include <dm/uclass-internal.h>
+#include <time.h>
static bool asynch_allowed;
@@ -172,6 +174,10 @@ int usb_get_max_xfer_size(struct usb_device *udev, size_t *size)
return ops->get_max_xfer_size(bus, size);
}
+#if CONFIG_IS_ENABLED(UTHREAD)
+static struct uthread_mutex mutex = UTHREAD_MUTEX_INITIALIZER;
+#endif
+
int usb_stop(void)
{
struct udevice *bus;
@@ -180,10 +186,14 @@ int usb_stop(void)
struct usb_uclass_priv *uc_priv;
int err = 0, ret;
+ uthread_mutex_lock(&mutex);
+
/* De-activate any devices that have been activated */
ret = uclass_get(UCLASS_USB, &uc);
- if (ret)
+ if (ret) {
+ uthread_mutex_unlock(&mutex);
return ret;
+ }
uc_priv = uclass_get_priv(uc);
@@ -218,28 +228,23 @@ int usb_stop(void)
uc_priv->companion_device_count = 0;
usb_started = 0;
+ uthread_mutex_unlock(&mutex);
+
return err;
}
-static void usb_scan_bus(struct udevice *bus, bool recurse)
+static void _usb_scan_bus(void *arg)
{
+ struct udevice *bus = (struct udevice *)arg;
struct usb_bus_priv *priv;
struct udevice *dev;
int ret;
priv = dev_get_uclass_priv(bus);
- assert(recurse); /* TODO: Support non-recusive */
-
- printf("scanning bus %s for devices... ", bus->name);
- debug("\n");
ret = usb_scan_device(bus, 0, USB_SPEED_FULL, &dev);
if (ret)
- printf("failed, error %d\n", ret);
- else if (priv->next_addr == 0)
- printf("No USB Device found\n");
- else
- printf("%d USB Device(s) found\n", priv->next_addr);
+ printf("Scanning bus %s failed, error %d\n", bus->name, ret);
}
static void remove_inactive_children(struct uclass *uc, struct udevice *bus)
@@ -287,64 +292,127 @@ static int usb_probe_companion(struct udevice *bus)
return 0;
}
+static void _usb_init_bus(void *arg)
+{
+ struct udevice *bus = (struct udevice *)arg;
+ int ret;
+
+ /* init low_level USB */
+
+ /*
+ * For Sandbox, we need scan the device tree each time when we
+ * start the USB stack, in order to re-create the emulated USB
+ * devices and bind drivers for them before we actually do the
+ * driver probe.
+ *
+ * For USB onboard HUB, we need to do some non-trivial init
+ * like enabling a power regulator, before enumeration.
+ */
+ if (IS_ENABLED(CONFIG_SANDBOX) ||
+ IS_ENABLED(CONFIG_USB_ONBOARD_HUB)) {
+ ret = dm_scan_fdt_dev(bus);
+ if (ret) {
+ printf("Bus %s: USB device scan from fdt failed (%d)\n",
+ bus->name, ret);
+ return;
+ }
+ }
+
+ ret = device_probe(bus);
+ if (ret == -ENODEV) { /* No such device. */
+ printf("Bus %s: Port not available.\n", bus->name);
+ return;
+ }
+
+ if (ret) { /* Other error. */
+ printf("Bus %s: probe failed, error %d\n", bus->name, ret);
+ return;
+ }
+
+ usb_probe_companion(bus);
+}
+
+static int nthr;
+static int grp_id;
+
+static void usb_init_bus(struct udevice *bus)
+{
+ if (!grp_id)
+ grp_id = uthread_grp_new_id();
+ if (!uthread_create(NULL, _usb_init_bus, (void *)bus, 0, grp_id))
+ nthr++;
+}
+
+static void usb_scan_bus(struct udevice *bus, bool recurse)
+{
+ if (!grp_id)
+ grp_id = uthread_grp_new_id();
+ if (!uthread_create(NULL, _usb_scan_bus, (void *)bus, 0, grp_id))
+ nthr++;
+}
+
+static void usb_report_devices(struct uclass *uc)
+{
+ struct usb_bus_priv *priv;
+ struct udevice *bus;
+
+ uclass_foreach_dev(bus, uc) {
+ if (!device_active(bus))
+ continue;
+ priv = dev_get_uclass_priv(bus);
+ printf("Bus %s: ", bus->name);
+ if (priv->next_addr == 0)
+ printf("No USB Device found\n");
+ else
+ printf("%d USB Device(s) found\n", priv->next_addr);
+ }
+}
+
+static void run_threads(void)
+{
+#if CONFIG_IS_ENABLED(UTHREAD)
+ if (!nthr)
+ return;
+ while (!uthread_grp_done(grp_id))
+ uthread_schedule();
+ nthr = 0;
+ grp_id = 0;
+#endif
+}
+
int usb_init(void)
{
int controllers_initialized = 0;
+ unsigned long t0 = timer_get_us();
struct usb_uclass_priv *uc_priv;
struct usb_bus_priv *priv;
struct udevice *bus;
struct uclass *uc;
int ret;
+ uthread_mutex_lock(&mutex);
+
+ if (usb_started) {
+ ret = 0;
+ goto out;
+ }
+
asynch_allowed = 1;
ret = uclass_get(UCLASS_USB, &uc);
if (ret)
- return ret;
+ goto out;
uc_priv = uclass_get_priv(uc);
uclass_foreach_dev(bus, uc) {
- /* init low_level USB */
- printf("Bus %s: ", bus->name);
-
- /*
- * For Sandbox, we need scan the device tree each time when we
- * start the USB stack, in order to re-create the emulated USB
- * devices and bind drivers for them before we actually do the
- * driver probe.
- *
- * For USB onboard HUB, we need to do some non-trivial init
- * like enabling a power regulator, before enumeration.
- */
- if (IS_ENABLED(CONFIG_SANDBOX) ||
- IS_ENABLED(CONFIG_USB_ONBOARD_HUB)) {
- ret = dm_scan_fdt_dev(bus);
- if (ret) {
- printf("USB device scan from fdt failed (%d)", ret);
- continue;
- }
- }
-
- ret = device_probe(bus);
- if (ret == -ENODEV) { /* No such device. */
- puts("Port not available.\n");
- controllers_initialized++;
- continue;
- }
-
- if (ret) { /* Other error. */
- printf("probe failed, error %d\n", ret);
- continue;
- }
+ usb_init_bus(bus);
+ }
- ret = usb_probe_companion(bus);
- if (ret)
- continue;
+ if (CONFIG_IS_ENABLED(UTHREAD))
+ run_threads();
- controllers_initialized++;
- usb_started = true;
- }
+ usb_started = true;
/*
* lowlevel init done, now scan the bus for devices i.e. search HUBs
@@ -354,11 +422,16 @@ int usb_init(void)
if (!device_active(bus))
continue;
+ controllers_initialized++;
+
priv = dev_get_uclass_priv(bus);
if (!priv->companion)
usb_scan_bus(bus, true);
}
+ if (CONFIG_IS_ENABLED(UTHREAD))
+ run_threads();
+
/*
* Now that the primary controllers have been scanned and have handed
* over any devices they do not understand to their companions, scan
@@ -375,21 +448,34 @@ int usb_init(void)
}
}
- debug("scan end\n");
+ if (CONFIG_IS_ENABLED(UTHREAD))
+ run_threads();
+
+ usb_report_devices(uc);
/* Remove any devices that were not found on this scan */
remove_inactive_children(uc, bus);
ret = uclass_get(UCLASS_USB_HUB, &uc);
if (ret)
- return ret;
+ goto out;
+
remove_inactive_children(uc, bus);
/* if we were not able to find at least one working bus, bail out */
if (controllers_initialized == 0)
printf("No USB controllers found\n");
+ debug("USB initialized in %ld ms\n",
+ (timer_get_us() - t0) / 1000);
+
+ uthread_mutex_unlock(&mutex);
+
return usb_started ? 0 : -ENOENT;
+out:
+ uthread_mutex_unlock(&mutex);
+
+ return ret;
}
int usb_setup_ehci_gadget(struct ehci_ctrl **ctlrp)
diff --git a/include/setjmp.h b/include/setjmp.h
index 37d3a8af85d..32dd48803e9 100644
--- a/include/setjmp.h
+++ b/include/setjmp.h
@@ -3,12 +3,27 @@
#ifndef _SETJMP_H_
#define _SETJMP_H_ 1
+/**
+ * DOC: Overview
+ *
+ * The long jump API allows to perform nonlocal gotos, that is jump from one
+ * function to another typically further down in the stack, while properly
+ * restoring the stack's state (unwinding). The two functions needed to do this
+ * are setjmp() and longjmp().
+ *
+ * In addition to these two standard POSIX.1-2001/C89 functions, a third one is
+ * present in U-Boot: initjmp(). It is an extension which allows to implement
+ * user-mode threads.
+ */
+
#ifdef CONFIG_HAVE_SETJMP
#include <asm/setjmp.h>
#else
struct jmp_buf_data {
};
#endif
+#include <linux/compiler_attributes.h>
+#include <stddef.h>
/**
* typedef jmp_buf - information needed to restore a calling environment
@@ -37,4 +52,21 @@ int setjmp(jmp_buf env);
*/
void longjmp(jmp_buf env, int val);
+/**
+ * initjmp() - prepare for a long jump to a given function with a given stack
+ *
+ * This function sets up a jump buffer for later use with longjmp(). It allows
+ * to branch to a specific function with a specific stack. Please note that
+ * @func MUST NOT return. It shall typically restore the main stack and resume
+ * execution by doing a long jump to a jump buffer initialized by setjmp()
+ * before the long jump. initjmp() allows to implement multithreading.
+ *
+ * @env: jump buffer
+ * @func: function to be called on longjmp(), MUST NOT RETURN
+ * @stack_base: the stack to be used by @func (lower address)
+ * @stack_sz: the stack size in bytes
+ */
+int initjmp(jmp_buf env, void __noreturn (*func)(void), void *stack_base,
+ size_t stack_sz);
+
#endif /* _SETJMP_H_ */
diff --git a/include/u-boot/schedule.h b/include/u-boot/schedule.h
index 4fd34c41229..4605971fdcb 100644
--- a/include/u-boot/schedule.h
+++ b/include/u-boot/schedule.h
@@ -3,6 +3,8 @@
#ifndef _U_BOOT_SCHEDULE_H
#define _U_BOOT_SCHEDULE_H
+#include <uthread.h>
+
#if CONFIG_IS_ENABLED(CYCLIC)
/**
* schedule() - Schedule all potentially waiting tasks
@@ -17,6 +19,7 @@ void schedule(void);
static inline void schedule(void)
{
+ uthread_schedule();
}
#endif
diff --git a/include/uthread.h b/include/uthread.h
new file mode 100644
index 00000000000..89fa552a6f6
--- /dev/null
+++ b/include/uthread.h
@@ -0,0 +1,183 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/*
+ * Copyright 2025 Linaro Limited
+ */
+
+#include <linux/list.h>
+#include <linux/types.h>
+#include <setjmp.h>
+
+#ifndef _UTHREAD_H_
+#define _UTHREAD_H_
+
+/**
+ * DOC: Overview
+ *
+ * The uthread framework is a basic task scheduler that allows to run functions
+ * "in parallel" on a single CPU core. The scheduling is cooperative, not
+ * preemptive -- meaning that context switches from one task to another task is
+ * voluntary, via a call to uthread_schedule(). This characteristic makes thread
+ * synchronization much easier, because a thread cannot be interrupted in the
+ * middle of a critical section (reading from or writing to shared state, for
+ * instance).
+ *
+ * CONFIG_UTHREAD in lib/Kconfig enables the uthread framework. When disabled,
+ * the uthread_create() and uthread_schedule() functions may still be used so
+ * that code differences between uthreads enabled and disabled can be reduced to
+ * a minimum.
+ */
+
+/**
+ * struct uthread - a thread object
+ *
+ * @fn: thread entry point
+ * @arg: argument passed to the entry point when the thread is started
+ * @ctx: context to resume execution of this thread (via longjmp())
+ * @stack: initial stack pointer for the thread
+ * @done: true once @fn has returned, false otherwise
+ * @grp_id: user-supplied identifier for this thread and possibly others. A
+ * thread can belong to zero or one group (not more), and a group may contain
+ * any number of threads.
+ * @list: link in the global scheduler list
+ */
+struct uthread {
+ void (*fn)(void *arg);
+ void *arg;
+ jmp_buf ctx;
+ void *stack;
+ bool done;
+ unsigned int grp_id;
+ struct list_head list;
+};
+
+/**
+ * Internal state of a struct uthread_mutex
+ */
+enum uthread_mutex_state {
+ UTHREAD_MUTEX_UNLOCKED = 0,
+ UTHREAD_MUTEX_LOCKED = 1
+};
+
+/**
+ * Uthread mutex
+ */
+struct uthread_mutex {
+ enum uthread_mutex_state state;
+};
+
+#define UTHREAD_MUTEX_INITIALIZER { .state = UTHREAD_MUTEX_UNLOCKED }
+
+#ifdef CONFIG_UTHREAD
+
+/**
+ * uthread_create() - Create a uthread object and make it ready for execution
+ *
+ * Threads are automatically deleted when they return from their entry point.
+ *
+ * @uthr: a pointer to a user-allocated uthread structure to store information
+ * about the new thread, or NULL to let the framework allocate and manage its
+ * own structure.
+ * @fn: the thread's entry point
+ * @arg: argument passed to the thread's entry point
+ * @stack_sz: stack size for the new thread (in bytes). The stack is allocated
+ * on the heap.
+ * @grp_id: an optional thread group ID that the new thread should belong to
+ * (zero for no group)
+ */
+int uthread_create(struct uthread *uthr, void (*fn)(void *), void *arg,
+ size_t stack_sz, unsigned int grp_id);
+/**
+ * uthread_schedule() - yield the CPU to the next runnable thread
+ *
+ * This function is called either by the main thread or any secondary thread
+ * (that is, any thread created via uthread_create()) to switch execution to
+ * the next runnable thread.
+ *
+ * Return: true if a thread was scheduled, false if no runnable thread was found
+ */
+bool uthread_schedule(void);
+/**
+ * uthread_grp_new_id() - return a new ID for a thread group
+ *
+ * Return: the new thread group ID
+ */
+unsigned int uthread_grp_new_id(void);
+/**
+ * uthread_grp_done() - test if all threads in a group are done
+ *
+ * @grp_id: the ID of the thread group that should be considered
+ * Return: false if the group contains at least one runnable thread (i.e., one
+ * thread which entry point has not returned yet), true otherwise
+ */
+bool uthread_grp_done(unsigned int grp_id);
+
+/**
+ * uthread_mutex_lock() - lock a mutex
+ *
+ * If the cwmutexlock is available (i.e., not owned by any other thread), then
+ * it is locked for use by the current thread. Otherwise the current thread
+ * blocks: it enters a wait loop by scheduling other threads until the mutex
+ * becomes unlocked.
+ *
+ * @mutex: pointer to the mutex to lock
+ * Return: 0 on success, in which case the lock is owned by the calling thread.
+ * != 0 otherwise (the lock is not owned by the calling thread).
+ */
+int uthread_mutex_lock(struct uthread_mutex *mutex);
+
+/**
+ * uthread_mutex_trylock() - lock a mutex if not currently locked
+ *
+ * Similar to uthread_mutex_lock() except return immediately if the mutex is
+ * locked already.
+ *
+ * @mutex: pointer to the mutex to lock
+ * Return: 0 on success, in which case the lock is owned by the calling thread.
+ * EBUSY if the mutex is already locked by another thread. Any other non-zero
+ * value on error.
+ */
+int uthread_mutex_trylock(struct uthread_mutex *mutex);
+
+/**
+ * uthread_mutex_unlock() - unlock a mutex
+ *
+ * The mutex is assumed to be owned by the calling thread on entry. On exit, it
+ * is unlocked.
+ *
+ * @mutex: pointer to the mutex to unlock
+ * Return: 0 on success, != 0 on error
+ */
+int uthread_mutex_unlock(struct uthread_mutex *mutex);
+
+#else
+
+static inline int uthread_create(struct uthread *uthr, void (*fn)(void *),
+ void *arg, size_t stack_sz,
+ unsigned int grp_id)
+{
+ fn(arg);
+ return 0;
+}
+
+static inline bool uthread_schedule(void)
+{
+ return false;
+}
+
+static inline unsigned int uthread_grp_new_id(void)
+{
+ return 0;
+}
+
+static inline bool uthread_grp_done(unsigned int grp_id)
+{
+ return true;
+}
+
+/* These are macros for convenience on the caller side */
+#define uthread_mutex_lock(_mutex) ({ 0; })
+#define uthread_mutex_trylock(_mutex) ({ 0 })
+#define uthread_mutex_unlock(_mutex) ({ 0; })
+
+#endif /* CONFIG_UTHREAD */
+#endif /* _UTHREAD_H_ */
diff --git a/lib/Kconfig b/lib/Kconfig
index ac34ec45bb1..b2aecd8a49e 100644
--- a/lib/Kconfig
+++ b/lib/Kconfig
@@ -1258,6 +1258,27 @@ config PHANDLE_CHECK_SEQ
enable this config option to distinguish them using
phandles in fdtdec_get_alias_seq() function.
+config UTHREAD
+ bool "Enable thread support"
+ depends on HAVE_INITJMP
+ help
+ Implement a simple form of cooperative multi-tasking based on
+ context-switching via initjmp(), setjmp() and longjmp(). The
+ uthread_ interface enables the main thread of execution to create
+ one or more secondary threads and schedule them until they all have
+ returned. At any point a thread may suspend its execution and
+ schedule another thread, which allows for the efficient multiplexing
+ of leghthy operations.
+
+config UTHREAD_STACK_SIZE
+ int "Default uthread stack size"
+ depends on UTHREAD
+ default 32768
+ help
+ The default stack size for uthreads. Each uthread has its own stack.
+ When the stack_sz argument to uthread_create() is zero then this
+ value is used.
+
endmenu
source "lib/fwu_updates/Kconfig"
diff --git a/lib/Makefile b/lib/Makefile
index 7178c5a3e3c..18ae0cd87bf 100644
--- a/lib/Makefile
+++ b/lib/Makefile
@@ -159,6 +159,8 @@ obj-$(CONFIG_LIB_ELF) += elf.o
obj-$(CONFIG_$(PHASE_)SEMIHOSTING) += semihosting.o
+obj-$(CONFIG_UTHREAD) += uthread.o
+
#
# Build a fast OID lookup registry from include/linux/oid_registry.h
#
diff --git a/lib/time.c b/lib/time.c
index d88edafb196..0e9b079f9cf 100644
--- a/lib/time.c
+++ b/lib/time.c
@@ -17,6 +17,7 @@
#include <asm/global_data.h>
#include <asm/io.h>
#include <linux/delay.h>
+#include <uthread.h>
#ifndef CFG_WD_PERIOD
# define CFG_WD_PERIOD (10 * 1000 * 1000) /* 10 seconds default */
@@ -197,7 +198,13 @@ void udelay(unsigned long usec)
do {
schedule();
kv = usec > CFG_WD_PERIOD ? CFG_WD_PERIOD : usec;
- __udelay(kv);
+ if (CONFIG_IS_ENABLED(UTHREAD)) {
+ ulong t0 = timer_get_us();
+ while (timer_get_us() - t0 < kv)
+ uthread_schedule();
+ } else {
+ __udelay(kv);
+ }
usec -= kv;
} while(usec);
}
diff --git a/lib/uthread.c b/lib/uthread.c
new file mode 100644
index 00000000000..062fca7d209
--- /dev/null
+++ b/lib/uthread.c
@@ -0,0 +1,165 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2021 Ahmad Fatoum, Pengutronix
+ * Copyright (C) 2025 Linaro Limited
+ *
+ * An implementation of cooperative multi-tasking inspired from barebox threads
+ * https://github.com/barebox/barebox/blob/master/common/bthread.c
+ */
+
+#include <compiler.h>
+#include <linux/errno.h>
+#include <linux/kernel.h>
+#include <linux/list.h>
+#include <malloc.h>
+#include <setjmp.h>
+#include <stdint.h>
+#include <uthread.h>
+
+static struct uthread main_thread = {
+ .list = LIST_HEAD_INIT(main_thread.list),
+};
+
+static struct uthread *current = &main_thread;
+
+/**
+ * uthread_trampoline() - Call the current thread's entry point then resume the
+ * main thread.
+ *
+ * This is a helper function which is used as the @func argument to the
+ * initjmp() function, and ultimately invoked via setjmp(). It does not return
+ * but instead longjmp()'s back to the main thread.
+ */
+static void __noreturn uthread_trampoline(void)
+{
+ struct uthread *curr = current;
+
+ curr->fn(curr->arg);
+ curr->done = true;
+ current = &main_thread;
+ longjmp(current->ctx, 1);
+ /* Not reached */
+ while (true)
+ ;
+}
+
+/**
+ * uthread_free() - Free memory used by a uthread object.
+ */
+static void uthread_free(struct uthread *uthread)
+{
+ if (!uthread)
+ return;
+ free(uthread->stack);
+ free(uthread);
+}
+
+int uthread_create(struct uthread *uthr, void (*fn)(void *), void *arg,
+ size_t stack_sz, unsigned int grp_id)
+{
+ bool user_allocated = false;
+
+ if (!stack_sz)
+ stack_sz = CONFIG_UTHREAD_STACK_SIZE;
+
+ if (uthr) {
+ user_allocated = true;
+ } else {
+ uthr = calloc(1, sizeof(*uthr));
+ if (!uthr)
+ return -1;
+ }
+
+ uthr->stack = memalign(16, stack_sz);
+ if (!uthr->stack)
+ goto err;
+
+ uthr->fn = fn;
+ uthr->arg = arg;
+ uthr->grp_id = grp_id;
+
+ list_add_tail(&uthr->list, &current->list);
+
+ initjmp(uthr->ctx, uthread_trampoline, uthr->stack, stack_sz);
+
+ return 0;
+err:
+ if (!user_allocated)
+ free(uthr);
+ return -1;
+}
+
+/**
+ * uthread_resume() - switch execution to a given thread
+ *
+ * @uthread: the thread object that should be resumed
+ */
+static void uthread_resume(struct uthread *uthread)
+{
+ if (!setjmp(current->ctx)) {
+ current = uthread;
+ longjmp(uthread->ctx, 1);
+ }
+}
+
+bool uthread_schedule(void)
+{
+ struct uthread *next;
+ struct uthread *tmp;
+
+ list_for_each_entry_safe(next, tmp, &current->list, list) {
+ if (!next->done) {
+ uthread_resume(next);
+ return true;
+ }
+ /* Found a 'done' thread, free its resources */
+ list_del(&next->list);
+ uthread_free(next);
+ }
+ return false;
+}
+
+unsigned int uthread_grp_new_id(void)
+{
+ static unsigned int id;
+
+ return ++id;
+}
+
+bool uthread_grp_done(unsigned int grp_id)
+{
+ struct uthread *next;
+
+ list_for_each_entry(next, &main_thread.list, list) {
+ if (next->grp_id == grp_id && !next->done)
+ return false;
+ }
+
+ return true;
+}
+
+int uthread_mutex_lock(struct uthread_mutex *mutex)
+{
+ while (mutex->state == UTHREAD_MUTEX_LOCKED)
+ uthread_schedule();
+
+ mutex->state = UTHREAD_MUTEX_LOCKED;
+ return 0;
+}
+
+int uthread_mutex_trylock(struct uthread_mutex *mutex)
+{
+ if (mutex->state == UTHREAD_MUTEX_UNLOCKED) {
+ mutex->state = UTHREAD_MUTEX_LOCKED;
+ return 0;
+ }
+
+ return -EBUSY;
+}
+
+int uthread_mutex_unlock(struct uthread_mutex *mutex)
+{
+ mutex->state = UTHREAD_MUTEX_UNLOCKED;
+
+ return 0;
+}
diff --git a/test/boot/bootdev.c b/test/boot/bootdev.c
index d5499918249..9af94786870 100644
--- a/test/boot/bootdev.c
+++ b/test/boot/bootdev.c
@@ -392,8 +392,7 @@ static int bootdev_test_hunter(struct unit_test_state *uts)
ut_assert_console_end();
ut_assertok(bootdev_hunt("usb1", false));
- ut_assert_nextline(
- "Bus usb@1: scanning bus usb@1 for devices... 5 USB Device(s) found");
+ ut_assert_skip_to_line("Bus usb@1: 5 USB Device(s) found");
ut_assert_console_end();
/* USB is 7th in the list, so bit 8 */
@@ -448,8 +447,7 @@ static int bootdev_test_cmd_hunt(struct unit_test_state *uts)
ut_assert_nextline("scanning bus for devices...");
ut_assert_skip_to_line("Hunting with: spi_flash");
ut_assert_nextline("Hunting with: usb");
- ut_assert_nextline(
- "Bus usb@1: scanning bus usb@1 for devices... 5 USB Device(s) found");
+ ut_assert_skip_to_line("Bus usb@1: 5 USB Device(s) found");
ut_assert_nextline("Hunting with: virtio");
ut_assert_console_end();
@@ -551,8 +549,7 @@ static int bootdev_test_hunt_prio(struct unit_test_state *uts)
ut_assertok(bootdev_hunt_prio(BOOTDEVP_5_SCAN_SLOW, true));
ut_assert_nextline("Hunting with: ide");
ut_assert_nextline("Hunting with: usb");
- ut_assert_nextline(
- "Bus usb@1: scanning bus usb@1 for devices... 5 USB Device(s) found");
+ ut_assert_skip_to_line("Bus usb@1: 5 USB Device(s) found");
ut_assert_console_end();
return 0;
@@ -604,7 +601,7 @@ static int bootdev_test_hunt_label(struct unit_test_state *uts)
ut_assertnonnull(dev);
ut_asserteq_str("usb_mass_storage.lun0.bootdev", dev->name);
ut_asserteq(BOOTFLOW_METHF_SINGLE_UCLASS, mflags);
- ut_assert_nextlinen("Bus usb@1: scanning bus usb@1");
+ ut_assert_nextline("Bus usb@1: 5 USB Device(s) found");
ut_assert_console_end();
return 0;
diff --git a/test/boot/bootflow.c b/test/boot/bootflow.c
index 5f9c037ff53..b261bd5f620 100644
--- a/test/boot/bootflow.c
+++ b/test/boot/bootflow.c
@@ -1290,7 +1290,7 @@ static int bootflow_efi(struct unit_test_state *uts)
ut_assertok(run_command("bootflow scan", 0));
ut_assert_skip_to_line(
- "Bus usb@1: scanning bus usb@1 for devices... 5 USB Device(s) found");
+ "Bus usb@1: 5 USB Device(s) found");
ut_assertok(run_command("bootflow list", 0));
diff --git a/test/cmd/Makefile b/test/cmd/Makefile
index 8596c5ad753..595e4cfcada 100644
--- a/test/cmd/Makefile
+++ b/test/cmd/Makefile
@@ -39,3 +39,4 @@ obj-$(CONFIG_CMD_WGET) += wget.o
endif
obj-$(CONFIG_ARM_FFA_TRANSPORT) += armffa.o
endif
+obj-$(CONFIG_CMD_SPAWN) += spawn.o
diff --git a/test/cmd/spawn.c b/test/cmd/spawn.c
new file mode 100644
index 00000000000..8f48f5ee25c
--- /dev/null
+++ b/test/cmd/spawn.c
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Tests for spawn and wait commands
+ *
+ * Copyright 2025, Linaro Ltd.
+ */
+
+#include <command.h>
+#include <test/cmd.h>
+#include <test/test.h>
+#include <test/ut.h>
+
+static int test_cmd_spawn(struct unit_test_state *uts)
+{
+ ut_assertok(run_command("wait; spawn sleep 2; setenv j ${job_id}; "
+ "spawn setenv spawned true; "
+ "setenv jj ${job_id}; wait; "
+ "echo ${j} ${jj} ${spawned}", 0));
+ console_record_readline(uts->actual_str, sizeof(uts->actual_str));
+ ut_asserteq_ptr(uts->actual_str,
+ strstr(uts->actual_str, "1 2 true"));
+
+ ut_assertok(run_command("spawn true; wait; setenv t $?; spawn false; "
+ "wait; setenv f $?; wait; echo $t $f $?", 0));
+ console_record_readline(uts->actual_str, sizeof(uts->actual_str));
+ ut_asserteq_ptr(uts->actual_str,
+ strstr(uts->actual_str, "0 1 0"));
+ ut_assert_console_end();
+
+ return 0;
+}
+CMD_TEST(test_cmd_spawn, UTF_CONSOLE);
diff --git a/test/lib/Makefile b/test/lib/Makefile
index 97ab71ba5d1..d620510f998 100644
--- a/test/lib/Makefile
+++ b/test/lib/Makefile
@@ -15,6 +15,7 @@ obj-$(CONFIG_SANDBOX) += kconfig.o
obj-y += lmb.o
obj-$(CONFIG_HAVE_SETJMP) += longjmp.o
obj-$(CONFIG_SANDBOX) += membuf.o
+obj-$(CONFIG_HAVE_INITJMP) += initjmp.o
obj-$(CONFIG_CONSOLE_RECORD) += test_print.o
obj-$(CONFIG_SSCANF) += sscanf.o
obj-$(CONFIG_$(PHASE_)STRTO) += str.o
@@ -31,6 +32,7 @@ obj-$(CONFIG_CRC8) += test_crc8.o
obj-$(CONFIG_UT_LIB_CRYPT) += test_crypt.o
obj-$(CONFIG_UT_TIME) += time.o
obj-$(CONFIG_$(PHASE_)UT_UNICODE) += unicode.o
+obj-$(CONFIG_UTHREAD) += uthread.o
obj-$(CONFIG_LIB_UUID) += uuid.o
else
obj-$(CONFIG_SANDBOX) += kconfig_spl.o
diff --git a/test/lib/initjmp.c b/test/lib/initjmp.c
new file mode 100644
index 00000000000..5b4b50b3f0f
--- /dev/null
+++ b/test/lib/initjmp.c
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright 2025 Linaro Limited
+ *
+ * Unit test for initjmp()
+ */
+
+#include <compiler.h>
+#include <setjmp.h>
+#include <stdbool.h>
+#include <test/lib.h>
+#include <test/ut.h>
+#include <vsprintf.h>
+
+static bool ep_entered;
+static jmp_buf return_buf;
+
+static void __noreturn entrypoint(void)
+{
+ ep_entered = true;
+
+ /* Jump back to the main routine */
+ longjmp(return_buf, 1);
+
+ /* Not reached */
+ panic("longjmp failed\n");
+}
+
+static int lib_initjmp(struct unit_test_state *uts)
+{
+ int ret;
+ void *stack;
+ jmp_buf buf;
+ /* Arbitrary but smaller values (< page size?) fail on SANDBOX */
+ size_t stack_sz = 8192;
+
+ (void)entrypoint;
+
+ ep_entered = false;
+
+ stack = malloc(stack_sz);
+ ut_assertnonnull(stack);
+
+ /*
+ * Prepare return_buf so that entrypoint may jump back just after the
+ * if()
+ */
+ if (!setjmp(return_buf)) {
+ /* return_buf initialized, entrypoint not yet called */
+
+ /*
+ * Prepare another jump buffer to jump into entrypoint with the
+ * given stack
+ */
+ ret = initjmp(buf, entrypoint, stack, stack_sz);
+ ut_assertok(ret);
+
+ /* Jump into entrypoint */
+ longjmp(buf, 1);
+ /*
+ * Not reached since entrypoint is expected to branch after
+ * the if()
+ */
+ ut_assert(false);
+ }
+
+ ut_assert(ep_entered);
+
+ free(stack);
+
+ return 0;
+}
+LIB_TEST(lib_initjmp, 0);
diff --git a/test/lib/uthread.c b/test/lib/uthread.c
new file mode 100644
index 00000000000..10a94d1c560
--- /dev/null
+++ b/test/lib/uthread.c
@@ -0,0 +1,146 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Copyright 2025 Linaro Limited
+ *
+ * Unit test for uthread
+ */
+
+#include <stdbool.h>
+#include <test/lib.h>
+#include <test/ut.h>
+#include <uthread.h>
+
+static int count;
+
+/* A thread entry point */
+static void worker(void *arg)
+{
+ int loops = (int)(unsigned long)arg;
+ int i;
+
+ for (i = 0; i < loops; i++) {
+ count++;
+ uthread_schedule();
+ }
+}
+
+/*
+ * uthread() - testing the uthread API
+ *
+ * This function creates two threads with the same entry point. The first one
+ * receives 5 as an argument, the second one receives 10. The number indicates
+ * the number of time the worker thread should loop on uthread_schedule()
+ * before returning. The workers increment a global counter each time they loop.
+ * As a result the main thread knows how many times it should call
+ * uthread_schedule() to let the two threads proceed, and it also knows which
+ * value the counter should have at any moment.
+ */
+static int uthread(struct unit_test_state *uts)
+{
+ int i;
+ int id1, id2;
+
+ count = 0;
+ id1 = uthread_grp_new_id();
+ ut_assert(id1 != 0);
+ id2 = uthread_grp_new_id();
+ ut_assert(id2 != 0);
+ ut_assert(id1 != id2);
+ ut_assertok(uthread_create(NULL, worker, (void *)5, 0, id1));
+ ut_assertok(uthread_create(NULL, worker, (void *)10, 0, 0));
+ /*
+ * The first call is expected to schedule the first worker, which will
+ * schedule the second one, which will schedule back to the main thread
+ * (here). Therefore count should be 2.
+ */
+ ut_assert(uthread_schedule());
+ ut_asserteq(2, count);
+ ut_assert(!uthread_grp_done(id1));
+ /* Four more calls should bring the count to 10 */
+ for (i = 0; i < 4; i++) {
+ ut_assert(!uthread_grp_done(id1));
+ ut_assert(uthread_schedule());
+ }
+ ut_asserteq(10, count);
+ /* This one allows the first worker to exit */
+ ut_assert(uthread_schedule());
+ /* At this point there should be no runnable thread in group 'id1' */
+ ut_assert(uthread_grp_done(id1));
+ /* Five more calls for the second worker to finish incrementing */
+ for (i = 0; i < 5; i++)
+ ut_assert(uthread_schedule());
+ ut_asserteq(15, count);
+ /* Plus one call to let the second worker return from its entry point */
+ ut_assert(uthread_schedule());
+ /* Now both tasks should be done, schedule should return false */
+ ut_assert(!uthread_schedule());
+
+ return 0;
+}
+LIB_TEST(uthread, 0);
+
+struct mw_args {
+ struct unit_test_state *uts;
+ struct uthread_mutex *m;
+ int flag;
+};
+
+static int mutex_worker_ret;
+
+static int _mutex_worker(struct mw_args *args)
+{
+ struct unit_test_state *uts = args->uts;
+
+ ut_asserteq(-EBUSY, uthread_mutex_trylock(args->m));
+ ut_assertok(uthread_mutex_lock(args->m));
+ args->flag = 1;
+ ut_assertok(uthread_mutex_unlock(args->m));
+
+ return 0;
+}
+
+static void mutex_worker(void *arg)
+{
+ mutex_worker_ret = _mutex_worker((struct mw_args *)arg);
+}
+
+/*
+ * thread_mutex() - testing uthread mutex operations
+ *
+ */
+static int uthread_mutex(struct unit_test_state *uts)
+{
+ struct uthread_mutex m = UTHREAD_MUTEX_INITIALIZER;
+ struct mw_args args = { .uts = uts, .m = &m, .flag = 0 };
+ int id;
+ int i;
+
+ id = uthread_grp_new_id();
+ ut_assert(id != 0);
+ /* Take the mutex */
+ ut_assertok(uthread_mutex_lock(&m));
+ /* Start a thread */
+ ut_assertok(uthread_create(NULL, mutex_worker, (void *)&args, 0,
+ id));
+ /* Let the thread run for a bit */
+ for (i = 0; i < 100; i++)
+ ut_assert(uthread_schedule());
+ /* Thread should not have set the flag due to the mutex */
+ ut_asserteq(0, args.flag);
+ /* Release the mutex */
+ ut_assertok(uthread_mutex_unlock(&m));
+ /* Schedule the thread until it is done */
+ while (uthread_schedule())
+ ;
+ /* Now the flag should be set */
+ ut_asserteq(1, args.flag);
+ /* And the mutex should be available */
+ ut_assertok(uthread_mutex_trylock(&m));
+ ut_assertok(uthread_mutex_unlock(&m));
+
+ /* Of course no error are expected from the thread routine */
+ ut_assertok(mutex_worker_ret);
+
+ return 0;
+}
+LIB_TEST(uthread_mutex, 0);