Skip to content

Commit 96573de

Browse files
committed
zephyr: test: userspace: add pipeline_two_components test
Add a new test to userspace_ll set that takes a step towards running full audio pipelines in user-space. The test creates a pipeline with two components (IPC4 host and DAI copiers), does pipeline prepare, one copy cycle in prepared state and tears down the pipeline. One user-space thread is created to manage the pipelines. This would be equivalent to user IPC handler thread. Another user-space thread is created for the LL scheduler. This test has some limitation, but is a useful test point to test resource allocations in pipeline, component and module adapter code. The code adds a reference test case where the same flow is fully run in kernel space. Signed-off-by: Kai Vehmanen <kai.vehmanen@linux.intel.com>
1 parent 03137b5 commit 96573de

1 file changed

Lines changed: 367 additions & 0 deletions

File tree

zephyr/test/userspace/test_ll_task.c

Lines changed: 367 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,27 @@
1414
#include <sof/schedule/ll_schedule.h>
1515
#include <sof/schedule/ll_schedule_domain.h>
1616
#include <sof/audio/pipeline.h>
17+
#include <sof/audio/component_ext.h>
18+
#include <sof/audio/buffer.h>
19+
#include <sof/ipc/common.h>
20+
#include <sof/ipc/topology.h>
1721
#include <rtos/task.h>
1822
#include <rtos/userspace_helper.h>
1923
#include <ipc4/fw_reg.h>
24+
#include <ipc4/module.h>
25+
#include <ipc4/gateway.h>
26+
#include <ipc4/header.h>
27+
#include <ipc4/base_fw_vendor.h>
28+
#include <module/ipc4/base-config.h>
29+
#include <rimage/sof/user/manifest.h>
2030

2131
#include <zephyr/kernel.h>
2232
#include <zephyr/ztest.h>
2333
#include <zephyr/logging/log.h>
2434
#include <zephyr/app_memory/app_memdomain.h>
2535

2636
#include <stddef.h> /* offsetof() */
37+
#include <string.h>
2738

2839
LOG_MODULE_DECLARE(sof_boot_test, LOG_LEVEL_DBG);
2940

@@ -36,6 +47,12 @@ K_APPMEM_PARTITION_DEFINE(userspace_ll_part);
3647
/* Global variable for test runs counter, accessible from user-space */
3748
K_APP_BMEM(userspace_ll_part) static int test_runs;
3849

50+
/* User-space thread for pipeline_two_components test */
51+
#define PPL_USER_STACKSIZE 4096
52+
53+
static struct k_thread ppl_user_thread;
54+
static K_THREAD_STACK_DEFINE(ppl_user_stack, PPL_USER_STACKSIZE);
55+
3956
static enum task_state task_callback(void *arg)
4057
{
4158
LOG_INF("entry");
@@ -129,6 +146,356 @@ ZTEST(userspace_ll, pipeline_check)
129146
pipeline_check();
130147
}
131148

