Close

Attempting to write a device tree overlay for the soundcard

A project log for Put a Raspberry Pi CM4 into an original iPad

Open up a 2010 iPad, remove its logic board, and fabricate a new board based around the Raspberry Pi CM4.

EvanEvan 03/22/2021 at 03:540 Comments

Since my last log, I connected the other ends of the magnet wires to female header pins so that I can attach to my RPi 4. I really, really would recommend against trying to do it this way. I'm either gonna spend the board space and use 0.1" headers next time, or go for something easy to solder like an FFC so I can make a matching breakout board.

In the mean time, I've tested the board a bit, and have been working on making it work under Linux.

For basic testing, I used command line tools for I2C (i2cdetect, i2cset, i2cget from the i2c-tools package. From this, I was able to confirm that the codec powers up successfully, I can speak to it over i2c, and that jack detection works. (This codec can tell the difference between a TRS stereo headset and a TRRS stereo/mic headset, based on the resistance between the different pins.)

I still have no idea whether I can actually make sounds with this thing, though. To get sound, I need to:

  1. Toggle GPIO pins to enable the dual LDO and the codec
  2. Configure the codec via i2c so that it knows which of its several output pins to use, and what format we'll be sending on its PCM/I2S pins.
  3. Actually send I2S to it.

I could probably script all that, using python or bash or something, but since in the end I want to make this work as a fully-supported alsa/pulseaudio/whatever output, I started learning how to do this Properly™. The basic approach is to make sure the kernel has a module available for the codec, then write a "device tree overlay" describing the board.

The Linux kernel source already have a driver for the TLV320AIC3104, but they aren't included by default in Raspberry Pi OS. After cloning, configuring to enable the tlv320aic3x driver, and rebuilding the kernel, I have the snd_soc_tlv320aic3x module available.

Next, I need a device tree overlay for my board. This will tell the kernel what hardware I've attached to the computer, and if all goes well, the kernel should load and instantiate the appropriate drivers.

The Raspberry Pi website has a decent page about device trees, overlays, and parameters. The kernel documentation also has some useful pages about it: Linux and the Device Tree. It's also helpful to  I cargo-culted my first version of my overlay, compiled it, put it in /boot/overlays, added it to config.txt, and rebooted, and... how do I know if it worked? After playing around and searching for things, I was able to find a couple useful debugging tips:

  1. Check dmesg for anything obvious. 
  2. On Raspberry Pi devices only, you can get bootloader logs with
    sudo vcdbg log msg
  3. This forum post recommends booting without the overlay enabled, then load the overlay at runtime with the dtoverlay command while running udevadm monitor. I've found it's also helpful to be running `dmesg -w` also.
    # in one terminal:
    dmesg -w
    # in a second terminal:
    sudo udevadm monitor
    # in a third terminal:
    sudo dtoverlay pipad

    You'll find devicetree-related messages in dmesg often have the prefix "OF:", which I think refers to OpenFirmware, where the device tree concept was originally created.

    Unfortunately, this doesn't shorten the iteration cycle -- every time your overlay fails to properly apply, you need to reboot to get back to a clean state. (It looks like you should be able to remove an overlay with dtoverlay -r, but this doesn't work if your overlay doesn't apply cleanly.)

After iterations, I've settled on this .dts file:

/dts-v1/;
/plugin/;

/ {
    compatible = "brcm,bcm2835";
    fragment@0 {
        target = <&gpio>;
        __overlay__ {
            aic3104_reset: aic3104_reset {
                brcm,pins = <17 27>;
                brcm,function = <1 1>;
                brcm,pull = <1 1>;
            };
        };
    };

    fragment@1 {
        target-path = "/";
        __overlay__ {

            vcc30: fixedregulator@1 {
                compatible = "regulator-fixed";
                regulator-name = "fixed-supply";
                regulator-min-microvolt = <3000000>;
                regulator-max-microvolt = <3000000>;
                gpio = <&gpio 22 1>;
                startup-delay-us = <70000>;
                enable-active-high;
                vin-supply = <&vdd_3v3_reg>;
            };
            vcc18: fixedregulator@2 {
                compatible = "regulator-fixed";
                regulator-name = "fixed-supply";
                regulator-min-microvolt = <1800000>;
                regulator-max-microvolt = <1800000>;
                gpio = <&gpio 22 1>;
                startup-delay-us = <70000>;
                enable-active-high;
                vin-supply = <&vdd_3v3_reg>;
            };

            amp: analog-amplifier {
                compatible = "simple-audio-amplifier";
                enable-gpios = <&gpio 17 1>;
                VCC-supply = <&vdd_3v3_reg>;
            };
        };
    };

    fragment@2 {
        target = <&i2c1>;
        __overlay__ {
            tlv320aic3104: tlv320aic3104@18 {
                #sound-dai-cells = <0>;
                compatible = "ti,tlv320aic3104";
                reg = <0x18>;

                reset-gpios = <&gpio 27 0>;

                AVDD-supply = <&vcc30>;
                DRVDD-supply = <&vcc30>;
                DVDD-supply = <&vcc18>;
                IOVDD-supply = <&vdd_3v3_reg>;
            };
        };
    };

    fragment@3 {
        target = <&i2s>;
        __overlay__ {
            status = "okay";
        };
    };

    fragment@4 {
        target = <&sound>;
        __overlay__ {
            compatible = "simple-audio-card";
            i2s_controller = <&i2s>;
            status = "okay";
            simple-audio-card,name = "pipad";
            simple-audio-card,format = "i2s";

            simple-audio-card,bitclock-master = <&dailink0_master>;
            simple-audio-card,frame-master = <&dailink0_master>;

            simple-audio-card,widgets =
                "Microphone", "Microphone Jack",
                "Microphone", "Internal Microphone",
                "Headphone", "Headphone Jack",
                "Line", "Line Out",
                "Speaker", "Internal Speaker";
            simple-audio-card,routing =
                "Line Out", "HPLCOM",
                "Line Out", "HPRCOM",
                "Headphone Jack", "HPLOUT",
                "Headphone Jack", "HPROUT",
                "Microphone Jack", "MIC2L",
                "Internal Microphone", "MIC1L",
                "Internal Speaker", "LLOUT",
                "Internal Speaker", "RLOUT";
            simple-audio-card,aux-devs = <&>;
            dailink0_master: simple-audio-card,cpu {
                sound-dai = <&i2s>;
            };
            simple-audio-card,codec {
                sound-dai = <&tlv320aic3104>;
                system-clock-frequency = <12288000>;
            };
        };
    };
};

This loads cleanly, and causes the snd_soc_tlv320aic3x, snd_soc_simple_card, and snd_soc_bcm2835_i2s modules to be loaded. This looks like it ought to work, but at the moment:

pi@raspberrypi:~ $ aplay -l
aplay: device_list:272: no soundcards found...
pi@raspberrypi:~ $ ls /dev/snd
seq  timer

If things were working correctly, this ought to look something like:

pi@raspberrypi:~ $ aplay -l
**** List of PLAYBACK Hardware Devices ****
card 0: b1 [bcm2835 HDMI 1], device 0: bcm2835 HDMI 1 [bcm2835 HDMI 1]
  Subdevices: 4/4
  Subdevice #0: subdevice #0
  Subdevice #1: subdevice #1
  Subdevice #2: subdevice #2
  Subdevice #3: subdevice #3
card 1: Headphones [bcm2835 Headphones], device 0: bcm2835 Headphones [bcm2835 Headphones]
  Subdevices: 4/4
  Subdevice #0: subdevice #0
  Subdevice #1: subdevice #1
  Subdevice #2: subdevice #2
  Subdevice #3: subdevice #3
pi@raspberrypi:~ $ ls /dev/snd/
by-path  controlC0  controlC1  pcmC0D0p  pcmC1D0p  seq  timer

(This is what you see if you set dtparam=audio=on in /boot/config.txt). I'm stuck until I figure out why controlC0 / pcmC0D0p are missing.

Discussions