Problems configuring I2S to receive from mono source.

Hi all. 

I've attached a single channel microphone  onto my nRF9151 and I'd like to capture blocks of audio for processing. I've configured the device tree like this:

i2s: &i2s0 {
  compatible = "nordic,nrf-i2s";
  pinctrl-0 = <&i2s0_default>;
  pinctrl-names = "default"; // , "sleep";
  clock-source = "PCLK32M_HFXO";
  status = "okay";
};

&pinctrl {
  i2s0_default: i2s0_default {
  group1 {
    psels = <NRF_PSEL(I2S_SCK_M, 0, 25)>,
                <NRF_PSEL(I2S_LRCK_M, 0, 23)>,
                <NRF_PSEL(I2S_SDIN, 0, 24)>;
    };
  };
}

Apologies for formatting - it's corrupting the symbols somehow. Then in a simple main.c, I put this:

	#define I2S_DEV 	DT_NODELABEL(i2s)
	static const struct device *i2s = DEVICE_DT_GET(I2S_DEV);
    
    ...

        #define SAMPLE_FREQUENCY    44100
		#define SAMPLE_BIT_WIDTH    24
		#define BYTES_PER_SAMPLE    sizeof(int32_t)
		#define NUMBER_OF_CHANNELS  2
		#define SAMPLES_PER_BLOCK   ((SAMPLE_FREQUENCY / 1000) * NUMBER_OF_CHANNELS)
		#define INITIAL_BLOCKS      2
		#define TIMEOUT             1000

		#define BLOCK_SIZE  (BYTES_PER_SAMPLE * SAMPLES_PER_BLOCK)
		#define BLOCK_COUNT (INITIAL_BLOCKS + 4)
		K_MEM_SLAB_DEFINE_STATIC(mem_slab, BLOCK_SIZE, BLOCK_COUNT, 4);


		if (!device_is_ready(i2s)) {
			printk("I2S device %s is not ready!\n\r", i2s->name);
			// return -1;
		}


        // Configure I2S
		struct i2s_config config = {
			.word_size = SAMPLE_BIT_WIDTH,											// 24 bit as ICS43434 Datasheet.
			.channels = NUMBER_OF_CHANNELS,											// 1 ch - single ICS43434 (left first! WS = 0).
			.format = I2S_FMT_DATA_FORMAT_I2S, 						                // Data on Left Ch - While WS is LOW
			.options = I2S_OPT_BIT_CLK_MASTER | I2S_OPT_FRAME_CLK_MASTER,			// nRF drives bit clk (SCK) & frame clk (WS).
			.frame_clk_freq = SAMPLE_FREQUENCY,										// 44kHz as usual.
			.mem_slab = &mem_slab,													// 
			.block_size = BLOCK_SIZE,												// 
			.timeout = TIMEOUT														// 
		};

		ret = i2s_configure(i2s, I2S_DIR_RX, &config);
		if (ret < 0) {
			printk("Failed to configure I2S RX stream: %d\n", ret);
			return false;
		}
		

		// Trigger ONCE - outside the loop
		ret = i2s_trigger(i2s, I2S_DIR_RX, I2S_TRIGGER_START);
		if (ret < 0) {
			printk("Failed to trigger START: %d\n", ret);
			return false;
		}

		// Read... 
		while(1) {
			void *mem_block;
			uint32_t block_size;

			// read
			ret = i2s_read(i2s, &mem_block, &block_size);
			if (ret < 0) {
				printk("Failed to read data: %d\n", ret);
				break;
			}

			// Debug: Print block info and sample data
			int32_t *samples = (int32_t *)mem_block;
			char buf[100];
			sprintf(buf, "BUF=%d %d %d %d %d %d %d %d\n", 
				samples[0], samples[1], samples[2], samples[3],
				samples[4], samples[5], samples[6], samples[7]);
			printk(buf);
		}

On the oscilloscope, everything looks great. WS, SCLK & SD are perfect. If I make a bit of noise, I can see the 24-bit data flipping higher bits etc. However, I can't get anything in the code. All zeros!? Any idea what's going on here? I'm sure it's something very basic I'm missing. 
NB - I set it up originally as num channels = 1 - same result. And I opted for 4 bytes per sample to align for simpler memory manipulation later. 
  • Yeah, the delay is huge - remember this is jut a sand pit to feel out how the peripheral behaves, before writing proper code for the application. But all the more reason why it's odd that the I2S only runs for 4 blocks. Even if I set block_count to 10, the debug variables tell me that only 4 are in use (6 are free) after the START trigger. Then as I read, it decreases one at a time (as expected). 

    Then it requires a PREPARE trigger for a further START to have any effect. Which suggests that an exception has been handled somewhere resulting in a state change from running -> error. 

    Update: I've gone rooting around inside i2snrfx.c just out of curiosity, and noticed this config `CONFIG_I2S_NRFX_RX_BLOCK_COUNT ` being used. Seems important?! So, I added an assignment to it in my main.c and ctrl clicked to see where it went:

    #define CONFIG_I2C_NRFX 1
    #define CONFIG_I2C_NRFX_TWIM 1
    #define CONFIG_I2C_NRFX_TRANSFER_TIMEOUT 500
    #define CONFIG_I2C_INIT_PRIORITY 50
    #define CONFIG_I2S 1
    #define CONFIG_I2S_INIT_PRIORITY 50
    #define CONFIG_I2S_NRFX 1
    #define CONFIG_I2S_NRFX_RX_BLOCK_COUNT 4
    #define CONFIG_I2S_NRFX_TX_BLOCK_COUNT 4
    The above stuff is inside my build folder!? In autoconf.h. It seems a bit rum that this value of 4 is in there. In my prj.conf I only set `CONFIG_I2S=y`: none of that other stuff... It must get pulled in through a Kconfig that I wasn't aware of being applied.
     
    Update:
    This stuff comes from kconfig.nrfx inside of zephyr/drivers/i2s. This confuses me.
    a) I thought I wasn't using the nrf i2s code - but rather the OS agnostic Zephyr library.
    b) The smoking gun is that this hardware config limitation is preventing the Zephyr I2S code from working - but it doesn't set off any alarm bells during build. 
    The fix is something like:
    #define BLOCK_COUNT  MIN(CONFIG_I2S_NRFX_RX_BLOCK_COUNT, 8)
    and maybe add something like this:

    _Static_assert(CONFIG_I2S_NRFX_RX_BLOCK_COUNT >= 4, "Need at least 4 I2S RX blocks");
  • Unless your code is too slow you should never need more than 4 blocks in those config variables.

    These determine the message queue length from the I²S interrupt handler to the application. Pending buffers are removed from this queue in the i2s_read call already. Thus I don't see how a properly designed application would ever have more than 2 of them pending in the queue - you want your audio code to run as fast and as high priority as possible.

    And yes, you want to stop and properly restart audio streaming when the hardware runs out of buffer space. 

    In case your app needs to support unusually high latency you can just add I2S_NRFX_RX_BLOCK_COUNT and I2S_NRFX_TX_BLOCK_COUNT to your prj.conf file. 

    But I would rather think very hard what your basic audio buffer size should be.You can also just simply define big audio buffers that are a second long - should just fit inside the MCU RAM IMHO.

Related