I2S output without Codec

Hi all,

I’m trying to get I2S to work on my Verdin i.MX8MP development board without the NAU8822 codec, so essentially just the I2S as a simple output for usage with wav files. I’m on TorizonOS 7.1.0.

With my current device tree overlay however, I’m not seeing any sound card and therefore alsa does not work. I tried to implement it with a dummy codec, but I’m not quite sure if this is the way-to-go.

Does anyone have experience using just the I2S as an output without a codec?
What could be missing here?

My overlay:

/dts-v1/;
/plugin/;

/ {
	compatible = "toradex,verdin-imx8mp-wifi-dev";
};

&{/} {

	sound {
		compatible = "simple-audio-card";
		status = "okay";
		simple-audio-card,name = "simple-audio";
		simple-audio-card,format = "i2s";

		simple-audio-card,cpu {
			sound-dai = <&sai1>;
		};

        simple-audio-card,codec {
            sound-dai = <&dummy_codec>;
        };
	};

    dummy_codec: dummy_codec {
        #sound-dai-cells = <0>;
        compatible = "linux,snd-soc-dummy";
    };
 
};

&sai1 {
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_sai1>;
	status = "okay";
};


&nau8822_1a {
	status = "disabled";
};
torizon@verdin-imx8mp-15337625:~$ cat /proc/asound/cards 
--- no soundcards ---
torizon@verdin-imx8mp-15337625:~$ dmesg | grep -iE "sound|sai|alsa|codec|audio"
[    0.074569] Advanced Linux Sound Architecture Driver Initialized.
[    2.643292] usbcore: registered new interface driver snd-usb-audio
[    2.947683] debugfs: Directory 'audiomix' with parent 'pm_genpd' already present!
[    2.973028] ALSA device list:
[    2.976011]   No soundcards found.
[   33.119387] platform sound: deferred probe pending
[   33.130083] platform sound-hdmi: deferred probe pending


Cheers,
Tim

Hi, I’m the Toradex AI Assistant. The Toradex Support team will follow up with you shortly!

Did you like my answer?

:+1: :-1:


Hello Tim,

Thank you for reaching out with your question about configuring I2S output on your Verdin i.MX8MP board without a codec. Using a dummy codec is indeed a valid approach for enabling the I2S interface without a physical codec.

Here are some steps to help you troubleshoot and refine your setup:

  1. Device Tree Overlay: Your overlay looks mostly correct. Ensure that the simple-audio-card and dummy_codec nodes are properly defined. The sound-dai property should correctly reference the SAI interface you intend to use, such as &sai1.

  2. Kernel Logs: Check the kernel logs using dmesg for any errors or warnings related to the sound subsystem. Look for messages that might indicate why the sound card is not being registered.

  3. ALSA Configuration: Verify that ALSA is configured to recognize the dummy codec. Use aplay -l to list available sound cards and ensure your dummy codec is listed.

  4. Kernel Modules: Ensure all necessary kernel modules for ALSA and I2S are loaded. You might need to manually load modules if they are not automatically loaded.

  5. Testing: Once the dummy codec is recognized, test the I2S output using aplay with a WAV file to confirm the I2S interface is functioning as expected.

  6. Simplify Configuration: If the sound card is still not appearing, consider simplifying your device tree overlay to isolate the issue. Start with a minimal configuration and gradually add components back.

By following these steps, you should be able to configure and test I2S output on your board without a physical codec. If issues persist, further examination of the device tree and kernel configuration may be necessary.

Best regards,
Toradex AI Assistant

Please, also check if these links can help you solve your problem:

NXP i.MX 8M Plus - Verdin System on Module.

Your DT looks good, but I wonder about the linux,snd-soc-dummy compatible string you use. I cannot find any mention of it in the toradex 5.15 kernel (did not check the 6.6 kernel I think you are using with torizoncore 7.1), nor in upstream linux. I did find this patch, but it was never merged it seems.

