I²S Left/Right Clock (or Word Select clock) runs too fast when selecting the Audio Oscillator

Environment info:

Tested on own integrated product, and nRF5340 Audio DK.

Firmware built on Fedora Linux 42,

Using the Rust programming language, using embassy-nrf & cortex-m crates.

Problem summary:

Intention: Master devices I²S, Sending Left channel data to single speaker.

- When using the Audio Oscillator as clock source for I²S peripheral, the Left/Right Clock AKA Word Select runs on 41,67 kHz instead of the expected 16kHz (measured using oscilloscope)

- When rerunning it with the internal clock, using the suggested parameters as mentioned on the I²S page it works with the 0.8% increased speed as advertised. (again, verified using oscilloscope)

Since the embassy-nrf crate does not yet implement an 'ready to use' peripheral, I've used the embassy-nrf::pac (peripheral access crate) to directly set the registers, just as is done in low-level C.

Set up all the registers and executed tests with following values and in written order.

1. Pin Configurations registers set:

PSEL.MCK = Disconnected, 0x10000000
PSEL.LRCK = Pin number & port configured, Connected, thus starting 0x0..... (works, could measure signals)
PSEL.SDIN = Disconnected, 0x10000000
PSEL.SDOUT = Pin number & port configured, Connected, thus starting 0x0..... (works, could measure signals)

2. Configuration registers set:

CONFIG.CLKCONFIG= CLKSRC depends on test case, BYPASS = false, so either 0x00 or 0x01
CONFIG.MODE = Master, 0x00
CONFIG.RXEN = Disabled, 0x00
CONFIG.TXEN = Enabled, 0x01
CONFIG.MCKEN = Enabled. 0x01
CONFIG.MCKFREQ: in ACLK: 175304704 (0xA72F000), in PCLK32M 135274496 (0x8102000) (see Configuration Examples)
CONFIG.RATIO: in ACLK: _32X (0x00) in PCLK32M _64X (0x02)
CONFIG.SWIDTH = 16Bit (0x01)
CONFIG.FORMAT = I2S (0x00)
CONFIG.ALIGN = Left (0x00)
CONFIG.CHANNELS = Left (0x01)
TXD.PTR = raw pointer
RXTXD.MAXCNT = size of buffer in 32-bit word: 1600 byte buffer, so 400 value. (0x190)

3. Bind interrupts
- Bind interrupt to ISR
- During interrupt:
    - TXD.PTR = set to new pointer
    - EVENTS_TXTRUP = NotGenerated (0x00)
    - <Wait with nop until EVENTS_TXRUP is truely 0x00>

4. (Only when testing ACLK)
HFCLKAUDIO.FREQUENY = 39846 (0x9BA6), (see Audio oscillator example table)
HFCLKAUDIOALWAYSRUN = AlwaysRun (0x01)
TASKS_HFCLKAUDIOSTART = Trigger (0x01)
<Wait until HFAUDIOSTAT = state.running (0b00000000000000010000000000010000)>

5. start I²S
EVENTS_TXTRUP = NotGenerated (0x00)
INTENSET = txtrupd, set, (0b00000000000000000000000000100000)
ENABLE = Enabled (0x01)
TASK_START = Trigger (0x01)

6. Measure

Noticed wrong pitch of sine tone, and that interrupts were triggered more than twice fast in case of ACLK use. Therefore, I started debugging and measuring.
Since we were working on own hardware, I switched to nRF5340 Audio DK, running the exact same program. Still same issue.

Therefore I grabbed an oscilloscope, and measured the LRCLK signal since that determines my sample rate, and measured 41,67 kHz instead of the expected clean 16 kHz.
When I set the clock source to PCLK32M and change the CONFIG.MCLKFREQ and CONFIG.RATIO accordingly, it runs nicely on the on the 16,128 kHz as described in the documentation

