diff --git a/Documentation/devicetree/bindings/soc/apple/apple,t6041-aop.yaml b/Documentation/devicetree/bindings/soc/apple/apple,t6041-aop.yaml new file mode 100644 index 0000000000000..d81cde8a8d534 --- /dev/null +++ b/Documentation/devicetree/bindings/soc/apple/apple,t6041-aop.yaml @@ -0,0 +1,117 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/soc/apple/apple,t6041-aop.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Apple T604x Always-On Processor + +maintainers: + - Jonathan Goyvaerts + +description: + The Always-On Processor provides audio and sensor services through firmware + loaded by the bootloader. The application processor patches the firmware's + boot arguments in the AOP SRAM, starts it through RTKit over one mailbox and + reaches its services through the RTKit endpoints. The bootloader reserves + the firmware's external code and log segments as /reserved-memory nodes + named aop-etext and aop-oslog; no mapping of them survives the reset of the + AOP's DART, so the operating system maps them again before the start. + +properties: + compatible: + const: apple,t6041-aop + + reg: + minItems: 2 + items: + - description: AOP SRAM, holding the boot-argument table + - description: ASC wrapper and processor control registers + - description: Second ASC register window + - description: AOP configuration registers + + reg-names: + minItems: 2 + items: + - const: firmware + - const: asc + - const: asc-aux + - const: config + + mboxes: + maxItems: 1 + + mbox-names: + const: mbox + + iommus: + maxItems: 1 + + audio: + type: object + $ref: /schemas/sound/apple,t6041-aop-audio.yaml# + description: The audio service. + + als: + type: object + description: The ambient light sensor service. + additionalProperties: false + + properties: + compatible: + items: + - const: apple,t6041-aop-als + - const: apple,aop-als + + required: + - compatible + + las: + type: object + description: The lid angle sensor service. + additionalProperties: false + + properties: + compatible: + items: + - const: apple,t6041-aop-las + - const: apple,aop-las + + required: + - compatible + +required: + - compatible + - reg + - mboxes + - mbox-names + - iommus + +additionalProperties: false + +examples: + - | + soc { + #address-cells = <2>; + #size-cells = <2>; + + aop@510c10000 { + compatible = "apple,t6041-aop"; + reg = <0x5 0x10c10000 0x0 0x2e0000>, + <0x5 0x10600000 0x0 0x88000>, + <0x5 0x10050000 0x0 0x4000>, + <0x5 0x08328000 0x0 0x24>; + reg-names = "firmware", "asc", "asc-aux", "config"; + mboxes = <&aop_mbox>; + mbox-names = "mbox"; + iommus = <&aop_dart 0>; + + als { + compatible = "apple,t6041-aop-als", "apple,aop-als"; + }; + + las { + compatible = "apple,t6041-aop-las", "apple,aop-las"; + }; + }; + }; diff --git a/Documentation/devicetree/bindings/sound/apple,macaudio.yaml b/Documentation/devicetree/bindings/sound/apple,macaudio.yaml index 8e986cca62594..a80f59e315196 100644 --- a/Documentation/devicetree/bindings/sound/apple,macaudio.yaml +++ b/Documentation/devicetree/bindings/sound/apple,macaudio.yaml @@ -20,6 +20,8 @@ properties: - apple,j274-macaudio - apple,j293-macaudio - apple,j314-macaudio + - apple,j616-macaudio + - apple,j616-macaudio-jack - apple,j700-macaudio - const: apple,macaudio diff --git a/Documentation/devicetree/bindings/sound/apple,t6041-aop-audio.yaml b/Documentation/devicetree/bindings/sound/apple,t6041-aop-audio.yaml new file mode 100644 index 0000000000000..b9660e080a91c --- /dev/null +++ b/Documentation/devicetree/bindings/sound/apple,t6041-aop-audio.yaml @@ -0,0 +1,92 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/sound/apple,t6041-aop-audio.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Apple T6041 AOP audio + +maintainers: + - Jonathan Goyvaerts + +description: | + The Always-On Processor on Apple T6041 SoCs runs the machine's audio + streams in its firmware: the microphone array, the internal speakers with + their voltage and current sense, and the 3.5 mm headset jack in both + directions. This node is a child of the AOP node and names the ADMAC + channels the host moves each stream's data over while the firmware runs + its serializer. The parent AOP binding describes the firmware transport. + + The headset jack's channels are optional: a node without them has no jack + streams. + +allOf: + - $ref: dai-common.yaml# + +properties: + compatible: + items: + - const: apple,t6041-aop-audio + - const: apple,t6030-aop-audio + + dmas: + minItems: 3 + items: + - description: Microphone array input + - description: Internal speaker output + - description: Speaker voltage and current sense input + - description: Headset jack output + - description: Headset microphone input + + dma-names: + minItems: 3 + items: + - const: dma + - const: spkr + - const: sense + - const: cout + - const: cin + + "#sound-dai-cells": + const: 1 + description: | + AOP audio DAI identifier: + 0, 1, 2 - the speaker card's front-ends: unused, the speakers and + their sense + 3 - the internal speaker back-end + 4 - the headset jack back-end + + apple,chassis-name: + $ref: /schemas/types.yaml#/definitions/string + description: + Chassis identifier used to name the microphone sound card. + + apple,machine-kind: + $ref: /schemas/types.yaml#/definitions/string + description: + Human-readable machine name used in the microphone sound card's + description. + +required: + - compatible + - dmas + - dma-names + - "#sound-dai-cells" + - apple,chassis-name + - apple,machine-kind + +unevaluatedProperties: false + +examples: + - | + aop { + audio { + compatible = "apple,t6041-aop-audio", "apple,t6030-aop-audio"; + apple,chassis-name = "J616"; + apple,machine-kind = "MacBook Pro"; + dmas = <&admac0 1>, <&admac1 0>, <&admac1 1>, + <&admac1 4>, <&admac1 5>; + dma-names = "dma", "spkr", "sense", "cout", "cin"; + #sound-dai-cells = <1>; + }; + }; diff --git a/Documentation/devicetree/bindings/sound/ti,tas27xx.yaml b/Documentation/devicetree/bindings/sound/ti,tas27xx.yaml index fcaae848e78a1..f48d3f37501ae 100644 --- a/Documentation/devicetree/bindings/sound/ti,tas27xx.yaml +++ b/Documentation/devicetree/bindings/sound/ti,tas27xx.yaml @@ -18,6 +18,12 @@ description: | allOf: - $ref: dai-common.yaml# + - if: + required: + - apple,sdz-group-fail-closed + then: + properties: + shutdown-gpios: false properties: compatible: @@ -42,6 +48,20 @@ properties: interrupts: maxItems: 1 + SDZ-supply: + description: + Regulator that drives the SDZ (shutdown) pin, an alternative to + shutdown-gpios for a line that several amplifiers share. + + apple,sdz-group-fail-closed: + type: boolean + description: + The amplifier belongs to the group of amplifiers that share its + SDZ-supply. An over-temperature or over-current fault on any member + turns the shared supply off, which shuts every amplifier of the group + down, and no member enables it again until the driver is reloaded. + A member needs its fault interrupt. + ti,imon-slot-no: $ref: /schemas/types.yaml#/definitions/uint32 description: TDM TX current sense time slot. @@ -59,6 +79,10 @@ required: - compatible - reg +dependencies: + apple,sdz-group-fail-closed: + - SDZ-supply + unevaluatedProperties: false examples: diff --git a/MAINTAINERS b/MAINTAINERS index 3ee360eff5339..0a50e30dd7f82 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2101,8 +2101,10 @@ F: Documentation/devicetree/bindings/rtc/apple,j700-pmu-rtc.yaml F: Documentation/devicetree/bindings/serial/apple,dockchannel-uart.yaml F: Documentation/devicetree/bindings/soc/apple/apple,dockchannel.yaml F: Documentation/devicetree/bindings/soc/apple/apple,rtk-helper-asc4.yaml +F: Documentation/devicetree/bindings/soc/apple/apple,t6041-aop.yaml F: Documentation/devicetree/bindings/soc/apple/apple,t8140-aop.yaml F: Documentation/devicetree/bindings/soc/apple/apple,t8140-mcc.yaml +F: Documentation/devicetree/bindings/sound/apple,t6041-aop-audio.yaml F: Documentation/devicetree/bindings/sound/apple,t8140-aop-audio.yaml F: Documentation/devicetree/bindings/usb/vialab,vl122.yaml F: Documentation/gpu/apple-dcp-backlight.rst diff --git a/drivers/iio/common/aop_sensors/aop_als.rs b/drivers/iio/common/aop_sensors/aop_als.rs index af2cbfbc954e0..3e262a003c9ea 100644 --- a/drivers/iio/common/aop_sensors/aop_als.rs +++ b/drivers/iio/common/aop_sensors/aop_als.rs @@ -72,8 +72,18 @@ fn enable_als( offset: usize, ) -> Result<()> { if offset != LUX_OFFSET_CT817 { - let fw = Firmware::request(c_str!("apple/aop-als-cal.bin"), dev.as_ref())?; - set_als_property(aop, svc, ALS_PROP_CALIBRATION, fw.data())?; + // The calibration blob comes from macOS and is not installed on every + // system. Without it the sensor still reports, uncalibrated. + match Firmware::request(c_str!("apple/aop-als-cal.bin"), dev.as_ref()) { + Ok(fw) => { + set_als_property(aop, svc, ALS_PROP_CALIBRATION, fw.data())?; + } + Err(e) => dev_warn!( + dev.as_ref(), + "no calibration ({:?}); readings are uncalibrated\n", + e + ), + } set_als_property(aop, svc, ALS_PROP_INTERVAL, &ALS_INTERVAL_US.to_le_bytes())?; return Ok(()); } diff --git a/drivers/soc/apple/aop.rs b/drivers/soc/apple/aop.rs index 331669f5e96c1..4bcf43a7060e3 100644 --- a/drivers/soc/apple/aop.rs +++ b/drivers/soc/apple/aop.rs @@ -91,6 +91,17 @@ const AOP_MMIO_SIZE: usize = 0x1e0000; const ASC_MMIO_SIZE: usize = 0x4000; const BOOTARGS_OFFSET: usize = 0x22c; const BOOTARGS_SIZE: usize = 0x230; +/// On the T6040 the bootloader reserves the firmware's external code segment +/// (`__ETEXT`, from the ADT) as `/reserved-memory/aop-etext@`. No +/// mapping of it survives the DART reset, so this driver maps it itself. +const ETEXT_NODE_PREFIX: &CStr = c_str!("aop-etext"); +/// The firmware's log segment (`__OS_LOG`), reserved the same way as +/// `/reserved-memory/aop-oslog@` and used by the firmware at its +/// physical address. Without it the firmware stops within a second on a DART +/// fault storm. +const OSLOG_NODE_PREFIX: &CStr = c_str!("aop-oslog"); +const ETEXT_MAX_SIZE: u64 = 0x10_0000; +const ETEXT_PAGE: u64 = 0x4000; const AFK_ENDPOINT_START: u8 = 0x20; const AFK_ENDPOINT_COUNT: u8 = 0xf; const AFK_OPC_GET_BUF: u64 = 0x89; @@ -1184,6 +1195,18 @@ impl ChildDevice { } } +/// The IOVA mapping of the firmware's external code, owned by this driver +/// (T6040 only). +struct EtextMapping { + domain: *mut bindings::iommu_domain, + iova: u64, + size: usize, +} + +// SAFETY: The domain pointer is only passed to `iommu_unmap()`, which may be +// called from any thread; the domain outlives the device's binding. +unsafe impl Send for EtextMapping {} + #[pin_data] struct AopData { dev: ARef, @@ -1202,6 +1225,9 @@ struct AopData { /// The co-processor was started; only then is there anything to shut /// down and to retain. cpu_started: Atomic, + /// The firmware mappings made by `map_reserved()`. + #[pin] + etext: Mutex>, /// Shut the co-processor down on removal, and if that cannot be /// confirmed, retain everything it may still DMA to. quiesce_on_unbind: bool, @@ -1349,6 +1375,7 @@ impl AopData { removing: Atomic::new(false), transport_closing: Atomic::new(false), cpu_started: Atomic::new(false), + etext <- new_mutex!(KVec::new()), quiesce_on_unbind: cfg.quiesce_on_unbind, required_endpoints: cfg.required_endpoints, setup_queue, @@ -1515,28 +1542,160 @@ impl AopData { ) -> Result<()> { let offset = aop_mmio.read32(BOOTARGS_OFFSET) as usize; let size = aop_mmio.read32(BOOTARGS_SIZE) as usize; + if offset > aop_mmio.size() || size > aop_mmio.size() - offset { + return Err(EINVAL); + } let mut arg_bytes = KVec::::from_elem(0, size, GFP_KERNEL)?; let aop_io_array: Mmio<'_, [u8; AOP_MMIO_SIZE]> = aop_mmio.try_cast()?; let bootargs = io_project!(aop_io_array, [try: offset .. offset+size]); bootargs.copy_to_slice(&mut arg_bytes); let mut idx = 0; while idx < size { + if size - idx < 8 { + return Err(EINVAL); + } let key = u32::from_le_bytes(arg_bytes[idx..idx + 4].try_into().unwrap()); - let size = u32::from_le_bytes(arg_bytes[idx + 4..idx + 8].try_into().unwrap()) as usize; + let len = u32::from_le_bytes(arg_bytes[idx + 4..idx + 8].try_into().unwrap()) as usize; idx += 8; + if len > size - idx { + return Err(EINVAL); + } for (k, v) in patches.iter() { if *k != key { continue; } - arg_bytes[idx..idx + size].copy_from_slice(&v.to_le_bytes()[..size]); + if len > mem::size_of::() { + return Err(EINVAL); + } + arg_bytes[idx..idx + len].copy_from_slice(&v.to_le_bytes()[..len]); break; } - idx += size; + idx += len; } bootargs.copy_from_slice(&arg_bytes); Ok(()) } + /// The physical range of the one `/reserved-memory/@*` node. + fn find_reserved(prefix: &CStr) -> Result<(u64, u64)> { + // SAFETY: The path is NUL-terminated. The node reference is released + // below (OF_DYNAMIC is on since 7.3: pmdomain/apple selects it). + let resv = unsafe { + bindings::of_find_node_opts_by_path( + c_str!("/reserved-memory").as_char_ptr(), + ptr::null_mut(), + ) + }; + if resv.is_null() { + return Err(ENODEV); + } + let mut found: Result> = Ok(None); + let mut child: *mut bindings::device_node = ptr::null_mut(); + loop { + // SAFETY: `resv` is a valid node; `child` is null or its previous + // child, whose reference this call drops. + child = unsafe { bindings::of_get_next_child(resv, child) }; + if child.is_null() { + break; + } + // SAFETY: `child` is a valid node and the prefix is NUL-terminated. + if !unsafe { bindings::of_node_name_prefix(child, prefix.as_char_ptr()) } { + continue; + } + let mut res = mem::MaybeUninit::::zeroed(); + // SAFETY: `res` is writable storage for one resource. + if unsafe { bindings::of_address_to_resource(child, 0, res.as_mut_ptr()) } != 0 { + found = Err(EINVAL); + break; + } + // SAFETY: of_address_to_resource() succeeded and filled it in. + let res = unsafe { res.assume_init() }; + if !matches!(found, Ok(None)) || res.end < res.start { + found = Err(EINVAL); + break; + } + found = Ok(Some((res.start as u64, (res.end - res.start) as u64 + 1))); + } + // SAFETY: Both are null or node references taken above; of_node_put() + // accepts null. + unsafe { + bindings::of_node_put(child); + bindings::of_node_put(resv); + } + found?.ok_or(ENOENT) + } + + /// Maps the `/reserved-memory/@*` segment into this device's DART + /// domain at `iova`, or at its physical address when `iova` is `None`. + /// Nothing is mapped unless every check passes. + fn map_reserved(&self, dev: &device::Device, prefix: &CStr, iova: Option) -> Result<()> { + let (phys, size) = Self::find_reserved(prefix)?; + let iova = iova.unwrap_or(phys); + if phys % ETEXT_PAGE != 0 + || size == 0 + || size % ETEXT_PAGE != 0 + || size > ETEXT_MAX_SIZE + || iova % ETEXT_PAGE != 0 + || iova.checked_add(size).is_none() + || phys.checked_add(size).is_none() + { + return Err(EINVAL); + } + // SAFETY: `dev` is a live device being probed. + let domain = unsafe { bindings::iommu_get_domain_for_dev(dev.as_raw()) }; + if domain.is_null() { + return Err(ENODEV); + } + // SAFETY: `domain` is the non-null domain returned above. + let ty = unsafe { (*domain).type_ }; + if ty == bindings::IOMMU_DOMAIN_IDENTITY || ty == bindings::IOMMU_DOMAIN_BLOCKED { + return Err(ENODEV); + } + let size_us = usize::try_from(size).map_err(|_| EOVERFLOW)?; + // SAFETY: The range was checked above; the DART domain validates the + // IOVA itself. + let ret = unsafe { + bindings::iommu_map( + domain, + iova as _, + phys as _, + size_us, + (bindings::IOMMU_READ | bindings::IOMMU_WRITE) as _, + bindings::GFP_KERNEL, + ) + }; + if ret != 0 { + return Err(Error::from_errno(ret)); + } + // SAFETY: as above. + let back = unsafe { bindings::iommu_iova_to_phys(domain, iova as _) } as u64; + if back != phys { + // SAFETY: The range was mapped just above. + unsafe { bindings::iommu_unmap(domain, iova as _, size_us) }; + return Err(EIO); + } + let map = EtextMapping { + domain, + iova, + size: size_us, + }; + if self.etext.lock().push(map, GFP_KERNEL).is_err() { + // SAFETY: The range was mapped just above. + unsafe { bindings::iommu_unmap(domain, iova as _, size_us) }; + return Err(ENOMEM); + } + Ok(()) + } + + /// Undoes `map_reserved()`; only once the firmware no longer runs. + fn unmap_etext(&self) { + let mut maps = self.etext.lock(); + while let Some(map) = maps.pop() { + // SAFETY: The mapping was created by map_reserved() on this domain. + unsafe { bindings::iommu_unmap(map.domain, map.iova as _, map.size) }; + } + } + /// Leaks every allocation the firmware may still access. Called when the /// shutdown could not be confirmed; the memory is lost until reboot. fn retain_dma_buffers(&self) { @@ -1796,6 +1955,9 @@ impl AOP for AopData { ); return; } + // The firmware is stopped or was never started: it no longer fetches + // its external code. + self.unmap_etext(); let arena = self.setup.lock().arena.take(); drop(arena); // The ring is DMA of the audio child; free it while the child is @@ -2251,6 +2413,12 @@ struct AopHwConfig { quiesce_on_unbind: bool, /// The endpoints that have to start; empty means all advertised ones. required_endpoints: &'static [u8], + /// Map the firmware's external code at `ec0p` and its `__OS_LOG` at its + /// physical address before starting it: no bootloader mapping survives + /// the DART reset (T6040). + map_etext: bool, + /// Halt the co-processor again when its start fails (T6040). + halt_on_failed_start: bool, } const HW_CFG_T8103: AopHwConfig = AopHwConfig { @@ -2262,6 +2430,8 @@ const HW_CFG_T8103: AopHwConfig = AopHwConfig { setup_port: false, quiesce_on_unbind: false, required_endpoints: &[], + map_etext: false, + halt_on_failed_start: false, }; const HW_CFG_T8112: AopHwConfig = AopHwConfig { ec0p: 0x020000, @@ -2272,6 +2442,8 @@ const HW_CFG_T8112: AopHwConfig = AopHwConfig { setup_port: false, quiesce_on_unbind: false, required_endpoints: &[], + map_etext: false, + halt_on_failed_start: false, }; const HW_CFG_T6000: AopHwConfig = AopHwConfig { ec0p: 0x020000, @@ -2282,6 +2454,8 @@ const HW_CFG_T6000: AopHwConfig = AopHwConfig { setup_port: false, quiesce_on_unbind: false, required_endpoints: &[], + map_etext: false, + halt_on_failed_start: false, }; const HW_CFG_T6020: AopHwConfig = AopHwConfig { ec0p: 0x0100_00000000, @@ -2292,6 +2466,24 @@ const HW_CFG_T6020: AopHwConfig = AopHwConfig { setup_port: false, quiesce_on_unbind: false, required_endpoints: &[], + map_etext: false, + halt_on_failed_start: false, +}; +/// T6040 (J616s): macOS writes the same four boot arguments as for the +/// T6020. The bootloader's mapping of the external code does not survive the +/// DART reset, so it is mapped here; the RTKit firmware is shut down on +/// unbind. +const HW_CFG_T6040: AopHwConfig = AopHwConfig { + ec0p: 0x0100_00000000, + aopt: 0, + alig: 64, + patch_bootargs: true, + epic_v4: false, + setup_port: false, + quiesce_on_unbind: true, + required_endpoints: &[], + map_etext: true, + halt_on_failed_start: true, }; /// T8140: the firmware is started with the boot arguments the bootloader /// left, boots through the setup port and speaks EPIC version 4. Of the @@ -2307,6 +2499,8 @@ const HW_CFG_T8140: AopHwConfig = AopHwConfig { setup_port: true, quiesce_on_unbind: true, required_endpoints: &[0x20, 0x21, 0x22, 0x23, 0x2b], + map_etext: false, + halt_on_failed_start: false, }; kernel::of_device_table!( @@ -2318,6 +2512,7 @@ kernel::of_device_table!( (of::DeviceId::new(c_str!("apple,t6000-aop")), &HW_CFG_T6000), (of::DeviceId::new(c_str!("apple,t6020-aop")), &HW_CFG_T6020), (of::DeviceId::new(c_str!("apple,t8140-aop")), &HW_CFG_T8140), + (of::DeviceId::new(c_str!("apple,t6041-aop")), &HW_CFG_T6040), ] ); @@ -2351,6 +2546,18 @@ impl platform::Driver for AopDriver { // registering; the same teardown as unbind's cleans that up. let probe_guard = ScopeGuard::new_with_data(data.clone(), |data| data.remove()); let aop_mmio = aop_mmio.as_view(); + if cfg.map_etext { + // Before anything is written to the AOP: no mapping, no start. + for (prefix, iova) in [ + (ETEXT_NODE_PREFIX, Some(cfg.ec0p)), + (OSLOG_NODE_PREFIX, None), + ] { + if let Err(e) = data.map_reserved(pdev.as_ref(), prefix, iova) { + dev_err!(pdev.as_ref(), "Failed to map {:?} ({:?})", prefix, e); + return Err(e); + } + } + } if cfg.patch_bootargs { data.patch_bootargs( &aop_mmio, @@ -2369,20 +2576,37 @@ impl platform::Driver for AopDriver { } asc_mmio.update(ASC_CPU_CONTROL, |r| r.with_const_cpu_run::<1>()); data.cpu_started.store(true, Release); - if cfg.setup_port { + let started = if cfg.setup_port { // The RTKit handshake up to AP power on comes first, then the // setup port's own power state and endpoint buffers, and only // then the AFK endpoints. - data.wake()?; - data.setup_finish_boot()?; - // Before any client reaches the ALS service: a failure here costs - // the sensor, not the AOP. - if let Err(e) = data.setup_als_calibration() { - dev_warn!(pdev.as_ref(), "ALS calibration failed ({:?})", e); - } - data.start_afk()?; + data.wake() + .and_then(|()| data.setup_finish_boot()) + .and_then(|()| { + // Before any client reaches the ALS service: a failure + // here costs the sensor, not the AOP. + if let Err(e) = data.setup_als_calibration() { + dev_warn!(pdev.as_ref(), "ALS calibration failed ({:?})", e); + } + data.start_afk() + }) } else { - data.start()?; + data.start() + }; + if let Err(e) = started { + if cfg.halt_on_failed_start { + // The same teardown as the probe guard's (which then does + // nothing), then back to the halted state the bootloader + // left. + data.remove(); + asc_mmio.update(ASC_CPU_CONTROL, |r| r.with_const_cpu_run::<0>()); + dev_err!( + pdev.as_ref(), + "Failed to start the AOP ({:?}), halted it again", + e + ); + } + return Err(e); } probe_guard.dismiss(); let data = data as Arc; diff --git a/sound/soc/apple/aop_audio.rs b/sound/soc/apple/aop_audio.rs index f57c6842e6e26..984cb763f70f8 100644 --- a/sound/soc/apple/aop_audio.rs +++ b/sound/soc/apple/aop_audio.rs @@ -37,11 +37,13 @@ use kernel::{ }, Arc, // }, - types::ForeignOwnable, // + types::{ForeignOwnable, ScopeGuard}, // }; use pin_init::Zeroable; +mod aop_spkr; + /// An unaligned u32 type. /// /// This is useful to avoid having to pack firmware structures entirely, since that is incompatible @@ -326,6 +328,16 @@ impl AudioAttachDevice { } } +// macOS IOPAudioLPMicDevice::sendChannelControlInfoToIOP sets node property 300 with the three +// masks of the ADT node /product/audio (supportedChannels, enabledChannels, historyChannels). +#[repr(C, packed(4))] +#[derive(Clone, Copy, Default)] +struct LpaiChannelControl { + supported: u32, + enabled: u32, + history: u32, +} + #[repr(C, packed(4))] #[derive(Clone, Copy, Default)] struct LpaiChannelConfig { @@ -457,6 +469,10 @@ static_assert!( static_assert!( mem::size_of::>() == 52 + mem::size_of::() ); +static_assert!( + mem::size_of::>() + == 52 + mem::size_of::() +); impl AudioSetDeviceProp { fn new(dev_id: u32, modifier: u32, data: T) -> AudioSetDeviceProp { @@ -473,7 +489,7 @@ impl AudioSetDeviceProp { } } -unsafe impl Zeroable for AudioSetDeviceProp {} +unsafe impl Zeroable for AudioSetDeviceProp {} impl AudioSetDeviceProp { fn try_init( @@ -502,6 +518,10 @@ impl AudioSetDeviceProp { struct SndSocAopHwConfig { bytes_per_sample: u32, hf_decimator: bool, + // The T6040 firmware follows macOS's IOPAudioLPMicDevice flow. LPAI property 301 is the + // read-only stream description; the channel masks are property 300 (12 bytes). A refusal of + // any of the configuration steps is logged and not fatal, so the next step's answer is seen. + t6040_flow: bool, } struct SndSocAopData { @@ -645,6 +665,21 @@ impl SndSocAopData { Ok(()) } } + fn set_lpai_channel_control(&self) -> Result<()> { + // ADT /product/audio on J616s: supportedChannels = enabledChannels = historyChannels = 7. + let ctl = LpaiChannelControl { + supported: 7, + enabled: 7, + history: 7, + }; + let msg = AudioSetDeviceProp::new(AUDIO_DEV_LPAI, 300, ctl); + let ret = self.epic_wrapped_call(&msg)?; + dev_dbg!(self.dev, "lpai channel control (prop 300): {:#x}\n", ret); + if ret != 0 { + return Err(EIO); + } + Ok(()) + } fn audio_attach_device(&self, dev_id: u32) -> Result<()> { let msg = AudioAttachDevice::new(dev_id); let ret = self.epic_wrapped_call(&msg)?; @@ -701,11 +736,19 @@ impl SndSocAopData { } } -#[repr(transparent)] -struct SndSocAopDriver(*mut bindings::snd_card); +struct SndSocAopDriver { + card: *mut bindings::snd_card, + /// The J616s speaker and jack ASoC components (T6040, where the node has a "spkr" DMA channel) + spkr: Option, +} fn copy_str(target: &mut [u8], source: &[u8]) { - target[..source.len()].copy_from_slice(source) + target.fill(0); + let len = source + .len() + .saturating_sub(1) + .min(target.len().saturating_sub(1)); + target[..len].copy_from_slice(&source[..len]); } unsafe fn dmaengine_slave_config( @@ -764,6 +807,9 @@ unsafe extern "C" fn aop_pcm_open(substream: *mut bindings::snd_pcm_substream) - Err(e) => return e.to_errno(), }; + // Until the PCM helper accepts ownership, every error must release this channel. + let channel = ScopeGuard::new(|| unsafe { bindings::dma_release_channel(dma_chan) }); + if unsafe { (*substream).dma_buffer.dev.type_ == bindings::SNDRV_DMA_TYPE_UNKNOWN as _ } { let ret = unsafe { bindings::snd_pcm_set_managed_buffer( @@ -776,9 +822,6 @@ unsafe extern "C" fn aop_pcm_open(substream: *mut bindings::snd_pcm_substream) - }; if ret < 0 { dev_err!(data.dev, "Unable to allocate dma buffers"); - unsafe { - bindings::dma_release_channel(dma_chan); - } return ret; } } @@ -796,14 +839,20 @@ unsafe extern "C" fn aop_pcm_open(substream: *mut bindings::snd_pcm_substream) - dev_err!(data.dev, "Unable to refine hwparams"); return ret; } + unsafe { (*(*substream).runtime).hw = hwparams }; + let ret = unsafe { bindings::snd_dmaengine_pcm_open(substream, dma_chan) }; + if ret < 0 { + return ret; + } + channel.dismiss(); if let Err(e) = data.set_audio_power(POWER_STATE_ON, 1) { dev_err!(data.dev, "Unable to power mic on"); + // The ALSA core will not call close after a failed open. Unwind the + // accepted host channel even when firmware's power result is uncertain. + unsafe { bindings::snd_dmaengine_pcm_close_release_chan(substream) }; return e.to_errno(); } - unsafe { - (*(*substream).runtime).hw = hwparams; - bindings::snd_dmaengine_pcm_open(substream, dma_chan) - } + 0 } unsafe extern "C" fn aop_pcm_prepare(_: *mut bindings::snd_pcm_substream) -> i32 { @@ -812,15 +861,17 @@ unsafe extern "C" fn aop_pcm_prepare(_: *mut bindings::snd_pcm_substream) -> i32 unsafe extern "C" fn aop_pcm_close(substream: *mut bindings::snd_pcm_substream) -> i32 { let data = unsafe { Arc::::borrow((*substream).private_data.cast()) }; - if let Err(e) = data.set_audio_power(POWER_STATE_IDLE, 1) { - dev_err!(data.dev, "Unable to power mic off"); - return e.to_errno(); - } + // ALSA discards close's error and detaches the runtime. Its host DMA + // callbacks must be drained before a firmware RPC can fail or time out. let ret = unsafe { bindings::snd_dmaengine_pcm_close_release_chan(substream) }; if ret != 0 { dev_err!(data.dev, "Unable to close channel"); return ret; } + if let Err(e) = data.set_audio_power(POWER_STATE_IDLE, 1) { + dev_err!(data.dev, "Unable to power mic off"); + return e.to_errno(); + } if let Err(e) = data.set_audio_power(POWER_STATE_OFF, 0) { dev_err!(data.dev, "Unable to enter 'idle' power state"); return e.to_errno(); @@ -844,7 +895,7 @@ impl SndSocAopDriver { ioctl: None, hw_params: Some(aop_hw_params), hw_free: None, - sync_stop: None, + sync_stop: Some(bindings::snd_dmaengine_pcm_sync_stop), get_time_info: None, fill_silence: None, copy: None, @@ -853,7 +904,10 @@ impl SndSocAopDriver { ack: None, }; fn new(data: Arc) -> Result { - let mut this = SndSocAopDriver(ptr::null_mut()); + let mut this = SndSocAopDriver { + card: ptr::null_mut(), + spkr: None, + }; let ret = unsafe { bindings::snd_card_new( data.dev.as_raw(), @@ -861,7 +915,7 @@ impl SndSocAopDriver { ptr::null(), kernel::module::this_module::().as_ptr(), 0, - &mut this.0, + &mut this.card, ) }; if ret < 0 { @@ -877,15 +931,15 @@ impl SndSocAopDriver { .property_read::(c_str!("apple,machine-kind")) .required_by(&data.dev)?; unsafe { - copy_str(&mut (*this.0).driver, c"aop_audio".to_bytes_with_nul()); + copy_str(&mut (*this.card).driver, c"aop_audio".to_bytes_with_nul()); } let id_str = CString::try_from_fmt(fmt!("Apple{}HPAI", *chassis))?; unsafe { - copy_str(&mut (*this.0).id, id_str.to_bytes_with_nul()); + copy_str(&mut (*this.card).id, id_str.to_bytes_with_nul()); } let shortname = CString::try_from_fmt(fmt!("{} {} HPAI", *machine_kind, *chassis))?; unsafe { - copy_str(&mut (*this.0).shortname, shortname.to_bytes_with_nul()); + copy_str(&mut (*this.card).shortname, shortname.to_bytes_with_nul()); } let longname = CString::try_from_fmt(fmt!( "{} {} High-Power Audio Interface", @@ -893,12 +947,12 @@ impl SndSocAopDriver { *chassis ))?; unsafe { - copy_str(&mut (*this.0).longname, longname.to_bytes_with_nul()); + copy_str(&mut (*this.card).longname, longname.to_bytes_with_nul()); } let mut pcm = ptr::null_mut(); let ret = - unsafe { bindings::snd_pcm_new(this.0, longname.as_ptr() as _, 0, 0, 1, &mut pcm) }; + unsafe { bindings::snd_pcm_new(this.card, longname.as_ptr() as _, 0, 0, 1, &mut pcm) }; if ret < 0 { dev_err!(data.dev, "Unable to allocate PCM device"); return Err(Error::from_errno(ret)); @@ -919,7 +973,7 @@ impl SndSocAopDriver { copy_str(&mut (*pcm).name, c"aop_audio".to_bytes_with_nul()); } - let ret = unsafe { bindings::snd_card_register(this.0) }; + let ret = unsafe { bindings::snd_card_register(this.card) }; if ret < 0 { dev_err!(data.dev, "Unable to register sound card"); return Err(Error::from_errno(ret)); @@ -930,9 +984,11 @@ impl SndSocAopDriver { impl Drop for SndSocAopDriver { fn drop(&mut self) { - if self.0 != ptr::null_mut() { + // The speaker component (and any card bound to its DAIs) goes before the microphone card. + self.spkr.take(); + if self.card != ptr::null_mut() { unsafe { - bindings::snd_card_free(self.0); + bindings::snd_card_free(self.card); } } } @@ -944,11 +1000,19 @@ unsafe impl Sync for SndSocAopDriver {} const HW_CFG_T8013: SndSocAopHwConfig = SndSocAopHwConfig { bytes_per_sample: 2, hf_decimator: false, + t6040_flow: false, }; const HW_CFG_T6030: SndSocAopHwConfig = SndSocAopHwConfig { bytes_per_sample: 3, hf_decimator: true, + t6040_flow: false, +}; + +const HW_CFG_T6040: SndSocAopHwConfig = SndSocAopHwConfig { + bytes_per_sample: 3, + hf_decimator: true, + t6040_flow: true, }; kernel::of_device_table!( @@ -960,6 +1024,10 @@ kernel::of_device_table!( of::DeviceId::new(c_str!("apple,t6030-aop-audio")), &HW_CFG_T6030 ), + ( + of::DeviceId::new(c_str!("apple,t6041-aop-audio")), + &HW_CFG_T6040 + ), ] ); @@ -1003,6 +1071,30 @@ impl platform::Driver for SndSocAopDriver { if info.hf_decimator { data.audio_attach_device(from_fourcc(b"lilb"))?; } + if info.t6040_flow { + // Property 301 is the read-only stream description on this firmware (writing it + // returns 0xE00002CF, kIOReturnNotWritable). Follow macOS: channel masks are property + // 300. A refusal is reported and does not stop the probe. + if data.set_lpai_channel_control().is_err() { + dev_warn!(data.dev, "lpai channel control refused, continuing\n"); + } + if data.set_pdm_config().is_err() { + dev_warn!(data.dev, "pdm config refused, continuing\n"); + } + if data.set_decimator_config_hf().is_err() { + dev_warn!(data.dev, "decimator config refused, continuing\n"); + } + let mut this = Self::new(data.clone())?; + // The speaker stream, where the node has its DMA channel. + if data + .fwnode + .property_match_string(c_str!("dma-names"), c_str!("spkr")) + .is_ok() + { + this.spkr = Some(aop_spkr::SpeakerComponent::register(data)?); + } + return Ok(this); + } data.set_lpai_channel_cfg()?; data.set_pdm_config()?; if info.hf_decimator { diff --git a/sound/soc/apple/aop_spkr.rs b/sound/soc/apple/aop_spkr.rs new file mode 100644 index 0000000000000..0f0de5b0afaa7 --- /dev/null +++ b/sound/soc/apple/aop_spkr.rs @@ -0,0 +1,1668 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT + +//! J616s (T6040) internal speakers on the AOP `spkr` stream, and the headphone jack on its `cout` +//! and `cin ` streams. +//! +//! The AOP firmware runs MCA cluster 0 for this stream (`spS `, serializer `ms00`, `mck0` from the +//! 12 MHz crystal): a 250-bit frame at 48 kHz with six 32-bit playback slots (24 valid bits) and +//! twelve 16-bit V/I sense slots coming back on the same serializer. The base-ns ADMAC moves both: +//! channel 0 (TX0) the playback, channel 1 (RX0) the sense, through AOP DART +//! stream 8. The AP side only attaches the +//! service, requests its power states and runs the DMA; the six SN012776 amps are their own codec +//! driver's, linked to this component by macaudio's J616 variant. +//! +//! This is an ASoC component next to the microphone card of the parent module: DPCM front-end PCMs +//! on the ADMAC channels (`j616-pcm-0..2`, the names macaudio's front-end links bind to) and one +//! back-end DAI, `spkr`, for the stream. The DAI table order is ABI: `sound-dai = <&aop_audio 3>` +//! is `spkr`. +//! +//! Power, from aurora's J616s control test (`spkr` attached, `pw1 `, `pwrd` with output flag 2, +//! `pw1 `, `idle`, each confirmed through property 200): `pw1 ` powers the audio fabric, the ADMAC +//! included, and `pwrd` runs the serializer clocks. The ADMAC registers only answer with the fabric +//! powered, so the back-end startup raises `pw1 ` before a front-end requests its channel (DPCM +//! starts the back-end first), and every front-end holds its own reference until it has released +//! the channel (DPCM shuts the back-end down before it closes the front-end). `pwrd` is requested +//! in the back-end prepare, before the amps unmute, and dropped in hw_free, after they muted: the +//! clocks run whenever an amp may be active. +//! +//! Alignment. Neither slot stream carries a frame marker: a channel lines up with its slots only +//! when its DMA meets a serializer that starts afresh (Asahi's MCA driver resets the serializer on +//! every trigger for this reason; here the firmware owns it). So every playback prepare takes a +//! running serializer down to `pw1 ` and back to `pwrd` (the playback DMA is stopped then, and the +//! playback channel starts on the fresh serializer, the order every playback start has always +//! had), and the sense is not a DMA channel of its front-end: the driver owns the sense channel, +//! arms it on the stopped serializer before every `pwrd`, and the sense front-end copies whole +//! frames out of that ring. A sense front-end that starts while the speakers play attaches at +//! the next whole frame; it never starts a channel mid-stream, which slipped the V/I pairs. +//! System sleep takes the serializer down with the streams suspended and brings it back, sense +//! ring first, before ASoC unmutes the amps. +//! +//! The 3.5 mm jack is two more services of the same firmware: `cout` (MCA cluster 2, serializer +//! `ms02`, `mck2` from `nco3` at 3.072 MHz: a 64-bit frame at 48 kHz, two 32-bit playback slots, +//! 24 valid bits) on base-ns ADMAC channel 4 (TX2), and `cin ` (the headset microphone, one slot +//! back on the same serializer) on channel 5 (RX2). The CS42L84 codec on i2c2 is its own driver's. +//! The firmware arbitrates the MCA and the audio power states between the speaker and jack streams, +//! so the two never program shared registers behind each other's back. Each service has its own +//! attachment and power states: `pw1 ` from back-end startup, `pwrd` (output flag 2 for `cout`, +//! input flag 1 for `cin `, as the microphones use) from prepare. System sleep takes both down +//! with the streams suspended and brings back what ran, before ASoC unmutes the codec. +//! +//! The jack is a second ASoC component on the same device, for a sound card of its own (macaudio's +//! J616 jack variant): front-ends `j616-jack-pcm-0` (the jack) and `j616-jack-pcm-1` (routes only) +//! and the `jack` back-end, `sound-dai = <&aop_audio 4>`. A component binds to one card only, and +//! a card binds only once every codec of it has probed: on its own card, a jack codec that never +//! answers costs the jack, not the speakers. The speaker component is the same with or without the +//! jack. The jack component exists only when the audio node lists the "cout" and "cin" DMA +//! channels. +//! +//! The structure (component context with the descriptor as its first field, DAI tables, dmaengine +//! front-ends) follows aurora's t8140_aop_audio.rs (Copyright (C) 2026 The Aurora Silicon +//! Contributors). + +use core::{ + ffi::c_void, + mem, ptr, slice, + sync::atomic::{fence, AtomicBool, AtomicI32, AtomicPtr, AtomicU64, AtomicUsize, Ordering}, +}; + +use kernel::{ + bindings, c_str, device, + error::from_err_ptr, + new_mutex, + prelude::*, + soc::apple::aop::from_fourcc, + sync::{aref::ARef, Arc, Mutex}, + types::Opaque, +}; + +use super::{ + AudioAttachDevice, AudioSetDeviceProp, PowerSetting, SndSocAopData, EPIC_SUBTYPE_WRAPPED_CALL, +}; + +const CALLTYPE_AUDIO_GET_PROP: u32 = 0xc3000004; +const AUDIO_DEV_SPKR: u32 = from_fourcc(b"spkr"); +/// The jack's output and headset microphone (ADT iop-audio-controller identifiers 'tuoc', ' nic'). +const AUDIO_DEV_COUT: u32 = from_fourcc(b"cout"); +const AUDIO_DEV_CIN: u32 = from_fourcc(b"cin "); +const PROP_POWER_STATE: u32 = 200; +const PROP_POWER_REQUEST: u32 = 202; +const STATE_IDLE: u32 = from_fourcc(b"idle"); +const STATE_PW1: u32 = from_fourcc(b"pw1 "); +const STATE_PWRD: u32 = from_fourcc(b"pwrd"); +/// The power request parameter for `pwrd` on an output stream (aurora: "output flag 2"). +const PWRD_OUTPUT: u32 = 2; +/// The same for an input stream (the microphones' `pwrd` request uses 1). +const PWRD_INPUT: u32 = 1; +/// kIOReturnBusy: the firmware keeps an attachment for its lifetime, so a reload sees this. +const AUDIO_RET_BUSY: u32 = 0xe00002d5; + +/// Property read (0xc3000004): the same header as the property write, with a trailing 1. +#[repr(C, packed(4))] +#[derive(Clone, Copy, Default)] +struct AudioGetDeviceProp { + _zero0: u32, + unk0: u32, + calltype: u32, + _zero1: [u8; 16], + _pad0: u32, + len: u64, + dev_id: u32, + modifier: u32, + unk6: u32, +} + +impl AudioGetDeviceProp { + fn new(dev_id: u32, modifier: u32) -> Self { + Self { + unk0: 0xffffffff, + calltype: CALLTYPE_AUDIO_GET_PROP, + len: 0x30, + dev_id, + modifier, + unk6: 1, + ..Default::default() + } + } +} + +/// A firmware audio service: attachment, power-state users and whether the serializer runs. +struct ServiceState { + attached: bool, + /// back-end directions started plus front-ends holding a DMA channel + users: u32, + running: bool, + /// The serializer ran when the system suspended: resume brings it back. + resume_run: bool, + sequence: u32, +} + +#[pin_data] +pub(crate) struct Service { + aop: Arc, + dev_id: u32, + /// The service name in log messages. + tag: &'static str, + /// The `pwrd` request parameter: output or input. + pwrd_param: u32, + /// The speaker service's sense ring; `None` for the jack's services. + ring: Option, + #[pin] + state: Mutex, +} + +impl Service { + fn new( + aop: Arc, + dev_id: u32, + tag: &'static str, + pwrd_param: u32, + ring: bool, + ) -> Result> { + Arc::pin_init( + pin_init!(Service { + aop, + dev_id, + tag, + pwrd_param, + ring: ring.then(SenseRing::new), + state <- new_mutex!(ServiceState { + attached: false, + users: 0, + running: false, + resume_run: false, + sequence: 0, + }), + }), + GFP_KERNEL, + ) + } + + fn call_ret(&self, msg: &T, ret_len: usize) -> Result> { + // SAFETY: T is a plain packed firmware record. + let bytes = + unsafe { slice::from_raw_parts(ptr::from_ref(msg).cast::(), mem::size_of::()) }; + let (retcode, ret) = self.aop.adata.epic_call_ret( + &self.aop.service, + EPIC_SUBTYPE_WRAPPED_CALL, + bytes, + ret_len, + )?; + if retcode != 0 { + dev_err!( + self.aop.dev, + "{} property read failed, return code {:#x}\n", + self.tag, + retcode + ); + return Err(EIO); + } + Ok(ret) + } + + /// Property 200: the service's current power state (reply: u32 length, then the FourCC). + fn power_state(&self) -> Result { + let ret = self.call_ret(&AudioGetDeviceProp::new(self.dev_id, PROP_POWER_STATE), 8)?; + if ret.len() < 8 || u32::from_le_bytes([ret[0], ret[1], ret[2], ret[3]]) != 4 { + dev_err!( + self.aop.dev, + "{} malformed power-state reply ({} bytes)\n", + self.tag, + ret.len() + ); + return Err(EIO); + } + Ok(u32::from_le_bytes([ret[4], ret[5], ret[6], ret[7]])) + } + + /// Property 202, then the property 200 readback: a state the firmware did not take fails the + /// stream. + fn set_power(&self, st: &mut ServiceState, state: u32, param: u32) -> Result<()> { + st.sequence = st.sequence.wrapping_add(1); + let msg = AudioSetDeviceProp::new( + self.dev_id, + PROP_POWER_REQUEST, + PowerSetting::new(self.dev_id, st.sequence, state, param), + ); + let ret = self.aop.epic_wrapped_call(&msg)?; + if ret != 0 { + dev_err!( + self.aop.dev, + "{} power request {:#x} rejected: {:#x}\n", + self.tag, + state, + ret + ); + return Err(EIO); + } + if self.power_state()? != state { + return Err(EIO); + } + Ok(()) + } + + /// A user of the service: the first one attaches it (once per firmware lifetime) and raises + /// `pw1 `. + fn get(&self) -> Result<()> { + let mut st = self.state.lock(); + if st.users == 0 { + // A power-down that failed or timed out can leave the firmware in another state than + // ours: note it; pw1 is valid from any. + if st.attached { + match self.power_state() { + Ok(now) if now != STATE_IDLE => { + dev_warn!( + self.aop.dev, + "{} found state {:#x} at startup\n", + self.tag, + now + ) + } + _ => {} + } + } + if !st.attached { + let ret = self + .aop + .epic_wrapped_call(&AudioAttachDevice::new(self.dev_id))?; + if ret != 0 && ret != AUDIO_RET_BUSY { + dev_err!( + self.aop.dev, + "{} attach failed, return code {:#x}\n", + self.tag, + ret + ); + return Err(EIO); + } + st.attached = true; + } + self.set_power(&mut st, STATE_PW1, 0)?; + } + st.users += 1; + Ok(()) + } + + /// The last user takes the service back through `pw1 ` (if it still runs) to `idle`. + fn put(&self) { + let mut st = self.state.lock(); + if st.users == 0 { + dev_warn!(self.aop.dev, "{} unbalanced put\n", self.tag); + return; + } + st.users -= 1; + if st.users != 0 { + return; + } + if st.running { + if let Err(e) = self.set_power(&mut st, STATE_PW1, 0) { + dev_err!( + self.aop.dev, + "{} pw1 at shutdown failed: {:?}\n", + self.tag, + e + ); + } + st.running = false; + } + // The channel goes while the fabric is still powered. + if let Some(ring) = &self.ring { + ring.release(); + } + if let Err(e) = self.set_power(&mut st, STATE_IDLE, 0) { + dev_err!(self.aop.dev, "{} idle failed: {:?}\n", self.tag, e); + } + } + + /// `pwrd`: the firmware starts the serializer clocks (before the amps or the codec unmute). + /// + /// The speaker service starts a fresh serializer every time: a running one goes down to + /// `pw1 ` first (the sense ring sees a gap of the two firmware round trips; the playback DMA + /// is stopped, as prepare only runs on a stopped stream), then the sense ring is armed and + /// `pwrd` starts the serializer on it. The playback channel starts after this, in the + /// trigger, from the FIFO the stopped stream drained into the running serializer. + /// `quiesce` runs before a running serializer stops: amps left unmuted (a re-prepare after a + /// resume or an xrun) must not see their clocks stop. + fn run(&self, quiesce: impl FnOnce()) -> Result<()> { + let mut st = self.state.lock(); + if st.users == 0 { + return Err(EINVAL); + } + let Some(ring) = &self.ring else { + if !st.running { + self.set_power(&mut st, STATE_PWRD, self.pwrd_param)?; + st.running = true; + } + return Ok(()); + }; + if st.running { + quiesce(); + self.set_power(&mut st, STATE_PW1, 0)?; + st.running = false; + } + self.start_with_ring(&mut st, ring) + } + + /// Arms the sense ring on the stopped serializer, then `pwrd`. + fn start_with_ring(&self, st: &mut ServiceState, ring: &SenseRing) -> Result<()> { + // The serializer stopped a firmware round trip ago: the channel has drained what the + // serializer last delivered. + ring.stop(); + ring.start(&self.aop.dev)?; + if let Err(e) = self.set_power(st, STATE_PWRD, self.pwrd_param) { + ring.stop(); + return Err(e); + } + st.running = true; + Ok(()) + } + + /// Back to `pw1 ` (after the amps or the codec muted); the sense ring stops after it. + fn stop(&self) { + let mut st = self.state.lock(); + self.stop_locked(&mut st); + } + + fn stop_locked(&self, st: &mut ServiceState) { + if st.running { + if let Err(e) = self.set_power(st, STATE_PW1, 0) { + dev_err!(self.aop.dev, "{} pw1 failed: {:?}\n", self.tag, e); + } + st.running = false; + } + if let Some(ring) = &self.ring { + ring.stop(); + } + } + + /// System sleep: the front-ends are suspended (their DMA stopped) and the amps muted; the + /// serializer and the sense ring stop. + fn suspend(&self) { + let mut st = self.state.lock(); + st.resume_run = st.running; + self.stop_locked(&mut st); + } + + /// Resume: a serializer that ran comes back, sense ring first, before ASoC unmutes the amps of + /// the suspended stream (the clocks must run when they do). The stream's own prepare then + /// restarts it once more, with its DMA. + fn resume(&self) -> Result<()> { + let mut st = self.state.lock(); + if !mem::take(&mut st.resume_run) || st.users == 0 { + return Ok(()); + } + match &self.ring { + Some(ring) => self.start_with_ring(&mut st, ring), + None => { + self.set_power(&mut st, STATE_PWRD, self.pwrd_param)?; + st.running = true; + Ok(()) + } + } + } +} + +const SENSE_FRAME_BYTES: usize = SENSE_CHANNELS as usize * 2; +/// 10 ms periods, eight of them: whole frames, so ring offsets on period edges stay on frames. +const SENSE_RING_PERIOD_BYTES: usize = 480 * SENSE_FRAME_BYTES; +const SENSE_RING_BYTES: usize = 8 * SENSE_RING_PERIOD_BYTES; +/// SNDRV_PCM_STATE_RUNNING (a cast macro the bindings do not carry). +const PCM_STATE_RUNNING: bindings::snd_pcm_state_t = 3; + +/// The driver-owned sense capture (base-ns ADMAC RX0): a cyclic ring armed before every `pwrd`, +/// so ring offset 0 is slot 0 of a frame and every whole-frame offset is a frame start. +/// +/// The ring fields change only with the channel stopped and synchronized (no callback runs); +/// `start` publishes them before the channel starts. +struct SenseRing { + chan: AtomicPtr, + cpu: AtomicPtr, + handle: AtomicU64, + cookie: AtomicI32, + /// Ring offset (a whole frame) up to which the callback has consumed the data. + done: AtomicUsize, + /// The attached sense front-end, or null; cleared under that front-end's stream lock, which + /// the callback holds while it uses the front-end. + sub: AtomicPtr, + /// Frames delivered to the front-end since its start; changed under its stream lock. + fe_pos: AtomicUsize, + /// The front-end runs (its trigger sets this under the stream lock). + fe_running: AtomicBool, +} + +impl SenseRing { + fn new() -> Self { + Self { + chan: AtomicPtr::new(ptr::null_mut()), + cpu: AtomicPtr::new(ptr::null_mut()), + handle: AtomicU64::new(0), + cookie: AtomicI32::new(0), + done: AtomicUsize::new(0), + sub: AtomicPtr::new(ptr::null_mut()), + fe_pos: AtomicUsize::new(0), + fe_running: AtomicBool::new(false), + } + } + + /// Requests the channel and the ring (once per power-up of the service) and starts the cyclic + /// transfer. The caller holds the service lock with the fabric powered and the serializer + /// stopped. + fn start(&self, dev: &device::Device) -> Result<()> { + let mut chan = self.chan.load(Ordering::Acquire); + if chan.is_null() { + // SAFETY: the device is live and the name NUL-terminated. + chan = from_err_ptr(unsafe { + bindings::dma_request_chan(dev.as_raw(), c_str!("sense").as_char_ptr()) + })?; + let mut handle: bindings::dma_addr_t = 0; + // SAFETY: the channel's device is the ADMAC behind AOP DART stream 8; it outlives the + // ring, which `release` frees before the channel. + let cpu = unsafe { + bindings::dma_alloc_attrs( + (*(*chan).device).dev, + SENSE_RING_BYTES, + &mut handle, + bindings::GFP_KERNEL, + 0, + ) + }; + if cpu.is_null() { + // SAFETY: requested above, unused. + unsafe { bindings::dma_release_channel(chan) }; + return Err(ENOMEM); + } + self.cpu.store(cpu.cast(), Ordering::Release); + self.handle.store(handle as u64, Ordering::Release); + self.chan.store(chan, Ordering::Release); + } + let handle = self.handle.load(Ordering::Acquire) as bindings::dma_addr_t; + let mut cfg = bindings::dma_slave_config::default(); + cfg.direction = bindings::dma_transfer_direction_DMA_DEV_TO_MEM; + cfg.src_addr_width = bindings::dma_slave_buswidth_DMA_SLAVE_BUSWIDTH_2_BYTES; + // FRAME_4_WORDS, as the front-ends' streams (see `pcm_hw_params`). + cfg.src_port_window_size = 4; + // SAFETY: `chan` is our live, stopped channel; the descriptor is ours until submitted. + unsafe { + let dma = (*chan).device; + let ret = match (*dma).device_config { + Some(f) => f(chan, &mut cfg), + None => ENOSYS.to_errno(), + }; + if ret < 0 { + return Err(Error::from_errno(ret)); + } + let prep = (*dma).device_prep_dma_cyclic.ok_or(ENOSYS)?; + let desc = prep( + chan, + handle, + SENSE_RING_BYTES, + SENSE_RING_PERIOD_BYTES, + bindings::dma_transfer_direction_DMA_DEV_TO_MEM, + bindings::dma_ctrl_flags_DMA_PREP_INTERRUPT as _, + ); + if desc.is_null() { + return Err(EIO); + } + (*desc).callback = Some(sense_ring_done); + (*desc).callback_param = ptr::from_ref(self).cast_mut().cast(); + self.done.store(0, Ordering::Release); + let cookie = ((*desc).tx_submit.ok_or(EIO)?)(desc); + if cookie < 0 { + return Err(Error::from_errno(cookie)); + } + self.cookie.store(cookie, Ordering::Release); + if let Some(f) = (*dma).device_issue_pending { + f(chan); + } + } + Ok(()) + } + + /// Stops the transfer and waits for its callback; a no-op without a channel. + fn stop(&self) { + let chan = self.chan.load(Ordering::Acquire); + if chan.is_null() { + return; + } + // SAFETY: our live channel; terminate and synchronize are valid on an idle one. + unsafe { + let dma = (*chan).device; + if let Some(f) = (*dma).device_terminate_all { + f(chan); + } + if let Some(f) = (*dma).device_synchronize { + f(chan); + } + } + } + + /// Stops, then frees the ring and the channel (with the fabric still powered). + fn release(&self) { + self.stop(); + let chan = self.chan.swap(ptr::null_mut(), Ordering::AcqRel); + if chan.is_null() { + return; + } + // SAFETY: the transfer is stopped and synchronized; the ring was allocated on this + // channel's device in `start`. + unsafe { + bindings::dma_free_attrs( + (*(*chan).device).dev, + SENSE_RING_BYTES, + self.cpu.swap(ptr::null_mut(), Ordering::AcqRel).cast(), + self.handle.load(Ordering::Acquire) as bindings::dma_addr_t, + 0, + ); + bindings::dma_release_channel(chan); + } + } + + fn attach(&self, substream: *mut bindings::snd_pcm_substream) { + self.fe_running.store(false, Ordering::Relaxed); + self.fe_pos.store(0, Ordering::Relaxed); + self.sub.store(substream, Ordering::Release); + } + + /// Detaches the front-end under its stream lock: a callback that read the pointer before + /// takes the same lock, sees the front-end gone and leaves it alone. No waiting. + /// + /// # Safety + /// + /// `substream` must be the attached front-end, in its close. + unsafe fn detach(&self, substream: *mut bindings::snd_pcm_substream) { + // SAFETY: by the caller's contract. + unsafe { + let flags = bindings::_snd_pcm_stream_lock_irqsave(substream); + self.sub.store(ptr::null_mut(), Ordering::Release); + bindings::snd_pcm_stream_unlock_irqrestore(substream, flags); + } + } + + /// Copies `bytes` (whole frames) from ring offset `from` into a running front-end's buffer. + /// + /// # Safety + /// + /// `sub` must be a substream this ring had attached. The substream itself belongs to the PCM + /// and outlives the ring's use of it: the ring stops and synchronizes with this callback when + /// the service's last user goes, before the card can go. + unsafe fn deliver(&self, sub: *mut bindings::snd_pcm_substream, from: usize, bytes: usize) { + // SAFETY: by the caller's contract; the stream lock orders this with the trigger, the + // pointer callback and `detach`. + let flags = unsafe { bindings::_snd_pcm_stream_lock_irqsave(sub) }; + // Still attached under the lock: the runtime lives until after the front-end's close. + let rt = if self.sub.load(Ordering::Acquire) == sub { + // SAFETY: as above. + unsafe { (*sub).runtime } + } else { + ptr::null_mut() + }; + // SAFETY: an attached front-end's runtime (see above). + if !rt.is_null() + && self.fe_running.load(Ordering::Relaxed) + && unsafe { (*rt).state } == PCM_STATE_RUNNING + && unsafe { (*rt).frame_bits } as usize == SENSE_FRAME_BYTES * 8 + { + // SAFETY: as above. + let (area, frames, period) = unsafe { + ( + (*rt).dma_area, + (*rt).buffer_size as usize, + (*rt).period_size as usize, + ) + }; + let ring = self.cpu.load(Ordering::Acquire); + let mut pos = self.fe_pos.load(Ordering::Relaxed); + let first = pos; + let mut src = from; + let mut left = bytes / SENSE_FRAME_BYTES; + while left > 0 && frames > 0 { + let off = pos % frames; + let n = left + .min(frames - off) + .min((SENSE_RING_BYTES - src) / SENSE_FRAME_BYTES); + // SAFETY: `n` frames fit at `off` in the front-end buffer and at `src` in the + // ring; the DMA wrote them before the residue we read said so. + unsafe { + ptr::copy_nonoverlapping( + ring.add(src), + area.add(off * SENSE_FRAME_BYTES), + n * SENSE_FRAME_BYTES, + ) + }; + pos += n; + left -= n; + src = (src + n * SENSE_FRAME_BYTES) % SENSE_RING_BYTES; + } + self.fe_pos.store(pos, Ordering::Relaxed); + // Once per completed period, as ALSA's interrupt accounting expects: it treats a call + // that advanced less than a period, some jiffies after the last one, as a missed + // buffer wrap (and the sense front-end's rate is 0 until a playback sets it, which + // makes that window two jiffies), so a period longer than the ring's 10 ms overran. + if first / period.max(1) != pos / period.max(1) { + // SAFETY: a running, attached front-end, under its stream lock. + unsafe { bindings::snd_pcm_period_elapsed_under_stream_lock(sub) }; + } + } + // SAFETY: locked above. + unsafe { bindings::snd_pcm_stream_unlock_irqrestore(sub, flags) }; + } +} + +/// The ring's period callback (the ADMAC tasklet): hands the whole frames written since the last +/// call to a running sense front-end, or drops them. +/// +/// # Safety +/// +/// `param` is the `SenseRing` that set up the transfer; `stop` synchronizes with this callback +/// before the ring changes or goes away. +unsafe extern "C" fn sense_ring_done(param: *mut c_void) { + // SAFETY: by the function's contract. + let ring = unsafe { &*param.cast::() }; + let chan = ring.chan.load(Ordering::Acquire); + if chan.is_null() { + return; + } + let mut state = bindings::dma_tx_state { + last: 0, + used: 0, + residue: 0, + in_flight_bytes: 0, + }; + // SAFETY: our live channel and cookie. + let status = unsafe { + match (*(*chan).device).device_tx_status { + Some(f) => f(chan, ring.cookie.load(Ordering::Acquire), &mut state), + None => return, + } + }; + let residue = state.residue as usize; + if status == bindings::dma_status_DMA_ERROR || residue > SENSE_RING_BYTES { + return; + } + // The device wrote everything up to here before it reported the residue. + fence(Ordering::SeqCst); + let now = + (SENSE_RING_BYTES - residue) % SENSE_RING_BYTES / SENSE_FRAME_BYTES * SENSE_FRAME_BYTES; + let from = ring.done.swap(now, Ordering::AcqRel); + let bytes = (now + SENSE_RING_BYTES - from) % SENSE_RING_BYTES; + if bytes == 0 { + return; + } + let sub = ring.sub.load(Ordering::Acquire); + if !sub.is_null() { + // SAFETY: a substream the ring had attached (`deliver` re-checks under its lock). + unsafe { ring.deliver(sub, from, bytes) }; + } +} + +// ---- the ASoC component ---- + +const FE_COUNT: usize = 5; +/// DAI ids: 0 lets ASoC assign one, so the front-ends carry 1..=5 (the speaker component's three, +/// then the jack component's two) and the back-ends 10 and 11. +const FE_DAI_ID_BASE: u32 = 1; +const BE_DAI_ID_SPKR: u32 = 10; +const BE_DAI_ID_JACK: u32 = 11; +/// The jack back-end's `sound-dai` argument on the audio node: 0..=3 are the speaker component's +/// DAIs by table index. +const DT_DAI_JACK: u32 = 4; +/// Which service's power a front-end stream needs. +#[derive(Clone, Copy)] +enum Svc { + Spkr, + Cout, + Cin, +} +/// A front-end stream: its channel name on the audio node, its service and its sample formats. +struct FeStream { + dma: &'static CStr, + svc: Svc, + formats: u64, + /// The sense: served from the service's ring, not from a channel of its own. + ring: bool, +} +/// Per front-end and stream [playback, capture]. The speaker card: j616-pcm-0 is macaudio's +/// "Primary" (the jack on other machines; none here), j616-pcm-1 its "Secondary", the speakers, +/// j616-pcm-2 the speaker sense. The jack card: j616-jack-pcm-0 its "Primary", the jack, +/// j616-jack-pcm-1 its "Secondary" (none). +static FE_STREAMS: [[Option; 2]; FE_COUNT] = [ + [None, None], + [ + Some(FeStream { + dma: c_str!("spkr"), + svc: Svc::Spkr, + formats: PLAYBACK_FORMATS, + ring: false, + }), + None, + ], + [ + None, + Some(FeStream { + dma: c_str!("sense"), + svc: Svc::Spkr, + formats: SENSE_FORMATS, + ring: true, + }), + ], + [ + Some(FeStream { + dma: c_str!("cout"), + svc: Svc::Cout, + formats: PLAYBACK_FORMATS, + ring: false, + }), + Some(FeStream { + dma: c_str!("cin"), + svc: Svc::Cin, + formats: PLAYBACK_FORMATS, + ring: false, + }), + ], + [None, None], +]; +const SPKR_CHANNELS: u32 = 6; +const SENSE_CHANNELS: u32 = 12; +const JACK_CHANNELS: u32 = 2; +const HEADSET_CHANNELS: u32 = 1; +/// The serializer takes 24-bit samples from the low bits of each 32-bit DMA word (as the AOP's +/// other 24-in-32 streams do; the jack's records say 24 valid bits as well); the sense words are +/// 16-bit. SNDRV_PCM_FMTBIT_S24_LE and _S16_LE (formats 6 and 2; the bindings only carry the float +/// one). +const PLAYBACK_FORMATS: u64 = 1 << 6; +const SENSE_FORMATS: u64 = 1 << 2; +const PCM_BUFFER_MAX: usize = 1024 * 1024; +const PCM_PERIOD_BYTES_MIN: usize = 256; +const PCM_PERIOD_BYTES_MAX: usize = 0x10000; +/// One ADMAC frame (FRAME_4_WORDS): the jack's 8- and 4-byte frames need whole bursts per period. +const ADMAC_BURST_BYTES: u32 = 16; + +/// Everything ASoC keeps pointers into, one per component; C retains the address of `component`, +/// so this lives from before registration until unregistration has finished every callback. +#[repr(C)] +struct AsocContext { + component: bindings::snd_soc_component_driver, + spkr: Arc, + cout: Arc, + cin: Arc, + dais: KVec>, + _ops: KBox, +} + +const _: () = assert!(mem::offset_of!(AsocContext, component) == 0); + +/// # Safety +/// +/// `component` must belong to this module's registered component. +unsafe fn context<'a>(component: *mut bindings::snd_soc_component) -> &'a AsocContext { + // SAFETY: by the caller's contract; the descriptor is the first field of its repr(C) context + // and C keeps the exact pointer passed to registration. + unsafe { &*(*component).driver.cast::() } +} + +impl AsocContext { + fn service(&self, svc: Svc) -> &Service { + match svc { + Svc::Spkr => &self.spkr, + Svc::Cout => &self.cout, + Svc::Cin => &self.cin, + } + } +} + +/// The service behind a back-end DAI and direction, and whether its prepare runs the serializer: +/// the speaker sense rides on the playback's `pwrd`, the jack's directions are separate services. +/// +/// # Safety +/// +/// `dai` must be a DAI of this module's registered component and `substream` live. +unsafe fn be_service<'a>( + dai: *mut bindings::snd_soc_dai, + substream: *mut bindings::snd_pcm_substream, +) -> (&'a Service, bool) { + // SAFETY: by the caller's contract. + let (ctx, id, playback) = unsafe { + ( + context((*dai).component), + (*dai).id as u32, + is_playback(substream), + ) + }; + match (id, playback) { + (BE_DAI_ID_JACK, true) => (&ctx.cout, true), + (BE_DAI_ID_JACK, false) => (&ctx.cin, true), + (_, playback) => (&ctx.spkr, playback), + } +} + +/// The front-end stream of a runtime and substream, `None` for a back-end or a stream this machine +/// does not have. +/// +/// # Safety +/// +/// `rtd` must be a live runtime of one of this component's links and `substream` live. +unsafe fn fe_stream( + rtd: *mut bindings::snd_soc_pcm_runtime, + substream: *mut bindings::snd_pcm_substream, +) -> Option<(usize, &'static FeStream)> { + // SAFETY: by the caller's contract. + let (fe, playback) = unsafe { (fe_index(rtd)?, is_playback(substream)) }; + Some((fe, FE_STREAMS[fe][usize::from(!playback)].as_ref()?)) +} + +/// # Safety +/// +/// `substream` must be live and belong to one of this component's links. +unsafe fn rtd_of( + substream: *mut bindings::snd_pcm_substream, +) -> *mut bindings::snd_soc_pcm_runtime { + // SAFETY: by the caller's contract. + unsafe { (*substream).private_data.cast() } +} + +/// # Safety +/// +/// `rtd` must be a live ASoC runtime. +unsafe fn is_be(rtd: *mut bindings::snd_soc_pcm_runtime) -> bool { + // SAFETY: by the caller's contract. + unsafe { (*(*rtd).dai_link).no_pcm() != 0 } +} + +/// The front-end index of a runtime (from its CPU DAI's id), `None` for a back-end. +/// +/// # Safety +/// +/// `rtd` must be a live ASoC runtime of one of this component's links. +unsafe fn fe_index(rtd: *mut bindings::snd_soc_pcm_runtime) -> Option { + // SAFETY: by the caller's contract; the first DAI of a runtime is its CPU DAI. + let id = unsafe { (**(*rtd).dais).id } as u32; + if (FE_DAI_ID_BASE..FE_DAI_ID_BASE + FE_COUNT as u32).contains(&id) { + Some((id - FE_DAI_ID_BASE) as usize) + } else { + None + } +} + +/// # Safety +/// +/// `substream` must be live. +unsafe fn is_playback(substream: *mut bindings::snd_pcm_substream) -> bool { + // SAFETY: by the caller's contract. + unsafe { (*substream).stream == bindings::SNDRV_PCM_STREAM_PLAYBACK as i32 } +} + +/// The sense ring behind a front-end substream, `None` for every other stream. +/// +/// # Safety +/// +/// As for `fe_stream`, and `component` must be this module's registered component. +unsafe fn sense_ring<'a>( + component: *mut bindings::snd_soc_component, + substream: *mut bindings::snd_pcm_substream, +) -> Option<&'a SenseRing> { + // SAFETY: by the caller's contract. + let (ctx, rtd) = unsafe { (context(component), rtd_of(substream)) }; + // SAFETY: as above. + if unsafe { is_be(rtd) } { + return None; + } + // SAFETY: as above. + let (_, stream) = unsafe { fe_stream(rtd, substream) }?; + if !stream.ring { + return None; + } + ctx.service(stream.svc).ring.as_ref() +} + +/// The sense front-end's buffer: plain memory the ring callback copies whole frames into. +fn sense_open(substream: *mut bindings::snd_pcm_substream) -> i32 { + // SAFETY: a live substream in its open. + unsafe { + if (*substream).dma_buffer.dev.type_ == bindings::SNDRV_DMA_TYPE_UNKNOWN as i32 { + let ret = bindings::snd_pcm_set_managed_buffer( + substream, + bindings::SNDRV_DMA_TYPE_VMALLOC as i32, + ptr::null_mut(), + 0, + 0, + ); + if ret < 0 { + return ret; + } + } + let mut hw = dmaengine_hw(); + hw.info |= bindings::SNDRV_PCM_INFO_BLOCK_TRANSFER | bindings::SNDRV_PCM_INFO_BATCH; + hw.formats = SENSE_FORMATS; + (*(*substream).runtime).hw = hw; + } + 0 +} + +fn dmaengine_hw() -> bindings::snd_pcm_hardware { + bindings::snd_pcm_hardware { + info: bindings::SNDRV_PCM_INFO_MMAP + | bindings::SNDRV_PCM_INFO_MMAP_VALID + | bindings::SNDRV_PCM_INFO_INTERLEAVED, + buffer_bytes_max: PCM_BUFFER_MAX, + period_bytes_min: PCM_PERIOD_BYTES_MIN, + period_bytes_max: PCM_PERIOD_BYTES_MAX, + periods_min: 2, + periods_max: (PCM_BUFFER_MAX / PCM_PERIOD_BYTES_MIN) as u32, + fifo_size: 16, + ..Default::default() + } +} + +/// Front-end open: a power-state reference (the back-end already raised `pw1 `, which powers the +/// ADMAC), then the channel and the dmaengine runtime on it. +/// +/// # Safety +/// +/// Called by ASoC on this module's registered component with live arguments. +unsafe extern "C" fn pcm_open( + component: *mut bindings::snd_soc_component, + substream: *mut bindings::snd_pcm_substream, +) -> i32 { + // SAFETY: ASoC calls the component op with live arguments of one of its links. + let (ctx, rtd) = unsafe { (context(component), rtd_of(substream)) }; + // SAFETY: as above. + if unsafe { is_be(rtd) } { + return 0; + } + // SAFETY: as above. + if unsafe { fe_index(rtd) }.is_none() { + return EINVAL.to_errno(); + } + // SAFETY: as above. + let Some((fe, stream)) = (unsafe { fe_stream(rtd, substream) }) else { + return ENODEV.to_errno(); + }; + let svc = ctx.service(stream.svc); + if let Err(e) = svc.get() { + return e.to_errno(); + } + if let (true, Some(ring)) = (stream.ring, &svc.ring) { + let ret = sense_open(substream); + if ret < 0 { + svc.put(); + return ret; + } + ring.attach(substream); + return 0; + } + // SAFETY: the device is live and the name NUL-terminated. + let chan = match from_err_ptr(unsafe { + bindings::dma_request_chan(svc.aop.dev.as_raw(), stream.dma.as_char_ptr()) + }) { + Ok(chan) => chan, + Err(e) => { + dev_err!( + svc.aop.dev, + "{} front-end {}: no DMA channel {:?}: {:?}\n", + svc.tag, + fe, + stream.dma, + e + ); + svc.put(); + return e.to_errno(); + } + }; + let mut ret = pcm_open_dmaengine(substream, chan, stream.formats); + if ret >= 0 && !matches!(stream.svc, Svc::Spkr) { + // SAFETY: the runtime exists for the op. + ret = unsafe { + bindings::snd_pcm_hw_constraint_step( + (*substream).runtime, + 0, + bindings::SNDRV_PCM_HW_PARAM_PERIOD_BYTES as _, + ADMAC_BURST_BYTES as _, + ) + }; + if ret < 0 { + // SAFETY: the dmaengine runtime owns the channel since the open succeeded. + unsafe { bindings::snd_dmaengine_pcm_close_release_chan(substream) }; + svc.put(); + return ret; + } + } + if ret < 0 { + // SAFETY: the channel was requested above and is not attached to the substream. + unsafe { bindings::dma_release_channel(chan) }; + svc.put(); + } + ret +} + +fn pcm_open_dmaengine( + substream: *mut bindings::snd_pcm_substream, + chan: *mut bindings::dma_chan, + formats: u64, +) -> i32 { + // SAFETY: a live substream and a channel the caller requested. + let unallocated = + unsafe { (*substream).dma_buffer.dev.type_ == bindings::SNDRV_DMA_TYPE_UNKNOWN as i32 }; + if unallocated { + // SAFETY: as above; the channel's device is the ADMAC behind AOP DART stream 8 and outlives + // the buffer. + let ret = unsafe { + bindings::snd_pcm_set_managed_buffer( + substream, + bindings::SNDRV_DMA_TYPE_DEV as i32, + (*(*chan).device).dev, + 0, + 0, + ) + }; + if ret < 0 { + return ret; + } + } + let mut hw = dmaengine_hw(); + let mut dma_data = bindings::snd_dmaengine_dai_dma_data::default(); + // SAFETY: the substream and the channel are live for the call. + let ret = unsafe { + bindings::snd_dmaengine_pcm_refine_runtime_hwparams(substream, &mut dma_data, &mut hw, chan) + }; + if ret < 0 { + return ret; + } + hw.formats &= formats; + if hw.formats == 0 { + return EINVAL.to_errno(); + } + // The slots line up only when a prepare restarts the serializer (`Service::run`): a paused + // DMA restarted mid-frame by resume or pause-release would shift them, so take neither. + hw.info &= !(bindings::SNDRV_PCM_INFO_PAUSE | bindings::SNDRV_PCM_INFO_RESUME); + // SAFETY: the runtime exists for the op; on success the dmaengine runtime owns `chan`. + unsafe { + (*(*substream).runtime).hw = hw; + bindings::snd_dmaengine_pcm_open(substream, chan) + } +} + +/// # Safety +/// +/// Called by ASoC on this module's registered component with live arguments. +unsafe extern "C" fn pcm_close( + component: *mut bindings::snd_soc_component, + substream: *mut bindings::snd_pcm_substream, +) -> i32 { + // SAFETY: ASoC calls the component op with live arguments of one of its links. + let (ctx, rtd) = unsafe { (context(component), rtd_of(substream)) }; + // SAFETY: as above. + if unsafe { is_be(rtd) } { + return 0; + } + // SAFETY: as above. + let Some((_, stream)) = (unsafe { fe_stream(rtd, substream) }) else { + return 0; + }; + let svc = ctx.service(stream.svc); + if let (true, Some(ring)) = (stream.ring, &svc.ring) { + // SAFETY: the attached sense front-end, in its close. + unsafe { ring.detach(substream) }; + svc.put(); + return 0; + } + // The channel goes first, with the fabric still powered by this front-end's reference. SAFETY: + // the dmaengine runtime was set up by `pcm_open`. + let ret = unsafe { bindings::snd_dmaengine_pcm_close_release_chan(substream) }; + svc.put(); + ret +} + +/// The ADMAC moves one frame per burst of up to four words: FRAME_4_WORDS for every stream, as the +/// ADT's records for these streams select (0x22 32-bit, 0x21 16-bit; the jack's records list both). +/// +/// # Safety +/// +/// Called by ASoC on this module's registered component with live arguments. +unsafe extern "C" fn pcm_hw_params( + component: *mut bindings::snd_soc_component, + substream: *mut bindings::snd_pcm_substream, + params: *mut bindings::snd_pcm_hw_params, +) -> i32 { + // SAFETY: as for `pcm_open`. + if unsafe { is_be(rtd_of(substream)) || sense_ring(component, substream).is_some() } { + return 0; + } + // SAFETY: the dmaengine runtime was set up by `pcm_open`. + let chan = unsafe { bindings::snd_dmaengine_pcm_get_chan(substream) }; + let mut cfg = bindings::dma_slave_config::default(); + // SAFETY: live substream and parameters. + let rc = unsafe { bindings::snd_hwparams_to_dma_slave_config(substream, params, &mut cfg) }; + if rc < 0 { + return rc; + } + // SAFETY: as above. + if unsafe { is_playback(substream) } { + cfg.dst_port_window_size = 4; + } else { + cfg.src_port_window_size = 4; + } + // SAFETY: `chan` is the live channel behind this substream. + unsafe { + match (*(*chan).device).device_config { + Some(f) => f(chan, &mut cfg), + None => ENOSYS.to_errno(), + } + } +} + +/// # Safety +/// +/// Called by ASoC on this module's registered component with live arguments. +unsafe extern "C" fn pcm_trigger( + component: *mut bindings::snd_soc_component, + substream: *mut bindings::snd_pcm_substream, + cmd: i32, +) -> i32 { + // SAFETY: as for `pcm_open`. + if unsafe { is_be(rtd_of(substream)) } { + return 0; + } + // The sense attaches to the ring at its next whole frame; ALSA holds the stream lock, which + // the ring callback takes too. + // SAFETY: as above. + if let Some(ring) = unsafe { sense_ring(component, substream) } { + match cmd as u32 { + bindings::SNDRV_PCM_TRIGGER_START => { + ring.fe_pos.store(0, Ordering::Relaxed); + ring.fe_running.store(true, Ordering::Relaxed); + } + bindings::SNDRV_PCM_TRIGGER_RESUME | bindings::SNDRV_PCM_TRIGGER_PAUSE_RELEASE => { + ring.fe_running.store(true, Ordering::Relaxed) + } + bindings::SNDRV_PCM_TRIGGER_STOP + | bindings::SNDRV_PCM_TRIGGER_SUSPEND + | bindings::SNDRV_PCM_TRIGGER_PAUSE_PUSH => { + ring.fe_running.store(false, Ordering::Relaxed) + } + _ => return EINVAL.to_errno(), + } + return 0; + } + // SAFETY: the dmaengine runtime was set up by `pcm_open`. + unsafe { bindings::snd_dmaengine_pcm_trigger(substream, cmd) } +} + +/// # Safety +/// +/// Called by ASoC on this module's registered component with live arguments. +unsafe extern "C" fn pcm_sync_stop( + component: *mut bindings::snd_soc_component, + substream: *mut bindings::snd_pcm_substream, +) -> i32 { + // SAFETY: as for `pcm_open`. + if unsafe { is_be(rtd_of(substream)) || sense_ring(component, substream).is_some() } { + return 0; + } + // SAFETY: the dmaengine runtime was set up by `pcm_open`. + unsafe { bindings::snd_dmaengine_pcm_sync_stop(substream) } +} + +/// # Safety +/// +/// Called by ASoC on this module's registered component with live arguments. +unsafe extern "C" fn pcm_pointer( + component: *mut bindings::snd_soc_component, + substream: *mut bindings::snd_pcm_substream, +) -> bindings::snd_pcm_uframes_t { + // SAFETY: as for `pcm_open`. + if unsafe { is_be(rtd_of(substream)) } { + return (-i64::from(bindings::ENOTSUPP)) as bindings::snd_pcm_uframes_t; + } + // SAFETY: as above; ALSA calls this under the stream lock with a live runtime. + if let Some(ring) = unsafe { sense_ring(component, substream) } { + // SAFETY: as above. + let frames = unsafe { (*(*substream).runtime).buffer_size } as usize; + return (ring.fe_pos.load(Ordering::Relaxed) % frames.max(1)) + as bindings::snd_pcm_uframes_t; + } + // SAFETY: the dmaengine runtime was set up by `pcm_open`. + unsafe { bindings::snd_dmaengine_pcm_pointer(substream) } +} + +// The firmware runs the serializer: format, slots and clocks are its own, whatever the machine +// driver asks. +unsafe extern "C" fn dai_set_fmt(_dai: *mut bindings::snd_soc_dai, _fmt: u32) -> i32 { + 0 +} +unsafe extern "C" fn dai_set_tdm_slot( + _dai: *mut bindings::snd_soc_dai, + _tx: u32, + _rx: u32, + _slots: i32, + _width: i32, +) -> i32 { + 0 +} +unsafe extern "C" fn dai_set_sysclk( + _dai: *mut bindings::snd_soc_dai, + _id: i32, + _freq: u32, + _dir: i32, +) -> i32 { + 0 +} +unsafe extern "C" fn dai_set_bclk_ratio(_dai: *mut bindings::snd_soc_dai, _ratio: u32) -> i32 { + 0 +} + +/// Back-end startup: `pw1 ` (the first user attaches the service). +/// +/// # Safety +/// +/// Called by ASoC with a live DAI of this component and a live substream. +unsafe extern "C" fn be_startup( + substream: *mut bindings::snd_pcm_substream, + dai: *mut bindings::snd_soc_dai, +) -> i32 { + // SAFETY: by the caller's contract. + match unsafe { be_service(dai, substream) }.0.get() { + Ok(()) => 0, + Err(e) => e.to_errno(), + } +} + +/// Prepare: `pwrd`. ASoC prepares this CPU DAI before it unmutes the amps or the codec. +/// +/// # Safety +/// +/// Called by ASoC with a live DAI of this component and a live substream. +unsafe extern "C" fn be_prepare( + substream: *mut bindings::snd_pcm_substream, + dai: *mut bindings::snd_soc_dai, +) -> i32 { + // SAFETY: by the caller's contract. + let (svc, runs) = unsafe { be_service(dai, substream) }; + if !runs { + return 0; + } + // SAFETY: the back-end runtime of this substream is live for the call. + match svc.run(|| unsafe { mute_codecs(rtd_of(substream)) }) { + Ok(()) => 0, + Err(e) => e.to_errno(), + } +} + +/// Mutes a back-end's codecs (the amps); `__soc_pcm_prepare` unmutes them right after this +/// prepare. +/// +/// # Safety +/// +/// `rtd` must be a live back-end runtime, in its prepare. +unsafe fn mute_codecs(rtd: *mut bindings::snd_soc_pcm_runtime) { + // SAFETY: by the caller's contract; the codec DAIs follow the CPU DAIs in `dais`. + unsafe { + let link = (*rtd).dai_link; + let cpus = (*link).num_cpus as usize; + for i in 0..(*link).num_codecs as usize { + bindings::snd_soc_dai_digital_mute( + *(*rtd).dais.add(cpus + i), + 1, + bindings::SNDRV_PCM_STREAM_PLAYBACK as i32, + ); + } + } +} + +/// hw_free: back to `pw1 `. ASoC mutes the amps or the codec before it frees the CPU DAI. +/// +/// # Safety +/// +/// Called by ASoC with a live DAI of this component and a live substream. +unsafe extern "C" fn be_hw_free( + substream: *mut bindings::snd_pcm_substream, + dai: *mut bindings::snd_soc_dai, +) -> i32 { + // SAFETY: by the caller's contract. + let (svc, runs) = unsafe { be_service(dai, substream) }; + if runs { + svc.stop(); + } + 0 +} + +/// # Safety +/// +/// Called by ASoC with a live DAI of this component and a live substream. +unsafe extern "C" fn be_shutdown( + substream: *mut bindings::snd_pcm_substream, + dai: *mut bindings::snd_soc_dai, +) { + // SAFETY: by the caller's contract. + unsafe { be_service(dai, substream) }.0.put(); +} + +struct DaiOps { + fe: bindings::snd_soc_dai_ops, + be: bindings::snd_soc_dai_ops, +} + +impl DaiOps { + fn new() -> Self { + let fe = bindings::snd_soc_dai_ops { + set_fmt: Some(dai_set_fmt), + set_tdm_slot: Some(dai_set_tdm_slot), + set_sysclk: Some(dai_set_sysclk), + set_bclk_ratio: Some(dai_set_bclk_ratio), + ..Default::default() + }; + DaiOps { + be: bindings::snd_soc_dai_ops { + startup: Some(be_startup), + prepare: Some(be_prepare), + hw_free: Some(be_hw_free), + shutdown: Some(be_shutdown), + ..fe + }, + fe, + } + } +} + +fn stream(name: &'static CStr, formats: u64, channels: u32) -> bindings::snd_soc_pcm_stream { + bindings::snd_soc_pcm_stream { + stream_name: name.as_char_ptr(), + formats, + rates: bindings::SNDRV_PCM_RATE_48000, + rate_min: 48000, + rate_max: 48000, + channels_min: channels, + channels_max: channels, + ..Default::default() + } +} + +/// The speaker component: [0..=2] the front-ends, [3] the back-end. The order is ABI +/// (`sound-dai = <&aop_audio 3>`). +fn speaker_dais(ops: &DaiOps) -> [bindings::snd_soc_dai_driver; 4] { + let mut dais = [ + bindings::snd_soc_dai_driver { + name: c_str!("j616-pcm-0").as_char_ptr(), + id: FE_DAI_ID_BASE, + ops: &ops.fe, + // No stream of this card's: both directions exist only for macaudio's fixed routes + // ("PCM0 TX", "PCM0 RX"), whose widgets must exist for the card to bind; opening either + // fails (ENODEV). + playback: stream(c_str!("PCM0 TX"), PLAYBACK_FORMATS, SPKR_CHANNELS), + capture: stream(c_str!("PCM0 RX"), SENSE_FORMATS, SENSE_CHANNELS), + ..Default::default() + }, + bindings::snd_soc_dai_driver { + name: c_str!("j616-pcm-1").as_char_ptr(), + id: FE_DAI_ID_BASE + 1, + ops: &ops.fe, + playback: stream(c_str!("PCM1 TX"), PLAYBACK_FORMATS, SPKR_CHANNELS), + ..Default::default() + }, + bindings::snd_soc_dai_driver { + name: c_str!("j616-pcm-2").as_char_ptr(), + id: FE_DAI_ID_BASE + 2, + ops: &ops.fe, + capture: stream(c_str!("PCM2 RX"), SENSE_FORMATS, SENSE_CHANNELS), + ..Default::default() + }, + bindings::snd_soc_dai_driver { + name: c_str!("spkr").as_char_ptr(), + id: BE_DAI_ID_SPKR, + ops: &ops.be, + playback: stream(c_str!("SPKR TX"), PLAYBACK_FORMATS, SPKR_CHANNELS), + capture: stream(c_str!("SPKR RX"), SENSE_FORMATS, SENSE_CHANNELS), + ..Default::default() + }, + ]; + // The sense runs at the playback's rate (macaudio reads the back-end's symmetric rate). + dais[3].set_symmetric_rate(1); + dais +} + +/// The jack component: [0] the jack's front-end, [1] the second front-end macaudio's fixed routes +/// need, [2] the back-end (`sound-dai = <&aop_audio 4>`, see `jack_of_xlate_dai_name`). +fn jack_dais(ops: &DaiOps) -> [bindings::snd_soc_dai_driver; 3] { + let mut dais = [ + bindings::snd_soc_dai_driver { + name: c_str!("j616-jack-pcm-0").as_char_ptr(), + id: FE_DAI_ID_BASE + 3, + ops: &ops.fe, + playback: stream(c_str!("PCM0 TX"), PLAYBACK_FORMATS, JACK_CHANNELS), + capture: stream(c_str!("PCM0 RX"), PLAYBACK_FORMATS, HEADSET_CHANNELS), + ..Default::default() + }, + bindings::snd_soc_dai_driver { + name: c_str!("j616-jack-pcm-1").as_char_ptr(), + id: FE_DAI_ID_BASE + 4, + ops: &ops.fe, + // No stream: "PCM1 TX" exists for macaudio's fixed routes; opening it fails (ENODEV). + playback: stream(c_str!("PCM1 TX"), PLAYBACK_FORMATS, JACK_CHANNELS), + ..Default::default() + }, + bindings::snd_soc_dai_driver { + name: c_str!("jack").as_char_ptr(), + id: BE_DAI_ID_JACK, + ops: &ops.be, + playback: stream(c_str!("JACK TX"), PLAYBACK_FORMATS, JACK_CHANNELS), + capture: stream(c_str!("JACK RX"), PLAYBACK_FORMATS, HEADSET_CHANNELS), + ..Default::default() + }, + ]; + // Both directions share the serializer's frame. + dais[2].set_symmetric_rate(1); + dais +} + +/// Both components sit on the audio node, and ASoC resolves a `sound-dai` there by asking each +/// component in turn (newest first) until one answers. The speaker component answers 0..=3 by +/// table index and refuses 4 (EINVAL, out of range). This one answers 4 and defers the rest: a +/// speaker card that probes after this component registered but before the speaker component did +/// must wait for it, not fail. +/// +/// # Safety +/// +/// Called by ASoC with live arguments. +unsafe extern "C" fn jack_of_xlate_dai_name( + _component: *mut bindings::snd_soc_component, + args: *const bindings::of_phandle_args, + dai_name: *mut *const kernel::ffi::c_char, +) -> i32 { + // SAFETY: by the caller's contract. + unsafe { + if (*args).args_count != 1 || (*args).args[0] != DT_DAI_JACK { + return EPROBE_DEFER.to_errno(); + } + *dai_name = c_str!("jack").as_char_ptr(); + } + 0 +} + +/// One registered component; dropping it unregisters it (and with it any card that uses its DAIs). +struct Registration { + dev: ARef, + ctx: KBox, +} + +impl Registration { + fn new( + aop: &SndSocAopData, + services: [&Arc; 3], + component: bindings::snd_soc_component_driver, + dai_drivers: fn(&DaiOps) -> Result>>, + ) -> Result { + let ops = KBox::new(DaiOps::new(), GFP_KERNEL)?; + let dais = dai_drivers(&ops)?; + let [spkr, cout, cin] = services.map(Arc::clone); + let ctx = KBox::new( + AsocContext { + component, + spkr, + cout, + cin, + dais, + _ops: ops, + }, + GFP_KERNEL, + )?; + // SAFETY: the descriptor and the DAI table are heap allocations owned by `ctx`, which + // `Self` keeps until after unregistration. + let ret = unsafe { + bindings::snd_soc_register_component_d( + aop.dev.as_raw(), + &ctx.component, + Opaque::cast_into(ctx.dais.as_ptr()), + ctx.dais.len() as i32, + ) + }; + if ret < 0 { + return Err(Error::from_errno(ret)); + } + Ok(Self { + dev: aop.dev.clone(), + ctx, + }) + } +} + +impl Drop for Registration { + fn drop(&mut self) { + // SAFETY: registered in `new`; the context stays allocated until unregistration has + // finished every PCM and DAI callback. + unsafe { + bindings::snd_soc_unregister_component_by_driver(self.dev.as_raw(), &self.ctx.component) + }; + } +} + +fn to_kvec( + dais: [bindings::snd_soc_dai_driver; N], +) -> Result>> { + let mut v = KVec::with_capacity(N, GFP_KERNEL)?; + for dai in dais { + v.push(Opaque::new(dai), GFP_KERNEL)?; + } + Ok(v) +} + +fn component_driver(name: &'static CStr) -> bindings::snd_soc_component_driver { + bindings::snd_soc_component_driver { + name: name.as_char_ptr(), + open: Some(pcm_open), + close: Some(pcm_close), + hw_params: Some(pcm_hw_params), + trigger: Some(pcm_trigger), + sync_stop: Some(pcm_sync_stop), + pointer: Some(pcm_pointer), + ..Default::default() + } +} + +/// System sleep for the speaker service (see `Service::suspend`). +/// +/// # Safety +/// +/// Called by ASoC on the registered speaker component. +unsafe extern "C" fn spkr_suspend(component: *mut bindings::snd_soc_component) -> i32 { + // SAFETY: by the caller's contract. + unsafe { context(component) }.spkr.suspend(); + 0 +} + +/// # Safety +/// +/// Called by ASoC on the registered speaker component. +unsafe extern "C" fn spkr_resume(component: *mut bindings::snd_soc_component) -> i32 { + // SAFETY: by the caller's contract. + match unsafe { context(component) }.spkr.resume() { + Ok(()) => 0, + Err(e) => e.to_errno(), + } +} + +/// System sleep for the jack's services: both directions share the jack's serializer. +/// +/// # Safety +/// +/// Called by ASoC on the registered jack component. +unsafe extern "C" fn jack_suspend(component: *mut bindings::snd_soc_component) -> i32 { + // SAFETY: by the caller's contract. + let ctx = unsafe { context(component) }; + ctx.cin.suspend(); + ctx.cout.suspend(); + 0 +} + +/// # Safety +/// +/// Called by ASoC on the registered jack component. +unsafe extern "C" fn jack_resume(component: *mut bindings::snd_soc_component) -> i32 { + // SAFETY: by the caller's contract. + let ctx = unsafe { context(component) }; + let cout = ctx.cout.resume(); + let cin = ctx.cin.resume(); + match cout.and(cin) { + Ok(()) => 0, + Err(e) => e.to_errno(), + } +} + +/// The speaker component, and the jack's where the audio node lists its DMA channels. +pub(crate) struct SpeakerComponent { + _spkr: Registration, + _jack: Option, +} + +impl SpeakerComponent { + pub(crate) fn register(aop: Arc) -> Result { + let spkr = Service::new(aop.clone(), AUDIO_DEV_SPKR, "spkr", PWRD_OUTPUT, true)?; + let cout = Service::new(aop.clone(), AUDIO_DEV_COUT, "cout", PWRD_OUTPUT, false)?; + let cin = Service::new(aop.clone(), AUDIO_DEV_CIN, "cin", PWRD_INPUT, false)?; + let services = [&spkr, &cout, &cin]; + let has_jack = [c_str!("cout"), c_str!("cin")].iter().all(|name| { + aop.fwnode + .property_match_string(c_str!("dma-names"), name) + .is_ok() + }); + // The jack goes first (see `jack_of_xlate_dai_name`); its failure costs the jack only. + let jack = if has_jack { + let mut driver = component_driver(c_str!("t6040-aop-jack")); + driver.of_xlate_dai_name = Some(jack_of_xlate_dai_name); + driver.suspend = Some(jack_suspend); + driver.resume = Some(jack_resume); + match Registration::new(&aop, services, driver, |ops| to_kvec(jack_dais(ops))) { + Ok(r) => Some(r), + Err(e) => { + dev_err!( + aop.dev, + "Unable to register the jack ASoC component: {:?}\n", + e + ); + None + } + } + } else { + None + }; + let mut spkr_driver = component_driver(c_str!("t6040-aop-spkr")); + spkr_driver.suspend = Some(spkr_suspend); + spkr_driver.resume = Some(spkr_resume); + let spkr = Registration::new(&aop, services, spkr_driver, |ops| { + to_kvec(speaker_dais(ops)) + }) + .inspect_err(|e| { + dev_err!( + aop.dev, + "Unable to register the speaker ASoC component: {}\n", + e.to_errno() + ) + })?; + Ok(Self { + _spkr: spkr, + _jack: jack, + }) + } +} + +// SAFETY: the raw pointers in the contexts are C descriptors that are only read after +// registration. +unsafe impl Send for SpeakerComponent {} +// SAFETY: as above. +unsafe impl Sync for SpeakerComponent {} diff --git a/sound/soc/apple/macaudio.c b/sound/soc/apple/macaudio.c index 095a0391ea411..99b79bf7337c9 100644 --- a/sound/soc/apple/macaudio.c +++ b/sound/soc/apple/macaudio.c @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -102,12 +103,26 @@ struct macaudio_platform_cfg { /* * A CPU component other than the MCA: its front-end DAI names in * place of "mca-pcm-N", the DAI format its serial links run (the - * default MACAUDIO_DAI_FMT when 0) and the primary front-end's - * bclk ratio when the link's frame length is not ours to choose. + * default MACAUDIO_DAI_FMT when 0) and the primary and secondary + * front-ends' bclk ratios when the link's frame length is not ours + * to choose. */ const char *fe_dai_names[MACAUDIO_NUM_FE_LINKS]; unsigned int dai_fmt; unsigned int primary_bclk_ratio; + unsigned int secondary_bclk_ratio; + /* + * The front-ends' CPU component is their PCM provider as well: no + * platform lookup by OF node, which takes every component on that + * node (the J616s AOP speaker and jack components share one, each + * for its own card). + */ + bool fe_cpu_is_platform; + /* + * The highest headphone volume, a "Jack DAC Playback Volume" value + * (CS42L84_DAC_DB()), 0 for the codec's own maximum. + */ + int jack_max_vol; }; static const char *volume_control_names[] = { @@ -130,6 +145,10 @@ static const char *volume_control_names[] = { #define MAX98360A_0DB 127 #define MAX98360A_DB(x) (MAX98360A_0DB + 2 * (x)) +/* CS42L84 DAC: 0.5 dB steps from -128 dB, 256 = 0 dB, up to +12 dB */ +#define CS42L84_DAC_0DB 256 +#define CS42L84_DAC_DB(x) (CS42L84_DAC_0DB + 2 * (x)) + struct ma_codec_idle { int idle_mode; u32 idle_mask; @@ -138,6 +157,8 @@ struct ma_codec_idle { struct macaudio_snd_data { struct snd_soc_card card; struct snd_soc_jack jack; + /* The jack links these into its pin list, so each card needs its own */ + struct snd_soc_jack_pin jack_pins[2]; int jack_plugin_state; const struct macaudio_platform_cfg *cfg; @@ -174,6 +195,9 @@ struct macaudio_snd_data { ktime_t speaker_lock_remain; struct delayed_work lock_timeout_work; struct work_struct lock_update_work; + /* Between PM_SUSPEND_PREPARE and PM_POST_SUSPEND: the speaker volumes stay locked */ + bool pm_quiesced; + struct notifier_block pm_nb; /* * Software-gain amplifier: a lower gain cannot change samples that @@ -185,6 +209,15 @@ struct macaudio_snd_data { atomic_t sw_gain_prepared_epoch; atomic_t sw_gain_limit_failed; struct work_struct sw_gain_stop_work; + + /* + * A lock that did not take on every speaker volume (a control write + * failed) is a failed lock: the playback streams are stopped and + * refused until the driver is reloaded, since a volume above the safe + * value may be left behind. + */ + bool speaker_lock_failed; + struct work_struct lock_fail_work; }; static int please_blow_up_my_speakers; @@ -283,8 +316,10 @@ static struct macaudio_link_props macaudio_fe_link_props[] = { static void macaudio_vlimit_unlock(struct macaudio_snd_data *ma, bool unlock) { - int ret, max; + int ret, max, hits = 0; const char *name = volume_control_names[ma->cfg->amp]; + struct snd_kcontrol *kctl; + bool failed = false; if (!name) { WARN_ON_ONCE(1); @@ -321,16 +356,91 @@ static void macaudio_vlimit_unlock(struct macaudio_snd_data *ma, bool unlock) break; } - ret = snd_soc_limit_volume(&ma->card, name, max); - if (ret < 0) + /* + * One control at a time: snd_soc_limit_volume() stops at the first + * control it cannot clip, which would leave the rest at their old + * limit and value. + */ + list_for_each_entry(kctl, &ma->card.snd_card->controls, list) { + if (!snd_soc_control_matches(kctl, name)) + continue; + + hits++; + ret = snd_soc_limit_volume(&ma->card, kctl->id.name, max); + if (ret < 0) { + dev_err(ma->card.dev, "Failed to %slock volume %s: %d\n", + unlock ? "un" : "", kctl->id.name, ret); + failed = true; + } + } + if (!hits) dev_err(ma->card.dev, "Failed to %slock volume %s: %d\n", - unlock ? "un" : "", name, ret); + unlock ? "un" : "", name, -EINVAL); + /* * With a software gain the limit is the whole protection: refuse to * play rather than run the amplifier at full scale unlimited. */ if (ma->cfg->amp == AMP_MAX98360A) - atomic_set(&ma->sw_gain_limit_failed, ret < 0); + atomic_set(&ma->sw_gain_limit_failed, failed || !hits); + + if (failed && !unlock) { + WRITE_ONCE(ma->speaker_lock_failed, true); + schedule_work(&ma->lock_fail_work); + } +} + +/* + * Stop every running playback stream of the card, or with sw_gain_only those + * on software-gain links whose prepare predates the current lock epoch. + */ +static void macaudio_stop_playback(struct macaudio_snd_data *ma, bool sw_gain_only) +{ + struct snd_soc_pcm_runtime *rtd; + struct snd_pcm_substream *substream; + unsigned long flags; + + /* + * No volume or control lock may be held while taking the stream lock. + * Pick the links from the runtime being walked: not every playback + * substream on the card carries a runtime as its private data. + */ + for_each_card_rtds(&ma->card, rtd) { + if (!rtd->pcm) + continue; + if (sw_gain_only && !ma->link_props[rtd->dai_link->id].sw_gain) + continue; + substream = rtd->pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].substream; + if (!substream) + continue; + snd_pcm_stream_lock_irqsave(substream, flags); + if (substream->runtime && + (!sw_gain_only || + atomic_read(&ma->sw_gain_prepared_epoch) != + atomic_read(&ma->sw_gain_lock_epoch))) { + switch (substream->runtime->state) { + case SNDRV_PCM_STATE_PREPARED: + case SNDRV_PCM_STATE_RUNNING: + case SNDRV_PCM_STATE_DRAINING: + case SNDRV_PCM_STATE_PAUSED: + snd_pcm_stop(substream, SNDRV_PCM_STATE_XRUN); + break; + default: + break; + } + } + snd_pcm_stream_unlock_irqrestore(substream, flags); + } +} + +/* Stop every running playback stream of the card after a failed lock. */ +static void macaudio_lock_fail_work(struct work_struct *work) +{ + struct macaudio_snd_data *ma = container_of(work, struct macaudio_snd_data, + lock_fail_work); + + dev_crit(ma->card.dev, "speaker volume lock failed: stopping and refusing playback\n"); + macaudio_stop_playback(ma, false); } static void macaudio_vlimit_update(struct macaudio_snd_data *ma) @@ -356,6 +466,18 @@ static void macaudio_vlimit_update(struct macaudio_snd_data *ma) unlock = false; } + /* + * System sleep: userspace is frozen long before the card suspends (over + * half a second on J616s), and a playback stream left running then plays + * out its stale buffer and stops on an xrun into the still unmuted amps. + * Lock the volumes before the freeze instead of when the frozen daemon's + * lease runs out. + */ + if (ma->pm_quiesced) { + reason = "System suspend"; + unlock = false; + } + /* Check that *every* limited control is locked by the same owner */ list_for_each_entry(kctl, &ma->card.snd_card->controls, list) { if(!snd_soc_control_matches(kctl, volume_control_names[ma->cfg->amp])) @@ -375,7 +497,9 @@ static void macaudio_vlimit_update(struct macaudio_snd_data *ma) dev_info(ma->card.dev, "Speaker volumes unlocked\n"); } else { dev_info(ma->card.dev, "Speaker volumes locked: %s\n", reason); - ma->speaker_volume_was_locked = true; + /* The system-sleep lock is the kernel's own, not a lease the daemon lost */ + if (!ma->pm_quiesced) + ma->speaker_volume_was_locked = true; } macaudio_vlimit_unlock(ma, unlock); @@ -390,6 +514,44 @@ static void macaudio_vlimit_update(struct macaudio_snd_data *ma) } } +static int macaudio_pm_notify(struct notifier_block *nb, unsigned long action, + void *data) +{ + struct macaudio_snd_data *ma = container_of(nb, struct macaudio_snd_data, pm_nb); + bool quiesce; + + switch (action) { + case PM_SUSPEND_PREPARE: + case PM_HIBERNATION_PREPARE: + quiesce = true; + break; + case PM_POST_SUSPEND: + case PM_POST_HIBERNATION: + case PM_POST_RESTORE: + quiesce = false; + break; + default: + return NOTIFY_DONE; + } + + mutex_lock(&ma->volume_lock_mutex); + ma->pm_quiesced = quiesce; + /* + * The lease ends here, so after the resume the volumes stay locked until + * the daemon's next ping. Its clock (CLOCK_MONOTONIC) stops across s2idle: + * a lease left running would unlock them at resume and expire before the + * thawed daemon pings, which reports a lost lock and restarts it cold. + */ + if (quiesce) { + cancel_delayed_work(&ma->lock_timeout_work); + ma->speaker_lock_remain = 0; + } + macaudio_vlimit_update(ma); + mutex_unlock(&ma->volume_lock_mutex); + + return NOTIFY_OK; +} + static void macaudio_vlimit_enable_timeout(struct macaudio_snd_data *ma) { mutex_lock(&ma->volume_lock_mutex); @@ -498,38 +660,8 @@ static void macaudio_sw_gain_stop_work(struct work_struct *work) { struct macaudio_snd_data *ma = container_of(work, struct macaudio_snd_data, sw_gain_stop_work); - struct snd_soc_pcm_runtime *rtd; - struct snd_pcm_substream *substream; - unsigned long flags; - /* - * No volume or control lock may be held while taking the stream lock. - * Pick the links from the runtime being walked: not every playback - * substream on the card carries a runtime as its private data. - */ - for_each_card_rtds(&ma->card, rtd) { - if (!rtd->pcm || !ma->link_props[rtd->dai_link->id].sw_gain) - continue; - substream = rtd->pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].substream; - if (!substream) - continue; - snd_pcm_stream_lock_irqsave(substream, flags); - if (substream->runtime && - atomic_read(&ma->sw_gain_prepared_epoch) != - atomic_read(&ma->sw_gain_lock_epoch)) { - switch (substream->runtime->state) { - case SNDRV_PCM_STATE_PREPARED: - case SNDRV_PCM_STATE_RUNNING: - case SNDRV_PCM_STATE_DRAINING: - case SNDRV_PCM_STATE_PAUSED: - snd_pcm_stop(substream, SNDRV_PCM_STATE_XRUN); - break; - default: - break; - } - } - snd_pcm_stream_unlock_irqrestore(substream, flags); - } + macaudio_stop_playback(ma, true); } static int macaudio_copy_link(struct device *dev, struct snd_soc_dai_link *target, @@ -717,6 +849,8 @@ static int macaudio_parse_of(struct macaudio_snd_data *ma) link->dai_fmt = ma->cfg->dai_fmt; if (i == 0 && ma->cfg->primary_bclk_ratio) link_props->bclk_ratio = ma->cfg->primary_bclk_ratio; + if (i == 1 && ma->cfg->secondary_bclk_ratio) + link_props->bclk_ratio = ma->cfg->secondary_bclk_ratio; if (ma->cfg->sw_gain_dai) link_props->sw_gain = !strcmp(link->cpus[0].dai_name, ma->cfg->sw_gain_dai); @@ -865,8 +999,12 @@ static int macaudio_parse_of(struct macaudio_snd_data *ma) num_links += num_bes; } - for (i = 0; i < ARRAY_SIZE(macaudio_fe_links); i++) - card->dai_link[i].platforms->of_node = platform; + for (i = 0; i < ARRAY_SIZE(macaudio_fe_links); i++) { + if (ma->cfg->fe_cpu_is_platform) + card->dai_link[i].num_platforms = 0; + else + card->dai_link[i].platforms->of_node = platform; + } /* Skip the speaker sense PCM link if this amp has no sense (or no speakers) */ if (!ma->has_sense) { @@ -1108,6 +1246,10 @@ static int macaudio_fe_prepare(struct snd_pcm_substream *substream) struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream); struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(rtd->card); + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK && + READ_ONCE(ma->speaker_lock_failed)) + return -EIO; + if (!macaudio_sw_gain_pcm(substream)) return 0; @@ -1260,7 +1402,7 @@ static int macaudio_fe_init(struct snd_soc_pcm_runtime *rtd) (1 << nslots) - 1, nslots, MACAUDIO_SLOTWIDTH); } -static struct snd_soc_jack_pin macaudio_jack_pins[] = { +static const struct snd_soc_jack_pin macaudio_jack_pins[] = { { .pin = "Headphone", .mask = SND_JACK_HEADPHONE, @@ -1278,12 +1420,15 @@ static int macaudio_probe(struct snd_soc_card *card) dev_dbg(card->dev, "%s!\n", __func__); + BUILD_BUG_ON(sizeof(ma->jack_pins) != sizeof(macaudio_jack_pins)); + memcpy(ma->jack_pins, macaudio_jack_pins, sizeof(ma->jack_pins)); + ret = snd_soc_card_jack_new_pins(card, "Headphone Jack", SND_JACK_HEADSET | SND_JACK_HEADPHONE | SND_JACK_BTN_0 | SND_JACK_BTN_1 | SND_JACK_BTN_2 | SND_JACK_BTN_3, - &ma->jack, macaudio_jack_pins, - ARRAY_SIZE(macaudio_jack_pins)); + &ma->jack, ma->jack_pins, + ARRAY_SIZE(ma->jack_pins)); if (ret < 0) { dev_err(card->dev, "jack creation failed: %d\n", ret); return ret; @@ -1422,6 +1567,15 @@ static int macaudio_late_probe(struct snd_soc_card *card) mutex_unlock(&ma->volume_lock_mutex); } + /* The notifier walks the card's controls: only while the card is bound */ + return register_pm_notifier(&ma->pm_nb); +} + +static int macaudio_card_remove(struct snd_soc_card *card) +{ + struct macaudio_snd_data *ma = snd_soc_card_get_drvdata(card); + + unregister_pm_notifier(&ma->pm_nb); return 0; } @@ -1510,6 +1664,11 @@ static int macaudio_fixup_controls(struct snd_soc_card *card) "Apple%s", p + 1); } + /* Before the card registers: userspace never sees the codec's full range */ + if (ma->cfg->jack_max_vol) + CHECK(snd_soc_limit_volume, "Jack DAC Playback Volume", + ma->cfg->jack_max_vol, 1); + if (!ma->has_speakers) return 0; @@ -1640,6 +1799,12 @@ static int macaudio_slk_put(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_v return -ETIMEDOUT; } + /* Between suspend-prepare and the freeze: the lease starts again after the resume */ + if (ma->pm_quiesced) { + mutex_unlock(&ma->volume_lock_mutex); + return 0; + } + cancel_delayed_work(&ma->lock_timeout_work); ma->speaker_lock_remain = ms_to_ktime(SPEAKER_LOCK_TIMEOUT); @@ -1837,6 +2002,51 @@ struct macaudio_platform_cfg macaudio_j493_cfg = { true, AMP_SN012776, SPKR_2W, true, 15, -20, }; +/* + * J616s (T6040, MacBook Pro 16" M4 Pro): the AOP firmware runs MCA cluster + * 0, so the CPU component is the AOP audio driver (front-ends j616-pcm-N, + * back-end spkr). One serializer carries all six SN012776 amps in a 250-bit + * frame at 12 MHz: six 32-bit playback slots, twelve 16-bit sense slots. The + * amps take I2S with an inverted frame clock and a normal bit clock, as on + * the J713 (T8132: the same amp and frame). + */ +struct macaudio_platform_cfg macaudio_j616_cfg = { + .enable_speakers = true, + .amp = AMP_SN012776, + .speakers = SPKR_2W1T, + .stereo = true, + .amp_gain = 15, + .safe_vol = -20, + .fe_dai_names = { "j616-pcm-0", "j616-pcm-1", "j616-pcm-2" }, + .dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_CBC_CFC | + SND_SOC_DAIFMT_GATED | SND_SOC_DAIFMT_NB_IF, + .secondary_bclk_ratio = 250, + .fe_cpu_is_platform = true, +}; + +/* + * The J616s headphone jack, a card of its own: a CS42L84 on the AOP's + * "jack" back-end, which the firmware runs on MCA cluster 2 as a 64-SCLK + * frame at 48 kHz (3.072 MHz, the primary front-end's ratio) with a + * one-SCLK FSYNC pulse, two 32-bit slots and data launched on the rising + * SCLK edge: DSP_A with an inverted bit clock, as on the J700. A card binds + * only once all of its codecs have probed; apart from the speaker card, a + * codec that never answers costs the jack only. The second front-end only + * satisfies the fixed routes: the AOP component refuses to open it. + */ +struct macaudio_platform_cfg macaudio_j616_jack_cfg = { + .fe_dai_names = { "j616-jack-pcm-0", "j616-jack-pcm-1" }, + .dai_fmt = SND_SOC_DAIFMT_DSP_A | SND_SOC_DAIFMT_CBC_CFC | SND_SOC_DAIFMT_IB_NF, + .fe_cpu_is_platform = true, + /* + * Above 0 dB the DAC only adds digital gain, which clips a full-scale + * signal. The 6 dB below that are margin: the analog full scale of + * this board's headphone output is unmeasured, and macOS adapts it to + * the impedance it detects, which this driver does not. + */ + .jack_max_vol = CS42L84_DAC_DB(-6), +}; + struct macaudio_platform_cfg macaudio_fallback_cfg = { false, AMP_NONE, SPKR_NONE, false, 0, 0, }; @@ -1896,6 +2106,10 @@ static const struct of_device_id macaudio_snd_device_id[] = { /* j575 AID33 sn012776 15 1x 1W Compat: apple,j375-macaudio */ /* j613 AID20 sn012776 15 2x 1W+1T Compat: apple,j413-macaudio */ /* j615 AID21 sn012776 15 2x 2W+1T Compat: apple,j415-macaudio */ + /* j616 AID35 sn012776 15 2x 2W+1T (AOP-fed, one serializer) */ + { .compatible = "apple,j616-macaudio", .data = &macaudio_j616_cfg }, + /* j616 AID35 cs42l84 - the jack, a card of its own (AOP-fed) */ + { .compatible = "apple,j616-macaudio-jack", .data = &macaudio_j616_jack_cfg }, /* j700 AID44 max98360a - 2x 1W (AOP-fed, software gain) */ { .compatible = "apple,j700-macaudio", .data = &macaudio_j700_cfg }, /* Fallback, jack only */ @@ -1937,6 +2151,7 @@ static int macaudio_snd_platform_probe(struct platform_device *pdev) card->num_controls = ARRAY_SIZE(macaudio_controls); card->probe = macaudio_probe; card->late_probe = macaudio_late_probe; + card->remove = macaudio_card_remove; card->component_chaining = true; card->fully_routed = true; @@ -1951,6 +2166,14 @@ static int macaudio_snd_platform_probe(struct platform_device *pdev) if (ret) return ret; + /* + * Without sense the PCM2 link is gone, and a card whose front-ends + * are not the MCA's (the J616 jack) has no PCM2 RX widget at all. + * The sense route is the table's last entry. + */ + if (!data->has_sense) + card->num_dapm_routes--; + /* Remove useless controls */ if (!data->has_speakers) /* No speakers, remove both */ card->num_controls -= MACAUDIO_NUM_SPEAKER_CONTROLS; @@ -1973,10 +2196,12 @@ static int macaudio_snd_platform_probe(struct platform_device *pdev) INIT_WORK(&data->lock_update_work, macaudio_vlimit_update_work); INIT_DELAYED_WORK(&data->lock_timeout_work, macaudio_vlimit_timeout_work); INIT_WORK(&data->sw_gain_stop_work, macaudio_sw_gain_stop_work); + INIT_WORK(&data->lock_fail_work, macaudio_lock_fail_work); atomic_set(&data->sw_gain_lock_epoch, 0); atomic_set(&data->sw_gain_prepared_epoch, -1); atomic_set(&data->sw_gain_limit_failed, 0); + data->pm_nb.notifier_call = macaudio_pm_notify; return snd_soc_register_card(card); } @@ -1992,6 +2217,7 @@ static void macaudio_snd_platform_remove(struct platform_device *pdev) cancel_work_sync(&ma->lock_update_work); cancel_delayed_work_sync(&ma->lock_timeout_work); cancel_work_sync(&ma->sw_gain_stop_work); + cancel_work_sync(&ma->lock_fail_work); } static struct platform_driver macaudio_snd_driver = { diff --git a/sound/soc/codecs/cs42l84.c b/sound/soc/codecs/cs42l84.c index 2c89dc4e5ba04..3e13f59cb569e 100644 --- a/sound/soc/codecs/cs42l84.c +++ b/sound/soc/codecs/cs42l84.c @@ -52,6 +52,7 @@ struct cs42l84_private { u8 pll_mclk_f; u32 srate; u8 stream_use; + bool dsp_a; int hs_type; bool remote_active; unsigned int remote_btn1_prev; @@ -483,6 +484,11 @@ static int cs42l84_pll_config(struct snd_soc_component *component) static int cs42l84_set_dai_fmt(struct snd_soc_dai *codec_dai, unsigned int fmt) { + struct snd_soc_component *component = codec_dai->component; + struct cs42l84_private *cs42l84 = snd_soc_component_get_drvdata(component); + unsigned int inv = fmt & SND_SOC_DAIFMT_INV_MASK; + unsigned int ch2_edge; + switch (fmt & SND_SOC_DAIFMT_CLOCK_PROVIDER_MASK) { case SND_SOC_DAIFMT_BC_FC: break; @@ -490,30 +496,60 @@ static int cs42l84_set_dai_fmt(struct snd_soc_dai *codec_dai, unsigned int fmt) return -EINVAL; } + /* + * Both formats have the same bus polarities: data one bit clock after + * the rising FSYNC edge, which ASoC calls IB_IF for I2S and IB_NF for + * DSP_A. In I2S each channel follows its own FSYNC edge, so the frame + * must be 50 %. DSP_A takes a pulse frame: in TDM mode both channels + * follow the frame-start edge, one slot apart (set in hw_params). + */ switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) { case SND_SOC_DAIFMT_I2S: + if (inv != SND_SOC_DAIFMT_IB_IF) + return -EINVAL; + cs42l84->dsp_a = false; break; - default: - return -EINVAL; - } - - /* Bitclock/frame inversion */ - switch (fmt & SND_SOC_DAIFMT_INV_MASK) { - case SND_SOC_DAIFMT_IB_IF: + case SND_SOC_DAIFMT_DSP_A: + if (inv != SND_SOC_DAIFMT_IB_NF) + return -EINVAL; + cs42l84->dsp_a = true; break; default: return -EINVAL; } + ch2_edge = cs42l84->dsp_a ? 0 : CS42L84_ASP_RX_CHx_CTL1_EDGE; + snd_soc_component_update_bits(component, CS42L84_ASP_CTL, + CS42L84_ASP_CTL_TDM_MODE, + cs42l84->dsp_a ? CS42L84_ASP_CTL_TDM_MODE : 0); + snd_soc_component_update_bits(component, CS42L84_ASP_RX_CH2_CTL1, + CS42L84_ASP_RX_CHx_CTL1_EDGE, ch2_edge); + snd_soc_component_update_bits(component, CS42L84_ASP_TX_CH2_CTL1, + CS42L84_ASP_RX_CHx_CTL1_EDGE, ch2_edge); + return 0; } +/* The first bit clock of a channel's slot, counted from its FSYNC edge. */ +static void cs42l84_set_slot_start(struct snd_soc_component *component, + unsigned int ctl1, unsigned int ctl2, + unsigned int bit) +{ + snd_soc_component_update_bits(component, ctl1, + CS42L84_ASP_RX_CHx_CTL1_SLOT_START_LSB, + FIELD_PREP(CS42L84_ASP_RX_CHx_CTL1_SLOT_START_LSB, bit)); + snd_soc_component_update_bits(component, ctl2, + CS42L84_ASP_RX_CHx_CTL2_SLOT_START_MSB, + FIELD_PREP(CS42L84_ASP_RX_CHx_CTL2_SLOT_START_MSB, bit >> 7)); +} + static int cs42l84_pcm_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 cs42l84_private *cs42l84 = snd_soc_component_get_drvdata(component); + unsigned int ch2_start; int ret; u32 ccm_samp_rate; @@ -548,12 +584,17 @@ static int cs42l84_pcm_hw_params(struct snd_pcm_substream *substream, snd_soc_component_write(component, CS42L84_CCM_SAMP_RATE, ccm_samp_rate); + /* DSP_A: channel 2 starts one slot after channel 1 */ + ch2_start = cs42l84->dsp_a ? params_physical_width(params) : 0; + switch (substream->stream) { case SNDRV_PCM_STREAM_PLAYBACK: snd_soc_component_write(component, CS42L84_ASP_RX_CH1_WIDTH, params_width(params) - 1); snd_soc_component_write(component, CS42L84_ASP_RX_CH2_WIDTH, params_width(params) - 1); + cs42l84_set_slot_start(component, CS42L84_ASP_RX_CH2_CTL1, + CS42L84_ASP_RX_CH2_CTL2, ch2_start); break; case SNDRV_PCM_STREAM_CAPTURE: @@ -561,6 +602,8 @@ static int cs42l84_pcm_hw_params(struct snd_pcm_substream *substream, params_width(params) - 1); snd_soc_component_write(component, CS42L84_ASP_TX_CH2_WIDTH, params_width(params) - 1); + cs42l84_set_slot_start(component, CS42L84_ASP_TX_CH2_CTL1, + CS42L84_ASP_TX_CH2_CTL2, ch2_start); break; } diff --git a/sound/soc/codecs/tas2764.c b/sound/soc/codecs/tas2764.c index 5416beee47829..a6e4bda499883 100644 --- a/sound/soc/codecs/tas2764.c +++ b/sound/soc/codecs/tas2764.c @@ -11,6 +11,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -30,6 +33,24 @@ enum tas2764_devid { DEVID_SN012776 = 1 }; +/* + * Amps that share one SDZ supply and carry "apple,sdz-group-fail-closed" form + * a group. An over-temperature or over-current fault on any of them forces the + * supply off, which puts every amp of the group in hardware shutdown, and no + * amp of the group enables the supply again until the driver is reloaded. The + * supply's own enable count cannot do this: one amp's disable leaves a shared + * line up for the others. + */ +struct tas2764_sdz_group { + struct list_head node; + struct device_node *supply; /* the SDZ-supply node: the group's key */ + refcount_t users; + bool failed; +}; + +static LIST_HEAD(tas2764_sdz_groups); +static DEFINE_MUTEX(tas2764_sdz_groups_lock); + struct tas2764_priv { struct snd_soc_component *component; struct gpio_desc *reset_gpio; @@ -45,6 +66,7 @@ struct tas2764_priv { bool dac_powered; bool unmuted; + struct tas2764_sdz_group *sdz_group; struct { int tx_mode; @@ -54,6 +76,97 @@ struct tas2764_priv { #include "tas2764-quirks.h" +/* LTCH0 faults that shut the whole SDZ group down: over temperature, over current */ +#define TAS2764_INT_LTCH0_GROUP_FAULTS (BIT(0) | BIT(1)) + +static bool tas2764_group_failed(struct tas2764_priv *tas2764) +{ + return tas2764->sdz_group && READ_ONCE(tas2764->sdz_group->failed); +} + +/* Force the shared SDZ supply off for the whole group; idempotent. */ +static void tas2764_group_shutdown(struct tas2764_priv *tas2764, unsigned int cause) +{ + struct tas2764_sdz_group *group = tas2764->sdz_group; + int ret; + + /* Under tas2764_sdz_enable()'s lock: no amp can enable the supply in between */ + mutex_lock(&tas2764_sdz_groups_lock); + WRITE_ONCE(group->failed, true); + ret = regulator_force_disable(tas2764->sdz_reg); + mutex_unlock(&tas2764_sdz_groups_lock); + dev_crit(tas2764->dev, "SDZ group shut down (cause %#x): %s\n", cause, + ret ? "forcing the supply off FAILED" : + "every amp of the group is in hardware shutdown"); +} + +/* Enable the SDZ supply unless the amp's group failed; the check and the enable are one step. */ +static int tas2764_sdz_enable(struct tas2764_priv *tas2764) +{ + int ret; + + if (!tas2764->sdz_group) + return regulator_enable(tas2764->sdz_reg); + + mutex_lock(&tas2764_sdz_groups_lock); + ret = READ_ONCE(tas2764->sdz_group->failed) ? -EIO : regulator_enable(tas2764->sdz_reg); + mutex_unlock(&tas2764_sdz_groups_lock); + return ret; +} + +static void tas2764_group_leave(void *data) +{ + struct tas2764_sdz_group *group = data; + + mutex_lock(&tas2764_sdz_groups_lock); + if (refcount_dec_and_test(&group->users)) { + list_del(&group->node); + of_node_put(group->supply); + kfree(group); + } + mutex_unlock(&tas2764_sdz_groups_lock); +} + +static int tas2764_group_join(struct device *dev, struct tas2764_priv *tas2764) +{ + struct tas2764_sdz_group *group; + struct device_node *supply; + + if (!of_property_read_bool(dev->of_node, "apple,sdz-group-fail-closed")) + return 0; + /* A group acts on the shared supply: an amp's own shutdown GPIO would bypass it */ + if (tas2764->sdz_gpio) + return dev_err_probe(dev, -EINVAL, "SDZ group with a shutdown GPIO of its own\n"); + /* Without its fault interrupt the amp could never take the group down */ + if (!tas2764->irq) + return dev_err_probe(dev, -EINVAL, "SDZ group without a fault interrupt\n"); + supply = of_parse_phandle(dev->of_node, "SDZ-supply", 0); + if (!supply) + return dev_err_probe(dev, -EINVAL, "SDZ group without an SDZ supply\n"); + + mutex_lock(&tas2764_sdz_groups_lock); + list_for_each_entry(group, &tas2764_sdz_groups, node) { + if (group->supply == supply) { + refcount_inc(&group->users); + of_node_put(supply); + goto found; + } + } + group = kzalloc(sizeof(*group), GFP_KERNEL); + if (!group) { + mutex_unlock(&tas2764_sdz_groups_lock); + of_node_put(supply); + return -ENOMEM; + } + group->supply = supply; + refcount_set(&group->users, 1); + list_add(&group->node, &tas2764_sdz_groups); +found: + mutex_unlock(&tas2764_sdz_groups_lock); + tas2764->sdz_group = group; + return devm_add_action_or_reset(dev, tas2764_group_leave, group); +} + static const char *tas2764_int_ltch0_msgs[8] = { "fault: over temperature", /* INT_LTCH0 & BIT(0) */ "fault: over current", @@ -99,6 +212,9 @@ static irqreturn_t tas2764_irq(int irq, void *data) } } + if ((latched[0] & TAS2764_INT_LTCH0_GROUP_FAULTS) && tas2764->sdz_group) + tas2764_group_shutdown(tas2764, latched[0]); + if (latched[0]) { dev_err_ratelimited(tas2764->dev, "other context to the fault: %02x,%02x,%02x,%02x,%02x", latched[1], latched[2], latched[3], latched[4], latched[5]); @@ -131,6 +247,9 @@ static int tas2764_update_pwr_ctrl(struct tas2764_priv *tas2764) unsigned int val; int ret; + if (tas2764_group_failed(tas2764)) + return -EIO; + if (tas2764->dac_powered) val = tas2764->unmuted ? TAS2764_PWR_CTRL_ACTIVE : TAS2764_PWR_CTRL_MUTE; @@ -149,6 +268,13 @@ static int tas2764_update_pwr_ctrl(struct tas2764_priv *tas2764) } #ifdef CONFIG_PM +/* + * System sleep leaves the SDZ line high: the amp waits in software shutdown, as it does between + * streams, and keeps its registers. Pulling SDZ low here meant a power-on reset at resume, and + * soc_resume_deferred() unmutes the amps of a running stream a few milliseconds later: their I/V + * sense then read a negative average power for several seconds, and the uncached page-0xfd quirk + * writes were lost. After cold boot no amp is unmuted that fresh out of reset. + */ static int tas2764_codec_suspend(struct snd_soc_component *component) { struct tas2764_priv *tas2764 = snd_soc_component_get_drvdata(component); @@ -161,11 +287,6 @@ static int tas2764_codec_suspend(struct snd_soc_component *component) if (ret < 0) return ret; - if (tas2764->sdz_gpio) - gpiod_set_value_cansleep(tas2764->sdz_gpio, 0); - - regulator_disable(tas2764->sdz_reg); - regcache_cache_only(tas2764->regmap, true); regcache_mark_dirty(tas2764->regmap); @@ -179,19 +300,6 @@ static int tas2764_codec_resume(struct snd_soc_component *component) struct tas2764_priv *tas2764 = snd_soc_component_get_drvdata(component); int ret; - ret = regulator_enable(tas2764->sdz_reg); - - if (ret) { - dev_err(tas2764->dev, "Failed to enable regulator\n"); - return ret; - } - - if (tas2764->sdz_gpio) { - gpiod_set_value_cansleep(tas2764->sdz_gpio, 1); - } - - usleep_range(1000, 2000); - regcache_cache_only(tas2764->regmap, false); ret = regcache_sync(tas2764->regmap); @@ -245,6 +353,16 @@ static const struct snd_soc_dapm_route tas2764_audio_map[] = { {"VSENSE", "Switch", "VMON"}, }; +/* A failed SDZ group refuses every stream until the driver is reloaded */ +static int tas2764_startup(struct snd_pcm_substream *substream, struct snd_soc_dai *dai) +{ + struct tas2764_priv *tas2764 = snd_soc_component_get_drvdata(dai->component); + + if (tas2764_group_failed(tas2764)) + return -EIO; + return 0; +} + static int tas2764_mute(struct snd_soc_dai *dai, int mute, int direction) { struct tas2764_priv *tas2764 = @@ -606,6 +724,7 @@ static int tas2764_set_bclk_ratio(struct snd_soc_dai *dai, unsigned int ratio) } static const struct snd_soc_dai_ops tas2764_dai_ops = { + .startup = tas2764_startup, .mute_stream = tas2764_mute, .hw_params = tas2764_hw_params, .set_fmt = tas2764_set_fmt, @@ -776,7 +895,7 @@ static int tas2764_codec_probe(struct snd_soc_component *component) tas2764->component = component; - ret = regulator_enable(tas2764->sdz_reg); + ret = tas2764_sdz_enable(tas2764); if (ret != 0) { dev_err(tas2764->dev, "Failed to enable regulator: %d\n", ret); return ret; @@ -815,6 +934,8 @@ static int tas2764_codec_probe(struct snd_soc_component *component) ret = devm_request_threaded_irq(tas2764->dev, tas2764->irq, NULL, tas2764_irq, IRQF_ONESHOT | IRQF_SHARED | IRQF_TRIGGER_LOW, "tas2764", tas2764); + if (ret && tas2764->sdz_group) + return dev_err_probe(tas2764->dev, ret, "SDZ group needs its fault IRQ\n"); if (ret) dev_warn(tas2764->dev, "failed to request IRQ: %d\n", ret); } @@ -999,7 +1120,7 @@ static int tas2764_parse_dt(struct device *dev, struct tas2764_priv *tas2764) if (ret) tas2764->v_sense_slot = -1; - return 0; + return tas2764_group_join(dev, tas2764); } static int tas2764_i2c_probe(struct i2c_client *client)