output_i2s2.cpp oversampling

domingo

Well-known member
Any good souls out there that could help me modding the code in output_i2s2.cpp, to oversample the frequency to 88.2kHz effectively? I'm trying to improve how my NOS DAC analog reconstruction filter works. This is a 4xTDA1387 DAC with a 3rd pole LPF and doubling the frequency would very much help me reduce aliasing.

Simply setting I2S_TCR2_DIV((0)) shows the correct 88.2kHz WS and 5.645MHz BCLK with an oscilloscope. But this is clearly not enough. Do I also need to adjust the fs to 88.2kHz in the PLL section? Doing so in addition to I2S_TCR2_DIV((0)) brings BCLK to over 10MHz. I guess I also need to double each sample size, but this AI ISR code isn't really working and I'm not clear what should I do with block size. It is set to default 128 in AudioStream.h.

Code:
void AudioOutputI2S2::isr(void)
{
    int16_t *dest;
    audio_block_t *blockL, *blockR;
    uint32_t saddr, offsetL, offsetR;

    saddr = (uint32_t)(dma.TCD->SADDR);
    dma.clearInterrupt();
    if (saddr < (uint32_t)i2s2_tx_buffer + sizeof(i2s2_tx_buffer) / 2) {
        // DMA is transmitting the first half of the buffer
        dest = (int16_t *)&i2s2_tx_buffer[AUDIO_BLOCK_SAMPLES / 2];
        if (AudioOutputI2S2::update_responsibility) AudioStream::update_all();
    } else {
        // DMA is transmitting the second half of the buffer
        dest = (int16_t *)i2s2_tx_buffer;
    }

    blockL = AudioOutputI2S2::block_left_1st;
    blockR = AudioOutputI2S2::block_right_1st;
    offsetL = AudioOutputI2S2::block_left_offset;
    offsetR = AudioOutputI2S2::block_right_offset;

    // Manual oversampling: duplicate each sample
    if (blockL && blockR) {
        for (int i = 0; i < AUDIO_BLOCK_SAMPLES / 2; i++) {
            // Left channel oversampling (duplicate each sample)
            dest[2*i] = blockL->data[i + offsetL];     // First sample
            dest[2*i + 1] = blockL->data[i + offsetL]; // Duplicate sample

            // Right channel oversampling (duplicate each sample)
            dest[2*i + AUDIO_BLOCK_SAMPLES] = blockR->data[i + offsetR];  // First sample
            dest[2*i + 1 + AUDIO_BLOCK_SAMPLES] = blockR->data[i + offsetR]; // Duplicate sample
        }
        offsetL += AUDIO_BLOCK_SAMPLES / 2;
        offsetR += AUDIO_BLOCK_SAMPLES / 2;
    } else if (blockL) {
        for (int i = 0; i < AUDIO_BLOCK_SAMPLES / 2; i++) {
            // Left channel oversampling (duplicate each sample)
            dest[2*i] = blockL->data[i + offsetL];     // First sample
            dest[2*i + 1] = blockL->data[i + offsetL]; // Duplicate sample
        }
        offsetL += AUDIO_BLOCK_SAMPLES / 2;
    } else if (blockR) {
        for (int i = 0; i < AUDIO_BLOCK_SAMPLES / 2; i++) {
            // Right channel oversampling (duplicate each sample)
            dest[2*i + AUDIO_BLOCK_SAMPLES] = blockR->data[i + offsetR];  // First sample
            dest[2*i + 1 + AUDIO_BLOCK_SAMPLES] = blockR->data[i + offsetR]; // Duplicate sample
        }
        offsetR += AUDIO_BLOCK_SAMPLES / 2;
    } else {
        memset(dest, 0, AUDIO_BLOCK_SAMPLES * 2);  // Clear the buffer if no data
    }

    #if IMXRT_CACHE_ENABLED >= 2
    arm_dcache_flush_delete(dest, sizeof(i2s2_tx_buffer) / 2);
    #endif   

    // Update offsets and release blocks
    if (offsetL < AUDIO_BLOCK_SAMPLES) {
        AudioOutputI2S2::block_left_offset = offsetL;
    } else {
        AudioOutputI2S2::block_left_offset = 0;
        AudioStream::release(blockL);
        AudioOutputI2S2::block_left_1st = AudioOutputI2S2::block_left_2nd;
        AudioOutputI2S2::block_left_2nd = NULL;
    }
    if (offsetR < AUDIO_BLOCK_SAMPLES) {
        AudioOutputI2S2::block_right_offset = offsetR;
    } else {
        AudioOutputI2S2::block_right_offset = 0;
        AudioStream::release(blockR);
        AudioOutputI2S2::block_right_1st = AudioOutputI2S2::block_right_2nd;
        AudioOutputI2S2::block_right_2nd = NULL;
    }
}

