Compare commits

...
Sign in to create a new pull request.

35 commits

Author SHA1 Message Date
a4301a82eb Merge branch 'sensors' into combined
QMC6308 magnetometer via bitbanged sensor i2c bus (gpio153/154).

# Conflicts:
#	arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts
2026-07-14 22:18:06 +02:00
fb5953a877 net: qrtr: ns: push adsp service announcements to the modem (FP6 voice)
The DSP firmwares expect the downstream IPC-router flooding model: a
node never sends NEW_LOOKUP for a service hosted on a peer node and
relies on the application processor pushing every NEW_SERVER/DEL_SERVER
to every link. Mainline's name service only broadcasts local servers;
remote servers reach other remote nodes solely via persistent lookups
nobody in the firmware issues.

Observed on the FP6 (SM7635): at boot the modem queries the in-kernel
pd-mapper for 'avs/audio' and receives 'msm/adsp/audio_pd' instance 74,
but never contacts the ADSP's service-registry notifier (svc 66 inst 74,
node 5 port 2) because it was never told that service exists - zero
forwarded modem->adsp packets across a full modem boot with the
forwarding path instrumented. Without the avs/audio-up notification the
modem's Voice Services never creates a vocoder session (empty
placeholder encoder/decoder during live VoLTE calls AND CVD loopback,
journal/calls.md sessions 15/16).

A first version of this patch flooded every remote announcement to every
hello'd node, downstream-style; it wedged the phone within ~20 s of the
DSPs booting, recurring in waves (control packets are exempt from qrtr
flow control, so any echo between routers runs unbounded, and DSP
crash/recovery cycles re-trigger it). Push only the one edge voice
needs instead: ADSP-hosted servers, to the modem, both live (new/del)
and replayed when the modem says HELLO. Any echo is structurally
loop-free: a re-announcement arriving from the modem is pushed to
nobody (target == origin). Pairs with the data-path forwarding carry
0c63174949.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-12 18:45:57 +02:00
ff614d787e voice-proto: dump module events, wake sync waiter on unlisted results
- apm_callback: hexdump APM_EVENT_MODULE_TO_CLIENT (0x03001000) packets
  so module events (e.g. VCPM's) are visible in dmesg if delivered.
- GPR_BASIC_RSP_RESULT default case now records the result and wakes the
  waiter: probe opcodes sent via voice-proto (REGISTER_MODULE_EVENTS,
  future ones) get their DSP error immediately instead of stalling for
  the full command timeout.
2026-07-11 23:03:10 +02:00
4f9a6b04d8 voice-proto: APM_CMD_GET_CFG support with response dump
Route GET_CFG through the voice-proto blob player: expect
APM_CMD_RSP_GET_CFG (hexdumped in apm_callback) and route ibasic error
results for GET_CFG so DSP rejections return immediately instead of
timing out.

Used as an in-graph probe: reading PARAM_ID_REAL_MODULE_ID from the
placeholder encoder/decoder during a live VoLTE call shows whether the
modem's Voice Services installed a vocoder (2026-07-11: it never does -
that is the remaining call-audio blocker; see journal/calls.md).
2026-07-11 22:25:03 +02:00
806ec77a1f voice-proto: long command timeout only for voice blob sends
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
2026-07-11 17:20:11 +02:00
2d3c1ed4cf voice-proto: don't free a reused static-PD heap on spawn failure
If a repeat audiopd spawn fails (e.g. remote invoke timeout), the error
path freed the persistent remote heap that a previous spawn had
SCM-assigned to the LPASS/ADSP VMIDs, without reclaiming it for HLOS.
The protected pages go back to the page allocator and the next 3 MB
dma_alloc dies with a synchronous external abort in dcache_clean_poc
(observed 2026-07-11 after an adsprpcd-audiopd restart). Free the heap
in the error path only when this spawn created it.

Pre-existing mainline bug, candidate for upstream once distilled.

Assisted-by: Claude:claude-fable-5
2026-07-11 16:45:23 +02:00
f3eb810dfe voice-proto: route audiopd to its dedicated context bank (pd-type)
The aDSP refuses to dlopen unsigned dynamic modules (fluence_nn) in an
audio static PD spawned on a generic context bank: it demands a
per-device testsig, which production-fused units cannot satisfy. Stock
(downstream adsprpc) binds audiopd to a dedicated CB — volcano-dsp.dtsi
cb12, SIDs 0x1005/0x1065, pd-type = <2> (AUDIO_STATICPD) — and there the
same unsigned fluence_nn loads fine on identical silicon.

Mirror that: tag compute-cb@5 (same SIDs) with pd-type = <2>, keep
pd-typed banks out of generic round-robin session allocation, and on
createstaticpd of "audiopd" re-route the client to the tagged session
before anything is allocated against it, so every IOVA the DSP sees for
this PD carries the audio bank's SID.

Local milos carry, not upstreamable as-is (upstream binding has no
pd-type model). See journal/calls.md 2026-07-11 session 10.

Assisted-by: Claude:claude-fable-5
2026-07-11 15:58:09 +02:00
cfc2b6a276 voice-proto: AMDB module pre-loading, longer cmd timeout
Three findings from the 2026-07-10 warm-capture session (journal/calls.md):
the stock voice sequence is byte-identical warm vs cold, so our replay
bytes were never the problem; VCPM rejects TX-side cal (EBADPARAM) while
accepting byte-identical RX cal through the same transport; and a voice
GRAPH_OPEN that has to FastRPC-load the Fluence pp modules takes >120 s
cold, so the client-side timeout/verify dance leaves failed opens in
VCPM's session history.

- apm_callback: handle AMDB_CMD_RSP_LOAD_MODULES (AMDB load responds
  with its own opcode, not an ibasic result); log per-module handles
  (handle 0 = not registered - e.g. 0x70010a3, which only appears after
  fluence_nn_module_fvxiii.so.1 is parsed).
- voice-proto: Q6VP_DSTPORT_FLAG record flag - first payload word
  carries the GPR destination port, so blobs can address static services
  other than APM (AMDB port 3 module pre-loading via mkamdbload.py).
- Raise the shared command timeout to 300 s so a cold TX voice
  GRAPH_OPEN (~125 s through the Fluence loads) completes inside ONE
  command with no retry dance.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-10 23:50:20 +02:00
3c381d76fc voice-proto: make cal registration safe; handle REGISTER_CFG responses
- apm_callback: store results for APM_CMD_REGISTER_CFG (0x01001008) so
  registrations complete synchronously instead of always timing out
  (their success/failure was previously invisible and each cost 5 s).
- voice-proto: REGISTER_CFG payloads get permanent bump-allocated
  slices in the shared region instead of reusing the scratch slot -
  the DSP keeps reading registered config memory, and overwriting it
  with the next record's payload while registered wedged the SoC hard
  (two force-reboots on 2026-07-08).
- Grow the region to 8 MiB (the stock sequence registers a 6.4 MiB
  global cal blob) and raise the shared command timeout to 30 s: a
  voice GRAPH_OPEN can block >5 s while the ADSP dynamically loads the
  Fluence modules through FastRPC.

With this the full RX-side stock sequence (open, 139 KB cal register,
VSID/cal-keys, EP configs, prepare, start) completes with status 0 on
a live call; TX GRAPH_START is the one remaining DSP rejection.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-09 00:44:51 +02:00
d36259f54b arm64: dts: qcom: milos: add fastrpc remote heap for the ADSP audio PD
The ADSP audio PD loads its voice post-processing modules (Fluence
NN/EF/pro-vc etc.) as dynamic shared objects fetched from the apps
filesystem at GRAPH_OPEN time through FastRPC (adsprpcd attached with
createstaticpd:audiopd serves the files - /vendor/dsp/adsp on stock).
The static-PD attach allocates a remote heap that must be SCM-assigned
to the DSP: without qcom,vmids the heap stays HLOS-owned and the PD's
first touch kills the SoC instantly (xPU violation, no kernel output).

Add a 12 MiB CMA pool (mirroring downstream's adsp_mem_heap: 32-bit
addressable, 4 MiB aligned) and the LPASS/ADSP_HEAP vmids, following
the sc7280 (kodiak) precedent where upstream audiopd support landed.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-08 01:31:08 +02:00
5f42f9a74a voice-proto: add out-of-band (shared memory) command support
Stock GSL delivers every voice GRAPH_OPEN out-of-band: the whole graph
(all subgraphs plus the cross-subgraph module connections and control
links, 4.3-4.7 KB) must be opened atomically in ONE command, which
exceeds the in-band GPR/GLINK intent limit. Splitting the open
per-subgraph to fit in-band makes the cross-referencing TX DevicePP
subgraph invalid - the root cause of every pp-subgraph open failure
since 2026-07-06 (byte-identical payloads open fine when sent whole;
verified against a gsl_log.bin packet capture of a working call on
stock Android).

Records with bit 31 set in the opcode are copied into a DMA buffer
allocated from the q6apm-dais device (it carries the ADSP SMMU
mapping), mapped once with the DSP via q6apm_map_memory_fixed_region
under a private graph id, and referenced by absolute DSP address in
the apm_cmd_header (mainline maps in absolute mode; GSL uses offset
mode - the DSP rejects offset references against our mapping).

Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-08 01:31:08 +02:00
57cbd69e36 Merge branch 'voice' into combined 2026-07-07 20:07:09 +02:00
0c63174949 net: qrtr: forward data packets between endpoints (FP6 voice prototype)
The DSP subsystems are star-connected through the application
processor: each has a qrtr link to us but none to each other. Incoming
packets whose dst_node is neither the local node nor broadcast are
currently delivered to a *local* port matching dst_port (usually
nothing) and dropped, so two remote DSPs can never exchange QMI
messages.

On the FP6 (SM7635, AudioReach) the modem's voice stack needs exactly
that: it must reach the ADSP's service-registry notifier / audio
service to attach the vocoder to a VCPM voice session. Without
forwarding, every voice graph starts cleanly but the modem never
streams a single mailbox packet - calls stay silent both ways (see
journal/calls.md 2026-07-07/08).

Forward DATA and RESUME_TX packets destined to another known node onto
that node's endpoint, re-using qrtr_node_enqueue for per-hop flow
control. RESUME_TX additionally releases this hop's flow token on the
arrival link; per-hop counters stay in lockstep with end-to-end ones
because every packet of a forwarded flow transits this node. Control
packets keep flowing to the local ns, which already redistributes
service announcements mesh-wide.

Prototype for the milos carry; needs discussion (loop prevention,
broadcast handling, flow-control semantics) before any upstream
attempt.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-07 20:07:01 +02:00
3cbf6d6fa8 Merge branch 'voice' into combined 2026-07-06 23:17:04 +02:00
2626032a50 voice-proto: continue on DSP rejections, log each record result
Assisted-by: Claude:claude-fable-5
2026-07-06 23:17:04 +02:00
63b5c43f02 Merge branch 'voice' into combined
Voice-call blob-player prototype (q6apm debugfs).
2026-07-06 23:06:35 +02:00
7695d5e6e4 ASoC: qdsp6: q6apm: add voice-call blob-player prototype (FP6 bring-up)
Replay pre-generated APM command sequences (GRAPH_OPEN/SET_CFG/PREPARE/
START, mined from the stock FP6 ACDB by utilities/mkvoiceblobs.py)
through the q6apm GPR channel via
/sys/kernel/debug/q6apm-voice-proto/play.

Bring-up prototype for cellular voice-call audio: the replayed graphs
contain the VCPM voice config; the modem and the ADSP VCPM service
attach the vocoder to them once a call is active. Local carry only,
NOT for upstream (the upstreamable form will be topology-driven).

Assisted-by: Claude:claude-fable-5
2026-07-06 23:06:35 +02:00
066348d67b Merge branch 'mic' into combined 2026-07-06 20:53:40 +02:00
08f135ee41 ASoC: codecs: wcd9378: sync with the mainline v1 submission
Mirror the driver as prepared for mainline (work/linux commit
917ea639925b, "ASoC: codecs: wcd9378: add TX/capture codec driver"),
per the carry-mirrors-upstream rule. On top of the previous carry
state this brings the maintainer-eye pre-review fixes:

 - latch the per-ADC sys-usage bit and target SDCA function at
   PRE_PMU; POST_PMD previously recomputed them from the live input
   mux and could tear down the wrong function after a mux change
 - clear the requested sys_usage_mask bit when no profile matches
 - drop the unreachable -EACCES carve-out on the TX runtime-PM hold
   (caused a usage-count underflow in unbind)
 - mark the write-1-clear FUNC_STAT registers volatile
 - drop the unused is_dapm parameter, the stale sys_usage write-skip
   cache, the SDW_SCP_INT1_IMPL_DEF unmask and the PS0 re-request
   hack; clamp DT channel-map reads
 - name the SWRS_SCP_SDCA_INTRTYPE registers; trim the PS0-failure
   debug dump; Kconfig imply + help text

One deviation from the mainline patch: v7.1.2 predates
sdw_slave_wait_for_init(), so the open-coded
wait_for_completion_timeout() stays here.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-06 20:53:39 +02:00
82284cc84a Merge branch 'mic' into combined 2026-07-06 02:18:05 +02:00
82c43dd3f1 ASoC: codecs: wcd9378: correct the ADC analog gain TLV to 1.5 dB steps
Measured acoustically on the FP6 with a fixed tone played through the
speakers: each TX gain code adds 1.5 dB (+6 dB per 4 codes, +30 dB over
the 0..20 range), not the 0.25 dB the TLV inherited from the wcd937x
driver family. With the correct scale, userspace volume mapping (e.g.
PulseAudio) can use the real analog range.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-06 02:18:05 +02:00
2931c83762 Merge branch 'mic' into combined
# Conflicts:
#	arch/arm64/boot/dts/qcom/milos-fairphone-fp6.dts
2026-07-06 01:14:20 +02:00
5694c4a5f3 ASoC: codecs: wcd9378: keep the TX SoundWire bus out of clock-stop
The SDCA function engine (the SmartMIC/SmartJACK/SmartAMP sequencer
machinery activated by the FUNC_ACT class-load) dies when the TX
SoundWire bus enters clock-stop. All its registers keep their values,
so a regcache sync on resume restores nothing visible - the PDE simply
never services power-state requests again: PDE11_ACT_PS stays in PS3,
SEQ_TXn_STAT stays at pwr_dn_rdy, and even the TXn_VALID_CFG_OVR /
TXn_SEQ_TRIGGER_OVR sequencer overrides and a TX0_SEQ_SOFT_RST pulse
are ignored. Re-toggling FUNC_ACT (a real 0->1 edge on the bus) does
not revive it either; only a full codec reset does. The result was
capture recording pure digital silence: the whole DPCM -> CDC-DMA ->
TX macro -> SoundWire transport ran, but the ADC never powered.

Hold a runtime PM reference on the TX slave for as long as the codec
is bound, so the bus never clock-stops. This matches the downstream
stack, which marks the TX SoundWire master 'qcom,is-always-on' - with
full documentation available, Qualcomm made the same trade-off.

Also perform the class-load activation with plain writes instead of
update_bits so the 0->1 activation edge always reaches the hardware
regardless of regcache state.

Verified on the FP6: from a fresh boot, repeated captures across what
were previously bus suspend/resume cycles now power the sequencer every
time (PDE11 reaches PS0) and record live mic signal instead of zeros.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-06 00:58:51 +02:00
d4c5ebbeee Merge branches 'audio' and 'nfc' into combined
combined = flashable working image: v2 speaker audio + NFC reader.
Excludes mic (WCD9378 capture, WIP) and nfc-ce (card emulation, broken).
2026-07-05 21:33:08 +02:00
52c785c315 arm64: dts: qcom: milos-fairphone-fp6: enable Senary MI2S dai-link
Uncomment the Senary MI2S speaker dai-link. The sc8280xp machine driver
gained Senary MI2S support and the aw88261 power-up check is fixed (both
carried on this branch), so the link is functional: both AW88261 amps
load their ACF firmware and play.

