hoaug Posted January 24 Share Posted January 24 (edited) Hi everyone, hope you guys have a good day ! I am writing this post to ask for help from anyone with experience in IOKit or VoodooI2C driver development. I have a Lenovo Legion 7 (2021) Hackintosh. The internal speakers do not work because they are driven by a Cirrus Logic CS35L41 Smart Amplifier, which communicates via I2C. The Realtek ALC287 codec is working fine (headphones work), but the amplifier stays "muted" because macOS lacks the specific driver to wake it up and load the firmware. What I have done so far: Try to fix with simple way, use verb (alc-verb commands to write verbs ) Try to fix by create new layout-id I was realize that problem not from layout-id or verbs. The problem stems from the Cirrus AMP chip (cs35l41) in my laptop. Try Reverse Engineering: I analyzed the Linux driver (cs35l41-hda) and I2C logs. The chip requires a specific initialization sequence (Soft Reset -> Check ID -> Load DSP Firmware -> Enable Output). ACPI/SSDT: I successfully separated the speakers into two I2C devices in the DSDT. They now appear in IORegistry attached to the I2C controller. Kext Development: I am trying to write a kext that attaches to VoodooI2CDeviceNub to send the wakeup commands. The Problem: My current Kext code compiles and loads, but it causes system instability (freezes or reboots). I suspect I am handling the IOKit lifecycle (start/stop) or the I2C resource management incorrectly. I have posted my draft code below. Could someone please review it and point out what I am doing wrong regarding VoodooI2CDeviceNub usage or memory management? I have generated a routing graph from my codec dump to visualize the connection paths. Please see the attached image (Node IDs are shown in decimal) Codec-Dump-Dec.txt Here is a detailed breakdown of my findings based on the codec dump: Speaker Nodes (Layout-id 13 analysis): I see Node 23 and Node 20 as potential speakers. However, Node 20 seems to be physically disconnected (Pin Default shows "N/A" - disconnect status). Node 23 looks like the main speaker, but no matter how I build the layout-id, it does not output sound. Node 27 is a mystery; I am not sure what its function is. Our device probably has multiple speakers, each handling a specific function. Like: one speaker handles bass, another handles treble (this is just my guess). Headphones (Node 33): This works perfectly. The audio is clear and have good quality. Optical Output (Node 30): This is likely the digital port, so i ignore it. After spending hours investigating the Windows drivers, I discovered a device named "Cirrus Logic Awesome Speaker Amps". The Realtek Codec does not drive the speakers directly. It sends the signal to this separate Cirrus Logic amplifier chip. This chip communicates via the I2C interface. I checked the situation on Linux, and they had the exact same problem. The issue was only fixed around 2022 when a specific driver for this Cirrus I2C Amp was added to the Linux Kernel. Since macOS does not have this driver, standard solutions like AppleALC or VoodooHDA cannot control this amplifier. If my research is correct, the main obstacle is the Cirrus AMP chip. The signal path seems to be: macOS → AppleHDA → AppleALC (+ layout-id) → Realtek → Cirrus AMP → Audio I don't have deep hardware knowledge. Although I know some C/C++, it doesn't seem enough to debug this right now. It looks like a long road ahead for the kext I'm working on. I don't think VoodooHDA or a customized AppleHDA will work. I already tried customizing the AppleALC layout-id (similar to the methods in your links), but the result was zero. I verified this via Linux (dmesg) logs, which explicitly show: ubuntu@ubuntu:~$ sudo dmesg | grep -i "cs35l41" [ 9.646164] cs35l41-hda i2c-CLSA0100:00-cs35l41-hda.0: Using extra _DSD properties, bypassing _DSD in ACPI [ 9.682219] cs35l41-hda i2c-CLSA0100:00-cs35l41-hda.0: Cirrus Logic CS35L41 (35a40), Revision: B2 [ 9.682477] cs35l41-hda i2c-CLSA0100:00-cs35l41-hda.1: Using extra _DSD properties, bypassing _DSD in ACPI [ 9.682480] cs35l41-hda i2c-CLSA0100:00-cs35l41-hda.1: Reset line busy, assuming shared reset [ 9.717154] cs35l41-hda i2c-CLSA0100:00-cs35l41-hda.1: Cirrus Logic CS35L41 (35a40), Revision: B2 [ 9.806121] cs35l41-hda i2c-CLSA0100:00-cs35l41-hda.0: DSP1: cirrus/cs35l41-dsp1-spk-prot-17aa3847.wmfw: format 3 timestamp 0x5f80448c [ 9.806125] cs35l41-hda i2c-CLSA0100:00-cs35l41-hda.0: DSP1: cirrus/cs35l41-dsp1-spk-prot-17aa3847.wmfw: Fri 09 Oct 2020 13:07:57 W. Europe Daylight Time [ 10.227967] cs35l41-hda i2c-CLSA0100:00-cs35l41-hda.0: DSP1: Firmware: 400a4 vendor: 0x2 v0.21.0, 1 algorithms [ 10.228674] cs35l41-hda i2c-CLSA0100:00-cs35l41-hda.0: DSP1: cirrus/cs35l41-dsp1-spk-prot-17aa3847-spkid1-l0.bin: v0.21.0 [ 10.228676] cs35l41-hda i2c-CLSA0100:00-cs35l41-hda.0: DSP1: spk-prot: C:\Cirrus\Project\Lenovo_PC_Y760\Smart PA Tuning\Release Version\Y760_AMD_Version4.5_LS639\Lenovo_Y7 [ 10.299587] cs35l41-hda i2c-CLSA0100:00-cs35l41-hda.0: Calibration applied: R0=5846 [ 10.313732] cs35l41-hda i2c-CLSA0100:00-cs35l41-hda.0: Firmware Loaded - Type: spk-prot, Gain: 17 [ 10.313803] cs35l41-hda i2c-CLSA0100:00-cs35l41-hda.0: CS35L41 Bound - SSID: 17aa3847, BST: 4, VSPK: 0, CH: L, FW EN: 1, SPKID: 1 [ 10.313809] snd_hda_codec_realtek hdaudioC2D0: bound i2c-CLSA0100:00-cs35l41-hda.0 (ops cs35l41_hda_comp_ops [snd_hda_scodec_cs35l41]) [ 10.316209] cs35l41-hda i2c-CLSA0100:00-cs35l41-hda.1: DSP1: cirrus/cs35l41-dsp1-spk-prot-17aa3847.wmfw: format 3 timestamp 0x5f80448c [ 10.316213] cs35l41-hda i2c-CLSA0100:00-cs35l41-hda.1: DSP1: cirrus/cs35l41-dsp1-spk-prot-17aa3847.wmfw: Fri 09 Oct 2020 13:07:57 W. Europe Daylight Time [ 10.738052] cs35l41-hda i2c-CLSA0100:00-cs35l41-hda.1: DSP1: Firmware: 400a4 vendor: 0x2 v0.21.0, 1 algorithms [ 10.738764] cs35l41-hda i2c-CLSA0100:00-cs35l41-hda.1: DSP1: cirrus/cs35l41-dsp1-spk-prot-17aa3847-spkid1-r0.bin: v0.21.0 [ 10.738767] cs35l41-hda i2c-CLSA0100:00-cs35l41-hda.1: DSP1: spk-prot: C:\Cirrus\Project\Lenovo_PC_Y760\Smart PA Tuning\Release Version\Y760_AMD_Version4.5_LS639\Lenovo_Y7 [ 10.809779] cs35l41-hda i2c-CLSA0100:00-cs35l41-hda.1: Calibration applied: R0=5933 [ 10.823862] cs35l41-hda i2c-CLSA0100:00-cs35l41-hda.1: Firmware Loaded - Type: spk-prot, Gain: 17 [ 10.823929] cs35l41-hda i2c-CLSA0100:00-cs35l41-hda.1: CS35L41 Bound - SSID: 17aa3847, BST: 4, VSPK: 0, CH: R, FW EN: 1, SPKID: 1 [ 10.823934] snd_hda_codec_realtek hdaudioC2D0: bound i2c-CLSA0100:00-cs35l41-hda.1 (ops cs35l41_hda_comp_ops [snd_hda_scodec_cs35l41]) In addition, I have attached the hardware dumps (Codec, Pins, and dmesg) from Ubuntu for further technical reference: linux_working_codec.txtlinux_pins.txtlinux_dmesg.txt This confirms that the amplifier requires Active I2C Initialization and Firmware Loading (DSP Config) at every boot to function. Sending Verbs to the Realtek codec is insufficient because the Realtek chip is just the audio source, but the Amplifier itself remains in a "shutdown/unconfigured" state without the I2C handshake. But i found that, It’s NOT Just I2C: At first glance, it looks like a simple I2C devic, but that’s misleading. What i see in IORegistry (IOReg) is only the control interface. The actual audio stream does NOT go through I2C. The datasheet here: From what I've learned, it seems to work like this: Control Port (I2C / SPI) Used only for configuration Power states DSP firmware / register programming Enable / mute / boost control Serial Audio Port (I2S / TDM) This is where real audio data flows Comes from the SoC audio DSP / Realtek codec Internal DSP & Boost Converter: Must be initialized correctly What I Tried Tracing the Linux Driver I booted Linux and traced the exact I2C write sequence sent to CS35L41 during: Boot Resume Enable Disable I captured real register writes and converted them into a static “playback” sequence that should theoretically wake the amplifier. ACPI - SSDT Modifications The laptop has two speakers (stereo - maybe true because Cirrus had run two driver which is 0 and 1), but firmware exposes them as a single logical device I created an SSDT to disable the original SPKR device and split it into: SPL → Left speaker (I2C address 0x40) SPR → Right speaker (I2C address 0x41) DefinitionBlock ("", "SSDT", 2, "HACK", "CS35ABS", 0x00000000) { External (_SB_.GPIO, DeviceObj) External (_SB_.I2CD, DeviceObj) External (_SB_.I2CD.SPKR, DeviceObj) /* * I disable original device with this function and a rename patch in config.plist ( rename STA to * XSTA and this function override _STA to disable on macOS ) */ Method (_SB.I2CD.SPKR._STA, 0, NotSerialized) // _STA: Status { If (_OSI ("Darwin")) { Return (Zero) } } /* * Split to two device - I'm not sure if it's necessary. I just noticed that in Linux they call two * drivers for two things, so I'm trying to do something similar to Linux. */ Scope (_SB.I2CD) { Device (SPL) { Name (_HID, "CLSA0100") // _HID: Hardware ID Name (_CID, "CLSA0100") // _CID: Compatible ID Name (_UID, Zero) // _UID: Unique ID Method (_STA, 0, NotSerialized) // _STA: Status { Return (0x0F) } Method (_CRS, 0, NotSerialized) // _CRS: Current Resource Settings { Name (RBUF, ResourceTemplate () { I2cSerialBusV2 (0x0040, ControllerInitiated, 0x00061A80, AddressingMode7Bit, "\\_SB.I2CD", 0x00, ResourceConsumer, , Exclusive, ) GpioIo (Exclusive, PullDown, 0x0000, 0x0000, IoRestrictionOutputOnly, "\\_SB.GPIO", 0x00, ResourceConsumer, , ) { // Pin list 0x0006 } }) Return (RBUF) /* \_SB_.I2CD.SPL_._CRS.RBUF */ } } Device (SPR) { Name (_HID, "CLSA0100") // _HID: Hardware ID Name (_CID, "CLSA0100") // _CID: Compatible ID Name (_UID, One) // _UID: Unique ID Method (_STA, 0, NotSerialized) // _STA: Status { Return (0x0F) } Method (_CRS, 0, NotSerialized) // _CRS: Current Resource Settings { Name (RBUF, ResourceTemplate () { I2cSerialBusV2 (0x0041, ControllerInitiated, 0x00061A80, AddressingMode7Bit, "\\_SB.I2CD", 0x00, ResourceConsumer, , Exclusive, ) GpioIo (Exclusive, PullDown, 0x0000, 0x0000, IoRestrictionOutputOnly, "\\_SB.GPIO", 0x00, ResourceConsumer, , ) { // Pin list 0x0006 } }) Return (RBUF) /* \_SB_.I2CD.SPR_._CRS.RBUF */ } } } } Result: Both devices appear correctly under VoodooI2C and original device disabled No ACPI errors Writing the Kext The idea was simple: Mimic the Linux driver Attach to SPL / SPR On start() → send the captured I2C register sequence Force the amplifier to wake up and unmute I dumped the Linux register writes and converted them into a C++ header used by the kext. Log of i2c write and firmware ( driver ) i got: i2c_trace.txti2c.txtFirmware.hcs35l41-dsp1-spk-prot-17aa3847-spkid1-r0.bincs35l41-dsp1-spk-prot-17aa3847-spkid1-l0.bincs35l41-dsp1-spk-prot-17aa3847.wmfw I will provide additional information regarding the operational process of the driver. Executive Summary & Methodology: This report details the operational logic of the Linux kernel driver for the Cirrus Logic CS35L41 Smart Power Amplifier, derived from a cross-analysis of dmesg system logs and ftrace (function tracer) I2C bus traffic. The analysis confirms that the device operates via the I2C bus (specifically i2c-1) using 32-bit Big-Endian register addressing. The driver follows a strict state-machine initialization sequence: ACPI Override → Soft Reset → Device Identification → Clock Configuration → Firmware Injection (DSP) → Impedance Calibration → Output Enable. Hardware Interface & Addressing: Target Hardware: Dual Cirrus Logic CS35L41 Amplifiers (Stereo Pair). Communication Bus: I2C Bus 1 (Synopsys DesignWare I2C adapter). Slave Addresses: Left Channel (implied): 0x40 Right Channel (implied): 0x41 Data Protocol: 32-bit Register Address space. Data payloads are transmitted in Big-Endian format. Detailed Operational Workflow Phase I: ACPI Bypass and Property Injection Timestamp: [9.646164] (dmesg) Action: The driver explicitly ignores the ACPI _DSD (Device Specific Data) tables provided by the system BIOS. Technical Detail: The log entry Using extra _DSD properties, bypassing _DSD in ACPI indicates that the stock ACPI tables likely contain incorrect or generic GPIO/Interrupt configurations for this laptop. The Linux driver identifies the hardware via DMI (Desktop Management Interface) strings and injects a custom, hardcoded property set (including GPIO resets and interrupt lines) directly into the kernel's device struct. Phase II: Initialization & Soft Reset Timestamp: 3291.624597 (i2c_trace) Target Register: 0x00000040 (Global Control / Reset) Payload: [02-b8-05-c0] Analysis: The driver initiates communication by writing to the Global Control register. This specific payload triggers a "Soft Reset" or a "Wake" command to bring the Digital Signal Processor (DSP) core out of a low-power standby state. This is a prerequisite for any subsequent register reads; otherwise, the chip would not acknowledge (NACK) the I2C transaction. Phase III: Handshake & Hardware Identification (Crucial Step) Timestamp: 3291.624598 onwards Action: The driver verifies the physical hardware before attempting to load firmware. Device ID Check: Operation: Write Address 0x00000000 -> Read 4 Bytes. Observed Response: [00-03-5a-40] (Big Endian). Interpretation: This matches the Device ID 0x35a40 logged in dmesg. This confirms communication with a genuine CS35L41 chip. Revision ID Check: Operation: Write Address 0x00000004 -> Read 4 Bytes. Observed Response: [00-00-00-b2]. Interpretation: The chip is Silicon Revision B2. The driver uses this to determine which specific firmware patches to apply. Phase IV: Phase-Locked Loop (PLL) & Clock Stabilization Timestamp: ~ 3307.xxxx range Target Registers: 0x00004xxx range (e.g., 0x00004448, 0x00004c28) Analysis: Before the DSP can accept firmware, its internal clock speed must be ramped up and stabilized. The trace shows a flurry of writes to the 0x4xxx register block. These writes configure the PLL to multiply the base clock (usually from the I2C bus or an external crystal) to the high frequencies required for audio processing. The driver polls specific registers during this phase to ensure the PLL has "locked" before proceeding. Phase V: DSP Firmware Injection (High-Volume Data Transfer) Timestamp: 3313.514xxx - 3313.936xxx Action: Loading the wmfw (Halo Core Firmware) and bin (Tuning Data) files. Technical Detail: This is the most critical and time-sensitive phase. The driver performs the following sequence: DSP Unlock: Writes a specific key to unlock the DSP memory region for writing. Payload Transmission: The trace reveals massive, contiguous I2C write transactions. Unlike standard register configuration (which are 4-8 bytes), these are bulk data dumps: Chunk 1: 64 bytes (0x40 length) at 3313.514121. Chunk 2: 1600 bytes (0x640 length) at 3313.515178. Chunk 3: 4012 bytes (0xfa4 length) at 3313.936881 (Massive). DSP Lock: After the upload is complete, the driver locks the memory to prevent corruption. Context: The dmesg log confirms these payloads correspond to the file cs35l41-dsp1-spk-prot-17aa3847.wmfw (DSP Firmware) and ...-spkid1-l0.bin (Speaker Protection Tuning). This firmware contains the "Smart PA" algorithms that protect the speakers from blowing out at high volumes. Phase VI: Power-Up & Impedance Calibration Timestamp: Post-Firmware Load (10.299587 in dmesg) Action: The driver enables the main output stage. Analysis: Once the firmware is running, the driver commands the chip to perform a "Calibration" pass. It sends a test signal (inaudible or ultrasonic) through the voice coil to measure the resistance (Impedance). Result: Calibration applied: R0=5846. Significance: R0 is the DC resistance of the speaker. The DSP uses this baseline to calculate thermal limits. If this step fails (R0 is 0 or infinite), the driver assumes the speaker is disconnected or damaged and will disable audio output. Phase VII: Audio Interface Binding Timestamp: [10.823934] Action: Binding to snd_hda_codec_realtek. Analysis: Finally, the CS35L41 driver notifies the main HDA controller (Realtek ALC287) that the amp is ready. It effectively "attaches" itself as a satellite amp to the main audio codec, allowing macOS/Linux to see a unified audio device. At the very beginning, I tried a simple approach: I took the entire raw dump of 403 I2C write commands from the Linux trace and wrote a loop to blast them all into the registers at once during the Kext start() routine. And... The machine froze instantly. But the fun part was that after I forced a reboot and went back into Windows, the speakers were completely dead there too. I honestly thought I had physically bricked the amplifier chip (LoL). It only started working again after a full shutdown (cold boot). I realized that sending 403 commands in a tight blocking loop inside the kernel context was a suicide mission. It flooded the I2C bus, causing a deadlock because the controller couldn't keep up. Furthermore, the CS35L41 chip likely entered a "latch-up" or undefined logic state due to the rapid-fire commands, which is why the issue persisted across warm reboots until the power was completely cut. So, we can't follow what Linux does for this Cirrus ? Here is my code of the kext (just skeleton - idea) CirrusAudioFixup.hpp #ifndef CirrusAudioFixup_hpp #define CirrusAudioFixup_hpp #include <IOKit/IOService.h> #include <IOKit/IOTimerEventSource.h> #include <IOKit/IOLib.h> // include header of VoodooI2C #include "VoodooI2CDeviceNub.hpp" class CirrusAudioFixup : public IOService { OSDeclareDefaultStructors(CirrusAudioFixup) private: // pointer point to I2C device (Nub) VoodooI2CDeviceNub *i2cNub; // timer to delay initialization IOTimerEventSource *initTimer; public: virtual bool start(IOService *provider) override; virtual void stop(IOService *provider) override; virtual void free() override; // main function, run afteer timer end void safeInitAction(OSObject *owner, IOTimerEventSource *sender); // helper function to read/write register 32-bit Big Endian bool writeRegister(uint32_t reg, uint32_t value); uint32_t readRegister(uint32_t reg); }; #endif CirrusAudioFixup.cpp #include "CirrusAudioFixup.hpp" #define super IOService OSDefineMetaClassAndStructors(CirrusAudioFixup, IOService) // stage one: start // only timer bool CirrusAudioFixup::start(IOService *provider) { // log IOLog("CS35L41 [Start]: Driver is loading...\n"); // if start fail, stop by return false if (!super::start(provider)) { return false; } // casting provider to VoodooI2CDeviceNub i2cNub = OSDynamicCast(VoodooI2CDeviceNub, provider); if (!i2cNub) { IOLog("CS35L41 [Error]: Provider is NOT VoodooI2CDeviceNub!\n"); return false; } i2cNub->retain(); // 2. open connect to Nub if (!i2cNub->open(this)) { IOLog("CS35L41 [Error]: Cannot open VoodooI2CDeviceNub!\n"); i2cNub->release(); return false; } // 3. create Timer Event Source // wait system boot success then write to hardware initTimer = IOTimerEventSource::timerEventSource ( this, OSMemberFunctionCast(IOTimerEventSource::Action, this, &CirrusAudioFixup::safeInitAction )); if (!initTimer) { IOLog("CS35L41 [Error]: Failed to create Timer!\n"); i2cNub->close(this); i2cNub->release(); return false; } // 4. register Timer to WorkLoop IOWorkLoop *workLoop = getWorkLoop(); if (workLoop) { workLoop->addEventSource(initTimer); // timer 5000ms (5 second) then run function safeInitAction initTimer->setTimeoutMS(5000); IOLog("CS35L41 [Info]: Timer scheduled. Waiting 5s for safe boot...\n"); } else { IOLog("CS35L41 [Error]: Cannot get WorkLoop!\n"); return false; } return true; } // stage two: stop and clean void CirrusAudioFixup::stop(IOService *provider) { // log IOLog("CS35L41 [Stop]: Driver unloading...\n"); // stop timer if (initTimer) { initTimer->cancelTimeout(); // stop timer from proccssing stream if (getWorkLoop()){ getWorkLoop()->removeEventSource(initTimer) }; // free initTimer->release(); initTimer = NULL; } // close and free i2cNub if (i2cNub) { i2cNub->close(this); i2cNub->release(); i2cNub = NULL; } // then stop all thing super::stop(provider); } // function free void CirrusAudioFixup::free() { super::free(); } // stage 3: write to chip after 5 second // just a skeleton. i dont do anything special here void CirrusAudioFixup::safeInitAction(OSObject *owner, IOTimerEventSource *sender) { IOLog("CS35L41 [Action]: Timer fired! Starting hardware probe...\n"); // try wake chip (soft reset) - send 0x55 to 0x40 (global control) like what linux did // writeRegister(0x00000040, 0x00000055); // wait for the chip to make its awake // IOSleep(10); // read device-id (Register 0x00) // expected ID: 0x405a0300 (like log Linux) uint32_t deviceID = readRegister(0x00000000); IOLog("CS35L41 [Result]: Read DeviceID = 0x%08x\n", deviceID); if (deviceID == 0x405a0300) { IOLog("CS35L41 [SUCCESS]: Chip Detected! Communication Valid.\n"); } else { IOLog("CS35L41 [FAIL]: Wrong DeviceID or Bus Error.\n"); } } // helper to read/write register uint32_t CirrusAudioFixup::readRegister(uint32_t reg) { // Cirrus use Big Endian for address uint32_t regBE = OSSwapInt32(reg); uint32_t valBE = 0; // send 4 byte address -> read 4 byte data // use API writeReadI2C of VoodooI2C IOReturn ret = i2cNub->writeReadI2C((uint8_t*)®BE, 4, (uint8_t*)&valBE, 4); if (ret != kIOReturnSuccess) { IOLog("CS35L41 [I2C Error]: Read failed at reg 0x%x\n", reg); return 0; } // data we have after read also Big Endian, convert it to Little Endian for CPU Intel understand return OSSwapInt32(valBE); } bool CirrusAudioFixup::writeRegister(uint32_t reg, uint32_t value) { uint8_t buffer[8]; // reverse address and data to Big Endian uint32_t regBE = OSSwapInt32(reg); uint32_t valBE = OSSwapInt32(value); // match them to 1 packet 8 bytes: [4 byte address] + [4 byte value] memcpy(buffer, ®BE, 4); memcpy(buffer + 4, &valBE, 4); IOReturn ret = i2cNub->writeI2C(buffer, 8); return (ret == kIOReturnSuccess); } Info.plist <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>CFBundleDevelopmentRegion</key> <string>en</string> <key>CFBundleExecutable</key> <string>CirrusAudioFixup</string> <key>CFBundleIdentifier</key> <string>com.hoaug.CirrusAudioFixup</string> <key>CFBundleInfoDictionaryVersion</key> <string>6.0</string> <key>CFBundleName</key> <string>CirrusAudioFixup</string> <key>CFBundlePackageType</key> <string>KEXT</string> <key>CFBundleShortVersionString</key> <string>1.0</string> <key>CFBundleVersion</key> <string>1.0</string> <key>IOKitPersonalities</key> <dict> <key>CirrusAudioFixup</key> <dict> <key>CFBundleIdentifier</key> <string>com.hoaug.CirrusAudioFixup</string> <key>IOClass</key> <string>CirrusAudioFixup</string> <key>IONameMatch</key> <array> <string>SPL</string> <string>SPR</string> </array> <key>IOProviderClass</key> <string>VoodooI2CDeviceNub</string> </dict> </dict> <key>OSBundleLibraries</key> <dict> <key>com.apple.kpi.iokit</key> <string>14</string> <key>com.apple.kpi.libkern</key> <string>14</string> <key>com.apple.kpi.mach</key> <string>14</string> <key>com.alexandred.VoodooI2C</key> <string>2.0</string> </dict> </dict> </plist> At this point, I am completely stuck with my kext. I am facing two scenarios: If the system boots successfully: The kext simply doesn't load. I’ve checked the logs, and there isn't a single debug line from my kext. If the system fails to boot: It hangs during the boot process. It usually gets stuck at a point where another kext is loading (or perhaps right after it finishes). Debugging has been incredibly difficult because I get no logs or clear information to understand what’s going wrong. This is my first time developing a kext, and I don’t have much experience in this field. If you see anything in my code that looks excessive, incorrect, or just plain stupid (stupid idea), please bear with me. I am writing this post in the hope of finding some guidance to get through this roadblock. Any help or advice would be greatly appreciated. Thank you all so much! Edited February 18 by hoaug fix codec schematic image 1 Quote Link to comment https://www.insanelymac.com/forum/topic/362242-help-wanted-developing-kext-for-cirrus-logic-cs35l41-amplifier-i2c-on-legion-7/ Share on other sites More sharing options...
Slice Posted February 15 Share Posted February 15 Consider to insert these codes inside VoodooHDA. You will have more success. Quote Link to comment https://www.insanelymac.com/forum/topic/362242-help-wanted-developing-kext-for-cirrus-logic-cs35l41-amplifier-i2c-on-legion-7/#findComment-2847418 Share on other sites More sharing options...
hoaug Posted February 18 Author Share Posted February 18 Thanks for the suggestion, Slice! I really appreciate your input. To be honest, I'm still very new to driver - kext development, so integrating I2C logic into VoodooHDA feels a bit out of my depth right now. From what I observed in the Linux logs, the CS35L41 seems to rely heavily on I2C transactions for initialization. I'm afraid this might bloat the kext or make the code structure 'less clean' (and harder for me to maintain) compared to a standalone driver. That’s why I’m leaning towards the VoodooI2CDeviceNub approach - it seems more modular for my level of understanding. Since I lack the experience to bridge I2C communication within the HDA environment, I thought building a standalone kext that piggybacks on VoodooI2C would be a 'safer' route for my skill level. Could you let me know if this approach-sticking with a separate I2C driver-is viable given these constraints, or would you still recommend trying the VoodooHDA route despite the complexity? I'm a bit confused about the hardware side: Do you think I really need an SSDT to force 'Polling Mode' and manually assign GPIO pins to match the Windows configuration? Also, could I ask for your advice on the Info.plist matching? I'm a bit torn between using the ACPI Device Name (e.g., SPL) or the Hardware ID (_HID like CLSA0100). Which method is generally considered 'best practice' or more stable for this kind of driver? Quote Link to comment https://www.insanelymac.com/forum/topic/362242-help-wanted-developing-kext-for-cirrus-logic-cs35l41-amplifier-i2c-on-legion-7/#findComment-2847537 Share on other sites More sharing options...
Slice Posted February 18 Share Posted February 18 See PCIbus->ChipsetHDAController->(some internal bus, for example I2C)->HDAcodec VoodooHDA operates with "verbs" which sends to the HDA controller to tune HDA codec knowing nothing about the internal protocol. It just obeys to HDA standard. Let us suppose CirrusLogic is not compatible with this protocol (hmm, impossible, else AppleHDA will not work with CL used by Apple). In this hypothetical case you may implement your own procedures to send verbs by I2C to final HDA codec. The whole other work will done by VoodooHDA: register nodes, connect one to another, tune mixers, and apply DMA flow to DAC. Quote Link to comment https://www.insanelymac.com/forum/topic/362242-help-wanted-developing-kext-for-cirrus-logic-cs35l41-amplifier-i2c-on-legion-7/#findComment-2847544 Share on other sites More sharing options...
newhacker1746 Posted May 20 Share Posted May 20 (edited) Hi, I am in the process of cleaning up a moderately large codebase for a kext that implements the complete initialization flow for the cs35l41, which for me is present as acpi CSC3551 under _SB.I2CD.SPKR). I actually ran into your thread while searching some documentation on voodooi2c and some IOKit apis I'm using in my current phase of implementation--OSKextRequestResource) and had no idea Hackintosh community had been on this a bit already! Cheers to your attempt, genuinely, and apologies for coming in so late. I've been studying the linux implementation and writing an IOKit implementation based off of it. I've settled on extracting the metadata (Linux has various tables of data looked up by subsystem id to match speaker configs, dsp firmware, etc) with some python scripts and encoding that into source files, and using that in original c++ logic I'm writing that operates on that metadata inspired by reading through the linux source. You are correct that the i2c interface enumerated in ACPI is actually only for control, and that I2S is the digital audio input I have far more extensive documentation that I've painstakingly written up in my currently private, local repo, and will publish as soon as the accompanying code that implements the design isn't so much as a spaghetti mess, but to summarize: - the audio CODEC (ie Realtek) hasn't needed specific changes to support it. The correct i2s output is linked to the i2s input in my setup (Zenbook UM3402Y) - like Slice said, the verbs operate as defined by hda standard in both the sender and receiver treating internal implementation in the receiver as a black box. The linux kernel interface for an HDA desktop codec with a separate amplifier (as in, not handled purely by HDA verbs) is to link them with "hooks" at runtime that handle components attached over i2c, spi like amplifiers and signal them to enter power save, run initialization routines, etc: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/sound/pci/hda/hda_component.c There is also the ASOc architecture which cs35l41 in linux also supports, where the kernel strings together an audio processing chain explicitly without depending on the hda standard or whatnot, but this is not of concern to us There may be a way to link VoodooHDA to the amp by defining some kind of interface between the amp kext and VoodooHDA to enable runtime power management or whatnot, but for basic audio output this is not really necessary and one can do without the hooks. I have AppleHDA through AppleALC working on my laptop and this is sufficient. I did in fact use VoodooI2C successfully since it already was publishing the node for my ACPI path, but it has one limitation: it only publishes one nub per I2cSerialBusv2, even though in my path at SPKR._DSD, there are actually two for the two amps. This is an assumption made in voodooi2c that any acpi device will only have one i2c bus. So I made a small fork of voodooi2c that adds explicit address read/write methods that allow overriding the bus for now, and I do some acpi parsing in my own kext to find the other buses. It's useful to anchor the kext logic in an autodetected I/O Registry path to an i2c bus, and use the default i2c methods on it, but either Voodooi2c has to change their assumption of one bus per acpi device, or the explicit addresss hack to write to an arbitrary address must be accepted as necessary. I need to talk to VoodooI2C to ask about their architecture and whether they'd add a new methods with explicit addresses like my fork hack, or rewrite their probing architecture entirely, which would possibly break a lot of other kexts, and presumably the motivation for VoodooI2C was mainly HID devices, not general purpose ones. In many ways writing a driver for this Smart Amp IC is expanding the usecases of existing hackintosh bus drivers There's one other player--the bringup sequence done by cs35l41 in linux under various configurations requires toggling a GPIO reset pin. VoodooGPIO does exist but as far as I have discovered, its existence was motivated mainly for interrupts for i2c trackpads, and doesn't actually have explicit gpio pin write/read methods. On intel VoodooGPIO has a complex system of matching platforms to various lookup tables and stuff, which is hard to replicate, and I don't have a machine anyway on intel GPIO, but VoodooGPIOAMD basically just uses straight MMIO whose base can be obtained by querying the loaded kext, so I have another hack to toggle the gpio through MMIO directly, but in the future a proper gpio read/write would be upstreamed there My code initially hardcoded a lot of things like various pins and addresses but I have autodiscovery for nearly everything done as well as using the linux structs and getting firmware upload working. By studying the linux code I discovered that there's a no-firmware fallback that uses a low amp gain without firmware for testing or on error, and the initi sequence for this is fairly low complexity so I sent that at first and it did work to enable audio output. with cs35l41 being a smart amp, similar to Apple's MacBooks which also use this design, the idea is that the amp can boost the speakers a lot more than otherwise would be safe in response to dynamic conditions like equalizing the low frequencies to avoid too much current. The nodsp fallback mode is very quiet but was useful as a proof of concept that I hacked together prior to committing to the full kext. It would be ideal if the amp would just automatically initialize in this fallback mode without any user intervention, since the nodsp fallback is necessarily safe, and the amp itself shuts down under various fault conditions by observing over current and other faults, but alas, you're right again that actual i2c operations are required to hear anything. The chip also supports an SPI mode but as far as I'm aware there aren't hackintosh community generic SPI kexts, though I know Apple has long had in-house intel SPI support i.e. Lpss and HSSPI for their own trackpads over SPI. The high level logic is quite similar and shared, operating over abstractions outside of the actual i2c writes/reads methods, so if an adequate spi bus driver existed it would be fairly trivial to expand the current code to use it. I admire your approach of dumping the raw register operations, and doing so is always important in debugging, but examining the linux driver's higher level logic in generating the operations was a much faster, and much easier and preferable way to implement this. Did you take a look at linux cs35l41 src? --In any case, thanks so much for your initial attempt and trying to breach IOKit; driver code and programming is by no means easy even for experienced user-space programmers. I do have some experience in the area which is why I largely knew where to start I'll write back as soon as I can iphone2g&3gfan, aka newhacker1746 Edited May 20 by newhacker1746 Quote Link to comment https://www.insanelymac.com/forum/topic/362242-help-wanted-developing-kext-for-cirrus-logic-cs35l41-amplifier-i2c-on-legion-7/#findComment-2850498 Share on other sites More sharing options...
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.