nRF54LM20B I2S DAC (MAX98357A) support – feasibility, sample application, and overlay file

Hello Nordic Team,

We are trying to test an I2S DAC (MAX98357A) with the nRF54LM20B EVK. Could you confirm if this is supported and which sample application is recommended for a simple I2S audio test?

Also, we tried the audio_codec sample (NCS v3.3.0), but it does not include a board overlay for nRF54LM20B—could you please share a reference overlay or confirm support?

Regards,

Subhiksha

Parents
  • Hi ,

    I2S is supported on the nRF54LM20 as part of the TDM peripheral. We have I2S driver test samples in Zephyr which support nRF54LM20B in nRF Connect SDK v3.3.0. You can use the nrf54lm20dk_nrf54lm20_common.dtsi file from either of them as a basis for how your overlay can look like. If you need to change the pin configuration, please refer to the pin assignments for CSP98 to ensure that SCK is configured to use a dedicated clock pin and that you are using GPIO port P1 or P3. 

    Best regards,

    Maria

  • Hello Maria,

    Thank you for your response.

    We tried building the I2S sample by referring to the nrf54lm20dk_nrf54lm20_common.dtsi as suggested and created an overlay accordingly. However, we are encountering a build error during compilation.

    Error observed:

    We have attached a screenshot of the complete build error for your reference. Could you please help us understand how to resolve this issue or if any specific configuration changes are required for nRF54LM20B?

    If any additional details (overlay file, prj.conf, logs, etc.) are needed, please let me know — I will share them.

    Regards,

    Subhiksha

  • Hi Subhiksha, 

    Could you try to share the error again? I don't see anything in the box under "Error observed:" 

    Ususally, Insert->Code works for sharing code and logs. 

    Note that a carbon copy of the .dtsi from the test samples is not sufficient for the i2s_codec sample. The sample tries to fetch the i2s_codec_tx alias, which is defined in the overlay files for the supported boards for the sample. Did you define this in your .overlay for the nRF54LM20B? 

    Best regards,

    Maria

  • Hi Maria,

    Actually, i'm not able to share complete logs as you mentioned. Please refer the image attached in this for "error observed". 

    Did you define this in your .overlay for the nRF54LM20B?  - Sorry i didn't get this what do you mean it? can you please come again?

