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
2839LOG_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 */
3748K_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+
3956static 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+
132499ZTEST_SUITE (userspace_ll , NULL , NULL , NULL , NULL , NULL );
133500
134501/**
0 commit comments