Anyway, here’s the DT snippet I use with torizoncore 6.7.0 and the Verdin imx8mp that works to drive a fairly dump MAX98357 codec/amplifier (I added my commented configuration for controlling its enable pin, which did not work, but what is shown below works if I manually toggle the amplifier enable pin). Note that I used the linux,spdif-dit compatible string for the code, which is commonly used since it is a codec that does not actually do or need anything else.


////////////////////////////////////////////////////
// Audio
////////////////////////////////////////////////////
/ {
 // Create simple audio card instance that outputs to I2S
 // The name of this node (and its parents if it would have them)
 // defines the name of the resulting pulseaudio sink.
 sound_i2s_dac {
  compatible = "simple-audio-card";
  simple-audio-card,name = "i2s-dac";

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

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

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

 max98357a_codec: max98357a {
  compatible = "linux,spdif-dit";
  #sound-dai-cells = <0>;
  // TODO: Below would tell the kernel about the actual codec used
  // and allow controlling the sdmode pin, but somehow this
  // crashed the kernel when playing the audio with
  // BUG: scheduling while atomic: aplay
  //compatible = "maxim,max98357a";
  //sdmode-delay = <5>; // ms to start bclk before enable sdmode to prevent pop. Driver docs suggest 5ms.
  //sdmode-gpios = <&gpio_expander_20 14 GPIO_ACTIVE_HIGH>;
 };
};

&sai1 {
 status = "okay";
 pinctrl-0 = <&pinctrl_sai1_bclk_lrclk_dout>;
 pinctrl-names = "default";
   
 // sai1_mclk1 is the only one set in imx8mp DT, so we configure
 // that. The driver automatically selects mclk1/2/3 as the source
 // for its bclk divider based on the required samplerate
 //
 // TODO: Below hardcodes the clock rates, meaning we can either play
 // 96/48/8khz or 44.1/22.050/11.025khz files. The sai driver can
 // dynamically switch between AUDIO_PLL1/2 clocks depending on
 // the requested samplerate if configured in the sai with the pll8k
 // and pll11k DT properties, but the simple-audio-card driver does
 // not set up the right clocks with set_sysclk, so this likely needs
 // switching to the imx-audio-card driver (with or without hardware
 // samplerate conversion with the ASR module).
 // See note in obsidian for details.
 assigned-clocks = <&audio_blk_ctrl IMX8MP_CLK_AUDIO_BLK_CTRL_SAI1_MCLK1_SEL>, <&clk IMX8MP_CLK_SAI1>;

 assigned-clock-parents = <&clk IMX8MP_CLK_SAI1_ROOT>, <&clk IMX8MP_AUDIO_PLL1_OUT>;
 assigned-clock-rates = <12288000>, <0>; // 256 * 48k = 12288000
 // Above configures a clock suitable for 48k and similar sample
 // rates. For 44.1k and related rates, use PLL2 and a different
 // mclk as shown below. Note that the exact mclk rate is not
 // important (we have no mclk output), only that it is a multiple of
 // the required sample rate (and it is a divisor of the PLL1/2
 // parent clock).
 //assigned-clock-parents = <&clk IMX8MP_CLK_SAI1_ROOT>, <&clk IMX8MP_AUDIO_PLL2_OUT>;
 //assigned-clock-rates = <11289600>, <0>; // 256 * 44.1k = 11289600
};

&iomuxc {
 pinctrl_sai1_bclk_lrclk_dout: pinctrl_sai1_bclk_lrclk_dout {
  fsl,pins =
   // TODO: Constants, comments
   <MX8MP_IOMUXC_SAI5_MCLK__AUDIOMIX_SAI1_TX_BCLK 0x1d6>, /* SODIMM 30 */
   <MX8MP_IOMUXC_SAI5_RXD1__AUDIOMIX_SAI1_TX_SYNC 0x1d6>, /* SODIMM 32 */
   <MX8MP_IOMUXC_SAI5_RXFS__AUDIOMIX_SAI1_TX_DATA00 0x96>; /* SODIMM 34 */
 };
};

With this DT applied, I can startup and I get a soundcard in /dev/ which is listed by aplay (running in a debian container with the right devices forwarded).

1 Like

Hi @TimSiegrist,

I’ve personally never tried to configure the I2S as a simple output before in the device tree. Though the suggestion from @matthijs seems reasonable. Could you give that a try and let us know how it works out.

I did also find this other thread from someone who did something similar on the Apalis i.MX8 in case it is also helpful here: I2s slave on Apalis iMX8 sai2

Best Regards,
Jeremias

I just realized I missed some relevant parts of our DT (that enable and set up sai1, and set up the pin mux). I’ve edited my previous post to make the DT complete.

Hi @matthijs and @jeremias.tx

Thanks a lot, we were able to make it work with the changes from your DT!

One thing I’m not yet sure about is the clocks. With your setup like this, you hardcode to only play 48khz files. Do you know if this limitation (e.g. the need to have it hardcoded) comes from the simple-audio-card driver itself or somewhere else?
So I guess you just make sure to only use that specific sample rate?
I also noticed that another file we used previously fails to play, even with the 48khz sample rate:

Playing WAVE '../sounds/scewo/Warning_style-A-03.wav' : Signed 24 bit Little Endian in 3bytes, Rate 48000 Hz, Mono
aplay: set_params:1416: Unable to install hw params:
ACCESS:  RW_INTERLEAVED
FORMAT:  S24_3LE
SUBFORMAT:  STD
SAMPLE_BITS: 24
FRAME_BITS: 24
CHANNELS: 1
RATE: 48000
PERIOD_TIME: 125000
PERIOD_SIZE: 6000
PERIOD_BYTES: 18000
PERIODS: 4
BUFFER_TIME: 500000
BUFFER_SIZE: 24000
BUFFER_BYTES: 72000
TICK_TIME: 0

Could this also be related to the simple-audio-card driver?
The following file played successfully:

Playing WAVE '../sounds/scewo/Intern_Calibration_calibration_beep.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Mono```

Best Regards,
Tim

Do you know if this limitation (e.g. the need to have it hardcoded) comes from the simple-audio-card driver itself or somewhere else?

I believe this is mostly from the clock that is configured for the sai1 node. In the device tree snippet provided by @matthijs, they have it setup for a rate of 48khz. Though this clock should also work for related rates like 96/8khz via clock integer dividers.

In theory you could also play audio files that were not sampled at 48khz, but there will probably be some distortion depending on how different the sample rate of the audio file is from 48khz. As a side-note if you were using an audio codec, many codecs also provide internal fractional dividers on the clock that can make the sample rate more flexible, than what is provided by the SoC directly.

I also noticed that another file we used previously fails to play, even with the 48khz sample rate:

I assume you previously played this audio file via the default configuration with the audio codec?

If that is the case, then as I said above the codec may have helped by compensating for the more rigid clock rate provided by the SoC. Now that you have no codec in your configuration, I assume whatever sample rate this audio file was sampled at is not that compatible with the clock rate provided by the SoC directly.

Best Regards,
Jeremias

Maybe some other param of the file is not supported by the soundcard, such as the number of channels (though I think mono worked for me) or 24-bit samplesize? Not entirely sure what determines the supported values, could be that the sai hardware provides some limits.

Yeah, see my comment about that:

 // TODO: Below hardcodes the clock rates, meaning we can either play
 // 96/48/8khz or 44.1/22.050/11.025khz files. The sai driver can
 // dynamically switch between AUDIO_PLL1/2 clocks depending on
 // the requested samplerate if configured in the sai with the pll8k
 // and pll11k DT properties, but the simple-audio-card driver does
 // not set up the right clocks with set_sysclk, so this likely needs
 // switching to the imx-audio-card driver (with or without hardware
 // samplerate conversion with the ASR module).
 // See note in obsidian for details.

The rest of this post below is a copy of my “note in obsidian”, where I noted down some further details. It is not superstructured or complete, but maybe gives a good starting point (if you’re not afraid to dig into kernel sources…). I pointed out some alternatives for simple-audio-card at the end, but have not investigated the further since at the time, I only needed to verify that audio output worked. In our final system, we have complete control over the audio files, so we can just enforce a rate. Still, if you figure out how to do the clock switching, please do report back since I would want to enable that for extra flexibility.

Earlier notes about audio clocks below

&clk DT configures some fixed clock rates:

  • AUDIO_PLL1 is 393216000 (32k * 8000 for e.g. 48k)
  • AUDIO_PLL2 is 361267200 (32k * 11025 for e.g. 44.1k)

The least common multiple of 96k and 44.1k would be 14.112Mhz, so it would make sense to generate that as the base clock, but maybe this cannot be precisely generated from 24Mhz?

SAI1 clock options (table has clock, clock root, clock gate columns)

Some of this is better shown in this diagram:

The SAIx_MCLK options are the clock input on the respective MCLK pins when using an external MCLK (section 14.1.1.1).

The audio_blk_control device is provided by a generic imx blk_ctrl driver, the definition of what it actually controls is imx8mp_audio_blk_ctrl_hws in drivers/clk/imx/clk-imx8mp.c (this defines clock names, offsets in the audio_blk_control clock controller, registers, bitmasks and corresponding parent clock names).

For example, it defines the sai1_mclk1_sel clock, which has offset IMX8MP_CLK_AUDIO_BLK_CTRL_SAI1_MCLK1_SEL (to be used to refer to the clock in DT) and can select between the sai1_root and sai1_mclk parents. It also defines a sai1_mclk1_clk clock (offset IMX8MP_CLK_AUDIO_BLK_CTRL_SAI1_MCLK1), which is clock gate that just passes sai1_mclk1_sel unmodified.

The sai1_mclk clock (corresonding to SAI1_MCLK input clock above) is modeled in DT using a fixed-clock node (I guess since there is nothing to configure on this clock, it just exists).

Summarizing, relevant clocks are:

Linux clock name Datasheet Parent Function DT
saix_mclk SAI1_MCLK / SAI1.MCLK - External clock from MCLK pin <&sai1_mclk>
saix_mclky_sel unnamed MUX Mux for sayx_mclky_clk <&audio_blk_control IMX8MP_CLK_AUDIO_BLK_CTRL_SAIx_MCLKy_SEL>
saix_mclky_clk saix_sai_mclky saix_mclk_y_sel Gate after saix_mclky_sel <&audio_blk_control IMX8MP_CLK_AUDIO_BLK_CTRL_SAIx_MCLKy>
sai_pll_ref_sel ? MUX Mux for sai_pll <&audio_blk_control IMX8MP_CLK_AUDIO_BLK_CTRL_SAI_PLL_REF_SEL>
sai_pll SAI PLL sai_pll_ref_sel PLL with divider and multiplier, fixed list of supported rates in imx_blk_ctrl_sai_pll_tbl <&audio_blk_control IMX8MP_CLK_AUDIO_BLK_CTRL_SAI_PLL>
saix unnamed MUX Mux for saix_root <&clk IMX8MP_CLK_SAIx>
saix_root CCM_SAIx_CLK_ROOT / SAIx_CLK_ROOT saix Gate after saix <&clk IMX8MP_CLK_SAIx_ROOT>
saix_ipg_clk saix_ipg_clk audio_ahb_root Gate <&audio_blk_control IMX8MP_CLK_AUDIO_BLK_CTRL_SAIx_IPG>
audio_pll1_ref_sel unnamed MUX MUX for audio_pllx <&clk IMX8MP_AUDIO_PLLx_REF_SEL>
audio_pllx AUDIO_PLLx_CLK audio_pllx_ref_sel PLL with divider and multiplier, fixed list of supported rates in imx_pll1443x_tbl <&clk IMX8MP_AUDIO_PLLx>
audio_pllx_bypass ? audio_pllx / audio_pllx_ref_sel PLL bypass (MUX) <&clk IMX8MP_AUDIO_PLL1_BYPASS>
audio_pllx_out ? audio_pllx_bypass Gate for audio_pllx <&clk IMX8MP_AUDIO_PLL1_OUT>

Final clock muxing in the SAI module is handled by the driver, based on the named clocks in the clocks/clock-names property of the &saix node. In particular:

  • The bus clock is hardwired to the saix_ipg_clk, the driver is told about this via the “bus” clock name so it can enable this clock and query its rate.
  • The mclk1/2/3 are hardwired to the saix_mclky_clk clocks, the driver is told about this via the “mclky” clock names so it can enable the clock it wants and query their rate.
  • The driver tries deriving the needed bclk from each of the mclk1/2/3 clocks in turn, using the one with least error (allowing up to 1/1000 error). It then configures the selected mux and clock divider in the SAI module directly outside of the linux clock subsystem. This happens in fsl_sai_set_bclk called from fsl_sai_hw_params exported as the hw_params dai op.
  • To support clocks in 8khz and 11.025khz multiples (e.g. 48 and 44.1), the driver supports configuring two root clocks for this via the pll8k and pll11k DT properties. These point to two different clocks and the driver will look up its clock tree for either of these and then, based on the mclk that it needs, change a clock parent to one of the other (this does require that either of these clocks are already set up as a parent in the clock tree).
    This happens in fsl_sai_set_dai_sysclk called from fsl_sai_set_dai_sysclk exported as the set_sysclk dai op. This is only done for output direction and mclky clocks (not the bus clock).
  • It is unclear from where the hw_params op is called exactly and how it orders compared to the set_sysclk op.
  • The set_sysclk dai op seems to be called by simple-card-utils.c, but that seems to only set up clock 0, which is the bus clock (configuring it with the mclk value needed, but only if mclk-fs is configured in DT).
  • Conclusion: The automatic 8k/11k switch (and automatic mclk rate setup at all) cannot happen with the fsl-sai driver combined with simple-card (but there seems to be fsl-specific cards that can do this, see below).

ASRC / fsl-asoc-card

The imx8mp has an Audio Sample Rate Converter block, which allows hardware resampling. To support using this, apparently the regular simple-card is too simple, so there exists a fsl-asoc-card driver. This driver supports configuring mclk-id which is then used to call set_sysclk on the dai (which seems a leaky abstraction at first, but maybe I’m misunderstanding how set_sysclk is designed). However, it does seem to support only a fixed list of audio codecs, and has specific code for these codecs (output format options, supported samplerates, more hardcoded stuff). More leaky abstractions…

imx-audio-card

There is also an imx-audio-card driver which seems to be an alternative to the simple-card driver for the IMX platform. This also has a list of supported codecs for which it hardcodes settings, but also has a dummy codec that might be useful.

Hi @matthijs

Thanks a lot for the extensive information!
I’ve managed to single out my problems, with using the imx-audio-card. If I specify no codec, the imx-card driver will use a dummy codec. Compared to the simple-audio-card driver, this one seems to support different sample sizes.
As for different sampling rates, I’ve ended up using ALSAs resampling, which worked for me. In our application we use snd_pcm_set_params(). I’m not quite sure why we use PCM (i think alsa just converts the PCM to I2S output in the end?), since I’m just porting the application. But it works quite well actually. I can play all the files we’ve had previously (different sample sizes and both 48 and 44.1khz).

This is my device tree now:

/dts-v1/;
/plugin/;

#include <imx8mp-pinfunc.h>

/ {
	compatible = "toradex,verdin-imx8mp";
};

&{/} {

/* Defines custom sound card with I2S, using the imx-audio-card driver (imx-card.c).
 * If we do not specify any codec, the driver uses a dummy codec. This driver support different sample sizes, 
 * but only a single sampling rate (48kHz)
 */ 
    sound_i2s_dac {
        compatible = "fsl,imx-audio-card";
        model = "imx-audio-card";
        pri-dai-link {
            link-name = "sai1";
            format = "i2s";
            cpu {
                sound-dai = <&sai1>;
            };
        };
    };
};

&sai1 {
    status = "okay";
    pinctrl-0 = <&pinctrl_sai1_bclk_lrclk_dout>;
    pinctrl-names = "default";
};

&iomuxc {
    pinctrl_sai1_bclk_lrclk_dout: pinctrl_sai1_bclk_lrclk_dout {
        fsl,pins =
        <MX8MP_IOMUXC_SAI5_MCLK__AUDIOMIX_SAI1_TX_BCLK 0x1d6>, /* SODIMM 30 */
        <MX8MP_IOMUXC_SAI5_RXD1__AUDIOMIX_SAI1_TX_SYNC 0x1d6>, /* SODIMM 32 */
        <MX8MP_IOMUXC_SAI5_RXFS__AUDIOMIX_SAI1_TX_DATA00 0x96>; /* SODIMM 34 */
    };
};

&nau8822_1a {
	status = "disabled";
};

All the best,
Tim

Cool, thanks for the followup. Nice to see that you got the imx-audio-card driver working. AFAIU that could be also made to work with different samplerates, but just resampling at the ALSA level as you do now is also fine (and probably unlikely to be audible unless your analog setup is really high-quality).

Maybe I’ll experiment a bit more to get both classes of samplerates working natively at some point (using your DT as a basis), but since that is not a priority for us now, probably not anytime soon :slight_smile:

I think “PCM” in this sense just means “raw sample-based audio” (to distinguish the PCM interface from other interfaces like control, mixer and MIDI).

@TimSiegrist Glad to see you got something working for your use-case. Do you have any issues/questions on this topic, or is it considered resolved?

Best Regards,
Jeremias

@jeremias.tx
I marked the solution since this works for us now. The only thing I am still wondering is where the limitation for the different sample rates comes from. So if anything more comes up related to this topic, this would surely be nice to know.

Alright then, good to hear. Glad to see the solution here works for your use-case.

I have the same use case as you, and will be following this topic. Thank you for providing the DT, I will go ahead and try your solution - I also tried the dummy codec as well but… of course it didn’t work. Thanks.

Hello @TimSiegrist

I’ve been able to follow your device tree and was able to see the sound card. However, I ran into the same issue as you had earlier:

aplay: set_params:1416: Unable to install hw params:

and I do not know how you were able to solve the issue, if by using snd_pcm_set_params() or something else.

Do you mind sharing how you solved this?

Hi @srzm

So you are using the imx-audio-card now right? With that card I was able to play different samplesizes (I tested 16 bit and 24 bit), but was still not able to play different sample rates (only 48kHz).

I’m guessing now that the sample frequency is still not correct, so thats where I ended up using the mentioned ALSA resampling. We have a cpp application, so we use the PCM interface from the ALSA lib (the snd_pcm_set_params()).
But since this is set in the application, if I want to manually play sounds (via aplay /path/to/my/sound.wav), I end up with the same error message as you.

To be able to do that in a same way as in our application, I would need to add/edit the alsa configuration file (.asoundrc). In our docker container, I create this file under /root/ anyway, since I also needed to add software volume control there.
So if I would want to also play sound manually, I would use this as my alsa configuration file, although only the last entry might be relevant for you:

/root/.asoundrc:

pcm.!default {
    type plug
    slave.pcm "softvol"
}

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

pcm.rate_convert {
    type rate
    slave {
        pcm "hw:0,0"
        rate 48000
    }
}

I was then able to also play different sample rates like this:

aplay -D rate_convert /path/to/my/sound.wav

Hope that helps!