This restores the enablement that was lost when the audio carries were
rebased from the v7.0.8 stack onto v7.1.2-milos: only the two driver
patches were carried over, leaving the sound card with zero dai-links
(the card registers but stays empty - no PCMs, no controls).

Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-05 21:32:11 +02:00
d571dd42d3 ASoC: qcom: q6apm-lpass-dais: start the graph at prepare
The DAPM power-up sequence runs during snd_pcm prepare, but the BE
port graph is only started at trigger time. A codec that powers up
synchronously from a DAPM widget event and needs a running bit clock
at that point - such as aw88261 since commit caea99ac809d ("ASoC:
codecs: aw88261: remove async start") - can therefore never see a
live clock: its power-up check runs before the trigger and fails on
every stream start.

Start the graph at the end of prepare instead, mirroring what
q6afe_dai_prepare() does on the legacy stack, so the interface
clocks already run when DAPM powers up the codec. The FE side
already starts its own graph at prepare in q6apm_dai_prepare();
only the BE waited for trigger. The trigger-time start is kept as
a fallback, guarded by is_port_started.

Tested on the Fairphone (Gen. 6) - 2x aw88261 on Senary MI2S:
without this the amplifiers fail to power up with SYSST reporting
"no clock" on every stream start; with it they start synchronously,
including for the first short stream of the boot.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-05 21:32:11 +02:00
79c7597fc5 ASoC: codecs: aw88261: backport mainline format negotiation + power-up fix
The v7.1.2-milos base predates two aw88261 changes this branch needs:
the hw_params format-negotiation series (Val Packett's work, in broonie
for-7.2) and our power-up SYSST fix. The Senary MI2S carry drives the
two AW88261 amps in S16_LE end to end, which relies on the amplifier
negotiating its format; without it the machine driver has to force
32-bit slots. The SYSST fix is also required: aw88261_dev_start()
otherwise fails "check sysst fail" on this firmware profile because it
demands SWS/BSTS while the amp is still muted.

Backport aw88261.c/.h wholesale to the mainline state (matching
work/linux tag audio-mainline-aw88261) so this branch tracks mainline
rather than the older milos base driver. Local carry only; milos
absorbs it on the next rebase past those commits.

Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-05 21:32:11 +02:00
692c0cd1a4 ASoC: qcom: sc8280xp: support Senary MI2S
Extend the clock-provider DAI fmt setup to Senary MI2S; without it
q6i2s_set_fmt() is never called, ws_src remains external and the DSP
does not drive the I2S clocks.

On the Fairphone (Gen. 6) the speaker amplifiers sit on this
interface; the board DTS enabling it is headed upstream separately
via linux-arm-msm.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-05 21:32:11 +02:00
9cd00a65e8 nfc: s3fwrn5: support the S3NRN4V variant
The S3NRN4V (e.g. on the Fairphone 6, SM7635) is an S3FWRN5-family NFC
controller that needs different bring-up, selected with a new
samsung,s3nrn4v compatible:

 - It ships with working firmware behind a bootloader protocol this
   driver does not implement (GET_BOOTINFO times out), so the firmware
   download step is skipped. Its RF registers are (re)loaded with the
   proprietary DUAL_OPTION command (the HW and SW register blobs merged
   into a single stream) instead of the START/SET/STOP_RFREG sequence.

 - Its reference clock speed is configured with the single-byte FW_CFG
   form, sent from the ->setup hook (after CORE_RESET, before CORE_INIT).
   The selector value (0x11) is taken from the vendor configuration for
   this part; its encoding is not documented.

 - It gates its XI clock through a CLK_REQ line: the chip drives it high
   when it needs the clock, notably to synthesise the 13.56 MHz poll
   carrier. Left always-on, the free-running clock never lets the chip's
   TX PLL lock on a fresh start and it cannot poll (it falls back to
   listen only). Service the handshake when a clk-req GPIO is described,
   gating the clock on it; without one the clock stays always-on.

The variant is carried as match data by both the OF and the I2C device
id tables so the two match paths agree, and the OF table is now
referenced unconditionally for its match data, so drop the
of_match_ptr()/__maybe_unused annotations from it.

The error policy differs between the two configuration steps on purpose:
a clock misconfiguration is fatal (a ->setup failure aborts CORE_INIT),
whereas an RF-register update failure is only warned about and bring-up
continues, since the chip falls back to the RF registers programmed in
its flash and NFC may still work.

Unlike the host-endian word read in the legacy rfreg path, the
DUAL_OPTION checksum is accumulated with get_unaligned_le32() and emitted
little-endian explicitly, so it is correct regardless of CPU endianness.

Existing S3FWRN5 / S3FWRN82 setups keep the firmware-download path and
the always-on clock, unchanged.

Assisted-by: Claude:claude-opus-4-8
Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-05 20:37:57 +02:00
d837afcfd6 arm64: dts: qcom: milos-fairphone-fp6: add NFC
Add the Samsung S3NRN4V NFC + eSE controller on i2c1. Its XI clock is
provided by the RF_CLK2 PMIC buffer and gated through the controller's
CLK_REQ line on tlmm GPIO6.

The enable line is routed to the chip's power-down input, which is
asserted high to turn the chip off (the downstream driver treats VEN as
active-low on this design) -- hence GPIO_ACTIVE_HIGH, unlike the
exynos5433-tm2 wiring of the same driver. The pin is pulled up so the
chip stays off while the line is not driven.

Assisted-by: Claude:claude-opus-4-8
Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-05 18:02:28 +02:00
e9a6b531cf arm64: dts: qcom: milos-fairphone-fp6: add WCD9378 codec node and capture dai-link
Add the wcd9378 audio-codec parent node (reset gpio162, l7b/l8b/bob
supplies, 1.8 V micbias x3, rx/tx slave phandles), tx/rx port mappings
on the SoundWire slave nodes (ADC1/2/3 on device ports 1/2/3 all mapped
to master port 1, per the downstream volcano tx_swr_ch_map), the WCD
Capture dai-link on TX_CODEC_DMA_TX_3 and the capture audio-routing
(TX SWR_INPUTn inputs - the milos TX macro is a v9.2 variant, not the
SWR_ADCn naming qcm6490 uses).

The vreg_l8b regulator-always-on DTB hack is obsolete: the codec node
now holds vdd-buck.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-05 17:37:15 +02:00
9df8d66027 ASoC: codecs: wcd9378: grow skeleton into TX/capture codec driver (WIP)
Replace the transport-test skeleton with a functional driver modeled on
wcd937x: platform parent device (qcom,wcd9378-codec) as component master
over the two SoundWire slaves, owning reset GPIO, supplies and micbias
config; regmap (MAPLE cache, 32-bit paged SDCA addresses) on the TX
slave; capture DAI (index 1) with sdw stream plumbing; DAPM TX path
AMICn -> ADCn MUX -> TXn SEQUENCER -> ADCn_OUTPUT with the SDCA
SmartMIC power sequence (ITxx_USAGE mode, PDE11 PS0 request, HPF init
hold) and IT11_MICB-based refcounted micbias control; sys-usage profile
auto-selection; SCP bus-clock indication (base clk, busclock scale,
host-clk-div2) per the downstream capture-start sequence.

Verified on FP6: probes and binds without any manual per-boot hacks
(gpio162 reset, runtime PM force, l8b always-on all obsolete), sound
card registers, full DPCM/SoundWire/CDC-DMA transport carries data.

KNOWN ISSUE: the SmartMIC sequencer never leaves PWR_DN (PDE11_ACT_PS
stays PS3, SEQ_TX0_STAT=PWR_DN_RDY) although every register the
downstream driver writes has been replicated and verified on hardware
by bypassed readback - capture records digital silence. Investigation
notes in journal/mic.md.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-05 17:37:02 +02:00
5c9c899ba9 arm64: dts: qcom: milos-fairphone-fp6: enable SoundWire buses with WCD9378 slaves
Enable the RX (swr1) and TX (swr2) SoundWire controllers and describe
the two WCD9378 codec slave devices, as enumerated on hardware:
unique-id 4 on the RX bus, unique-id 3 on the TX bus, SoundWire id
sdw20217011000 (mfg 0x0217, part 0x0110). A firmware description is
required for the sdw core to bind a driver since dynamically enumerated
devices are visible but not bindable.

WIP: the codec node set is minimal (no port mappings, no codec parent
node yet); grows with the wcd9378 driver.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-05 02:52:32 +02:00
2fd346efe3 ASoC: codecs: wcd9378: add SoundWire skeleton driver (WIP)
Bring-up skeleton for the Qualcomm WCD9378 codec (SoundWire dev id
0x0217:0x0110, one slave per RX/TX bus). Probes both slaves, maps the
SDCA control space (32-bit paged addresses) through regmap-sdw with
prop.paging_support set, and dumps the device identity registers on
ATTACHED as a transport self-test:

  DEV_MANU_ID_0/1 = 0x17/0x02 (Qualcomm 0x0217)
  DEV_PART_ID_0/1 = 0x10/0x01 (WCD9378 0x0110)
  ANA_TX_CH1 0x20, ANA_MICB1 0x10 (downstream reset defaults)

The analog core is WCD937x register-compatible; full codec function
(TX/ADC path first) to be built on top of this. Not for upstream in
this form.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-05 02:52:32 +02:00
8ed47c2b0a soundwire: qcom: add SCP address paging support
The Qualcomm SoundWire controller driver ignored the paging fields of
struct sdw_msg, so any register access above the 16-bit address space
(e.g. the SDCA control space used by the WCD9378 codec) silently read
the low 15 bits only. The core already splits the address into
addr_page1/addr_page2 and sets msg->page; write the two SCP_AddrPage
registers through the command FIFO before the transfer, as the vendor
swr-mstr-ctrl driver does.

Verified on Fairphone 6 (SM7635): WCD9378 SDCA registers (0x40000000+)
read back their documented reset defaults; without this every paged
read returned zeros.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
2026-07-05 02:52:32 +02:00
27 changed files with 3852 additions and 320 deletions

View file

@ -237,14 +237,34 @@
sound { sound {
compatible = "qcom,milos-sndcard", "qcom,sm8450-sndcard"; compatible = "qcom,milos-sndcard", "qcom,sm8450-sndcard";
model = "Fairphone (Gen. 6)"; model = "Fairphone (Gen. 6)";
// audio-routing = ... audio-routing = "AMIC1", "MIC BIAS1",
"AMIC2", "MIC BIAS2",
"AMIC3", "MIC BIAS3",
"TX SWR_INPUT0", "ADC1_OUTPUT",
"TX SWR_INPUT1", "ADC2_OUTPUT",
"TX SWR_INPUT2", "ADC3_OUTPUT";
pinctrl-0 = <&lpi_i2s2_active>; pinctrl-0 = <&lpi_i2s2_active>;
pinctrl-1 = <&lpi_i2s2_sleep>; pinctrl-1 = <&lpi_i2s2_sleep>;
pinctrl-names = "default", pinctrl-names = "default",
"sleep"; "sleep";
/* wcd-capture-dai-link {
link-name = "WCD Capture";
codec {
sound-dai = <&wcd9378 1>, <&swr2 0>, <&lpass_txmacro 0>;
};
cpu {
sound-dai = <&q6apmbedai TX_CODEC_DMA_TX_3>;
};
platform {
sound-dai = <&q6apm>;
};
};
i2s-dai-link { i2s-dai-link {
link-name = "Senary MI2S Playback"; link-name = "Senary MI2S Playback";
@ -260,7 +280,29 @@
sound-dai = <&q6apm>; sound-dai = <&q6apm>;
}; };
}; };
*/ };
wcd9378: audio-codec {
compatible = "qcom,wcd9378-codec";
pinctrl-0 = <&wcd_reset_n_active>;
pinctrl-names = "default";
reset-gpios = <&tlmm 162 GPIO_ACTIVE_LOW>;
vdd-buck-supply = <&vreg_l8b>;
vdd-rxtx-supply = <&vreg_l7b>;
vdd-io-supply = <&vreg_l7b>;
vdd-mic-bias-supply = <&vreg_bob>;
qcom,micbias1-microvolt = <1800000>;
qcom,micbias2-microvolt = <1800000>;
qcom,micbias3-microvolt = <1800000>;
qcom,rx-device = <&wcd9378_rx>;
qcom,tx-device = <&wcd9378_tx>;
#sound-dai-cells = <1>;
}; };
thermal-zones { thermal-zones {
@ -728,9 +770,28 @@
}; };
&i2c1 { &i2c1 {
/* Samsung NFC @ 0x27 */
status = "okay"; status = "okay";
/*
* Samsung S3NRN4V NFC controller. XI is driven by the RF_CLK2 PMIC
* buffer; the chip has no oscillator of its own and gates the clock
* via its CLK_REQ line, so the clock must be voted on in response
* to it.
*/
nfc@27 {
compatible = "samsung,s3nrn4v";
reg = <0x27>;
clk-req-gpios = <&tlmm 6 GPIO_ACTIVE_HIGH>;
clocks = <&rpmhcc RPMH_RF_CLK2>;
en-gpios = <&tlmm 56 GPIO_ACTIVE_HIGH>;
interrupt-parent = <&tlmm>;
interrupts = <31 IRQ_TYPE_EDGE_RISING>;
pinctrl-0 = <&nfc_clk_req_default>, <&nfc_irq_default>,
<&nfc_pd_default>;
pinctrl-names = "default";
wake-gpios = <&tlmm 7 GPIO_ACTIVE_HIGH>;
};
}; };
&i2c3 { &i2c3 {
@ -1114,11 +1175,48 @@
}; };
}; };
&swr1 {
status = "okay";
/* WCD9378 RX */
wcd9378_rx: codec@0,4 {
compatible = "sdw20217011000";
reg = <0 4>;
qcom,rx-port-mapping = <1 2 3 4 5>;
};
};
&swr2 {
status = "okay";
/* WCD9378 TX */
wcd9378_tx: codec@0,3 {
compatible = "sdw20217011000";
reg = <0 3>;
/*
* WCD9378 TX port 1 (ADC1) <=> SWR2 port 1 (SWRM_TX1)
* WCD9378 TX port 2 (ADC2) <=> SWR2 port 1
* WCD9378 TX port 3 (ADC3) <=> SWR2 port 1
* WCD9378 TX port 4 (DMIC0,1, MBHC) <=> SWR2 port 2
* WCD9378 TX port 5 (DMIC2..5) <=> SWR2 port 3
*/
qcom,tx-port-mapping = <1 1 1 2 3>;
};
};
&tlmm { &tlmm {
gpio-reserved-ranges = <8 4>, /* Fingerprint SPI */ gpio-reserved-ranges = <8 4>, /* Fingerprint SPI */
<13 1>, /* NC */ <13 1>, /* NC */
<63 2>; /* WLAN UART */ <63 2>; /* WLAN UART */
nfc_clk_req_default: nfc-clk-req-default-state {
pins = "gpio6";
function = "gpio";
drive-strength = <2>;
bias-disable;
};
sensor_i2c_default: sensor-i2c-default-state { sensor_i2c_default: sensor-i2c-default-state {
/* SDA, SCL; external pull-ups to vreg_l10b */ /* SDA, SCL; external pull-ups to vreg_l10b */
pins = "gpio153", "gpio154"; pins = "gpio153", "gpio154";
@ -1141,6 +1239,13 @@
bias-disable; bias-disable;
}; };
nfc_irq_default: nfc-irq-default-state {
pins = "gpio31";
function = "gpio";
drive-strength = <2>;
bias-disable;
};
qup_uart11_sleep_cts: qup-uart11-sleep-cts-state { qup_uart11_sleep_cts: qup-uart11-sleep-cts-state {
pins = "gpio48"; pins = "gpio48";
function = "gpio"; function = "gpio";
@ -1208,6 +1313,17 @@
bias-pull-down; bias-pull-down;
}; };
/*
* Pulled up so the NFC chip stays powered down while the line is
* not driven (it is the chip's active-high power-down input).
*/
nfc_pd_default: nfc-pd-default-state {
pins = "gpio56";
function = "gpio";
drive-strength = <2>;
bias-pull-up;
};
sdc2_card_det_n: sdc2-card-det-state { sdc2_card_det_n: sdc2-card-det-state {
pins = "gpio65"; pins = "gpio65";
function = "gpio"; function = "gpio";
@ -1249,6 +1365,13 @@
drive-strength = <2>; drive-strength = <2>;
bias-pull-down; bias-pull-down;
}; };
wcd_reset_n_active: wcd-reset-n-active-state {
pins = "gpio162";
function = "gpio";
drive-strength = <16>;
output-high;
};
}; };
&uart5 { &uart5 {

View file

@ -548,6 +548,14 @@
no-map; no-map;
}; };
adsp_rpc_heap_mem: adsp-rpc-heap {
compatible = "shared-dma-pool";
alloc-ranges = <0x0 0x0 0x0 0xffffffff>;
reusable;
alignment = <0x0 0x400000>;
size = <0x0 0xc00000>;
};
pvm_fw_mem: pvm-fw-region@824a0000 { pvm_fw_mem: pvm-fw-region@824a0000 {
reg = <0x0 0x824a0000 0x0 0x100000>; reg = <0x0 0x824a0000 0x0 0x100000>;
no-map; no-map;
@ -1398,6 +1406,9 @@
qcom,glink-channels = "fastrpcglink-apps-dsp"; qcom,glink-channels = "fastrpcglink-apps-dsp";
label = "adsp"; label = "adsp";
qcom,non-secure-domain; qcom,non-secure-domain;
memory-region = <&adsp_rpc_heap_mem>;
qcom,vmids = <QCOM_SCM_VMID_LPASS
QCOM_SCM_VMID_ADSP_HEAP>;
#address-cells = <1>; #address-cells = <1>;
#size-cells = <0>; #size-cells = <0>;
@ -1422,6 +1433,8 @@
reg = <5>; reg = <5>;
iommus = <&apps_smmu 0x1005 0x0>, iommus = <&apps_smmu 0x1005 0x0>,
<&apps_smmu 0x1065 0x0>; <&apps_smmu 0x1065 0x0>;
/* AUDIO_STATICPD bank, per downstream volcano-dsp.dtsi */
pd-type = <2>;
dma-coherent; dma-coherent;
}; };

View file