149+
/* Copier UUID: 9ba00c83-ca12-4a83-943c-1fa2e82f9dda */
150+
static const uint8_t copier_uuid[16] = {
151+
0x83, 0x0c, 0xa0, 0x9b, 0x12, 0xca, 0x83, 0x4a,
152+
0x94, 0x3c, 0x1f, 0xa2, 0xe8, 0x2f, 0x9d, 0xda
153+
};
154+
155+
/**
156+
* Find the module_id (manifest entry index) for the copier module
157+
* by iterating the firmware manifest and matching the copier UUID.
158+
*/
159+
static int find_copier_module_id(void)
160+
{
161+
const struct sof_man_fw_desc *desc = basefw_vendor_get_manifest();
162+
const struct sof_man_module *mod;
163+
uint32_t i;
164+
165+
if (!desc)
166+
return -1;
167+
168+
for (i = 0; i < desc->header.num_module_entries; i++) {
169+
mod = (const struct sof_man_module *)((const char *)desc +
170+
SOF_MAN_MODULE_OFFSET(i));
171+
if (!memcmp(&mod->uuid, copier_uuid, sizeof(copier_uuid)))
172+
return (int)i;
173+
}
174+
175+
return -1;
176+
}
177+
178+
/**
179+
* IPC4 copier module config - used as payload for comp_new_ipc4().
180+
* Placed at MAILBOX_HOSTBOX_BASE before calling comp_new_ipc4().
181+
* Layout matches struct ipc4_copier_module_cfg from copier.h.
182+
*/
183+
struct copier_init_data {
184+
struct ipc4_base_module_cfg base;
185+
struct ipc4_audio_format out_fmt;
186+
uint32_t copier_feature_mask;
187+
/* Gateway config (matches struct ipc4_copier_gateway_cfg) */
188+
union ipc4_connector_node_id node_id;
189+
uint32_t dma_buffer_size;
190+
uint32_t config_length;
191+
} __packed __aligned(4);
192+
193+
static void fill_audio_format(struct ipc4_audio_format *fmt)
194+
{
195+
memset(fmt, 0, sizeof(*fmt));
196+
fmt->sampling_frequency = IPC4_FS_48000HZ;
197+
fmt->depth = IPC4_DEPTH_32BIT;
198+
fmt->ch_cfg = IPC4_CHANNEL_CONFIG_STEREO;
199+
fmt->channels_count = 2;
200+
fmt->valid_bit_depth = 32;
201+
fmt->s_type = IPC4_TYPE_MSB_INTEGER;
202+
fmt->interleaving_style = IPC4_CHANNELS_INTERLEAVED;
203+
}
204+
205+
/**
206+
* Create a copier component via IPC4.
207+
*
208+
* @param module_id Copier module_id from manifest
209+
* @param instance_id Instance ID for this component
210+
* @param pipeline_id Parent pipeline ID
211+
* @param node_id Gateway node ID (type + virtual DMA index)
212+
*/
213+
static struct comp_dev *create_copier(int module_id, int instance_id,
214+
int pipeline_id,
215+
union ipc4_connector_node_id node_id)
216+
{
217+
struct ipc4_module_init_instance module_init;
218+
struct copier_init_data cfg;
219+
struct comp_dev *dev;
220+
221+
/* Prepare copier config payload */
222+
memset(&cfg, 0, sizeof(cfg));
223+
fill_audio_format(&cfg.base.audio_fmt);
224+
/* 2 channels * 4 bytes * 48 frames = 384 bytes */
225+
cfg.base.ibs = 384;
226+
cfg.base.obs = 384;
227+
cfg.base.is_pages = 0;
228+
cfg.base.cpc = 0;
229+
cfg.out_fmt = cfg.base.audio_fmt;
230+
cfg.copier_feature_mask = 0;
231+
cfg.node_id = node_id;
232+
cfg.dma_buffer_size = 768;
233+
cfg.config_length = 0;
234+
235+
/* Write config data to mailbox hostbox (where comp_new_ipc4 reads it).
236+
* Flush cache so that data is visible in SRAM before comp_new_ipc4()
237+
* invalidates the cache line (in normal IPC flow, host writes via DMA
238+
* directly to SRAM, so the invalidation reads fresh data; here the DSP
239+
* core itself writes, so an explicit flush is needed).
240+
*/
241+
memcpy((void *)MAILBOX_HOSTBOX_BASE, &cfg, sizeof(cfg));
242+
sys_cache_data_flush_range((void *)MAILBOX_HOSTBOX_BASE, sizeof(cfg));
243+
244+
/* Prepare IPC4 module init header */
245+
memset(&module_init, 0, sizeof(module_init));
246+
module_init.primary.r.module_id = module_id;
247+
module_init.primary.r.instance_id = instance_id;
248+
module_init.primary.r.type = SOF_IPC4_MOD_INIT_INSTANCE;
249+
module_init.primary.r.msg_tgt = SOF_IPC4_MESSAGE_TARGET_MODULE_MSG;
250+
module_init.primary.r.rsp = SOF_IPC4_MESSAGE_DIR_MSG_REQUEST;
251+
252+
module_init.extension.r.param_block_size = sizeof(cfg) / sizeof(uint32_t);
253+
module_init.extension.r.ppl_instance_id = pipeline_id;
254+
module_init.extension.r.core_id = 0;
255+
module_init.extension.r.proc_domain = 0; /* LL */
256+
257+
dev = comp_new_ipc4(&module_init);
258+
259+
/*
260+
* We use the IPC code to create the components. This code runs
261+
* in kernel space, so we need to separately assign thecreated
262+
* components to the user LL and IPC threads before it can be used.
263+
*/
264+
comp_grant_access_to_thread(dev, zephyr_domain_thread_tid(zephyr_ll_domain()));
265+
comp_grant_access_to_thread(dev, &ppl_user_thread);
266+
267+
return dev;
268+
}
269+
270+
/**
271+
* Context shared between kernel setup and the user-space pipeline thread.
272+
*/
273+
struct ppl_test_ctx {
274+
struct pipeline *p;
275+
struct k_heap *heap;
276+
struct comp_dev *host_comp;
277+
struct comp_dev *dai_comp;
278+
struct comp_buffer *buf;
279+
struct ipc *ipc;
280+
struct ipc_comp_dev *ipc_pipe;
281+
};
282+
283+
/**
284+
* Pipeline operations: connect, complete, prepare, copy, verify, and clean up.
285+
* This function is called either directly (kernel mode) or from a user-space
286+
* thread, exercising pipeline_*() calls from the requested context.
287+
*/
288+
static void pipeline_ops(struct ppl_test_ctx *ctx)
289+
{
290+
struct pipeline *p = ctx->p;
291+
struct comp_dev *host_comp = ctx->host_comp;
292+
struct comp_dev *dai_comp = ctx->dai_comp;
293+
struct comp_buffer *buf = ctx->buf;
294+
int ret;
295+
296+
LOG_INF("pipeline_ops: user_context=%d", k_is_user_context());
297+
298+
/* Step 6: Connect host -> buffer -> DAI */
299+
ret = pipeline_connect(host_comp, buf, PPL_CONN_DIR_COMP_TO_BUFFER);
300+
zassert_equal(ret, 0, "connect host to buffer failed");
301+
302+
ret = pipeline_connect(dai_comp, buf, PPL_CONN_DIR_BUFFER_TO_COMP);
303+
zassert_equal(ret, 0, "connect buffer to DAI failed");
304+
305+
LOG_INF("host -> buffer -> DAI connected");
306+
307+
/* Step 7: Complete the pipeline */
308+
ret = pipeline_complete(p, host_comp, dai_comp);
309+
zassert_equal(ret, 0, "pipeline complete failed");
310+
311+
/* Step 8: Prepare the pipeline */
312+
p->sched_comp = host_comp;
313+
k_sleep(K_MSEC(10));
314+
315+
ret = pipeline_prepare(p, host_comp);
316+
zassert_equal(ret, 0, "pipeline prepare failed");
317+
318+
LOG_INF("pipeline complete, status = %d", p->status);
319+
320+
/* Step 9: Run copies */
321+
pipeline_copy(p);
322+
pipeline_copy(p);
323+
324+
/* Verify pipeline source and sink assignments */
325+
zassert_equal(p->source_comp, host_comp, "source comp mismatch");
326+
zassert_equal(p->sink_comp, dai_comp, "sink comp mismatch");
327+
328+
LOG_INF("pipeline_ops done");
329+
}
330+
331+
/**
332+
* User-space thread entry point for pipeline_two_components test.
333+
* p1 points to the ppl_test_ctx shared with the kernel launcher.
334+
*/
335+
static void pipeline_user_thread(void *p1, void *p2, void *p3)
336+
{
337+
struct ppl_test_ctx *ctx = (struct ppl_test_ctx *)p1;
338+
339+
zassert_true(k_is_user_context(), "expected user context");
340+
pipeline_ops(ctx);
341+
}
342+
343+
/**
344+
* Test creating a pipeline with a host copier and a DAI (link) copier,
345+
* connected through a shared buffer.
346+
*
347+
* When run_in_user is true, all pipeline_*() calls are made from a
348+
* separate user-space thread.
349+
*/
350+
static void pipeline_two_components(bool run_in_user)
351+
{
352+
struct ppl_test_ctx *ctx;
353+
struct k_heap *heap = NULL;
354+
uint32_t pipeline_id = 2;
355+
uint32_t priority = 0;
356+
uint32_t comp_id;
357+
int copier_module_id;
358+
int host_instance_id = 0;
359+
int dai_instance_id = 1;
360+
int ret;
361+
362+
/* Step: Find the copier module_id from the firmware manifest */
363+
copier_module_id = find_copier_module_id();
364+
zassert_true(copier_module_id >= 0, "copier module not found in manifest");
365+
LOG_INF("copier module_id = %d", copier_module_id);
366+
367+
/* Step: Create pipeline */
368+
if (run_in_user) {
369+
LOG_INF("running test with user memory domain");
370+
heap = zephyr_ll_user_heap();
371+
zassert_not_null(heap, "user heap not found");
372+
} else {
373+
LOG_INF("running test with kernel memory domain");
374+
}
375+
376+
ctx = sof_heap_alloc(heap, SOF_MEM_FLAG_USER, sizeof(*ctx), 0);
377+
ctx->heap = heap;
378+
ctx->ipc = ipc_get();
379+
380+
comp_id = IPC4_COMP_ID(copier_module_id, host_instance_id);
381+
ctx->p = pipeline_new(ctx->heap, pipeline_id, priority, comp_id, NULL);
382+
zassert_not_null(ctx->p, "pipeline creation failed");
383+
384+
/* Set pipeline period so components get correct dev->period and dev->frames.
385+
* This mirrors what ipc4_create_pipeline() does in normal IPC flow.
386+
*/
387+
ctx->p->time_domain = SOF_TIME_DOMAIN_TIMER;
388+
ctx->p->period = LL_TIMER_PERIOD_US;
389+
390+
/* Register pipeline in IPC component list so comp_new_ipc4() can
391+
* find it via ipc_get_comp_by_ppl_id() and set dev->period.
392+
*/
393+
ctx->ipc_pipe = rzalloc(SOF_MEM_FLAG_USER | SOF_MEM_FLAG_COHERENT,
394+
sizeof(struct ipc_comp_dev));
395+
zassert_not_null(ctx->ipc_pipe, "ipc_comp_dev alloc failed");
396+
ctx->ipc_pipe->pipeline = ctx->p;
397+
ctx->ipc_pipe->type = COMP_TYPE_PIPELINE;
398+
ctx->ipc_pipe->id = pipeline_id;
399+
ctx->ipc_pipe->core = 0;
400+
list_item_append(&ctx->ipc_pipe->list, &ctx->ipc->comp_list);
401+
402+
/* Step: Create host copier with HDA host output gateway */
403+
union ipc4_connector_node_id host_node_id = { .f = {
404+
.dma_type = ipc4_hda_host_output_class,
405+
.v_index = 0
406+
}};
407+
ctx->host_comp = create_copier(copier_module_id, host_instance_id, pipeline_id,
408+
host_node_id);
409+
zassert_not_null(ctx->host_comp, "host copier creation failed");
410+
411+
/* Assign pipeline to host component */
412+
ctx->host_comp->pipeline = ctx->p;
413+
ctx->host_comp->ipc_config.type = SOF_COMP_HOST;
414+
415+
LOG_INF("host copier created, comp_id = 0x%x", ctx->host_comp->ipc_config.id);
416+
417+
/* Step: Create link copier with HDA link output gateway */
418+
union ipc4_connector_node_id link_node_id = { .f = {
419+
.dma_type = ipc4_hda_link_output_class,
420+
.v_index = 0
421+
}};
422+
ctx->dai_comp = create_copier(copier_module_id, dai_instance_id, pipeline_id,
423+
link_node_id);
424+
zassert_not_null(ctx->dai_comp, "DAI copier creation failed");
425+
426+
/* Assign pipeline to DAI component */
427+
ctx->dai_comp->pipeline = ctx->p;
428+
ctx->dai_comp->ipc_config.type = SOF_COMP_DAI;
429+
430+
LOG_INF("DAI copier created, comp_id = 0x%x", ctx->dai_comp->ipc_config.id);
431+
432+
/* Step: Allocate a buffer to connect host -> DAI */
433+
ctx->buf = buffer_alloc(ctx->heap, 384, 0, 0, false);
434+
zassert_not_null(ctx->buf, "buffer allocation failed");
435+
436+
if (run_in_user) {
437+
/* Create a user-space thread to execute pipeline operations */
438+
k_thread_create(&ppl_user_thread, ppl_user_stack, PPL_USER_STACKSIZE,
439+
pipeline_user_thread, ctx, NULL, NULL,
440+
-1, K_USER, K_FOREVER);
441+
442+
/* Add thread to LL memory domain so it can access pipeline memory */
443+
k_mem_domain_add_thread(zephyr_ll_mem_domain(), &ppl_user_thread);
444+
445+
user_grant_dai_access_all(&ppl_user_thread);
446+
user_grant_dma_access_all(&ppl_user_thread);
447+
user_access_to_mailbox(zephyr_ll_mem_domain(), &ppl_user_thread);
448+
449+
k_thread_start(&ppl_user_thread);
450+
451+
LOG_INF("user thread started, waiting for completion");
452+
453+
k_thread_join(&ppl_user_thread, K_FOREVER);
454+
} else {
455+
/* Run pipeline operations directly in kernel context */
456+
pipeline_ops(ctx);
457+
}
458+
459+
/* Step: Clean up - reset, disconnect, free buffer, free components, free pipeline */
460+
/* Reset pipeline to bring components back to COMP_STATE_READY,
461+
* required before ipc_comp_free() which rejects non-READY components.
462+
*/
463+
ret = pipeline_reset(ctx->p, ctx->host_comp);
464+
zassert_equal(ret, 0, "pipeline reset failed");
465+
466+
pipeline_disconnect(ctx->host_comp, ctx->buf, PPL_CONN_DIR_COMP_TO_BUFFER);
467+
pipeline_disconnect(ctx->dai_comp, ctx->buf, PPL_CONN_DIR_BUFFER_TO_COMP);
468+
469+
buffer_free(ctx->buf);
470+
471+
/* Free components through IPC to properly remove from IPC device list */
472+
ret = ipc_comp_free(ctx->ipc, ctx->host_comp->ipc_config.id);
473+
zassert_equal(ret, 0, "host comp free failed");
474+
475+
ret = ipc_comp_free(ctx->ipc, ctx->dai_comp->ipc_config.id);
476+
zassert_equal(ret, 0, "DAI comp free failed");
477+
478+
/* Unregister pipeline from IPC component list */
479+
list_item_del(&ctx->ipc_pipe->list);
480+
rfree(ctx->ipc_pipe);
481+
482+
ret = pipeline_free(ctx->p);
483+
zassert_equal(ret, 0, "pipeline free failed");
484+
485+
sof_heap_free(heap, ctx);
486+
LOG_INF("two component pipeline test complete");
487+
}
488+
489+
ZTEST(userspace_ll, pipeline_two_components_kernel)
490+
{
491+
pipeline_two_components(false);
492+
}
493+
494+
ZTEST(userspace_ll, pipeline_two_components_user)
495+
{
496+
pipeline_two_components(true);
497+
}
498+
132499
ZTEST_SUITE(userspace_ll, NULL, NULL, NULL, NULL, NULL);
133500

134501
/**

0 commit comments

Comments
 (0)