diff --git a/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml b/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml index add726470307d..eb6d3f5580761 100644 --- a/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml +++ b/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml @@ -76,6 +76,14 @@ properties: AOP setup-port mailbox with a separate register layout. const: apple,t8140-aop-setup-mailbox + - description: + Later AOP setup-port mailboxes with the T8140 register layout. + items: + - enum: + - apple,t6050-aop-setup-mailbox + - apple,t8142-aop-setup-mailbox + - const: apple,t8140-aop-setup-mailbox + - description: An older ASC mailbox interface found on T2 and A11 that is also used for the NVMe coprocessor and the system management diff --git a/Documentation/devicetree/bindings/sound/apple,macaudio.yaml b/Documentation/devicetree/bindings/sound/apple,macaudio.yaml index 0d9004fab9c1d..f6e098134ab27 100644 --- a/Documentation/devicetree/bindings/sound/apple,macaudio.yaml +++ b/Documentation/devicetree/bindings/sound/apple,macaudio.yaml @@ -51,6 +51,8 @@ properties: - apple,j715-macaudio-jack - apple,j773-macaudio - apple,j773-macaudio-jack + - apple,j813-macaudio + - apple,j873-macaudio - const: apple,macaudio - description: Machines that share the speaker and jack layout of an earlier diff --git a/Documentation/devicetree/bindings/sound/ti,tas27xx.yaml b/Documentation/devicetree/bindings/sound/ti,tas27xx.yaml index 1f6bd7b4635b6..6071212d17c1e 100644 --- a/Documentation/devicetree/bindings/sound/ti,tas27xx.yaml +++ b/Documentation/devicetree/bindings/sound/ti,tas27xx.yaml @@ -122,6 +122,14 @@ properties: $ref: /schemas/types.yaml#/definitions/uint32 description: TDM TX voltage sense time slot. + ti,bclk-ratio: + $ref: /schemas/types.yaml#/definitions/uint32 + enum: [250] + description: + Fixed BCLK/FS ratio of the TDM frame, for a frame the amplifier's ratio + auto-detection does not accept (250 BCLKs on the Mac mini (M6, J873) + speaker). + '#sound-dai-cells': # The codec has a single DAI, the #sound-dai-cells=<1>; case is left in for backward # compatibility but is deprecated. diff --git a/arch/arm64/boot/dts/apple/adt-coverage/t6050-j716s.txt b/arch/arm64/boot/dts/apple/adt-coverage/t6050-j716s.txt index 82218ccb772ab..d2d99f8601003 100644 --- a/arch/arm64/boot/dts/apple/adt-coverage/t6050-j716s.txt +++ b/arch/arm64/boot/dts/apple/adt-coverage/t6050-j716s.txt @@ -453,7 +453,7 @@ /arm-io/dart-aop/mapper-admac-leap-s - -> /soc@200000000/iommu@290fc0000 /arm-io/dart-aop/mapper-admac-base-s - -> /soc@200000000/iommu@290fc0000 /arm-io/dart-aop/mapper-scm - -> /soc@200000000/iommu@290fc0000 -/arm-io/aop-exclave-mailbox 0x4d067c000 -> /soc/mailbox@4d067c000 +/arm-io/aop-exclave-mailbox 0x2d067c000 -> /soc/mailbox@2d067c000 /arm-io/aop-exclave-ioreporting - -> MISSING /arm-io/aop2 0x291600000 -> /soc/aop@291600000 /arm-io/aop2/iop-aop2-nub - -> /soc/aop@291600000 diff --git a/arch/arm64/boot/dts/apple/adt-coverage/t6050-j873s.txt b/arch/arm64/boot/dts/apple/adt-coverage/t6050-j873s.txt index c014ce038d1f0..b155feb7c8e31 100644 --- a/arch/arm64/boot/dts/apple/adt-coverage/t6050-j873s.txt +++ b/arch/arm64/boot/dts/apple/adt-coverage/t6050-j873s.txt @@ -377,7 +377,7 @@ /arm-io/dart-aop/mapper-admac-leap-s - -> /soc@200000000/iommu@290fc0000 /arm-io/dart-aop/mapper-admac-base-s - -> /soc@200000000/iommu@290fc0000 /arm-io/dart-aop/mapper-scm - -> /soc@200000000/iommu@290fc0000 -/arm-io/aop-exclave-mailbox 0x4d067c000 -> /soc/mailbox@4d067c000 +/arm-io/aop-exclave-mailbox 0x2d067c000 -> /soc/mailbox@2d067c000 /arm-io/aop-exclave-ioreporting - -> MISSING /arm-io/aop2 0x291600000 -> /soc/aop@291600000 /arm-io/aop2/iop-aop2-nub - -> /soc/aop@291600000 diff --git a/arch/arm64/boot/dts/apple/t6050-die0.dtsi b/arch/arm64/boot/dts/apple/t6050-die0.dtsi index 5b140884a52a0..7a93a3592bd5f 100644 --- a/arch/arm64/boot/dts/apple/t6050-die0.dtsi +++ b/arch/arm64/boot/dts/apple/t6050-die0.dtsi @@ -650,9 +650,9 @@ /* * The AOP's audio path: the AP GPIO block, the I2C1/I2C2 buses and their power states (in * pmgr1, below), and the AOP with its mailbox, dart-aop and the ADMAC its audio streams use. - * From the ADT (arm-io ranges +0x200000000). The AOP, its mailbox, dart-aop and the ADMAC are disabled: - * iBoot leaves the AOP halted on locked dart-aop tables, and Aura enables them only when its - * checks of that state pass (chosen.aurora,t6050-aop=1). + * From the ADT (arm-io ranges +0x200000000). The AOP, its mailboxes, dart-aop and the ADMAC are + * disabled here: iBoot leaves the AOP halted on locked dart-aop tables, and the boards that + * boot it enable them. */ pinctrl_ap: pinctrl@29c000000 { compatible = "apple,t6050-pinctrl", "apple,t8103-pinctrl"; @@ -790,17 +790,15 @@ /* * The AOP: its SRAM window (ADT reg[2]) and its ASC (reg[0]). Its firmware is loaded and - * left halted by iBoot. Booted as on T8152, over the RTKit mailbox alone: the ADT's - * aop-exclave-mailbox (the setup port T8140 and T8142 boot through) is exclave-assigned - * here and takes an SError from the AP. + * left halted by iBoot. Booted as on T8142, over the RTKit mailbox and the setup port. */ aop: aop@290c00000 { compatible = "apple,t6050-aop", "apple,t8152-aop"; reg = <0x2 0x90c00000 0x0 0x1e0000>, <0x2 0x90600000 0x0 0x4000>; reg-names = "mmio", "asc"; - mboxes = <&aop_mbox>; - mbox-names = "mbox"; + mboxes = <&aop_mbox>, <&aop_setup_mbox>; + mbox-names = "mbox", "setup"; iommus = <&aop_dart 0>; apple,dma-range = <0x110 0x0 0x10 0x0>; status = "disabled"; diff --git a/arch/arm64/boot/dts/apple/t6050-j714s.dts b/arch/arm64/boot/dts/apple/t6050-j714s.dts index f599a4e16bf1c..f2bdb7d0a1985 100644 --- a/arch/arm64/boot/dts/apple/t6050-j714s.dts +++ b/arch/arm64/boot/dts/apple/t6050-j714s.dts @@ -450,3 +450,20 @@ &i2c2 { status = "disabled"; }; + +/* The AOP, booted through its setup port, with its sensor and audio services. */ +&aop_mbox { + status = "okay"; +}; + +&aop_setup_mbox { + status = "okay"; +}; + +&aop_dart { + status = "okay"; +}; + +&aop { + status = "okay"; +}; diff --git a/arch/arm64/boot/dts/apple/t6050-peripherals.dtsi b/arch/arm64/boot/dts/apple/t6050-peripherals.dtsi index cfdae30f9062e..fdc1e2fe16d85 100644 --- a/arch/arm64/boot/dts/apple/t6050-peripherals.dtsi +++ b/arch/arm64/boot/dts/apple/t6050-peripherals.dtsi @@ -1965,12 +1965,20 @@ status = "disabled"; }; - aop_exclave_mailbox: mailbox@4d067c000 { - compatible = "apple,t6050-secure-mailbox", "apple,secure-mailbox"; - reg = <0x4 0xd067c000 0x0 0x4000>; - interrupts = , - ; + /* + * The AOP setup port, ADT aop-exclave-mailbox. Its ADT reg is not behind the arm-io + * translation: AOP ASC + 0x4007c000, as on T8140 and T8142. ADT interrupts [585, 582]. + * dart-aop stream 1: ADT vm-base-1 0x20000000000, vm-size-1 0x2000000000. + */ + aop_setup_mbox: mailbox@2d067c000 { + compatible = "apple,t6050-aop-setup-mailbox", "apple,t8140-aop-setup-mailbox"; + reg = <0x2 0xd067c000 0x0 0x4000>; + interrupts = , + ; + interrupt-names = "send-empty", "recv-not-empty"; iommus = <&aop_dart 1>; + apple,dma-range = <0x200 0x0 0x20 0x0>; + #mbox-cells = <0>; status = "disabled"; }; diff --git a/arch/arm64/configs/asahi.config b/arch/arm64/configs/asahi.config index 5581318240726..178fb4c200184 100644 --- a/arch/arm64/configs/asahi.config +++ b/arch/arm64/configs/asahi.config @@ -75,6 +75,7 @@ CONFIG_DRM_ACCEL_ANE=m CONFIG_DRM_SIMPLEDRM=y CONFIG_SYSFB_SIMPLEFB=y CONFIG_SND_SOC_APPLE_AOP_AUDIO=m +CONFIG_SND_SOC_APPLE_T8152_AOP_AUDIO=m CONFIG_SND_SOC_APPLE_MCA=m CONFIG_SND_SOC_APPLE_MACAUDIO=m CONFIG_SND_SOC_CS42L83=m diff --git a/drivers/dma/apple-admac-test.h b/drivers/dma/apple-admac-test.h index 057a7c3597e7a..24d390b9d6d5b 100644 --- a/drivers/dma/apple-admac-test.h +++ b/drivers/dma/apple-admac-test.h @@ -136,3 +136,64 @@ static struct kunit_suite admac_pos_suite = { }; kunit_test_suite(admac_pos_suite); + +/* + * The paused-capture realignment (admac_cyclic_realign()) is the AOP audio + * driver's alone: it passes the frame size in the peripheral config, for its + * speaker sense capture. The MCA's PCM zeroes its slave config, so every MCA + * channel, and every playback channel, resumes where it paused. + */ +static void admac_realign_needs_a_frame_size(struct kunit *test) +{ + struct dma_slave_config config = {}; + u32 frame = 16; + u64 wrong = 16; + + /* As mca_hw_params() configures a channel */ + config.direction = DMA_DEV_TO_MEM; + config.src_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES; + config.src_port_window_size = 4; + KUNIT_EXPECT_EQ(test, admac_config_realign_bytes(&config), 0); + + config.peripheral_config = &wrong; + config.peripheral_size = sizeof(wrong); + KUNIT_EXPECT_EQ(test, admac_config_realign_bytes(&config), 0); + + config.peripheral_config = &frame; + config.peripheral_size = sizeof(frame); + KUNIT_EXPECT_EQ(test, admac_config_realign_bytes(&config), 16); +} + +static void admac_realign_only_a_configured_capture(struct kunit *test) +{ + struct admac_tx tx = admac_test_tx(0, 0); + struct admac_chan adchan = { .current_tx = &tx }; + + /* Playback (even channels): never, frame size or not */ + adchan.no = 0; + KUNIT_EXPECT_FALSE(test, admac_resume_realigns(&adchan)); + adchan.realign_bytes = 16; + KUNIT_EXPECT_FALSE(test, admac_resume_realigns(&adchan)); + + /* Capture (odd channels): only with a frame size and a transfer */ + adchan.no = 1; + adchan.realign_bytes = 0; + KUNIT_EXPECT_FALSE(test, admac_resume_realigns(&adchan)); + adchan.realign_bytes = 16; + KUNIT_EXPECT_TRUE(test, admac_resume_realigns(&adchan)); + adchan.current_tx = NULL; + KUNIT_EXPECT_FALSE(test, admac_resume_realigns(&adchan)); +} + +static struct kunit_case admac_realign_cases[] = { + KUNIT_CASE(admac_realign_needs_a_frame_size), + KUNIT_CASE(admac_realign_only_a_configured_capture), + {} +}; + +static struct kunit_suite admac_realign_suite = { + .name = "apple-admac-realign", + .test_cases = admac_realign_cases, +}; + +kunit_test_suite(admac_realign_suite); diff --git a/drivers/dma/apple-admac.c b/drivers/dma/apple-admac.c index b7336f97ee9fc..939e5d5e7bdb9 100644 --- a/drivers/dma/apple-admac.c +++ b/drivers/dma/apple-admac.c @@ -128,6 +128,8 @@ struct admac_chan { spinlock_t lock; struct admac_tx *current_tx; int nperiod_acks; + /* frame size a paused capture restarts aligned to on resume, or 0 */ + u32 realign_bytes; /* * We maintain a 'submitted' and 'issued' list mainly for interface @@ -340,6 +342,21 @@ static struct dma_async_tx_descriptor *admac_prep_dma_cyclic( return &adtx->tx; } +/* + * Write one hardware descriptor. + */ +static void admac_write_hw_desc(struct admac_data *ad, int channo, + dma_addr_t addr, size_t len) +{ + dev_dbg(ad->dev, "ch%d descriptor: addr=0x%pad len=0x%zx flags=0x%lx\n", + channo, &addr, len, FLAG_DESC_NOTIFY); + + writel_relaxed(lower_32_bits(addr), ad->base + REG_DESC_WRITE(channo)); + writel_relaxed(upper_32_bits(addr), ad->base + REG_DESC_WRITE(channo)); + writel_relaxed(len, ad->base + REG_DESC_WRITE(channo)); + writel_relaxed(FLAG_DESC_NOTIFY, ad->base + REG_DESC_WRITE(channo)); +} + /* * Write one hardware descriptor for a dmaengine cyclic transaction. */ @@ -356,13 +373,7 @@ static bool admac_cyclic_write_one_desc(struct admac_data *ad, int channo, /* If happens means we have buggy code */ WARN_ON_ONCE(addr + tx->period_len > tx->buf_end); - dev_dbg(ad->dev, "ch%d descriptor: addr=0x%pad len=0x%zx flags=0x%lx\n", - channo, &addr, tx->period_len, FLAG_DESC_NOTIFY); - - writel_relaxed(lower_32_bits(addr), ad->base + REG_DESC_WRITE(channo)); - writel_relaxed(upper_32_bits(addr), ad->base + REG_DESC_WRITE(channo)); - writel_relaxed(tx->period_len, ad->base + REG_DESC_WRITE(channo)); - writel_relaxed(FLAG_DESC_NOTIFY, ad->base + REG_DESC_WRITE(channo)); + admac_write_hw_desc(ad, channo, addr, tx->period_len); /* Include a descriptor already consumed while the ring is refilled. */ tx->nperiods_outstanding++; @@ -679,6 +690,73 @@ static int admac_pause(struct dma_chan *chan) return 0; } +static int admac_drain_reports(struct admac_data *ad, int channo); + +/* + * Restart a paused cyclic capture at the next frame boundary of the current + * period. A source that stopped in the middle of a frame (a serializer that + * firmware stops whenever it likes) leaves a partial frame in the buffer, and + * without this every later frame would be off by its words. Called with the + * channel stopped and the lock held. + */ +static void admac_cyclic_realign(struct admac_chan *adchan) +{ + struct admac_data *ad = adchan->host; + struct admac_tx *tx = adchan->current_tx; + int ch = adchan->no; + size_t done, aligned; + int nreports; + + /* Periods completed before the pause */ + nreports = admac_drain_reports(ad, ch); + tx->nperiods_outstanding -= min_t(unsigned int, nreports, tx->nperiods_outstanding); + adchan->nperiod_acks += nreports; + tx->reclaimed_pos += nreports * tx->period_len; + tx->reclaimed_pos %= 2 * tx->buf_len; + + done = tx->period_len - + min_t(size_t, readl_relaxed(ad->base + REG_RESIDUE(ad, ch)), tx->period_len); + aligned = roundup(done, adchan->realign_bytes); + if (aligned >= tx->period_len) { + adchan->nperiod_acks++; + tx->reclaimed_pos += tx->period_len; + tx->reclaimed_pos %= 2 * tx->buf_len; + aligned = 0; + } + if (adchan->nperiod_acks) + tasklet_schedule(&adchan->tasklet); + + dev_dbg(ad->dev, "ch%d realign: %zu bytes into the period, restart at %zu\n", + ch, done, aligned); + + admac_reset_rings(adchan); + writel_relaxed(0, ad->base + REG_CHAN_CTL(ad, ch)); + /* The reset dropped every descriptor still in the ring. */ + tx->nperiods_outstanding = 0; + + /* The rest of the current period, then whole periods as before */ + tx->submitted_pos = tx->reclaimed_pos; + admac_write_hw_desc(ad, ch, tx->buf_addr + (tx->submitted_pos % tx->buf_len) + aligned, + tx->period_len - aligned); + tx->nperiods_outstanding++; + tx->submitted_pos += tx->period_len; + tx->submitted_pos %= 2 * tx->buf_len; + + admac_start_chan(adchan); + admac_cyclic_write_desc(ad, ch, tx); +} + +/* + * Whether a resume restarts the channel aligned (admac_cyclic_realign()): only a + * capture whose client gave a frame size, with a transfer to restart. Every + * other channel, and every playback one, starts again where it paused. + */ +static bool admac_resume_realigns(const struct admac_chan *adchan) +{ + return adchan->realign_bytes && adchan->current_tx && + admac_chan_direction(adchan->no) == DMA_DEV_TO_MEM; +} + static int admac_resume(struct dma_chan *chan) { struct admac_chan *adchan = to_admac_chan(chan); @@ -688,6 +766,8 @@ static int admac_resume(struct dma_chan *chan) spin_lock_irqsave(&adchan->lock, flags); if (adchan->current_tx && adchan->current_tx->error) ret = -EIO; + else if (admac_resume_realigns(adchan)) + admac_cyclic_realign(adchan); else admac_start_chan(adchan); spin_unlock_irqrestore(&adchan->lock, flags); @@ -987,6 +1067,18 @@ static void admac_chan_tasklet(struct tasklet_struct *t) } } +/* + * The frame size a paused capture is restarted aligned to on resume (see + * admac_cyclic_realign()): an optional u32 in bytes in the peripheral config. + * A client that passes none, as the MCA's PCM does, gets 0: no realignment. + */ +static u32 admac_config_realign_bytes(const struct dma_slave_config *config) +{ + if (config->peripheral_config && config->peripheral_size == sizeof(u32)) + return *(const u32 *)config->peripheral_config; + return 0; +} + static int admac_device_config(struct dma_chan *chan, struct dma_slave_config *config) { @@ -1041,6 +1133,8 @@ static int admac_device_config(struct dma_chan *chan, writel_relaxed(bus_width, ad->base + REG_BUS_WIDTH(ad, adchan->no)); + adchan->realign_bytes = admac_config_realign_bytes(config); + /* * By FIFOCTL_LIMIT we seem to set the maximal number of bytes allowed to be * held in controller's per-channel FIFO. Transfers seem to be triggered diff --git a/drivers/soc/apple/aop.rs b/drivers/soc/apple/aop.rs index be2f353c63b10..c062835a60011 100644 --- a/drivers/soc/apple/aop.rs +++ b/drivers/soc/apple/aop.rs @@ -174,6 +174,8 @@ const SETUP_BUFFER_ENTRY_SIZE: usize = 16; /// The service endpoints that request buffers; boot completes once all of /// them have theirs. const SETUP_ENDPOINTS: [u8; 5] = [0x20, 0x21, 0x23, 0x30, 0x32]; +/// Service endpoints a setup port may announce in its EPMAP. +const SETUP_MAX_ENDPOINTS: usize = 16; const SETUP_BOOT_TIMEOUT_MS: u32 = 15000; /// How long the AP power request waits for the last endpoint map fragment. const SETUP_MAP_TIMEOUT_MS: u32 = 1000; @@ -1030,7 +1032,14 @@ struct SetupState { arena: Option>, /// The next unassigned page of the arena. next_page: usize, - endpoints: [Option; SETUP_ENDPOINTS.len()], + endpoints: [Option; SETUP_MAX_ENDPOINTS], + /// The service endpoints that must get buffers: T8140's fixed set, or + /// those the EPMAP announces. + expected: [u8; SETUP_MAX_ENDPOINTS], + expected_count: usize, + epmap_endpoints: bool, + /// Boot also waits for the system endpoints (below 0x20) it maps. + wait_system: bool, map_done: bool, ap_ready: bool, power_sent: bool, @@ -1039,12 +1048,22 @@ struct SetupState { } impl SetupState { - fn new() -> SetupState { + fn new(epmap_endpoints: bool, wait_system: bool) -> SetupState { + let mut expected = [0; SETUP_MAX_ENDPOINTS]; + let mut expected_count = 0; + if !epmap_endpoints { + expected[..SETUP_ENDPOINTS.len()].copy_from_slice(&SETUP_ENDPOINTS); + expected_count = SETUP_ENDPOINTS.len(); + } SetupState { mbox: None, arena: None, next_page: 0, - endpoints: [None; SETUP_ENDPOINTS.len()], + endpoints: [None; SETUP_MAX_ENDPOINTS], + expected, + expected_count, + epmap_endpoints, + wait_system, map_done: false, ap_ready: false, power_sent: false, @@ -1084,9 +1103,17 @@ impl SetupState { } /// Boot is complete once the AP power state is on and every service - /// endpoint has its buffers. + /// endpoint has its buffers. A system endpoint that never asks for one + /// is not waited for unless `wait_system` is set. fn ready(&self) -> bool { - self.ap_ready && self.endpoint_count() == SETUP_ENDPOINTS.len() + self.ap_ready + && self.expected_count != 0 + && self.expected[..self.expected_count] + .iter() + .zip(self.endpoints.iter()) + .all(|(ep, slot)| { + slot.is_some() || (!self.wait_system && *ep < AFK_ENDPOINT_START) + }) } /// Sends one word to a setup-port endpoint. Callers run in process @@ -1384,7 +1411,10 @@ impl AopData { required_endpoints: cfg.required_endpoints, setup_queue, setup_lost: Atomic::new(false), - setup <- new_mutex!(SetupState::new()), + setup <- new_mutex!(SetupState::new( + cfg.setup_epmap_endpoints, + cfg.setup_wait_system_endpoints + )), setup_cv <- new_condvar!(), rtkit <- new_mutex!(None), endpoints <- pin_init::pin_init_array_from_fn(|i| { @@ -2214,6 +2244,24 @@ impl AopData { // Each fragment of the endpoint map is acknowledged by // echoing it. st.send(SETUP_MGMT_EP, word)?; + if st.epmap_endpoints { + // RTKit layout: a 32-endpoint bitmap, its base in + // bits 32..34. Endpoint 0 is this management one. + let base = ((word >> 32) & 7) as u8 * 32; + dev_dbg!(self.dev, "setup port: EPMAP {:#x}\n", word); + for bit in 0..32u8 { + let ep = base + bit; + if word & (1 << bit) != 0 && ep != SETUP_MGMT_EP + && !st.expected[..st.expected_count].contains(&ep) + { + if st.expected_count == SETUP_MAX_ENDPOINTS { + return Err(ENOSPC); + } + st.expected[st.expected_count] = ep; + st.expected_count += 1; + } + } + } if word & SETUP_EPMAP_LAST != 0 { st.map_done = true; } @@ -2270,7 +2318,7 @@ impl AopData { /// from the arena. fn setup_endpoint_request(&self, st: &mut SetupState, ep: u8, word: u64) -> Result<()> { let requested = (word & 0xffff_ffff) as usize; - let slot = SETUP_ENDPOINTS + let slot = st.expected[..st.expected_count] .iter() .position(|e| *e == ep) .ok_or(EINVAL)?; @@ -2519,7 +2567,7 @@ impl AopData { "setup port: boot incomplete (AP power on: {}, endpoints: {}/{}): {:?}", st.ap_ready, st.endpoint_count(), - SETUP_ENDPOINTS.len(), + st.expected_count, e ); return Err(e); @@ -2587,6 +2635,10 @@ struct AopHwConfig { epic_v4: bool, /// The firmware boots through a second, "setup", mailbox as well. setup_port: bool, + /// Its service endpoints are the ones its EPMAP announces, not T8140's. + setup_epmap_endpoints: bool, + /// Its boot waits for the system endpoints the EPMAP names as well. + setup_wait_system_endpoints: bool, /// Shut the co-processor down on unbind and retain its buffers if the /// shutdown cannot be confirmed. quiesce_on_unbind: bool, @@ -2609,6 +2661,8 @@ const HW_CFG_T8103: AopHwConfig = AopHwConfig { patch_bootargs: true, epic_v4: false, setup_port: false, + setup_epmap_endpoints: false, + setup_wait_system_endpoints: true, quiesce_on_unbind: false, required_endpoints: &[], fw_regions: false, @@ -2625,6 +2679,8 @@ const HW_CFG_T8112: AopHwConfig = AopHwConfig { patch_bootargs: true, epic_v4: false, setup_port: false, + setup_epmap_endpoints: false, + setup_wait_system_endpoints: true, quiesce_on_unbind: false, required_endpoints: &[], fw_regions: false, @@ -2640,6 +2696,8 @@ const HW_CFG_T6000: AopHwConfig = AopHwConfig { patch_bootargs: true, epic_v4: false, setup_port: false, + setup_epmap_endpoints: false, + setup_wait_system_endpoints: true, quiesce_on_unbind: false, required_endpoints: &[], fw_regions: false, @@ -2654,6 +2712,8 @@ const HW_CFG_T6020: AopHwConfig = AopHwConfig { patch_bootargs: true, epic_v4: false, setup_port: false, + setup_epmap_endpoints: false, + setup_wait_system_endpoints: true, quiesce_on_unbind: false, required_endpoints: &[], fw_regions: false, @@ -2673,6 +2733,8 @@ const HW_CFG_T6040: AopHwConfig = AopHwConfig { patch_bootargs: true, epic_v4: false, setup_port: false, + setup_epmap_endpoints: false, + setup_wait_system_endpoints: true, quiesce_on_unbind: true, required_endpoints: &[], fw_regions: true, @@ -2696,12 +2758,61 @@ const HW_CFG_T8140: AopHwConfig = AopHwConfig { patch_bootargs: false, epic_v4: true, setup_port: true, + setup_epmap_endpoints: false, + setup_wait_system_endpoints: true, quiesce_on_unbind: true, required_endpoints: &[0x20, 0x21, 0x22, 0x23, 0x2b], fw_regions: false, bootargs_word_access: false, halt_on_failed_start: false, }; +/// T8152: like T8140, but the setup port (mailbox 1 of the AOP bank, and the +/// ADT aop-exclave-mailbox) is not reachable from the AP: both bus-error. +const HW_CFG_T8152: AopHwConfig = AopHwConfig { + ec0p: 0, + ncal: None, + aopt: 0, + alig: 0, + patch_bootargs: false, + epic_v4: true, + setup_port: false, + setup_epmap_endpoints: false, + setup_wait_system_endpoints: true, + quiesce_on_unbind: true, + required_endpoints: &[], + fw_regions: false, + bootargs_word_access: false, + halt_on_failed_start: false, +}; + +/// T8142 (M5): booted through a setup port as on T8140, at the ADT's +/// aop-exclave-mailbox (0x3d067c000, AOP ASC + 0x4007c000, untranslated), +/// whose EPMAP names the service endpoints (0x4, 0x20, 0x23 and 0x32 on the +/// MacBook Air J813, not T8140's set). EPIC and AFK as on T8152; the +/// firmware runs with the boot arguments iBoot left. +const HW_CFG_T8142: AopHwConfig = AopHwConfig { + ec0p: 0, + ncal: None, + aopt: 0, + alig: 0, + patch_bootargs: false, + epic_v4: true, + setup_port: true, + setup_epmap_endpoints: true, + setup_wait_system_endpoints: true, + quiesce_on_unbind: true, + required_endpoints: &[], + fw_regions: false, + bootargs_word_access: false, + halt_on_failed_start: false, +}; +/// T6050 (M5 Pro and Max): booted as T8142, through the setup port at +/// AOP ASC + 0x4007c000. Its EPMAP also names system endpoint 4, which never +/// asks for a buffer, so boot doesn't wait for it. +const HW_CFG_T6050: AopHwConfig = AopHwConfig { + setup_wait_system_endpoints: false, + ..HW_CFG_T8142 +}; kernel::of_device_table!( OF_TABLE, @@ -2714,6 +2825,9 @@ kernel::of_device_table!( (of::DeviceId::new(c_str!("apple,t8140-aop")), &HW_CFG_T8140), (of::DeviceId::new(c_str!("apple,t6041-aop")), &HW_CFG_T6040), (of::DeviceId::new(c_str!("apple,t8132-aop")), &HW_CFG_T8132), + (of::DeviceId::new(c_str!("apple,t6050-aop")), &HW_CFG_T6050), + (of::DeviceId::new(c_str!("apple,t8142-aop")), &HW_CFG_T8142), + (of::DeviceId::new(c_str!("apple,t8152-aop")), &HW_CFG_T8152), ] ); @@ -2763,7 +2877,14 @@ impl platform::Driver for AopDriver { cfg.bootargs_word_access, )?; } - let rtkit = rtkit::RtKit::::new(pdev.as_ref(), None, 0, data.clone())?; + // The T8142 AOP places its own crashlog buffer in its firmware-mapped + // DART slot and asks for it on endpoint 1 with that IOVA; take it as + // the firmware's instead of refusing the request. + let rtkit = if cfg.setup_epmap_endpoints { + rtkit::RtKit::::new_with_firmware_crashlog(pdev.as_ref(), None, 0, data.clone())? + } else { + rtkit::RtKit::::new(pdev.as_ref(), None, 0, data.clone())? + }; data.rtkit.lock().as_mut().set(Some(rtkit)); if cfg.setup_port { AopData::setup_open(&data, pdev.as_ref())?; diff --git a/sound/soc/apple/Kconfig b/sound/soc/apple/Kconfig index 92ffcb6073797..32265803617ad 100644 --- a/sound/soc/apple/Kconfig +++ b/sound/soc/apple/Kconfig @@ -14,6 +14,18 @@ config SND_SOC_APPLE_AOP_AUDIO machines, and on T8140 the back-end DAIs for the headphone jack and the internal speaker, which the macaudio driver links into its card. +config SND_SOC_APPLE_T8152_AOP_AUDIO + tristate "Apple T8152 AOP audio driver" + depends on ARCH_APPLE || COMPILE_TEST + depends on RUST + select APPLE_AOP + select SND_DMAENGINE_PCM + help + This option enables a driver for the audio paths owned by the AOP + co-processor on Apple T8152 (Mac mini M6) and T8142 (MacBook Air M5) + SoCs: the headphone jack, the internal speakers and, where present, + the microphone array. + config SND_SOC_APPLE_MCA tristate "Apple Silicon MCA driver" depends on ARCH_APPLE || COMPILE_TEST diff --git a/sound/soc/apple/Makefile b/sound/soc/apple/Makefile index 52d4abf30916b..71885319572cf 100644 --- a/sound/soc/apple/Makefile +++ b/sound/soc/apple/Makefile @@ -3,6 +3,9 @@ snd-soc-aop-y := aop_audio.o obj-$(CONFIG_SND_SOC_APPLE_AOP_AUDIO) += snd-soc-aop.o +snd-soc-t8152-aop-audio-y := t8152_aop_audio.o +obj-$(CONFIG_SND_SOC_APPLE_T8152_AOP_AUDIO) += snd-soc-t8152-aop-audio.o + snd-soc-apple-mca-y := mca.o obj-$(CONFIG_SND_SOC_APPLE_MCA) += snd-soc-apple-mca.o diff --git a/sound/soc/apple/macaudio-admission-test.h b/sound/soc/apple/macaudio-admission-test.h index b526f460487bf..5ad72fc3d0b32 100644 --- a/sound/soc/apple/macaudio-admission-test.h +++ b/sound/soc/apple/macaudio-admission-test.h @@ -11,6 +11,9 @@ static void macaudio_admission_needs_a_safe_level(struct kunit *test) KUNIT_EXPECT_FALSE(test, macaudio_speakers_allowed(&macaudio_j45x_cfg)); KUNIT_EXPECT_TRUE(test, macaudio_speakers_allowed(&macaudio_j413_cfg)); KUNIT_EXPECT_TRUE(test, macaudio_speakers_allowed(&macaudio_j700_cfg)); + /* The AOP-fed machines: J813 with its safe level, J873 without one */ + KUNIT_EXPECT_TRUE(test, macaudio_speakers_allowed(&macaudio_j813_cfg)); + KUNIT_EXPECT_FALSE(test, macaudio_speakers_allowed(&macaudio_j873_cfg)); cfg = macaudio_j413_cfg; cfg.safe_vol = 0; KUNIT_EXPECT_FALSE(test, macaudio_speakers_allowed(&cfg)); diff --git a/sound/soc/apple/macaudio-amp-level-test.h b/sound/soc/apple/macaudio-amp-level-test.h index 8d8181f96f6cc..007e2bbdefb3e 100644 --- a/sound/soc/apple/macaudio-amp-level-test.h +++ b/sound/soc/apple/macaudio-amp-level-test.h @@ -58,6 +58,7 @@ static void macaudio_level_no_table(struct kunit *test) &macaudio_j37x_j47x_cfg, &macaudio_j413_cfg, &macaudio_j415_cfg, &macaudio_j493_cfg, &macaudio_j575c_j773_cfg, &macaudio_j614_cfg, &macaudio_j616_cfg, &macaudio_j713_cfg, &macaudio_j715_cfg, + &macaudio_j813_cfg, &macaudio_j873_cfg, }; unsigned int i; diff --git a/sound/soc/apple/macaudio-iface-test.h b/sound/soc/apple/macaudio-iface-test.h index 83155d80b04d9..cdaf64dfa4a12 100644 --- a/sound/soc/apple/macaudio-iface-test.h +++ b/sound/soc/apple/macaudio-iface-test.h @@ -15,15 +15,16 @@ struct macaudio_test_iface { struct snd_ctl_file *daemon; }; -/* A bound card with both interfaces: the volume lock in charge, at its ceiling */ -static void macaudio_test_iface_card(struct kunit *test, struct macaudio_test_iface *f) +/* A bound card of @cfg with both interfaces: the volume lock in charge, at its ceiling */ +static void macaudio_test_iface_card_cfg(struct kunit *test, struct macaudio_test_iface *f, + const struct macaudio_platform_cfg *cfg) { struct snd_kcontrol_new slk = macaudio_test_control("Speaker Volume Unlock"); struct snd_kcontrol_new iface = macaudio_test_control("Speaker Protection Interface"); struct macaudio_snd_data *ma; unsigned int i; - macaudio_test_spk_card(test, &f->t, &macaudio_j316_cfg, SN012776_0DB); + macaudio_test_spk_card(test, &f->t, cfg, SN012776_0DB); ma = f->t.ma; f->daemon = kunit_kzalloc(test, sizeof(*f->daemon), GFP_KERNEL); KUNIT_ASSERT_NOT_NULL(test, f->daemon); @@ -44,6 +45,12 @@ static void macaudio_test_iface_card(struct kunit *test, struct macaudio_test_if mutex_unlock(&ma->volume_lock_mutex); } +/* As macaudio_test_iface_card_cfg(), for J316 */ +static void macaudio_test_iface_card(struct kunit *test, struct macaudio_test_iface *f) +{ + macaudio_test_iface_card_cfg(test, f, &macaudio_j316_cfg); +} + static int macaudio_test_iface_write(struct kunit *test, struct macaudio_test_iface *f, unsigned int item) { @@ -238,6 +245,51 @@ static void macaudio_iface_needs_cap_sys_admin(struct kunit *test) KUNIT_EXPECT_EQ(test, macaudio_test_iface_write(test, &f, 2), -EINVAL); } +/* + * J813: its four SN012776 (the fake card's Left/Right Woofer/Tweeter + * volumes, the J813 amps' prefixes) are behind both interfaces from the + * bind, as on the MCA machines. Under the volume lock they stay at its + * ceiling until a daemon holds the lock and pings; under the lease they are + * at the floor and no speaker start is admitted until an owner is granted. + */ +static void macaudio_iface_j813_speakers_protected(struct kunit *test) +{ + struct macaudio_test_iface f; + + KUNIT_ASSERT_TRUE(test, macaudio_speakers_allowed(&macaudio_j813_cfg)); + macaudio_test_iface_card_cfg(test, &f, &macaudio_j813_cfg); + KUNIT_EXPECT_FALSE(test, macaudio_spk_leased(f.t.ma)); + KUNIT_EXPECT_FALSE(test, f.t.ma->speaker_volume_unlocked); + macaudio_test_expect_limit(test, &f, 1, 1); + + /* A daemon that holds the lock but has not pinged lifts nothing */ + macaudio_test_daemon_take(test, &f); + KUNIT_EXPECT_FALSE(test, f.t.ma->speaker_volume_unlocked); + macaudio_test_expect_limit(test, &f, 1, 1); + KUNIT_EXPECT_EQ(test, macaudio_test_daemon_ping(test, &f), 0); + KUNIT_EXPECT_TRUE(test, f.t.ma->speaker_volume_unlocked); + macaudio_test_expect_limit(test, &f, 1, SN012776_0DB); + + /* Into the lease: the floor, and nothing admitted without an owner */ + KUNIT_EXPECT_EQ(test, macaudio_test_iface_write(test, &f, MACAUDIO_SPK_IFACE_LEASE), 1); + KUNIT_EXPECT_TRUE(test, macaudio_spk_leased(f.t.ma)); + KUNIT_EXPECT_FALSE(test, f.t.ma->speaker_volume_unlocked); + KUNIT_EXPECT_FALSE(test, macaudio_spk_admitted(f.t.ma)); + macaudio_test_expect_limit(test, &f, 0, SN012776_0DB); + KUNIT_EXPECT_EQ(test, macaudio_test_write(test, &f.t, 50), -EPERM); + KUNIT_EXPECT_FALSE(test, macaudio_spk_admitted(f.t.ma)); + + /* Its owner is granted the operating gain, and the floor on release */ + macaudio_test_take(test, &f.t); + KUNIT_EXPECT_EQ(test, macaudio_test_write(test, &f.t, 50), 0); + KUNIT_EXPECT_TRUE(test, macaudio_spk_admitted(f.t.ma)); + macaudio_test_expect_limit(test, &f, SN012776_0DB, SN012776_0DB); + KUNIT_EXPECT_EQ(test, macaudio_test_write(test, &f.t, 0), 0); + macaudio_test_settle(f.t.ma); + KUNIT_EXPECT_FALSE(test, macaudio_spk_admitted(f.t.ma)); + macaudio_test_expect_limit(test, &f, 0, SN012776_0DB); +} + /* The switch is no mixer setting: alsactl neither saves nor restores it */ static void macaudio_iface_control_is_inactive(struct kunit *test) { @@ -254,6 +306,7 @@ static struct kunit_case macaudio_iface_cases[] = { KUNIT_CASE(macaudio_iface_unowned_lease_reverts), KUNIT_CASE(macaudio_iface_needs_cap_sys_admin), KUNIT_CASE(macaudio_iface_control_is_inactive), + KUNIT_CASE(macaudio_iface_j813_speakers_protected), {} }; diff --git a/sound/soc/apple/macaudio.c b/sound/soc/apple/macaudio.c index 4e1b642e4b0a5..cc020da9e6172 100644 --- a/sound/soc/apple/macaudio.c +++ b/sound/soc/apple/macaudio.c @@ -176,6 +176,13 @@ struct macaudio_platform_cfg { * along with the safe level. */ bool tweeter_hpf_granted; + /* The speaker links' DAI format, if not dai_fmt */ + unsigned int spk_dai_fmt; + /* + * The speakers can only be fed from the secondary front-end: each + * front-end owns the DMA channel of one serializer (AOP-fed models). + */ + bool spk_secondary_only; }; static const char *volume_control_names[] = { @@ -2731,6 +2738,8 @@ static int macaudio_parse_of(struct macaudio_snd_data *ma) link_props->is_speakers = speakers; link_props->is_headphones = !speakers; + if (speakers && ma->cfg->spk_dai_fmt) + link->dai_fmt = ma->cfg->spk_dai_fmt; link_props->codecs = devm_kcalloc(dev, ncodecs_per_cpu, sizeof(struct ma_codec_idle), GFP_KERNEL); @@ -3673,6 +3682,9 @@ static int macaudio_fixup_controls(struct snd_soc_card *card) return ret; } + if (ma->cfg->spk_secondary_only) + CHECK(snd_soc_set_enum_kctl, "Speaker Playback Mux", "Secondary", 1); + /* * This needs some care to avoid matches against cs42l84's * "Jack HPF Corner Frequency". @@ -4004,6 +4016,59 @@ struct macaudio_platform_cfg macaudio_j700_cfg = { .primary_bclk_ratio = 125, }; +/* + * J873 (T8152, Mac mini M6): as on J700, the AOP firmware owns the audio + * fabric and the CPU component is the T8152 AOP audio driver. The jack is a + * CS42L84 on the AOP's "cout" back-end, the speaker one SN012776 on "spkr"; + * both serializers run on the firmware's clocks. The speaker frame is 250 + * BCLKs of 12 MHz (the amp is told so by its ti,bclk-ratio), with the data + * sampled on the rising edge, as macOS programs the amp. + */ +struct macaudio_platform_cfg macaudio_j873_cfg = { + /* + * No qualified speaker protection exists for J873 yet (no verified + * sense path, thermal model or power budget), and there is no safe + * level: macaudio_speakers_allowed() keeps the speaker off. The jack + * is unaffected. + */ + .enable_speakers = false, + .amp = AMP_SN012776, + .speakers = SPKR_1W, + .stereo = false, + .safe_vol = 0, + .fe_dai_names = { "j873-pcm-0", "j873-pcm-1", "j873-pcm-2" }, + /* jack: a one-BCLK sync pulse, data launched on the rising edge */ + .dai_fmt = SND_SOC_DAIFMT_DSP_A | SND_SOC_DAIFMT_CBC_CFC | SND_SOC_DAIFMT_IB_NF, + .spk_dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_CBC_CFC | + SND_SOC_DAIFMT_GATED | SND_SOC_DAIFMT_NB_IF, + .spk_secondary_only = true, +}; + +/* + * J813 (T8142, MacBook Air M5 13"): as on J873, the AOP firmware owns the + * audio fabric and the CPU component is the T8152-style AOP audio driver. + * The jack is a CS42L84 on "cout"; the speakers are four SN012776 (one + * woofer and one tweeter per side) on "spkr", whose voltage and current + * sense return on the same stream. As on the other SN012776 laptops, the + * speakers are behind the speaker protection interfaces from the bind: the + * volumes stay at the lock's ceiling until a protection daemon + * (speakersafetyd) holds them, or under "Speaker Protection" at the floor + * until an owner is granted. Each amp's level is its apple,speaker-config + * from the boot loader. + */ +struct macaudio_platform_cfg macaudio_j813_cfg = { + .enable_speakers = true, + .amp = AMP_SN012776, + .speakers = SPKR_1W1T, + .stereo = true, + .safe_vol = -20, + .fe_dai_names = { "j873-pcm-0", "j873-pcm-1", "j873-pcm-2" }, + .dai_fmt = SND_SOC_DAIFMT_DSP_A | SND_SOC_DAIFMT_CBC_CFC | SND_SOC_DAIFMT_IB_NF, + .spk_dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_CBC_CFC | + SND_SOC_DAIFMT_GATED | SND_SOC_DAIFMT_NB_IF, + .spk_secondary_only = true, +}; + struct macaudio_platform_cfg macaudio_j314_cfg = { true, AMP_SN012776, SPKR_2W1T, true, 0, -20, }; @@ -4254,6 +4319,10 @@ static const struct of_device_id macaudio_snd_device_id[] = { { .compatible = "apple,j773-macaudio", .data = &macaudio_j575c_j773_cfg }, /* j773 - cs42l84 - the jack, a card of its own (AOP-fed) */ { .compatible = "apple,j773-macaudio-jack", .data = &macaudio_j616_jack_cfg }, + /* j873 AID48 sn012776 15 1x 1W (AOP-fed, no protection) */ + { .compatible = "apple,j873-macaudio", .data = &macaudio_j873_cfg }, + /* j813 AID? sn012776 15 2x 1W+1T (AOP-fed, V/I sense) */ + { .compatible = "apple,j813-macaudio", .data = &macaudio_j813_cfg }, /* Fallback, jack only */ { .compatible = "apple,macaudio"}, { } diff --git a/sound/soc/apple/t8152_aop_audio.rs b/sound/soc/apple/t8152_aop_audio.rs new file mode 100644 index 0000000000000..e99a1250015e9 --- /dev/null +++ b/sound/soc/apple/t8152_aop_audio.rs @@ -0,0 +1,1540 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT + +//! Apple T8152 (J873, Mac mini M6) and T8142 (J813, MacBook Air M5) AOP audio +//! +//! The AOP firmware owns the audio fabric, as on T8140. The ADT +//! (/arm-io/iop-audio-controller) names its services; all of them run on the +//! base-ns ADMAC: +//! +//! * `cout`/`cin`: the CS42L84 headphone jack, base-ns TX5/RX5 (Linux +//! channels 10 and 11). +//! * `spkr`: the SN012776 speaker amplifier, base-ns TX1 (channel 2), its +//! sense data on RX1. +//! * `hpai` (J813): the microphone array, three PDM microphones the firmware +//! delivers as float32 on leap-s ADMAC RX0. It has no codec, so it is the +//! capture PCM of a small card of its own. +//! +//! Each service is an ASoC back-end DAI: startup attaches it and requests +//! `pw0 ` (which also brings up the audio fabric), prepare requests `pwrd` +//! (the firmware then runs the serializer and its clocks) and shutdown goes +//! back to `pw0 `. The serializer (MCA) leaves are the AP's to power. The +//! front-end PCMs are plain dmaengine PCMs on the base-ns channels; macaudio +//! links them to the codecs. +//! +//! Copyright (C) The Asahi Linux Contributors +//! Copyright (C) 2026 The Aurora Silicon Contributors + +use core::{mem, ptr, slice}; + +use kernel::{ + bindings, c_str, + device, + device::Core, + error::from_err_ptr, + module_platform_driver, of, platform, + prelude::*, + soc::apple::aop::{from_fourcc, EPICService, AOP}, + str::CString, + sync::{ + aref::ARef, + atomic::{Atomic, AtomicFlag, Relaxed}, + Arc, ArcBorrow, + }, + types::{ForeignOwnable, Opaque}, + workqueue::{self, impl_has_work, new_work, Work, WorkItem}, +}; + +const EPIC_SUBTYPE_WRAPPED_CALL: u16 = 0x20; +const CALLTYPE_AUDIO_ATTACH_DEVICE: u32 = 0xc3000002; +const CALLTYPE_AUDIO_GET_PROP: u32 = 0xc3000004; +const CALLTYPE_AUDIO_SET_PROP: u32 = 0xc3000005; +const PROP_POWER_STATE: u32 = 200; +const PROP_POWER_REQUEST: u32 = 202; +const POWER_STATE_PW0: u32 = from_fourcc(b"pw0 "); +const POWER_STATE_PWRD: u32 = from_fourcc(b"pwrd"); +/// kIOReturnBusy: the device is already attached (a module reload). +const AUDIO_RET_BUSY: u32 = 0xe00002d5; +const AOP_TIMEBASE_HZ: u64 = 24_000_000; + +/// Leaf power-up attempts while the firmware raises the fabric after `pw0 `. +const PD_GET_ATTEMPTS: u32 = 60; +const PD_GET_RETRY_MS: i64 = 10; +/// The MCA serializer leaves (audio_mca0_m .. audio_mca5_m). +const MCA_COUNT: usize = 6; +const MCA_NAMES: [&CStr; MCA_COUNT] = [c"mca0", c"mca1", c"mca2", c"mca3", c"mca4", c"mca5"]; +/// The microphone array's LEAP leaves: the leap-s ADMAC, its RX0 channel and +/// the LEAP serializer. A LEAP ADMAC's channel registers only answer with +/// these powered (an access without them faults the bus). +const LEAP_NAMES: [&CStr; 3] = [c"leap_s_a", c"leap_rx0", c"leap_mca"]; +/// The microphone array's stream (ADT audio-hp-mic-proxy physical +/// description): 48 kHz, three float32 channels. +const HPAI_RATE: u32 = 48000; +const HPAI_CHANNELS: u32 = 3; +const HPAI_START_WORK: u64 = 1; + +fn aop_timestamp() -> u64 { + // SAFETY: both helpers read the architected timer and take no arguments. + let (counter, rate) = + unsafe { (bindings::arch_timer_counter(), bindings::arch_timer_rate()) }; + let rate = u64::from(rate); + (counter / rate) * AOP_TIMEBASE_HZ + (counter % rate) * AOP_TIMEBASE_HZ / rate +} + +fn le_u32(buf: &[u8], off: usize) -> Result { + let b = buf.get(off..off + 4).ok_or(EINVAL)?; + Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]])) +} + +#[repr(C, packed(4))] +#[derive(Clone, Copy, Default)] +struct AudioAttachDevice { + _zero0: u32, + unk0: u32, + calltype: u32, + _zero1: [u8; 16], + cookie: u32, + len: u64, + dev_id: u32, + _zero2: u32, +} + +#[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, +} + +#[repr(C, packed(4))] +#[derive(Clone, Copy, Default)] +struct AudioSetDeviceProp { + _zero0: u32, + unk0: u32, + calltype: u32, + _zero1: [u8; 16], + _pad0: u32, + len: u64, + dev_id: u32, + modifier: u32, + len2: u32, + data: T, +} + +/// Property 202: the device, a per-device sequence, the request time in the +/// firmware's 24 MHz timebase and the target state. +#[repr(C, packed(4))] +#[derive(Clone, Copy, Default)] +struct PowerRequest { + dev_id: u32, + sequence: u32, + timestamp: u64, + _zero0: u32, + state: u32, + param: u32, + _zero1: [u8; 20], +} + +/// A named power domain of this device, runtime-powered around the streams +/// that need it. +struct PowerDomain { + dev: ptr::NonNull, +} + +// SAFETY: the virtual domain device belongs to the PM core, which serializes +// its runtime-PM state; this handle only passes the pointer to those calls. +unsafe impl Send for PowerDomain {} +// SAFETY: as for `Send`. +unsafe impl Sync for PowerDomain {} + +impl PowerDomain { + fn attach(dev: &device::Device, name: &CStr) -> Result> { + // SAFETY: `dev` is live for the call and `name` is NUL-terminated. + let pd = from_err_ptr(unsafe { + bindings::dev_pm_domain_attach_by_name(dev.as_raw(), name.as_char_ptr()) + })?; + Ok(ptr::NonNull::new(pd).map(|dev| PowerDomain { dev })) + } + + /// Takes a runtime-PM reference, retrying while the parent fabric that + /// the firmware brings up after `pw0 ` is still off. + fn get(&self) -> Result<()> { + let mut rc = 0; + for _ in 0..PD_GET_ATTEMPTS { + // SAFETY: the domain device stays attached for the lifetime of `self`. + rc = unsafe { bindings::pm_runtime_get_sync(self.dev.as_ptr()) }; + if rc >= 0 { + return Ok(()); + } + // SAFETY: as above; undo the usage count and the error state. + unsafe { + bindings::pm_runtime_put_noidle(self.dev.as_ptr()); + bindings::__pm_runtime_set_status( + self.dev.as_ptr(), + bindings::rpm_status_RPM_SUSPENDED as u32, + ); + } + kernel::time::delay::fsleep(kernel::time::Delta::from_millis(PD_GET_RETRY_MS)); + } + Err(Error::from_errno(rc)) + } + + fn put(&self) { + // SAFETY: the domain device stays attached for the lifetime of `self`. + unsafe { bindings::pm_runtime_put_sync(self.dev.as_ptr()) }; + } +} + +impl Drop for PowerDomain { + fn drop(&mut self) { + // SAFETY: attached in `attach`; every `get` has been balanced by then. + unsafe { bindings::dev_pm_domain_detach(self.dev.as_ptr(), true) }; + } +} + +/// One firmware audio service driven as an ASoC back-end. +struct Service { + dev_id: u32, + name: &'static str, + sequence: Atomic, + /// In `pwrd`. + running: AtomicFlag, + /// Stop and restart the serializer on every prepare, so that a stream + /// restarted after an underrun begins on a frame boundary. The mono + /// speaker has one slot per frame and keeps its clocks running instead: + /// its amplifier stays unmuted between prepares and reports "bad TDM + /// clock" whenever the bit clock stops under it. + restart_on_prepare: bool, +} + +impl Service { + const fn new(dev_id: u32, name: &'static str, restart_on_prepare: bool) -> Service { + Service { + dev_id, + name, + sequence: Atomic::new(0), + running: AtomicFlag::new(false), + restart_on_prepare, + } + } +} + +#[pin_data] +struct T8152AopAudio { + dev: ARef, + adata: Arc, + service: EPICService, + cout: Service, + cin: Service, + spkr: Service, + mca: KVec, + cfg: &'static HwConfig, + /// The open sense capture front-end, if any. + sense: Atomic<*mut bindings::snd_pcm_substream>, + hpai: Service, + /// The microphone array's leaves (`LEAP_NAMES`); empty without one. + leap: KVec, + /// The microphone capture is triggered. + hpai_want_run: AtomicFlag, + #[pin] + hpai_start_work: Work, +} + +impl_has_work! { + impl HasWork for T8152AopAudio { self.hpai_start_work } +} + +/// The microphone array's `pwrd`, queued by the capture trigger (a sleeping +/// firmware call). The ADMAC is running by then: started before it, the LEAP +/// produces into a channel nobody services and the capture stays noise. +impl WorkItem for T8152AopAudio { + type Pointer = Arc; + + fn run(this: Arc) { + if let Err(e) = this.hpai_go() { + dev_err!(this.dev, "hpai: start failed: {:?}\n", e); + } + } +} + +impl T8152AopAudio { + fn call_ret(&self, data: &T, ret_len: usize) -> Result<(u32, KVec)> { + // SAFETY: T is a plain packed firmware record. + let msg = unsafe { + slice::from_raw_parts(ptr::from_ref(data).cast::(), mem::size_of::()) + }; + self.adata + .epic_call_ret(&self.service, EPIC_SUBTYPE_WRAPPED_CALL, msg, ret_len) + } + + fn call(&self, data: &T) -> Result { + // SAFETY: T is a plain packed firmware record. + let msg = unsafe { + slice::from_raw_parts(ptr::from_ref(data).cast::(), mem::size_of::()) + }; + self.adata + .epic_call(&self.service, EPIC_SUBTYPE_WRAPPED_CALL, msg) + } + + /// The firmware keeps attachments for its lifetime, so a module reload + /// sees kIOReturnBusy. + fn attach(&self, svc: &Service) -> Result<()> { + let ret = self.call(&AudioAttachDevice { + unk0: 0xffffffff, + calltype: CALLTYPE_AUDIO_ATTACH_DEVICE, + len: 0x2c, + dev_id: svc.dev_id, + ..Default::default() + })?; + match ret { + 0 | AUDIO_RET_BUSY => Ok(()), + _ => { + dev_err!(self.dev, "unable to attach {}: {:#x}\n", svc.name, ret); + Err(EIO) + } + } + } + + fn power_state(&self, svc: &Service) -> Result { + let (ret, data) = self.call_ret( + &AudioGetDeviceProp { + unk0: 0xffffffff, + calltype: CALLTYPE_AUDIO_GET_PROP, + len: 0x30, + dev_id: svc.dev_id, + modifier: PROP_POWER_STATE, + unk6: 1, + ..Default::default() + }, + 8, + )?; + if ret != 0 { + return Err(EIO); + } + le_u32(&data, 4) + } + + fn set_power(&self, svc: &Service, state: u32) -> Result<()> { + let req = PowerRequest { + dev_id: svc.dev_id, + sequence: svc.sequence.fetch_add(1, Relaxed) + 1, + timestamp: aop_timestamp(), + state, + ..Default::default() + }; + let ret = self.call(&AudioSetDeviceProp { + unk0: 0xffffffff, + calltype: CALLTYPE_AUDIO_SET_PROP, + len: mem::size_of::() as u64 + 0x30, + dev_id: svc.dev_id, + modifier: PROP_POWER_REQUEST, + len2: mem::size_of::() as u32, + data: req, + ..Default::default() + })?; + if ret != 0 { + dev_err!( + self.dev, + "{}: power request {:#x} rejected: {:#x}\n", + svc.name, + state, + ret + ); + return Err(EIO); + } + let now = self.power_state(svc)?; + if now != state { + dev_err!( + self.dev, + "{}: power state {:#x} after requesting {:#x}\n", + svc.name, + now, + state + ); + return Err(EIO); + } + Ok(()) + } + + fn leaves_up(&self, pds: &[PowerDomain], what: &str) -> Result<()> { + for (i, pd) in pds.iter().enumerate() { + if let Err(e) = pd.get() { + dev_err!(self.dev, "{} leaf {} did not power up: {:?}\n", what, i, e); + for pd in pds[..i].iter().rev() { + pd.put(); + } + return Err(e); + } + } + Ok(()) + } + + fn leaves_down(pds: &[PowerDomain]) { + for pd in pds.iter().rev() { + pd.put(); + } + } + + fn mca_up(&self) -> Result<()> { + self.leaves_up(&self.mca, "MCA") + } + + fn mca_down(&self) { + Self::leaves_down(&self.mca) + } + + /// Back-end startup: attach, `pw0 ` (which raises the fabric), then the + /// serializer leaves. + fn startup(&self, svc: &Service) -> Result<()> { + self.attach(svc)?; + self.set_power(svc, POWER_STATE_PW0)?; + if let Err(e) = self.mca_up() { + let _ = self.set_power(svc, POWER_STATE_PW0); + return Err(e); + } + svc.running.store(false, Relaxed); + Ok(()) + } + + /// The sense capture's back-end. Its front-end requests the ADMAC + /// channel next, and the ADMAC only answers while the firmware holds + /// the audio fabric up, which it does while a service is in `pw0 ` or + /// `pwrd`. Opening the sense before any other stream since boot (as + /// speakersafetyd does) otherwise faults the bus (SError). Shutdown + /// leaves the service in `pw0 `, as the other streams do. + fn sense_startup(&self) -> Result<()> { + self.attach(&self.spkr)?; + if !self.spkr.running.load(Relaxed) { + self.set_power(&self.spkr, POWER_STATE_PW0)?; + } + Ok(()) + } + + /// `pwrd`: the firmware starts the serializer clocks and the stream. + fn run(&self, svc: &Service) -> Result<()> { + if svc.running.load(Relaxed) { + return Ok(()); + } + if ptr::eq(svc, &self.spkr) && self.cfg.realign_sense { + self.realign_sense(); + } + self.set_power(svc, POWER_STATE_PWRD)?; + svc.running.store(true, Relaxed); + Ok(()) + } + + /// Before the speaker's serializer starts, restart a running sense + /// capture at a frame boundary. The sense words are the serializer's RX, + /// which the firmware stops anywhere in a frame; a capture that stays + /// open across speaker streams (speakersafetyd keeps it) would carry + /// the partial frame forward and attribute one amplifier's voltage and + /// current to another. Nothing flows while the serializer is stopped, + /// so this loses no data; the partial frame reads as zeros. + fn realign_sense(&self) { + let ss = self.sense.load(Relaxed); + if ss.is_null() { + return; + } + // SAFETY: the sense front-end's open and close and this back-end's + // prepare all run under the card's DPCM mutex, so `ss` is live; its + // stream lock orders this against the front-end's trigger. + unsafe { + bindings::snd_pcm_stream_lock_irq(ss); + let rt = (*ss).runtime; + if !rt.is_null() && (*rt).state == SNDRV_PCM_STATE_RUNNING { + let chan = bindings::snd_dmaengine_pcm_get_chan(ss); + let ops = &*(*chan).device; + if let (Some(pause), Some(resume)) = (ops.device_pause, ops.device_resume) { + pause(chan); + let frame = ((*rt).frame_bits / 8) as usize; + let pos = bindings::snd_dmaengine_pcm_pointer(ss) as usize; + if frame != 0 && pos < (*rt).buffer_size as usize && !(*rt).dma_area.is_null() { + ptr::write_bytes((*rt).dma_area.add(pos * frame), 0, frame); + } + resume(chan); + } + } + bindings::snd_pcm_stream_unlock_irq(ss); + } + } + + /// The clocks run from prepare on: the codecs see a bit clock as soon + /// as ASoC powers and unmutes them. A PCM underrun stops the DMA + /// without closing the back-end; a service that restarts on prepare + /// retires its run so that the next start begins afresh. + fn prepare(&self, svc: &Service) -> Result<()> { + if svc.restart_on_prepare && svc.running.load(Relaxed) { + self.set_power(svc, POWER_STATE_PW0)?; + svc.running.store(false, Relaxed); + } + self.run(svc) + } + + fn shutdown(&self, svc: &Service) { + if svc.running.load(Relaxed) { + if let Err(e) = self.set_power(svc, POWER_STATE_PW0) { + dev_err!(self.dev, "{}: pw0 failed: {:?}\n", svc.name, e); + } + svc.running.store(false, Relaxed); + } + self.mca_down(); + } + + /// Microphone open: attach, `pw0 ` (which raises the fabric), then the + /// LEAP leaves, before the capture requests its ADMAC channel. + fn hpai_startup(&self) -> Result<()> { + self.attach(&self.hpai)?; + self.set_power(&self.hpai, POWER_STATE_PW0)?; + self.leaves_up(&self.leap, "LEAP")?; + self.hpai.running.store(false, Relaxed); + Ok(()) + } + + fn hpai_go(&self) -> Result<()> { + if !self.hpai_want_run.load(Relaxed) { + return Ok(()); + } + self.run(&self.hpai) + } + + /// Finish a queued firmware start before retiring the DMA runtime. + fn flush_hpai_start(&self) { + self.hpai_want_run.store(false, Relaxed); + // SAFETY: the work item lives in `self`; flushing waits for a queued + // `hpai_go`. + unsafe { bindings::flush_work(Work::raw_get(&self.hpai_start_work)) }; + } + + /// Microphone close, after the channel is released: `pw0 ` then leaves. + fn hpai_shutdown(&self) { + self.flush_hpai_start(); + if self.hpai.running.load(Relaxed) { + if let Err(e) = self.set_power(&self.hpai, POWER_STATE_PW0) { + dev_err!(self.dev, "hpai: pw0 failed: {:?}\n", e); + } + self.hpai.running.store(false, Relaxed); + } + Self::leaves_down(&self.leap); + } +} + +// ---- the ASoC component ---- + +/// Front-end index of a runtime by CPU DAI id; back-ends have none. +const FE_DAI_ID_BASE: u32 = 1; +const FE_COUNT: usize = 3; +const BE_DAI_ID_COUT: u32 = 10; +const BE_DAI_ID_SPKR: u32 = 11; +/// DMA channel names (dma-names on the audio node) per front-end, [playback, +/// capture]: the jack, the speaker and the speaker's sense data. +const FE_DMA_NAMES: [[Option<&CStr>; 2]; FE_COUNT] = [ + [Some(c"cout"), Some(c"cin")], + [Some(c"spkr"), None], + [None, Some(c"sense")], +]; + +const PCM_BUFFER_MAX: usize = 1024 * 1024; +const PCM_PERIOD_BYTES_MIN: usize = 256; +const PCM_PERIOD_BYTES_MAX: usize = 0x10000; + +/// The AOP-run serializers take 24-bit samples in 32-bit words. +const FORMATS: u64 = bindings::BINDINGS_SNDRV_PCM_FMTBIT_S24_LE; +/// SNDRV_PCM_FMTBIT_S16_LE (1 << SNDRV_PCM_FORMAT_S16_LE), which the +/// bindings do not export. +const FMTBIT_S16_LE: u64 = 1 << 2; +/// SNDRV_PCM_STATE_RUNNING, likewise. +const SNDRV_PCM_STATE_RUNNING: bindings::snd_pcm_state_t = 3; + +/// What differs between the machines: the shape of the firmware's `spkr` +/// stream (ADT iop-audio-controller/audio-speaker). +struct HwConfig { + /// Speaker slots (one per amplifier). + spkr_channels: u32, + /// Words the amplifiers send back on the stream's RX side (V/I sense). + sense_channels: u32, + sense_formats: u64, + /// The sense capture is the speaker stream's RX: restart it at a frame + /// boundary whenever the speaker starts (see `realign_sense`). + realign_sense: bool, +} + +/// J873: one amplifier in one 32-bit slot, nothing sent back. +static HW_CFG_T8152: HwConfig = HwConfig { + spkr_channels: 1, + sense_channels: 2, + sense_formats: FORMATS, + realign_sense: false, +}; + +/// J813: four amplifiers (left woofer, left tweeter, right woofer, right +/// tweeter) in 32-bit slots 0-3, their voltage and current sense back as +/// eight 16-bit slots (V, I per amplifier) on the stream's RX (base-ns RX0). +static HW_CFG_T8142: HwConfig = HwConfig { + spkr_channels: 4, + sense_channels: 8, + sense_formats: FMTBIT_S16_LE, + realign_sense: true, +}; + +/// # Safety +/// +/// `substream` must be live and belong to one of this component's links. +unsafe fn substream_rtd( + 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 rtd_is_be(rtd: *mut bindings::snd_soc_pcm_runtime) -> bool { + // SAFETY: by the caller's contract. + unsafe { (*(*rtd).dai_link).no_pcm() != 0 } +} + +/// # 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. + 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 context behind the component's descriptor (its first field). +#[repr(C)] +struct AsocContext { + component: bindings::snd_soc_component_driver, + data: Arc, + dais: KBox>, + _ops: KBox, +} + +const _: () = assert!(mem::offset_of!(AsocContext, component) == 0); + +/// # Safety +/// +/// `component` must be this driver's live registered component. +unsafe fn component_data<'a>(component: *mut bindings::snd_soc_component) -> &'a T8152AopAudio { + // SAFETY: by the caller's contract; C keeps the descriptor pointer we + // registered, which is the start of the `AsocContext`. + unsafe { &(*(*component).driver.cast::()).data } +} + +/// # Safety +/// +/// `dai` must be a live DAI of this driver's registered component. +unsafe fn dai_data<'a>(dai: *mut bindings::snd_soc_dai) -> &'a T8152AopAudio { + // SAFETY: by the caller's contract. + unsafe { component_data((*dai).component) } +} + +/// The dmaengine PCMs' common description: mmap-able, interleaved, periods +/// from 256 bytes to the ADMAC's segment size. Rates, channels and formats +/// are left to the caller (for a front-end, ASoC fills them in from the DAI). +fn pcm_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() + } +} + +/// Sets up a dmaengine substream on `chan`: a managed buffer on the DMA +/// device (once per substream), `hw` refined by the channel and narrowed to +/// `formats`, then the dmaengine runtime. On failure the channel is still +/// the caller's. +/// +/// # Safety +/// +/// `substream` must be live and `chan` a channel the caller requested. +unsafe fn dmaengine_open( + substream: *mut bindings::snd_pcm_substream, + chan: *mut bindings::dma_chan, + mut hw: bindings::snd_pcm_hardware, + formats: u64, +) -> i32 { + // SAFETY: by the caller's contract. + unsafe { + let mut ret = 0; + if (*substream).dma_buffer.dev.type_ == bindings::SNDRV_DMA_TYPE_UNKNOWN as i32 { + ret = bindings::snd_pcm_set_managed_buffer( + substream, + bindings::SNDRV_DMA_TYPE_DEV as i32, + (*(*chan).device).dev, + 0, + 0, + ); + } + if ret == 0 { + let mut dma_data = bindings::snd_dmaengine_dai_dma_data::default(); + ret = bindings::snd_dmaengine_pcm_refine_runtime_hwparams( + substream, + &mut dma_data, + &mut hw, + chan, + ); + } + if ret == 0 { + hw.formats &= formats; + (*(*substream).runtime).hw = hw; + ret = bindings::snd_dmaengine_pcm_open(substream, chan); + } + ret + } +} + +/// # Safety +/// +/// Called by ASoC on this driver's component with live arguments. +unsafe extern "C" fn asoc_pcm_open( + component: *mut bindings::snd_soc_component, + substream: *mut bindings::snd_pcm_substream, +) -> i32 { + // SAFETY: by the caller's contract. + let data = unsafe { component_data(component) }; + // SAFETY: as above. + let rtd = unsafe { substream_rtd(substream) }; + // SAFETY: as above. + if unsafe { rtd_is_be(rtd) } { + return 0; + } + // SAFETY: as above. + let Some(fe) = (unsafe { fe_index(rtd) }) else { + return EINVAL.to_errno(); + }; + // SAFETY: as above. + let playback = unsafe { is_playback(substream) }; + let Some(name) = FE_DMA_NAMES[fe][usize::from(!playback)] else { + return ENODEV.to_errno(); + }; + // SAFETY: the device is live and the name is NUL-terminated. + let chan = match from_err_ptr(unsafe { + bindings::dma_request_chan(data.dev.as_raw(), name.as_char_ptr()) + }) { + Ok(chan) => chan, + Err(e) => { + dev_err!(data.dev, "DMA channel {:?} unavailable: {:?}\n", name, e); + return e.to_errno(); + } + }; + let formats = if fe == 2 { data.cfg.sense_formats } else { FORMATS }; + // SAFETY: the substream is live and the channel was requested above. + let ret = unsafe { dmaengine_open(substream, chan, pcm_hw(), formats) }; + if ret < 0 { + // SAFETY: the open failed, so the channel is still ours. + unsafe { bindings::dma_release_channel(chan) }; + } else if fe == 2 && !playback { + data.sense.store(substream, Relaxed); + } + ret +} + +/// # Safety +/// +/// Called by ASoC on this driver's component with live arguments. +unsafe extern "C" fn asoc_pcm_close( + component: *mut bindings::snd_soc_component, + substream: *mut bindings::snd_pcm_substream, +) -> i32 { + // SAFETY: by the caller's contract. + if unsafe { rtd_is_be(substream_rtd(substream)) } { + return 0; + } + // SAFETY: as above. + let data = unsafe { component_data(component) }; + if data.sense.load(Relaxed) == substream { + data.sense.store(ptr::null_mut(), Relaxed); + } + // SAFETY: the dmaengine runtime was set up in `asoc_pcm_open`. + unsafe { bindings::snd_dmaengine_pcm_close_release_chan(substream) } +} + +/// # Safety +/// +/// Called by ASoC on this driver's component with live arguments. +unsafe extern "C" fn asoc_pcm_hw_params( + component: *mut bindings::snd_soc_component, + substream: *mut bindings::snd_pcm_substream, + params: *mut bindings::snd_pcm_hw_params, +) -> i32 { + // SAFETY: by the caller's contract. + if unsafe { rtd_is_be(substream_rtd(substream)) } { + return 0; + } + // SAFETY: the dmaengine runtime was set up in `asoc_pcm_open`. + let chan = unsafe { bindings::snd_dmaengine_pcm_get_chan(substream) }; + let mut cfg = bindings::dma_slave_config::default(); + // SAFETY: the substream and the parameters are live for the op. + let rc = unsafe { bindings::snd_hwparams_to_dma_slave_config(substream, params, &mut cfg) }; + if rc < 0 { + return rc; + } + // SAFETY: ASoC calls hw_params with live parameters. + let channels = unsafe { + (*params).intervals[(bindings::SNDRV_PCM_HW_PARAM_CHANNELS + - bindings::SNDRV_PCM_HW_PARAM_FIRST_INTERVAL) as usize] + .min + }; + // One frame per burst, up to four words. + let window = core::cmp::min(channels, 4); + // SAFETY: the substream is live. + if unsafe { is_playback(substream) } { + cfg.dst_port_window_size = window; + } else { + cfg.src_port_window_size = window; + } + // The sense capture: the frame size its channel restarts aligned to + // (apple-admac reads a u32 here; see `realign_sense`). + // SAFETY: by the caller's contract. + let data = unsafe { component_data(component) }; + let mut frame_bytes = channels * cfg.src_addr_width as u32; + if ptr::eq(substream, data.sense.load(Relaxed)) && data.cfg.realign_sense { + cfg.peripheral_config = ptr::from_mut(&mut frame_bytes).cast(); + cfg.peripheral_size = mem::size_of::(); + } + // SAFETY: `chan` is the live channel behind the substream. + unsafe { + match (*(*chan).device).device_config { + Some(f) => f(chan, &mut cfg), + None => ENOSYS.to_errno(), + } + } +} + +/// # Safety +/// +/// Called by ASoC on this driver's component with live arguments. +unsafe extern "C" fn asoc_pcm_trigger( + _component: *mut bindings::snd_soc_component, + substream: *mut bindings::snd_pcm_substream, + cmd: i32, +) -> i32 { + // SAFETY: by the caller's contract. + if unsafe { rtd_is_be(substream_rtd(substream)) } { + return 0; + } + // SAFETY: the dmaengine runtime was set up in `asoc_pcm_open`. + unsafe { bindings::snd_dmaengine_pcm_trigger(substream, cmd) } +} + +/// Wait for asynchronous DMA termination before ALSA reuses a front-end's buffer. +/// +/// # Safety +/// +/// Called by ASoC on this driver's component with a live substream. +unsafe extern "C" fn asoc_pcm_sync_stop( + _component: *mut bindings::snd_soc_component, + substream: *mut bindings::snd_pcm_substream, +) -> i32 { + // SAFETY: by the caller's contract. + if unsafe { rtd_is_be(substream_rtd(substream)) } { + return 0; + } + // SAFETY: the dmaengine runtime was set up in `asoc_pcm_open`. + unsafe { bindings::snd_dmaengine_pcm_sync_stop(substream) } +} + +/// # Safety +/// +/// Called by ASoC on this driver's component with live arguments. +unsafe extern "C" fn asoc_pcm_pointer( + _component: *mut bindings::snd_soc_component, + substream: *mut bindings::snd_pcm_substream, +) -> bindings::snd_pcm_uframes_t { + // SAFETY: by the caller's contract. + if unsafe { rtd_is_be(substream_rtd(substream)) } { + return (-i64::from(bindings::ENOTSUPP)) as bindings::snd_pcm_uframes_t; + } + // SAFETY: the dmaengine runtime was set up in `asoc_pcm_open`. + unsafe { bindings::snd_dmaengine_pcm_pointer(substream) } +} + +/// # Safety +/// +/// Called by ASoC with a live DAI of this component. +unsafe extern "C" fn dai_accept_fmt(_dai: *mut bindings::snd_soc_dai, _fmt: u32) -> i32 { + // The firmware runs the serializers; format, slots and clocks are its. + 0 +} + +/// # Safety +/// +/// Called by ASoC with a live DAI of this component. +unsafe extern "C" fn dai_accept_sysclk( + _dai: *mut bindings::snd_soc_dai, + _id: i32, + _freq: u32, + _dir: i32, +) -> i32 { + 0 +} + +/// # Safety +/// +/// Called by ASoC with a live DAI of this component. +unsafe extern "C" fn dai_accept_bclk_ratio(_dai: *mut bindings::snd_soc_dai, _ratio: u32) -> i32 { + 0 +} + +/// # Safety +/// +/// Called by ASoC with a live DAI of this component. +unsafe extern "C" fn dai_accept_tdm_slot( + _dai: *mut bindings::snd_soc_dai, + _tx_mask: u32, + _rx_mask: u32, + _slots: i32, + _slot_width: i32, +) -> i32 { + 0 +} + +/// The back-end's service for a stream; `None` for the speaker's sense +/// capture, which runs with the speaker's playback. +/// +/// # Safety +/// +/// `dai` must be a live DAI of this component, `substream` live. +unsafe fn be_service<'a>( + dai: *mut bindings::snd_soc_dai, + substream: *mut bindings::snd_pcm_substream, +) -> Option<(&'a T8152AopAudio, &'a Service)> { + // SAFETY: by the caller's contract. + let (data, id, playback) = unsafe { (dai_data(dai), (*dai).id as u32, is_playback(substream)) }; + let svc = match (id, playback) { + (BE_DAI_ID_COUT, true) => &data.cout, + (BE_DAI_ID_COUT, false) => &data.cin, + (BE_DAI_ID_SPKR, true) => &data.spkr, + _ => return None, + }; + Some((data, svc)) +} + +/// # 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. + let Some((data, svc)) = (unsafe { be_service(dai, substream) }) else { + // SAFETY: as above. + let (data, id) = unsafe { (dai_data(dai), (*dai).id as u32) }; + if id == BE_DAI_ID_SPKR { + return match data.sense_startup() { + Ok(()) => 0, + Err(e) => e.to_errno(), + }; + } + return 0; + }; + match data.startup(svc) { + Ok(()) => 0, + Err(e) => e.to_errno(), + } +} + +/// # 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 Some((data, svc)) = (unsafe { be_service(dai, substream) }) else { + return 0; + }; + match data.prepare(svc) { + Ok(()) => 0, + Err(e) => e.to_errno(), + } +} + +/// # 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. + if let Some((data, svc)) = unsafe { be_service(dai, substream) } { + data.shutdown(svc); + } +} + +struct DaiOps { + be: bindings::snd_soc_dai_ops, + fe: bindings::snd_soc_dai_ops, +} + +impl DaiOps { + fn new() -> Self { + let fe = bindings::snd_soc_dai_ops { + set_fmt: Some(dai_accept_fmt), + set_sysclk: Some(dai_accept_sysclk), + set_bclk_ratio: Some(dai_accept_bclk_ratio), + set_tdm_slot: Some(dai_accept_tdm_slot), + ..Default::default() + }; + DaiOps { + be: bindings::snd_soc_dai_ops { + startup: Some(be_startup), + prepare: Some(be_prepare), + shutdown: Some(be_shutdown), + ..fe + }, + fe, + } + } +} + +fn pcm_stream(name: &'static CStr, channels: u32) -> bindings::snd_soc_pcm_stream { + pcm_stream_fmt(name, channels, FORMATS) +} + +fn pcm_stream_fmt( + name: &'static CStr, + channels: u32, + formats: u64, +) -> 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 DAI table, indexed by `sound-dai = <&aop_audio N>` in the DTS: +/// 0 cout (jack back-end), 1 and 2 the playback front-ends, 3 spkr (speaker +/// back-end), 4 the sense front-end. +const DAI_COUNT: usize = 2 + FE_COUNT; + +fn dai_drivers(ops: &DaiOps, cfg: &HwConfig) -> [bindings::snd_soc_dai_driver; DAI_COUNT] { + [ + bindings::snd_soc_dai_driver { + name: c"cout".as_char_ptr(), + id: BE_DAI_ID_COUT, + ops: &ops.be, + playback: pcm_stream(c"COUT TX", 2), + capture: pcm_stream(c"CIN RX", 1), + ..Default::default() + }, + bindings::snd_soc_dai_driver { + name: c"j873-pcm-0".as_char_ptr(), + id: FE_DAI_ID_BASE, + ops: &ops.fe, + playback: pcm_stream(c"PCM0 TX", 2), + capture: pcm_stream(c"PCM0 RX", 1), + ..Default::default() + }, + // The speaker: one 32-bit slot per amplifier. + bindings::snd_soc_dai_driver { + name: c"j873-pcm-1".as_char_ptr(), + id: FE_DAI_ID_BASE + 1, + ops: &ops.fe, + playback: pcm_stream(c"PCM1 TX", cfg.spkr_channels), + ..Default::default() + }, + bindings::snd_soc_dai_driver { + name: c"spkr".as_char_ptr(), + id: BE_DAI_ID_SPKR, + ops: &ops.be, + playback: pcm_stream(c"SPKR TX", cfg.spkr_channels), + capture: pcm_stream_fmt(c"SPKR RX", cfg.sense_channels, cfg.sense_formats), + ..Default::default() + }, + bindings::snd_soc_dai_driver { + name: c"j873-pcm-2".as_char_ptr(), + id: FE_DAI_ID_BASE + 2, + ops: &ops.fe, + capture: pcm_stream_fmt(c"PCM2 RX", cfg.sense_channels, cfg.sense_formats), + ..Default::default() + }, + ] +} + +impl AsocContext { + fn new(data: Arc) -> Result> { + let ops = KBox::new(DaiOps::new(), GFP_KERNEL)?; + let dais = KBox::new(Opaque::new(dai_drivers(&ops, data.cfg)), GFP_KERNEL)?; + let component = bindings::snd_soc_component_driver { + name: c"t8152-aop-audio".as_char_ptr(), + open: Some(asoc_pcm_open), + close: Some(asoc_pcm_close), + hw_params: Some(asoc_pcm_hw_params), + trigger: Some(asoc_pcm_trigger), + sync_stop: Some(asoc_pcm_sync_stop), + pointer: Some(asoc_pcm_pointer), + ..Default::default() + }; + Ok(KBox::new( + Self { + component, + data, + dais, + _ops: ops, + }, + GFP_KERNEL, + )?) + } +} + +// ---- the microphone array's card ---- + +/// # Safety +/// +/// `substream` must be a live substream of the microphone PCM, whose private +/// data is the `Arc` leaked into it by `MicCard::new` (released in +/// `mic_pcm_free` after the last op). +unsafe fn mic_data<'a>(substream: *mut bindings::snd_pcm_substream) -> ArcBorrow<'a, T8152AopAudio> { + // SAFETY: by the caller's contract. + unsafe { Arc::::borrow((*(*substream).pcm).private_data.cast()) } +} + +/// The LEAP produces float32 into the channel requested here; the channel's +/// registers only answer with the LEAP leaves up, so the service (and with +/// it the leaves) comes up before the request and goes down after the +/// release. +/// +/// # Safety +/// +/// Called by ALSA with a live substream of the microphone PCM. +unsafe extern "C" fn mic_pcm_open(substream: *mut bindings::snd_pcm_substream) -> i32 { + // SAFETY: by the caller's contract. + let data = unsafe { mic_data(substream) }; + // A failed startup has undone its leaves; the service stays in `pw0 `. + if let Err(e) = data.hpai_startup() { + return e.to_errno(); + } + // SAFETY: the device is live and the name is NUL-terminated. + let chan = match from_err_ptr(unsafe { + bindings::dma_request_chan(data.dev.as_raw(), c"hpai".as_char_ptr()) + }) { + Ok(chan) => chan, + Err(e) => { + dev_err!(data.dev, "hpai: DMA channel unavailable: {:?}\n", e); + data.hpai_shutdown(); + return e.to_errno(); + } + }; + let hw = bindings::snd_pcm_hardware { + rates: bindings::SNDRV_PCM_RATE_48000, + rate_min: HPAI_RATE, + rate_max: HPAI_RATE, + channels_min: HPAI_CHANNELS, + channels_max: HPAI_CHANNELS, + ..pcm_hw() + }; + // SAFETY: the substream is live and the channel was requested above. + let ret = unsafe { + dmaengine_open(substream, chan, hw, bindings::BINDINGS_SNDRV_PCM_FMTBIT_FLOAT_LE) + }; + if ret < 0 { + // SAFETY: the open failed, so the channel is still ours. + unsafe { bindings::dma_release_channel(chan) }; + data.hpai_shutdown(); + } + ret +} + +/// # Safety +/// +/// Called by ALSA with a live substream of the microphone PCM. +unsafe extern "C" fn mic_pcm_close(substream: *mut bindings::snd_pcm_substream) -> i32 { + // SAFETY: by the caller's contract. + let data = unsafe { mic_data(substream) }; + data.flush_hpai_start(); + // SAFETY: the dmaengine runtime was set up in `mic_pcm_open`. + let ret = unsafe { bindings::snd_dmaengine_pcm_close_release_chan(substream) }; + data.hpai_shutdown(); + ret +} + +/// Finish queued firmware work and asynchronous DMA termination before ALSA +/// reuses or frees the capture buffer, as in the T8140 capture path. +/// +/// # Safety +/// +/// Called by ALSA with a live microphone substream and dmaengine runtime. +unsafe extern "C" fn mic_pcm_sync_stop(substream: *mut bindings::snd_pcm_substream) -> i32 { + // SAFETY: by the caller's contract. + let data = unsafe { mic_data(substream) }; + data.flush_hpai_start(); + // SAFETY: the runtime remains live until close. + unsafe { bindings::snd_dmaengine_pcm_sync_stop(substream) } +} + +/// # Safety +/// +/// Called by ALSA with a live substream of the microphone PCM and live +/// parameters. +unsafe extern "C" fn mic_pcm_hw_params( + substream: *mut bindings::snd_pcm_substream, + params: *mut bindings::snd_pcm_hw_params, +) -> i32 { + // SAFETY: the dmaengine runtime was set up in `mic_pcm_open`. + let chan = unsafe { bindings::snd_dmaengine_pcm_get_chan(substream) }; + let mut cfg = bindings::dma_slave_config::default(); + // SAFETY: the substream and the parameters are live for the op. + let rc = unsafe { bindings::snd_hwparams_to_dma_slave_config(substream, params, &mut cfg) }; + if rc < 0 { + return rc; + } + // Three words per frame, which the ADMAC cannot mark (it frames 2 or 4): + // a plain word stream. + // SAFETY: `chan` is the live channel behind the substream. + unsafe { + match (*(*chan).device).device_config { + Some(f) => f(chan, &mut cfg), + None => ENOSYS.to_errno(), + } + } +} + +/// ALSA calls prepare unconditionally. Nothing to arm: the DMA start and +/// the queued firmware run belong to trigger. +/// +/// # Safety +/// +/// Called by ALSA with a live substream of the microphone PCM. +unsafe extern "C" fn mic_pcm_prepare(_substream: *mut bindings::snd_pcm_substream) -> i32 { + 0 +} + +/// With the stream lock held: the DMA first, then the firmware's `pwrd` +/// queued behind it. +/// +/// # Safety +/// +/// Called by ALSA with a live substream of the microphone PCM. +unsafe extern "C" fn mic_pcm_trigger(substream: *mut bindings::snd_pcm_substream, cmd: i32) -> i32 { + // SAFETY: by the caller's contract. + let data = unsafe { mic_data(substream) }; + // SAFETY: the dmaengine runtime was set up in `mic_pcm_open`. + let ret = unsafe { bindings::snd_dmaengine_pcm_trigger(substream, cmd) }; + if ret < 0 { + return ret; + } + match cmd as u32 { + bindings::SNDRV_PCM_TRIGGER_START + | bindings::SNDRV_PCM_TRIGGER_RESUME + | bindings::SNDRV_PCM_TRIGGER_PAUSE_RELEASE => { + data.hpai_want_run.store(true, Relaxed); + // Err: already queued, and that run sees `hpai_want_run`. + let _ = workqueue::system().enqueue::<_, HPAI_START_WORK>(Arc::from(data)); + } + _ => data.hpai_want_run.store(false, Relaxed), + } + 0 +} + +/// # Safety +/// +/// Called once by ALSA when the PCM goes away. +unsafe extern "C" fn mic_pcm_free(pcm: *mut bindings::snd_pcm) { + // SAFETY: the `Arc` leaked into the private data by `MicCard::new`. + drop(unsafe { Arc::::from_foreign((*pcm).private_data.cast()) }); +} + +static MIC_PCM_OPS: bindings::snd_pcm_ops = bindings::snd_pcm_ops { + open: Some(mic_pcm_open), + close: Some(mic_pcm_close), + ioctl: None, + hw_params: Some(mic_pcm_hw_params), + hw_free: None, + prepare: Some(mic_pcm_prepare), + trigger: Some(mic_pcm_trigger), + sync_stop: Some(mic_pcm_sync_stop), + pointer: Some(bindings::snd_dmaengine_pcm_pointer), + get_time_info: None, + fill_silence: None, + copy: None, + page: None, + mmap: None, + ack: None, +}; + +/// Copies a NUL-terminated string into a fixed-size field, truncated to fit. +fn copy_str(target: &mut [u8], source: &CStr) { + let b = source.to_bytes(); + let n = b.len().min(target.len() - 1); + target[..n].copy_from_slice(&b[..n]); + target[n] = 0; +} + +/// The microphone array's card: one capture PCM, named like the other AOP +/// microphone cards (AppleJxxxHPAI) so that the UCM and DSP configurations +/// for those find it. +struct MicCard(*mut bindings::snd_card); + +impl MicCard { + fn new(data: &Arc) -> Result { + let fwnode = data.dev.fwnode().ok_or(ENODEV)?; + let chassis = fwnode + .property_read::(c_str!("apple,chassis-name")) + .required_by(&data.dev)?; + let kind = fwnode + .property_read::(c_str!("apple,machine-kind")) + .required_by(&data.dev)?; + let id = CString::try_from_fmt(fmt!("Apple{}HPAI", *chassis))?; + let shortname = CString::try_from_fmt(fmt!("{} {} HPAI", *kind, *chassis))?; + let longname = + CString::try_from_fmt(fmt!("{} {} High-Power Audio Interface", *kind, *chassis))?; + let mut card = ptr::null_mut(); + // SAFETY: the device and the module are live; `card` receives the card. + let ret = unsafe { + bindings::snd_card_new( + data.dev.as_raw(), + -1, + ptr::null(), + kernel::module::this_module::().as_ptr(), + 0, + &mut card, + ) + }; + if ret < 0 { + return Err(Error::from_errno(ret)); + } + // From here on, dropping `this` frees the card. + let this = MicCard(card); + // SAFETY: the card is new and unregistered. + unsafe { + copy_str(&mut (*card).driver, c"aop_audio"); + copy_str(&mut (*card).id, &id); + copy_str(&mut (*card).shortname, &shortname); + copy_str(&mut (*card).longname, &longname); + } + let mut pcm = ptr::null_mut(); + // SAFETY: the card is live; `pcm` receives the PCM. + let ret = unsafe { bindings::snd_pcm_new(card, c"hpai".as_char_ptr(), 0, 0, 1, &mut pcm) }; + if ret < 0 { + return Err(Error::from_errno(ret)); + } + // SAFETY: `pcm` is live; the ops are static and the private data is + // an `Arc` that `mic_pcm_free` releases. + unsafe { + bindings::snd_pcm_set_ops(pcm, bindings::SNDRV_PCM_STREAM_CAPTURE as i32, &MIC_PCM_OPS); + (*pcm).private_data = data.clone().into_foreign().cast(); + (*pcm).private_free = Some(mic_pcm_free); + copy_str(&mut (*pcm).name, c"Microphone Array"); + } + // SAFETY: the card is live and complete. + let ret = unsafe { bindings::snd_card_register(card) }; + if ret < 0 { + return Err(Error::from_errno(ret)); + } + Ok(this) + } +} + +impl Drop for MicCard { + fn drop(&mut self) { + // SAFETY: created by `snd_card_new`; this waits for open files. + unsafe { bindings::snd_card_free(self.0) }; + } +} + +struct T8152AopAudioDriver { + dev: ARef, + asoc: KBox, + registered: AtomicFlag, + /// Held for its drop, which frees the card after unbind. + _mic: Option, +} + +// SAFETY: the ASoC context and the card are only touched by ALSA/ASoC +// callbacks and by probe and remove, which the driver core serializes. +unsafe impl Send for T8152AopAudioDriver {} +// SAFETY: as for `Send`. +unsafe impl Sync for T8152AopAudioDriver {} + +impl T8152AopAudioDriver { + /// Unregisters the component. Must run from unbind: the component is + /// device-managed and freed with the device's resources after unbind. + fn unregister(&self) { + if !self.registered.xchg(false, Relaxed) { + return; + } + // SAFETY: registered in probe; the context outlives the unregister. + unsafe { + bindings::snd_soc_unregister_component_by_driver( + self.dev.as_raw(), + &self.asoc.component, + ) + }; + } +} + +kernel::of_device_table!( + OF_TABLE, + ::IdInfo, + [ + (of::DeviceId::new(c_str!("apple,t8142-aop-audio")), &HW_CFG_T8142), + (of::DeviceId::new(c_str!("apple,t8152-aop-audio")), &HW_CFG_T8152), + ] +); + +impl platform::Driver for T8152AopAudioDriver { + type IdInfo = &'static HwConfig; + type Data<'bound> = Self; + + const OF_ID_TABLE: Option> = Some(&OF_TABLE); + + fn probe<'bound>( + pdev: &'bound platform::Device>, + info: Option<&'bound Self::IdInfo>, + ) -> impl PinInit, Error> + 'bound { + let dev = ARef::::from(pdev.as_ref()); + let cfg: &'static HwConfig = info.copied().unwrap_or(&HW_CFG_T8152); + // SAFETY: this is a service device the AOP core registered. + let adata = unsafe { ::from_child(pdev.as_ref()) }?; + // SAFETY: the AOP core sets the child's platform data to its EPIC service. + let service = unsafe { (*dev.as_raw()).platform_data as *const EPICService }; + if service.is_null() { + return Err(ENODEV); + } + // SAFETY: checked above; it lives as long as the child. + let service = unsafe { *service }; + + // Every serializer leaf is required: the firmware's streams run on + // them, so the driver must not bind without one (or before its + // provider has probed). + let mut mca = KVec::new(); + for name in MCA_NAMES { + match PowerDomain::attach(&dev, name) { + Ok(Some(pd)) => mca.push(pd, GFP_KERNEL)?, + Ok(None) => { + dev_err!(dev, "power domain {:?} is missing\n", name); + return Err(ENODEV); + } + Err(e) => { + if e != EPROBE_DEFER { + dev_err!(dev, "power domain {:?}: {:?}\n", name, e); + } + return Err(e); + } + } + } + + // The microphone array, where the DT describes it: all of its leaves + // or none. + let mut leap = KVec::new(); + for name in LEAP_NAMES { + match PowerDomain::attach(&dev, name) { + Ok(Some(pd)) => leap.push(pd, GFP_KERNEL)?, + Ok(None) => {} + Err(e) => return Err(e), + } + } + if !leap.is_empty() && leap.len() != LEAP_NAMES.len() { + dev_err!(dev, "incomplete microphone power domains\n"); + return Err(ENODEV); + } + + let data_dev = dev.clone(); + let data = Arc::pin_init( + pin_init!(T8152AopAudio { + dev: data_dev, + adata, + service, + cout: Service::new(from_fourcc(b"cout"), "cout", true), + cin: Service::new(from_fourcc(b"cin "), "cin", true), + spkr: Service::new(from_fourcc(b"spkr"), "spkr", false), + mca, + cfg, + sense: Atomic::new(ptr::null_mut()), + hpai: Service::new(from_fourcc(b"hpai"), "hpai", false), + leap, + hpai_want_run: AtomicFlag::new(false), + hpai_start_work <- new_work!("T8152AopAudio::hpai_start_work"), + }), + GFP_KERNEL, + )?; + for svc in [&data.cout, &data.cin, &data.spkr] { + data.attach(svc)?; + } + + // The microphones are optional: the speakers and the jack stay + // usable without them. + let mic = if data.leap.is_empty() { + None + } else { + match MicCard::new(&data) { + Ok(card) => Some(card), + Err(e) => { + dev_err!(dev, "microphone card: {:?}\n", e); + None + } + } + }; + + let asoc = AsocContext::new(data)?; + // SAFETY: the descriptor and the DAI table are heap allocations owned + // by `asoc`, which the driver data keeps until after unregister. + let ret = unsafe { + bindings::snd_soc_register_component_d( + dev.as_raw(), + &asoc.component, + asoc.dais.get().cast(), + DAI_COUNT as i32, + ) + }; + if ret < 0 { + dev_err!(dev, "unable to register the ASoC component: {}\n", ret); + return Err(Error::from_errno(ret)); + } + Ok(Self { + dev, + asoc, + registered: AtomicFlag::new(true), + _mic: mic, + }) + } + + fn unbind(_dev: &platform::Device>, this: Pin<&Self>) { + this.unregister(); + } +} + +module_platform_driver! { + type: T8152AopAudioDriver, + name: "snd_soc_t8152_aop_audio", + description: "Apple T8152 AOP audio: headphone jack, speakers and microphones", + license: "Dual MIT/GPL", +} diff --git a/sound/soc/codecs/cs42l84.c b/sound/soc/codecs/cs42l84.c index 022420be6bfb5..07a47433ef4d9 100644 --- a/sound/soc/codecs/cs42l84.c +++ b/sound/soc/codecs/cs42l84.c @@ -279,6 +279,8 @@ static const struct snd_soc_dapm_route cs42l84_audio_map[] = { {"ADC", NULL, "HS"}, {"SDOUT1 Select", "ADC", "ADC"}, + {"SDOUT1 Select", "ASP RX CH1", "SDIN1"}, + {"SDOUT1 Select", "ASP RX CH2", "SDIN2"}, {"SDOUT1", NULL, "SDOUT1 Select"}, {"Capture", NULL, "SDOUT1"}, diff --git a/sound/soc/codecs/tas2764-bclk-ratio-test.h b/sound/soc/codecs/tas2764-bclk-ratio-test.h new file mode 100644 index 0000000000000..e395d8b099751 --- /dev/null +++ b/sound/soc/codecs/tas2764-bclk-ratio-test.h @@ -0,0 +1,158 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * KUnit tests of tas2764_parse_bclk_ratio() and tas2764_apply_bclk_ratio(): + * an amp whose node has no ti,bclk-ratio, as on every MCA machine (M1, M2, + * M3, M4), keeps the ratio auto-detection and its probe writes neither + * TAS2764_CLK_CFG nor TDM_CFG0's fixed-ratio bit; only the property sets + * them. Uses the nodes of tas2764-kunit.dtso and tas2764_test_priv(). + */ +#include +#include + +/* Page 0, where both registers are */ +#define TAS2764_TEST_NREGS TAS2764_REG(0x1, 0x0) + +/* Register values and every write, behind a regmap without a bus */ +struct tas2764_test_regs { + unsigned int val[TAS2764_TEST_NREGS]; + unsigned int writes; +}; + +static int tas2764_test_reg_read(void *context, unsigned int reg, unsigned int *val) +{ + struct tas2764_test_regs *regs = context; + + if (reg >= TAS2764_TEST_NREGS) + return -EINVAL; + *val = regs->val[reg]; + return 0; +} + +static int tas2764_test_reg_write(void *context, unsigned int reg, unsigned int val) +{ + struct tas2764_test_regs *regs = context; + + if (reg >= TAS2764_TEST_NREGS) + return -EINVAL; + regs->val[reg] = val; + regs->writes++; + return 0; +} + +static const struct regmap_config tas2764_test_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .max_register = TAS2764_TEST_NREGS - 1, + .reg_read = tas2764_test_reg_read, + .reg_write = tas2764_test_reg_write, + .cache_type = REGCACHE_NONE, +}; + +/* + * The driver state for test node @node, as tas2764_i2c_probe() leaves it + * before the device tree is read, with a recording register map. + */ +static struct tas2764_priv *tas2764_test_bclk_priv(struct kunit *test, const char *node, + struct device **devp, + struct tas2764_test_regs **regsp) +{ + struct tas2764_test_regs *regs; + struct tas2764_priv *tas2764; + struct device *mapdev; + + tas2764 = tas2764_test_priv(test, node, DEVID_SN012776, devp); + regs = kunit_kzalloc(test, sizeof(*regs), GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, regs); + mapdev = kunit_device_register(test, "tas2764-bclk-test"); + KUNIT_ASSERT_FALSE(test, IS_ERR(mapdev)); + tas2764->regmap = devm_regmap_init(mapdev, NULL, regs, &tas2764_test_regmap_config); + KUNIT_ASSERT_FALSE(test, IS_ERR(tas2764->regmap)); + + tas2764->bclk_ratio_code = -1; + /* Whatever TDM_CFG0 held: only the fixed-ratio bit may change */ + regs->val[TAS2764_TDM_CFG0] = TAS2764_TDM_CFG0_44_1_48KHZ | TAS2764_TDM_CFG0_FRAME_START; + *regsp = regs; + return tas2764; +} + +/* Amp nodes without ti,bclk-ratio */ +static const char *const tas2764_bclk_absent_nodes[] = { + /* With the ADT blocks the boot loader copies on the MCA machines */ + "tas2764-kunit-bop-good", + "tas2764-kunit-bop-none", +}; + +static void tas2764_bclk_absent_desc(const char *const *node, char *desc) +{ + strscpy(desc, *node, KUNIT_PARAM_DESC_SIZE); +} + +KUNIT_ARRAY_PARAM(tas2764_bclk_absent, tas2764_bclk_absent_nodes, tas2764_bclk_absent_desc); + +/* No ti,bclk-ratio: auto-detection, and not one register written */ +static void tas2764_bclk_ratio_absent(struct kunit *test) +{ + const char *const *node = test->param_value; + struct tas2764_test_regs *regs; + struct tas2764_priv *tas2764; + struct device *dev; + + tas2764 = tas2764_test_bclk_priv(test, *node, &dev, ®s); + KUNIT_EXPECT_EQ(test, tas2764_parse_bclk_ratio(dev, tas2764), 0); + KUNIT_EXPECT_EQ(test, tas2764->bclk_ratio_code, -1); + KUNIT_EXPECT_EQ(test, tas2764_apply_bclk_ratio(tas2764), 0); + KUNIT_EXPECT_EQ(test, regs->writes, 0); + KUNIT_EXPECT_EQ(test, regs->val[TAS2764_CLK_CFG], 0); + KUNIT_EXPECT_EQ(test, regs->val[TAS2764_TDM_CFG0], + TAS2764_TDM_CFG0_44_1_48KHZ | TAS2764_TDM_CFG0_FRAME_START); +} + +/* 250 BCLKs (J813, J873): its code, and the fixed-ratio bit beside the rest */ +static void tas2764_bclk_ratio_250(struct kunit *test) +{ + struct tas2764_test_regs *regs; + struct tas2764_priv *tas2764; + struct device *dev; + + tas2764 = tas2764_test_bclk_priv(test, "tas2764-kunit-bclk-250", &dev, ®s); + KUNIT_EXPECT_EQ(test, tas2764_parse_bclk_ratio(dev, tas2764), 0); + KUNIT_EXPECT_EQ(test, tas2764->bclk_ratio_code, 0x30); + KUNIT_EXPECT_EQ(test, tas2764_apply_bclk_ratio(tas2764), 0); + KUNIT_EXPECT_EQ(test, regs->val[TAS2764_CLK_CFG], 0x30); + KUNIT_EXPECT_EQ(test, regs->val[TAS2764_TDM_CFG0], + TAS2764_TDM_CFG0_44_1_48KHZ | TAS2764_TDM_CFG0_FRAME_START | + TAS2764_TDM_CFG0_FIXED_RATIO); + KUNIT_EXPECT_EQ(test, regs->writes, 2); + /* The register cache keeps both across a reset and a resume */ + KUNIT_EXPECT_TRUE(test, tas2764_writeable_register(NULL, TAS2764_CLK_CFG)); + KUNIT_EXPECT_TRUE(test, tas2764_writeable_register(NULL, TAS2764_TDM_CFG0)); +} + +/* A ratio without a known code fails the probe instead of guessing */ +static void tas2764_bclk_ratio_unknown(struct kunit *test) +{ + struct tas2764_test_regs *regs; + struct tas2764_priv *tas2764; + struct device *dev; + + tas2764 = tas2764_test_bclk_priv(test, "tas2764-kunit-bclk-64", &dev, ®s); + KUNIT_EXPECT_EQ(test, tas2764_parse_bclk_ratio(dev, tas2764), -EINVAL); + KUNIT_EXPECT_EQ(test, tas2764->bclk_ratio_code, -1); + KUNIT_EXPECT_EQ(test, tas2764_apply_bclk_ratio(tas2764), 0); + KUNIT_EXPECT_EQ(test, regs->writes, 0); +} + +static struct kunit_case tas2764_bclk_ratio_test_cases[] = { + KUNIT_CASE_PARAM(tas2764_bclk_ratio_absent, tas2764_bclk_absent_gen_params), + KUNIT_CASE(tas2764_bclk_ratio_250), + KUNIT_CASE(tas2764_bclk_ratio_unknown), + {} +}; + +static struct kunit_suite tas2764_bclk_ratio_test_suite = { + .name = "tas2764-bclk-ratio", + .init = tas2764_bop_test_init, + .test_cases = tas2764_bclk_ratio_test_cases, +}; + +kunit_test_suite(tas2764_bclk_ratio_test_suite); diff --git a/sound/soc/codecs/tas2764-kunit.dtso b/sound/soc/codecs/tas2764-kunit.dtso index db624a512f563..d24172668d8b5 100644 --- a/sound/soc/codecs/tas2764-kunit.dtso +++ b/sound/soc/codecs/tas2764-kunit.dtso @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-2.0-only /* - * Nodes for the TAS2764 KUnit tests (tas2764-bop-test.h). The values are made - * up; only their length and placement matter. + * Nodes for the TAS2764 KUnit tests (tas2764-bop-test.h and + * tas2764-bclk-ratio-test.h). The values are made up; only their length + * and placement matter. */ /dts-v1/; /plugin/; @@ -32,4 +33,15 @@ tas2764-kunit-bop-none { compatible = "test,tas2764-kunit"; }; + + /* tas2764-bclk-ratio-test.h */ + tas2764-kunit-bclk-250 { + compatible = "test,tas2764-kunit"; + ti,bclk-ratio = <250>; + }; + + tas2764-kunit-bclk-64 { + compatible = "test,tas2764-kunit"; + ti,bclk-ratio = <64>; + }; }; diff --git a/sound/soc/codecs/tas2764.c b/sound/soc/codecs/tas2764.c index d91944c975ce7..d038e0424659f 100644 --- a/sound/soc/codecs/tas2764.c +++ b/sound/soc/codecs/tas2764.c @@ -66,6 +66,8 @@ struct tas2764_priv { int v_sense_slot; int i_sense_slot; + /* TAS2764_CLK_CFG value for a fixed BCLK/FS ratio, or -1 to auto-detect */ + int bclk_ratio_code; bool dac_powered; bool unmuted; @@ -1067,6 +1069,26 @@ static const struct hwmon_chip_info tas2764_hwmon_chip_info = { .info = tas2764_hwmon_info, }; +/* + * Program the fixed BCLK/FS ratio from ti,bclk-ratio (tas2764_parse_bclk_ratio()). + * Without one nothing is written: the amplifier auto-detects the ratio. + */ +static int tas2764_apply_bclk_ratio(struct tas2764_priv *tas2764) +{ + int ret; + + if (tas2764->bclk_ratio_code < 0) + return 0; + + ret = regmap_write(tas2764->regmap, TAS2764_CLK_CFG, tas2764->bclk_ratio_code); + if (ret < 0) + return ret; + + return regmap_update_bits(tas2764->regmap, TAS2764_TDM_CFG0, + TAS2764_TDM_CFG0_FIXED_RATIO, + TAS2764_TDM_CFG0_FIXED_RATIO); +} + static int tas2764_codec_probe(struct snd_soc_component *component) { struct tas2764_priv *tas2764 = snd_soc_component_get_drvdata(component); @@ -1181,6 +1203,10 @@ static int tas2764_codec_probe(struct snd_soc_component *component) break; } + ret = tas2764_apply_bclk_ratio(tas2764); + if (ret < 0) + goto err_sdz; + /* * Last, so that no failure leaves the handler behind. The component owns the IRQ and * the remove frees it; not a devres of the I2C device, which outlives the component @@ -1499,6 +1525,41 @@ static int tas2764_parse_tx_zd_config(struct device *dev, struct tas2764_priv *t return 0; } +/* + * BCLK/FS ratios and the TAS2764_CLK_CFG value that makes the amplifier + * accept them. Only the J873 speaker frame (250 BCLKs, which the ratio + * auto-detection rejects with "bad TDM clock") is verified on hardware; + * other ratios are left to the auto-detection. + */ +static const struct { + u16 ratio; + u8 code; +} tas2764_bclk_ratio_codes[] = { + { 250, 0x30 }, +}; + +/* + * ti,bclk-ratio: a fixed BCLK/FS ratio, as its TAS2764_CLK_CFG code in + * bclk_ratio_code. Without the property bclk_ratio_code stays -1, set before + * the device tree is read, and the ratio is auto-detected. + */ +static int tas2764_parse_bclk_ratio(struct device *dev, struct tas2764_priv *tas2764) +{ + u32 ratio; + int i; + + if (device_property_read_u32(dev, "ti,bclk-ratio", &ratio)) + return 0; + + for (i = 0; i < ARRAY_SIZE(tas2764_bclk_ratio_codes); i++) + if (tas2764_bclk_ratio_codes[i].ratio == ratio) + tas2764->bclk_ratio_code = tas2764_bclk_ratio_codes[i].code; + if (tas2764->bclk_ratio_code < 0) + return dev_err_probe(dev, -EINVAL, "unsupported ti,bclk-ratio %u\n", ratio); + + return 0; +} + static int tas2764_parse_dt(struct device *dev, struct tas2764_priv *tas2764) { int ret = 0; @@ -1559,6 +1620,10 @@ static int tas2764_parse_dt(struct device *dev, struct tas2764_priv *tas2764) if (ret) return ret; + ret = tas2764_parse_bclk_ratio(dev, tas2764); + if (ret) + return ret; + return tas2764_group_join(dev, tas2764); } @@ -1580,6 +1645,8 @@ static int tas2764_i2c_probe(struct i2c_client *client) result = devm_mutex_init(&client->dev, &tas2764->fault_lock); if (result) return result; + /* Auto-detect the ratio unless the firmware node sets one. */ + tas2764->bclk_ratio_code = -1; i2c_set_clientdata(client, tas2764); dev_set_drvdata(&client->dev, tas2764); @@ -1649,4 +1716,5 @@ MODULE_LICENSE("GPL v2"); #if IS_ENABLED(CONFIG_SND_SOC_TAS2764_KUNIT_TEST) #include "tas2764-bop-test.h" +#include "tas2764-bclk-ratio-test.h" #endif diff --git a/sound/soc/codecs/tas2764.h b/sound/soc/codecs/tas2764.h index e6c6b064d6cc2..19691d94c4a1d 100644 --- a/sound/soc/codecs/tas2764.h +++ b/sound/soc/codecs/tas2764.h @@ -58,6 +58,12 @@ #define TAS2764_TDM_CFG0_44_1_48KHZ BIT(3) #define TAS2764_TDM_CFG0_88_2_96KHZ (BIT(3) | BIT(1)) #define TAS2764_TDM_CFG0_FRAME_START BIT(0) +/* + * Set by the macOS SN012776 driver together with an explicit BCLK/FS + * ratio in TAS2764_CLK_CFG; without it the ratio is auto-detected, which + * fails ("bad TDM clock") for frames such as 250 BCLKs. + */ +#define TAS2764_TDM_CFG0_FIXED_RATIO BIT(6) /* TDM Configuration Reg1 */ #define TAS2764_TDM_CFG1 TAS2764_REG(0X0, 0x09) @@ -124,6 +130,8 @@ /* Clock/IRQ Settings */ #define TAS2764_INT_CLK_CFG TAS2764_REG(0x0, 0x5c) #define TAS2764_INT_CLK_CFG_IRQZ_CLR BIT(2) +/* BCLK/FS ratio code, written by macOS for the J873 speaker frame */ +#define TAS2764_CLK_CFG TAS2764_REG(0x0, 0x60) #define TAS2764_BOP_CFG0 TAS2764_REG(0X0, 0x1d) /* BOP_CFG0 to BOP_CFG24, page 0 0x1d to 0x34 */