The blanket 240 s audioreach_send_cmd_sync timeout (cfc2b6a276) broke
boot audio: apm_probe's GET_SPF_STATE ready poll gets no answer when
sent before the DSP audio framework is up (the command is simply lost),
so probe held the shared command mutex for the full 240 s. That stalls
snd_q6apm's module init past udev's worker timeout, q6prm never gets
loaded, the LPI pinctrl never finds its 'core' clock and the sound card
never assembles (every boot since 2026-07-10 had no card). It also
queued every later voice-proto command behind the mutex for minutes,
which sessions 8-11 misread as slow DSP responses.
Restore the mainline 30 s default via audioreach_send_cmd_sync_timeout
and pass 240 s only from the voice-proto blob paths (a cold TX
GRAPH_OPEN through the Fluence FastRPC loads takes >120 s).
Assisted-by: Claude:claude-fable-5
377 lines
11 KiB
C
377 lines
11 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* Voice-call bring-up prototype for the Fairphone (Gen. 6): replay
|
|
* pre-generated APM command sequences through the q6apm GPR channel.
|
|
*
|
|
* The blobs reproduce, byte for byte, the GRAPH_OPEN/SET_CFG/PREPARE/START
|
|
* sequence the stock Android PAL/AGM/GSL stack sends to set up the
|
|
* handset voice-call graphs (mined from the device ACDB; generated by
|
|
* utilities/mkvoiceblobs.py in the bring-up repo). The modem's voice
|
|
* engine and the ADSP VCPM service wire up the vocoder themselves once
|
|
* these graphs are running.
|
|
*
|
|
* Local prototype only, NOT for upstream. Interface:
|
|
* echo <fw-file> > /sys/kernel/debug/q6apm-voice-proto/play
|
|
* where <fw-file> is relative to /lib/firmware. Playback aborts on the
|
|
* first command the DSP rejects.
|
|
*/
|
|
|
|
#include <linux/debugfs.h>
|
|
#include <linux/dma-mapping.h>
|
|
#include <linux/firmware.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/of_platform.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/soc/qcom/apr.h>
|
|
#include <linux/uaccess.h>
|
|
#include <sound/soc.h>
|
|
#include "audioreach.h"
|
|
#include "q6apm.h"
|
|
|
|
#define Q6VP_MAGIC 0x50563651 /* "Q6VP" little-endian */
|
|
#define Q6VP_VERSION 1
|
|
|
|
/*
|
|
* A record opcode with this bit set is sent out-of-band: the payload is
|
|
* placed in an ADSP-mapped shared-memory region and the GPR command carries
|
|
* only an apm_cmd_header referencing it. Stock GSL sends every voice
|
|
* GRAPH_OPEN this way; it is mandatory because a voice graph must be opened
|
|
* atomically as one command (all subgraphs + cross-subgraph module
|
|
* connections and control links), and the whole payload (~4.3-4.7 KB)
|
|
* exceeds the ~4 KiB in-band GPR/GLINK intent limit. Splitting the open
|
|
* per-subgraph to fit in-band makes the cross-referencing TX DevicePP
|
|
* subgraph invalid (rejected at open) — see journal/calls.md 2026-07-08.
|
|
*/
|
|
#define Q6VP_OOB_FLAG 0x80000000
|
|
/*
|
|
* A record opcode with this bit set carries the GPR destination port in the
|
|
* first payload word (stripped before sending). Lets a blob address static
|
|
* services other than APM — e.g. AMDB (port 3) module pre-loading, so the
|
|
* dynamic Fluence pp modules can be made resident BEFORE the voice
|
|
* GRAPH_OPENs. A first open that has to FastRPC-load them can fail or block
|
|
* so long that the client-side retry dance corrupts VCPM's per-VSID voice
|
|
* session (TX cal EBADPARAM / TX START EFAILED, journal/calls.md
|
|
* 2026-07-10). In-band records only.
|
|
*/
|
|
#define Q6VP_DSTPORT_FLAG 0x40000000
|
|
#define Q6VP_GRAPH_ID 0xf000 /* private mem-map handle owner */
|
|
#define Q6VP_BUF_SZ 0x800000 /* 8 MiB shared region */
|
|
#define Q6VP_SCRATCH_SZ 0x10000 /* [0, 64K): transient commands */
|
|
#define Q6VP_SID_MASK 0xf
|
|
|
|
struct q6vp_hdr {
|
|
__le32 magic;
|
|
__le32 version;
|
|
__le32 num_records;
|
|
} __packed;
|
|
|
|
struct q6vp_rec {
|
|
__le32 opcode;
|
|
__le32 size;
|
|
} __packed;
|
|
|
|
static DEFINE_MUTEX(q6vp_play_lock);
|
|
static struct q6apm *q6vp_apm;
|
|
static struct dentry *q6vp_dir;
|
|
static struct device *q6vp_dais_dev;
|
|
static struct audioreach_graph_info *q6vp_info;
|
|
static void *q6vp_buf;
|
|
static dma_addr_t q6vp_buf_iova;
|
|
static phys_addr_t q6vp_buf_dsp_addr;
|
|
|
|
static int q6vp_send(uint32_t opcode, const void *payload, uint32_t size)
|
|
{
|
|
uint32_t dest_port = APM_MODULE_INSTANCE_ID;
|
|
uint32_t rsp_opcode = 0;
|
|
struct gpr_pkt *pkt;
|
|
int ret;
|
|
|
|
if (opcode & Q6VP_DSTPORT_FLAG) {
|
|
if (size < sizeof(__le32))
|
|
return -EINVAL;
|
|
dest_port = le32_to_cpup(payload);
|
|
payload += sizeof(__le32);
|
|
size -= sizeof(__le32);
|
|
opcode &= ~Q6VP_DSTPORT_FLAG;
|
|
}
|
|
|
|
/* AMDB load responds with its own opcode, not an ibasic result */
|
|
if (opcode == AMDB_CMD_LOAD_MODULES)
|
|
rsp_opcode = AMDB_CMD_RSP_LOAD_MODULES;
|
|
|
|
pkt = audioreach_alloc_cmd_pkt(size, opcode, 0, GPR_APM_MODULE_IID,
|
|
dest_port);
|
|
if (IS_ERR(pkt))
|
|
return PTR_ERR(pkt);
|
|
|
|
memcpy((void *)pkt + GPR_HDR_SIZE + APM_CMD_HDR_SIZE, payload, size);
|
|
|
|
/*
|
|
* Voice blobs need the long wait: a cold TX GRAPH_OPEN that has to
|
|
* FastRPC-load the Fluence pp modules takes >120 s. Regular audio
|
|
* commands (and especially the probe-time GET_SPF_STATE ready poll)
|
|
* must keep the short default or they hold the shared command mutex
|
|
* hostage at boot: a 240 s stall in apm_probe delays module loading
|
|
* past udev's worker timeout and the sound card never assembles.
|
|
*/
|
|
ret = q6apm_send_cmd_sync_timeout(q6vp_apm, pkt, rsp_opcode, 240);
|
|
kfree(pkt);
|
|
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Lazily set up the shared-memory region used for OOB commands: allocate a
|
|
* DMA buffer against the q6apm-dais child device (it carries the iommus
|
|
* mapping the ADSP expects, same as the PCM data path), register a private
|
|
* graph_info so the APM_CMD_RSP_SHARED_MEM_MAP_REGIONS callback has a slot
|
|
* to store the handle in, and map the region with the DSP. The mapping is
|
|
* kept until the module goes away.
|
|
*/
|
|
static int q6vp_oob_init(void)
|
|
{
|
|
struct device *dev = q6vp_apm->dev;
|
|
struct of_phandle_args args;
|
|
struct platform_device *pdev;
|
|
struct device_node *np;
|
|
u64 sid = 0;
|
|
u32 graph_id = Q6VP_GRAPH_ID;
|
|
int ret;
|
|
|
|
if (q6vp_info && q6vp_info->mem_map_handle)
|
|
return 0;
|
|
|
|
if (!q6vp_dais_dev) {
|
|
np = of_get_compatible_child(dev->of_node, "qcom,q6apm-dais");
|
|
if (!np) {
|
|
dev_err(dev, "voice-proto: no q6apm-dais node\n");
|
|
return -ENODEV;
|
|
}
|
|
if (!of_parse_phandle_with_fixed_args(np, "iommus", 1, 0,
|
|
&args)) {
|
|
sid = args.args[0] & Q6VP_SID_MASK;
|
|
of_node_put(args.np);
|
|
}
|
|
pdev = of_find_device_by_node(np);
|
|
of_node_put(np);
|
|
if (!pdev) {
|
|
dev_err(dev, "voice-proto: no q6apm-dais device\n");
|
|
return -ENODEV;
|
|
}
|
|
q6vp_dais_dev = &pdev->dev;
|
|
|
|
q6vp_buf = dma_alloc_coherent(q6vp_dais_dev, Q6VP_BUF_SZ,
|
|
&q6vp_buf_iova, GFP_KERNEL);
|
|
if (!q6vp_buf)
|
|
return -ENOMEM;
|
|
q6vp_buf_dsp_addr = q6vp_buf_iova | (sid << 32);
|
|
}
|
|
|
|
if (!q6vp_info) {
|
|
q6vp_info = kzalloc(sizeof(*q6vp_info), GFP_KERNEL);
|
|
if (!q6vp_info)
|
|
return -ENOMEM;
|
|
INIT_LIST_HEAD(&q6vp_info->sg_list);
|
|
q6vp_info->id = Q6VP_GRAPH_ID;
|
|
ret = idr_alloc_u32(&q6vp_apm->graph_info_idr, q6vp_info,
|
|
&graph_id, graph_id, GFP_KERNEL);
|
|
if (ret < 0) {
|
|
kfree(q6vp_info);
|
|
q6vp_info = NULL;
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
ret = q6apm_map_memory_fixed_region(q6vp_dais_dev, Q6VP_GRAPH_ID,
|
|
q6vp_buf_dsp_addr, Q6VP_BUF_SZ);
|
|
if (ret) {
|
|
dev_err(dev, "voice-proto: mem map failed (%d)\n", ret);
|
|
return ret;
|
|
}
|
|
|
|
dev_info(dev, "voice-proto: OOB region mapped, handle 0x%x\n",
|
|
q6vp_info->mem_map_handle);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Persistent-slice bump offset: APM_CMD_REGISTER_CFG registers the payload
|
|
* with the DSP, which keeps reading that memory for as long as the config
|
|
* stays registered — those payloads must never be overwritten. They get
|
|
* bump-allocated slices above Q6VP_SCRATCH_SZ that live until reboot;
|
|
* transient commands (opens etc.) reuse the scratch slot at offset 0.
|
|
*/
|
|
static u32 q6vp_persist_off = Q6VP_SCRATCH_SZ;
|
|
|
|
static int q6vp_send_oob(uint32_t opcode, const void *payload, uint32_t size)
|
|
{
|
|
struct apm_cmd_header *cmd_header;
|
|
struct gpr_pkt *pkt;
|
|
u32 off = 0;
|
|
int ret;
|
|
|
|
ret = q6vp_oob_init();
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (opcode == APM_CMD_REGISTER_CFG) {
|
|
if (q6vp_persist_off + size > Q6VP_BUF_SZ)
|
|
return -EFBIG;
|
|
off = q6vp_persist_off;
|
|
q6vp_persist_off += ALIGN(size, SZ_4K);
|
|
} else if (size > Q6VP_SCRATCH_SZ) {
|
|
return -EFBIG;
|
|
}
|
|
|
|
memcpy(q6vp_buf + off, payload, size);
|
|
|
|
pkt = audioreach_alloc_apm_cmd_pkt(0, opcode, 0);
|
|
if (IS_ERR(pkt))
|
|
return PTR_ERR(pkt);
|
|
|
|
/*
|
|
* The region is mapped in absolute-address mode (mainline's
|
|
* q6apm_map_memory_fixed_region passes property_flag 0, unlike GSL
|
|
* which maps with IS_OFFSET_MODE and then references offset 0), so
|
|
* the header carries the full DSP-visible address of the buffer.
|
|
*/
|
|
cmd_header = (void *)pkt + GPR_HDR_SIZE;
|
|
cmd_header->payload_address_lsw = lower_32_bits(q6vp_buf_dsp_addr + off);
|
|
cmd_header->payload_address_msw = upper_32_bits(q6vp_buf_dsp_addr + off);
|
|
cmd_header->mem_map_handle = q6vp_info->mem_map_handle;
|
|
cmd_header->payload_size = size;
|
|
|
|
ret = q6apm_send_cmd_sync_timeout(q6vp_apm, pkt, 0, 240);
|
|
kfree(pkt);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int q6vp_play(const char *name)
|
|
{
|
|
const struct firmware *fw;
|
|
const struct q6vp_hdr *hdr;
|
|
const struct q6vp_rec *rec;
|
|
struct device *dev = q6vp_apm->dev;
|
|
size_t off;
|
|
uint32_t i, num, opcode, size;
|
|
int ret, err = 0;
|
|
|
|
ret = request_firmware(&fw, name, dev);
|
|
if (ret) {
|
|
dev_err(dev, "voice-proto: cannot load %s (%d)\n", name, ret);
|
|
return ret;
|
|
}
|
|
|
|
hdr = (const struct q6vp_hdr *)fw->data;
|
|
if (fw->size < sizeof(*hdr) ||
|
|
le32_to_cpu(hdr->magic) != Q6VP_MAGIC ||
|
|
le32_to_cpu(hdr->version) != Q6VP_VERSION) {
|
|
dev_err(dev, "voice-proto: %s: bad header\n", name);
|
|
ret = -EINVAL;
|
|
goto out;
|
|
}
|
|
|
|
num = le32_to_cpu(hdr->num_records);
|
|
off = sizeof(*hdr);
|
|
for (i = 0; i < num; i++) {
|
|
if (off + sizeof(*rec) > fw->size) {
|
|
ret = -EINVAL;
|
|
goto out;
|
|
}
|
|
rec = (const struct q6vp_rec *)(fw->data + off);
|
|
opcode = le32_to_cpu(rec->opcode);
|
|
size = le32_to_cpu(rec->size);
|
|
off += sizeof(*rec);
|
|
if (off + size > fw->size) {
|
|
ret = -EINVAL;
|
|
goto out;
|
|
}
|
|
|
|
if (opcode & Q6VP_OOB_FLAG)
|
|
ret = (opcode & Q6VP_DSTPORT_FLAG) ? -EINVAL :
|
|
q6vp_send_oob(opcode & ~Q6VP_OOB_FLAG,
|
|
fw->data + off, size);
|
|
else
|
|
ret = q6vp_send(opcode, fw->data + off, size);
|
|
dev_info(dev, "voice-proto: %s[%u] opcode 0x%08x size %u -> %d\n",
|
|
name, i,
|
|
opcode & ~(Q6VP_OOB_FLAG | Q6VP_DSTPORT_FLAG),
|
|
size, ret);
|
|
/* keep going on DSP rejections; each result is logged */
|
|
if (ret)
|
|
err = ret;
|
|
|
|
off += ALIGN(size, 4);
|
|
}
|
|
|
|
out:
|
|
release_firmware(fw);
|
|
return ret ? ret : err;
|
|
}
|
|
|
|
static ssize_t q6vp_play_write(struct file *file, const char __user *ubuf,
|
|
size_t count, loff_t *ppos)
|
|
{
|
|
char name[128];
|
|
int ret;
|
|
|
|
if (!q6vp_apm)
|
|
return -ENODEV;
|
|
if (count >= sizeof(name))
|
|
return -EINVAL;
|
|
if (copy_from_user(name, ubuf, count))
|
|
return -EFAULT;
|
|
name[count] = '\0';
|
|
strim(name);
|
|
|
|
/*
|
|
* Serialize blob playback: concurrent debugfs writers (e.g. a
|
|
* dropped ssh session whose write is still stuck in a DSP wait)
|
|
* would interleave their records on the GPR channel and corrupt
|
|
* both sequences (seen 2026-07-10).
|
|
*/
|
|
ret = mutex_lock_interruptible(&q6vp_play_lock);
|
|
if (ret)
|
|
return ret;
|
|
ret = q6vp_play(name);
|
|
mutex_unlock(&q6vp_play_lock);
|
|
|
|
return ret ? ret : count;
|
|
}
|
|
|
|
static const struct file_operations q6vp_play_fops = {
|
|
.open = simple_open,
|
|
.write = q6vp_play_write,
|
|
.llseek = default_llseek,
|
|
};
|
|
|
|
void q6apm_voice_proto_init(struct q6apm *apm)
|
|
{
|
|
q6vp_apm = apm;
|
|
q6vp_dir = debugfs_create_dir("q6apm-voice-proto", NULL);
|
|
debugfs_create_file("play", 0200, q6vp_dir, NULL, &q6vp_play_fops);
|
|
}
|
|
|
|
void q6apm_voice_proto_exit(void)
|
|
{
|
|
debugfs_remove_recursive(q6vp_dir);
|
|
q6vp_dir = NULL;
|
|
|
|
if (q6vp_info) {
|
|
if (q6vp_info->mem_map_handle)
|
|
q6apm_unmap_memory_fixed_region(q6vp_dais_dev,
|
|
Q6VP_GRAPH_ID);
|
|
idr_remove(&q6vp_apm->graph_info_idr, Q6VP_GRAPH_ID);
|
|
kfree(q6vp_info);
|
|
q6vp_info = NULL;
|
|
}
|
|
if (q6vp_buf) {
|
|
dma_free_coherent(q6vp_dais_dev, Q6VP_BUF_SZ, q6vp_buf,
|
|
q6vp_buf_iova);
|
|
q6vp_buf = NULL;
|
|
}
|
|
q6vp_dais_dev = NULL;
|
|
q6vp_apm = NULL;
|
|
}
|