Reply Children
  • Hi Subhiksha,

    Subhiksha said:
    Did you define this in your .overlay for the nRF54LM20B?  - Sorry i didn't get this what do you mean it? can you please come again?

    I meant to ask if you have included something like this in your .overlay: 

    / {
    	aliases {
    		i2s-codec-tx = &tdm;
    	};
    };

    Another note about the i2s_codec sample. It expects a control line to the codec, which the MAX98357A does not have. So the sample would need to be modified to be compliant with MAX98357A. I have not tested this with the hardware, since I don't have a MAX98357A available, but I was able to successfully build the sample for nRF54LM20B with this .overlay, main.c, Kconfig, and .conf fragment: 

    nrf54lm20dk_nrf54lm20b_cpuapp_ns.overlay

    /*
     * Copyright 2024-2025 NXP
     *
     * SPDX-License-Identifier: Apache-2.0
     */
    
    #include <zephyr/kernel.h>
    #include <zephyr/sys/printk.h>
    #include <zephyr/audio/dmic.h>
    #include <zephyr/drivers/i2s.h>
    #if CONFIG_USE_AUDIO_CODEC
    #include <zephyr/audio/codec.h>
    #endif
    #include <zephyr/toolchain.h>
    #include <string.h>
    
    #ifndef CONFIG_USE_DMIC
    #include "sine.h"
    #endif
    
    #define I2S_CODEC_TX DT_ALIAS(i2s_codec_tx)
    
    #define SAMPLE_FREQUENCY CONFIG_SAMPLE_FREQ
    #define SAMPLE_BIT_WIDTH CONFIG_SAMPLE_WIDTH
    #define BYTES_PER_SAMPLE CONFIG_BYTES_PER_SAMPLE
    #if CONFIG_USE_DMIC
    #define NUMBER_OF_CHANNELS CONFIG_DMIC_CHANNELS
    #else
    #define NUMBER_OF_CHANNELS (2U)
    #endif
    /* Such block length provides an echo with the delay of 100 ms. */
    #define SAMPLES_PER_BLOCK ((SAMPLE_FREQUENCY / 10) * NUMBER_OF_CHANNELS)
    #define INITIAL_BLOCKS    CONFIG_I2S_INIT_BUFFERS
    #define TIMEOUT           (2000U)
    
    #define BLOCK_SIZE  (BYTES_PER_SAMPLE * SAMPLES_PER_BLOCK)
    #define BLOCK_COUNT (INITIAL_BLOCKS + CONFIG_EXTRA_BLOCKS)
    
    K_MEM_SLAB_DEFINE_IN_SECT_STATIC(mem_slab, __nocache, BLOCK_SIZE, BLOCK_COUNT, 4);
    
    static bool configure_tx_streams(const struct device *i2s_dev, struct i2s_config *config)
    {
    	int ret;
    
    	ret = i2s_configure(i2s_dev, I2S_DIR_TX, config);
    	if (ret < 0) {
    		printk("Failed to configure codec stream: %d\n", ret);
    		return false;
    	}
    
    	return true;
    }
    
    static bool trigger_command(const struct device *i2s_dev_codec, enum i2s_trigger_cmd cmd)
    {
    	int ret;
    
    	ret = i2s_trigger(i2s_dev_codec, I2S_DIR_TX, cmd);
    	if (ret < 0) {
    		printk("Failed to trigger command %d on TX: %d\n", cmd, ret);
    		return false;
    	}
    
    	return true;
    }
    
    int main(void)
    {
    	const struct device *const i2s_dev_codec = DEVICE_DT_GET(I2S_CODEC_TX);
    #if CONFIG_USE_DMIC
    	const struct device *const dmic_dev = DEVICE_DT_GET(DT_NODELABEL(dmic_dev));
    #endif
    #if CONFIG_USE_AUDIO_CODEC
    	const struct device *const codec_dev = DEVICE_DT_GET(DT_NODELABEL(audio_codec));
    	struct audio_codec_cfg audio_cfg;
    #endif
    	struct i2s_config config;
    	int ret = 0;
    
    #if CONFIG_USE_DMIC
    	struct pcm_stream_cfg stream = {
    		.pcm_width = SAMPLE_BIT_WIDTH,
    		.mem_slab = &mem_slab,
    	};
    	struct dmic_cfg cfg = {
    		.io = {
    			/* These fields can be used to limit the PDM clock
    			 * configurations that the driver is allowed to use
    			 * to those supported by the microphone.
    			 */
    			.min_pdm_clk_freq = 1000000,
    			.max_pdm_clk_freq = 3500000,
    			.min_pdm_clk_dc   = 40,
    			.max_pdm_clk_dc   = 60,
    		},
    		.streams = &stream,
    		.channel = {
    			.req_num_streams = 1,
    		},
    	};
    #endif
    	printk("codec sample\n");
    
    #if CONFIG_USE_DMIC
    	if (!device_is_ready(dmic_dev)) {
    		printk("%s is not ready", dmic_dev->name);
    		return 0;
    	}
    #endif
    
    	if (!device_is_ready(i2s_dev_codec)) {
    		printk("%s is not ready\n", i2s_dev_codec->name);
    		return 0;
    	}
    
    
    #if CONFIG_USE_AUDIO_CODEC
    	if (!device_is_ready(codec_dev)) {
    		printk("%s is not ready", codec_dev->name);
    		return 0;
    	}
    	audio_cfg.dai_route = AUDIO_ROUTE_PLAYBACK;
    	audio_cfg.dai_type = AUDIO_DAI_TYPE_I2S;
    	audio_cfg.dai_cfg.i2s.word_size = SAMPLE_BIT_WIDTH;
    	audio_cfg.dai_cfg.i2s.channels = 2;
    	audio_cfg.dai_cfg.i2s.format = I2S_FMT_DATA_FORMAT_I2S;
    #ifdef CONFIG_USE_CODEC_CLOCK
    	audio_cfg.dai_cfg.i2s.options = I2S_OPT_FRAME_CLK_MASTER | I2S_OPT_BIT_CLK_MASTER;
    #else
    	audio_cfg.dai_cfg.i2s.options = I2S_OPT_FRAME_CLK_SLAVE | I2S_OPT_BIT_CLK_SLAVE;
    #endif
    	audio_cfg.dai_cfg.i2s.frame_clk_freq = SAMPLE_FREQUENCY;
    	audio_cfg.dai_cfg.i2s.mem_slab = &mem_slab;
    	audio_cfg.dai_cfg.i2s.block_size = BLOCK_SIZE;
    	audio_codec_configure(codec_dev, &audio_cfg);
    	k_msleep(1000);
    #endif
    
    #if CONFIG_USE_DMIC
    	cfg.channel.req_num_chan = 2;
    	cfg.channel.req_chan_map_lo = dmic_build_channel_map(0, 0, PDM_CHAN_LEFT) |
    				      dmic_build_channel_map(1, 0, PDM_CHAN_RIGHT);
    	cfg.streams[0].pcm_rate = SAMPLE_FREQUENCY;
    	cfg.streams[0].block_size = BLOCK_SIZE;
    
    	printk("PCM output rate: %u, channels: %u\n", cfg.streams[0].pcm_rate,
    	       cfg.channel.req_num_chan);
    
    	ret = dmic_configure(dmic_dev, &cfg);
    	if (ret < 0) {
    		printk("Failed to configure the driver: %d", ret);
    		return ret;
    	}
    #endif
    
    	config.word_size = SAMPLE_BIT_WIDTH;
    	config.channels = NUMBER_OF_CHANNELS;
    	config.format = I2S_FMT_DATA_FORMAT_I2S;
    #ifdef CONFIG_USE_CODEC_CLOCK
    	config.options = I2S_OPT_BIT_CLK_SLAVE | I2S_OPT_FRAME_CLK_SLAVE;
    #else
    	config.options = I2S_OPT_BIT_CLK_MASTER | I2S_OPT_FRAME_CLK_MASTER;
    #endif
    	config.frame_clk_freq = SAMPLE_FREQUENCY;
    	config.mem_slab = &mem_slab;
    	config.block_size = BLOCK_SIZE;
    	config.timeout = TIMEOUT;
    	if (!configure_tx_streams(i2s_dev_codec, &config)) {
    		printk("failure to config streams\n");
    		return 0;
    	}
    
    	printk("start streams\n");
    	for (;;) {
    		bool started = false;
    #if CONFIG_USE_DMIC
    		ret = dmic_trigger(dmic_dev, DMIC_TRIGGER_START);
    		if (ret < 0) {
    			printk("START trigger failed: %d", ret);
    			return ret;
    		}
    #endif
    		while (1) {
    			void *mem_block;
    			uint32_t block_size = BLOCK_SIZE;
    			int i;
    
    			for (i = 0; i < CONFIG_I2S_INIT_BUFFERS; i++) {
    #if CONFIG_USE_DMIC
    				/* If using DMIC, use a buffer (memory slab) from dmic_read */
    				ret = dmic_read(dmic_dev, 0, &mem_block, &block_size, TIMEOUT);
    				if (ret < 0) {
    					printk("read failed: %d", ret);
    					break;
    				}
    
    				ret = i2s_write(i2s_dev_codec, mem_block, block_size);
    #else
    				/* If not using DMIC, play a sine wave 440Hz */
    
    				BUILD_ASSERT(
    					BLOCK_SIZE <= __16kHz16bit_stereo_sine_pcm_len,
    					"BLOCK_SIZE is bigger than test sine wave buffer size."
    				);
    				mem_block = (void *)&__16kHz16bit_stereo_sine_pcm;
    
    				ret = i2s_buf_write(i2s_dev_codec, mem_block, block_size);
    #endif
    				if (ret < 0) {
    					printk("Failed to write data: %d\n", ret);
    					break;
    				}
    			}
    			if (ret < 0) {
    				printk("error %d\n", ret);
    				break;
    			}
    			if (!started) {
    				i2s_trigger(i2s_dev_codec, I2S_DIR_TX, I2S_TRIGGER_START);
    				started = true;
    			}
    		}
    		if (!trigger_command(i2s_dev_codec, I2S_TRIGGER_DROP)) {
    			printk("Send I2S trigger DRAIN failed: %d", ret);
    			return 0;
    		}
    #if CONFIG_USE_DMIC
    		ret = dmic_trigger(dmic_dev, DMIC_TRIGGER_STOP);
    		if (ret < 0) {
    			printk("STOP trigger failed: %d", ret);
    			return 0;
    		}
    #endif
    		printk("Streams stopped\n");
    		return 0;
    	}
    }
    

    Kconfig (only my addition): 

    config USE_AUDIO_CODEC
    	bool "Configure external codec control device"
    	default y
    	help
    	  Enable this if the audio output device has a Zephyr audio codec control
    	  interface (for example over I2C). Disable this for I2S-only DACs.

    nrf54lm20dk_nrf54lm20b_cpuapp_ns.overlay

    In short I added the CONFIG_USE_AUDIO_CODEC Kconfig symbol and wrapped audio_codec related code within #if #endif CONFIG_USE_AUDIO_CODEC. I got some help from Copilot to ensure that all the code snippets related to a codec with control line was covered. 

    Best regards,

    Maria

  • Hi Maria,

    Thank you for your response.

    May I know which sample application you have taken and built it?

    Here I'm attaching the overlay file which i have been using.

    / {
    	aliases {
    		i2s-node0 = &tdm;
    	};
    };
    
    &pinctrl {
    	tdm_default_alt: tdm_default_alt {
    		group1 {
    			psels = <NRF_PSEL(TDM_SCK_M, 1,10)>,
    				    <NRF_PSEL(TDM_FSYNC_M, 0, 7)>,
    				    <NRF_PSEL(TDM_SDOUT, 1, 12)>;  /* TDM_SDOUT shorted to TDM_SDIN */
    		};
    	};
    };
    
    &tdm {
    	status = "okay";
    	pinctrl-0 = <&tdm_default_alt>;
    	pinctrl-names = "default";
    };

    Regards,

    Subhiksha

  • Hi Subhiksha,

    I used zephyr/samples/drivers/i2s/i2s_codec. I can see now that you wrote audio_codec, which I assume is the zephyr/samples/drivers/audio/codec sample. My apologies for the confusion. 

    I am not able to reproduce the errors you showed in your screenshot with the audio/codec sample. Are you building another sample here? Or are you building your own application? 

    Also, I note that your build target is the nrf54lm20dk/nrf54lm20/cpuapp/ns. Make sure that you have an .overlay for the build target you are using. 

    Just to let you know, I will be away until Tuesday, but I'll follow up when I get back. 

    Best regards,

    Maria

Related