This discussion has been locked.
You can no longer post new replies to this discussion. If you have a question you can start a new discussion

Streaming audio data from ADC with I2S

Hello,

I´m working on my audio project and I try to stream some audio data (currently a 1 kHz square wave signal with 3.3 V peak) over I2S. The audio signal is sampled by an ADC in the free-running mode.

#include "app_error.h"
#include "nrf_drv_ppi.h"
#include "nrf_drv_i2s.h"
#include "nrf_drv_saadc.h"
#include "nrf_drv_timer.h"
#include "nrf_gpio.h"

#include "nrf_log.h"
#include "nrf_log_ctrl.h"
#include "nrf_log_default_backends.h"

#define LED_0			17
#define BUTTON_0		13
#define BUTTON_1		14
#define TRIGGER			31
#define SAMPLES			128

static bool			IsStarted		= false;
static volatile uint32_t*	I2S_BlockToFill		= NULL;
static volatile uint32_t*	ADC_BlockToFill		= NULL;

static uint32_t			I2S_Buffer[2][SAMPLES];
static nrf_saadc_value_t	ADC_Buffer[2][SAMPLES];
static nrf_ppi_channel_t	PPI_Channel;

static const nrf_drv_timer_t	Timer = NRF_DRV_TIMER_INSTANCE(0);

void on_Timer_Handler(nrf_timer_event_t EventType, void* p_Context)
{
}

void on_SAADC_Handler(nrf_drv_saadc_evt_t const* p_Event)
{
    if(p_Event->type == NRF_DRV_SAADC_EVT_DONE)
    {
        APP_ERROR_CHECK(nrf_drv_saadc_buffer_convert(p_Event->data.done.p_buffer, SAMPLES));

	ADC_BlockToFill = (uint32_t*)p_Event->data.done.p_buffer;
    }
}

static void DataHandler(nrf_drv_i2s_buffers_t const* p_Released, uint32_t Status)
{
    // 'nrf_drv_i2s_next_buffers_set' is called directly from the handler
    // each time next buffers are requested, so data corruption is not
    // expected.
    ASSERT(p_Released);

    // When the handler is called after the transfer has been stopped
    // (no next buffers are needed, only the used buffers are to be
    // released), there is nothing to do.
    if(!(Status & NRFX_I2S_STATUS_NEXT_BUFFERS_NEEDED))
    {
        return;
    }

    // First call of this handler occurs right after the transfer is started.
    // No data has been transferred yet at this point, so there is nothing to
    // check. Only the buffers for the next part of the transfer should be
    // provided.
    if(!p_Released->p_tx_buffer)
    {
        nrf_drv_i2s_buffers_t const NextBuffer = {
            .p_tx_buffer = I2S_Buffer[1],
	    .p_rx_buffer = NULL,
        };

        APP_ERROR_CHECK(nrf_drv_i2s_next_buffers_set(&NextBuffer));

	nrf_gpio_pin_clear(TRIGGER);

        I2S_BlockToFill = I2S_Buffer[1];
    }
    else
    {
        // The driver has just finished accessing the buffers pointed by
        // 'p_Released'. They can be used for the next part of the transfer
        // that will be scheduled now.
        APP_ERROR_CHECK(nrf_drv_i2s_next_buffers_set(p_Released));

        // The pointer needs to be typecasted here, so that it is possible to
        // modify the content it is pointing to (it is marked in the structure
        // as pointing to constant data because the driver is not supposed to
        // modify the provided data).
        I2S_BlockToFill = (uint32_t*)p_Released->p_tx_buffer;
    }
}

