diff --git a/app/maxim_dsm_overlay.conf b/app/maxim_dsm_overlay.conf new file mode 100644 index 000000000000..7353ed620400 --- /dev/null +++ b/app/maxim_dsm_overlay.conf @@ -0,0 +1,35 @@ +# ACP7x smart_amp overlay -- enables Maxim DSM in three steps. +# +# Usage: +# ./scripts/xtensa-build-zephyr.py -p acp_7_x -o app/maxim_dsm_overlay.conf +# +# The three configurations (each step only flips switches in this file, +# opening them up progressively): +# +# Step 1, PASSTHRU_AMP : CONFIG_COMP_SMART_AMP=y only +# -> the Kconfig choice falls back to PASSTHRU_AMP +# -> builds smart_amp.c + smart_amp_generic.c + +# smart_amp_passthru.c +# -> does not link libdsm.a. Use this to validate the +# smart_amp framework, the topology and the duplex +# clocking. +# -> SMART_UUID in the topology must be +# passthru_smart_amp +# (64a794f0-55d3-4bca-9d5b-7b588badd037) +# +# Step 2, MAXIM_DSM_STUB : adds CONFIG_MAXIM_DSM=y + CONFIG_MAXIM_DSM_STUB=y +# -> swaps in smart_amp_maxim_dsm.c + maxim_dsm_stub.c +# -> still does not link libdsm.a (NOT STUB is false +# in CMakeLists) +# -> SMART_UUID in the topology becomes maxim_dsm +# (0cd84e80-ebd3-11ea-adc1-0242ac120002) +# +# Step 3, real libdsm.a : drops CONFIG_MAXIM_DSM_STUB +# -> zephyr_library_import takes effect and links +# src/audio/smart_amp/lib/release/dsm_lib/libdsm.a + + +CONFIG_COMP_SMART_AMP=y +CONFIG_MAXIM_DSM=y +# CONFIG_MAXIM_DSM_STUB=y + diff --git a/src/audio/smart_amp/maxim_dsm_stub.c b/src/audio/smart_amp/maxim_dsm_stub.c index 025db661c62d..eb2725d75793 100644 --- a/src/audio/smart_amp/maxim_dsm_stub.c +++ b/src/audio/smart_amp/maxim_dsm_stub.c @@ -5,11 +5,60 @@ // Author: Curtis Malainey // +/* Stub implementation of the Maxim DSM closed source API. + * + * The upstream stub returns DSM_API_OK from every function without writing + * any output fields. That satisfies the compiler (the Kconfig help text + * also says "only be used for CI and testing"), but always fails at runtime + * during comp_new: + * + * dsm_api_get_mem() never writes omemsizerequestedbytes + * -> maxim_dsm_get_handle_size() returns 0 + * -> smart_amp_alloc_mod_memblk(): the `if (ret == 0) return 0` branch + * skips both the allocation and the set_memblk() call + * -> hspk->dsmhandle remains NULL + * -> maxim_dsm_init(): `if (!hspk->dsmhandle) return -EINVAL` fires + * -> host sees `ipc tx error for 0x30010000 ... -22` + * + `Failed to setup widget SMART_AMP2.0`, card registration fails. + * + * This stub fills all output fields read back by the caller so that the + * smart_amp Maxim glue layer can fully initialise and run the data path. + * Feed-forward processing is a straight pass-through (input -> output), so + * audio is still audible under the stub, allowing the glue layer + topology + * + blob layout to be validated independently from the closed-source + * libdsm.a itself. + */ + #include "dsm_api_public.h" +/* Must match DSM_FRM_SZ in smart_amp_maxim_dsm.c: that file hard-codes + * DSM_FRM_SZ samples per frame in its ff/fb interleave/de-interleave loops, + * and a mismatch here causes sample misalignment. + */ +#define DSM_STUB_FRAME_SZ_SAMPLES 48 + +/* The real libdsm.a handle size is unknown. The stub itself does not use + * this memory; any non-zero value works. 4 KB is chosen to also exercise + * the PRIVATE memblk allocation path. + */ +#define DSM_STUB_HANDLE_SZ_BYTES 4096 + +/* maxim_dsm_get_volatile_param() iterates up to DSM_API_ADAPTIVE_PARAM_END + * (0x14); the parameter count must exceed that value or caldata overflows. + * Use the maximum value the API allows. + */ +#define DSM_STUB_MAX_NUM_PARAM DSM_DEFAULT_MAX_NUM_PARAM + enum DSM_API_MESSAGE dsm_api_get_mem(struct dsm_api_memory_size_ext_t *iopmmemparam, int iparamsize) { + if (!iopmmemparam) + return DSM_API_MSG_NULL_PARAM_POINTER; + if (iparamsize != (int)sizeof(*iopmmemparam)) + return DSM_API_MSG_INVALID_PARAM; + + iopmmemparam->omemsizerequestedbytes = DSM_STUB_HANDLE_SZ_BYTES; + return DSM_API_OK; } @@ -17,36 +66,94 @@ enum DSM_API_MESSAGE dsm_api_init(void *ipmodulehandler, struct dsm_api_init_ext_t *iopparamstruct, int iparamsize) { + if (!ipmodulehandler) + return DSM_API_MSG_NULL_MODULE_HANDLER; + if (!iopparamstruct) + return DSM_API_MSG_NULL_PARAM_POINTER; + if (iparamsize != (int)sizeof(*iopparamstruct)) + return DSM_API_MSG_INVALID_PARAM; + + /* Caller uses these two fields to compute ifsamples / ibsamples. */ + iopparamstruct->off_framesizesamples = DSM_STUB_FRAME_SZ_SAMPLES; + iopparamstruct->ofb_framesizesamples = DSM_STUB_FRAME_SZ_SAMPLES; + return DSM_API_OK; } -enum DSM_API_MESSAGE dsm_api_ff_process(void *ipmodulehandler, - int channelmask, - short *ibufferorg, - int *ipnrsamples, - short *obufferorg, - int *opnrsamples) +enum DSM_API_MESSAGE dsm_api_ff_process(void *ipmodulehandler, int channelmask, + short *ibufferorg, int *ipnrsamples, + short *obufferorg, int *opnrsamples) { + int nsamples; + int idx; + + if (!ipmodulehandler) + return DSM_API_MSG_NULL_MODULE_HANDLER; + if (!ibufferorg || !ipnrsamples) + return DSM_API_MSG_NULL_INPUT_BUFFER_POINTER; + if (!obufferorg || !opnrsamples) + return DSM_API_MSG_NULL_OUTPUT_BUFFER_POINTER; + + /* Pass-through: copy input to output unchanged so audio is audible under the stub. */ + nsamples = *ipnrsamples; + for (idx = 0; idx < nsamples; idx++) + obufferorg[idx] = ibufferorg[idx]; + + *opnrsamples = nsamples; + return DSM_API_OK; } -enum DSM_API_MESSAGE dsm_api_fb_process(void *ipmodulehandler, - int ichannelmask, - short *icurrbuffer, - short *ivoltbuffer, +enum DSM_API_MESSAGE dsm_api_fb_process(void *ipmodulehandler, int ichannelmask, + short *icurrbuffer, short *ivoltbuffer, int *iopnrsamples) { + if (!ipmodulehandler) + return DSM_API_MSG_NULL_MODULE_HANDLER; + if (!icurrbuffer || !ivoltbuffer || !iopnrsamples) + return DSM_API_MSG_NULL_INPUT_BUFFER_POINTER; + + /* Stub does no adaptation; IV feedback data is silently discarded. */ + return DSM_API_OK; } -enum DSM_API_MESSAGE dsm_api_set_params(void *ipmodulehandler, - int icommandnumber, void *ipparamsbuffer) +enum DSM_API_MESSAGE dsm_api_set_params(void *ipmodulehandler, int icommandnumber, + void *ipparamsbuffer) { + if (!ipmodulehandler) + return DSM_API_MSG_NULL_MODULE_HANDLER; + if (!ipparamsbuffer) + return DSM_API_MSG_NULL_PARAM_POINTER; + + /* Stub has no internal state; written tuning parameters are discarded. */ + return DSM_API_OK; } -enum DSM_API_MESSAGE dsm_api_get_params(void *ipmodulehandler, - int icommandnumber, void *opparams) +enum DSM_API_MESSAGE dsm_api_get_params(void *ipmodulehandler, int icommandnumber, + void *opparams) { + int *cmdblock = (int *)opparams; + + if (!ipmodulehandler) + return DSM_API_MSG_NULL_MODULE_HANDLER; + if (!cmdblock) + return DSM_API_MSG_NULL_PARAM_POINTER; + + /* Caller places the command id in cmdblock[DSM_GET_ID_IDX]; return + * values are read from CH1/CH2. The upstream stub leaves these fields + * unwritten, so the caller reads uninitialised stack data. + * maxim_dsm_get_num_param() is especially critical: it uses this result + * to size the caldata allocation. + */ + if (DSM_CH_MASK(cmdblock[DSM_GET_ID_IDX]) == DSM_API_GET_MAXIMUM_CMD_ID) { + cmdblock[DSM_GET_CH1_IDX] = DSM_STUB_MAX_NUM_PARAM; + cmdblock[DSM_GET_CH2_IDX] = DSM_STUB_MAX_NUM_PARAM; + } else { + cmdblock[DSM_GET_CH1_IDX] = 0; + cmdblock[DSM_GET_CH2_IDX] = 0; + } + return DSM_API_OK; } diff --git a/src/audio/smart_amp/smart_amp.c b/src/audio/smart_amp/smart_amp.c index 38577545b36e..52e6ca228f5f 100644 --- a/src/audio/smart_amp/smart_amp.c +++ b/src/audio/smart_amp/smart_amp.c @@ -90,10 +90,23 @@ static inline void smart_amp_free_mod_memories(struct smart_amp_data *sad) sad->mod_data = NULL; } +/* Alignment required for every buffer handed to the inner model. + * + * mod_alloc() requests no particular alignment, and on platforms built with + * CONFIG_SYS_HEAP_SMALL_ONLY=y and without CONFIG_SOF_ZEPHYR_HEAP_CACHED the + * Zephyr sys_heap chunk header is only 4 bytes, so every returned pointer is + * 4 modulo 8 (see chunk_mem() in zephyr/lib/heap/heap.c). Inner models using + * Xtensa HiFi 8-byte loads and stores (AE_L32X2 / AE_S32X2 / AE_L64 / AE_S64) + * then take an unhandled LoadStoreAlignmentCause exception. Ask for 16 bytes + * explicitly, matching the alignment the vendor libraries lay their internal + * memory blocks out on. + */ +#define SMART_AMP_BUF_ALIGN 16 + static inline int smart_amp_buf_alloc(struct processing_module *mod, struct smart_amp_buf *buf, size_t size) { - buf->data = mod_alloc(mod, size); + buf->data = mod_alloc_align(mod, size, SMART_AMP_BUF_ALIGN); if (!buf->data) return -ENOMEM; buf->size = size; @@ -739,21 +752,54 @@ static int smart_amp_prepare(struct processing_module *mod, int ret, i; comp_dbg(dev, "%d sources %d sinks", num_of_sources, num_of_sinks); + #if CONFIG_IPC_MAJOR_4 smart_amp_ipc4_params(mod); #endif /* In module API, state is managed by the framework, so no comp_set_state needed */ + + /* Neither pointer is cleared by smart_amp_reset(), so a re-prepare after + * the feedback branch was disconnected would otherwise keep using the + * stale buffer from the previous run. + */ + sad->source_buf = NULL; + sad->feedback_buf = NULL; + for (i = 0; i < num_of_sources; i++) { - /* NOTE: This should not work in module based environment: - * sources[i]->bound_module->dev->ipc_config.type == SOF_COMP_DEMUX - * So let's check which one of the sources is from a capture stream. - * The code is not tested and may not work. + struct comp_buffer *buf = comp_buffer_get_from_source(sources[i]); + struct comp_dev *producer; + + if (!buf) { + comp_err(dev, "source %d has no buffer", i); + return -ENOTCONN; + } + + /* + * Tell the feedback source from the playback one. + * + * Do not use sources[i]->bound_module here: it is only ever assigned by + * module_bind(), whose sole caller lives in module_adapter_ipc4.c, so it + * stays NULL on IPC3 platforms (e.g. AMD ACP7x) and dereferencing it + * faults the DSP. + * + * The feedback buffer is filled by the demux of the echo reference + * capture pipeline, so its producer belongs to a different pipeline than + * this module, while the playback source is filled by the host component + * of our own pipeline. Comparing pipeline ids works under both IPC + * versions, and unlike comp_dev::direction it is already valid here even + * when the capture pipeline has not been parametrised yet. */ - if (sources[i]->bound_module->dev->direction == SOF_IPC_STREAM_CAPTURE) - sad->feedback_buf = comp_buffer_get_from_source(sources[i]); + producer = comp_buffer_get_source_component(buf); + if (producer && dev_comp_pipe_id(producer) != dev_comp_pipe_id(dev)) + sad->feedback_buf = buf; else - sad->source_buf = comp_buffer_get_from_source(sources[i]); + sad->source_buf = buf; + } + + if (!sad->source_buf) { + comp_err(dev, "no playback source buffer"); + return -ENOTCONN; } /* sink buffer */ diff --git a/src/audio/smart_amp/smart_amp_maxim_dsm.c b/src/audio/smart_amp/smart_amp_maxim_dsm.c index 4b808dfaaf36..24fb0e014338 100644 --- a/src/audio/smart_amp/smart_amp_maxim_dsm.c +++ b/src/audio/smart_amp/smart_amp_maxim_dsm.c @@ -27,7 +27,55 @@ LOG_MODULE_DECLARE(smart_amp, CONFIG_SOF_LOG_LEVEL); #define DSM_FF_BUF_SZ (DSM_FRM_SZ * SMART_AMP_FF_MAX_CH_NUM) #define DSM_FB_BUF_SZ (DSM_FRM_SZ * SMART_AMP_FB_MAX_CH_NUM) -#define DSM_FF_BUF_DB_SZ (DSM_FF_BUF_SZ * SMART_AMP_FF_MAX_CH_NUM) +/* Capacity of the ff accumulation ring buffer. + * + * libdsm's frame size is fixed at 1 ms, so one DSM frame is DSM_FRM_SZ=48 + * samples/channel = DSM_FF_BUF_SZ=96 samples and a SOF period holds + * period_us/1000 frames: ACP7x TDM2 at 2000 us delivers 2 frames. + * + * The upstream value (DSM_FF_BUF_SZ * SMART_AMP_FF_MAX_CH_NUM = 192) fits one + * period but not also the pre-fill frame, so maxim_dsm_ff_proc's entry check + * (96 + 192 > 192) is always true: it returns -EOVERFLOW forever and never + * calls dsm_api_ff_process. It was written assuming period = 1000 us. + * + * 4 * DSM_FF_BUF_SZ = 384 holds a residual w_ptr (< 96) plus one period, and + * covers periods up to 3000 us. ff_out shares the constant; at 3000 us its + * r_ptr peaks at 384, exactly saturating the buffer. + */ +#define DSM_FF_BUF_DB_SZ (DSM_FF_BUF_SZ * 4) + +/* IV format for the feedback path. + * + * Upstream de-interleaves at 4*idx, assuming 4 TDM slots in VIVI order. This + * platform (Steam Deck / TDM2 / 2x max98388) has only 2 slots: each max98388 + * owns one slot and alternates V and I within it using the sample MSB as the + * flag, i.e. libdsm's DSM_IV_FORMAT_INTERLEAVED_16_BIT_MSB (ivFormat=3). + * libdsm then reads the two input buffers as the left and right channel streams + * (so the "voltage"/"current" names are misleading in this mode), takes + * DSM_FRM_SZ consecutive samples from each, splits them into 24 V + 24 I by MSB + * and duplicates each for 24k -> 48k upsampling. + * + * Keeping the 4*idx stride here would read every other sample of one channel; + * as the MSB strictly alternates, that picks a single polarity and leaves the + * other class empty, making libdsm return DSM_API_MSG_INSUFFICIENT_INPUT_DATA. + */ +#define MAXIM_DSM_FB_IVFMT3 1 + +/* Command ID and IV format enum from the vendor dsm_api.h; not included in + * SOF's trimmed dsm_api_public.h. + */ +#define DSM_API_SETGET_IV_FORMAT 146 +#define DSM_IV_FORMAT_INTERLEAVED_16_BIT_MSB 3 + +/* Samples consumed from the accumulation buffer per DSM fb frame. + * ivFormat=3: DSM_FRM_SZ consecutive samples per channel x 2 physical channels. + * Upstream 4-channel VIVI: DSM_FB_BUF_SZ (= DSM_FRM_SZ * 4). + */ +#if MAXIM_DSM_FB_IVFMT3 +#define DSM_FB_FRM_SZ (DSM_FRM_SZ * 2) +#else +#define DSM_FB_FRM_SZ DSM_FB_BUF_SZ +#endif #define DSM_FB_BUF_DB_SZ (DSM_FB_BUF_SZ * SMART_AMP_FB_MAX_CH_NUM) /* DSM parameter table structure @@ -168,6 +216,22 @@ static int maxim_dsm_init(struct smart_amp_mod_struct_t *hspk) * initparam.ichannels; } +#if MAXIM_DSM_FB_IVFMT3 + /* dsm_api_init_ext_t has no ivFormat field and libdsm defaults to + * DSM_IV_FORMAT_DEINTERLEAVED_16_BIT(0), so the MSB-interleaved layout + * this platform uses must be set explicitly. Runs before + * maxim_dsm_get_all_param() so the parameter db captures it. + */ + { + int value[DSM_SET_PARAM_SZ_PAYLOAD]; + + value[DSM_SET_ID_IDX] = DSM_SET_CMD_ID(DSM_API_SETGET_IV_FORMAT); + value[DSM_SET_VALUE_IDX] = DSM_IV_FORMAT_INTERLEAVED_16_BIT_MSB; + if (dsm_api_set_params(hspk->dsmhandle, 1, value) != DSM_API_OK) + comp_err(dev, "[DSM] set ivFormat failed"); + } +#endif + comp_dbg(dev, "[DSM] Initialization completed. (module:%p, dsm:%p)", (uintptr_t)hspk, (uintptr_t)hspk->dsmhandle); @@ -468,6 +532,29 @@ static int maxim_dsm_set_config(struct smart_amp_mod_data_base *mod, return maxim_dsm_set_param(hspk, cdata); } +/* Shift the unconsumed tail of an accumulation buffer down to its base: copy + * @count samples from [@offset, @offset + @count) to [0, @count), in a buffer + * whose samples are @szsample bytes wide. + * + * memcpy_s() must not be used: SOF's implementation + * (zephyr/include/rtos/string.h) detects overlapping src/dst, returns -EINVAL + * and copies nothing. The ranges overlap as soon as count > offset, which + * every period longer than 2 ms produces on every cycle, and the call sites + * advance their write pointer regardless of the return value -- so the buffer + * would silently desynchronise. Losing memcpy_s()'s bounds check costs + * nothing: every call site passes dest_size == count, making it vacuous, and + * the invariant that matters (offset + count inside the buffer) is held by the + * overflow test each caller runs first. + */ +static void dsm_buf_shift_down(void *base, int offset, int count, int szsample) +{ + if (count <= 0) + return; + + memmove(base, (char *)base + (size_t)offset * szsample, + (size_t)count * szsample); +} + static int maxim_dsm_ff_proc(struct smart_amp_mod_data_base *mod, uint32_t frames, struct smart_amp_mod_stream *in, @@ -484,7 +571,6 @@ static int maxim_dsm_ff_proc(struct smart_amp_mod_data_base *mod, bool is_16bit = (in->frame_fmt == SOF_IPC_FRAME_S16_LE); int szsample = (is_16bit ? 2 : 4); int nsamples = frames * in->channels; - int remain; int idx; int ret = 0; @@ -518,8 +604,13 @@ static int maxim_dsm_ff_proc(struct smart_amp_mod_data_base *mod, goto error; } - /* Run DSM Feedforward process if the buffer is ready */ - if (*w_ptr >= DSM_FF_BUF_SZ) { + /* Run DSM Feedforward process if the buffer is ready. + * Use while, not if: a 2 ms SOF period contains 2 DSM frames (1 ms each); + * an if would drain only one frame per call, causing w_ptr to grow by + * DSM_FF_BUF_SZ every period and the buffer to overflow. + */ + while (*w_ptr >= DSM_FF_BUF_SZ && + *r_ptr + DSM_FF_BUF_SZ <= DSM_FF_BUF_DB_SZ) { if (is_16bit) { /* Buffer ordering for DSM : LRLR... -> LL...RR... */ for (idx = 0; idx < DSM_FRM_SZ; idx++) { @@ -535,19 +626,22 @@ static int maxim_dsm_ff_proc(struct smart_amp_mod_data_base *mod, } } - remain = (*w_ptr - DSM_FF_BUF_SZ); - if (remain) { - if (is_16bit) - memcpy_s(&buf.buf16[0], remain * szsample, - &buf.buf16[DSM_FF_BUF_SZ], - remain * szsample); - else - memcpy_s(&buf.buf32[0], remain * szsample, - &buf.buf32[DSM_FF_BUF_SZ], - remain * szsample); - } + /* Drop the DSM frame just de-interleaved out of the head of + * the accumulation buffer. Overlaps whenever w_ptr > 2 * + * DSM_FF_BUF_SZ, i.e. at any period above 2 ms; see + * dsm_buf_shift_down(). + */ + dsm_buf_shift_down(hspk->buf.ff.buf, DSM_FF_BUF_SZ, + *w_ptr - DSM_FF_BUF_SZ, szsample); *w_ptr -= DSM_FF_BUF_SZ; + /* One call covers both sample widths: input/input32 and + * output/output32 are just differently typed views of the same + * hspk->buf.input / hspk->buf.output, and the short * in the + * prototype is vestigial -- libdsm strides the buffers by + * pCommonParams->iSampleByteWidth, which dsm_api_init() derived + * from initparam.isamplebitwidth (16 -> 2 bytes, 24/32 -> 4). + */ hspk->ifsamples = hspk->nchannels * hspk->ff_fr_sz_samples; dsm_api_ff_process(hspk->dsmhandle, hspk->channelmask, input, &hspk->ifsamples, @@ -561,9 +655,6 @@ static int maxim_dsm_ff_proc(struct smart_amp_mod_data_base *mod, output[idx + DSM_FRM_SZ]; } } else { - dsm_api_ff_process(hspk->dsmhandle, hspk->channelmask, - (short *)input32, &hspk->ifsamples, - (short *)output32, &hspk->ofsamples); for (idx = 0; idx < DSM_FRM_SZ; idx++) { buf_out.buf32[*r_ptr + 2 * idx] = output32[idx]; buf_out.buf32[*r_ptr + 2 * idx + 1] = @@ -583,17 +674,14 @@ static int maxim_dsm_ff_proc(struct smart_amp_mod_data_base *mod, memcpy_s(out->buf.data, nsamples * szsample, buf_out.buf32, nsamples * szsample); - remain = (*r_ptr - nsamples); - if (remain) { - if (is_16bit) - memcpy_s(&buf_out.buf16[0], remain * szsample, - &buf_out.buf16[nsamples], - remain * szsample); - else - memcpy_s(&buf_out.buf32[0], remain * szsample, - &buf_out.buf32[nsamples], - remain * szsample); - } + /* Same construct on the output side. It does not overlap at + * the periods used today (r_ptr never exceeds 2 * nsamples), + * but that bound is an emergent property of the loop guard + * above rather than anything enforced here, so keep it on the + * overlap-safe primitive too. + */ + dsm_buf_shift_down(hspk->buf.ff_out.buf, nsamples, + *r_ptr - nsamples, szsample); *r_ptr -= nsamples; return ret; } @@ -626,7 +714,6 @@ static int maxim_dsm_fb_proc(struct smart_amp_mod_data_base *mod, bool is_16bit = (in->frame_fmt == SOF_IPC_FRAME_S16_LE); int szsample = (is_16bit ? 2 : 4); int nsamples = frames * in->channels; - int remain; int idx; buf.buf16 = (int16_t *)hspk->buf.fb.buf; @@ -654,9 +741,27 @@ static int maxim_dsm_fb_proc(struct smart_amp_mod_data_base *mod, return -EOVERFLOW; } - /* Run DSM Feedback process if the buffer is ready */ - if (*w_ptr >= DSM_FB_BUF_SZ) { + /* Run DSM Feedback process if the buffer is ready. + * Use while, not if: same reason as the ff path -- a 2 ms SOF period + * contains 2 DSM frames. + */ + while (*w_ptr >= DSM_FB_FRM_SZ) { if (is_16bit) { +#if MAXIM_DSM_FB_IVFMT3 + /* De-interleave 2-channel PCM into per-slot streams. + * In ivFormat=3, libdsm treats icurrbuffer as the left + * channel (slot 0 / amp0) and ivoltbuffer as the right + * channel (slot 1 / amp1), extracting V and I from each + * using the sample MSB. The variable names i/v are + * inherited from the upstream VIVI path and are misleading + * here: both buffers contain interleaved V+I, not pure + * current or voltage. + */ + for (idx = 0; idx < DSM_FRM_SZ; idx++) { + i[idx] = buf.buf16[2 * idx]; /* slot0 / amp0 -> left */ + v[idx] = buf.buf16[2 * idx + 1]; /* slot1 / amp1 -> right */ + } +#else for (idx = 0; idx < DSM_FRM_SZ; idx++) { /* Buffer ordering for DSM : VIVI... -> VV... II...*/ v[idx] = buf.buf16[4 * idx]; @@ -664,6 +769,7 @@ static int maxim_dsm_fb_proc(struct smart_amp_mod_data_base *mod, v[idx + DSM_FRM_SZ] = buf.buf16[4 * idx + 2]; i[idx + DSM_FRM_SZ] = buf.buf16[4 * idx + 3]; } +#endif } else { for (idx = 0; idx < DSM_FRM_SZ; idx++) { v[idx] = buf.buf32[4 * idx]; @@ -675,28 +781,30 @@ static int maxim_dsm_fb_proc(struct smart_amp_mod_data_base *mod, } } - remain = (*w_ptr - DSM_FB_BUF_SZ); - if (remain) { - if (is_16bit) - memcpy_s(&buf.buf16[0], remain * szsample, - &buf.buf16[DSM_FB_BUF_SZ], - remain * szsample); - else - memcpy_s(&buf.buf32[0], remain * szsample, - &buf.buf32[DSM_FB_BUF_SZ], - remain * szsample); - } - *w_ptr -= DSM_FB_BUF_SZ; - + /* Drop the DSM frame just de-interleaved; same overlap + * boundary as the ff path. See dsm_buf_shift_down(). + */ + dsm_buf_shift_down(hspk->buf.fb.buf, DSM_FB_FRM_SZ, + *w_ptr - DSM_FB_FRM_SZ, szsample); + *w_ptr -= DSM_FB_FRM_SZ; + + /* *ipNrSamples is the total across both buffers regardless of + * ivFormat: this path divides it by FB_DSM_channels (= 2), then + * rejects the call unless the per-channel count is a multiple of + * ffFrameSize. So it must be fb_fr_sz_samples * nchannels = 96 + * (96 / 2 = 48, 48 % 48 == 0, frameNum = 1); passing 48 gives + * 24 % 48 != 0 and returns INVALID_PARAM every call. The + * ivFormat-aware variant that skips the divide is + * DSM_API_FB_process_v2, which this path does not call. + */ hspk->ibsamples = hspk->fb_fr_sz_samples * hspk->nchannels; if (is_16bit) - dsm_api_fb_process(hspk->dsmhandle, - hspk->channelmask, + dsm_api_fb_process(hspk->dsmhandle, hspk->channelmask, i, v, &hspk->ibsamples); else - dsm_api_fb_process(hspk->dsmhandle, - hspk->channelmask, - (short *)i32, (short *)v32, &hspk->ibsamples); + dsm_api_fb_process(hspk->dsmhandle, hspk->channelmask, + (short *)i32, (short *)v32, + &hspk->ibsamples); } return 0; } diff --git a/tools/topology/topology1/sof-acp_7_x_i2s.m4 b/tools/topology/topology1/sof-acp_7_x_i2s.m4 index 6f633f700a56..98658539c4ba 100644 --- a/tools/topology/topology1/sof-acp_7_x_i2s.m4 +++ b/tools/topology/topology1/sof-acp_7_x_i2s.m4 @@ -137,18 +137,70 @@ dnl PCM_DUPLEX_ADD(name, pcm_id, playback_pipeline, capture_pipeline) PCM_DUPLEX_ADD(I2STDM1, 1, PIPELINE_PCM_1, PIPELINE_PCM_4) #==================================================================================================================== -# TDM instance 2 +# TDM instance 2 -- speaker path through smart_amp (2x max98388) +# +# playback: host PCM 2 --B0--> smart_amp --B1--> ACPTDM2 TX +# ^ +# B2 (IV feedback) +# | +# capture : ACPTDM2 RX --B1--> demux --B0--> host PCM 2 (echo reference) +# + +dnl Macros required by both smart_amp pipe files; either missing triggers fatal_error. +dnl Values are for a 2-channel configuration, consistent with the +dnl ACP_TDM(2, 32, 3, 3) (2 slots) used by this link. +dnl SMART_PB_PPL_ID : playback pipeline id +dnl SMART_REF_PPL_ID : echo reference capture pipeline id +dnl SMART_PB_CH_NUM : playback channel count +dnl SMART_TX_CHANNELS/SMART_RX_CHANNELS : DAI TX/RX channel count +dnl SMART_FB_CHANNELS: feedback (IV) channel count +dnl SMART_REF_CH_NUM : echo reference channel count +define(`SMART_PB_PPL_ID', 2) +define(`SMART_REF_PPL_ID', 5) +define(`SMART_PB_CH_NUM', 2) +define(`SMART_TX_CHANNELS', 2) +define(`SMART_RX_CHANNELS', 2) +define(`SMART_FB_CHANNELS', 2) +define(`SMART_REF_CH_NUM', 2) + +dnl ACP7x TDM2 has only 2 slots; each max98388 occupies one slot and +dnl interleaves V and I within that slot (ivFormat=3, MSB flag). The echo +dnl reference second channel must therefore come from in ch1 (slot 1, second +dnl amp), not from the pipe default in ch2 (which is the slot-2 position in a +dnl 4-slot layout, does not exist in a 2-channel stream, and is silently +dnl discarded by mux.c:215, permanently silencing ch1). +define(`REF_CHMAP', `0x01,0x02,0x00,0x00,0x00,0x00,0x00,0x00') + +dnl smart_amp.c registers one of two UUIDs depending on CONFIG_MAXIM_DSM: +dnl CONFIG_MAXIM_DSM=y -> maxim_dsm 0cd84e80-ebd3-11ea-adc1-0242ac120002 +dnl otherwise (PASSTHRU_AMP) -> passthru_smart_amp 64a794f0-55d3-4bca-9d5b-7b588badd037 +dnl A mismatch between the topology and the firmware causes comp_new to fail +dnl with -EINVAL (driver not found), so this must be updated whenever the +dnl Kconfig is switched. +dnl SMART_UUID must be defined before PIPELINE_PCM_ADD: the playback pipe +dnl tests `ifdef(`SMART_UUID',...)`. Defining it explicitly here (rather +dnl than relying on the pipe's own default) makes the current build mode +dnl visible at a glance in this file. +DECLARE_SOF_RT_UUID("maxim_dsm", maxim_dsm_smart_amp_uuid, 0x0cd84e80, 0xebd3, 0x11ea, 0xad, 0xc1, 0x02, 0x42, 0xac, 0x12, 0x00, 0x02) +define(`SMART_UUID', maxim_dsm_smart_amp_uuid) +dnl Passthru variant (use when CONFIG_MAXIM_DSM is not set): +dnl DECLARE_SOF_RT_UUID("passthru_smart_amp", passthru_smart_amp_uuid, 0x64a794f0, 0x55d3, 0x4bca, 0x9d, 0x5b, 0x7b, 0x58, 0x8b, 0xad, 0xd0, 0x37) +dnl define(`SMART_UUID', passthru_smart_amp_uuid) dnl PIPELINE_PCM_ADD(pipeline, dnl pipe id, pcm, max channels, format, dnl period, priority, core, dnl pcm_min_rate, pcm_max_rate, pipeline_rate) -PIPELINE_PCM_ADD(sof/pipe-passthrough-playback.m4, +dnl Playback uses the upstream pipe; SMART_UUID defined above selects the Maxim +dnl DSM build of smart_amp. +PIPELINE_PCM_ADD(sof/pipe-smart-amplifier-playback.m4, 2, 2, 2, s16le, 2000, 0, 0, 48000, 48000, 48000) -PIPELINE_PCM_ADD(sof/pipe-passthrough-capture.m4, + +dnl Capture uses the upstream original (the reference project has no AMD-specific version). +PIPELINE_PCM_ADD(sof/pipe-amp-ref-capture.m4, 5, 2, 2, s16le, 2000, 0, 0, 48000, 48000, 48000) @@ -158,6 +210,9 @@ dnl pipe id, dai type, dai_index, dai_be, dnl buffer, periods, format, dnl deadline, priority, core, time_domain) +dnl Keep SCHEDULE_TIME_DOMAIN_TIMER as used by the existing ACP7x platform. +dnl The reference project uses DMA scheduling, which is a Vangogh/ACPHS +dnl platform difference that does not apply here. DAI_ADD(sof/pipe-dai-playback.m4, 2, ACPTDM, 2, acp-i2s2-codec, PIPELINE_SOURCE_2, 2, s16le, @@ -180,8 +235,38 @@ DAI_CONFIG(ACPTDM, 2, 2, acp-i2s2-codec, ACP_CLOCK(fsync, 48000, codec_consumer), ACP_TDM(2, 32, 3, 3), ACPTDM_CONFIG_DATA(ACPTDM, 2, 48000, 2, 0))) +dnl Wire the capture pipeline's demux output into the smart_amp feedback buffer +dnl in the playback pipeline. This cross-pipeline connection cannot be +dnl expressed in a single pipe file and must be added here. +dnl N_SMART_REF_BUF is defined by the playback pipe (BUF2.2); +dnl N_SMART_DEMUX is defined by the capture pipe. +ifdef(`N_SMART_REF_BUF',`',`fatal_error(note: N_SMART_REF_BUF undefined - playback pipe missing +)') +ifdef(`N_SMART_DEMUX',`',`fatal_error(note: N_SMART_DEMUX undefined - ref capture pipe missing +)') +SectionGraph."PIPE_SMART_AMP" { + index "0" + + lines [ + # demux -> smart_amp feedback + dapm(N_SMART_REF_BUF, N_SMART_DEMUX) + ] +} + dnl PCM_DUPLEX_ADD(name, pcm_id, playback_pipeline, capture_pipeline) +dnl Keep the name I2STDM2 to avoid disturbing host-side UCM / existing test commands. PCM_DUPLEX_ADD(I2STDM2, 2, PIPELINE_PCM_2, PIPELINE_PCM_5) + +dnl Clean up macros introduced by this section. +undefine(`SMART_PB_PPL_ID') +undefine(`SMART_REF_PPL_ID') +undefine(`SMART_PB_CH_NUM') +undefine(`SMART_TX_CHANNELS') +undefine(`SMART_RX_CHANNELS') +undefine(`SMART_FB_CHANNELS') +undefine(`SMART_REF_CH_NUM') +undefine(`REF_CHMAP') +undefine(`SMART_UUID') #==================================================================================================================== DEBUG_END diff --git a/tools/topology/topology1/sof/pipe-amp-ref-capture.m4 b/tools/topology/topology1/sof/pipe-amp-ref-capture.m4 index f605496144bf..eb4468dbd9ec 100644 --- a/tools/topology/topology1/sof/pipe-amp-ref-capture.m4 +++ b/tools/topology/topology1/sof/pipe-amp-ref-capture.m4 @@ -43,10 +43,17 @@ ifelse(SMART_FB_CHANNELS, `8', `define(`FB_CHMAP',`0x01,0x02,0x04,0x08,0x00,0x00,0x00,0x00')' ) -ifelse(SMART_REF_CH_NUM, `4', +dnl REF_CHMAP may be pre-defined by the platform m4 to override the default. +dnl The default non-4-channel branch maps out ch1 to in ch2, which is correct +dnl for 4-slot VIVI-interleaved hardware. On platforms with only 2 TDM slots +dnl where V and I are interleaved within the same slot (e.g. AMD ACP7x with +dnl 2x max98388), the second amp's data sits in slot 1, not slot 2; using the +dnl default would map out ch1 to a non-existent in ch2 which mux.c silently +dnl discards, making ch1 permanently silent. +ifdef(`REF_CHMAP',`',`ifelse(SMART_REF_CH_NUM, `4', `define(`REF_CHMAP',`0x01,0x02,0x04,0x08,0x00,0x00,0x00,0x00')', `define(`REF_CHMAP',`0x01,0x04,0x00,0x00,0x00,0x00,0x00,0x00')' -) +)') # should be aligned with struct sof_mux_config, used for mux input/output configuration.