Skip to content

Conversation

@kv2019i
Copy link
Collaborator

@kv2019i kv2019i commented Jan 29, 2026

Series that starts building up support for running SOF LL tasks in user-space (on platforms supporting Zephyr user-space). We already have support for DP tasks, so with both LL and DP supported, in theory all audio can be moved to user-space and run in separate memory space. This will isolate audio code from direct hardware access, protect kernel memory and device driver state.

This PR contains initial support for LL scheduler and adds a separate test case to mimic usage of SOF audio pipeline, without yet bringing in any audio dependencies.

The telemetry infra is calling privileged timer functions, so
if the Low-Latency tasks are run in user-space, telemetry must
be disabled.

Signed-off-by: Kai Vehmanen <[email protected]>
The load tracking for Low-Latency tasks depends on low-overhead
access to cycle counter (e.g. CCOUNT on xtensa), which is not
currently available from user-space tasks. Add a dependency to
ensure the LL stats can only be enabled if LL tasks are run in
kernel mode.

Signed-off-by: Kai Vehmanen <[email protected]>
Add option to build SOF with support for running LL scheduler
in user-space. This commit adds initial support in the scheduler
and does not yet allow to run full SOF application using the new
scheduler configuration, but has enough functionality to run
scheduler level tests.

No functional change to default build configuration where LL
scheduler is run in kernel mode, or to platforms with no userspace
support.

Signed-off-by: Kai Vehmanen <[email protected]>
Add a test case to run tasks with low-latency (LL) scheduler in
user-space. The test does not yet use any audio pipeline functionality,
but uses similar interfaces towards the SOF scheduler interface.

Signed-off-by: Kai Vehmanen <[email protected]>
There are multiple style variants used in SOF for CMakeLists.txt,
but this file now contains multiple variants in the same file. Fix
this and align style to Zephyr style (2 space for indent, no tabs,
no space before opening brackets).

Signed-off-by: Kai Vehmanen <[email protected]>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds initial support for running SOF Low-Latency (LL) scheduler tasks in Zephyr user-space, providing memory protection and isolation between audio code and kernel resources.

Changes:

  • Adds user-space LL scheduler support with dedicated memory domains and heap management
  • Replaces spinlocks with mutexes for user-space compatibility
  • Introduces test case to validate LL task creation and execution in user-space mode

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
zephyr/test/userspace/test_ll_task.c New test case validating user-space LL scheduler functionality with task lifecycle management
zephyr/test/userspace/README.md Documentation update describing new LL scheduler test
zephyr/test/CMakeLists.txt Build configuration to include LL task test when CONFIG_SOF_USERSPACE_LL is enabled
zephyr/Kconfig New CONFIG_SOF_USERSPACE_LL option for enabling user-space LL pipelines
src/schedule/zephyr_ll.c Core LL scheduler implementation modified to support user-space execution with dynamic memory allocation
src/schedule/zephyr_domain.c Domain thread management updated for user-space with mutex-based synchronization
src/schedule/Kconfig Statistics logging disabled for user-space LL scheduler
src/init/init.c Initialization hook for user-space LL resources
src/include/sof/schedule/ll_schedule_domain.h Header updates exposing user-space LL APIs and mutex-based locking
src/include/sof/schedule/ll_schedule.h API declarations for user-space LL heap and memory domain management
src/debug/telemetry/Kconfig Telemetry disabled when user-space LL is enabled

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- Test Zephyr DAI interface, together with SOF DMA
wrapper from a user thread. Mimics the call flows done in
sof/src/audio/dai-zephyr.c. Use cavstool.py as host runner.
- test_ll_test.c
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected spelling of 'test_ll_test.c' to 'test_ll_task.c'.

Suggested change
- test_ll_test.c
- test_ll_task.c