@ -103,6 +103,13 @@
#define USER_PD (1) #define USER_PD (1)
#define SENSORS_PD (2) #define SENSORS_PD (2)
/*
* Downstream DT "pd-type" context-bank designations (volcano-dsp.dtsi).
* A static PD must be spawned on its designated context bank / SMMU
* stream or the DSP treats it as a generic signed PD.
*/
#define FASTRPC_DT_PD_TYPE_AUDIO_STATICPD 2
#define miscdev_to_fdevice(d) container_of(d, struct fastrpc_device, miscdev) #define miscdev_to_fdevice(d) container_of(d, struct fastrpc_device, miscdev)
struct fastrpc_phy_page { struct fastrpc_phy_page {
@ -255,6 +262,8 @@ struct fastrpc_session_ctx {
int sid; int sid;
bool used; bool used;
bool valid; bool valid;
/* downstream DT pd-type this context bank is dedicated to (0 = generic) */
u32 pd_type;
}; };
struct fastrpc_soc_data { struct fastrpc_soc_data {
@ -314,6 +323,11 @@ struct fastrpc_user {
struct kref refcount; struct kref refcount;
}; };
static struct fastrpc_session_ctx *fastrpc_session_alloc_pd_type(
struct fastrpc_user *fl, u32 pd_type);
static void fastrpc_session_free(struct fastrpc_channel_ctx *cctx,
struct fastrpc_session_ctx *session);
/* Extract SMMU PA from consolidated IOVA */ /* Extract SMMU PA from consolidated IOVA */
static inline dma_addr_t fastrpc_ipa_to_dma_addr(struct fastrpc_channel_ctx *cctx, dma_addr_t iova) static inline dma_addr_t fastrpc_ipa_to_dma_addr(struct fastrpc_channel_ctx *cctx, dma_addr_t iova)
{ {
@ -1343,7 +1357,7 @@ static int fastrpc_init_create_static_process(struct fastrpc_user *fl,
struct fastrpc_phy_page pages[1]; struct fastrpc_phy_page pages[1];
char *name; char *name;
int err; int err;
bool scm_done = false; bool scm_done = false, heap_created = false;
struct { struct {
int client_id; int client_id;
u32 namelen; u32 namelen;
@ -1371,11 +1385,35 @@ static int fastrpc_init_create_static_process(struct fastrpc_user *fl,
goto err; goto err;
} }
/*
* The audio static PD must live on its designated context bank so the
* DSP sees its memory on the audio SMMU stream; on a generic bank the
* aDSP enforces module signing in the PD (dlopen of unsigned dynamic
* modules fails demanding a testsig). Re-route before anything is
* allocated against the session. Falls back to the already-held
* generic session if no bank is tagged in DT.
*/
if (init.namelen >= strlen("audiopd") &&
!memcmp(name, "audiopd", strlen("audiopd"))) {
struct fastrpc_session_ctx *audio_sess;
audio_sess = fastrpc_session_alloc_pd_type(fl,
FASTRPC_DT_PD_TYPE_AUDIO_STATICPD);
if (audio_sess) {
fastrpc_session_free(fl->cctx, fl->sctx);
fl->sctx = audio_sess;
}
dev_info(&fl->cctx->rpdev->dev,
"audiopd: using session sid %d (%s)\n", fl->sctx->sid,
audio_sess ? "dedicated audio CB" : "no audio CB in DT");
}
if (!fl->cctx->remote_heap) { if (!fl->cctx->remote_heap) {
err = fastrpc_remote_heap_alloc(fl, fl->sctx->dev, init.memlen, err = fastrpc_remote_heap_alloc(fl, fl->sctx->dev, init.memlen,
&fl->cctx->remote_heap); &fl->cctx->remote_heap);
if (err) if (err)
goto err_name; goto err_name;
heap_created = true;
/* Map if we have any heap VMIDs associated with this ADSP Static Process. */ /* Map if we have any heap VMIDs associated with this ADSP Static Process. */
if (fl->cctx->vmcount) { if (fl->cctx->vmcount) {
@ -1446,8 +1484,16 @@ err_invoke:
&fl->cctx->remote_heap->dma_addr, fl->cctx->remote_heap->size, err); &fl->cctx->remote_heap->dma_addr, fl->cctx->remote_heap->size, err);
} }
err_map: err_map:
fastrpc_buf_free(fl->cctx->remote_heap); /*
fl->cctx->remote_heap = NULL; * A heap reused from an earlier spawn is still SCM-assigned to the
* remote VMIDs; freeing it here would return protected pages to the
* allocator and a later touch crashes with a synchronous external
* abort. Keep it for the next spawn attempt.
*/
if (heap_created) {
fastrpc_buf_free(fl->cctx->remote_heap);
fl->cctx->remote_heap = NULL;
}
err_name: err_name:
kfree(name); kfree(name);
err: err:
@ -1582,7 +1628,9 @@ static struct fastrpc_session_ctx *fastrpc_session_alloc(
spin_lock_irqsave(&cctx->lock, flags); spin_lock_irqsave(&cctx->lock, flags);
for (i = 0; i < cctx->sesscount; i++) { for (i = 0; i < cctx->sesscount; i++) {
if (!cctx->session[i].used && cctx->session[i].valid) { /* dedicated static-PD context banks are assigned by pd_type only */
if (!cctx->session[i].used && cctx->session[i].valid &&
!cctx->session[i].pd_type) {
cctx->session[i].used = true; cctx->session[i].used = true;
session = &cctx->session[i]; session = &cctx->session[i];
/* any non-zero ID will work, session_idx + 1 is the simplest one */ /* any non-zero ID will work, session_idx + 1 is the simplest one */
@ -1595,6 +1643,29 @@ static struct fastrpc_session_ctx *fastrpc_session_alloc(
return session; return session;
} }
static struct fastrpc_session_ctx *fastrpc_session_alloc_pd_type(
struct fastrpc_user *fl, u32 pd_type)
{
struct fastrpc_channel_ctx *cctx = fl->cctx;
struct fastrpc_session_ctx *session = NULL;
unsigned long flags;
int i;
spin_lock_irqsave(&cctx->lock, flags);
for (i = 0; i < cctx->sesscount; i++) {
if (!cctx->session[i].used && cctx->session[i].valid &&
cctx->session[i].pd_type == pd_type) {
cctx->session[i].used = true;
session = &cctx->session[i];
fl->client_id = i + 1;
break;
}
}
spin_unlock_irqrestore(&cctx->lock, flags);
return session;
}
static void fastrpc_session_free(struct fastrpc_channel_ctx *cctx, static void fastrpc_session_free(struct fastrpc_channel_ctx *cctx,
struct fastrpc_session_ctx *session) struct fastrpc_session_ctx *session)
{ {
@ -2251,6 +2322,9 @@ static int fastrpc_cb_probe(struct platform_device *pdev)
if (of_property_read_u32(dev->of_node, "reg", &sess->sid)) if (of_property_read_u32(dev->of_node, "reg", &sess->sid))
dev_info(dev, "FastRPC Session ID not specified in DT\n"); dev_info(dev, "FastRPC Session ID not specified in DT\n");
sess->pd_type = 0;
of_property_read_u32(dev->of_node, "pd-type", &sess->pd_type);
if (sessions > 0) { if (sessions > 0) {
struct fastrpc_session_ctx *dup_sess; struct fastrpc_session_ctx *dup_sess;

View file

@ -122,11 +122,47 @@ static int s3fwrn5_nci_send(struct nci_dev *ndev, struct sk_buff *skb)
return 0; return 0;
} }
static int s3fwrn5_nci_setup(struct nci_dev *ndev)
{
struct s3fwrn5_info *info = nci_get_drvdata(ndev);
/*
* Runs after CORE_RESET, before CORE_INIT. The S3NRN4V needs its
* reference clock configured here (the downstream stack does it in the
* bootloader, before CORE_RESET, but this is the earliest hook the NCI
* core offers and the chip accepts it).
*/
if (info->variant == S3FWRN5_VARIANT_S3NRN4V)
return s3fwrn5_nci_clk_cfg(info);
return 0;
}
static int s3fwrn5_nci_post_setup(struct nci_dev *ndev) static int s3fwrn5_nci_post_setup(struct nci_dev *ndev)
{ {
struct s3fwrn5_info *info = nci_get_drvdata(ndev); struct s3fwrn5_info *info = nci_get_drvdata(ndev);
int ret; int ret;
if (info->variant == S3FWRN5_VARIANT_S3NRN4V) {
/*
* The S3NRN4V ships with working firmware behind a bootloader
* protocol this driver does not implement, so there is no
* download step; the NCI core has already done CORE_RESET +
* CORE_INIT. Just (re)load the RF registers via DUAL_OPTION.
*/
ret = s3fwrn5_nci_rf_configure_dual(info, "sec_s3nrn4v_hwreg.bin",
"sec_s3nrn4v_swreg.bin");
/*
* Keep going even if the blobs could not be loaded: the chip
* still enumerates and falls back to the RF registers programmed
* in its flash, so NFC may work anyway.
*/
if (ret < 0)
dev_warn(&ndev->nfc_dev->dev,
"rfreg configure failed (%d)\n", ret);
return 0;
}
if (s3fwrn5_firmware_init(info)) { if (s3fwrn5_firmware_init(info)) {
//skip bootloader mode //skip bootloader mode
return 0; return 0;
@ -152,13 +188,14 @@ static const struct nci_ops s3fwrn5_nci_ops = {
.open = s3fwrn5_nci_open, .open = s3fwrn5_nci_open,
.close = s3fwrn5_nci_close, .close = s3fwrn5_nci_close,
.send = s3fwrn5_nci_send, .send = s3fwrn5_nci_send,
.setup = s3fwrn5_nci_setup,
.post_setup = s3fwrn5_nci_post_setup, .post_setup = s3fwrn5_nci_post_setup,
.prop_ops = s3fwrn5_nci_prop_ops, .prop_ops = s3fwrn5_nci_prop_ops,
.n_prop_ops = ARRAY_SIZE(s3fwrn5_nci_prop_ops), .n_prop_ops = ARRAY_SIZE(s3fwrn5_nci_prop_ops),
}; };
int s3fwrn5_probe(struct nci_dev **ndev, void *phy_id, struct device *pdev, int s3fwrn5_probe(struct nci_dev **ndev, void *phy_id, struct device *pdev,
const struct s3fwrn5_phy_ops *phy_ops) const struct s3fwrn5_phy_ops *phy_ops, enum s3fwrn5_variant variant)
{ {
struct s3fwrn5_info *info; struct s3fwrn5_info *info;
int ret; int ret;
@ -170,6 +207,7 @@ int s3fwrn5_probe(struct nci_dev **ndev, void *phy_id, struct device *pdev,
info->phy_id = phy_id; info->phy_id = phy_id;
info->pdev = pdev; info->pdev = pdev;
info->phy_ops = phy_ops; info->phy_ops = phy_ops;
info->variant = variant;
mutex_init(&info->mutex); mutex_init(&info->mutex);
s3fwrn5_set_mode(info, S3FWRN5_MODE_COLD); s3fwrn5_set_mode(info, S3FWRN5_MODE_COLD);

View file

@ -23,9 +23,76 @@ struct s3fwrn5_i2c_phy {
struct i2c_client *i2c_dev; struct i2c_client *i2c_dev;
struct clk *clk; struct clk *clk;
/*
* Optional hardware clock-request handshake. When a CLK_REQ GPIO is
* wired, the chip drives it high while it needs its XI clock -- notably
* to generate the poll/reader carrier -- and the clock is gated on it
* instead of being left always-on (which never lets the chip's TX PLL
* lock on a fresh clock start, leaving it unable to poll).
*/
struct gpio_desc *gpio_clk_req;
bool clk_on;
struct mutex clk_lock; /* serialises clk_on against the CLK_REQ irq */
unsigned int irq_skip:1; unsigned int irq_skip:1;
}; };
static void s3fwrn5_i2c_clk_set_locked(struct s3fwrn5_i2c_phy *phy, bool on)
{
lockdep_assert_held(&phy->clk_lock);
if (on && !phy->clk_on) {
int ret = clk_prepare_enable(phy->clk);
if (ret == 0)
phy->clk_on = true;
else
dev_warn_once(&phy->i2c_dev->dev,
"failed to enable clock (%d); NFC may not poll\n",
ret);
} else if (!on && phy->clk_on) {
clk_disable_unprepare(phy->clk);
phy->clk_on = false;
}
}
/*
* Apply the current CLK_REQ level. Reading the GPIO under clk_lock makes
* concurrent callers (the CLK_REQ irq thread and the probe-time seeding)
* safe: whoever runs last applies a level read after the earlier update,
* never a stale one.
*/
static void s3fwrn5_i2c_clk_sync(struct s3fwrn5_i2c_phy *phy)
{
int level;
mutex_lock(&phy->clk_lock);
level = gpiod_get_value_cansleep(phy->gpio_clk_req);
if (level >= 0)
s3fwrn5_i2c_clk_set_locked(phy, level > 0);
else
dev_warn_once(&phy->i2c_dev->dev,
"failed to read CLK_REQ (%d); keeping clock state\n",
level);
mutex_unlock(&phy->clk_lock);
}
static void s3fwrn5_i2c_clk_disable_action(void *data)
{
struct s3fwrn5_i2c_phy *phy = data;
mutex_lock(&phy->clk_lock);
s3fwrn5_i2c_clk_set_locked(phy, false);
mutex_unlock(&phy->clk_lock);
}
static irqreturn_t s3fwrn5_i2c_clk_req_thread(int irq, void *phy_id)
{
s3fwrn5_i2c_clk_sync(phy_id);
return IRQ_HANDLED;
}
static void s3fwrn5_i2c_set_mode(void *phy_id, enum s3fwrn5_mode mode) static void s3fwrn5_i2c_set_mode(void *phy_id, enum s3fwrn5_mode mode)
{ {
struct s3fwrn5_i2c_phy *phy = phy_id; struct s3fwrn5_i2c_phy *phy = phy_id;
@ -146,6 +213,7 @@ out:
static int s3fwrn5_i2c_probe(struct i2c_client *client) static int s3fwrn5_i2c_probe(struct i2c_client *client)
{ {
enum s3fwrn5_variant variant;
struct s3fwrn5_i2c_phy *phy; struct s3fwrn5_i2c_phy *phy;
int ret; int ret;
@ -172,15 +240,61 @@ static int s3fwrn5_i2c_probe(struct i2c_client *client)
* S3FWRN5 depends on a clock input ("XI" pin) to function properly. * S3FWRN5 depends on a clock input ("XI" pin) to function properly.
* Depending on the hardware configuration this could be an always-on * Depending on the hardware configuration this could be an always-on
* oscillator or some external clock that must be explicitly enabled. * oscillator or some external clock that must be explicitly enabled.
* Make sure the clock is running before starting S3FWRN5. *
* If a CLK_REQ GPIO is wired, the chip gates the clock itself (driving
* CLK_REQ high when it needs XI); service that handshake. Otherwise just
* make sure the clock is running before starting S3FWRN5.
*/ */
phy->clk = devm_clk_get_optional_enabled(&client->dev, NULL); mutex_init(&phy->clk_lock);
if (IS_ERR(phy->clk)) phy->gpio_clk_req = devm_gpiod_get_optional(&client->dev, "clk-req",
return dev_err_probe(&client->dev, PTR_ERR(phy->clk), GPIOD_IN);
"failed to get clock\n"); if (IS_ERR(phy->gpio_clk_req))
return PTR_ERR(phy->gpio_clk_req);
if (phy->gpio_clk_req) {
int clk_req_irq;
phy->clk = devm_clk_get_optional(&client->dev, NULL);
if (IS_ERR(phy->clk))
return dev_err_probe(&client->dev, PTR_ERR(phy->clk),
"failed to get clock\n");
/*
* Unlike the always-on branch below, this clock is enabled by
* hand from the CLK_REQ handler, so devm will not disable it on
* unbind. Gate it off explicitly if it is still on at teardown.
*/
ret = devm_add_action_or_reset(&client->dev,
s3fwrn5_i2c_clk_disable_action,
phy);
if (ret)
return ret;
clk_req_irq = gpiod_to_irq(phy->gpio_clk_req);
if (clk_req_irq < 0)
return clk_req_irq;
ret = devm_request_threaded_irq(&client->dev, clk_req_irq, NULL,
s3fwrn5_i2c_clk_req_thread,
IRQF_TRIGGER_RISING |
IRQF_TRIGGER_FALLING |
IRQF_ONESHOT,
"s3fwrn5_clk_req", phy);
if (ret)
return ret;
/* Seed the clock state from the current CLK_REQ level. */
s3fwrn5_i2c_clk_sync(phy);
} else {
phy->clk = devm_clk_get_optional_enabled(&client->dev, NULL);
if (IS_ERR(phy->clk))
return dev_err_probe(&client->dev, PTR_ERR(phy->clk),
"failed to get clock\n");
}
variant = (uintptr_t)i2c_get_match_data(client);
ret = s3fwrn5_probe(&phy->common.ndev, phy, &phy->i2c_dev->dev, ret = s3fwrn5_probe(&phy->common.ndev, phy, &phy->i2c_dev->dev,
&i2c_phy_ops); &i2c_phy_ops, variant);
if (ret < 0) if (ret < 0)
return ret; return ret;
@ -205,13 +319,17 @@ static void s3fwrn5_i2c_remove(struct i2c_client *client)
} }
static const struct i2c_device_id s3fwrn5_i2c_id_table[] = { static const struct i2c_device_id s3fwrn5_i2c_id_table[] = {
{ S3FWRN5_I2C_DRIVER_NAME }, { .name = S3FWRN5_I2C_DRIVER_NAME, .driver_data = S3FWRN5_VARIANT_FWDL },
{} { .name = "s3nrn4v", .driver_data = S3FWRN5_VARIANT_S3NRN4V },
{ }
}; };
MODULE_DEVICE_TABLE(i2c, s3fwrn5_i2c_id_table); MODULE_DEVICE_TABLE(i2c, s3fwrn5_i2c_id_table);
static const struct of_device_id of_s3fwrn5_i2c_match[] __maybe_unused = { static const struct of_device_id of_s3fwrn5_i2c_match[] = {
{ .compatible = "samsung,s3fwrn5-i2c", }, { .compatible = "samsung,s3fwrn5-i2c",
.data = (void *)S3FWRN5_VARIANT_FWDL, },
{ .compatible = "samsung,s3nrn4v",
.data = (void *)S3FWRN5_VARIANT_S3NRN4V, },
{} {}
}; };
MODULE_DEVICE_TABLE(of, of_s3fwrn5_i2c_match); MODULE_DEVICE_TABLE(of, of_s3fwrn5_i2c_match);
@ -219,7 +337,7 @@ MODULE_DEVICE_TABLE(of, of_s3fwrn5_i2c_match);
static struct i2c_driver s3fwrn5_i2c_driver = { static struct i2c_driver s3fwrn5_i2c_driver = {
.driver = { .driver = {
.name = S3FWRN5_I2C_DRIVER_NAME, .name = S3FWRN5_I2C_DRIVER_NAME,
.of_match_table = of_match_ptr(of_s3fwrn5_i2c_match), .of_match_table = of_s3fwrn5_i2c_match,
}, },
.probe = s3fwrn5_i2c_probe, .probe = s3fwrn5_i2c_probe,
.remove = s3fwrn5_i2c_remove, .remove = s3fwrn5_i2c_remove,

View file

@ -8,6 +8,9 @@
#include <linux/completion.h> #include <linux/completion.h>
#include <linux/firmware.h> #include <linux/firmware.h>
#include <linux/minmax.h>
#include <linux/slab.h>
#include <linux/unaligned.h>
#include "s3fwrn5.h" #include "s3fwrn5.h"
#include "nci.h" #include "nci.h"
@ -20,7 +23,7 @@ static int s3fwrn5_nci_prop_rsp(struct nci_dev *ndev, struct sk_buff *skb)
return 0; return 0;
} }
const struct nci_driver_ops s3fwrn5_nci_prop_ops[4] = { const struct nci_driver_ops s3fwrn5_nci_prop_ops[5] = {
{ {
.opcode = nci_opcode_pack(NCI_GID_PROPRIETARY, .opcode = nci_opcode_pack(NCI_GID_PROPRIETARY,
NCI_PROP_SET_RFREG), NCI_PROP_SET_RFREG),
@ -41,6 +44,11 @@ const struct nci_driver_ops s3fwrn5_nci_prop_ops[4] = {
NCI_PROP_FW_CFG), NCI_PROP_FW_CFG),
.rsp = s3fwrn5_nci_prop_rsp, .rsp = s3fwrn5_nci_prop_rsp,
}, },
{
.opcode = nci_opcode_pack(NCI_GID_PROPRIETARY,
NCI_PROP_DUAL_OPTION),
.rsp = s3fwrn5_nci_prop_rsp,
},
}; };
#define S3FWRN5_RFREG_SECTION_SIZE 252 #define S3FWRN5_RFREG_SECTION_SIZE 252
@ -117,3 +125,112 @@ out:
release_firmware(fw); release_firmware(fw);
return ret; return ret;
} }
/*
* Configure the reference clock. The S3NRN4V expects the single-byte FW_CFG
* form (just the clock-speed selector). The downstream stack sends this in the
* bootloader before CORE_RESET; the earliest the mainline NCI core lets us in
* is the ->setup hook (after CORE_RESET, before CORE_INIT), which works.
*/
int s3fwrn5_nci_clk_cfg(struct s3fwrn5_info *info)
{
u8 clk_speed = NCI_PROP_FW_CFG_CLK_SPEED;
return nci_prop_cmd(info->ndev, NCI_PROP_FW_CFG, 1, &clk_speed);
}
/*
* S3NRN4V RF register update. The HW and SW register blobs are merged into a
* single stream (HW first) and pushed via the DUAL_OPTION command:
* START_UPDATE, one SET_OPTION per 252-byte section, then STOP_UPDATE carrying
* a 16-bit checksum (running sum of the merged stream as 32-bit words).
*/
int s3fwrn5_nci_rf_configure_dual(struct s3fwrn5_info *info,
const char *hw_name, const char *sw_name)
{
const struct firmware *hw_fw = NULL, *sw_fw = NULL;
struct nci_prop_dual_set_option_cmd set_option;
struct device *dev = &info->ndev->nfc_dev->dev;
size_t merged_size, i, len;
u8 *merged = NULL;
u8 stop_cmd[3];
u32 checksum;
u8 sub_oid;
int ret;
ret = request_firmware(&hw_fw, hw_name, dev);
if (ret < 0)
return ret;
ret = request_firmware(&sw_fw, sw_name, dev);
if (ret < 0)
goto out_hw;
merged_size = hw_fw->size + sw_fw->size;
/*
* The stream is checksummed as 32-bit words and pushed in at most 256
* sections (the section index is a single byte); reject blobs that
* would silently break either.
*/
if (merged_size % 4 ||
merged_size > 256 * NCI_PROP_DUAL_SECTION_SIZE) {
dev_err(dev, "invalid rfreg blob size (%zu)\n", merged_size);
ret = -EINVAL;
goto out;
}
merged = kmalloc(merged_size, GFP_KERNEL);
if (!merged) {
ret = -ENOMEM;
goto out;
}
memcpy(merged, hw_fw->data, hw_fw->size);
memcpy(merged + hw_fw->size, sw_fw->data, sw_fw->size);
/* Running sum of the merged stream as little-endian 32-bit words. */
checksum = 0;
for (i = 0; i + 4 <= merged_size; i += 4)
checksum += get_unaligned_le32(merged + i);
dev_dbg(dev, "rfreg dual-option update: %s + %s\n", hw_name, sw_name);
/* START_UPDATE */
sub_oid = NCI_PROP_DUAL_SUB_START_UPDATE;
ret = nci_prop_cmd(info->ndev, NCI_PROP_DUAL_OPTION, 1, &sub_oid);
if (ret < 0) {
dev_err(dev, "Unable to start rfreg update\n");
goto out;
}
/* SET_OPTION per section */
set_option.sub_oid = NCI_PROP_DUAL_SUB_SET_OPTION;
set_option.index = 0;
for (i = 0; i < merged_size; i += NCI_PROP_DUAL_SECTION_SIZE) {
len = min_t(size_t, merged_size - i, NCI_PROP_DUAL_SECTION_SIZE);
memcpy(set_option.data, merged + i, len);
ret = nci_prop_cmd(info->ndev, NCI_PROP_DUAL_OPTION,
len + 2, (__u8 *)&set_option);
if (ret < 0) {
dev_err(dev, "rfreg update error (code=%d)\n", ret);
goto out;
}
set_option.index++;
}
/* STOP_UPDATE with checksum */
stop_cmd[0] = NCI_PROP_DUAL_SUB_STOP_UPDATE;
put_unaligned_le16(checksum, &stop_cmd[1]);
ret = nci_prop_cmd(info->ndev, NCI_PROP_DUAL_OPTION, 3, stop_cmd);
if (ret < 0) {
dev_err(dev, "Unable to stop rfreg update\n");
goto out;
}
dev_dbg(dev, "rfreg dual-option update: success\n");
out:
kfree(merged);
release_firmware(sw_fw);
out_hw:
release_firmware(hw_fw);
return ret;
}

View file

@ -40,6 +40,13 @@ struct nci_prop_stop_rfreg_rsp {
#define NCI_PROP_FW_CFG 0x28 #define NCI_PROP_FW_CFG 0x28
/*
* Single-byte FW_CFG payload (clock-speed selector) for the S3NRN4V reference
* clock. Taken from the vendor configuration for this part (the encoding is
* not documented).
*/
#define NCI_PROP_FW_CFG_CLK_SPEED 0x11
struct nci_prop_fw_cfg_cmd { struct nci_prop_fw_cfg_cmd {
__u8 clk_type; __u8 clk_type;
__u8 clk_speed; __u8 clk_speed;
@ -50,7 +57,30 @@ struct nci_prop_fw_cfg_rsp {
__u8 status; __u8 status;
}; };
extern const struct nci_driver_ops s3fwrn5_nci_prop_ops[4]; /*
* The S3NRN4V updates its RF registers through a single "dual option" command
* (a sub-OID selects the operation) instead of the START/SET/STOP_RFREG
* opcodes above, and expects the HW and SW register blobs merged into one
* stream.
*/
#define NCI_PROP_DUAL_OPTION 0x2a
#define NCI_PROP_DUAL_SUB_START_UPDATE 0x01
#define NCI_PROP_DUAL_SUB_SET_OPTION 0x02
#define NCI_PROP_DUAL_SUB_STOP_UPDATE 0x03
#define NCI_PROP_DUAL_SECTION_SIZE 252
struct nci_prop_dual_set_option_cmd {
__u8 sub_oid; /* NCI_PROP_DUAL_SUB_SET_OPTION */
__u8 index;
__u8 data[NCI_PROP_DUAL_SECTION_SIZE];
};
extern const struct nci_driver_ops s3fwrn5_nci_prop_ops[5];
int s3fwrn5_nci_rf_configure(struct s3fwrn5_info *info, const char *fw_name); int s3fwrn5_nci_rf_configure(struct s3fwrn5_info *info, const char *fw_name);
int s3fwrn5_nci_rf_configure_dual(struct s3fwrn5_info *info,
const char *hw_name, const char *sw_name);
int s3fwrn5_nci_clk_cfg(struct s3fwrn5_info *info);
#endif /* __LOCAL_S3FWRN5_NCI_H_ */ #endif /* __LOCAL_S3FWRN5_NCI_H_ */

View file

@ -21,6 +21,17 @@ enum s3fwrn5_mode {
S3FWRN5_MODE_FW, S3FWRN5_MODE_FW,
}; };
enum s3fwrn5_variant {
/* S3FWRN5 / S3FWRN82: firmware is downloaded by this driver */
S3FWRN5_VARIANT_FWDL,
/*
* S3NRN4V: ships with working firmware behind a bootloader protocol
* this driver does not implement; skip the download, configure the
* clock (FW_CFG) and update the RF registers via the DUAL_OPTION cmd.
*/
S3FWRN5_VARIANT_S3NRN4V,
};
struct s3fwrn5_phy_ops { struct s3fwrn5_phy_ops {
void (*set_wake)(void *id, bool sleep); void (*set_wake)(void *id, bool sleep);
void (*set_mode)(void *id, enum s3fwrn5_mode); void (*set_mode)(void *id, enum s3fwrn5_mode);
@ -36,6 +47,7 @@ struct s3fwrn5_info {
const struct s3fwrn5_phy_ops *phy_ops; const struct s3fwrn5_phy_ops *phy_ops;
struct s3fwrn5_fw_info fw_info; struct s3fwrn5_fw_info fw_info;
enum s3fwrn5_variant variant;
struct mutex mutex; struct mutex mutex;
}; };
@ -78,7 +90,7 @@ static inline int s3fwrn5_write(struct s3fwrn5_info *info, struct sk_buff *skb)
} }
int s3fwrn5_probe(struct nci_dev **ndev, void *phy_id, struct device *pdev, int s3fwrn5_probe(struct nci_dev **ndev, void *phy_id, struct device *pdev,
const struct s3fwrn5_phy_ops *phy_ops); const struct s3fwrn5_phy_ops *phy_ops, enum s3fwrn5_variant variant);
void s3fwrn5_remove(struct nci_dev *ndev); void s3fwrn5_remove(struct nci_dev *ndev);
int s3fwrn5_recv_frame(struct nci_dev *ndev, struct sk_buff *skb, int s3fwrn5_recv_frame(struct nci_dev *ndev, struct sk_buff *skb,

View file

@ -137,7 +137,7 @@ static int s3fwrn82_uart_probe(struct serdev_device *serdev)
} }
ret = s3fwrn5_probe(&phy->common.ndev, phy, &phy->ser_dev->dev, ret = s3fwrn5_probe(&phy->common.ndev, phy, &phy->ser_dev->dev,
&uart_phy_ops); &uart_phy_ops, S3FWRN5_VARIANT_FWDL);
if (ret < 0) if (ret < 0)
goto err_serdev; goto err_serdev;

View file

@ -976,6 +976,20 @@ static enum sdw_command_response qcom_swrm_xfer_msg(struct sdw_bus *bus,
struct qcom_swrm_ctrl *ctrl = to_qcom_sdw(bus); struct qcom_swrm_ctrl *ctrl = to_qcom_sdw(bus);
int ret, i, len; int ret, i, len;
if (msg->page) {
ret = qcom_swrm_cmd_fifo_wr_cmd(ctrl, msg->addr_page1,
msg->dev_num,
SDW_SCP_ADDRPAGE1);
if (ret)
return SDW_CMD_IGNORED;
ret = qcom_swrm_cmd_fifo_wr_cmd(ctrl, msg->addr_page2,
msg->dev_num,
SDW_SCP_ADDRPAGE2);
if (ret)
return SDW_CMD_IGNORED;
}
if (msg->flags == SDW_MSG_FLAG_READ) { if (msg->flags == SDW_MSG_FLAG_READ) {
for (i = 0; i < msg->len;) { for (i = 0; i < msg->len;) {
len = min(msg->len - i, QCOM_SWRM_MAX_RD_LEN); len = min(msg->len - i, QCOM_SWRM_MAX_RD_LEN);

View file

@ -520,6 +520,54 @@ int qrtr_endpoint_post(struct qrtr_endpoint *ep, const void *data, size_t len)
qrtr_node_assign(node, le32_to_cpu(pkt->server.node)); qrtr_node_assign(node, le32_to_cpu(pkt->server.node));
} }
/* The DSPs are star-connected through this node: forward packets
* destined to another node onto that node's endpoint (e.g. the
* modem's voice stack talking to the ADSP's audio service). Only
* DATA and RESUME_TX transit; control packets keep going to the
* local ns, which does its own mesh-wide redistribution. RESUME_TX
* additionally releases this hop's flow-control token on the
* arrival link: per-hop counters advance in lockstep with the
* end-to-end ones since every packet of the flow transits here.
*/
if (cb->dst_node != qrtr_local_nid &&
cb->dst_node != QRTR_NODE_BCAST &&
(cb->type == QRTR_TYPE_DATA || cb->type == QRTR_TYPE_RESUME_TX)) {
struct sockaddr_qrtr from = {AF_QIPCRTR,
cb->src_node, cb->src_port};
struct sockaddr_qrtr to = {AF_QIPCRTR,
cb->dst_node, cb->dst_port};
struct qrtr_node *dst;
dst = qrtr_node_lookup(cb->dst_node);
if (!dst || dst == node) {
if (dst)
qrtr_node_release(dst);
goto err;
}
if (cb->type == QRTR_TYPE_RESUME_TX) {
struct sk_buff *clone;
clone = skb_clone(skb, GFP_ATOMIC);
if (clone)
qrtr_tx_resume(node, clone);
}
pr_debug("qrtr: fwd %u:%u -> %u:%u type %d len %zu\n",
cb->src_node, cb->src_port,
cb->dst_node, cb->dst_port, cb->type, size);
if (skb_cow_head(skb, sizeof(struct qrtr_hdr_v1))) {
qrtr_node_release(dst);
goto err;
}
qrtr_node_enqueue(dst, skb, cb->type, &from, &to);
qrtr_node_release(dst);
return 0;
}
if (cb->type == QRTR_TYPE_RESUME_TX) { if (cb->type == QRTR_TYPE_RESUME_TX) {
qrtr_tx_resume(node, skb); qrtr_tx_resume(node, skb);
} else { } else {

View file

@ -70,6 +70,7 @@ struct qrtr_node {
unsigned int id; unsigned int id;
struct xarray servers; struct xarray servers;
u32 server_count; u32 server_count;
bool hello;
}; };
/* Max nodes, server, lookup limits are chosen based on the current platform /* Max nodes, server, lookup limits are chosen based on the current platform
@ -212,6 +213,60 @@ static void lookup_notify(struct sockaddr_qrtr *to, struct qrtr_server *srv,
pr_err("failed to send lookup notification\n"); pr_err("failed to send lookup notification\n");
} }
/* FP6 voice bring-up: the DSP firmwares expect the downstream IPC-router
* flooding model - they never send NEW_LOOKUP for services on their peers
* and instead rely on the application processor pushing announcements to
* every link. The modem's voice stack locates the ADSP's service-registry
* notifier this way; without it Voice Services never sees avs/audio come
* up and never creates a vocoder session.
*
* Flooding every announcement to every node wedges the phone within
* seconds of the DSPs booting (control packets are exempt from qrtr flow
* control, so any echo between routers runs unbounded). Push exactly the
* one edge voice needs: ADSP-hosted servers, to the modem, only. Any echo
* is structurally loop-free: a re-announcement of an ADSP server arriving
* from the modem is pushed to nobody (target == origin).
*/
#define QRTR_NS_FWD_SRC_NODE 5 /* adsp */
#define QRTR_NS_FWD_DST_NODE 0 /* modem */
/* Runtime kill-switch (qrtr.fp6_ns_push): off by default so a boot is
* always calm; flip it on, then restart the modem rproc so its HELLO
* triggers the replay.
*/
static bool fp6_ns_push;
module_param(fp6_ns_push, bool, 0644);
MODULE_PARM_DESC(fp6_ns_push, "FP6: push adsp service announcements to the modem");
static void service_announce_remotes(struct qrtr_server *srv,
unsigned int origin, bool new)
{
struct sockaddr_qrtr sq;
struct qrtr_node *node;
if (!fp6_ns_push)
return;
if (srv->node != QRTR_NS_FWD_SRC_NODE)
return;
if (origin == QRTR_NS_FWD_DST_NODE ||
srv->node == QRTR_NS_FWD_DST_NODE ||
qrtr_ns.local_node == QRTR_NS_FWD_DST_NODE)
return;
node = xa_load(&nodes, QRTR_NS_FWD_DST_NODE);
if (!node || !node->hello)
return;
sq.sq_family = AF_QIPCRTR;
sq.sq_node = QRTR_NS_FWD_DST_NODE;
sq.sq_port = QRTR_PORT_CTRL;
if (new)
service_announce_new(&sq, srv);
else
service_announce_del(&sq, srv);
}
static int announce_servers(struct sockaddr_qrtr *sq) static int announce_servers(struct sockaddr_qrtr *sq)
{ {
struct qrtr_server *srv; struct qrtr_server *srv;
@ -234,6 +289,24 @@ static int announce_servers(struct sockaddr_qrtr *sq)
return ret; return ret;
} }
} }
/* FP6 voice: replay the ADSP's servers to a (re)booting modem (see
* service_announce_remotes())
*/
if (fp6_ns_push && sq->sq_node == QRTR_NS_FWD_DST_NODE) {
node = xa_load(&nodes, QRTR_NS_FWD_SRC_NODE);
if (!node)
return 0;
xa_for_each(&node->servers, index, srv) {
ret = service_announce_new(sq, srv);
if (ret < 0 && ret != -ENODEV) {
pr_err("failed to announce new service\n");
return ret;
}
}
}
return 0; return 0;
} }
@ -308,6 +381,8 @@ static int server_del(struct qrtr_node *node, unsigned int port, bool bcast)
/* Broadcast the removal of local servers */ /* Broadcast the removal of local servers */
if (srv->node == qrtr_ns.local_node && bcast) if (srv->node == qrtr_ns.local_node && bcast)
service_announce_del(&qrtr_ns.bcast_sq, srv); service_announce_del(&qrtr_ns.bcast_sq, srv);
else if (bcast)
service_announce_remotes(srv, srv->node, false);
/* Announce the service's disappearance to observers */ /* Announce the service's disappearance to observers */
list_for_each(li, &qrtr_ns.lookups) { list_for_each(li, &qrtr_ns.lookups) {
@ -352,8 +427,13 @@ static int say_hello(struct sockaddr_qrtr *dest)
/* Announce the list of servers registered on the local node */ /* Announce the list of servers registered on the local node */
static int ctrl_cmd_hello(struct sockaddr_qrtr *sq) static int ctrl_cmd_hello(struct sockaddr_qrtr *sq)
{ {
struct qrtr_node *node;
int ret; int ret;
node = node_get(sq->sq_node);
if (node)
node->hello = true;
ret = say_hello(sq); ret = say_hello(sq);
if (ret < 0) if (ret < 0)
return ret; return ret;
@ -521,6 +601,8 @@ static int ctrl_cmd_new_server(struct sockaddr_qrtr *from,
pr_err("failed to announce new service\n"); pr_err("failed to announce new service\n");
return ret; return ret;
} }
} else {
service_announce_remotes(srv, from->sq_node, true);
} }
/* Notify any potential lookups about the new server */ /* Notify any potential lookups about the new server */

View file

@ -301,6 +301,7 @@ config SND_SOC_ALL_CODECS
imply SND_SOC_WCD9335 imply SND_SOC_WCD9335
imply SND_SOC_WCD934X imply SND_SOC_WCD934X
imply SND_SOC_WCD937X_SDW imply SND_SOC_WCD937X_SDW
imply SND_SOC_WCD9378_SDW
imply SND_SOC_WCD938X_SDW imply SND_SOC_WCD938X_SDW
imply SND_SOC_WCD939X_SDW imply SND_SOC_WCD939X_SDW
imply SND_SOC_LPASS_MACRO_COMMON imply SND_SOC_LPASS_MACRO_COMMON
@ -2431,6 +2432,24 @@ config SND_SOC_WCD937X_SDW
via soundwire. via soundwire.
To compile this codec driver say Y or m. To compile this codec driver say Y or m.
config SND_SOC_WCD9378
depends on SND_SOC_WCD9378_SDW
tristate
depends on SOUNDWIRE || !SOUNDWIRE
select SND_SOC_WCD_COMMON
config SND_SOC_WCD9378_SDW
tristate "WCD9378 Codec - SDW"
select SND_SOC_WCD9378
depends on SOUNDWIRE
select REGMAP_SOUNDWIRE
help
The WCD9378 is an audio codec IC connected to the host over
SoundWire, found on Qualcomm SM7635 boards such as the
Fairphone 6. It has three ADCs for analog microphones, mic
bias supplies, headphone/earpiece outputs and MBHC.
To compile this codec driver say Y or m.
config SND_SOC_WCD938X config SND_SOC_WCD938X
depends on SND_SOC_WCD938X_SDW depends on SND_SOC_WCD938X_SDW
tristate tristate

View file

@ -356,6 +356,8 @@ snd-soc-wcd9335-y := wcd9335.o
snd-soc-wcd934x-y := wcd934x.o snd-soc-wcd934x-y := wcd934x.o
snd-soc-wcd937x-y := wcd937x.o snd-soc-wcd937x-y := wcd937x.o
snd-soc-wcd937x-sdw-y := wcd937x-sdw.o snd-soc-wcd937x-sdw-y := wcd937x-sdw.o
snd-soc-wcd9378-y := wcd9378.o
snd-soc-wcd9378-sdw-y := wcd9378-sdw.o
snd-soc-wcd938x-y := wcd938x.o snd-soc-wcd938x-y := wcd938x.o
snd-soc-wcd938x-sdw-y := wcd938x-sdw.o snd-soc-wcd938x-sdw-y := wcd938x-sdw.o
snd-soc-wcd939x-y := wcd939x.o snd-soc-wcd939x-y := wcd939x.o
@ -796,6 +798,11 @@ ifdef CONFIG_SND_SOC_WCD937X_SDW
# avoid link failure by forcing sdw code built-in when needed # avoid link failure by forcing sdw code built-in when needed
obj-$(CONFIG_SND_SOC_WCD937X) += snd-soc-wcd937x-sdw.o obj-$(CONFIG_SND_SOC_WCD937X) += snd-soc-wcd937x-sdw.o
endif endif
obj-$(CONFIG_SND_SOC_WCD9378) += snd-soc-wcd9378.o
ifdef CONFIG_SND_SOC_WCD9378_SDW
# avoid link failure by forcing sdw code built-in when needed
obj-$(CONFIG_SND_SOC_WCD9378) += snd-soc-wcd9378-sdw.o
endif
obj-$(CONFIG_SND_SOC_WCD938X) += snd-soc-wcd938x.o obj-$(CONFIG_SND_SOC_WCD938X) += snd-soc-wcd938x.o
ifdef CONFIG_SND_SOC_WCD938X_SDW ifdef CONFIG_SND_SOC_WCD938X_SDW
# avoid link failure by forcing sdw code built-in when needed # avoid link failure by forcing sdw code built-in when needed

View file

@ -10,9 +10,12 @@
#include <linux/i2c.h> #include <linux/i2c.h>
#include <linux/firmware.h> #include <linux/firmware.h>
#include <linux/bitops.h>
#include <linux/regmap.h> #include <linux/regmap.h>
#include <linux/regulator/consumer.h> #include <linux/regulator/consumer.h>
#include <sound/soc.h> #include <sound/soc.h>
#include <sound/pcm_params.h>
#include <sound/tlv.h>
#include "aw88261.h" #include "aw88261.h"
#include "aw88395/aw88395_data_type.h" #include "aw88395/aw88395_data_type.h"
#include "aw88395/aw88395_device.h" #include "aw88395/aw88395_device.h"
@ -27,64 +30,10 @@ static const struct regmap_config aw88261_remap_config = {
static void aw88261_dev_set_volume(struct aw_device *aw_dev, unsigned int value) static void aw88261_dev_set_volume(struct aw_device *aw_dev, unsigned int value)
{ {
struct aw_volume_desc *vol_desc = &aw_dev->volume_desc; unsigned int volume = min(value, (unsigned int)AW88261_MUTE_VOL);
unsigned int real_value, volume;
unsigned int reg_value;
volume = min((value + vol_desc->init_volume), (unsigned int)AW88261_MUTE_VOL); regmap_update_bits(aw_dev->regmap, AW88261_SYSCTRL2_REG,
real_value = DB_TO_REG_VAL(volume); ~AW88261_VOL_MASK, DB_TO_REG_VAL(volume));
regmap_read(aw_dev->regmap, AW88261_SYSCTRL2_REG, &reg_value);
real_value = (real_value | (reg_value & AW88261_VOL_START_MASK));
dev_dbg(aw_dev->dev, "value 0x%x , real_value:0x%x", value, real_value);
regmap_write(aw_dev->regmap, AW88261_SYSCTRL2_REG, real_value);
}
static void aw88261_dev_fade_in(struct aw_device *aw_dev)
{
struct aw_volume_desc *desc = &aw_dev->volume_desc;
int fade_in_vol = desc->ctl_volume;
int fade_step = aw_dev->fade_step;
int i;
if (fade_step == 0 || aw_dev->fade_in_time == 0) {
aw88261_dev_set_volume(aw_dev, fade_in_vol);
return;
}
for (i = AW88261_MUTE_VOL; i >= fade_in_vol; i -= fade_step) {
aw88261_dev_set_volume(aw_dev, i);
usleep_range(aw_dev->fade_in_time,
aw_dev->fade_in_time + 10);
}
if (i != fade_in_vol)
aw88261_dev_set_volume(aw_dev, fade_in_vol);
}
static void aw88261_dev_fade_out(struct aw_device *aw_dev)
{
struct aw_volume_desc *desc = &aw_dev->volume_desc;
int fade_step = aw_dev->fade_step;
int i;
if (fade_step == 0 || aw_dev->fade_out_time == 0) {
aw88261_dev_set_volume(aw_dev, AW88261_MUTE_VOL);
return;
}
for (i = desc->ctl_volume; i <= AW88261_MUTE_VOL; i += fade_step) {
aw88261_dev_set_volume(aw_dev, i);
usleep_range(aw_dev->fade_out_time, aw_dev->fade_out_time + 10);
}
if (i != AW88261_MUTE_VOL) {
aw88261_dev_set_volume(aw_dev, AW88261_MUTE_VOL);
usleep_range(aw_dev->fade_out_time, aw_dev->fade_out_time + 10);
}
} }
static void aw88261_dev_i2s_tx_enable(struct aw_device *aw_dev, bool flag) static void aw88261_dev_i2s_tx_enable(struct aw_device *aw_dev, bool flag)
@ -120,13 +69,13 @@ static void aw88261_dev_amppd(struct aw_device *aw_dev, bool amppd)
static void aw88261_dev_mute(struct aw_device *aw_dev, bool is_mute) static void aw88261_dev_mute(struct aw_device *aw_dev, bool is_mute)
{ {
if (is_mute) { if (is_mute) {
aw88261_dev_fade_out(aw_dev); aw88261_dev_set_volume(aw_dev, AW88261_MUTE_VOL);
regmap_update_bits(aw_dev->regmap, AW88261_SYSCTRL_REG, regmap_update_bits(aw_dev->regmap, AW88261_SYSCTRL_REG,
~AW88261_HMUTE_MASK, AW88261_HMUTE_ENABLE_VALUE); ~AW88261_HMUTE_MASK, AW88261_HMUTE_ENABLE_VALUE);
} else { } else {
regmap_update_bits(aw_dev->regmap, AW88261_SYSCTRL_REG, regmap_update_bits(aw_dev->regmap, AW88261_SYSCTRL_REG,
~AW88261_HMUTE_MASK, AW88261_HMUTE_DISABLE_VALUE); ~AW88261_HMUTE_MASK, AW88261_HMUTE_DISABLE_VALUE);
aw88261_dev_fade_in(aw_dev); aw88261_dev_set_volume(aw_dev, aw_dev->volume_desc.ctl_volume);
} }
} }
@ -151,21 +100,21 @@ static int aw88261_dev_get_iis_status(struct aw_device *aw_dev)
if (ret) if (ret)
return ret; return ret;
if ((reg_val & AW88261_BIT_PLL_CHECK) != AW88261_BIT_PLL_CHECK) { if ((reg_val & AW88261_BIT_PLL_CHECK) != AW88261_BIT_PLL_CHECK) {
dev_err(aw_dev->dev, "check pll lock fail,reg_val:0x%04x", reg_val); dev_dbg(aw_dev->dev, "check pll lock fail,reg_val:0x%04x", reg_val);
return -EINVAL; return -EINVAL;
} }
return ret; return ret;
} }
static int aw88261_dev_check_mode1_pll(struct aw_device *aw_dev) static int aw88261_dev_check_pll(struct aw_device *aw_dev)
{ {
int ret, i; int ret, i;
for (i = 0; i < AW88261_DEV_SYSST_CHECK_MAX; i++) { for (i = 0; i < AW88261_DEV_SYSST_CHECK_MAX; i++) {
ret = aw88261_dev_get_iis_status(aw_dev); ret = aw88261_dev_get_iis_status(aw_dev);
if (ret) { if (ret) {
dev_err(aw_dev->dev, "mode1 iis signal check error"); dev_dbg(aw_dev->dev, "mode1 iis signal check error");
usleep_range(AW88261_2000_US, AW88261_2000_US + 10); usleep_range(AW88261_2000_US, AW88261_2000_US + 10);
} else { } else {
return ret; return ret;
@ -175,71 +124,74 @@ static int aw88261_dev_check_mode1_pll(struct aw_device *aw_dev)
return -EPERM; return -EPERM;
} }
static int aw88261_dev_check_mode2_pll(struct aw_device *aw_dev) static int aw88261_dev_configure_syspll(struct aw88261 *aw88261)
{
unsigned int reg_val;
int ret, i;
ret = regmap_read(aw_dev->regmap, AW88261_PLLCTRL1_REG, &reg_val);
if (ret)
return ret;
reg_val &= (~AW88261_CCO_MUX_MASK);
if (reg_val == AW88261_CCO_MUX_DIVIDED_VALUE) {
dev_dbg(aw_dev->dev, "CCO_MUX is already divider");
return -EPERM;
}
/* change mode2 */
ret = regmap_update_bits(aw_dev->regmap, AW88261_PLLCTRL1_REG,
~AW88261_CCO_MUX_MASK, AW88261_CCO_MUX_DIVIDED_VALUE);
if (ret)
return ret;
for (i = 0; i < AW88261_DEV_SYSST_CHECK_MAX; i++) {
ret = aw88261_dev_get_iis_status(aw_dev);
if (ret) {
dev_err(aw_dev->dev, "mode2 iis signal check error");
usleep_range(AW88261_2000_US, AW88261_2000_US + 10);
} else {
break;
}
}
/* change mode1 */
ret = regmap_update_bits(aw_dev->regmap, AW88261_PLLCTRL1_REG,
~AW88261_CCO_MUX_MASK, AW88261_CCO_MUX_BYPASS_VALUE);
if (ret == 0) {
usleep_range(AW88261_2000_US, AW88261_2000_US + 10);
for (i = 0; i < AW88261_DEV_SYSST_CHECK_MAX; i++) {
ret = aw88261_dev_check_mode1_pll(aw_dev);
if (ret) {
dev_err(aw_dev->dev, "mode2 switch to mode1, iis signal check error");
usleep_range(AW88261_2000_US, AW88261_2000_US + 10);
} else {
break;
}
}
}
return ret;
}
static int aw88261_dev_check_syspll(struct aw_device *aw_dev)
{ {
struct aw_device *aw_dev = aw88261->aw_pa;
int ret; int ret;
ret = aw88261_dev_check_mode1_pll(aw_dev); /* Configure TDM slots (I2S is represented as no slots) */
if (ret) { ret = regmap_update_bits(aw_dev->regmap, AW88261_I2SCTRL2_REG,
dev_dbg(aw_dev->dev, "mode1 check iis failed try switch to mode2 check"); ~AW88261_SLOT_NUM_MASK, aw88261->slot_num_value);
ret = aw88261_dev_check_mode2_pll(aw_dev); if (ret)
if (ret) { return ret;
dev_err(aw_dev->dev, "mode2 check iis failed");
return ret;
}
}
return ret; ret = regmap_update_bits(aw_dev->regmap, AW88261_I2SCTRL2_REG,
~AW88261_I2S_TX_SLOTVLD_MASK,
aw88261->tx_slotvld_mask);
if (ret)
return ret;
ret = regmap_update_bits(aw_dev->regmap, AW88261_I2SCTRL2_REG,
~AW88261_I2S_RXL_SLOTVLD_MASK,
aw88261->rxl_slotvld_mask);
if (ret)
return ret;
ret = regmap_update_bits(aw_dev->regmap, AW88261_I2SCTRL2_REG,
~AW88261_I2S_RXR_SLOTVLD_MASK,
aw88261->rxr_slotvld_mask);
if (ret)
return ret;
/* PLL divider must be used for 8/16/32 kHz modes */
ret = regmap_update_bits(aw_dev->regmap, AW88261_PLLCTRL1_REG,
~AW88261_CCO_MUX_MASK, aw88261->cco_mux_value);
if (ret)
return ret;
/* The word clock (WCK) defines the beginning of a frame */
ret = regmap_update_bits(aw_dev->regmap, AW88261_I2SCTRL1_REG,
~AW88261_I2SSR_MASK, aw88261->sr_value);
if (ret)
return ret;
/* The bit clock (BCK) defines the length of a frame */
ret = regmap_update_bits(aw_dev->regmap, AW88261_I2SCTRL1_REG,
~AW88261_I2SBCK_MASK,
(aw88261->tdm_bck_value != AW88261_TDM_BCK_UNSET)
? aw88261->tdm_bck_value : aw88261->bck_value);
if (ret)
return ret;
/* The logical frame size is the width of data for 1 slot */
ret = regmap_update_bits(aw_dev->regmap, AW88261_I2SCTRL1_REG,
~AW88261_I2SFS_MASK, aw88261->fs_value);
if (ret)
return ret;
/* The I2S interface mode (Philips standard, LSB/MSB justified) */
ret = regmap_update_bits(aw_dev->regmap, AW88261_I2SCTRL1_REG,
~AW88261_I2SMD_MASK, aw88261->md_value);
if (ret)
return ret;
/* The polarity of the bit clock (BCK) */
ret = regmap_update_bits(aw_dev->regmap, AW88261_SYSCTRL_REG,
~AW88261_BCKINV_MASK, aw88261->bck_inv_value);
if (ret)
return ret;
return aw88261_dev_check_pll(aw_dev);
} }
static int aw88261_dev_check_sysst(struct aw_device *aw_dev) static int aw88261_dev_check_sysst(struct aw_device *aw_dev)
@ -254,10 +206,10 @@ static int aw88261_dev_check_sysst(struct aw_device *aw_dev)
return ret; return ret;
check_val = reg_val & (~AW88261_BIT_SYSST_CHECK_MASK) check_val = reg_val & (~AW88261_BIT_SYSST_CHECK_MASK)
& AW88261_BIT_SYSST_CHECK; & AW88261_BIT_PLL_CHECK;
if (check_val != AW88261_BIT_SYSST_CHECK) { if (check_val != AW88261_BIT_PLL_CHECK) {
dev_err(aw_dev->dev, "check sysst fail, reg_val=0x%04x, check:0x%x", dev_dbg(aw_dev->dev, "check sysst fail, reg_val=0x%04x, check:0x%x",
reg_val, AW88261_BIT_SYSST_CHECK); reg_val, AW88261_BIT_PLL_CHECK);
usleep_range(AW88261_2000_US, AW88261_2000_US + 10); usleep_range(AW88261_2000_US, AW88261_2000_US + 10);
} else { } else {
return 0; return 0;
@ -284,22 +236,22 @@ static void aw88261_reg_force_set(struct aw88261 *aw88261)
if (aw88261->frcset_en == AW88261_FRCSET_ENABLE) { if (aw88261->frcset_en == AW88261_FRCSET_ENABLE) {
/* set FORCE_PWM */ /* set FORCE_PWM */
regmap_update_bits(aw88261->regmap, AW88261_BSTCTRL3_REG, regmap_update_bits(aw88261->regmap, AW88261_BSTCTRL3_REG,
AW88261_FORCE_PWM_MASK, AW88261_FORCE_PWM_FORCEMINUS_PWM_VALUE); ~AW88261_FORCE_PWM_MASK, AW88261_FORCE_PWM_FORCEMINUS_PWM_VALUE);
/* set BOOST_OS_WIDTH */ /* set BOOST_OS_WIDTH */
regmap_update_bits(aw88261->regmap, AW88261_BSTCTRL5_REG, regmap_update_bits(aw88261->regmap, AW88261_BSTCTRL5_REG,
AW88261_BST_OS_WIDTH_MASK, AW88261_BST_OS_WIDTH_50NS_VALUE); ~AW88261_BST_OS_WIDTH_MASK, AW88261_BST_OS_WIDTH_50NS_VALUE);
/* set BURST_LOOPR */ /* set BURST_LOOPR */
regmap_update_bits(aw88261->regmap, AW88261_BSTCTRL6_REG, regmap_update_bits(aw88261->regmap, AW88261_BSTCTRL6_REG,
AW88261_BST_LOOPR_MASK, AW88261_BST_LOOPR_340K_VALUE); ~AW88261_BST_LOOPR_MASK, AW88261_BST_LOOPR_340K_VALUE);
/* set RSQN_DLY */ /* set RSQN_DLY */
regmap_update_bits(aw88261->regmap, AW88261_BSTCTRL7_REG, regmap_update_bits(aw88261->regmap, AW88261_BSTCTRL7_REG,
AW88261_RSQN_DLY_MASK, AW88261_RSQN_DLY_35NS_VALUE); ~AW88261_RSQN_DLY_MASK, AW88261_RSQN_DLY_35NS_VALUE);
/* set BURST_SSMODE */ /* set BURST_SSMODE */
regmap_update_bits(aw88261->regmap, AW88261_BSTCTRL8_REG, regmap_update_bits(aw88261->regmap, AW88261_BSTCTRL8_REG,
AW88261_BURST_SSMODE_MASK, AW88261_BURST_SSMODE_FAST_VALUE); ~AW88261_BURST_SSMODE_MASK, AW88261_BURST_SSMODE_FAST_VALUE);
/* set BST_BURST */ /* set BST_BURST */
regmap_update_bits(aw88261->regmap, AW88261_BSTCTRL9_REG, regmap_update_bits(aw88261->regmap, AW88261_BSTCTRL9_REG,
AW88261_BST_BURST_MASK, AW88261_BST_BURST_30MA_VALUE); ~AW88261_BST_BURST_MASK, AW88261_BST_BURST_30MA_VALUE);
} else { } else {
dev_dbg(aw88261->aw_pa->dev, "needn't set reg value"); dev_dbg(aw88261->aw_pa->dev, "needn't set reg value");
} }
@ -550,7 +502,7 @@ static int aw88261_dev_start(struct aw88261 *aw88261)
int ret; int ret;
if (aw_dev->status == AW88261_DEV_PW_ON) { if (aw_dev->status == AW88261_DEV_PW_ON) {
dev_info(aw_dev->dev, "already power on"); dev_dbg(aw_dev->dev, "already power on");
return 0; return 0;
} }
@ -558,9 +510,9 @@ static int aw88261_dev_start(struct aw88261 *aw88261)
aw88261_dev_pwd(aw_dev, false); aw88261_dev_pwd(aw_dev, false);
usleep_range(AW88261_2000_US, AW88261_2000_US + 10); usleep_range(AW88261_2000_US, AW88261_2000_US + 10);
ret = aw88261_dev_check_syspll(aw_dev); ret = aw88261_dev_configure_syspll(aw88261);
if (ret) { if (ret) {
dev_err(aw_dev->dev, "pll check failed cannot start"); dev_dbg(aw_dev->dev, "pll check failed");
goto pll_check_fail; goto pll_check_fail;
} }
@ -571,7 +523,7 @@ static int aw88261_dev_start(struct aw88261 *aw88261)
/* check i2s status */ /* check i2s status */
ret = aw88261_dev_check_sysst(aw_dev); ret = aw88261_dev_check_sysst(aw_dev);
if (ret) { if (ret) {
dev_err(aw_dev->dev, "sysst check failed"); dev_dbg(aw_dev->dev, "sysst check failed");
goto sysst_check_fail; goto sysst_check_fail;
} }
@ -672,31 +624,25 @@ static void aw88261_start_pa(struct aw88261 *aw88261)
for (i = 0; i < AW88261_START_RETRIES; i++) { for (i = 0; i < AW88261_START_RETRIES; i++) {
ret = aw88261_reg_update(aw88261, aw88261->phase_sync); ret = aw88261_reg_update(aw88261, aw88261->phase_sync);
if (ret) { if (ret) {
dev_err(aw88261->aw_pa->dev, "fw update failed, cnt:%d\n", i); dev_dbg(aw88261->aw_pa->dev,
"aw88261_reg_update failed, cnt:%d, ret:%d\n", i, ret);
continue; continue;
} }
ret = aw88261_dev_start(aw88261); ret = aw88261_dev_start(aw88261);
if (ret) { if (ret) {
dev_err(aw88261->aw_pa->dev, "aw88261 device start failed. retry = %d", i); dev_dbg(aw88261->aw_pa->dev,
"aw88261_dev_start failed, cnt:%d, ret:%d\n", i, ret);
continue; continue;
} else { } else {
dev_info(aw88261->aw_pa->dev, "start success\n"); dev_dbg(aw88261->aw_pa->dev, "start success\n");
break; break;
} }
} }
if (ret != 0)
dev_err(aw88261->aw_pa->dev, "start failure (%d)\n", ret);
} }
static void aw88261_startup_work(struct work_struct *work) static void aw88261_start(struct aw88261 *aw88261)
{
struct aw88261 *aw88261 =
container_of(work, struct aw88261, start_work.work);
mutex_lock(&aw88261->lock);
aw88261_start_pa(aw88261);
mutex_unlock(&aw88261->lock);
}
static void aw88261_start(struct aw88261 *aw88261, bool sync_start)
{ {
if (aw88261->aw_pa->fw_status != AW88261_DEV_FW_OK) if (aw88261->aw_pa->fw_status != AW88261_DEV_FW_OK)
return; return;
@ -704,14 +650,226 @@ static void aw88261_start(struct aw88261 *aw88261, bool sync_start)
if (aw88261->aw_pa->status == AW88261_DEV_PW_ON) if (aw88261->aw_pa->status == AW88261_DEV_PW_ON)
return; return;
if (sync_start == AW88261_SYNC_START) aw88261_start_pa(aw88261);
aw88261_start_pa(aw88261);
else
queue_delayed_work(system_dfl_wq,
&aw88261->start_work,
AW88261_START_WORK_DELAY_MS);
} }
static int aw88261_set_fmt(struct snd_soc_dai *dai, unsigned int fmt)
{
struct snd_soc_component *component = dai->component;
struct aw88261 *aw88261 = snd_soc_component_get_drvdata(component);
switch (fmt & SND_SOC_DAIFMT_INV_MASK) {
case SND_SOC_DAIFMT_NB_NF:
aw88261->bck_inv_value = AW88261_BCKINV_NOT_INVERT_VALUE;
break;
case SND_SOC_DAIFMT_IB_NF:
aw88261->bck_inv_value = AW88261_BCKINV_INVERTED_VALUE;
break;
default:
dev_err(aw88261->aw_pa->dev, "unsupported invert mode 0x%x\n",
fmt & SND_SOC_DAIFMT_INV_MASK);
return -EINVAL;
}
switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) {
case SND_SOC_DAIFMT_I2S:
case SND_SOC_DAIFMT_DSP_A:
aw88261->md_value = AW88261_I2SMD_PHILIPS_STANDARD_VALUE;
break;
case SND_SOC_DAIFMT_MSB:
case SND_SOC_DAIFMT_DSP_B:
aw88261->md_value = AW88261_I2SMD_MSB_JUSTIFIED_VALUE;
break;
case SND_SOC_DAIFMT_LSB:
aw88261->md_value = AW88261_I2SMD_LSB_JUSTIFIED_VALUE;
break;
default:
dev_err(aw88261->aw_pa->dev, "unsupported DAI format 0x%x\n",
fmt & SND_SOC_DAIFMT_FORMAT_MASK);
return -EINVAL;
}
return 0;
}
static int aw88261_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params,
struct snd_soc_dai *dai)
{
struct snd_soc_component *component = dai->component;
struct aw88261 *aw88261 = snd_soc_component_get_drvdata(component);
if (substream->stream == SNDRV_PCM_STREAM_CAPTURE)
return 0;
aw88261->cco_mux_value = AW88261_CCO_MUX_BYPASS_VALUE;
switch (params_rate(params)) {
case 8000:
aw88261->sr_value = AW88261_I2SSR_8KHZ_VALUE;
aw88261->cco_mux_value = AW88261_CCO_MUX_DIVIDED_VALUE;
break;
case 11025:
aw88261->sr_value = AW88261_I2SSR_11P025KHZ_VALUE;
break;
case 12000:
aw88261->sr_value = AW88261_I2SSR_12KHZ_VALUE;
break;
case 16000:
aw88261->sr_value = AW88261_I2SSR_16KHZ_VALUE;
aw88261->cco_mux_value = AW88261_CCO_MUX_DIVIDED_VALUE;
break;
case 22050:
aw88261->sr_value = AW88261_I2SSR_22P05KHZ_VALUE;
break;
case 24000:
aw88261->sr_value = AW88261_I2SSR_24KHZ_VALUE;
break;
case 32000:
aw88261->sr_value = AW88261_I2SSR_32KHZ_VALUE;
aw88261->cco_mux_value = AW88261_CCO_MUX_DIVIDED_VALUE;
break;
case 44100:
aw88261->sr_value = AW88261_I2SSR_44P1KHZ_VALUE;
break;
case 48000:
aw88261->sr_value = AW88261_I2SSR_48KHZ_VALUE;
break;
case 96000:
aw88261->sr_value = AW88261_I2SSR_96KHZ_VALUE;
break;
case 192000:
aw88261->sr_value = AW88261_I2SSR_192KHZ_VALUE;
break;
default:
dev_err(aw88261->aw_pa->dev, "unsupported sample rate %d\n",
params_rate(params));
return -EINVAL;
}
switch (params_width(params)) {
case 16:
aw88261->fs_value = AW88261_I2SFS_16_BITS_VALUE;
break;
case 20:
aw88261->fs_value = AW88261_I2SFS_20_BITS_VALUE;
break;
case 24:
aw88261->fs_value = AW88261_I2SFS_24_BITS_VALUE;
break;
case 32:
aw88261->fs_value = AW88261_I2SFS_32_BITS_VALUE;
break;
default:
dev_err(aw88261->aw_pa->dev, "unsupported bit width %d\n",
params_width(params));
return -EINVAL;
}
switch (params_physical_width(params)) {
case 16:
aw88261->bck_value = AW88261_I2SBCK_32FS_VALUE;
break;
case 24:
aw88261->bck_value = AW88261_I2SBCK_48FS_VALUE;
break;
case 32:
aw88261->bck_value = AW88261_I2SBCK_64FS_VALUE;
break;
default:
dev_err(aw88261->aw_pa->dev, "unsupported physical bit width %d\n",
params_physical_width(params));
return -EINVAL;
}
return 0;
}
static int aw88261_set_tdm_slot(struct snd_soc_dai *dai,
unsigned int tx_mask, unsigned int rx_mask, int slots, int slot_width)
{
struct snd_soc_component *component = dai->component;
struct aw88261 *aw88261 = snd_soc_component_get_drvdata(component);
int chan;
switch (slots) {
case 0:
/* Just reset everything TDM related to I2S values */
aw88261->slot_num_value = AW88261_SLOT_NUM_I2S_MODE_VALUE;
aw88261->tdm_bck_value = AW88261_TDM_BCK_UNSET;
aw88261->tx_slotvld_mask = 0 << AW88261_I2S_TX_SLOTVLD_START_BIT;
aw88261->rxl_slotvld_mask = 0 << AW88261_I2S_RXL_SLOTVLD_START_BIT;
aw88261->rxr_slotvld_mask = 1 << AW88261_I2S_RXR_SLOTVLD_START_BIT;
return 0;
case 1:
aw88261->slot_num_value = AW88261_SLOT_NUM_TDM1S_VALUE;
break;
case 2:
aw88261->slot_num_value = AW88261_SLOT_NUM_TDM2S_VALUE;
break;
case 4:
aw88261->slot_num_value = AW88261_SLOT_NUM_TDM4S_VALUE;
break;
case 6:
aw88261->slot_num_value = AW88261_SLOT_NUM_TDM6S_VALUE;
break;
case 8:
aw88261->slot_num_value = AW88261_SLOT_NUM_TDM8S_VALUE;
break;
case 16:
aw88261->slot_num_value = AW88261_SLOT_NUM_TDM16S_VALUE;
break;
default:
dev_err(aw88261->aw_pa->dev, "unsupported slot count %d\n", slots);
return -EINVAL;
}
switch (slot_width) {
case 16:
aw88261->tdm_bck_value = AW88261_I2SBCK_32FS_VALUE;
break;
case 20:
case 24:
aw88261->tdm_bck_value = AW88261_I2SBCK_48FS_VALUE;
break;
case 32:
aw88261->tdm_bck_value = AW88261_I2SBCK_64FS_VALUE;
break;
default:
dev_err(aw88261->aw_pa->dev, "unsupported slot width %d\n",
slot_width);
return -EINVAL;
}
if (tx_mask != 0) {
if ((chan = __ffs(tx_mask)) > 16)
return -EINVAL;
aw88261->tx_slotvld_mask = chan << AW88261_I2S_TX_SLOTVLD_START_BIT;
}
if (rx_mask != 0) {
if ((chan = __ffs(rx_mask)) > 16)
return -EINVAL;
aw88261->rxl_slotvld_mask = chan << AW88261_I2S_RXL_SLOTVLD_START_BIT;
}
if ((rx_mask & ~BIT(chan)) != 0) {
if ((chan = __ffs(rx_mask & ~BIT(chan))) > 16)
return -EINVAL;
aw88261->rxr_slotvld_mask = chan << AW88261_I2S_RXR_SLOTVLD_START_BIT;
}
return 0;
}
static const struct snd_soc_dai_ops aw88261_dai_ops = {
.set_fmt = aw88261_set_fmt,
.hw_params = aw88261_hw_params,
.set_tdm_slot = aw88261_set_tdm_slot,
};
static struct snd_soc_dai_driver aw88261_dai[] = { static struct snd_soc_dai_driver aw88261_dai[] = {
{ {
.name = "aw88261-aif", .name = "aw88261-aif",
@ -730,78 +888,10 @@ static struct snd_soc_dai_driver aw88261_dai[] = {
.rates = AW88261_RATES, .rates = AW88261_RATES,
.formats = AW88261_FORMATS, .formats = AW88261_FORMATS,
}, },
.ops = &aw88261_dai_ops,
}, },
}; };
static int aw88261_get_fade_in_time(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_value *ucontrol)
{
struct snd_soc_component *component = snd_kcontrol_chip(kcontrol);
struct aw88261 *aw88261 = snd_soc_component_get_drvdata(component);
struct aw_device *aw_dev = aw88261->aw_pa;
ucontrol->value.integer.value[0] = aw_dev->fade_in_time;
return 0;
}
static int aw88261_set_fade_in_time(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_value *ucontrol)
{
struct snd_soc_component *component = snd_kcontrol_chip(kcontrol);
struct aw88261 *aw88261 = snd_soc_component_get_drvdata(component);
struct soc_mixer_control *mc =
(struct soc_mixer_control *)kcontrol->private_value;
struct aw_device *aw_dev = aw88261->aw_pa;
int time;
time = ucontrol->value.integer.value[0];
if (time < mc->min || time > mc->max)
return -EINVAL;
if (time != aw_dev->fade_in_time) {
aw_dev->fade_in_time = time;
return 1;
}
return 0;
}
static int aw88261_get_fade_out_time(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_value *ucontrol)
{
struct snd_soc_component *component = snd_kcontrol_chip(kcontrol);
struct aw88261 *aw88261 = snd_soc_component_get_drvdata(component);
struct aw_device *aw_dev = aw88261->aw_pa;
ucontrol->value.integer.value[0] = aw_dev->fade_out_time;
return 0;
}
static int aw88261_set_fade_out_time(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_value *ucontrol)
{
struct snd_soc_component *component = snd_kcontrol_chip(kcontrol);
struct aw88261 *aw88261 = snd_soc_component_get_drvdata(component);
struct soc_mixer_control *mc =
(struct soc_mixer_control *)kcontrol->private_value;
struct aw_device *aw_dev = aw88261->aw_pa;
int time;
time = ucontrol->value.integer.value[0];
if (time < mc->min || time > mc->max)
return -EINVAL;
if (time != aw_dev->fade_out_time) {
aw_dev->fade_out_time = time;
return 1;
}
return 0;
}
static int aw88261_dev_set_profile_index(struct aw_device *aw_dev, int index) static int aw88261_dev_set_profile_index(struct aw_device *aw_dev, int index)
{ {
/* check the index whether is valid */ /* check the index whether is valid */
@ -880,7 +970,7 @@ static int aw88261_profile_set(struct snd_kcontrol *kcontrol,
if (aw88261->aw_pa->status) { if (aw88261->aw_pa->status) {
aw88261_dev_stop(aw88261->aw_pa); aw88261_dev_stop(aw88261->aw_pa);
aw88261_start(aw88261, AW88261_SYNC_START); aw88261_start(aw88261);
} }
mutex_unlock(&aw88261->lock); mutex_unlock(&aw88261->lock);
@ -895,7 +985,8 @@ static int aw88261_volume_get(struct snd_kcontrol *kcontrol,
struct aw88261 *aw88261 = snd_soc_component_get_drvdata(codec); struct aw88261 *aw88261 = snd_soc_component_get_drvdata(codec);
struct aw_volume_desc *vol_desc = &aw88261->aw_pa->volume_desc; struct aw_volume_desc *vol_desc = &aw88261->aw_pa->volume_desc;
ucontrol->value.integer.value[0] = vol_desc->ctl_volume; ucontrol->value.integer.value[0] =
(AW88261_MUTE_VOL - vol_desc->ctl_volume) / 2;
return 0; return 0;
} }
@ -908,13 +999,13 @@ static int aw88261_volume_set(struct snd_kcontrol *kcontrol,
struct aw_volume_desc *vol_desc = &aw88261->aw_pa->volume_desc; struct aw_volume_desc *vol_desc = &aw88261->aw_pa->volume_desc;
struct soc_mixer_control *mc = struct soc_mixer_control *mc =
(struct soc_mixer_control *)kcontrol->private_value; (struct soc_mixer_control *)kcontrol->private_value;
int value; int value = ucontrol->value.integer.value[0];
value = ucontrol->value.integer.value[0];
if (value < mc->min || value > mc->max) if (value < mc->min || value > mc->max)
return -EINVAL; return -EINVAL;
value = AW88261_MUTE_VOL - (value * 2);
if (vol_desc->ctl_volume != value) { if (vol_desc->ctl_volume != value) {
vol_desc->ctl_volume = value; vol_desc->ctl_volume = value;
aw88261_dev_set_volume(aw88261->aw_pa, vol_desc->ctl_volume); aw88261_dev_set_volume(aw88261->aw_pa, vol_desc->ctl_volume);
@ -925,48 +1016,18 @@ static int aw88261_volume_set(struct snd_kcontrol *kcontrol,
return 0; return 0;
} }
static int aw88261_get_fade_step(struct snd_kcontrol *kcontrol, /*
struct snd_ctl_elem_value *ucontrol) * The field contains 4 bits in units of 6dB + 6 bits in units of 0.125dB
{ * which is too precise for TLV (!) so we have to multiply the scale by 2.
struct snd_soc_component *codec = snd_kcontrol_chip(kcontrol); *
struct aw88261 *aw88261 = snd_soc_component_get_drvdata(codec); * The range is clamped at -90dB to prevent overflowing the 4-bit part.
*/
ucontrol->value.integer.value[0] = aw88261->aw_pa->fade_step; static const DECLARE_TLV_DB_SCALE(volume_tlv, -9000, 25, 0);
return 0;
}
static int aw88261_set_fade_step(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_value *ucontrol)
{
struct snd_soc_component *codec = snd_kcontrol_chip(kcontrol);
struct aw88261 *aw88261 = snd_soc_component_get_drvdata(codec);
struct soc_mixer_control *mc =
(struct soc_mixer_control *)kcontrol->private_value;
int value;
value = ucontrol->value.integer.value[0];
if (value < mc->min || value > mc->max)
return -EINVAL;
if (aw88261->aw_pa->fade_step != value) {
aw88261->aw_pa->fade_step = value;
return 1;
}
return 0;
}
static const struct snd_kcontrol_new aw88261_controls[] = { static const struct snd_kcontrol_new aw88261_controls[] = {
SOC_SINGLE_EXT("PCM Playback Volume", AW88261_SYSCTRL2_REG, SOC_SINGLE_EXT_TLV("PCM Playback Volume", AW88261_SYSCTRL2_REG,
6, AW88261_MUTE_VOL, 0, aw88261_volume_get, 6, AW88261_CTL_MAX_VOL, 1,
aw88261_volume_set), aw88261_volume_get, aw88261_volume_set, volume_tlv),
SOC_SINGLE_EXT("Fade Step", 0, 0, AW88261_MUTE_VOL, 0,
aw88261_get_fade_step, aw88261_set_fade_step),
SOC_SINGLE_EXT("Volume Ramp Up Step", 0, 0, FADE_TIME_MAX, FADE_TIME_MIN,
aw88261_get_fade_in_time, aw88261_set_fade_in_time),
SOC_SINGLE_EXT("Volume Ramp Down Step", 0, 0, FADE_TIME_MAX, FADE_TIME_MIN,
aw88261_get_fade_out_time, aw88261_set_fade_out_time),
AW88261_PROFILE_EXT("Profile Set", aw88261_profile_info, AW88261_PROFILE_EXT("Profile Set", aw88261_profile_info,
aw88261_profile_get, aw88261_profile_set), aw88261_profile_get, aw88261_profile_set),
}; };
@ -980,7 +1041,7 @@ static int aw88261_playback_event(struct snd_soc_dapm_widget *w,
mutex_lock(&aw88261->lock); mutex_lock(&aw88261->lock);
switch (event) { switch (event) {
case SND_SOC_DAPM_PRE_PMU: case SND_SOC_DAPM_PRE_PMU:
aw88261_start(aw88261, AW88261_ASYNC_START); aw88261_start(aw88261);
break; break;
case SND_SOC_DAPM_POST_PMD: case SND_SOC_DAPM_POST_PMD:
aw88261_dev_stop(aw88261->aw_pa); aw88261_dev_stop(aw88261->aw_pa);
@ -1057,8 +1118,6 @@ static int aw88261_dev_init(struct aw88261 *aw88261, struct aw_container *aw_cfg
if (ret) if (ret)
return ret; return ret;
aw_dev->fade_in_time = AW88261_500_US;
aw_dev->fade_out_time = AW88261_500_US;
aw_dev->prof_cur = AW88261_INIT_PROFILE; aw_dev->prof_cur = AW88261_INIT_PROFILE;
aw_dev->prof_index = AW88261_INIT_PROFILE; aw_dev->prof_index = AW88261_INIT_PROFILE;
@ -1094,6 +1153,7 @@ static int aw88261_dev_init(struct aw88261 *aw88261, struct aw_container *aw_cfg
static int aw88261_request_firmware_file(struct aw88261 *aw88261) static int aw88261_request_firmware_file(struct aw88261 *aw88261)
{ {
const struct firmware *cont = NULL; const struct firmware *cont = NULL;
struct aw_container *aw_cfg;
const char *fw_name; const char *fw_name;
int ret; int ret;
@ -1111,15 +1171,17 @@ static int aw88261_request_firmware_file(struct aw88261 *aw88261)
dev_info(aw88261->aw_pa->dev, "loaded %s - size: %zu\n", dev_info(aw88261->aw_pa->dev, "loaded %s - size: %zu\n",
fw_name, cont ? cont->size : 0); fw_name, cont ? cont->size : 0);
aw88261->aw_cfg = devm_kzalloc(aw88261->aw_pa->dev, cont->size + sizeof(int), GFP_KERNEL); aw_cfg = devm_kzalloc(aw88261->aw_pa->dev, struct_size(aw_cfg, data, cont->size), GFP_KERNEL);
if (!aw88261->aw_cfg) { if (!aw_cfg) {
release_firmware(cont); release_firmware(cont);
return -ENOMEM; return -ENOMEM;
} }
aw88261->aw_cfg->len = (int)cont->size; aw_cfg->len = (int)cont->size;
memcpy(aw88261->aw_cfg->data, cont->data, cont->size); memcpy(aw_cfg->data, cont->data, cont->size);
release_firmware(cont); release_firmware(cont);
aw88261->aw_cfg = aw_cfg;
ret = aw88395_dev_load_acf_check(aw88261->aw_pa, aw88261->aw_cfg); ret = aw88395_dev_load_acf_check(aw88261->aw_pa, aw88261->aw_cfg);
if (ret) { if (ret) {
dev_err(aw88261->aw_pa->dev, "load [%s] failed !", fw_name); dev_err(aw88261->aw_pa->dev, "load [%s] failed !", fw_name);
@ -1142,8 +1204,6 @@ static int aw88261_codec_probe(struct snd_soc_component *component)
struct aw88261 *aw88261 = snd_soc_component_get_drvdata(component); struct aw88261 *aw88261 = snd_soc_component_get_drvdata(component);
int ret; int ret;
INIT_DELAYED_WORK(&aw88261->start_work, aw88261_startup_work);
ret = aw88261_request_firmware_file(aw88261); ret = aw88261_request_firmware_file(aw88261);
if (ret) if (ret)
return dev_err_probe(aw88261->aw_pa->dev, ret, return dev_err_probe(aw88261->aw_pa->dev, ret,
@ -1167,16 +1227,8 @@ static int aw88261_codec_probe(struct snd_soc_component *component)
return ret; return ret;
} }
static void aw88261_codec_remove(struct snd_soc_component *aw_codec)
{
struct aw88261 *aw88261 = snd_soc_component_get_drvdata(aw_codec);
cancel_delayed_work_sync(&aw88261->start_work);
}
static const struct snd_soc_component_driver soc_codec_dev_aw88261 = { static const struct snd_soc_component_driver soc_codec_dev_aw88261 = {
.probe = aw88261_codec_probe, .probe = aw88261_codec_probe,
.remove = aw88261_codec_remove,
}; };
static void aw88261_parse_channel_dt(struct aw88261 *aw88261) static void aw88261_parse_channel_dt(struct aw88261 *aw88261)
@ -1208,7 +1260,7 @@ static int aw88261_init(struct aw88261 *aw88261, struct i2c_client *i2c, struct
return ret; return ret;
} }
if (chip_id != AW88261_CHIP_ID) { if (chip_id != AW88261_CHIP_ID) {
dev_err(&i2c->dev, "unsupported device"); dev_err(&i2c->dev, "unsupported device id = %x", chip_id);
return -ENXIO; return -ENXIO;
} }
@ -1229,8 +1281,7 @@ static int aw88261_init(struct aw88261 *aw88261, struct i2c_client *i2c, struct
aw_dev->prof_info.prof_type = AW88395_DEV_NONE_TYPE_ID; aw_dev->prof_info.prof_type = AW88395_DEV_NONE_TYPE_ID;
aw_dev->channel = 0; aw_dev->channel = 0;
aw_dev->fw_status = AW88261_DEV_FW_FAILED; aw_dev->fw_status = AW88261_DEV_FW_FAILED;
aw_dev->fade_step = AW88261_VOLUME_STEP_DB; aw_dev->volume_desc.ctl_volume = AW88261_CTL_DEFAULT_VOL;
aw_dev->volume_desc.ctl_volume = AW88261_VOL_DEFAULT_VALUE;
aw_dev->volume_desc.mute_volume = AW88261_MUTE_VOL; aw_dev->volume_desc.mute_volume = AW88261_MUTE_VOL;
aw88261_parse_channel_dt(aw88261); aw88261_parse_channel_dt(aw88261);
@ -1249,6 +1300,17 @@ static int aw88261_i2c_probe(struct i2c_client *i2c)
if (!aw88261) if (!aw88261)
return -ENOMEM; return -ENOMEM;
/* set defaults */
aw88261->slot_num_value = AW88261_SLOT_NUM_I2S_MODE_VALUE;
aw88261->sr_value = AW88261_I2SSR_48KHZ_VALUE;
aw88261->cco_mux_value = AW88261_CCO_MUX_BYPASS_VALUE;
aw88261->fs_value = AW88261_I2SFS_24_BITS_VALUE;
aw88261->bck_value = AW88261_I2SBCK_64FS_VALUE;
aw88261->bck_inv_value = AW88261_BCKINV_NOT_INVERT_VALUE;
aw88261->tdm_bck_value = AW88261_TDM_BCK_UNSET;
aw88261->md_value = AW88261_I2SMD_PHILIPS_STANDARD_VALUE;
aw88261->rxr_slotvld_mask = 1 << AW88261_I2S_RXR_SLOTVLD_START_BIT;
mutex_init(&aw88261->lock); mutex_init(&aw88261->lock);
i2c_set_clientdata(i2c, aw88261); i2c_set_clientdata(i2c, aw88261);
@ -1274,7 +1336,7 @@ static int aw88261_i2c_probe(struct i2c_client *i2c)
} }
static const struct i2c_device_id aw88261_i2c_id[] = { static const struct i2c_device_id aw88261_i2c_id[] = {
{ "aw88261" }, { .name = "aw88261" },
{ } { }
}; };
MODULE_DEVICE_TABLE(i2c, aw88261_i2c_id); MODULE_DEVICE_TABLE(i2c, aw88261_i2c_id);

View file

@ -116,6 +116,19 @@
#define AW88261_VCALK_SHIFT (0) #define AW88261_VCALK_SHIFT (0)
#define AW88261_VCALKL_SHIFT (0) #define AW88261_VCALKL_SHIFT (0)
#define AW88261_BCKINV_START_BIT (4)
#define AW88261_BCKINV_BITS_LEN (1)
#define AW88261_BCKINV_MASK \
(~(((1<<AW88261_BCKINV_BITS_LEN)-1) << AW88261_BCKINV_START_BIT))
#define AW88261_BCKINV_NOT_INVERT (0)
#define AW88261_BCKINV_NOT_INVERT_VALUE \
(AW88261_BCKINV_NOT_INVERT << AW88261_BCKINV_START_BIT)
#define AW88261_BCKINV_INVERTED (1)
#define AW88261_BCKINV_INVERTED_VALUE \
(AW88261_BCKINV_INVERTED << AW88261_BCKINV_START_BIT)
#define AW88261_AMPPD_START_BIT (1) #define AW88261_AMPPD_START_BIT (1)
#define AW88261_AMPPD_BITS_LEN (1) #define AW88261_AMPPD_BITS_LEN (1)
#define AW88261_AMPPD_MASK \ #define AW88261_AMPPD_MASK \
@ -181,12 +194,6 @@
AW88261_OTHS_OT_VALUE | \ AW88261_OTHS_OT_VALUE | \
AW88261_PLLS_LOCKED_VALUE)) AW88261_PLLS_LOCKED_VALUE))
#define AW88261_BIT_SYSST_CHECK \
(AW88261_BSTS_FINISHED_VALUE | \
AW88261_SWS_SWITCHING_VALUE | \
AW88261_CLKS_STABLE_VALUE | \
AW88261_PLLS_LOCKED_VALUE)
#define AW88261_ULS_HMUTE_START_BIT (14) #define AW88261_ULS_HMUTE_START_BIT (14)
#define AW88261_ULS_HMUTE_BITS_LEN (1) #define AW88261_ULS_HMUTE_BITS_LEN (1)
#define AW88261_ULS_HMUTE_MASK \ #define AW88261_ULS_HMUTE_MASK \
@ -249,7 +256,8 @@
#define AW88261_VOL_MASK \ #define AW88261_VOL_MASK \
(~(((1<<AW88261_VOL_BITS_LEN)-1) << AW88261_VOL_START_BIT)) (~(((1<<AW88261_VOL_BITS_LEN)-1) << AW88261_VOL_START_BIT))
#define AW88261_VOL_DEFAULT_VALUE (0) #define AW88261_CTL_MAX_VOL (AW88261_MUTE_VOL / 2)
#define AW88261_CTL_DEFAULT_VOL (AW88261_CTL_MAX_VOL / 2)
#define AW88261_I2STXEN_START_BIT (6) #define AW88261_I2STXEN_START_BIT (6)
#define AW88261_I2STXEN_BITS_LEN (1) #define AW88261_I2STXEN_BITS_LEN (1)
@ -264,7 +272,148 @@
#define AW88261_I2STXEN_ENABLE_VALUE \ #define AW88261_I2STXEN_ENABLE_VALUE \
(AW88261_I2STXEN_ENABLE << AW88261_I2STXEN_START_BIT) (AW88261_I2STXEN_ENABLE << AW88261_I2STXEN_START_BIT)
#define AW88261_CCO_MUX_START_BIT (14) #define AW88261_I2SMD_START_BIT (8)
#define AW88261_I2SMD_BITS_LEN (2)
#define AW88261_I2SMD_MASK \
(~(((1<<AW88261_I2SMD_BITS_LEN)-1) << AW88261_I2SMD_START_BIT))
#define AW88261_I2SMD_PHILIPS_STANDARD (0)
#define AW88261_I2SMD_PHILIPS_STANDARD_VALUE \
(AW88261_I2SMD_PHILIPS_STANDARD << AW88261_I2SMD_START_BIT)
#define AW88261_I2SMD_MSB_JUSTIFIED (1)
#define AW88261_I2SMD_MSB_JUSTIFIED_VALUE \
(AW88261_I2SMD_MSB_JUSTIFIED << AW88261_I2SMD_START_BIT)
#define AW88261_I2SMD_LSB_JUSTIFIED (2)
#define AW88261_I2SMD_LSB_JUSTIFIED_VALUE \
(AW88261_I2SMD_LSB_JUSTIFIED << AW88261_I2SMD_START_BIT)
#define AW88261_I2SFS_START_BIT (6)
#define AW88261_I2SFS_BITS_LEN (2)
#define AW88261_I2SFS_MASK \
(~(((1<<AW88261_I2SFS_BITS_LEN)-1)<<AW88261_I2SFS_START_BIT))
#define AW88261_I2SFS_16_BITS (0)
#define AW88261_I2SFS_16_BITS_VALUE \
(AW88261_I2SFS_16_BITS << AW88261_I2SFS_START_BIT)
#define AW88261_I2SFS_20_BITS (1)
#define AW88261_I2SFS_20_BITS_VALUE \
(AW88261_I2SFS_20_BITS << AW88261_I2SFS_START_BIT)
#define AW88261_I2SFS_24_BITS (2)
#define AW88261_I2SFS_24_BITS_VALUE \
(AW88261_I2SFS_24_BITS << AW88261_I2SFS_START_BIT)
#define AW88261_I2SFS_32_BITS (3)
#define AW88261_I2SFS_32_BITS_VALUE \
(AW88261_I2SFS_32_BITS << AW88261_I2SFS_START_BIT)
#define AW88261_I2SBCK_START_BIT (4)
#define AW88261_I2SBCK_BITS_LEN (2)
#define AW88261_I2SBCK_MASK \
(~(((1<<AW88261_I2SBCK_BITS_LEN)-1) << AW88261_I2SBCK_START_BIT))
#define AW88261_I2SBCK_32FS (0)
#define AW88261_I2SBCK_32FS_VALUE \
(AW88261_I2SBCK_32FS << AW88261_I2SBCK_START_BIT)
#define AW88261_I2SBCK_48FS (1)
#define AW88261_I2SBCK_48FS_VALUE \
(AW88261_I2SBCK_48FS << AW88261_I2SBCK_START_BIT)
#define AW88261_I2SBCK_64FS (2)
#define AW88261_I2SBCK_64FS_VALUE \
(AW88261_I2SBCK_64FS << AW88261_I2SBCK_START_BIT)
#define AW88261_TDM_BCK_UNSET UINT_MAX
#define AW88261_I2SSR_START_BIT (0)
#define AW88261_I2SSR_BITS_LEN (4)
#define AW88261_I2SSR_MASK \
(~(((1<<AW88261_I2SSR_BITS_LEN)-1) << AW88261_I2SSR_START_BIT))
#define AW88261_I2SSR_8KHZ (0)
#define AW88261_I2SSR_8KHZ_VALUE \
(AW88261_I2SSR_8KHZ << AW88261_I2SSR_START_BIT)
#define AW88261_I2SSR_11P025KHZ (1)
#define AW88261_I2SSR_11P025KHZ_VALUE \
(AW88261_I2SSR_11P025KHZ << AW88261_I2SSR_START_BIT)
#define AW88261_I2SSR_12KHZ (2)
#define AW88261_I2SSR_12KHZ_VALUE \
(AW88261_I2SSR_12KHZ << AW88261_I2SSR_START_BIT)
#define AW88261_I2SSR_16KHZ (3)
#define AW88261_I2SSR_16KHZ_VALUE \
(AW88261_I2SSR_16KHZ << AW88261_I2SSR_START_BIT)
#define AW88261_I2SSR_22P05KHZ (4)
#define AW88261_I2SSR_22P05KHZ_VALUE \
(AW88261_I2SSR_22P05KHZ << AW88261_I2SSR_START_BIT)
#define AW88261_I2SSR_24KHZ (5)
#define AW88261_I2SSR_24KHZ_VALUE \
(AW88261_I2SSR_24KHZ << AW88261_I2SSR_START_BIT)
#define AW88261_I2SSR_32KHZ (6)
#define AW88261_I2SSR_32KHZ_VALUE \
(AW88261_I2SSR_32KHZ << AW88261_I2SSR_START_BIT)
#define AW88261_I2SSR_44P1KHZ (7)
#define AW88261_I2SSR_44P1KHZ_VALUE \
(AW88261_I2SSR_44P1KHZ << AW88261_I2SSR_START_BIT)
#define AW88261_I2SSR_48KHZ (8)
#define AW88261_I2SSR_48KHZ_VALUE \
(AW88261_I2SSR_48KHZ << AW88261_I2SSR_START_BIT)
#define AW88261_I2SSR_96KHZ (9)
#define AW88261_I2SSR_96KHZ_VALUE \
(AW88261_I2SSR_96KHZ << AW88261_I2SSR_START_BIT)
#define AW88261_I2SSR_192KHZ (10)
#define AW88261_I2SSR_192KHZ_VALUE \
(AW88261_I2SSR_192KHZ << AW88261_I2SSR_START_BIT)
#define AW88261_SLOT_NUM_START_BIT (12)
#define AW88261_SLOT_NUM_BITS_LEN (3)
#define AW88261_SLOT_NUM_MASK \
(~(((1<<AW88261_SLOT_NUM_BITS_LEN)-1) << AW88261_SLOT_NUM_START_BIT))
#define AW88261_SLOT_NUM_I2S_MODE (0)
#define AW88261_SLOT_NUM_I2S_MODE_VALUE \
(AW88261_SLOT_NUM_I2S_MODE << AW88261_SLOT_NUM_START_BIT)
#define AW88261_SLOT_NUM_TDM1S (1)
#define AW88261_SLOT_NUM_TDM1S_VALUE \
(AW88261_SLOT_NUM_TDM1S << AW88261_SLOT_NUM_START_BIT)
#define AW88261_SLOT_NUM_TDM2S (2)
#define AW88261_SLOT_NUM_TDM2S_VALUE \
(AW88261_SLOT_NUM_TDM2S << AW88261_SLOT_NUM_START_BIT)
#define AW88261_SLOT_NUM_TDM4S (3)
#define AW88261_SLOT_NUM_TDM4S_VALUE \
(AW88261_SLOT_NUM_TDM4S << AW88261_SLOT_NUM_START_BIT)
#define AW88261_SLOT_NUM_TDM6S (4)
#define AW88261_SLOT_NUM_TDM6S_VALUE \
(AW88261_SLOT_NUM_TDM6S << AW88261_SLOT_NUM_START_BIT)
#define AW88261_SLOT_NUM_TDM8S (5)
#define AW88261_SLOT_NUM_TDM8S_VALUE \
(AW88261_SLOT_NUM_TDM8S << AW88261_SLOT_NUM_START_BIT)
#define AW88261_SLOT_NUM_TDM16S (6)
#define AW88261_SLOT_NUM_TDM16S_VALUE \
(AW88261_SLOT_NUM_TDM16S << AW88261_SLOT_NUM_START_BIT)
#define AW88261_I2S_TX_SLOTVLD_START_BIT (8)
#define AW88261_I2S_TX_SLOTVLD_BITS_LEN (4)
#define AW88261_I2S_TX_SLOTVLD_MASK \
(~(((1<<AW88261_I2S_TX_SLOTVLD_BITS_LEN)-1) << AW88261_I2S_TX_SLOTVLD_START_BIT))
#define AW88261_I2S_RXR_SLOTVLD_START_BIT (4)
#define AW88261_I2S_RXR_SLOTVLD_BITS_LEN (4)
#define AW88261_I2S_RXR_SLOTVLD_MASK \
(~(((1<<AW88261_I2S_RXR_SLOTVLD_BITS_LEN)-1) << AW88261_I2S_RXR_SLOTVLD_START_BIT))
#define AW88261_I2S_RXL_SLOTVLD_START_BIT (0)
#define AW88261_I2S_RXL_SLOTVLD_BITS_LEN (4)
#define AW88261_I2S_RXL_SLOTVLD_MASK \
(~(((1<<AW88261_I2S_RXL_SLOTVLD_BITS_LEN)-1) << AW88261_I2S_RXL_SLOTVLD_START_BIT))
#define AW88261_CCO_MUX_START_BIT (6)
#define AW88261_CCO_MUX_BITS_LEN (1) #define AW88261_CCO_MUX_BITS_LEN (1)
#define AW88261_CCO_MUX_MASK \ #define AW88261_CCO_MUX_MASK \
(~(((1<<AW88261_CCO_MUX_BITS_LEN)-1) << AW88261_CCO_MUX_START_BIT)) (~(((1<<AW88261_CCO_MUX_BITS_LEN)-1) << AW88261_CCO_MUX_START_BIT))
@ -370,21 +519,20 @@
#define AW88261_START_RETRIES (5) #define AW88261_START_RETRIES (5)
#define AW88261_START_WORK_DELAY_MS (0) #define AW88261_START_WORK_DELAY_MS (0)
/* NOTE: 192000 has a reg value donwstream but not listed in datasheet */
#define AW88261_RATES (SNDRV_PCM_RATE_8000_48000 | \ #define AW88261_RATES (SNDRV_PCM_RATE_8000_48000 | \
SNDRV_PCM_RATE_12000 | \
SNDRV_PCM_RATE_24000 | \
SNDRV_PCM_RATE_96000) SNDRV_PCM_RATE_96000)
#define AW88261_FORMATS (SNDRV_PCM_FMTBIT_S16_LE | \ #define AW88261_FORMATS (SNDRV_PCM_FMTBIT_S16_LE | \
SNDRV_PCM_FMTBIT_S24_LE | \ SNDRV_PCM_FMTBIT_S24_LE | \
SNDRV_PCM_FMTBIT_S32_LE) SNDRV_PCM_FMTBIT_S32_LE)
#define FADE_TIME_MAX 100000
#define FADE_TIME_MIN 0
#define AW88261_DEV_DEFAULT_CH (0) #define AW88261_DEV_DEFAULT_CH (0)
#define AW88261_ACF_FILE "aw88261_acf.bin" #define AW88261_ACF_FILE "aw88261_acf.bin"
#define AW88261_DEV_SYSST_CHECK_MAX (10) #define AW88261_DEV_SYSST_CHECK_MAX (10)
#define AW88261_SOFT_RESET_VALUE (0x55aa) #define AW88261_SOFT_RESET_VALUE (0x55aa)
#define AW88261_REG_TO_DB (0x3f) #define AW88261_REG_TO_DB (0x3f)
#define AW88261_VOL_START_MASK (0xfc00)
#define AW88261_INIT_PROFILE (0) #define AW88261_INIT_PROFILE (0)
#define REG_VAL_TO_DB(value) ((((value) >> AW88261_VOL_6DB_START) * \ #define REG_VAL_TO_DB(value) ((((value) >> AW88261_VOL_6DB_START) * \
@ -403,11 +551,6 @@
.put = profile_set, \ .put = profile_set, \
} }
enum {
AW88261_SYNC_START = 0,
AW88261_ASYNC_START,
};
enum aw88261_id { enum aw88261_id {
AW88261_CHIP_ID = 0x2113, AW88261_CHIP_ID = 0x2113,
}; };
@ -442,7 +585,6 @@ struct aw88261 {
struct aw_device *aw_pa; struct aw_device *aw_pa;
struct mutex lock; struct mutex lock;
struct gpio_desc *reset_gpio; struct gpio_desc *reset_gpio;
struct delayed_work start_work;
struct regmap *regmap; struct regmap *regmap;
struct aw_container *aw_cfg; struct aw_container *aw_cfg;
@ -451,6 +593,19 @@ struct aw88261 {
unsigned int mute_st; unsigned int mute_st;
unsigned int amppd_st; unsigned int amppd_st;
unsigned int sr_value;
unsigned int cco_mux_value;
unsigned int fs_value;
unsigned int bck_value;
unsigned int bck_inv_value;
unsigned int tdm_bck_value;
unsigned int md_value;
unsigned int slot_num_value;
unsigned int tx_slotvld_mask;
unsigned int rxl_slotvld_mask;
unsigned int rxr_slotvld_mask;
bool phase_sync; bool phase_sync;
}; };

View file

@ -0,0 +1,495 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright (c) 2026, Jorijn van der Graaf
*
* SoundWire slave driver for the Qualcomm WCD9378 audio codec.
*
* The codec presents two SoundWire slaves (RX and TX, mfg 0x0217 part
* 0x0110); the SDCA control space is a 32-bit paged register map accessed
* through the TX slave.
*/
#include <linux/component.h>
#include <linux/device.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/pm_runtime.h>
#include <linux/regmap.h>
#include <linux/soundwire/sdw.h>
#include <linux/soundwire/sdw_registers.h>
#include <linux/soundwire/sdw_type.h>
#include <sound/pcm_params.h>
#include "wcd-common.h"
#include "wcd9378.h"
#define WCD9378_SDW_CH(id, pn, cmask, mmask) \
[id] = { \
.port_num = pn, \
.ch_mask = cmask, \
.master_ch_mask = mmask, \
}
/*
* Each ADC sits alone on its own TX device port (channel 1); by default
* they land on channels 1/2/3 of the same master port (SWRM_TX1 on the
* FP6). DMIC/MBHC masks per the downstream qcom,tx_swr_ch_map.
*/
static struct wcd_sdw_ch_info wcd9378_sdw_tx_ch_info[] = {
WCD9378_SDW_CH(WCD9378_ADC1, WCD9378_ADC_1_PORT, BIT(0), BIT(0)),
WCD9378_SDW_CH(WCD9378_ADC2, WCD9378_ADC_2_PORT, BIT(0), BIT(1)),
WCD9378_SDW_CH(WCD9378_ADC3, WCD9378_ADC_3_PORT, BIT(0), BIT(2)),
WCD9378_SDW_CH(WCD9378_DMIC0, WCD9378_DMIC_0_1_MBHC_PORT, BIT(2), BIT(0)),
WCD9378_SDW_CH(WCD9378_DMIC1, WCD9378_DMIC_0_1_MBHC_PORT, BIT(3), BIT(1)),
WCD9378_SDW_CH(WCD9378_MBHC, WCD9378_DMIC_0_1_MBHC_PORT, BIT(2), BIT(2)),
WCD9378_SDW_CH(WCD9378_DMIC2, WCD9378_DMIC_2_5_PORT, BIT(0), BIT(2)),
WCD9378_SDW_CH(WCD9378_DMIC3, WCD9378_DMIC_2_5_PORT, BIT(1), BIT(3)),
WCD9378_SDW_CH(WCD9378_DMIC4, WCD9378_DMIC_2_5_PORT, BIT(2), BIT(0)),
WCD9378_SDW_CH(WCD9378_DMIC5, WCD9378_DMIC_2_5_PORT, BIT(3), BIT(1)),
};
static struct wcd_sdw_ch_info wcd9378_sdw_rx_ch_info[] = {
WCD9378_SDW_CH(WCD9378_HPH_L, WCD9378_HPH_PORT, BIT(0), BIT(0)),
WCD9378_SDW_CH(WCD9378_HPH_R, WCD9378_HPH_PORT, BIT(1), BIT(1)),
WCD9378_SDW_CH(WCD9378_CLSH, WCD9378_CLSH_PORT, BIT(0), BIT(0)),
WCD9378_SDW_CH(WCD9378_COMP_L, WCD9378_COMP_PORT, BIT(0), BIT(0)),
WCD9378_SDW_CH(WCD9378_COMP_R, WCD9378_COMP_PORT, BIT(1), BIT(1)),
WCD9378_SDW_CH(WCD9378_LO, WCD9378_LO_PORT, BIT(0), BIT(0)),
WCD9378_SDW_CH(WCD9378_DSD_L, WCD9378_DSD_PORT, BIT(0), BIT(0)),
WCD9378_SDW_CH(WCD9378_DSD_R, WCD9378_DSD_PORT, BIT(1), BIT(1)),
};
static struct sdw_dpn_prop wcd9378_dpn_prop[WCD9378_MAX_SWR_PORTS] = {
{
.num = 1,
.type = SDW_DPN_SIMPLE,
.min_ch = 1,
.max_ch = 8,
.simple_ch_prep_sm = true,
}, {
.num = 2,
.type = SDW_DPN_SIMPLE,
.min_ch = 1,
.max_ch = 4,
.simple_ch_prep_sm = true,
}, {
.num = 3,
.type = SDW_DPN_SIMPLE,
.min_ch = 1,
.max_ch = 4,
.simple_ch_prep_sm = true,
}, {
.num = 4,
.type = SDW_DPN_SIMPLE,
.min_ch = 1,
.max_ch = 4,
.simple_ch_prep_sm = true,
}, {
.num = 5,
.type = SDW_DPN_SIMPLE,
.min_ch = 1,
.max_ch = 4,
.simple_ch_prep_sm = true,
}
};
int wcd9378_sdw_hw_params(struct wcd9378_sdw_priv *wcd,
struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params,
struct snd_soc_dai *dai)
{
struct sdw_port_config port_config[WCD9378_MAX_SWR_PORTS];
unsigned long ch_mask;
int i, j;
wcd->sconfig.ch_count = 1;
wcd->active_ports = 0;
for (i = 0; i < WCD9378_MAX_SWR_PORTS; i++) {
ch_mask = wcd->port_config[i].ch_mask;
if (!ch_mask)
continue;
for_each_set_bit(j, &ch_mask, 4)
wcd->sconfig.ch_count++;
port_config[wcd->active_ports] = wcd->port_config[i];
wcd->active_ports++;
}
wcd->sconfig.bps = 1;
wcd->sconfig.frame_rate = params_rate(params);
wcd->sconfig.direction = wcd->is_tx ? SDW_DATA_DIR_TX : SDW_DATA_DIR_RX;
wcd->sconfig.type = SDW_STREAM_PCM;
return sdw_stream_add_slave(wcd->sdev, &wcd->sconfig,
&port_config[0], wcd->active_ports,
wcd->sruntime);
}
EXPORT_SYMBOL_GPL(wcd9378_sdw_hw_params);
/*
* Tell the codec the bus clock: base 19.2 MHz plus a scale (div) per bank.
* The downstream driver writes these raw SCP registers on every capture
* start; here the bus_config callback covers bank switches.
*/
static int wcd9378_bus_config(struct sdw_slave *slave,
struct sdw_bus_params *params)
{
u8 scale;
switch (params->curr_dr_freq) {
case 4800000:
scale = WCD9378_SWRS_CLK_SCALE_DIV4;
break;
case 9600000:
default:
scale = WCD9378_SWRS_CLK_SCALE_DIV2;
break;
}
sdw_write(slave, WCD9378_SWRS_SCP_HOST_CLK_DIV2_CTL(params->next_bank),
0x01);
sdw_write(slave, WCD9378_SWRS_SCP_BASE_CLK,
WCD9378_SWRS_BASE_CLK_19P2MHZ);
sdw_write(slave, WCD9378_SWRS_SCP_BUSCLK_SCALE_BANK0, scale);
sdw_write(slave, WCD9378_SWRS_SCP_BUSCLK_SCALE_BANK1, scale);
return 0;
}
static const struct sdw_slave_ops wcd9378_slave_ops = {
.update_status = wcd_update_status,
.bus_config = wcd9378_bus_config,
};
static const struct reg_default wcd9378_defaults[] = {
{ WCD9378_ANA_BIAS, 0x00 },
{ WCD9378_ANA_TX_CH1, 0x20 },
{ WCD9378_ANA_TX_CH2, 0x00 },
{ WCD9378_ANA_TX_CH3, 0x20 },
{ WCD9378_ANA_TX_CH3_HPF, 0x00 },
{ WCD9378_ANA_MICB2_RAMP, 0x00 },
{ WCD9378_BIAS_VBG_FINE_ADJ, 0x55 },
{ WCD9378_MBHC_CTL_SPARE_1, 0x02 },
{ WCD9378_MICB1_TEST_CTL_2, 0x00 },
{ WCD9378_MICB2_TEST_CTL_2, 0x00 },
{ WCD9378_MICB3_TEST_CTL_2, 0x80 },
{ WCD9378_TX_COM_TXFE_DIV_CTL, 0x22 },
{ WCD9378_SLEEP_CTL, 0x16 },
{ WCD9378_TX_NEW_CH12_MUX, 0x11 },
{ WCD9378_TX_NEW_CH34_MUX, 0x23 },
{ WCD9378_TOP_CLK_CFG, 0x00 },
{ WCD9378_CDC_ANA_TX_CLK_CTL, 0x0e },
{ WCD9378_CDC_AMIC_CTL, 0x07 },
{ WCD9378_PDM_WD_CTL0, 0x0f },
{ WCD9378_PDM_WD_CTL1, 0x0f },
{ WCD9378_PLATFORM_CTL, 0x01 },
{ WCD9378_SYS_USAGE_CTRL, 0x00 },
{ WCD9378_HPH_UP_T0, 0x02 },
{ WCD9378_HPH_UP_T9, 0x02 },
{ WCD9378_HPH_DN_T0, 0x05 },
{ WCD9378_MICB_REMAP_TABLE_VAL_3, 0x00 },
{ WCD9378_MICB_REMAP_TABLE_VAL_4, 0x00 },
{ WCD9378_MICB_REMAP_TABLE_VAL_5, 0x00 },
{ WCD9378_SM0_MB_SEL, 0x00 },
{ WCD9378_SM1_MB_SEL, 0x00 },
{ WCD9378_SM2_MB_SEL, 0x00 },
{ WCD9378_MB_PULLUP_EN, 0x00 },
{ WCD9378_SMP_AMP_FUNC_ACT, 0x00 },
{ WCD9378_CMT_GRP_MASK, 0x00 },
{ WCD9378_SMP_JACK_IT31_MICB, 0x00 },
{ WCD9378_SMP_JACK_IT31_USAGE, 0x03 },
{ WCD9378_SMP_JACK_PDE34_REQ_PS, 0x03 },
{ WCD9378_SMP_JACK_FUNC_ACT, 0x00 },
{ WCD9378_SMP_MIC_IT11_MICB(0), 0x00 },
{ WCD9378_SMP_MIC_IT11_USAGE(0), 0x03 },
{ WCD9378_SMP_MIC_PDE11_REQ_PS(0), 0x03 },
{ WCD9378_SMP_MIC_FUNC_ACT(0), 0x00 },
{ WCD9378_SMP_MIC_IT11_MICB(1), 0x00 },
{ WCD9378_SMP_MIC_IT11_USAGE(1), 0x03 },
{ WCD9378_SMP_MIC_PDE11_REQ_PS(1), 0x03 },
{ WCD9378_SMP_MIC_FUNC_ACT(1), 0x00 },
{ WCD9378_SMP_MIC_IT11_MICB(2), 0x00 },
{ WCD9378_SMP_MIC_IT11_USAGE(2), 0x03 },
{ WCD9378_SMP_MIC_PDE11_REQ_PS(2), 0x03 },
{ WCD9378_SMP_MIC_FUNC_ACT(2), 0x00 },
};
static bool wcd9378_rdwr_register(struct device *dev, unsigned int reg)
{
switch (reg) {
case WCD9378_ANA_BIAS:
case WCD9378_ANA_TX_CH1:
case WCD9378_ANA_TX_CH2:
case WCD9378_ANA_TX_CH3:
case WCD9378_ANA_TX_CH3_HPF:
case WCD9378_ANA_MICB2_RAMP:
case WCD9378_BIAS_VBG_FINE_ADJ:
case WCD9378_MBHC_CTL_SPARE_1:
case WCD9378_MICB1_TEST_CTL_2:
case WCD9378_MICB2_TEST_CTL_2:
case WCD9378_MICB3_TEST_CTL_2:
case WCD9378_TX_COM_TXFE_DIV_CTL:
case WCD9378_SLEEP_CTL:
case WCD9378_TX_NEW_CH12_MUX:
case WCD9378_TX_NEW_CH34_MUX:
case WCD9378_HPH_RDAC_GAIN_CTL:
case WCD9378_HPH_RDAC_HD2_CTL_L:
case WCD9378_HPH_RDAC_HD2_CTL_R:
case WCD9378_TOP_CLK_CFG:
case WCD9378_CDC_ANA_TX_CLK_CTL:
case WCD9378_CDC_AMIC_CTL:
case WCD9378_PDM_WD_CTL0:
case WCD9378_PDM_WD_CTL1:
case WCD9378_PLATFORM_CTL:
case WCD9378_SYS_USAGE_CTRL:
case WCD9378_HPH_UP_T0:
case WCD9378_HPH_UP_T9:
case WCD9378_HPH_DN_T0:
case WCD9378_MICB_REMAP_TABLE_VAL_3:
case WCD9378_MICB_REMAP_TABLE_VAL_4:
case WCD9378_MICB_REMAP_TABLE_VAL_5:
case WCD9378_SM0_MB_SEL:
case WCD9378_SM1_MB_SEL:
case WCD9378_SM2_MB_SEL:
case WCD9378_MB_PULLUP_EN:
case WCD9378_SMP_AMP_FUNC_STAT:
case WCD9378_SMP_AMP_FUNC_ACT:
case WCD9378_CMT_GRP_MASK:
case WCD9378_SMP_JACK_IT31_MICB:
case WCD9378_SMP_JACK_IT31_USAGE:
case WCD9378_SMP_JACK_PDE34_REQ_PS:
case WCD9378_SMP_JACK_FUNC_STAT:
case WCD9378_SMP_JACK_FUNC_ACT:
case WCD9378_SMP_MIC_IT11_MICB(0):
case WCD9378_SMP_MIC_IT11_USAGE(0):
case WCD9378_SMP_MIC_PDE11_REQ_PS(0):
case WCD9378_SMP_MIC_FUNC_STAT(0):
case WCD9378_SMP_MIC_FUNC_ACT(0):
case WCD9378_SMP_MIC_IT11_MICB(1):
case WCD9378_SMP_MIC_IT11_USAGE(1):
case WCD9378_SMP_MIC_PDE11_REQ_PS(1):
case WCD9378_SMP_MIC_FUNC_STAT(1):
case WCD9378_SMP_MIC_FUNC_ACT(1):
case WCD9378_SMP_MIC_IT11_MICB(2):
case WCD9378_SMP_MIC_IT11_USAGE(2):
case WCD9378_SMP_MIC_PDE11_REQ_PS(2):
case WCD9378_SMP_MIC_FUNC_STAT(2):
case WCD9378_SMP_MIC_FUNC_ACT(2):
return true;
}
return false;
}
static bool wcd9378_volatile_register(struct device *dev, unsigned int reg)
{
switch (reg) {
case WCD9378_FUNC_EXT_ID_0:
case WCD9378_FUNC_EXT_ID_1:
case WCD9378_FUNC_EXT_VER:
case WCD9378_FUNC_STAT:
case WCD9378_DEV_MANU_ID_0:
case WCD9378_DEV_MANU_ID_1:
case WCD9378_DEV_PART_ID_0:
case WCD9378_DEV_PART_ID_1:
case WCD9378_DEV_VER:
case WCD9378_EFUSE_REG_16:
case WCD9378_EFUSE_REG_29:
case WCD9378_SEQ_TX0_STAT:
case WCD9378_SEQ_TX1_STAT:
case WCD9378_SEQ_TX2_STAT:
case WCD9378_SMP_AMP_FUNC_STAT:
case WCD9378_SMP_JACK_FUNC_STAT:
case WCD9378_SMP_JACK_PDE34_ACT_PS:
case WCD9378_SMP_MIC_FUNC_STAT(0):
case WCD9378_SMP_MIC_FUNC_STAT(1):
case WCD9378_SMP_MIC_FUNC_STAT(2):
case WCD9378_SMP_MIC_OT10_USAGE(0):
case WCD9378_SMP_MIC_PDE11_ACT_PS(0):
case WCD9378_SMP_MIC_OT10_USAGE(1):
case WCD9378_SMP_MIC_PDE11_ACT_PS(1):
case WCD9378_SMP_MIC_OT10_USAGE(2):
case WCD9378_SMP_MIC_PDE11_ACT_PS(2):
return true;
}
return false;
}
static bool wcd9378_readable_register(struct device *dev, unsigned int reg)
{
if (wcd9378_volatile_register(dev, reg))
return true;
return wcd9378_rdwr_register(dev, reg);
}
static const struct regmap_config wcd9378_regmap_config = {
.name = "wcd9378_csr",
.reg_bits = 32,
.val_bits = 8,
.cache_type = REGCACHE_MAPLE,
.reg_defaults = wcd9378_defaults,
.num_reg_defaults = ARRAY_SIZE(wcd9378_defaults),
.max_register = WCD9378_MAX_REGISTER,
.readable_reg = wcd9378_readable_register,
.writeable_reg = wcd9378_rdwr_register,
.volatile_reg = wcd9378_volatile_register,
};
static int wcd9378_sdw_probe(struct sdw_slave *pdev,
const struct sdw_device_id *id)
{
struct device *dev = &pdev->dev;
struct wcd9378_sdw_priv *wcd;
u8 master_ch_mask[WCD9378_MAX_SWR_CH_IDS];
int master_ch_mask_size = 0;
int ret, i;
wcd = devm_kzalloc(dev, sizeof(*wcd), GFP_KERNEL);
if (!wcd)
return -ENOMEM;
/* Port map index starts at 0, however the data ports start at index 1 */
if (of_property_present(dev->of_node, "qcom,tx-port-mapping")) {
wcd->is_tx = true;
ret = of_property_read_u32_array(dev->of_node, "qcom,tx-port-mapping",
&pdev->m_port_map[1],
WCD9378_MAX_TX_SWR_PORTS);
} else {
ret = of_property_read_u32_array(dev->of_node, "qcom,rx-port-mapping",
&pdev->m_port_map[1],
WCD9378_MAX_SWR_PORTS);
}
if (ret < 0)
dev_info(dev, "Error getting static port mapping for %s (%d)\n",
wcd->is_tx ? "TX" : "RX", ret);
wcd->sdev = pdev;
dev_set_drvdata(dev, wcd);
pdev->prop.scp_int1_mask = SDW_SCP_INT1_BUS_CLASH |
SDW_SCP_INT1_PARITY;
pdev->prop.lane_control_support = true;
pdev->prop.simple_clk_stop_capable = true;
/* The SDCA control space sits above the 16-bit address range */
pdev->prop.paging_support = true;
memset(master_ch_mask, 0, WCD9378_MAX_SWR_CH_IDS);
if (wcd->is_tx) {
master_ch_mask_size = of_property_count_u8_elems(dev->of_node,
"qcom,tx-channel-mapping");
master_ch_mask_size = min_t(int, master_ch_mask_size,
ARRAY_SIZE(wcd9378_sdw_tx_ch_info));
if (master_ch_mask_size > 0)
ret = of_property_read_u8_array(dev->of_node,
"qcom,tx-channel-mapping",
master_ch_mask,
master_ch_mask_size);
} else {
master_ch_mask_size = of_property_count_u8_elems(dev->of_node,
"qcom,rx-channel-mapping");
master_ch_mask_size = min_t(int, master_ch_mask_size,
ARRAY_SIZE(wcd9378_sdw_rx_ch_info));
if (master_ch_mask_size > 0)
ret = of_property_read_u8_array(dev->of_node,
"qcom,rx-channel-mapping",
master_ch_mask,
master_ch_mask_size);
}
if (wcd->is_tx) {
pdev->prop.source_ports = GENMASK(WCD9378_MAX_TX_SWR_PORTS, 1);
pdev->prop.src_dpn_prop = wcd9378_dpn_prop;
wcd->ch_info = &wcd9378_sdw_tx_ch_info[0];
for (i = 0; i < master_ch_mask_size; i++)
wcd->ch_info[i].master_ch_mask = WCD9378_SWRM_CH_MASK(master_ch_mask[i]);
pdev->prop.wake_capable = true;
wcd->regmap = devm_regmap_init_sdw(pdev, &wcd9378_regmap_config);
if (IS_ERR(wcd->regmap))
return dev_err_probe(dev, PTR_ERR(wcd->regmap),
"Regmap init failed\n");
/* Start in cache-only until device is enumerated */
regcache_cache_only(wcd->regmap, true);
} else {
pdev->prop.sink_ports = GENMASK(WCD9378_MAX_SWR_PORTS, 1);
pdev->prop.sink_dpn_prop = wcd9378_dpn_prop;
wcd->ch_info = &wcd9378_sdw_rx_ch_info[0];
for (i = 0; i < master_ch_mask_size; i++)
wcd->ch_info[i].master_ch_mask = WCD9378_SWRM_CH_MASK(master_ch_mask[i]);
}
ret = component_add(dev, &wcd_sdw_component_ops);
if (ret)
return ret;
/* Set suspended until aggregate device is bind */
pm_runtime_set_suspended(dev);
return 0;
}
static void wcd9378_sdw_remove(struct sdw_slave *pdev)
{
struct device *dev = &pdev->dev;
component_del(dev, &wcd_sdw_component_ops);
}
static const struct sdw_device_id wcd9378_sdw_id[] = {
SDW_SLAVE_ENTRY(0x0217, 0x0110, 0),
{ },
};
MODULE_DEVICE_TABLE(sdw, wcd9378_sdw_id);
static int wcd9378_sdw_runtime_suspend(struct device *dev)
{
struct wcd9378_sdw_priv *wcd = dev_get_drvdata(dev);
if (wcd->regmap) {
regcache_cache_only(wcd->regmap, true);
regcache_mark_dirty(wcd->regmap);
}
return 0;
}
static int wcd9378_sdw_runtime_resume(struct device *dev)
{
struct wcd9378_sdw_priv *wcd = dev_get_drvdata(dev);
if (wcd->regmap) {
regcache_cache_only(wcd->regmap, false);
regcache_sync(wcd->regmap);
}
return 0;
}
static const struct dev_pm_ops wcd9378_sdw_pm_ops = {
RUNTIME_PM_OPS(wcd9378_sdw_runtime_suspend, wcd9378_sdw_runtime_resume, NULL)
};
static struct sdw_driver wcd9378_sdw_driver = {
.probe = wcd9378_sdw_probe,
.remove = wcd9378_sdw_remove,
.ops = &wcd9378_slave_ops,
.id_table = wcd9378_sdw_id,
.driver = {
.name = "wcd9378-sdw",
.pm = pm_ptr(&wcd9378_sdw_pm_ops),
}
};
module_sdw_driver(wcd9378_sdw_driver);
MODULE_DESCRIPTION("WCD9378 SDW codec driver");
MODULE_LICENSE("GPL");

1390
sound/soc/codecs/wcd9378.c Normal file

File diff suppressed because it is too large Load diff

246
sound/soc/codecs/wcd9378.h Normal file
View file

@ -0,0 +1,246 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* Copyright (c) 2026, Jorijn van der Graaf
*
* Register map for the Qualcomm WCD9378 audio codec.
*
* The codec exposes its control registers in the SoundWire SDCA control
* address space (bit 30 set, SDCA function number in bits 25:22), accessed
* through the TX SoundWire slave. The analog core registers (function 0,
* implementation-defined region at +0x180000) are layout-compatible with
* the WCD937x family; on top of that the chip adds SDCA-style functions
* (SmartMIC0/1/2, SmartJACK, SmartAMP) whose sequencers drive the analog
* power-up autonomously.
*/
#ifndef __WCD9378_H__
#define __WCD9378_H__
#include <linux/regmap.h>
#include <linux/soundwire/sdw.h>
#include <linux/soundwire/sdw_type.h>
#include <sound/soc.h>
/* SDCA function 0 (extension unit): device identity */
#define WCD9378_FUNC_EXT_ID_0 0x40000048
#define WCD9378_FUNC_EXT_ID_1 0x40000049
#define WCD9378_FUNC_EXT_VER 0x40000050
#define WCD9378_FUNC_STAT 0x40080000
#define WCD9378_DEV_MANU_ID_0 0x40100060
#define WCD9378_DEV_MANU_ID_1 0x40100061
#define WCD9378_DEV_PART_ID_0 0x40100068
#define WCD9378_DEV_PART_ID_1 0x40100069
#define WCD9378_DEV_VER 0x40100070
/* Analog core (WCD937x-compatible layout), function 0 + 0x180000 */
#define WCD9378_ANA_PAGE 0x40180000
#define WCD9378_ANA_BIAS 0x40180001
#define WCD9378_ANA_BIAS_ANALOG_BIAS_EN BIT(7)
#define WCD9378_ANA_BIAS_PRECHRG_EN BIT(6)
#define WCD9378_ANA_RX_SUPPLIES 0x40180008
#define WCD9378_ANA_TX_CH1 0x4018000e
#define WCD9378_ANA_TX_CH2 0x4018000f
#define WCD9378_ANA_TX_CH2_HPF1_INIT BIT(6)
#define WCD9378_ANA_TX_CH2_HPF2_INIT BIT(5)
#define WCD9378_ANA_TX_CH3 0x40180010
#define WCD9378_ANA_TX_CH3_HPF 0x40180011
#define WCD9378_ANA_TX_CH3_HPF3_INIT BIT(6)
#define WCD9378_ANA_TX_GAIN_MASK GENMASK(4, 0)
#define WCD9378_ANA_MICB1 0x40180022
#define WCD9378_ANA_MICB2 0x40180023
#define WCD9378_ANA_MICB2_RAMP 0x40180024
#define WCD9378_ANA_MICB2_RAMP_SHIFT_CTL_MASK GENMASK(4, 2)
#define WCD9378_ANA_MICB2_RAMP_EN BIT(7)
#define WCD9378_ANA_MICB3 0x40180025
#define WCD9378_BIAS_VBG_FINE_ADJ 0x40180029
#define WCD9378_MBHC_CTL_SPARE_1 0x40180058
#define WCD9378_MICB1_TEST_CTL_2 0x4018006c
#define WCD9378_MICB2_TEST_CTL_2 0x4018006f
#define WCD9378_MICB3_TEST_CTL_2 0x40180072
#define WCD9378_TX_COM_TXFE_DIV_CTL 0x4018007b
#define WCD9378_TX_COM_TXFE_DIV_SEQ_BYPASS BIT(7)
#define WCD9378_SLEEP_CTL 0x40180103
#define WCD9378_SLEEP_CTL_BG_CTL_MASK GENMASK(3, 1)
#define WCD9378_SLEEP_CTL_BG_EN BIT(7)
#define WCD9378_SLEEP_CTL_LDOL_BG_SEL BIT(6)
#define WCD9378_TX_NEW_CH12_MUX 0x4018012e
#define WCD9378_TX_NEW_CH12_MUX_CH1_SEL_MASK GENMASK(2, 0)
#define WCD9378_TX_NEW_CH12_MUX_CH2_SEL_MASK GENMASK(5, 3)
#define WCD9378_TX_NEW_CH34_MUX 0x4018012f
#define WCD9378_TX_NEW_CH34_MUX_CH3_SEL_MASK GENMASK(2, 0)
#define WCD9378_HPH_RDAC_GAIN_CTL 0x40180132
#define WCD9378_HPH_RDAC_HD2_CTL_L 0x40180133
#define WCD9378_HPH_RDAC_HD2_CTL_R 0x40180136
/* Digital page */
#define WCD9378_TOP_CLK_CFG 0x40180407
#define WCD9378_CDC_ANA_TX_CLK_CTL 0x40180417
#define WCD9378_CDC_ANA_TXSCBIAS_CLK_EN BIT(0)
#define WCD9378_CDC_AMIC_CTL 0x4018045a
#define WCD9378_PDM_WD_CTL0 0x40180465
#define WCD9378_PDM_WD_CTL1 0x40180466
#define WCD9378_EFUSE_REG_16 0x401804c0
#define WCD9378_EFUSE_REG_29 0x401804cd
#define WCD9378_PLATFORM_CTL 0x401804f0
/* Sequencer block (SEQR) */
#define WCD9378_SYS_USAGE_CTRL 0x40180501
#define WCD9378_SYS_USAGE_CTRL_MASK GENMASK(3, 0)
#define WCD9378_HPH_UP_T0 0x40180510
#define WCD9378_HPH_UP_T9 0x40180519
#define WCD9378_HPH_DN_T0 0x4018051b
#define WCD9378_SEQ_TX0_STAT 0x40180592
#define WCD9378_SEQ_TX1_STAT 0x40180593
#define WCD9378_SEQ_TX2_STAT 0x40180594
#define WCD9378_MICB_REMAP_TABLE_VAL_3 0x401805a3
#define WCD9378_MICB_REMAP_TABLE_VAL_4 0x401805a4
#define WCD9378_MICB_REMAP_TABLE_VAL_5 0x401805a5
#define WCD9378_SM0_MB_SEL 0x401805b0
#define WCD9378_SM1_MB_SEL 0x401805b1
#define WCD9378_SM2_MB_SEL 0x401805b2
#define WCD9378_SM_MB_SEL_MASK GENMASK(1, 0)
#define WCD9378_MB_PULLUP_EN 0x401805b3
/* SmartAMP SDCA function */
#define WCD9378_SMP_AMP_FUNC_STAT 0x40880000
#define WCD9378_SMP_AMP_FUNC_ACT 0x40880008
/* SmartJACK SDCA function (hosts ADC2 when fed from AMIC2) */
#define WCD9378_CMT_GRP_MASK 0x40c00008
#define WCD9378_SMP_JACK_IT31_MICB 0x40c00798
#define WCD9378_SMP_JACK_IT31_USAGE 0x40c007a0
#define WCD9378_SMP_JACK_PDE34_REQ_PS 0x40c00808
#define WCD9378_SMP_JACK_FUNC_STAT 0x40c80000
#define WCD9378_SMP_JACK_FUNC_ACT 0x40c80008
#define WCD9378_SMP_JACK_PDE34_ACT_PS 0x40c80800
/* SmartMIC0/1/2 SDCA functions (ADC1/ADC2/ADC3 sequencers) */
#define WCD9378_SMP_MIC_BASE(n) (0x41000000 + (n) * 0x400000)
#define WCD9378_SMP_MIC_IT11_MICB(n) (WCD9378_SMP_MIC_BASE(n) + 0x98)
#define WCD9378_SMP_MIC_IT11_USAGE(n) (WCD9378_SMP_MIC_BASE(n) + 0xa0)
#define WCD9378_SMP_MIC_PDE11_REQ_PS(n) (WCD9378_SMP_MIC_BASE(n) + 0x108)
#define WCD9378_SMP_MIC_OT10_USAGE(n) (WCD9378_SMP_MIC_BASE(n) + 0x3a0)
#define WCD9378_SMP_MIC_FUNC_STAT(n) (WCD9378_SMP_MIC_BASE(n) + 0x80000)
#define WCD9378_SMP_MIC_FUNC_ACT(n) (WCD9378_SMP_MIC_BASE(n) + 0x80008)
#define WCD9378_SMP_MIC_PDE11_ACT_PS(n) (WCD9378_SMP_MIC_BASE(n) + 0x80100)
#define WCD9378_MAX_REGISTER 0x41900070
/*
* Raw (16-bit, non-paged) Qualcomm slave SCP registers, written with
* sdw_write() directly. Bus clock indication towards the codec and
* SDCA interrupt type configuration.
*/
#define WCD9378_SWRS_SCP_BASE_CLK 0x4d
#define WCD9378_SWRS_SCP_BUSCLK_SCALE_BANK0 0x62
#define WCD9378_SWRS_SCP_BUSCLK_SCALE_BANK1 0x72
#define WCD9378_SWRS_SCP_SDCA_INTRTYPE_1 0xf4
#define WCD9378_SWRS_SCP_SDCA_INTRTYPE_2 0xf8
#define WCD9378_SWRS_SCP_SDCA_INTRTYPE_3 0xfc
#define WCD9378_SWRS_SCP_HOST_CLK_DIV2_CTL(m) (0xe0 + 0x10 * (m))
#define WCD9378_SWRS_BASE_CLK_19P2MHZ 0x01
#define WCD9378_SWRS_CLK_SCALE_DIV2 0x02 /* 9.6 MHz */
#define WCD9378_SWRS_CLK_SCALE_DIV4 0x03 /* 4.8 MHz */
/* ITxx_USAGE ADC mode values */
#define WCD9378_ADC_USAGE_HIFI 0x01
#define WCD9378_ADC_USAGE_LO_HIF 0x02
#define WCD9378_ADC_USAGE_NORMAL 0x03
#define WCD9378_ADC_USAGE_LP 0x05
#define WCD9378_ADC_USAGE_OFF 0x00
/* ITxx_MICB usage values */
#define WCD9378_MICB_USAGE_OFF 0x00
#define WCD9378_MICB_USAGE_PULL_DOWN 0x01
#define WCD9378_MICB_USAGE_1P2V 0x02
#define WCD9378_MICB_USAGE_1P8V_OR_PULLUP 0x03
#define WCD9378_MICB_USAGE_2P5V 0x04
#define WCD9378_MICB_USAGE_2P75V 0x05
#define WCD9378_MICB_USAGE_2P2V 0xf0
#define WCD9378_MICB_USAGE_2P7V 0xf1
#define WCD9378_MICB_USAGE_2P8V 0xf2
#define WCD9378_MICB_USAGE_REMAP_TABLE_3 0xf3
#define WCD9378_MICB_USAGE_REMAP_TABLE_4 0xf4
#define WCD9378_MICB_USAGE_REMAP_TABLE_5 0xf5
/* PDExx_REQ_PS power states */
#define WCD9378_PDE_PS0_ON 0x00
#define WCD9378_PDE_PS3_OFF 0x03
#define WCD9378_MAX_MICBIAS 3
#define WCD9378_MAX_SWR_CH_IDS 15
#define WCD9378_SWRM_CH_MASK(ch_idx) BIT((ch_idx) - 1)
enum wcd9378_tx_sdw_ports {
WCD9378_ADC_1_PORT = 1,
WCD9378_ADC_2_PORT,
WCD9378_ADC_3_PORT,
WCD9378_DMIC_0_1_MBHC_PORT,
WCD9378_DMIC_2_5_PORT,
WCD9378_MAX_TX_SWR_PORTS = WCD9378_DMIC_2_5_PORT,
};
enum wcd9378_rx_sdw_ports {
WCD9378_HPH_PORT = 1,
WCD9378_CLSH_PORT,
WCD9378_COMP_PORT,
WCD9378_LO_PORT,
WCD9378_DSD_PORT,
WCD9378_MAX_SWR_PORTS = WCD9378_DSD_PORT,
};
enum wcd9378_tx_sdw_channels {
WCD9378_ADC1,
WCD9378_ADC2,
WCD9378_ADC3,
WCD9378_DMIC0,
WCD9378_DMIC1,
WCD9378_MBHC,
WCD9378_DMIC2,
WCD9378_DMIC3,
WCD9378_DMIC4,
WCD9378_DMIC5,
};
enum wcd9378_rx_sdw_channels {
WCD9378_HPH_L,
WCD9378_HPH_R,
WCD9378_CLSH,
WCD9378_COMP_L,
WCD9378_COMP_R,
WCD9378_LO,
WCD9378_DSD_L,
WCD9378_DSD_R,
};
struct wcd9378_priv;
struct wcd9378_sdw_priv {
struct sdw_slave *sdev;
struct sdw_stream_config sconfig;
struct sdw_stream_runtime *sruntime;
struct sdw_port_config port_config[WCD9378_MAX_SWR_PORTS];
struct wcd_sdw_ch_info *ch_info;
bool port_enable[WCD9378_MAX_SWR_CH_IDS];
unsigned int master_channel_map[SDW_MAX_PORTS];
int active_ports;
bool is_tx;
struct wcd9378_priv *wcd9378;
struct regmap *regmap;
};
#if IS_ENABLED(CONFIG_SND_SOC_WCD9378_SDW)
int wcd9378_sdw_hw_params(struct wcd9378_sdw_priv *wcd,
struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params,
struct snd_soc_dai *dai);
#else
static inline int wcd9378_sdw_hw_params(struct wcd9378_sdw_priv *wcd,
struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params,
struct snd_soc_dai *dai)
{
return -EOPNOTSUPP;
}
#endif
#endif /* __WCD9378_H__ */

View file

@ -1,6 +1,6 @@
# SPDX-License-Identifier: GPL-2.0-only # SPDX-License-Identifier: GPL-2.0-only
snd-q6dsp-common-y := q6dsp-common.o q6dsp-lpass-ports.o q6dsp-lpass-clocks.o snd-q6dsp-common-y := q6dsp-common.o q6dsp-lpass-ports.o q6dsp-lpass-clocks.o
snd-q6apm-y := q6apm.o audioreach.o topology.o snd-q6apm-y := q6apm.o audioreach.o topology.o q6apm-voice-proto.o
obj-$(CONFIG_SND_SOC_QDSP6_COMMON) += snd-q6dsp-common.o obj-$(CONFIG_SND_SOC_QDSP6_COMMON) += snd-q6dsp-common.o
obj-$(CONFIG_SND_SOC_QDSP6_CORE) += q6core.o obj-$(CONFIG_SND_SOC_QDSP6_CORE) += q6core.o

View file

@ -576,10 +576,11 @@ void *audioreach_alloc_graph_pkt(struct q6apm *apm,
} }
EXPORT_SYMBOL_GPL(audioreach_alloc_graph_pkt); EXPORT_SYMBOL_GPL(audioreach_alloc_graph_pkt);
int audioreach_send_cmd_sync(struct device *dev, gpr_device_t *gdev, int audioreach_send_cmd_sync_timeout(struct device *dev, gpr_device_t *gdev,
struct gpr_ibasic_rsp_result_t *result, struct mutex *cmd_lock, struct gpr_ibasic_rsp_result_t *result, struct mutex *cmd_lock,
gpr_port_t *port, wait_queue_head_t *cmd_wait, gpr_port_t *port, wait_queue_head_t *cmd_wait,
const struct gpr_pkt *pkt, uint32_t rsp_opcode) const struct gpr_pkt *pkt, uint32_t rsp_opcode,
unsigned long timeout_s)
{ {
const struct gpr_hdr *hdr = &pkt->hdr; const struct gpr_hdr *hdr = &pkt->hdr;
@ -601,9 +602,9 @@ int audioreach_send_cmd_sync(struct device *dev, gpr_device_t *gdev,
if (rsp_opcode) if (rsp_opcode)
rc = wait_event_timeout(*cmd_wait, (result->opcode == hdr->opcode) || rc = wait_event_timeout(*cmd_wait, (result->opcode == hdr->opcode) ||
(result->opcode == rsp_opcode), 5 * HZ); (result->opcode == rsp_opcode), timeout_s * HZ);
else else
rc = wait_event_timeout(*cmd_wait, (result->opcode == hdr->opcode), 5 * HZ); rc = wait_event_timeout(*cmd_wait, (result->opcode == hdr->opcode), timeout_s * HZ);
if (!rc) { if (!rc) {
dev_err(dev, "CMD timeout for [%x] opcode\n", hdr->opcode); dev_err(dev, "CMD timeout for [%x] opcode\n", hdr->opcode);
@ -620,6 +621,16 @@ err:
mutex_unlock(cmd_lock); mutex_unlock(cmd_lock);
return rc; return rc;
} }
EXPORT_SYMBOL_GPL(audioreach_send_cmd_sync_timeout);
int audioreach_send_cmd_sync(struct device *dev, gpr_device_t *gdev,
struct gpr_ibasic_rsp_result_t *result, struct mutex *cmd_lock,
gpr_port_t *port, wait_queue_head_t *cmd_wait,
const struct gpr_pkt *pkt, uint32_t rsp_opcode)
{
return audioreach_send_cmd_sync_timeout(dev, gdev, result, cmd_lock,
port, cmd_wait, pkt, rsp_opcode, 30);
}
EXPORT_SYMBOL_GPL(audioreach_send_cmd_sync); EXPORT_SYMBOL_GPL(audioreach_send_cmd_sync);
int audioreach_graph_send_cmd_sync(struct q6apm_graph *graph, const struct gpr_pkt *pkt, int audioreach_graph_send_cmd_sync(struct q6apm_graph *graph, const struct gpr_pkt *pkt,

View file

@ -58,12 +58,15 @@ struct q6apm_graph;
#define APM_CMD_GRAPH_FLUSH 0x01001005 #define APM_CMD_GRAPH_FLUSH 0x01001005
#define APM_CMD_SET_CFG 0x01001006 #define APM_CMD_SET_CFG 0x01001006
#define APM_CMD_GET_CFG 0x01001007 #define APM_CMD_GET_CFG 0x01001007
#define APM_CMD_REGISTER_CFG 0x01001008
#define APM_CMD_SHARED_MEM_MAP_REGIONS 0x0100100C #define APM_CMD_SHARED_MEM_MAP_REGIONS 0x0100100C
#define APM_CMD_SHARED_MEM_UNMAP_REGIONS 0x0100100D #define APM_CMD_SHARED_MEM_UNMAP_REGIONS 0x0100100D
#define APM_CMD_RSP_SHARED_MEM_MAP_REGIONS 0x02001001 #define APM_CMD_RSP_SHARED_MEM_MAP_REGIONS 0x02001001
#define APM_CMD_RSP_GET_CFG 0x02001000 #define APM_CMD_RSP_GET_CFG 0x02001000
#define APM_CMD_CLOSE_ALL 0x01001013 #define APM_CMD_CLOSE_ALL 0x01001013
#define APM_CMD_REGISTER_SHARED_CFG 0x0100100A #define APM_CMD_REGISTER_SHARED_CFG 0x0100100A
#define AMDB_CMD_LOAD_MODULES 0x01001020
#define AMDB_CMD_RSP_LOAD_MODULES 0x02001008
#define APM_MEMORY_MAP_SHMEM8_4K_POOL 3 #define APM_MEMORY_MAP_SHMEM8_4K_POOL 3
@ -842,6 +845,11 @@ void audioreach_graph_free_buf(struct q6apm_graph *graph);
int audioreach_send_cmd_sync(struct device *dev, gpr_device_t *gdev, struct gpr_ibasic_rsp_result_t *result, int audioreach_send_cmd_sync(struct device *dev, gpr_device_t *gdev, struct gpr_ibasic_rsp_result_t *result,
struct mutex *cmd_lock, gpr_port_t *port, wait_queue_head_t *cmd_wait, struct mutex *cmd_lock, gpr_port_t *port, wait_queue_head_t *cmd_wait,
const struct gpr_pkt *pkt, uint32_t rsp_opcode); const struct gpr_pkt *pkt, uint32_t rsp_opcode);
int audioreach_send_cmd_sync_timeout(struct device *dev, gpr_device_t *gdev,
struct gpr_ibasic_rsp_result_t *result,
struct mutex *cmd_lock, gpr_port_t *port, wait_queue_head_t *cmd_wait,
const struct gpr_pkt *pkt, uint32_t rsp_opcode,
unsigned long timeout_s);
int audioreach_graph_send_cmd_sync(struct q6apm_graph *graph, const struct gpr_pkt *pkt, int audioreach_graph_send_cmd_sync(struct q6apm_graph *graph, const struct gpr_pkt *pkt,
uint32_t rsp_opcode); uint32_t rsp_opcode);
int audioreach_set_media_format(struct q6apm_graph *graph, int audioreach_set_media_format(struct q6apm_graph *graph,

View file

@ -224,6 +224,21 @@ static int q6apm_lpass_dai_prepare(struct snd_pcm_substream *substream, struct s
dev_err(dai->dev, "Failed to prepare Graph %d\n", rc); dev_err(dai->dev, "Failed to prepare Graph %d\n", rc);
goto err; goto err;
} }
/*
* Start the port already at prepare, like q6afe does: this starts
* the interface clocks before the DAPM power-up sequence runs, so
* codecs that need a live BCLK at power-up (e.g. aw88261) can
* start synchronously. The trigger callback keeps its start as a
* no-op fallback via is_port_started.
*/
rc = q6apm_graph_start(dai_data->graph[dai->id]);
if (rc < 0) {
dev_err(dai->dev, "Failed to start APM port %d\n", dai->id);
goto err;
}
dai_data->is_port_started[dai->id] = true;
return 0; return 0;
err: err:
if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {

View file

@ -0,0 +1,380 @@
// 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;
/* GET_CFG responds with the param data; payload dumped in callback */
if (opcode == APM_CMD_GET_CFG)
rsp_opcode = APM_CMD_RSP_GET_CFG;
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;
}

View file

@ -38,6 +38,16 @@ int q6apm_send_cmd_sync(struct q6apm *apm, const struct gpr_pkt *pkt,
NULL, &apm->wait, pkt, rsp_opcode); NULL, &apm->wait, pkt, rsp_opcode);
} }
int q6apm_send_cmd_sync_timeout(struct q6apm *apm, const struct gpr_pkt *pkt,
uint32_t rsp_opcode, unsigned long timeout_s)
{
gpr_device_t *gdev = apm->gdev;
return audioreach_send_cmd_sync_timeout(&gdev->dev, gdev, &apm->result,
&apm->lock, NULL, &apm->wait,
pkt, rsp_opcode, timeout_s);
}
static struct audioreach_graph *q6apm_get_audioreach_graph(struct q6apm *apm, uint32_t graph_id) static struct audioreach_graph *q6apm_get_audioreach_graph(struct q6apm *apm, uint32_t graph_id)
{ {
struct audioreach_graph_info *info; struct audioreach_graph_info *info;
@ -764,6 +774,8 @@ static int apm_probe(gpr_device_t *gdev)
g_apm = apm; g_apm = apm;
q6apm_voice_proto_init(apm);
q6apm_get_apm_state(apm); q6apm_get_apm_state(apm);
ret = snd_soc_register_component(dev, &q6apm_audio_component, NULL, 0); ret = snd_soc_register_component(dev, &q6apm_audio_component, NULL, 0);
@ -781,6 +793,7 @@ static int apm_probe(gpr_device_t *gdev)
static void apm_remove(gpr_device_t *gdev) static void apm_remove(gpr_device_t *gdev)
{ {
q6apm_voice_proto_exit();
of_platform_depopulate(&gdev->dev); of_platform_depopulate(&gdev->dev);
snd_soc_unregister_component(&gdev->dev); snd_soc_unregister_component(&gdev->dev);
} }
@ -817,6 +830,7 @@ static int apm_callback(const struct gpr_resp_pkt *data, void *priv, int op)
case GPR_BASIC_RSP_RESULT: case GPR_BASIC_RSP_RESULT:
switch (result->opcode) { switch (result->opcode) {
case APM_CMD_SHARED_MEM_MAP_REGIONS: case APM_CMD_SHARED_MEM_MAP_REGIONS:
case APM_CMD_REGISTER_CFG:
case APM_CMD_GRAPH_START: case APM_CMD_GRAPH_START:
case APM_CMD_GRAPH_OPEN: case APM_CMD_GRAPH_OPEN:
case APM_CMD_GRAPH_PREPARE: case APM_CMD_GRAPH_PREPARE:
@ -824,6 +838,7 @@ static int apm_callback(const struct gpr_resp_pkt *data, void *priv, int op)
case APM_CMD_GRAPH_FLUSH: case APM_CMD_GRAPH_FLUSH:
case APM_CMD_GRAPH_STOP: case APM_CMD_GRAPH_STOP:
case APM_CMD_SET_CFG: case APM_CMD_SET_CFG:
case APM_CMD_GET_CFG:
apm->result.opcode = result->opcode; apm->result.opcode = result->opcode;
apm->result.status = result->status; apm->result.status = result->status;
if (result->status) if (result->status)
@ -846,9 +861,62 @@ static int apm_callback(const struct gpr_resp_pkt *data, void *priv, int op)
wake_up(&apm->wait); wake_up(&apm->wait);
break; break;
default: default:
/*
* Unlisted command results (debug/probe opcodes sent
* via voice-proto) must still wake the sync waiter or
* every such send stalls for the full timeout.
*/
apm->result.opcode = result->opcode;
apm->result.status = result->status;
if (result->status)
dev_err(dev, "Error (%d) Processing 0x%08x cmd\n",
result->status, result->opcode);
wake_up(&apm->wait);
break; break;
} }
break; break;
case 0x03001000: { /* APM_EVENT_MODULE_TO_CLIENT */
/* apm_module_event_t: event_id, payload_size, payload */
const u32 *ev = data->payload;
u32 ev_sz = hdr->pkt_size - hdr->hdr_size * 4;
dev_info(dev, "MODULE EVENT from 0x%x: event_id 0x%08x size %u\n",
hdr->src_port, ev[0], ev[1]);
print_hex_dump(KERN_INFO, "EVENT: ", DUMP_PREFIX_OFFSET,
16, 4, ev, ev_sz, false);
break;
}
case APM_CMD_RSP_GET_CFG: {
/* status word, then apm_module_param_data_t sections */
const u32 *rsp_pl = data->payload;
u32 pl_sz = hdr->pkt_size - hdr->hdr_size * 4;
dev_info(dev, "GET_CFG rsp: status %d, %u bytes\n",
rsp_pl[0], pl_sz);
print_hex_dump(KERN_INFO, "GET_CFG: ", DUMP_PREFIX_OFFSET,
16, 4, rsp_pl, pl_sz, false);
apm->result.opcode = hdr->opcode;
apm->result.status = rsp_pl[0];
wake_up(&apm->wait);
break;
}
case AMDB_CMD_RSP_LOAD_MODULES: {
/* amdb_module_load_unload_t: proc_domain, error_code,
* num_modules, {module_id, handle_lsw, handle_msw}[].
* A zero handle = module not registered with the AMDB.
*/
const u32 *amdb = data->payload;
u32 i, n = amdb[2];
for (i = 0; i < n; i++)
dev_info(dev, "AMDB load: module 0x%08x handle 0x%08x%08x\n",
amdb[3 + 3 * i], amdb[5 + 3 * i],
amdb[4 + 3 * i]);
apm->result.opcode = hdr->opcode;
apm->result.status = amdb[1];
wake_up(&apm->wait);
break;
}
case APM_CMD_RSP_SHARED_MEM_MAP_REGIONS: case APM_CMD_RSP_SHARED_MEM_MAP_REGIONS:
apm->result.opcode = hdr->opcode; apm->result.opcode = hdr->opcode;
apm->result.status = 0; apm->result.status = 0;

View file

@ -144,6 +144,8 @@ int q6apm_unmap_memory_fixed_region(struct device *dev, unsigned int graph_id);
/* Helpers */ /* Helpers */
int q6apm_send_cmd_sync(struct q6apm *apm, const struct gpr_pkt *pkt, int q6apm_send_cmd_sync(struct q6apm *apm, const struct gpr_pkt *pkt,
uint32_t rsp_opcode); uint32_t rsp_opcode);
int q6apm_send_cmd_sync_timeout(struct q6apm *apm, const struct gpr_pkt *pkt,
uint32_t rsp_opcode, unsigned long timeout_s);
/* Callback for graph specific */ /* Callback for graph specific */
struct audioreach_module *q6apm_find_module_by_mid(struct q6apm_graph *graph, struct audioreach_module *q6apm_find_module_by_mid(struct q6apm_graph *graph,
@ -157,4 +159,8 @@ int q6apm_remove_initial_silence(struct device *dev, struct q6apm_graph *graph,
int q6apm_remove_trailing_silence(struct device *dev, struct q6apm_graph *graph, uint32_t samples); int q6apm_remove_trailing_silence(struct device *dev, struct q6apm_graph *graph, uint32_t samples);
int q6apm_set_real_module_id(struct device *dev, struct q6apm_graph *graph, uint32_t codec_id); int q6apm_set_real_module_id(struct device *dev, struct q6apm_graph *graph, uint32_t codec_id);
int q6apm_get_hw_pointer(struct q6apm_graph *graph, int dir); int q6apm_get_hw_pointer(struct q6apm_graph *graph, int dir);
/* FP6 voice-call bring-up prototype (q6apm-voice-proto.c) */
void q6apm_voice_proto_init(struct q6apm *apm);
void q6apm_voice_proto_exit(void);
#endif /* __APM_GRAPH_ */ #endif /* __APM_GRAPH_ */

View file

@ -34,6 +34,7 @@ static int sc8280xp_snd_init(struct snd_soc_pcm_runtime *rtd)
switch (cpu_dai->id) { switch (cpu_dai->id) {
case PRIMARY_MI2S_RX...QUATERNARY_MI2S_TX: case PRIMARY_MI2S_RX...QUATERNARY_MI2S_TX:
case QUINARY_MI2S_RX...QUINARY_MI2S_TX: case QUINARY_MI2S_RX...QUINARY_MI2S_TX:
case SENARY_MI2S_RX...SENARY_MI2S_TX:
snd_soc_dai_set_fmt(cpu_dai, SND_SOC_DAIFMT_BP_FP); snd_soc_dai_set_fmt(cpu_dai, SND_SOC_DAIFMT_BP_FP);
break; break;
case WSA_CODEC_DMA_RX_0: case WSA_CODEC_DMA_RX_0: