Linux: Generic I2S audio output driver

Created on 8 May 2018  路  23Comments  路  Source: raspberrypi/linux

Hi,

I want to connect a DSP to the raspberry via I2S audio output. I don't need any configuration via SPI or I2C. I was wondering if there is any generic and simple i2s audio driver yet ?

On my research I found the simple audio card for device tree overlays. Indeed I'm not very familiar with coding overlays for linux platforms.
Has anyone already done something like this?

The only parameters which should be adjustable (as parameters for the device tree overlay would be perfect) would be, if the bitclock and the frame-select-clock (LRCLK) are generated from the raspberry or if they are provided externally (raspberry as master or slave on i2s bus). And maybe the maximum bit depth and max sample rate would be useful.

Best regards,
Markus

All 23 comments

There is a small but growing number of users of the simple-audio-card driver - look at the adau7002-simple, applepi-dac, mbed-dac, pibell and superaudioboard overlays, the source for which can all be found in https://github.com/raspberrypi/linux/blob/rpi-4.14.y/arch/arm/boot/dts/overlays/. In addition, the allo-katana-dac overlay uses audio-graph-card, a very similar generic driver but with a different way of identifying the components.

You can use the generic spdif transmitter codec (see https://github.com/raspberrypi/linux/blob/rpi-4.14.y/Documentation/devicetree/bindings/sound/spdif-transmitter.txt) in combination with the simple card or audio graph card. This will give you a sound device capable of 16 and 24 bit audio output at 8-192kHz. Search the Interfacing and Devicetree sections of the RPi forum, I posted several examples there.

Setting a range of or maximum bit depth / sample rate is not possible, but you can partially work around this via an .asoundrc file to configure a fixed bit depth and/or samplerate (use the format and rate properties of the plug plugin).

Configuration of bit/frame clock master is possible via the bitclock-master and frame-master DT properties. But keep in mind that using a codec without configuriation possibilities (like the generic spdif codec) as a master will have odd side effects:

You need to make sure you only play audio files at the rate and bit depth the codec is configured to. If you eg play a 24-bit 96kHz file the kernel assumes that the codec has been setup accordingly. But if your codec drives the clock lines for a 44.1kHz 16-bit setup you'll just get garbage output (or output at the wrong pitch). If the codec doesn't drive the clock lines at all you'll get a hang and a bit later some error.

Thanks for your comments. That was exactly what I was looking for.
I edited the overlay of the PiBell.
I need the Raspberry to be a clock-slave. I'm not sure who to define that in the overlay, because the bitclock-slave/frame-slave property just contains the handle to the codec. How do I have to setup the codec to act as clock-slave?
That's what I have so far:

/dts-v1/;
/plugin/;

/* Device Tree Overlay for generic I2S audio output on Raspberry Pi
*/

/ {
    compatible = "brcm,bcm2708";

    fragment@0 {
        target = <&sound>;
        __overlay__ {

            compatible = "simple-audio-card";
            simple-audio-card,name = "GenericSoundcard";

            status="okay";

            playback_link: simple-audio-card,dai-link@1 {
                format = "i2s";

                bitclock-master = <&codec_out>;
                frame-master = <&codec_out>;

                p_cpu_dai: cpu {
                    sound-dai = <&i2s>;

                    /* TDM slot configuration - BCLK ratio: 64 x Fs (2 x 32 bit) */
                    dai-tdm-slot-num = <2>;
                    dai-tdm-slot-width = <32>;

                };

                p_codec_dai: codec {
                    sound-dai = <&codec_out>;
                };
            };
        };
    };

    fragment@1 {
        target-path = "/";
        __overlay__ {
            codec_out: spdif-transmitter {
                #address-cells = <0>;
                #size-cells = <0>;
                #sound-dai-cells = <0>;
                compatible = "linux,spdif-dit";
                status = "okay";
            };
        };
    };

    fragment@2 {
        target = <&i2s>;
        __overlay__ {
            #sound-dai-cells = <0>;
            status = "okay";
        };
    };
};

But keep in mind that using a codec without configuriation possibilities (like the generic spdif codec) as a master will have odd side effect

Yes I'm aware of that problem. I will ensure the input will be fixed to 24 bit/48 kHz.

I need the Raspberry to be a clock-slave. I'm not sure who to define that in the overlay, because the bitclock-slave/frame-slave property just contains the handle to the codec.

ALSA is smart enough to know that if the codec is the master for the bit and frame clocks then the SoC must be the slave.

ALSA is smart enough to know that if the codec is the master for the bit and frame clocks

Thank's for the advice.

I compiled the code I posted before with:
dtc -@ -H epapr -O dtb -o genericsoundcard.dtbo -W no-unit_address_vs_reg genericsoundcard.dts
The compiling seemed to be ok, because no error was displayed.
I copied the overlay to /boot/overlays and added it in /boot/config.txt as dtoverlay=genericsoundcard.

After a reboot the modules seem to be loaded correctly:

pi@raspberrypi:~ $ lsmod
Module                  Size  Used by
bnep                   12051  2
bluetooth             365511  5 bnep
cfg80211              543027  0
rfkill                 20851  3 bluetooth,cfg80211
evdev                  12423  2
snd_soc_simple_card     6297  0
snd_soc_bcm2835_i2s     6546  0
snd_soc_simple_card_utils     5196  1 snd_soc_simple_card
snd_soc_core          179915  3 snd_soc_simple_card_utils,snd_soc_bcm2835_i2s,snd_soc_simple_card
snd_compress           10384  1 snd_soc_core
snd_pcm_dmaengine       5894  1 snd_soc_core
snd_pcm                98501  2 snd_pcm_dmaengine,snd_soc_core
snd_timer              23968  1 snd_pcm
snd                    70032  4 snd_compress,snd_timer,snd_soc_core,snd_pcm
bcm2835_gpiomem         3940  0
uio_pdrv_genirq         3923  0
uio                    10204  1 uio_pdrv_genirq
fixed                   3285  0
i2c_dev                 6913  0
fuse                   99603  3
ipv6                  408971  50

But I can't see any playback device in alsa (aplay -l). Do I have to configure anything to use the overlay as an audio output device ?
(I'm working on Raspbian Jessie Lite)

I would have expected ALSA to see the stream named "Playback" supported by the codec and make that appear as a playback device. However, looking at the list of modules I don't see the codec driver - snd_soc_spdif_tx. Can you modprobe it? What does modinfo snd_soc_spdif_tx show?

modinfo snd_soc_spdif_tx

Says modinfo: ERROR: Module snd_soc_spdif_tx not found.
To modprobe the snd_soc_spdif_tx module is not possible. The same error as of modinfo occurs.

My kernel version is:
Linux raspberrypi 4.9.35-v7+ #1014 SMP Fri Jun 30 14:47:43 BST 2017 armv7l GNU/Linux
Is it possible that the snd_soc_spdif_tx is missing in Raspbian Jessie ?

Ooh - close, but no cigar. The commit adding CONFIG_SND_SOC_SPDIF=m to the Pi defconfigs went in on 10 Jul 2017 less than a fortnight after the Jessie release kernel.

You can install the oldest firmware release including the module using:

$ sudo rpi-update 20ba6ed

5c80565 gets you the last 4.9 mainstream release, and omit the hash altogether to get the latest 4.14 kernel.

I installed the last 4.9 version and now the module is loaded and alsa recognises the driver as soundoutput.

pi@raspberrypi:~ $ aplay -l
**** List of PLAYBACK Hardware Devices ****
card 0: GenericSoundcar [GenericSoundcard], device 0: bcm2835-i2s-dit-hifi dit-hifi-0 []
  Subdevices: 1/1
  Subdevice #0: subdevice #0

How do I have to setup alsa to be able to choose the generic Soundcard as an output in Raspbian Jessie on the taskbar ?

There are some rules that the volume plugin uses to determine if an ALSA device is a sensible sound output, which amount to:

  1. there must be a simple control, and
  2. it must have a mixer element called one of:

    • Master*

    • Front*

    • PCM*

    • LineOut*

    • Digital*

    • Headphone*

    • Speaker*

    • *a2dp

      (* is a wildcard, and the matching is case-insensitive)

I added this to /etc/asound.conf:

pi@raspberrypi:~ $ cat /etc/asound.conf
pcm.!default {
    type             plug
    slave.pcm       "GenericSoundcard"
}

pcm.GenericSoundcard {
    type softvol
    slave.pcm "plughw:0,0"
    control.name "Master"
    control.card 0
}

Is that correct ?
Still no device selectable in Raspbian.
Do I also have to configure the ~/.asoundrc file ?

Ping @spl237.

You can't add simple controls in asound.conf; they need to be in the driver software for the device itself. If "amixer scontrols" does not return at least one control with a name from the list above, then the driver needs to be modified to add a simple control. There is no simple workaround to this from changing config files of which I am aware.

amixer scontrols doesn't show any controls.
Do I really need to modify the driver if I just want a simple software volume control ?
I thought this "scaling" of the pcm data would be done in the alsa subsystem and the scaled data is passed to the codec afterwards.

You need to distinguish between what ALSA (via alsamixer, etc.) is capable of and what the lxpanel plugin understands (which is a subset). What does "amixer controls" return?

The plugin accesses simple controls - that's the way it works. I am not aware of any alternative way of doing this; unless a driver offers a simple control, it is not accessible via the ALSA mixer.

What does "amixer controls" return?

Nothing.

The software volume would be a nice feature (just accessible through alsamixer, system will run headless on Jessie Lite, so the controls of the lxpanel plugin won't be present either).

The main thing is the i2s audio output. I can also regulate the volume on the dsp. I will test if the audio out is working.

If you have neither controls, nor simple controls, no ALSA mixer software will be able to communicate with your device, so forget about controlling anything on it. Drivers really need to offer controls!

I got a strange issue while testing. The driver (the code I posted above) seems to work as it should.
I configured the BCLK to 64 x Fs (2 channels, 32 bit slot width).
That's exactly what my dsp needs/outputs (bclk = 64x Fs = 3.072 MHz, mclk of the dsp is 256x Fs = 12.288 MHz @ 48 kHz/24 bit).
I measured the clock outputs from the dsp, which feed the raspberry, and those are perfectly as expected.

For testing I use audacity on raspbian to generate sine waves, because I can control the samplerate and bitdepth easily.

When testing, I hear the 440 Hz sine wave but it's badly distorted (playing @ 48 kHz/24bit). I checked all connections twice. Then I removed the clock wires from the raspberry and measured at the bitclock pin. There is a bitclock of ~3.071 MHz measurable. That astonishes me, because I configured the raspberry to be the clock slave.
Is there any basic fault I did?

Sorry - mis-click due to browser stall.

That does sound odd - I would not expect the I2S interface to be generating a clock in slave mode. The error, if there is one, isn't obvious.

Ah, the bindings document for simple-card says:

- frame-master                          : Indicates dai-link frame master.
                                          phandle to a cpu or codec subnode.
- bitclock-master                       : Indicates dai-link bit clock master.
                                          phandle to a cpu or codec subnode.

In your clock master declarations you have used a reference to the node that actually declares the device that performs the role (which isn't a subnode of the simple-card node), rather than the node that declares the role (and links to the actual device), which is a subnode. Perhaps this error is causing it to use default roles of CPU=master, codec=slave. Yes, this the relevant code fragment:

                if (codec == bitclkmaster)
                        daifmt |= (codec == framemaster) ?
                                SND_SOC_DAIFMT_CBM_CFM : SND_SOC_DAIFMT_CBM_CFS;
                else
                        daifmt |= (codec == framemaster) ?
                                SND_SOC_DAIFMT_CBS_CFM : SND_SOC_DAIFMT_CBS_CFS;

codec here is the subnode called "codec", and with your overlay those comparisons will all fail, resulting in the default of SND_SOC_DAIFMT_CBS_CFS (the codec is the slave for both clocks) being used.

Try replacing:

                bitclock-master = <&codec_out>;
                frame-master = <&codec_out>;

with

                bitclock-master = <&p_codec_dai>;
                frame-master = <&p_codec_dai>;

Yes, wrong node is the culprit - posted that a couple of hours ago on the forum and just noticed the forum poster and the reporter here are probably the same person :)
https://www.raspberrypi.org/forums/viewtopic.php?p=1317745#p1317677

The duplicated effort is a shame, but I'm happy we reached the same conclusion.

Was this page helpful?
0 / 5 - 0 ratings