Any guidance is much much appreciated!
Regards,
Domingo
 
The single simplest thing to do would be to change AUDIO_SAMPLE_RATE_EXACT to 88.2kHz in AudioStream.h. The whole audio engine will then run at the higher speed, and so long as the resulting clocks are within the TDA1387's limits most stuff will Just Work.

If you need to keep everything apart from the I²S data stream running at 44.1kHz, then you'll need to modify output_i2s2.cpp to:
  • double the size of the DMA buffer
  • edit config_i2s() to use double the AUDIO_SAMPLE_RATE_EXACT value for the output stream
  • implement whatever oversampling scheme you need to in the ISR to create 256 samples to send to the DAC from 128 in the incoming audio blocks
I found a datasheet for the TDA1387 (though not on an official website); looks like BCLK can go to 18.4MHz, so "over 10MHz" should be no problem.
 
Thank you h4yn0nnym0u5e! I need to keep things separate, or I lose the ability to stream audio via USB at regular 44.1kHz.
DMA buffer: I didn't know this. I tried doubling the size with:
Code:
DMAMEM __attribute__((aligned(32))) static uint32_t i2s2_tx_buffer[AUDIO_BLOCK_SAMPLES * 2];

But the result is plain noise. I also tried directly replacing all calls to AUDIO_BLOCK_SAMPLES for 256, but no luck.
PLL: I wrote fs = 88200 in the PLL section, but the result is lower than expected: 82.76kHz(WS) and 5.292MHz(BCLK). This is with I2S_TCR2_DIV(1). Without division (I2S_TCR2_DIV(0)) the result if further away from 88.2kHz. I don't understand why.
 
I managed to make a working code for 2x oversampling (88.2kHz) via I2S (T4). It just works and might have some weaknesses or mistakes, because it is basically the PT8211 code adapted and shrink for stereo. May this serve anyone in the future and please don't hesitate if any comments or remarks.

The registers for TDA1543 seem to be the same for the TDA1387. Alliasing is fine when combined with a 4rd order LPF filter at the DAC's output.

There might be something wrong with my DAC circuit, but the advantage of 2X OS for me is significant lower noise floor out of the DAC, when I compare it to 4X OS (like 10dB difference).

Code:
#if defined(__IMXRT1062__)
#include <Arduino.h>
#include "output_i2s2.h"
#include "memcpy_audio.h"
#include "utility/imxrt_hw.h"

audio_block_t * AudioOutputI2S2::block_left_1st = NULL;
audio_block_t * AudioOutputI2S2::block_right_1st = NULL;
audio_block_t * AudioOutputI2S2::block_left_2nd = NULL;
audio_block_t * AudioOutputI2S2::block_right_2nd = NULL;
uint16_t  AudioOutputI2S2::block_left_offset = 0;
uint16_t  AudioOutputI2S2::block_right_offset = 0;
bool AudioOutputI2S2::update_responsibility = false;
DMAChannel AudioOutputI2S2::dma(false);
DMAMEM __attribute__((aligned(32))) static uint32_t i2s2_tx_buffer[AUDIO_BLOCK_SAMPLES*2];   // Changed to *2


FLASHMEM
void AudioOutputI2S2::begin(void)
{
    dma.begin(true); // Allocate the DMA channel first

    block_left_1st = NULL;
    block_right_1st = NULL;

    config_i2s();

    // if AudioInputI2S2 set I2S_TCSR_TE (for clock sync), disable it
    I2S2_TCSR = 0;
    while (I2S2_TCSR & I2S_TCSR_TE) ; //wait for transmit disabled

    CORE_PIN2_CONFIG  = 2;  //EMC_04, 2=SAI2_TX_DATA, page 428

    dma.TCD->SADDR = i2s2_tx_buffer;
    dma.TCD->SOFF = 2;
    dma.TCD->ATTR = DMA_TCD_ATTR_SSIZE(1) | DMA_TCD_ATTR_DSIZE(1);
    dma.TCD->NBYTES_MLNO = 2;
    dma.TCD->SLAST = -sizeof(i2s2_tx_buffer);
    dma.TCD->DOFF = 0;
    dma.TCD->CITER_ELINKNO = sizeof(i2s2_tx_buffer) / 2;
    dma.TCD->DLASTSGA = 0;
    dma.TCD->BITER_ELINKNO = sizeof(i2s2_tx_buffer) / 2;
    dma.TCD->CSR = DMA_TCD_CSR_INTHALF | DMA_TCD_CSR_INTMAJOR;
    dma.TCD->DADDR = (void *)((uint32_t)&I2S2_TDR0);
    dma.triggerAtHardwareEvent(DMAMUX_SOURCE_SAI2_TX);
    dma.enable();

    I2S2_TCSR |= I2S_TCSR_TE | I2S_TCSR_BCE | I2S_TCSR_FRDE | I2S_TCSR_FR;

    update_responsibility = update_setup();
    dma.attachInterrupt(isr);
}

void AudioOutputI2S2::isr(void)
{
    int16_t *dest, *dest_copy;
    audio_block_t *blockL, *blockR;
    uint32_t saddr, offsetL, offsetR;

    saddr = (uint32_t)(dma.TCD->SADDR);
    dma.clearInterrupt();
    if (saddr < (uint32_t)i2s2_tx_buffer + sizeof(i2s2_tx_buffer) / 2) {
        // DMA is transmitting the first half of the buffer
        // so we must fill the second half
        dest = (int16_t *)&i2s2_tx_buffer[AUDIO_BLOCK_SAMPLES/2*2];   // OS2x: Changed from to *2
        if (AudioOutputI2S2::update_responsibility) AudioStream::update_all();
    } else {
        // DMA is transmitting the second half of the buffer
        // so we must fill the first half
        dest = (int16_t *)i2s2_tx_buffer;
    }
    dest_copy = dest;

    blockL = AudioOutputI2S2::block_left_1st;
    blockR = AudioOutputI2S2::block_right_1st;
    offsetL = AudioOutputI2S2::block_left_offset;
    offsetR = AudioOutputI2S2::block_right_offset;

    static int32_t oldL = 0;  // OS2x
    static int32_t oldR = 0;  // OS2x
    
    if (blockL && blockR) {
        for (int i=0; i< AUDIO_BLOCK_SAMPLES / 2; i++, offsetL++, offsetR++) {
                    int32_t valL = blockL->data[offsetL];
                    int32_t valR = blockR->data[offsetR];
                    int32_t nL = (oldL+valL) >> 1;
                    int32_t nR = (oldR+valR) >> 1;
                    // OS2x: Changed from 8 samples to 4 samples per iteration
                    *(dest+0) = oldL;
                    *(dest+1) = oldR;
                    *(dest+2) = nL;
                    *(dest+3) = nR;
                    dest+=4;
                    oldL = valL;
                    oldR = valR;
            }
    }
    
    arm_dcache_flush_delete(dest_copy, sizeof(i2s2_tx_buffer) / 2);

    if (offsetL < AUDIO_BLOCK_SAMPLES) {
        AudioOutputI2S2::block_left_offset = offsetL;
    } else {
        AudioOutputI2S2::block_left_offset = 0;
        AudioStream::release(blockL);
        AudioOutputI2S2::block_left_1st = AudioOutputI2S2::block_left_2nd;
        AudioOutputI2S2::block_left_2nd = NULL;
    }
    if (offsetR < AUDIO_BLOCK_SAMPLES) {
        AudioOutputI2S2::block_right_offset = offsetR;
    } else {
        AudioOutputI2S2::block_right_offset = 0;
        AudioStream::release(blockR);
        AudioOutputI2S2::block_right_1st = AudioOutputI2S2::block_right_2nd;
        AudioOutputI2S2::block_right_2nd = NULL;
    }
}

void AudioOutputI2S2::update(void)
{
    // null audio device: discard all incoming data
    //if (!active) return;
    //audio_block_t *block = receiveReadOnly();
    //if (block) release(block);

    audio_block_t *block;
    block = receiveReadOnly(0); // input 0 = left channel
    if (block) {
        __disable_irq();
        if (block_left_1st == NULL) {
            block_left_1st = block;
            block_left_offset = 0;
            __enable_irq();
        } else if (block_left_2nd == NULL) {
            block_left_2nd = block;
            __enable_irq();
        } else {
            audio_block_t *tmp = block_left_1st;
            block_left_1st = block_left_2nd;
            block_left_2nd = block;
            block_left_offset = 0;
            __enable_irq();
            release(tmp);
        }
    }
    block = receiveReadOnly(1); // input 1 = right channel
    if (block) {
        __disable_irq();
        if (block_right_1st == NULL) {
            block_right_1st = block;
            block_right_offset = 0;
            __enable_irq();
        } else if (block_right_2nd == NULL) {
            block_right_2nd = block;
            __enable_irq();
        } else {
            audio_block_t *tmp = block_right_1st;
            block_right_1st = block_right_2nd;
            block_right_2nd = block;
            block_right_offset = 0;
            __enable_irq();
            release(tmp);
        }
    }
}

void AudioOutputI2S2::config_i2s(void)
{

    CCM_CCGR5 |= CCM_CCGR5_SAI2(CCM_CCGR_ON);
//PLL:
    int fs = AUDIO_SAMPLE_RATE_EXACT;
    // PLL between 27*24 = 648MHz und 54*24=1296MHz
    int n1 = 4; //SAI prescaler 4 => (n1*n2) = multiple of 4
    int n2 = 1 + (24000000 * 27) / (fs * 256 * n1);

    double C = ((double)fs * 256 * n1 * n2) / 24000000;
    int c0 = C;
    int c2 = 10000;
    int c1 = C * c2 - (c0 * c2);
    set_audioClock(c0, c1, c2);

    CCM_CSCMR1 = (CCM_CSCMR1 & ~(CCM_CSCMR1_SAI2_CLK_SEL_MASK))
           | CCM_CSCMR1_SAI2_CLK_SEL(2); // &0x03 // (0,1,2): PLL3PFD0, PLL5, PLL4,
    CCM_CS2CDR = (CCM_CS2CDR & ~(CCM_CS2CDR_SAI2_CLK_PRED_MASK | CCM_CS2CDR_SAI2_CLK_PODF_MASK))
           | CCM_CS2CDR_SAI2_CLK_PRED(n1-1)
           | CCM_CS2CDR_SAI2_CLK_PODF(n2-1);
    IOMUXC_GPR_GPR1 = (IOMUXC_GPR_GPR1 & ~(IOMUXC_GPR_GPR1_SAI2_MCLK3_SEL_MASK))
            | (IOMUXC_GPR_GPR1_SAI2_MCLK_DIR | IOMUXC_GPR_GPR1_SAI2_MCLK3_SEL(0));    //Select MCLK

    if (I2S2_TCSR & I2S_TCSR_TE) return;

    //CORE_PIN5_CONFIG  = 2;  //2:MCLK
    CORE_PIN4_CONFIG  = 2;  //2:TX_BCLK
    CORE_PIN3_CONFIG  = 2;  //2:TX_SYNC

    // configure transmitter
    I2S2_TMR = 0;
    I2S2_TCR1 = I2S_TCR1_RFW(0);
    I2S2_TCR2 = I2S_TCR2_SYNC(0) | I2S_TCR2_BCP | I2S_TCR2_MSEL(1) | I2S_TCR2_BCD | I2S_TCR2_DIV(1);
    I2S2_TCR3 = I2S_TCR3_TCE;
    I2S2_TCR4 = I2S_TCR4_FRSZ(1) | I2S_TCR4_SYWD(15) | I2S_TCR4_MF | I2S_TCR4_FSE | I2S_TCR4_FSP | I2S_TCR4_FSD; //TDA1543
    I2S2_TCR5 = I2S_TCR5_WNW(15) | I2S_TCR5_W0W(15) | I2S_TCR5_FBT(15);
    
}

#endif //defined(__IMXRT1062__)
 
Back
Top