void SAADC_Init(void)
{
    APP_ERROR_CHECK(nrf_drv_saadc_init(NULL, on_SAADC_Handler));
    APP_ERROR_CHECK(nrf_drv_ppi_init());

    nrf_drv_timer_config_t timer_cfg = NRF_DRV_TIMER_DEFAULT_CONFIG;
    timer_cfg.bit_width = NRF_TIMER_BIT_WIDTH_32;

    APP_ERROR_CHECK(nrf_drv_timer_init(&Timer, &timer_cfg, on_Timer_Handler));
    nrf_drv_timer_extended_compare(&Timer, NRF_TIMER_CC_CHANNEL0, nrf_drv_timer_us_to_ticks(&Timer, 23), NRF_TIMER_SHORT_COMPARE0_CLEAR_MASK, false);
    nrf_drv_timer_enable(&Timer);

    APP_ERROR_CHECK(nrf_drv_ppi_channel_alloc(&PPI_Channel));
    APP_ERROR_CHECK(nrf_drv_ppi_channel_assign(PPI_Channel, nrf_drv_timer_compare_event_address_get(&Timer, NRF_TIMER_CC_CHANNEL0), nrf_drv_saadc_sample_task_get()));

    nrf_saadc_channel_config_t Channel0 = NRF_DRV_SAADC_DEFAULT_CHANNEL_CONFIG_SE(NRF_SAADC_INPUT_AIN1);
    Channel0.acq_time = NRF_SAADC_ACQTIME_10US;

    APP_ERROR_CHECK(nrf_drv_saadc_channel_init(0, &Channel0));
    APP_ERROR_CHECK(nrf_drv_saadc_buffer_convert(ADC_Buffer[0], SAMPLES));
    APP_ERROR_CHECK(nrf_drv_saadc_buffer_convert(ADC_Buffer[1], SAMPLES));
    APP_ERROR_CHECK(nrf_drv_ppi_channel_enable(PPI_Channel));
}

int main(void)
{
    APP_ERROR_CHECK(NRF_LOG_INIT(NULL));
    NRF_LOG_DEFAULT_BACKENDS_INIT();

    SAADC_Init();

    nrf_gpio_cfg_input(BUTTON_0, NRF_GPIO_PIN_PULLUP);
    nrf_gpio_cfg_input(BUTTON_1, NRF_GPIO_PIN_PULLUP);

    nrf_gpio_cfg_output(TRIGGER);
    nrf_gpio_cfg_output(LED_0);
    nrf_gpio_pin_clear(TRIGGER);
    nrf_gpio_pin_set(LED_0);

    nrf_drv_i2s_config_t I2S_Config = NRF_DRV_I2S_DEFAULT_CONFIG;
    I2S_Config.sdin_pin	    = NRFX_I2S_PIN_NOT_USED;
    I2S_Config.sdout_pin    = 27;
    I2S_Config.lrck_pin	    = 26;
    I2S_Config.sck_pin	    = 25;
    I2S_Config.mck_pin	    = 2;
    I2S_Config.mck_setup    = NRF_I2S_MCK_32MDIV8;
    I2S_Config.ratio	    = NRF_I2S_RATIO_96X;
    I2S_Config.channels	    = NRF_I2S_CHANNELS_STEREO;
    APP_ERROR_CHECK(nrf_drv_i2s_init(&I2S_Config, DataHandler));

    nrf_drv_i2s_buffers_t const InitialBuffer = {
	.p_tx_buffer = I2S_Buffer[0],
        .p_rx_buffer = NULL,
    };

    while(1)
    {
	if(nrf_gpio_pin_read(BUTTON_0) == false)
	{
	    if(IsStarted == false)
	    {
		APP_ERROR_CHECK(nrf_drv_i2s_start(&InitialBuffer, SAMPLES, 0));
		NRF_LOG_INFO("I2S sender started...");
		IsStarted = true;
		nrf_gpio_pin_set(TRIGGER);
		nrf_gpio_pin_clear(LED_0);
	    }
	}
	else if(nrf_gpio_pin_read(BUTTON_1) == false)
	{
	    if(IsStarted)
	    {
		nrf_drv_i2s_stop();
		NRF_LOG_INFO("I2S sender stopped...");
		IsStarted = false;
		nrf_gpio_pin_clear(TRIGGER);
		nrf_gpio_pin_set(LED_0);
	    }
	}

	if(I2S_BlockToFill && ADC_BlockToFill)
        {
	    for(uint32_t i = 0x00; i < SAMPLES; i++)
	    {
		//int16_t Sample = (int16_t)(32768 * sin(2.0 * 3.14 * (double)i / ((double)SAMPLES)));
		I2S_BlockToFill[i] = (uint32_t)(0x0000 | (ADC_BlockToFill[i] & 0xFFFF));
	    }

            I2S_BlockToFill = NULL;
	    ADC_BlockToFill = NULL;
        }

	if(NRF_LOG_PROCESS())
	{
	    NRF_LOG_FLUSH();
	}
    }
}