All measuring has been done on the nRF5340 Audio DK, Did I miss anything switching to using the ACLK?

  • Hi Matthew,

    Can you double-check that clock source is set to ACLK?
    Clock source for master clock generator can be set using CONFIG:CLKCONFIG.

    Matthew512 said:
    This config.write writes it into the registers. (can also share that if you'd like)

    You can share this.

    Best regards,
    Dejan

  • Hi Dejan, 

    Of course I can share it.

    So I made a builder patter, which initializes/constructs once is known if it is master or slave on the bus and if it is a receiving, sending or both sending/receiving device. Although we have only implemented the master & audio sending for now. 

    Builder construction function: 

    impl<SampleDataType> Builder<MasterDevice, SendingDevice, SampleDataType> {
        ///Will consume the builder, and create a correctly configured I²S device, ready for sending.
        pub fn init_i2s_device(self) -> Device<SendingDevice, SampleDataType> {
            self.pin_config.write_pin_config(&self.i2s_peripheral);
    
            let (format, alignment) = self.data_format.to_nrf_enums();
            // When using the external clock, Dont forget to set the A_CLOCK speed outside of this lib!
            let config: Config = Config {
                bus_mode: BusMode::MASTER,
                rx_enabled: false,
                tx_enabled: true,
                clock_source_selection: ClockSource::ACLK,
                master_clock_enabled: true,
                master_frequency: 175304704,
                left_right_clock_ratio: LeftRightClockRatio::_32X,
                sample_size: SampleWidth::_16BIT,
                bus_message_format: format,
                bus_message_alignment: alignment.unwrap_or(Align::LEFT),
                audio_channel: self.audio_channel,
            };
            config.write_config(&self.i2s_peripheral);
            Device {
                _device_type: SendingDevice {},
                sample_data_type: self._sample_type,
            }
        }
    }

    Side note, the comment in my code also mentions setting the ACLK freq and activating it. That is done, see step 4 in my initial post.

    Config struct:

    pub(crate) struct Config {
        pub(crate) bus_mode: BusMode,
        pub(crate) rx_enabled: bool,
        pub(crate) tx_enabled: bool,
        pub(crate) clock_source_selection: ClockSource,
        pub(crate) master_clock_enabled: bool,
        pub(crate) master_frequency: u32,
        pub(crate) left_right_clock_ratio: LeftRightClockRatio,
        pub(crate) sample_size: SampleWidth,
        pub(crate) bus_message_alignment: BusMessageAlignment,
        pub(crate) bus_message_format: BusMessageFormat,
        pub(crate) audio_channel: AudioChannels,
    }
    

    And the actual Config::write_config() function:

    impl Config {
        pub(crate) fn write_config(&self, peripheral_instance: &I2s) {
            peripheral_instance
                .config()
                .mode()
                .write(|config_reg| config_reg.set_mode(self.bus_mode));
            peripheral_instance
                .config()
                .clkconfig()
                .write(|config_reg| config_reg.set_clksrc(self.clock_source_selection));
            peripheral_instance
                .config()
                .clkconfig()
                .write(|config_reg| config_reg.set_bypass(false));
            peripheral_instance
                .config()
                .mckfreq()
                .write(|config_reg| config_reg.set_mckfreq(Mckfreq(self.master_frequency)));
            peripheral_instance
                .config()
                .mcken()
                .write(|config_reg| config_reg.set_mcken(self.master_clock_enabled));
            peripheral_instance
                .config()
                .rxen()
                .write(|config_reg| config_reg.set_rxen(self.rx_enabled));
            peripheral_instance
                .config()
                .txen()
                .write(|config_reg| config_reg.set_txen(self.tx_enabled));
            peripheral_instance
                .config()
                .ratio()
                .write(|config_reg| config_reg.set_ratio(self.left_right_clock_ratio));
            peripheral_instance
                .config()
                .swidth()
                .write(|config_reg| config_reg.set_swidth(self.sample_size));
            peripheral_instance
                .config()
                .align()
                .write(|config_reg| config_reg.set_align(self.bus_message_alignment));
            peripheral_instance
                .config()
                .format()
                .write(|config_reg| config_reg.set_format(self.bus_message_format));
            peripheral_instance
                .config()
                .channels()
                .write(|config_reg| config_reg.set_channels(self.audio_channel));
        }
    }

    So the Builder fills in the configuration, and calls "write". The "Device" is then of a type of "SendingDevice" which only supports sending functionality.

    In the mean time we've tested replaying voice recording samples from the PDM mic to the speaker, which works like this with the PCLK32M clock, just not when we use the ACLK.

    If you have any questions, don't hesitate to reach out!

    Thank you for investigating, and kind regards, 

    -Matthew

  • Hi Matthew,

    It seems that in your case I2S still uses PCLK32M instead of ACLK. I assume this might be related to non-Nordic tools that you use for which we do not have support.

    Best regards,
    Dejan

  • Hi Dejan,

    You are absolutely right!

    The clock source indeed didn't switch, but I never tested what would happen if we didn't set the ACLK, but did set the frequency, so I didn't notice.

    I have verified that I was indeed having the correct pointer by checking the base address & offset, and comparing that to the printed raw address. And that seemed to be fine. Then I checked the raw value of the register after setting it, which also seemed to work. Then I reprinted the raw values at the end of my function, and noticed that the register was reset.

    The "set_bypass(false)" call indeed cleared the whole register, and not only the 8th bit index.

    Thank you for putting the effort in to help me out.

    Kind regards, 

    Matthew

Related