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 again,

    Thank you for your patience with this.

    Kampino said:
    sorry I meant I2S (not I2C as you mentioned - I have fixed it). Please take a look at the overview drawing.

    Thank you for clarifying this with the drawing - this does indeed make the overview a lot more clear! :) 

    Kampino said:
    The "Microphone" is replaced by my squarewave generator, but the idea is that the nRF52 will sample the analog input signal with a SAADC and transmit the sampled signal over I2S to my master.

    Sure, this is a good way to simulate it for testing.
    Could you perhaps set the generator to a sine-wave, and share the SAADC's raw measurements of this with me? This should help us pinpoint the issue.

    Kampino said:
    So I think that the conversion with the ADC and the transmission over I2S runs into some sort of error (i. e. overwriting data while they got transmitted, etc.), but I can not figure out the problem.

    Yes, from your testing it sounds like it might be an issue with the SAADC usage. However, I do not immediately see what that might be what might be causing this. Is the code you shared earlier the complete code that is running, or only a section of a larger program?

    Best regards,
    Karl

  • Hi Karl,

    no this is the complete program and the program shouldn´t do more tasks. I only want to use the nRF52 to test the I2S receiver for my FPGA.

  • Kampino said:
    no this is the complete program and the program shouldn´t do more tasks. I only want to use the nRF52 to test the I2S receiver for my FPGA.

    Thank you for clarifying - I assumed so from your original post, but I thought it best to be completely sure.

    Could you also share with me your sdk_config.h file?

    Best regards,
    Karl

  • Hi Karl,

    please take a look at the attached file.

    60011.sdk_config.h

  • 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

Reply
  • 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

Children
  • 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

  • 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

Related