The code from above will give a pretty poor signal when I use the ADC.

I have tried to figure out the reason for this and replaced the line

I2S_BlockToFill[i] = (uint32_t)(0x0000 | (ADC_BlockToFill[i] & 0xFFFF));

with

I2S_BlockToFill[i] = (uint32_t)(0x0000 | (i & 0xFFFF));

to produce data from a ramp signal and the signal looks much better.

So it looks like the problem is based on the ADC. I have tried to use two buffers to use the same swapping buffer system as in the I2S example.

How can I improve this result?

Parents
  • Hello,

    I try to stream some audio data (currently a 1 kHz square wave signal with 3.3 V peak) over I2S. The audio signal is sampled by an ADC in the free-running mode.
    So it looks like the problem is based on the ADC. I have tried to use two buffers to use the same swapping buffer system as in the I2S example.

    How can I improve this result?

    Is there a particular reason why you are not using the nRF52832's I2S peripheral directly for this?
    If there is, please let me know about your application requirements and constraints, so I may advice you on how to increase the performance according to this.
    If there is an option to instead use the I2S peripheral directly then this is what I would advice, as this should give you a great performance increase compared to what you are seeing now, without involving the SAADC peripheral.

    If you are not sure how to use the I2S peripheral it may be helpful to take a look at the I2S loopback example from the nRF5 SDK which demonstrates both output and input using the peripheral.

    Best regards,
    Karl

  • Hi Karl,

    Is there a particular reason why you are not using the nRF52832's I2S peripheral directly for this?

    what do you mean by this question? I already use the I2S peripheral to stream out the data from the nRF52832, but the data is generated by an ADC that samples some sort of device to get the audio data. In my setup, the "device" is a 3.3 V square wave generator, but it should get replaced with an analog microphone. I only use the generator to get a known signal.

    Feel free to ask if you have additional questions :)

  • Hello,

    Thank you for your patience.

    Could you set your SAADC to be in non-low power mode? This is configured by the NRFX_SAADC_CONFIG_LP_MODE. Low power mode has a slightly higher latency on buffer shifts, which may degrade performance in your case.

    I also notice in the sdk_config that you have the legacy TIMER_ENABLED defined, while also having the NRFX_TIMER_ENABLED defined. This will cause the apply_old_config file to overwrite your NRFX_TIMER configurations. It is recommended that you remove all the legacy define (remove completely, do not leave it defined to 0) when using the NRFX drivers. I notice that this is also the case for your I2S configuration.
    Please change this as well, so that only the NRFX_ configurations remains.

    Let me know if you see any difference when you are no longer using the SAADC in low-power mode.

    Best regards,
    Karl

  • Hi Karl,

    thank you for your reply. I will check it as soon as I have time for it. Slight smile (hopefully today or tomorrow)

  • Great - I look forward to hearing your results! :) 
    In the case that you do not see a noticeable difference, could you have the SAADC sample a sine wave instead, and share its raw digital output with me, along with the sine's frequency and amplitude, so I may take a closer look at what might be going wrong.

    Best regards,
    Karl

  • Hi 

    I have visualized the transmission between the nRF52 and the FPGA over I2S with my ILA. The gap and the transmissions have the same length.


    So there is a transmission gap for sure. 

    Please also note that I don´t use any input signal in this case. I just sample the noise of the ADC.

  • Hello Kampino,

    Kampino said:
    I have visualized the transmission between the nRF52 and the FPGA over I2S with my ILA. The gap and the transmissions have the same length.
    Kampino said:
    So there is a transmission gap for sure. 

    Thank you for this update - this is very helpful.
    How long are each of these gaps?
    In your other ticket you mentioned seeing a performance increase when changing the SDK configuration as suggested in my previous comment - could you detail this performance increase?
    Could you also possibly provide a similar plot for the case in which you were not using the SAADC to gather the I2S output?

    Kampino said:
    Please also note that I don´t use any input signal in this case. I just sample the noise of the ADC.

    Thank you for clarifying - This should not matter for the frequency of the sampling / outputted samples, but it is good to know nevertheless.

    Best regards,
    Karl