Copilot uses AI. Check for mistakes.
sch->lock = k_object_alloc(K_OBJ_MUTEX);
if (!sch->lock) {
tr_err(&ll_tr, "mutex allocation failed");
sof_heap_free(sch->heap, sch);
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using sch->heap after it should be initialized but may not be. The heap field is only set in the CONFIG_SOF_USERSPACE_LL branch, but this error path is also within that branch before heap assignment at line 611. Use the local heap variable instead.

Suggested change
sof_heap_free(sch->heap, sch);
sof_heap_free(heap, sch);

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sch_heap isn't set yet

Comment on lines 484 to +486
ll_sch_domain_set_pdata(domain, zephyr_domain);

struct zephyr_domain_thread *dt = zephyr_domain->domain_thread + cpu_get_id();
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable declaration should be at the beginning of the function or block. Move this declaration to the top of the function for consistency with C89/C90 style if required by the project, or to improve readability.

Suggested change
ll_sch_domain_set_pdata(domain, zephyr_domain);
struct zephyr_domain_thread *dt = zephyr_domain->domain_thread + cpu_get_id();
struct zephyr_domain_thread *dt;
ll_sch_domain_set_pdata(domain, zephyr_domain);
dt = zephyr_domain->domain_thread + cpu_get_id();

Copilot uses AI. Check for mistakes.
Comment on lines 283 to 285
#define timer_domain_init(timer, clk) zephyr_domain_init(clk)
k_tid_t zephyr_domain_thread_tid(struct ll_schedule_domain *domain);
#endif
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declaration of zephyr_domain_thread_tid is placed outside the CONFIG_SOF_USERSPACE_LL guard (lines 103-109) where it logically belongs based on its implementation. This function is only implemented when CONFIG_SOF_USERSPACE_LL is enabled, so its declaration should be within the same conditional block to prevent link errors.

Suggested change
#define timer_domain_init(timer, clk) zephyr_domain_init(clk)
k_tid_t zephyr_domain_thread_tid(struct ll_schedule_domain *domain);
#endif
#define timer_domain_init(timer, clk) zephyr_domain_init(clk)
#if CONFIG_SOF_USERSPACE_LL
k_tid_t zephyr_domain_thread_tid(struct ll_schedule_domain *domain);
#endif
#endif

Copilot uses AI. Check for mistakes.
@kv2019i
Copy link
Collaborator Author

kv2019i commented Jan 29, 2026

Example test run (on Intel PTL):

[    0.000000] <inf> init: print_version_banner: FW ABI 0x301d001 DBG ABI 0x5003000 tags SOF:v2.14-pre-rc-386-g773835cd8fcd zephyr:v4.3.0-4334-gc1a2b3be459d src hash 0xeab6f675 (ref hash 0xeab6f675)
[    0.000000] <dbg> ll_schedule: zephyr_ll_heap_init: init ll heap 0xa0239000, size 94208 (cached)
[    0.000000] <dbg> ll_schedule: zephyr_ll_heap_init: init ll heap 0x40239000, size 94208 (uncached)
[    0.000000] <dbg> ll_schedule: zephyr_ll_scheduler_init: init on core 0

[    0.000000] <dbg> ll_schedule: zephyr_ll_scheduler_init: ll-scheduler init done, sch 0xa02391c0 sch->lock 0x400cd4f8
[    0.000000] <dbg> ll_schedule: zephyr_ll_task_init: ll-scheduler task 0xa01aaa00 init
[    0.000000] <inf> ipc: ipc_init: SOF_BOOT_TEST_STANDALONE, disabling IPC.
*** Booting Zephyr OS build v4.3.0-4334-gc1a2b3be459d ***
===================================================================
Running TESTSUITE userspace_ll
===================================================================
START - ll_task_test
[    0.000095] <dbg> ll_schedule: zephyr_ll_task_init: ll-scheduler task 0x400d5000 init
[    0.000095] <inf> sof_boot_test: ll_task_test: task init done
[    0.000095] <inf> ll_schedule: zephyr_ll_task_schedule_common: task add 0xa02392c0 0xa00ca950U priority 0 flags 0x0
[    0.000095] <dbg> ll_schedule: zephyr_domain_register: entry
[    0.000095] <dbg> ll_schedule: zephyr_domain_register: Grant access to 0x400cd4b8 (core 0, thread 0x400cd540)
[    0.000095] <dbg> ll_schedule: zephyr_domain_register: Added access to 0x40239100
[    0.000095] <inf> ll_schedule: zephyr_domain_register: zephyr_domain_register domain->type 1 domain->clk 0 domain->ticks_per_ms 38400 period 1000
[    0.000095] <dbg> ll_schedule: zephyr_domain_thread_tid: entry
[    0.000095] <dbg> ll_schedule: zephyr_domain_thread_tid: entry
[    0.000095] <dbg> ll_schedule: zephyr_domain_thread_tid: entry
[    0.000095] <dbg> ll_schedule: zephyr_ll_task_schedule_common: granting access to lock 0x400cd4f8 for thread 0x400cd540
[    0.000095] <dbg> ll_schedule: zephyr_domain_thread_tid: entry
[    0.000095] <dbg> ll_schedule: zephyr_ll_task_schedule_common: granting access to domain lock 0x40239090 for thread 0x400cd540
[    0.000095] <inf> sof_boot_test: ll_task_test: task scheduled and running
[    0.000095] <inf> ll_schedule: zephyr_domain_thread_fn: ll core 0 thread starting
[    0.000095] <dbg> ll_schedule: zephyr_ll_run: entry
[    0.000095] <inf> sof_boot_test: task_callback: entry
[    0.000095] <dbg> ll_schedule: zephyr_ll_run: entry
[    0.000095] <inf> sof_boot_test: task_callback: entry
[    0.000095] <dbg> ll_schedule: zephyr_ll_run: entry
[    0.000095] <inf> sof_boot_test: task_callback: entry
[    0.000096] <dbg> ll_schedule: zephyr_ll_run: entry
[    0.000096] <inf> sof_boot_test: task_callback: entry
[    0.000096] <inf> ll_schedule: zephyr_ll_task_done: task complete 0xa02392c0 0xa00ca950U
[    0.000096] <inf> ll_schedule: zephyr_ll_task_done: num_tasks 1 total_num_tasks 1
[    0.000096] <dbg> ll_schedule: zephyr_domain_unregister: entry
[    0.000096] <inf> ll_schedule: zephyr_domain_unregister: zephyr_domain_unregister domain->type 1 domain->clk 0
[    0.000096] <dbg> ll_schedule: zephyr_domain_unregister: exit
[    0.000098] <inf> sof_boot_test: ll_task_test: test complete
 PASS - ll_task_test in 0.011 seconds

#define schedule_task_init_ll zephyr_ll_task_init

struct task *zephyr_ll_task_alloc(void);
k_tid_t zephyr_ll_get_thread(int core);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: I think we use struct k_thread * mostly in SOF and it seems to "work better" with various simulation / testing builds, I was getting "undefined" errors when I tried to use k_tid_t


#if defined(__ZEPHYR__)
struct ll_schedule_domain *zephyr_ll_domain(void);
struct ll_schedule_domain *zephyr_domain_init(int clk);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

zephyr_ll_domain_init()?

struct k_mutex *lock; /**< standard lock */
#else
struct k_spinlock lock; /**< standard lock */
#endif
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: this change actually modifies the current behaviour already.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack @lyakh , discussed offline and I think I'll go back a step and keep the kernel LL build using spinlocks (and/or make it a separate PR).


#if defined(CONFIG_SOF_USERSPACE_LL)
/* Allocate mutex dynamically for userspace access */
domain->lock = k_object_alloc(K_OBJ_MUTEX);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this allocates cached?


#if CONFIG_SOF_USERSPACE_LL

k_tid_t zephyr_domain_thread_tid(struct ll_schedule_domain *domain)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

struct k_thread * maybe


list_init(&sch->tasks);
sch->ll_domain = domain;
sch->core = cpu_get_id();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

core

sch->lock = k_object_alloc(K_OBJ_MUTEX);
if (!sch->lock) {
tr_err(&ll_tr, "mutex allocation failed");
sof_heap_free(sch->heap, sch);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sch_heap isn't set yet

ZTEST(userspace_ll, ll_task_test)
{
ll_task_test();
ztest_test_pass();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually removed these from my tests, they're doing some long jumps... Are you sure you need this?

* SOF main has booted up and IPC handling is stopped.
* Run test suites with ztest_run_all.
*/
static int run_tests(void)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be good to have a similar test for DP


if (CONFIG_SOF_BOOT_TEST_STANDALONE AND CONFIG_SOF_USERSPACE_LL)
if(CONFIG_SOF_BOOT_TEST_STANDALONE AND CONFIG_SOF_USERSPACE_LL)
zephyr_library_sources(userspace/test_ll_task.c)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like aligning to use TABs instead would make the path smaller

Copy link
Collaborator

@softwarecki softwarecki left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First quick remarks. I still have 2 commits left to review... There is a lot of conditional code added here. Would it not be better to make this a separate scheduler? SOF already supports multiple different schedulers


#if defined(__ZEPHYR__) && CONFIG_SOF_USERSPACE_LL
domain = sof_heap_alloc(zephyr_ll_heap(), SOF_MEM_FLAG_USER | SOF_MEM_FLAG_COHERENT,
sizeof(*domain), sizeof(void *));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing memset

domain->lock = rzalloc(SOF_MEM_FLAG_KERNEL | SOF_MEM_FLAG_COHERENT, sizeof(*domain->lock));
#endif
if (!domain->lock) {
rfree(domain);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

heap_free(zephyr_ll_heap(), ... for CONFIG_SOF_USERSPACE_LL?

#endif /* CONFIG_SOF_USERSPACE_LL */

struct zephyr_domain_thread {
struct k_thread ll_thread;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we keep these static objects when CONFIG_SOF_USERSPACE_LL is not enabled? We could keep pointer fields that point to static objects, similar to the dp scheduler solution.

}

key = k_spin_lock(&domain->lock);
k_mutex_lock(domain->lock, K_FOREVER);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spinlock still in use without defined(__ZEPHYR__)

(void *)mem_partition.start,
heap->heap.init_bytes);

mem_partition.start = (uintptr_t)sys_cache_uncached_ptr_get(heap->heap.init_mem);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Zephyr maps cached and non-cached addresses when the double map config is enabled. Maybe it is worth check it here?

void zephyr_ll_resources_init(void)
{
k_mem_domain_init(&ll_mem_resources.mem_domain, 0, NULL);
k_mutex_init(&ll_mem_resources.lock);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static kobjects are initialized by default, so there is no need to initialize them manually.

zephyr_domain->timer = k_object_alloc(K_OBJ_TIMER);
if (!zephyr_domain->timer) {
tr_err(&ll_tr, "timer allocation failed");
rfree(zephyr_domain);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

heap_free(zephyr_ll_heap(), ...

/* Add zephyr_domain_ops to the memory domain for user thread access */
struct k_mem_partition ops_partition;

ops_partition.start = (uintptr_t)&zephyr_domain_ops;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partition size must be aligned to page size. Consider use APP_TASK_DATA in the zephyr_domain_ops declatarion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants