Skip to content

Conversation

@kv2019i
Copy link
Collaborator

@kv2019i kv2019i commented Feb 9, 2026

A small set of patches that allow granting access to all DAIs and DMAs to a user-space thread, and this is case, the LL scheduler threads when run in user-space. This PR uses the existing helper code in SOF that allow to enumerate the devices in a platform-agnostic manner, so this PR works for all SOF build targerts.

@kv2019i
Copy link
Collaborator Author

kv2019i commented Feb 9, 2026

Looks like this when running the "userspace_ll" ztest on a Intel PTL system:

Running TESTSUITE userspace_ll
===================================================================
START - ll_task_test
[    0.000095] <dbg> ll_schedule: zephyr_ll_task_init: ll-scheduler task 0x400de000 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 0xa02422c0 0xa00d38e4U priority 0 flags 0x0
[    0.000095] <dbg> ll_schedule: zephyr_domain_register_user: entry
thread_alloc user, heap 0x400e09dc, sys-heap 0x400e09dc
thread_alloc user, heap 0x400e09dc, sys-heap 0x400e09dc
[    0.000095] <dbg> userspace_helper: user_grant_dai_access_all: Granted DAI access to thread 0x400d6640 for 45 devices
[    0.000095] <dbg> userspace_helper: user_grant_dma_access_all: Granted DMA device access: 0 dma@72c00 to thread 0x400d6640
[    0.000095] <dbg> userspace_helper: user_grant_dma_access_all: Granted DMA device access: 1 dma@72800 to thread 0x400d6640
[    0.000095] <dbg> userspace_helper: user_grant_dma_access_all: Granted DMA device access: 2 dma@79800 to thread 0x400d6640
[    0.000095] <dbg> userspace_helper: user_grant_dma_access_all: Granted DMA device access: 3 dma@79400 to thread 0x400d6640
[    0.000095] <dbg> ll_schedule: zephyr_domain_register_user: granted LL access to thread 0x400d6640 (core 0)

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

Adds user-space support for LL scheduler threads by granting them access to all platform DAIs and DMAs, using existing SOF device enumeration helpers.

Changes:

  • Add helpers to grant a user thread access to all DAI/DMA Zephyr devices
  • Expose DMA/DAI device list APIs for platform-agnostic enumeration
  • Integrate the new access grants into Zephyr LL domain thread registration

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
zephyr/lib/userspace_helper.c Adds userspace helpers to grant access to all DAIs/DMAs and introduces logging
zephyr/lib/dma.c Adds API to enumerate DMA Zephyr devices
zephyr/include/sof/lib/dma.h Declares the new DMA device list API
zephyr/include/rtos/userspace_helper.h Declares new userspace grant helper APIs
src/schedule/zephyr_domain.c Grants DAI/DMA access to LL user threads during registration
src/lib/dai.c Adds API to enumerate DAI Zephyr devices
src/include/sof/lib/dai-zephyr.h Declares the new DAI device list API

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

Comment on lines +96 to 99
#ifdef CONFIG_SOF_USERSPACE_LL

int user_access_to_mailbox(struct k_mem_domain *domain, k_tid_t thread_id)
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

user_access_to_mailbox() is now compiled only when CONFIG_SOF_USERSPACE_LL is enabled, but it appears to be a general userspace helper (and is still declared under #if CONFIG_USERSPACE in the header). This creates a concrete risk of link failures or missing functionality in CONFIG_USERSPACE=y builds that don’t enable CONFIG_SOF_USERSPACE_LL. Consider moving the #ifdef CONFIG_SOF_USERSPACE_LL guard so it only wraps the new DAI/DMA grant helpers (not user_access_to_mailbox()), or alternatively provide stubs / adjust header declarations so the build remains consistent across configurations.

Copilot uses AI. Check for mistakes.
Comment on lines 338 to +340
k_thread_access_grant(thread, dt->sem, domain->lock, zephyr_domain->timer);
user_grant_dai_access_all(thread);
user_grant_dma_access_all(thread);
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

These calls are unconditional, but the implementations in userspace_helper.c are built only under CONFIG_USERSPACE && CONFIG_SOF_USERSPACE_LL (and currently the header declarations are not similarly guarded). This can lead to unresolved symbols in configurations where this file is compiled but CONFIG_SOF_USERSPACE_LL (or even CONFIG_USERSPACE) is off. Guard these calls with the same Kconfig option(s), and/or provide static inline no-op stubs in the header for non-enabled configurations.

Suggested change
k_thread_access_grant(thread, dt->sem, domain->lock, zephyr_domain->timer);
user_grant_dai_access_all(thread);
user_grant_dma_access_all(thread);
k_thread_access_grant(thread, dt->sem, domain->lock, zephyr_domain->timer);
#if CONFIG_USERSPACE && CONFIG_SOF_USERSPACE_LL
user_grant_dai_access_all(thread);
user_grant_dma_access_all(thread);
#endif

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

Choose a reason for hiding this comment

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

This is a valid concern, but this code is already guarded by CONFIG_SOF_USERSPACE_LL and that depends on CONFIG_USERSPACE, so this is sufficient.

*
* @param thread user-space thread for which DAI access is granted
*/
void user_grant_dai_access_all(struct k_thread *thread);
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

These APIs are declared for all CONFIG_USERSPACE builds, but the implementations are under CONFIG_SOF_USERSPACE_LL in the .c file. To keep the public API consistent and avoid configuration-dependent link issues, either (1) wrap these declarations with #ifdef CONFIG_SOF_USERSPACE_LL to match the implementation, or (2) provide static inline no-op stubs when CONFIG_SOF_USERSPACE_LL is disabled.

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

Choose a reason for hiding this comment

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

This is on purpose. One will get linker errors if trying to use these in the wrong config and this should be sufficient.

*
* @param thread user-space thread for which DMA access is granted
*/
void user_grant_dma_access_all(struct k_thread *thread);
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

These APIs are declared for all CONFIG_USERSPACE builds, but the implementations are under CONFIG_SOF_USERSPACE_LL in the .c file. To keep the public API consistent and avoid configuration-dependent link issues, either (1) wrap these declarations with #ifdef CONFIG_SOF_USERSPACE_LL to match the implementation, or (2) provide static inline no-op stubs when CONFIG_SOF_USERSPACE_LL is disabled.

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

Choose a reason for hiding this comment

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

This is on purpose. One will get linker errors if trying to use these in the wrong config and this should be sufficient.

* \param count Pointer to store the number of devices in the list.
* \return Pointer to the array of device pointers.
*/
const struct device **dma_get_device_list(size_t *count);
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

Returning const struct device ** allows callers to modify the returned array contents (even if the devices themselves are const). Since this API is exposing a shared internal list, it should return an immutable list type: const struct device * const *. Same applies to dai_get_device_list() for consistency/safety.

Suggested change
const struct device **dma_get_device_list(size_t *count);
const struct device * const *dma_get_device_list(size_t *count);

Copilot uses AI. Check for mistakes.
zephyr/lib/dma.c Outdated
Comment on lines 222 to 225
const struct device **dma_get_device_list(size_t *count)
{
static const struct device *device_list[ARRAY_SIZE(dma)];
int i;
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

dma_get_device_list() writes to a function-static array on every call. If multiple threads can call this concurrently, this introduces a data race on device_list[]. To avoid racy writes, populate the list once during a single-threaded init phase (e.g., in dmac_init()), or implement a one-time initialization guard that does not perform concurrent writes.

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

Choose a reason for hiding this comment

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

Will modify for V2

zephyr/lib/dma.c Outdated
Comment on lines 225 to 232
int i;

if (count)
*count = ARRAY_SIZE(dma);

for (i = 0; i < ARRAY_SIZE(dma); i++)
device_list[i] = dma[i].z_dev;

Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

dma_get_device_list() writes to a function-static array on every call. If multiple threads can call this concurrently, this introduces a data race on device_list[]. To avoid racy writes, populate the list once during a single-threaded init phase (e.g., in dmac_init()), or implement a one-time initialization guard that does not perform concurrent writes.

Suggested change
int i;
if (count)
*count = ARRAY_SIZE(dma);
for (i = 0; i < ARRAY_SIZE(dma); i++)
device_list[i] = dma[i].z_dev;
static struct k_spinlock device_list_lock;
static int device_list_initialized;
int i;
k_spinlock_key_t key;
if (count)
*count = ARRAY_SIZE(dma);
key = k_spin_lock(&device_list_lock);
if (!device_list_initialized) {
for (i = 0; i < ARRAY_SIZE(dma); i++)
device_list[i] = dma[i].z_dev;
device_list_initialized = 1;
}
k_spin_unlock(&device_list_lock, key);

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

Choose a reason for hiding this comment

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

Will modify for V2. Above is way too much code for the need.

Comment on lines 160 to 166
for (i = 0; i < count; i++) {
k_thread_access_grant(thread, devices[i]);
LOG_DBG("Granted DMA device access: %d %s to thread %p",
i, devices[i]->name, thread);
}
}

Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

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

Logging once per DMA device can become noisy and adds overhead (even at LOG_DBG, it still does formatting work depending on Zephyr logging configuration). Consider logging a single summary line (as done for DAI) or gating per-device debug logs behind a more verbose config.

Suggested change
for (i = 0; i < count; i++) {
k_thread_access_grant(thread, devices[i]);
LOG_DBG("Granted DMA device access: %d %s to thread %p",
i, devices[i]->name, thread);
}
}
for (i = 0; i < count; i++)
k_thread_access_grant(thread, devices[i]);
LOG_DBG("Granted DMA access to thread %p for %zu devices", thread, count);
}

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