Reply
  • Hello Kampino,

    Kampino said:
    I have visualized the transmission between the nRF52 and the FPGA over I2S with my ILA. The gap and the transmissions have the same length.
    Kampino said:
    So there is a transmission gap for sure. 

    Thank you for this update - this is very helpful.
    How long are each of these gaps?
    In your other ticket you mentioned seeing a performance increase when changing the SDK configuration as suggested in my previous comment - could you detail this performance increase?
    Could you also possibly provide a similar plot for the case in which you were not using the SAADC to gather the I2S output?

    Kampino said:
    Please also note that I don´t use any input signal in this case. I just sample the noise of the ADC.

    Thank you for clarifying - This should not matter for the frequency of the sampling / outputted samples, but it is good to know nevertheless.

    Best regards,
    Karl

Children
  • Hi Karl,

    How long are each of these gaps?

    The gaps and the transmissions are around 1.5 ms long (~41.74 kHz LRCLK).

    In your other ticket you mentioned seeing a performance increase when changing the SDK configuration as suggested in my previous comment - could you detail this performance increase?

    Sorry, I forgot the screenshot. I think the signal quality has increased because the signal looks better (always the same height) and is less noisy (you can see it in the Fourier series).

    Could you also possibly provide a similar plot for the case in which you were not using the SAADC to gather the I2S output?

    Sure. Please take a look at the following plots.

    Change the code from

    I2S_BlockToFill[i] = (uint32_t)(0x0000 | (ADC_BlockToFill[i] & 0xFFFF));

    to

    I2S_BlockToFill[i] = (uint32_t)(0x0000 | (i & 0xFFFF));

    Transmission captured with ILA:

    I also attach my sdk_config (just to be sure Slight smile)


    8360.sdk_config.h

  • Hi Karl,

    How long are each of these gaps?

    The gaps and the transmissions are around 1.5 ms long (~41.74 kHz LRCLK).

    In your other ticket you mentioned seeing a performance increase when changing the SDK configuration as suggested in my previous comment - could you detail this performance increase?

    Sorry, I forgot the screenshot. I think the signal quality has increased because the signal looks better (always the same height) and is less noisy (you can see it in the Fourier series).

    Could you also possibly provide a similar plot for the case in which you were not using the SAADC to gather the I2S output?

    Sure. Please take a look at the following plots.

    Change the code from

    I2S_BlockToFill[i] = (uint32_t)(0x0000 | (ADC_BlockToFill[i] & 0xFFFF));

    to

    I2S_BlockToFill[i] = (uint32_t)(0x0000 | (i & 0xFFFF));

    Transmission captured with ILA:

    I also attach my sdk_config (just to be sure Slight smile)


    8360.sdk_config.h

    Edit:

    I have decreased the sampling rate from us to ms by changing

    nrf_drv_timer_extended_compare(&Timer, NRF_TIMER_CC_CHANNEL0, nrf_drv_timer_us_to_ticks(&Timer, 23), NRF_TIMER_SHORT_COMPARE0_CLEAR_MASK, false);

    to

    nrf_drv_timer_extended_compare(&Timer, NRF_TIMER_CC_CHANNEL0, nrf_drv_timer_ms_to_ticks(&Timer, 23), NRF_TIMER_SHORT_COMPARE0_CLEAR_MASK, false);

    Edit 2:

    I have figured out that the I2S buffer isn´t filled correctly. Only the first half is filled (I suggest because of the int16 and uint32 data types):

Related