1 | // SPDX-License-Identifier: (GPL-2.0-only OR BSD-3-Clause) |
2 | // |
3 | // This file is provided under a dual BSD/GPLv2 license. When using or |
4 | // redistributing this file, you may do so under either license. |
5 | // |
6 | // Copyright(c) 2022 Intel Corporation. All rights reserved. |
7 | // |
8 | |
9 | #include <sound/pcm_params.h> |
10 | #include <sound/sof/ipc4/header.h> |
11 | #include "sof-audio.h" |
12 | #include "sof-priv.h" |
13 | #include "ops.h" |
14 | #include "ipc4-priv.h" |
15 | #include "ipc4-topology.h" |
16 | #include "ipc4-fw-reg.h" |
17 | |
18 | /** |
19 | * struct sof_ipc4_timestamp_info - IPC4 timestamp info |
20 | * @host_copier: the host copier of the pcm stream |
21 | * @dai_copier: the dai copier of the pcm stream |
22 | * @stream_start_offset: reported by fw in memory window (converted to frames) |
23 | * @stream_end_offset: reported by fw in memory window (converted to frames) |
24 | * @llp_offset: llp offset in memory window |
25 | * @boundary: wrap boundary should be used for the LLP frame counter |
26 | * @delay: Calculated and stored in pointer callback. The stored value is |
27 | * returned in the delay callback. |
28 | */ |
29 | struct sof_ipc4_timestamp_info { |
30 | struct sof_ipc4_copier *host_copier; |
31 | struct sof_ipc4_copier *dai_copier; |
32 | u64 stream_start_offset; |
33 | u64 stream_end_offset; |
34 | u32 llp_offset; |
35 | |
36 | u64 boundary; |
37 | snd_pcm_sframes_t delay; |
38 | }; |
39 | |
40 | static int sof_ipc4_set_multi_pipeline_state(struct snd_sof_dev *sdev, u32 state, |
41 | struct ipc4_pipeline_set_state_data *trigger_list) |
42 | { |
43 | struct sof_ipc4_msg msg = {{ 0 }}; |
44 | u32 primary, ipc_size; |
45 | |
46 | /* trigger a single pipeline */ |
47 | if (trigger_list->count == 1) |
48 | return sof_ipc4_set_pipeline_state(sdev, id: trigger_list->pipeline_instance_ids[0], |
49 | state); |
50 | |
51 | primary = state; |
52 | primary |= SOF_IPC4_MSG_TYPE_SET(SOF_IPC4_GLB_SET_PIPELINE_STATE); |
53 | primary |= SOF_IPC4_MSG_DIR(SOF_IPC4_MSG_REQUEST); |
54 | primary |= SOF_IPC4_MSG_TARGET(SOF_IPC4_FW_GEN_MSG); |
55 | msg.primary = primary; |
56 | |
57 | /* trigger multiple pipelines with a single IPC */ |
58 | msg.extension = SOF_IPC4_GLB_PIPE_STATE_EXT_MULTI; |
59 | |
60 | /* ipc_size includes the count and the pipeline IDs for the number of pipelines */ |
61 | ipc_size = sizeof(u32) * (trigger_list->count + 1); |
62 | msg.data_size = ipc_size; |
63 | msg.data_ptr = trigger_list; |
64 | |
65 | return sof_ipc_tx_message_no_reply(ipc: sdev->ipc, msg_data: &msg, msg_bytes: ipc_size); |
66 | } |
67 | |
68 | int sof_ipc4_set_pipeline_state(struct snd_sof_dev *sdev, u32 instance_id, u32 state) |
69 | { |
70 | struct sof_ipc4_msg msg = {{ 0 }}; |
71 | u32 primary; |
72 | |
73 | dev_dbg(sdev->dev, "ipc4 set pipeline instance %d state %d" , instance_id, state); |
74 | |
75 | primary = state; |
76 | primary |= SOF_IPC4_GLB_PIPE_STATE_ID(instance_id); |
77 | primary |= SOF_IPC4_MSG_TYPE_SET(SOF_IPC4_GLB_SET_PIPELINE_STATE); |
78 | primary |= SOF_IPC4_MSG_DIR(SOF_IPC4_MSG_REQUEST); |
79 | primary |= SOF_IPC4_MSG_TARGET(SOF_IPC4_FW_GEN_MSG); |
80 | |
81 | msg.primary = primary; |
82 | |
83 | return sof_ipc_tx_message_no_reply(ipc: sdev->ipc, msg_data: &msg, msg_bytes: 0); |
84 | } |
85 | EXPORT_SYMBOL(sof_ipc4_set_pipeline_state); |
86 | |
87 | static void sof_ipc4_add_pipeline_by_priority(struct ipc4_pipeline_set_state_data *trigger_list, |
88 | struct snd_sof_widget *pipe_widget, |
89 | s8 *pipe_priority, bool ascend) |
90 | { |
91 | struct sof_ipc4_pipeline *pipeline = pipe_widget->private; |
92 | int i, j; |
93 | |
94 | for (i = 0; i < trigger_list->count; i++) { |
95 | /* add pipeline from low priority to high */ |
96 | if (ascend && pipeline->priority < pipe_priority[i]) |
97 | break; |
98 | /* add pipeline from high priority to low */ |
99 | else if (!ascend && pipeline->priority > pipe_priority[i]) |
100 | break; |
101 | } |
102 | |
103 | for (j = trigger_list->count - 1; j >= i; j--) { |
104 | trigger_list->pipeline_instance_ids[j + 1] = trigger_list->pipeline_instance_ids[j]; |
105 | pipe_priority[j + 1] = pipe_priority[j]; |
106 | } |
107 | |
108 | trigger_list->pipeline_instance_ids[i] = pipe_widget->instance_id; |
109 | trigger_list->count++; |
110 | pipe_priority[i] = pipeline->priority; |
111 | } |
112 | |
113 | static void |
114 | sof_ipc4_add_pipeline_to_trigger_list(struct snd_sof_dev *sdev, int state, |
115 | struct snd_sof_pipeline *spipe, |
116 | struct ipc4_pipeline_set_state_data *trigger_list, |
117 | s8 *pipe_priority) |
118 | { |
119 | struct snd_sof_widget *pipe_widget = spipe->pipe_widget; |
120 | struct sof_ipc4_pipeline *pipeline = pipe_widget->private; |
121 | |
122 | if (pipeline->skip_during_fe_trigger && state != SOF_IPC4_PIPE_RESET) |
123 | return; |
124 | |
125 | switch (state) { |
126 | case SOF_IPC4_PIPE_RUNNING: |
127 | /* |
128 | * Trigger pipeline if all PCMs containing it are paused or if it is RUNNING |
129 | * for the first time |
130 | */ |
131 | if (spipe->started_count == spipe->paused_count) |
132 | sof_ipc4_add_pipeline_by_priority(trigger_list, pipe_widget, pipe_priority, |
133 | ascend: false); |
134 | break; |
135 | case SOF_IPC4_PIPE_RESET: |
136 | /* RESET if the pipeline is neither running nor paused */ |
137 | if (!spipe->started_count && !spipe->paused_count) |
138 | sof_ipc4_add_pipeline_by_priority(trigger_list, pipe_widget, pipe_priority, |
139 | ascend: true); |
140 | break; |
141 | case SOF_IPC4_PIPE_PAUSED: |
142 | /* Pause the pipeline only when its started_count is 1 more than paused_count */ |
143 | if (spipe->paused_count == (spipe->started_count - 1)) |
144 | sof_ipc4_add_pipeline_by_priority(trigger_list, pipe_widget, pipe_priority, |
145 | ascend: true); |
146 | break; |
147 | default: |
148 | break; |
149 | } |
150 | } |
151 | |
152 | static void |
153 | sof_ipc4_update_pipeline_state(struct snd_sof_dev *sdev, int state, int cmd, |
154 | struct snd_sof_pipeline *spipe, |
155 | struct ipc4_pipeline_set_state_data *trigger_list) |
156 | { |
157 | struct snd_sof_widget *pipe_widget = spipe->pipe_widget; |
158 | struct sof_ipc4_pipeline *pipeline = pipe_widget->private; |
159 | int i; |
160 | |
161 | if (pipeline->skip_during_fe_trigger && state != SOF_IPC4_PIPE_RESET) |
162 | return; |
163 | |
164 | /* set state for pipeline if it was just triggered */ |
165 | for (i = 0; i < trigger_list->count; i++) { |
166 | if (trigger_list->pipeline_instance_ids[i] == pipe_widget->instance_id) { |
167 | pipeline->state = state; |
168 | break; |
169 | } |
170 | } |
171 | |
172 | switch (state) { |
173 | case SOF_IPC4_PIPE_PAUSED: |
174 | switch (cmd) { |
175 | case SNDRV_PCM_TRIGGER_PAUSE_PUSH: |
176 | /* |
177 | * increment paused_count if the PAUSED is the final state during |
178 | * the PAUSE trigger |
179 | */ |
180 | spipe->paused_count++; |
181 | break; |
182 | case SNDRV_PCM_TRIGGER_STOP: |
183 | case SNDRV_PCM_TRIGGER_SUSPEND: |
184 | /* |
185 | * decrement started_count if PAUSED is the final state during the |
186 | * STOP trigger |
187 | */ |
188 | spipe->started_count--; |
189 | break; |
190 | default: |
191 | break; |
192 | } |
193 | break; |
194 | case SOF_IPC4_PIPE_RUNNING: |
195 | switch (cmd) { |
196 | case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: |
197 | /* decrement paused_count for RELEASE */ |
198 | spipe->paused_count--; |
199 | break; |
200 | case SNDRV_PCM_TRIGGER_START: |
201 | case SNDRV_PCM_TRIGGER_RESUME: |
202 | /* increment started_count for START/RESUME */ |
203 | spipe->started_count++; |
204 | break; |
205 | default: |
206 | break; |
207 | } |
208 | break; |
209 | default: |
210 | break; |
211 | } |
212 | } |
213 | |
214 | /* |
215 | * The picture below represents the pipeline state machine wrt PCM actions corresponding to the |
216 | * triggers and ioctls |
217 | * +---------------+ |
218 | * | | |
219 | * | INIT | |
220 | * | | |
221 | * +-------+-------+ |
222 | * | |
223 | * | |
224 | * | START |
225 | * | |
226 | * | |
227 | * +----------------+ +------v-------+ +-------------+ |
228 | * | | START | | HW_FREE | | |
229 | * | RUNNING <-------------+ PAUSED +--------------> + RESET | |
230 | * | | PAUSE | | | | |
231 | * +------+---------+ RELEASE +---------+----+ +-------------+ |
232 | * | ^ |
233 | * | | |
234 | * | | |
235 | * | | |
236 | * | PAUSE | |
237 | * +---------------------------------+ |
238 | * STOP/SUSPEND |
239 | * |
240 | * Note that during system suspend, the suspend trigger is followed by a hw_free in |
241 | * sof_pcm_trigger(). So, the final state during suspend would be RESET. |
242 | * Also, since the SOF driver doesn't support full resume, streams would be restarted with the |
243 | * prepare ioctl before the START trigger. |
244 | */ |
245 | |
246 | /* |
247 | * Chained DMA is a special case where there is no processing on |
248 | * DSP. The samples are just moved over by host side DMA to a single |
249 | * buffer on DSP and directly from there to link DMA. However, the |
250 | * model on SOF driver has two notional pipelines, one at host DAI, |
251 | * and another at link DAI. They both shall have the use_chain_dma |
252 | * attribute. |
253 | */ |
254 | |
255 | static int sof_ipc4_chain_dma_trigger(struct snd_sof_dev *sdev, |
256 | int direction, |
257 | struct snd_sof_pcm_stream_pipeline_list *pipeline_list, |
258 | int state, int cmd) |
259 | { |
260 | struct sof_ipc4_fw_data *ipc4_data = sdev->private; |
261 | bool allocate, enable, set_fifo_size; |
262 | struct sof_ipc4_msg msg = {{ 0 }}; |
263 | int i; |
264 | |
265 | switch (state) { |
266 | case SOF_IPC4_PIPE_RUNNING: /* Allocate and start chained dma */ |
267 | allocate = true; |
268 | enable = true; |
269 | /* |
270 | * SOF assumes creation of a new stream from the presence of fifo_size |
271 | * in the message, so we must leave it out in pause release case. |
272 | */ |
273 | if (cmd == SNDRV_PCM_TRIGGER_PAUSE_RELEASE) |
274 | set_fifo_size = false; |
275 | else |
276 | set_fifo_size = true; |
277 | break; |
278 | case SOF_IPC4_PIPE_PAUSED: /* Disable chained DMA. */ |
279 | allocate = true; |
280 | enable = false; |
281 | set_fifo_size = false; |
282 | break; |
283 | case SOF_IPC4_PIPE_RESET: /* Disable and free chained DMA. */ |
284 | allocate = false; |
285 | enable = false; |
286 | set_fifo_size = false; |
287 | break; |
288 | default: |
289 | dev_err(sdev->dev, "Unexpected state %d" , state); |
290 | return -EINVAL; |
291 | } |
292 | |
293 | msg.primary = SOF_IPC4_MSG_TYPE_SET(SOF_IPC4_GLB_CHAIN_DMA); |
294 | msg.primary |= SOF_IPC4_MSG_DIR(SOF_IPC4_MSG_REQUEST); |
295 | msg.primary |= SOF_IPC4_MSG_TARGET(SOF_IPC4_FW_GEN_MSG); |
296 | |
297 | /* |
298 | * To set-up the DMA chain, the host DMA ID and SCS setting |
299 | * are retrieved from the host pipeline configuration. Likewise |
300 | * the link DMA ID and fifo_size are retrieved from the link |
301 | * pipeline configuration. |
302 | */ |
303 | for (i = 0; i < pipeline_list->count; i++) { |
304 | struct snd_sof_pipeline *spipe = pipeline_list->pipelines[i]; |
305 | struct snd_sof_widget *pipe_widget = spipe->pipe_widget; |
306 | struct sof_ipc4_pipeline *pipeline = pipe_widget->private; |
307 | |
308 | if (!pipeline->use_chain_dma) { |
309 | dev_err(sdev->dev, |
310 | "All pipelines in chained DMA stream should have use_chain_dma attribute set." ); |
311 | return -EINVAL; |
312 | } |
313 | |
314 | msg.primary |= pipeline->msg.primary; |
315 | |
316 | /* Add fifo_size (actually DMA buffer size) field to the message */ |
317 | if (set_fifo_size) |
318 | msg.extension |= pipeline->msg.extension; |
319 | } |
320 | |
321 | if (direction == SNDRV_PCM_STREAM_CAPTURE) { |
322 | /* |
323 | * For ChainDMA the DMA ids are unique with the following mapping: |
324 | * playback: 0 - (num_playback_streams - 1) |
325 | * capture: num_playback_streams - (num_playback_streams + |
326 | * num_capture_streams - 1) |
327 | * |
328 | * Add the num_playback_streams offset to the DMA ids stored in |
329 | * msg.primary in case capture |
330 | */ |
331 | msg.primary += SOF_IPC4_GLB_CHAIN_DMA_HOST_ID(ipc4_data->num_playback_streams); |
332 | msg.primary += SOF_IPC4_GLB_CHAIN_DMA_LINK_ID(ipc4_data->num_playback_streams); |
333 | } |
334 | |
335 | if (allocate) |
336 | msg.primary |= SOF_IPC4_GLB_CHAIN_DMA_ALLOCATE_MASK; |
337 | |
338 | if (enable) |
339 | msg.primary |= SOF_IPC4_GLB_CHAIN_DMA_ENABLE_MASK; |
340 | |
341 | return sof_ipc_tx_message_no_reply(ipc: sdev->ipc, msg_data: &msg, msg_bytes: 0); |
342 | } |
343 | |
344 | static int sof_ipc4_trigger_pipelines(struct snd_soc_component *component, |
345 | struct snd_pcm_substream *substream, int state, int cmd) |
346 | { |
347 | struct snd_sof_dev *sdev = snd_soc_component_get_drvdata(c: component); |
348 | struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream); |
349 | struct snd_sof_pcm_stream_pipeline_list *pipeline_list; |
350 | struct sof_ipc4_fw_data *ipc4_data = sdev->private; |
351 | struct ipc4_pipeline_set_state_data *trigger_list; |
352 | struct snd_sof_widget *pipe_widget; |
353 | struct sof_ipc4_pipeline *pipeline; |
354 | struct snd_sof_pipeline *spipe; |
355 | struct snd_sof_pcm *spcm; |
356 | u8 *pipe_priority; |
357 | int ret; |
358 | int i; |
359 | |
360 | dev_dbg(sdev->dev, "trigger cmd: %d state: %d\n" , cmd, state); |
361 | |
362 | spcm = snd_sof_find_spcm_dai(scomp: component, rtd); |
363 | if (!spcm) |
364 | return -EINVAL; |
365 | |
366 | pipeline_list = &spcm->stream[substream->stream].pipeline_list; |
367 | |
368 | /* nothing to trigger if the list is empty */ |
369 | if (!pipeline_list->pipelines || !pipeline_list->count) |
370 | return 0; |
371 | |
372 | spipe = pipeline_list->pipelines[0]; |
373 | pipe_widget = spipe->pipe_widget; |
374 | pipeline = pipe_widget->private; |
375 | |
376 | /* |
377 | * If use_chain_dma attribute is set we proceed to chained DMA |
378 | * trigger function that handles the rest for the substream. |
379 | */ |
380 | if (pipeline->use_chain_dma) |
381 | return sof_ipc4_chain_dma_trigger(sdev, direction: substream->stream, |
382 | pipeline_list, state, cmd); |
383 | |
384 | /* allocate memory for the pipeline data */ |
385 | trigger_list = kzalloc(struct_size(trigger_list, pipeline_instance_ids, |
386 | pipeline_list->count), GFP_KERNEL); |
387 | if (!trigger_list) |
388 | return -ENOMEM; |
389 | |
390 | pipe_priority = kzalloc(size: pipeline_list->count, GFP_KERNEL); |
391 | if (!pipe_priority) { |
392 | kfree(objp: trigger_list); |
393 | return -ENOMEM; |
394 | } |
395 | |
396 | mutex_lock(&ipc4_data->pipeline_state_mutex); |
397 | |
398 | /* |
399 | * IPC4 requires pipelines to be triggered in order starting at the sink and |
400 | * walking all the way to the source. So traverse the pipeline_list in the order |
401 | * sink->source when starting PCM's and in the reverse order to pause/stop PCM's. |
402 | * Skip the pipelines that have their skip_during_fe_trigger flag set. If there is a fork |
403 | * in the pipeline, the order of triggering between the left/right paths will be |
404 | * indeterministic. But the sink->source trigger order sink->source would still be |
405 | * guaranteed for each fork independently. |
406 | */ |
407 | if (state == SOF_IPC4_PIPE_RUNNING || state == SOF_IPC4_PIPE_RESET) |
408 | for (i = pipeline_list->count - 1; i >= 0; i--) { |
409 | spipe = pipeline_list->pipelines[i]; |
410 | sof_ipc4_add_pipeline_to_trigger_list(sdev, state, spipe, trigger_list, |
411 | pipe_priority); |
412 | } |
413 | else |
414 | for (i = 0; i < pipeline_list->count; i++) { |
415 | spipe = pipeline_list->pipelines[i]; |
416 | sof_ipc4_add_pipeline_to_trigger_list(sdev, state, spipe, trigger_list, |
417 | pipe_priority); |
418 | } |
419 | |
420 | /* return if all pipelines are in the requested state already */ |
421 | if (!trigger_list->count) { |
422 | ret = 0; |
423 | goto free; |
424 | } |
425 | |
426 | /* no need to pause before reset or before pause release */ |
427 | if (state == SOF_IPC4_PIPE_RESET || cmd == SNDRV_PCM_TRIGGER_PAUSE_RELEASE) |
428 | goto skip_pause_transition; |
429 | |
430 | /* |
431 | * set paused state for pipelines if the final state is PAUSED or when the pipeline |
432 | * is set to RUNNING for the first time after the PCM is started. |
433 | */ |
434 | ret = sof_ipc4_set_multi_pipeline_state(sdev, state: SOF_IPC4_PIPE_PAUSED, trigger_list); |
435 | if (ret < 0) { |
436 | dev_err(sdev->dev, "failed to pause all pipelines\n" ); |
437 | goto free; |
438 | } |
439 | |
440 | /* update PAUSED state for all pipelines just triggered */ |
441 | for (i = 0; i < pipeline_list->count ; i++) { |
442 | spipe = pipeline_list->pipelines[i]; |
443 | sof_ipc4_update_pipeline_state(sdev, state: SOF_IPC4_PIPE_PAUSED, cmd, spipe, |
444 | trigger_list); |
445 | } |
446 | |
447 | /* return if this is the final state */ |
448 | if (state == SOF_IPC4_PIPE_PAUSED) { |
449 | struct sof_ipc4_timestamp_info *time_info; |
450 | |
451 | /* |
452 | * Invalidate the stream_start_offset to make sure that it is |
453 | * going to be updated if the stream resumes |
454 | */ |
455 | time_info = spcm->stream[substream->stream].private; |
456 | if (time_info) |
457 | time_info->stream_start_offset = SOF_IPC4_INVALID_STREAM_POSITION; |
458 | |
459 | goto free; |
460 | } |
461 | skip_pause_transition: |
462 | /* else set the RUNNING/RESET state in the DSP */ |
463 | ret = sof_ipc4_set_multi_pipeline_state(sdev, state, trigger_list); |
464 | if (ret < 0) { |
465 | dev_err(sdev->dev, "failed to set final state %d for all pipelines\n" , state); |
466 | /* |
467 | * workaround: if the firmware is crashed while setting the |
468 | * pipelines to reset state we must ignore the error code and |
469 | * reset it to 0. |
470 | * Since the firmware is crashed we will not send IPC messages |
471 | * and we are going to see errors printed, but the state of the |
472 | * widgets will be correct for the next boot. |
473 | */ |
474 | if (sdev->fw_state != SOF_FW_CRASHED || state != SOF_IPC4_PIPE_RESET) |
475 | goto free; |
476 | |
477 | ret = 0; |
478 | } |
479 | |
480 | /* update RUNNING/RESET state for all pipelines that were just triggered */ |
481 | for (i = 0; i < pipeline_list->count; i++) { |
482 | spipe = pipeline_list->pipelines[i]; |
483 | sof_ipc4_update_pipeline_state(sdev, state, cmd, spipe, trigger_list); |
484 | } |
485 | |
486 | free: |
487 | mutex_unlock(lock: &ipc4_data->pipeline_state_mutex); |
488 | kfree(objp: trigger_list); |
489 | kfree(objp: pipe_priority); |
490 | return ret; |
491 | } |
492 | |
493 | static int sof_ipc4_pcm_trigger(struct snd_soc_component *component, |
494 | struct snd_pcm_substream *substream, int cmd) |
495 | { |
496 | int state; |
497 | |
498 | /* determine the pipeline state */ |
499 | switch (cmd) { |
500 | case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: |
501 | case SNDRV_PCM_TRIGGER_RESUME: |
502 | case SNDRV_PCM_TRIGGER_START: |
503 | state = SOF_IPC4_PIPE_RUNNING; |
504 | break; |
505 | case SNDRV_PCM_TRIGGER_PAUSE_PUSH: |
506 | case SNDRV_PCM_TRIGGER_SUSPEND: |
507 | case SNDRV_PCM_TRIGGER_STOP: |
508 | state = SOF_IPC4_PIPE_PAUSED; |
509 | break; |
510 | default: |
511 | dev_err(component->dev, "%s: unhandled trigger cmd %d\n" , __func__, cmd); |
512 | return -EINVAL; |
513 | } |
514 | |
515 | /* set the pipeline state */ |
516 | return sof_ipc4_trigger_pipelines(component, substream, state, cmd); |
517 | } |
518 | |
519 | static int sof_ipc4_pcm_hw_free(struct snd_soc_component *component, |
520 | struct snd_pcm_substream *substream) |
521 | { |
522 | /* command is not relevant with RESET, so just pass 0 */ |
523 | return sof_ipc4_trigger_pipelines(component, substream, state: SOF_IPC4_PIPE_RESET, cmd: 0); |
524 | } |
525 | |
526 | static void ipc4_ssp_dai_config_pcm_params_match(struct snd_sof_dev *sdev, const char *link_name, |
527 | struct snd_pcm_hw_params *params) |
528 | { |
529 | struct snd_sof_dai_link *slink; |
530 | struct snd_sof_dai *dai; |
531 | bool dai_link_found = false; |
532 | int i; |
533 | |
534 | list_for_each_entry(slink, &sdev->dai_link_list, list) { |
535 | if (!strcmp(slink->link->name, link_name)) { |
536 | dai_link_found = true; |
537 | break; |
538 | } |
539 | } |
540 | |
541 | if (!dai_link_found) |
542 | return; |
543 | |
544 | for (i = 0; i < slink->num_hw_configs; i++) { |
545 | struct snd_soc_tplg_hw_config *hw_config = &slink->hw_configs[i]; |
546 | |
547 | if (params_rate(p: params) == le32_to_cpu(hw_config->fsync_rate)) { |
548 | /* set current config for all DAI's with matching name */ |
549 | list_for_each_entry(dai, &sdev->dai_list, list) |
550 | if (!strcmp(slink->link->name, dai->name)) |
551 | dai->current_config = le32_to_cpu(hw_config->id); |
552 | break; |
553 | } |
554 | } |
555 | } |
556 | |
557 | /* |
558 | * Fixup DAI link parameters for sampling rate based on |
559 | * DAI copier configuration. |
560 | */ |
561 | static int sof_ipc4_pcm_dai_link_fixup_rate(struct snd_sof_dev *sdev, |
562 | struct snd_pcm_hw_params *params, |
563 | struct sof_ipc4_copier *ipc4_copier) |
564 | { |
565 | struct sof_ipc4_pin_format *pin_fmts = ipc4_copier->available_fmt.input_pin_fmts; |
566 | struct snd_interval *rate = hw_param_interval(params, SNDRV_PCM_HW_PARAM_RATE); |
567 | int num_input_formats = ipc4_copier->available_fmt.num_input_formats; |
568 | unsigned int fe_rate = params_rate(p: params); |
569 | bool fe_be_rate_match = false; |
570 | bool single_be_rate = true; |
571 | unsigned int be_rate; |
572 | int i; |
573 | |
574 | /* |
575 | * Copier does not change sampling rate, so we |
576 | * need to only consider the input pin information. |
577 | */ |
578 | for (i = 0; i < num_input_formats; i++) { |
579 | unsigned int val = pin_fmts[i].audio_fmt.sampling_frequency; |
580 | |
581 | if (i == 0) |
582 | be_rate = val; |
583 | else if (val != be_rate) |
584 | single_be_rate = false; |
585 | |
586 | if (val == fe_rate) { |
587 | fe_be_rate_match = true; |
588 | break; |
589 | } |
590 | } |
591 | |
592 | /* |
593 | * If rate is different than FE rate, topology must |
594 | * contain an SRC. But we do require topology to |
595 | * define a single rate in the DAI copier config in |
596 | * this case (FE rate may be variable). |
597 | */ |
598 | if (!fe_be_rate_match) { |
599 | if (!single_be_rate) { |
600 | dev_err(sdev->dev, "Unable to select sampling rate for DAI link\n" ); |
601 | return -EINVAL; |
602 | } |
603 | |
604 | rate->min = be_rate; |
605 | rate->max = rate->min; |
606 | } |
607 | |
608 | return 0; |
609 | } |
610 | |
611 | static int sof_ipc4_pcm_dai_link_fixup(struct snd_soc_pcm_runtime *rtd, |
612 | struct snd_pcm_hw_params *params) |
613 | { |
614 | struct snd_soc_component *component = snd_soc_rtdcom_lookup(rtd, SOF_AUDIO_PCM_DRV_NAME); |
615 | struct snd_sof_dai *dai = snd_sof_find_dai(scomp: component, name: rtd->dai_link->name); |
616 | struct snd_mask *fmt = hw_param_mask(params, SNDRV_PCM_HW_PARAM_FORMAT); |
617 | struct snd_sof_dev *sdev = snd_soc_component_get_drvdata(c: component); |
618 | struct snd_soc_dai *cpu_dai = snd_soc_rtd_to_cpu(rtd, 0); |
619 | struct sof_ipc4_audio_format *ipc4_fmt; |
620 | struct sof_ipc4_copier *ipc4_copier; |
621 | bool single_fmt = false; |
622 | u32 valid_bits = 0; |
623 | int dir, ret; |
624 | |
625 | if (!dai) { |
626 | dev_err(component->dev, "%s: No DAI found with name %s\n" , __func__, |
627 | rtd->dai_link->name); |
628 | return -EINVAL; |
629 | } |
630 | |
631 | ipc4_copier = dai->private; |
632 | if (!ipc4_copier) { |
633 | dev_err(component->dev, "%s: No private data found for DAI %s\n" , |
634 | __func__, rtd->dai_link->name); |
635 | return -EINVAL; |
636 | } |
637 | |
638 | for_each_pcm_streams(dir) { |
639 | struct snd_soc_dapm_widget *w = snd_soc_dai_get_widget(dai: cpu_dai, stream: dir); |
640 | |
641 | if (w) { |
642 | struct sof_ipc4_available_audio_format *available_fmt = |
643 | &ipc4_copier->available_fmt; |
644 | struct snd_sof_widget *swidget = w->dobj.private; |
645 | struct snd_sof_widget *pipe_widget = swidget->spipe->pipe_widget; |
646 | struct sof_ipc4_pipeline *pipeline = pipe_widget->private; |
647 | |
648 | /* Chain DMA does not use copiers, so no fixup needed */ |
649 | if (pipeline->use_chain_dma) |
650 | return 0; |
651 | |
652 | if (dir == SNDRV_PCM_STREAM_PLAYBACK) { |
653 | if (sof_ipc4_copier_is_single_format(sdev, |
654 | pin_fmts: available_fmt->output_pin_fmts, |
655 | pin_fmts_size: available_fmt->num_output_formats)) { |
656 | ipc4_fmt = &available_fmt->output_pin_fmts->audio_fmt; |
657 | single_fmt = true; |
658 | } |
659 | } else { |
660 | if (sof_ipc4_copier_is_single_format(sdev, |
661 | pin_fmts: available_fmt->input_pin_fmts, |
662 | pin_fmts_size: available_fmt->num_input_formats)) { |
663 | ipc4_fmt = &available_fmt->input_pin_fmts->audio_fmt; |
664 | single_fmt = true; |
665 | } |
666 | } |
667 | } |
668 | } |
669 | |
670 | ret = sof_ipc4_pcm_dai_link_fixup_rate(sdev, params, ipc4_copier); |
671 | if (ret) |
672 | return ret; |
673 | |
674 | if (single_fmt) { |
675 | snd_mask_none(mask: fmt); |
676 | valid_bits = SOF_IPC4_AUDIO_FORMAT_CFG_V_BIT_DEPTH(ipc4_fmt->fmt_cfg); |
677 | dev_dbg(component->dev, "Set %s to %d bit format\n" , dai->name, valid_bits); |
678 | } |
679 | |
680 | /* Set format if it is specified */ |
681 | switch (valid_bits) { |
682 | case 16: |
683 | snd_mask_set_format(mask: fmt, SNDRV_PCM_FORMAT_S16_LE); |
684 | break; |
685 | case 24: |
686 | snd_mask_set_format(mask: fmt, SNDRV_PCM_FORMAT_S24_LE); |
687 | break; |
688 | case 32: |
689 | snd_mask_set_format(mask: fmt, SNDRV_PCM_FORMAT_S32_LE); |
690 | break; |
691 | default: |
692 | break; |
693 | } |
694 | |
695 | switch (ipc4_copier->dai_type) { |
696 | case SOF_DAI_INTEL_SSP: |
697 | ipc4_ssp_dai_config_pcm_params_match(sdev, link_name: (char *)rtd->dai_link->name, params); |
698 | break; |
699 | default: |
700 | break; |
701 | } |
702 | |
703 | return 0; |
704 | } |
705 | |
706 | static void sof_ipc4_pcm_free(struct snd_sof_dev *sdev, struct snd_sof_pcm *spcm) |
707 | { |
708 | struct snd_sof_pcm_stream_pipeline_list *pipeline_list; |
709 | int stream; |
710 | |
711 | for_each_pcm_streams(stream) { |
712 | pipeline_list = &spcm->stream[stream].pipeline_list; |
713 | kfree(objp: pipeline_list->pipelines); |
714 | pipeline_list->pipelines = NULL; |
715 | kfree(objp: spcm->stream[stream].private); |
716 | spcm->stream[stream].private = NULL; |
717 | } |
718 | } |
719 | |
720 | static int sof_ipc4_pcm_setup(struct snd_sof_dev *sdev, struct snd_sof_pcm *spcm) |
721 | { |
722 | struct snd_sof_pcm_stream_pipeline_list *pipeline_list; |
723 | struct sof_ipc4_fw_data *ipc4_data = sdev->private; |
724 | struct sof_ipc4_timestamp_info *stream_info; |
725 | bool support_info = true; |
726 | u32 abi_version; |
727 | u32 abi_offset; |
728 | int stream; |
729 | |
730 | abi_offset = offsetof(struct sof_ipc4_fw_registers, abi_ver); |
731 | sof_mailbox_read(sdev, offset: sdev->fw_info_box.offset + abi_offset, message: &abi_version, |
732 | bytes: sizeof(abi_version)); |
733 | |
734 | if (abi_version < SOF_IPC4_FW_REGS_ABI_VER) |
735 | support_info = false; |
736 | |
737 | /* For delay reporting the get_host_byte_counter callback is needed */ |
738 | if (!sof_ops(sdev) || !sof_ops(sdev)->get_host_byte_counter) |
739 | support_info = false; |
740 | |
741 | for_each_pcm_streams(stream) { |
742 | pipeline_list = &spcm->stream[stream].pipeline_list; |
743 | |
744 | /* allocate memory for max number of pipeline IDs */ |
745 | pipeline_list->pipelines = kcalloc(n: ipc4_data->max_num_pipelines, |
746 | size: sizeof(struct snd_sof_widget *), GFP_KERNEL); |
747 | if (!pipeline_list->pipelines) { |
748 | sof_ipc4_pcm_free(sdev, spcm); |
749 | return -ENOMEM; |
750 | } |
751 | |
752 | if (!support_info) |
753 | continue; |
754 | |
755 | stream_info = kzalloc(size: sizeof(*stream_info), GFP_KERNEL); |
756 | if (!stream_info) { |
757 | sof_ipc4_pcm_free(sdev, spcm); |
758 | return -ENOMEM; |
759 | } |
760 | |
761 | spcm->stream[stream].private = stream_info; |
762 | } |
763 | |
764 | return 0; |
765 | } |
766 | |
767 | static void sof_ipc4_build_time_info(struct snd_sof_dev *sdev, struct snd_sof_pcm_stream *spcm) |
768 | { |
769 | struct sof_ipc4_copier *host_copier = NULL; |
770 | struct sof_ipc4_copier *dai_copier = NULL; |
771 | struct sof_ipc4_llp_reading_slot llp_slot; |
772 | struct sof_ipc4_timestamp_info *info; |
773 | struct snd_soc_dapm_widget *widget; |
774 | struct snd_sof_dai *dai; |
775 | int i; |
776 | |
777 | /* find host & dai to locate info in memory window */ |
778 | for_each_dapm_widgets(spcm->list, i, widget) { |
779 | struct snd_sof_widget *swidget = widget->dobj.private; |
780 | |
781 | if (!swidget) |
782 | continue; |
783 | |
784 | if (WIDGET_IS_AIF(swidget->widget->id)) { |
785 | host_copier = swidget->private; |
786 | } else if (WIDGET_IS_DAI(swidget->widget->id)) { |
787 | dai = swidget->private; |
788 | dai_copier = dai->private; |
789 | } |
790 | } |
791 | |
792 | /* both host and dai copier must be valid for time_info */ |
793 | if (!host_copier || !dai_copier) { |
794 | dev_err(sdev->dev, "host or dai copier are not found\n" ); |
795 | return; |
796 | } |
797 | |
798 | info = spcm->private; |
799 | info->host_copier = host_copier; |
800 | info->dai_copier = dai_copier; |
801 | info->llp_offset = offsetof(struct sof_ipc4_fw_registers, llp_gpdma_reading_slots) + |
802 | sdev->fw_info_box.offset; |
803 | |
804 | /* find llp slot used by current dai */ |
805 | for (i = 0; i < SOF_IPC4_MAX_LLP_GPDMA_READING_SLOTS; i++) { |
806 | sof_mailbox_read(sdev, offset: info->llp_offset, message: &llp_slot, bytes: sizeof(llp_slot)); |
807 | if (llp_slot.node_id == dai_copier->data.gtw_cfg.node_id) |
808 | break; |
809 | |
810 | info->llp_offset += sizeof(llp_slot); |
811 | } |
812 | |
813 | if (i < SOF_IPC4_MAX_LLP_GPDMA_READING_SLOTS) |
814 | return; |
815 | |
816 | /* if no llp gpdma slot is used, check aggregated sdw slot */ |
817 | info->llp_offset = offsetof(struct sof_ipc4_fw_registers, llp_sndw_reading_slots) + |
818 | sdev->fw_info_box.offset; |
819 | for (i = 0; i < SOF_IPC4_MAX_LLP_SNDW_READING_SLOTS; i++) { |
820 | sof_mailbox_read(sdev, offset: info->llp_offset, message: &llp_slot, bytes: sizeof(llp_slot)); |
821 | if (llp_slot.node_id == dai_copier->data.gtw_cfg.node_id) |
822 | break; |
823 | |
824 | info->llp_offset += sizeof(llp_slot); |
825 | } |
826 | |
827 | if (i < SOF_IPC4_MAX_LLP_SNDW_READING_SLOTS) |
828 | return; |
829 | |
830 | /* check EVAD slot */ |
831 | info->llp_offset = offsetof(struct sof_ipc4_fw_registers, llp_evad_reading_slot) + |
832 | sdev->fw_info_box.offset; |
833 | sof_mailbox_read(sdev, offset: info->llp_offset, message: &llp_slot, bytes: sizeof(llp_slot)); |
834 | if (llp_slot.node_id != dai_copier->data.gtw_cfg.node_id) |
835 | info->llp_offset = 0; |
836 | } |
837 | |
838 | static int sof_ipc4_pcm_hw_params(struct snd_soc_component *component, |
839 | struct snd_pcm_substream *substream, |
840 | struct snd_pcm_hw_params *params, |
841 | struct snd_sof_platform_stream_params *platform_params) |
842 | { |
843 | struct snd_sof_dev *sdev = snd_soc_component_get_drvdata(c: component); |
844 | struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream); |
845 | struct sof_ipc4_timestamp_info *time_info; |
846 | struct snd_sof_pcm *spcm; |
847 | |
848 | spcm = snd_sof_find_spcm_dai(scomp: component, rtd); |
849 | if (!spcm) |
850 | return -EINVAL; |
851 | |
852 | time_info = spcm->stream[substream->stream].private; |
853 | /* delay calculation is not supported by current fw_reg ABI */ |
854 | if (!time_info) |
855 | return 0; |
856 | |
857 | time_info->stream_start_offset = SOF_IPC4_INVALID_STREAM_POSITION; |
858 | time_info->llp_offset = 0; |
859 | |
860 | sof_ipc4_build_time_info(sdev, spcm: &spcm->stream[substream->stream]); |
861 | |
862 | return 0; |
863 | } |
864 | |
865 | static int sof_ipc4_get_stream_start_offset(struct snd_sof_dev *sdev, |
866 | struct snd_pcm_substream *substream, |
867 | struct snd_sof_pcm_stream *stream, |
868 | struct sof_ipc4_timestamp_info *time_info) |
869 | { |
870 | struct sof_ipc4_copier *host_copier = time_info->host_copier; |
871 | struct sof_ipc4_copier *dai_copier = time_info->dai_copier; |
872 | struct sof_ipc4_pipeline_registers ppl_reg; |
873 | u32 dai_sample_size; |
874 | u32 ch, node_index; |
875 | u32 offset; |
876 | |
877 | if (!host_copier || !dai_copier) |
878 | return -EINVAL; |
879 | |
880 | if (host_copier->data.gtw_cfg.node_id == SOF_IPC4_INVALID_NODE_ID) |
881 | return -EINVAL; |
882 | |
883 | node_index = SOF_IPC4_NODE_INDEX(host_copier->data.gtw_cfg.node_id); |
884 | offset = offsetof(struct sof_ipc4_fw_registers, pipeline_regs) + node_index * sizeof(ppl_reg); |
885 | sof_mailbox_read(sdev, offset: sdev->fw_info_box.offset + offset, message: &ppl_reg, bytes: sizeof(ppl_reg)); |
886 | if (ppl_reg.stream_start_offset == SOF_IPC4_INVALID_STREAM_POSITION) |
887 | return -EINVAL; |
888 | |
889 | ch = dai_copier->data.out_format.fmt_cfg; |
890 | ch = SOF_IPC4_AUDIO_FORMAT_CFG_CHANNELS_COUNT(ch); |
891 | dai_sample_size = (dai_copier->data.out_format.bit_depth >> 3) * ch; |
892 | |
893 | /* convert offsets to frame count */ |
894 | time_info->stream_start_offset = ppl_reg.stream_start_offset; |
895 | do_div(time_info->stream_start_offset, dai_sample_size); |
896 | time_info->stream_end_offset = ppl_reg.stream_end_offset; |
897 | do_div(time_info->stream_end_offset, dai_sample_size); |
898 | |
899 | /* |
900 | * Calculate the wrap boundary need to be used for delay calculation |
901 | * The host counter is in bytes, it will wrap earlier than the frames |
902 | * based link counter. |
903 | */ |
904 | time_info->boundary = div64_u64(dividend: ~((u64)0), |
905 | divisor: frames_to_bytes(runtime: substream->runtime, size: 1)); |
906 | /* Initialize the delay value to 0 (no delay) */ |
907 | time_info->delay = 0; |
908 | |
909 | return 0; |
910 | } |
911 | |
912 | static int sof_ipc4_pcm_pointer(struct snd_soc_component *component, |
913 | struct snd_pcm_substream *substream, |
914 | snd_pcm_uframes_t *pointer) |
915 | { |
916 | struct snd_sof_dev *sdev = snd_soc_component_get_drvdata(c: component); |
917 | struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream); |
918 | struct sof_ipc4_timestamp_info *time_info; |
919 | struct sof_ipc4_llp_reading_slot llp; |
920 | snd_pcm_uframes_t head_cnt, tail_cnt; |
921 | struct snd_sof_pcm_stream *stream; |
922 | u64 dai_cnt, host_cnt, host_ptr; |
923 | struct snd_sof_pcm *spcm; |
924 | int ret; |
925 | |
926 | spcm = snd_sof_find_spcm_dai(scomp: component, rtd); |
927 | if (!spcm) |
928 | return -EOPNOTSUPP; |
929 | |
930 | stream = &spcm->stream[substream->stream]; |
931 | time_info = stream->private; |
932 | if (!time_info) |
933 | return -EOPNOTSUPP; |
934 | |
935 | /* |
936 | * stream_start_offset is updated to memory window by FW based on |
937 | * pipeline statistics and it may be invalid if host query happens before |
938 | * the statistics is complete. And it will not change after the first initiailization. |
939 | */ |
940 | if (time_info->stream_start_offset == SOF_IPC4_INVALID_STREAM_POSITION) { |
941 | ret = sof_ipc4_get_stream_start_offset(sdev, substream, stream, time_info); |
942 | if (ret < 0) |
943 | return -EOPNOTSUPP; |
944 | } |
945 | |
946 | /* For delay calculation we need the host counter */ |
947 | host_cnt = snd_sof_pcm_get_host_byte_counter(sdev, component, substream); |
948 | host_ptr = host_cnt; |
949 | |
950 | /* convert the host_cnt to frames */ |
951 | host_cnt = div64_u64(dividend: host_cnt, divisor: frames_to_bytes(runtime: substream->runtime, size: 1)); |
952 | |
953 | /* |
954 | * If the LLP counter is not reported by firmware in the SRAM window |
955 | * then read the dai (link) counter via host accessible means if |
956 | * available. |
957 | */ |
958 | if (!time_info->llp_offset) { |
959 | dai_cnt = snd_sof_pcm_get_dai_frame_counter(sdev, component, substream); |
960 | if (!dai_cnt) |
961 | return -EOPNOTSUPP; |
962 | } else { |
963 | sof_mailbox_read(sdev, offset: time_info->llp_offset, message: &llp, bytes: sizeof(llp)); |
964 | dai_cnt = ((u64)llp.reading.llp_u << 32) | llp.reading.llp_l; |
965 | } |
966 | dai_cnt += time_info->stream_end_offset; |
967 | |
968 | /* In two cases dai dma counter is not accurate |
969 | * (1) dai pipeline is started before host pipeline |
970 | * (2) multiple streams mixed into one. Each stream has the same dai dma |
971 | * counter |
972 | * |
973 | * Firmware calculates correct stream_start_offset for all cases |
974 | * including above two. |
975 | * Driver subtracts stream_start_offset from dai dma counter to get |
976 | * accurate one |
977 | */ |
978 | |
979 | /* |
980 | * On stream start the dai counter might not yet have reached the |
981 | * stream_start_offset value which means that no frames have left the |
982 | * DSP yet from the audio stream (on playback, capture streams have |
983 | * offset of 0 as we start capturing right away). |
984 | * In this case we need to adjust the distance between the counters by |
985 | * increasing the host counter by (offset - dai_counter). |
986 | * Otherwise the dai_counter needs to be adjusted to reflect the number |
987 | * of valid frames passed on the DAI side. |
988 | * |
989 | * The delay is the difference between the counters on the two |
990 | * sides of the DSP. |
991 | */ |
992 | if (dai_cnt < time_info->stream_start_offset) { |
993 | host_cnt += time_info->stream_start_offset - dai_cnt; |
994 | dai_cnt = 0; |
995 | } else { |
996 | dai_cnt -= time_info->stream_start_offset; |
997 | } |
998 | |
999 | /* Wrap the dai counter at the boundary where the host counter wraps */ |
1000 | div64_u64_rem(dividend: dai_cnt, divisor: time_info->boundary, remainder: &dai_cnt); |
1001 | |
1002 | if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { |
1003 | head_cnt = host_cnt; |
1004 | tail_cnt = dai_cnt; |
1005 | } else { |
1006 | head_cnt = dai_cnt; |
1007 | tail_cnt = host_cnt; |
1008 | } |
1009 | |
1010 | if (head_cnt < tail_cnt) { |
1011 | time_info->delay = time_info->boundary - tail_cnt + head_cnt; |
1012 | goto out; |
1013 | } |
1014 | |
1015 | time_info->delay = head_cnt - tail_cnt; |
1016 | |
1017 | out: |
1018 | /* |
1019 | * Convert the host byte counter to PCM pointer which wraps in buffer |
1020 | * and it is in frames |
1021 | */ |
1022 | div64_u64_rem(dividend: host_ptr, divisor: snd_pcm_lib_buffer_bytes(substream), remainder: &host_ptr); |
1023 | *pointer = bytes_to_frames(runtime: substream->runtime, size: host_ptr); |
1024 | |
1025 | return 0; |
1026 | } |
1027 | |
1028 | static snd_pcm_sframes_t sof_ipc4_pcm_delay(struct snd_soc_component *component, |
1029 | struct snd_pcm_substream *substream) |
1030 | { |
1031 | struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream); |
1032 | struct sof_ipc4_timestamp_info *time_info; |
1033 | struct snd_sof_pcm_stream *stream; |
1034 | struct snd_sof_pcm *spcm; |
1035 | |
1036 | spcm = snd_sof_find_spcm_dai(scomp: component, rtd); |
1037 | if (!spcm) |
1038 | return 0; |
1039 | |
1040 | stream = &spcm->stream[substream->stream]; |
1041 | time_info = stream->private; |
1042 | /* |
1043 | * Report the stored delay value calculated in the pointer callback. |
1044 | * In the unlikely event that the calculation was skipped/aborted, the |
1045 | * default 0 delay returned. |
1046 | */ |
1047 | if (time_info) |
1048 | return time_info->delay; |
1049 | |
1050 | /* No delay information available, report 0 as delay */ |
1051 | return 0; |
1052 | |
1053 | } |
1054 | |
1055 | const struct sof_ipc_pcm_ops ipc4_pcm_ops = { |
1056 | .hw_params = sof_ipc4_pcm_hw_params, |
1057 | .trigger = sof_ipc4_pcm_trigger, |
1058 | .hw_free = sof_ipc4_pcm_hw_free, |
1059 | .dai_link_fixup = sof_ipc4_pcm_dai_link_fixup, |
1060 | .pcm_setup = sof_ipc4_pcm_setup, |
1061 | .pcm_free = sof_ipc4_pcm_free, |
1062 | .pointer = sof_ipc4_pcm_pointer, |
1063 | .delay = sof_ipc4_pcm_delay, |
1064 | .ipc_first_on_start = true, |
1065 | .platform_stop_during_hw_free = true, |
1066 | }; |
1067 | |