Choose a reason for hiding this comment

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

This is tuned for the common case. Most SOF targets have a handful of DMAs, so the logging is not too much (and detail is useful). With DAIs, we have many targets where amount of logging is too much, so I went with a summary instead.

Copy link
Member

@lgirdwood lgirdwood left a comment

Choose a reason for hiding this comment

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

Nothing major from me, just one question.

src/lib/dai.c Outdated

const struct device **dai_get_device_list(size_t *count)
{
if (count)
Copy link
Member

Choose a reason for hiding this comment

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

Seems count is mandatory ? assert ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

True, let me turn this to an assert

@softwarecki
Copy link
Collaborator

The static array allocated inside dma_get_device_list and returning a pointer to it feels a bit unusual. I assume the intention was to mirror the interface of dai_get_device_list, but I'm wondering if there is a cleaner option.

Both of these functions could instead fill a buffer provided by the caller or invoke a callback for each element. I expect this function will be used more in the future, so regenerating the list on every call seems unnecessary. Keeping a duplicated list permanently also wastes memory. The current approach ends up combining both drawbacks.

I think that the best option would be to drop the internal list entirely and use sof->dma_info in user_grant_dma_access_all. If we really need to keep the static list, it should probably be built once in dmac_init rather than inside the getter.

zephyr/lib/dma.c Outdated

const struct device **dma_get_device_list(size_t *count)
{
static const struct device *device_list[ARRAY_SIZE(dma)];
Copy link
Collaborator

Choose a reason for hiding this comment

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

yeah, maybe at least a comment why this doesn't have to be protected, or maybe it does? Can we use a caller-supplied array instead?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ok, let me see how to make this cleaner for V2 given copilot, you @lyakh and @softwarecki all disliked the implementation. Maybe I can add a cold_rodata array for this information, so we don't waste any SRAM for this info and minimize cost in code size.

Copy link
Collaborator Author

@kv2019i kv2019i left a comment

Choose a reason for hiding this comment

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

Thank you for reviews, will address in V2.

src/lib/dai.c Outdated

const struct device **dai_get_device_list(size_t *count)
{
if (count)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

True, let me turn this to an assert

Comment on lines 338 to +340
k_thread_access_grant(thread, dt->sem, domain->lock, zephyr_domain->timer);
user_grant_dai_access_all(thread);
user_grant_dma_access_all(thread);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is a valid concern, but this code is already guarded by CONFIG_SOF_USERSPACE_LL and that depends on CONFIG_USERSPACE, so this is sufficient.

*
* @param thread user-space thread for which DAI access is granted
*/
void user_grant_dai_access_all(struct k_thread *thread);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is on purpose. One will get linker errors if trying to use these in the wrong config and this should be sufficient.

*
* @param thread user-space thread for which DMA access is granted
*/
void user_grant_dma_access_all(struct k_thread *thread);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is on purpose. One will get linker errors if trying to use these in the wrong config and this should be sufficient.

zephyr/lib/dma.c Outdated

const struct device **dma_get_device_list(size_t *count)
{
static const struct device *device_list[ARRAY_SIZE(dma)];
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ok, let me see how to make this cleaner for V2 given copilot, you @lyakh and @softwarecki all disliked the implementation. Maybe I can add a cold_rodata array for this information, so we don't waste any SRAM for this info and minimize cost in code size.

zephyr/lib/dma.c Outdated
Comment on lines 222 to 225
const struct device **dma_get_device_list(size_t *count)
{
static const struct device *device_list[ARRAY_SIZE(dma)];
int i;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Will modify for V2

zephyr/lib/dma.c Outdated
Comment on lines 225 to 232
int i;

if (count)
*count = ARRAY_SIZE(dma);

for (i = 0; i < ARRAY_SIZE(dma); i++)
device_list[i] = dma[i].z_dev;

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Will modify for V2. Above is way too much code for the need.

Comment on lines 160 to 166
for (i = 0; i < count; i++) {
k_thread_access_grant(thread, devices[i]);
LOG_DBG("Granted DMA device access: %d %s to thread %p",
i, devices[i]->name, thread);
}
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is tuned for the common case. Most SOF targets have a handful of DMAs, so the logging is not too much (and detail is useful). With DAIs, we have many targets where amount of logging is too much, so I went with a summary instead.

Add helper functions to provide a list of all Zephyr devices
and to grant a user thread access to them.

Signed-off-by: Kai Vehmanen <kai.vehmanen@linux.intel.com>
Add helper function to grant a user thread access to all system
DMA devices.

Signed-off-by: Kai Vehmanen <kai.vehmanen@linux.intel.com>
If the low-latency domain tasks are run in user-space, grant the LL
threads access to all DAI and DMA devices.

In future, there may be need to filter access permissions more, and/or
have variation between different build targets, but for now the user LL
thread will have access to the same devices as it does in kernel space.

Note that access to memory is separately controlled and in addition
to having access to a DMA device, user-space thread also needs access
to the memory before it can do DMA transactions.

Signed-off-by: Kai Vehmanen <kai.vehmanen@linux.intel.com>
Now that we have CONFIG_SOF_USERSPACE_LL, use it to conditionally
drop some of the thread access helpers. Access to mailbox, DAIs
and DMAs is currently only needed if LL thread is run in user-space.
Leave these out if SOF is built for LL thread scheduling in kernel
space.

Signed-off-by: Kai Vehmanen <kai.vehmanen@linux.intel.com>
@kv2019i kv2019i force-pushed the 202602-ll-sched-daidma-access branch from b18e8ca to a153d34 Compare February 10, 2026 15:59
@kv2019i
Copy link
Collaborator Author

kv2019i commented Feb 10, 2026

V2 pushed:

  • came up with better solution to enumerate DMA Zephyr devices, no need to add a new function to (avoids the problem of how to build the z_dev only array); this was raised by multiple reviewers
  • added assert (comment from @lgirdwood )
  • rebased as PR was no longer mergeable

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.

Looks much better now. The code is cleaner, easier to read, and its overall size is reduced. Good work!

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.

4 participants