Overview#

Introduction#

The XMOS I2S library provides software defined, industry-standard, I2S (Integrated Interchip Sound) components that allows you to stream audio between devices using xCORE GPIO ports.

I2S is a specific type of PCM digital audio communication using a serial clock (sometimes refered as bit clock) line, word clock line and at least one multiplexed data line.

The library includes features such as I2S master (newly termed controller), I2S slave (newly termed target), and TDM master components. This application note uses the library to create an I2S master digital loopback.

Block diagram#

../../_images/block_diagram.drawio.png

Fig. 1 Application block diagram#

The main application fits within one thread with a remote I2C task to configure the audio hardware remotely from the other tile. The lib_board_support library, which includes I2C, takes care of the audio hardware setup.

The I2S task calls back to the i2s_loopback task, and the processing in the i2s_loopback task occurs in-between the I/O operations of I2S.

I2S loopback demo#

The CMakeLists.txt file#

XMOS applications use the xcommon-cmake build and dependency management system. This is bundled with the XMOS XTC tools.

To start using the I2S, include lib_i2s as a dependent module in the application’s CMakeLists.txt file:

set(APP_DEPENDENT_MODULES   "lib_i2s")

This demo also uses the I2C library (lib_i2c) which lib_board_support includes as a dependent module. The application uses I2C to configure the audio CODECs. Consequently, the application’s CMakeLists.txt includes both lib_i2s and lib_board_support as dependent modules.

set(APP_DEPENDENT_MODULES  "lib_board_support(1.1.1)"
                           "lib_i2s(6.0.1)")

Includes#

All xC files which declare the application main() function need to include platform.h. XMOS xcore specific defines for declaring and initialising hardware appear in xs1.h.

#include <platform.h>
#include <xs1.h>

The i2s.h file defines the I2S library functions. This header must be included to use the library.

#include "i2s.h"
#include "xk_audio_316_mc_ab/board.h"

The other include gives access to the the board setup code.

Allocating hardware resources#

An I2S interface requires both clock and data pins in order to communicate with the audio CODEC device. On an xcore the pins are controlled by ports.

The ports used by the I2S library are declared on the tile on which they reside. Their declaration includes each port’s direction and buffered nature. This loopback application uses four 1-bit ports for input and four more for output.

on tile[1]: in port p_mclk =                                PORT_MCLK_IN;
on tile[1]: buffered out port:32 p_lrclk =                  PORT_I2S_LRCLK;
on tile[1]: out port p_bclk =                               PORT_I2S_BCLK;
on tile[1]: buffered out port:32 p_dac[NUM_I2S_LINES] =     {PORT_I2S_DAC0, PORT_I2S_DAC1, PORT_I2S_DAC2, PORT_I2S_DAC3};
on tile[1]: buffered in port:32 p_adc[NUM_I2S_LINES] =      {PORT_I2S_ADC0 ,PORT_I2S_ADC1, PORT_I2S_ADC2, PORT_I2S_ADC3};

The xcore also provides clock block hardware to efficiently generate a clock signal that can either be driven out of a port or used to control a port. This application uses one clock block.

on tile[1]: clock bclk =                                    XS1_CLKBLK_1;

The application main() function#

The main() function in the program sets up the tasks in the application.

Firstly, it declares interfaces. An xC interface provides a means for concurrent tasks to communicate with each other. This application includes an interface for the I2S master and an interface for the I2C master.

            interface i2s_frame_callback_if i_i2s;
    interface i2c_master_if i_i2c[1]; // Cross tile interface

The rest of the main() function starts all the tasks in parallel using the xC par construct:

    par {
        on tile[0]: {
            xk_audio_316_mc_ab_board_setup(hw_config); // Setup must be done on tile[0]
            xk_audio_316_mc_ab_i2c_master(i_i2c);      // Run I2C master server task to allow control from tile[1]
        }

        on tile[1]: {
            interface i2s_frame_callback_if i_i2s;

            par {
                // The application - loopback the I2S samples - note callbacks are inlined so does not take a thread
                [[distribute]] i2s_loopback(i_i2s, i_i2c[0]);
                i2s_frame_master(i_i2s, p_dac, NUM_I2S_LINES, p_adc, NUM_I2S_LINES, DATA_BITS, p_bclk, p_lrclk, p_mclk, bclk);
            }
        }
    }

This code starts the I2S master, the I2C master, the board setup logic, and the loopback application.

The call to the i2s_loopback task in the par is marked with the [[distribute]] attribute, and the corresponding i2s_loopback() function is marked with the [[distributable]] attribute. These attributes mean that the i2s_loopback task will run on an existing logical core if possible rather than creating a new one. In this case it will share the logical core used by the I2S master.

Configuring audio CODECs#

All of the audio hardware is setup using functions in lib_board_support. The previous inclusion of board.h from the xk_audio_316_mc_ab directory targets the hardware setup to the XU316 Multichannel Audio board (XK-AUDIO-316-MC). These lines perform some board-specific initialisation and start the I2C task.

            xk_audio_316_mc_ab_board_setup(hw_config); // Setup must be done on tile[0]

The hw_config struct specifies the hardware configuration. In this case, it sets up the xcore to be an I2S master with the following settings:

#define SAMPLE_FREQUENCY        48000
#define MASTER_CLOCK_FREQUENCY  24576000
#define DATA_BITS               32
#define CHANS_PER_FRAME         2
#define NUM_I2S_LINES           4

The following functions, called from the i2s_loopback task, complete the initialisation and configuration of the ADCs and DACs:

xk_audio_316_mc_ab_AudioHwInit(i_i2c, hw_config);
xk_audio_316_mc_ab_AudioHwConfig(i_i2c, hw_config, SAMPLE_FREQUENCY, MASTER_CLOCK_FREQUENCY, 0, DATA_BITS, DATA_BITS);

For full documentation of the lib_board_support API, please refer to the following link: lib_board_support.

The i2s_loopback application#

The I2S loopback task provides the function of a digital loopback so that all I2S samples received by the device will be forwarded on.

The task itself is declared as a [[distributable]] function allowing it to share a logical core with other tasks. This xC feature can be enabled for any task with the form:

...
while(1) {
  select {
    ...
  }
}

The function takes a number of arguments:

[[distributable]]
void i2s_loopback(server i2s_frame_callback_if i_i2s, client i2c_master_if i_i2c)

The interface to the I2S master, server i2s_frame_callback_if i_i2s, provides a set of callback functions. The I2S master will call these functions as needed.

The i2s_loopback task uses the I2C master interface, client i2c_master_if i_i2c, to configure the CODECs (ADCs and DACs) remotely.

The main loop in the i2s_loopback task handles the I2S interface calls.

    while (1) {
    select {
        case i_i2s.init(i2s_config_t &?i2s_config, tdm_config_t &?tdm_config):
            i2s_config.mode = I2S_MODE_I2S;
            i2s_config.mclk_bclk_ratio = (MASTER_CLOCK_FREQUENCY / (SAMPLE_FREQUENCY * CHANS_PER_FRAME * DATA_BITS));
            xk_audio_316_mc_ab_AudioHwConfig(i_i2c, hw_config, SAMPLE_FREQUENCY, MASTER_CLOCK_FREQUENCY, 0, DATA_BITS, DATA_BITS);
            break;

        case i_i2s.receive(size_t n_chans, int32_t in_samps[n_chans]):
            for (int i = 0; i < n_chans; i++){
                samples[i] = in_samps[i]; // copy samples
            }
            break;

        case i_i2s.send(size_t n_chans, int32_t out_samps[n_chans]):
            for (int i = 0; i < n_chans; i++){
                out_samps[i] = samples[i]; // copy samples
            }
            break;

        case i_i2s.restart_check() -> i2s_restart_t restart:
            restart = I2S_NO_RESTART; // Keep on looping
            break;
        } // End select
    } // End while (1)

The I2S master library calls the init() method before it starts any data streaming. This call allows the application to reset and configure the audio CODECs, for example when the sample rate changes.

The receive() interface method is called when the master has received a frame of audio samples (all channels in one sample period). The receive() method stores the samples in the samples array.

The I2S master calls the send() interface method when it needs a new frame of samples to send. In this case the application simply returns the frame of samples previously received.

Finally, the restart_check() method is called by the I2S master once per frame. It allows the application to control restart or shutdown of the I2S master. In this case the application continues to run forever and so always returns I2S_NO_RESTART.

Building the application#

The following section assumes you have downloaded and installed the XMOS XTC tools. See the README file for required version. Installation instructions can be found here. Be sure to pay attention to the section Installation of required third-party tools.

The application uses the xcommon-cmake build system as bundled with the XTC tools.

The AN00162_i2s_loopback_demo software zip-file should be downloaded and unzipped to a chosen directory.

To configure the build, run the following from an XTC command prompt:

cd an00162
cd app_an00162
cmake -G "Unix Makefiles" -B build

All required dependencies are included in the software download. However, if any are missing, they will be downloaded by the build system.

Finally, the application binaries can be built using xmake:

xmake -j -C build

The application uses approximately 3 kiB on Tile[0] and 7 kiB on Tile[1] (each tile has 512 kiB available).

Demo hardware setup#

Please refer to the XU316 Multichannel Audio board hardware platform documentation.

The demo is designed to run on the XU316 Multichannel Audio board. To run the demo:

  • Connect a USB cable from your host to the DEBUG connector.

  • Connect a USB cable from your host to the USB DEVICE connector.

  • Connect a sound source to the 3.5mm line in. Channels 1-2, 3-4, 5-6 or 7-8 can be used.

  • Connect headphones or speakers to the corresponding line out.

Running the demo application#

To run the application return to the app_an00162 directory and run the following command:

xrun bin/app_an00162.xe

You should hear the audio connected to the analog input jacks looped back to the output jacks.

References#

Full source code listing#

Source code for main.xc#

// Copyright 2014-2024 XMOS LIMITED.
// This Software is subject to the terms of the XMOS Public Licence: Version 1.

#include <platform.h>
#include <xs1.h>
#include "i2s.h"
#include "xk_audio_316_mc_ab/board.h"

#define SAMPLE_FREQUENCY        48000
#define MASTER_CLOCK_FREQUENCY  24576000
#define DATA_BITS               32
#define CHANS_PER_FRAME         2
#define NUM_I2S_LINES           4

// I2S resources
on tile[1]: in port p_mclk =                                PORT_MCLK_IN;
on tile[1]: buffered out port:32 p_lrclk =                  PORT_I2S_LRCLK;
on tile[1]: out port p_bclk =                               PORT_I2S_BCLK;
on tile[1]: buffered out port:32 p_dac[NUM_I2S_LINES] =     {PORT_I2S_DAC0, PORT_I2S_DAC1, PORT_I2S_DAC2, PORT_I2S_DAC3};
on tile[1]: buffered in port:32 p_adc[NUM_I2S_LINES] =      {PORT_I2S_ADC0 ,PORT_I2S_ADC1, PORT_I2S_ADC2, PORT_I2S_ADC3};
on tile[1]: clock bclk =                                    XS1_CLKBLK_1;


// Board configuration from lib_board_support
static const xk_audio_316_mc_ab_config_t hw_config = {
        CLK_FIXED,              // clk_mode. Drive a fixed MCLK output
        0,                      // 1 = dac_is_clock_master
        MASTER_CLOCK_FREQUENCY,
        0,                      // pll_sync_freq (unused when driving fixed clock)
        AUD_316_PCM_FORMAT_I2S,
        DATA_BITS,
        CHANS_PER_FRAME
};

[[distributable]]
void i2s_loopback(server i2s_frame_callback_if i_i2s, client i2c_master_if i_i2c)
{
    int32_t samples[NUM_I2S_LINES * CHANS_PER_FRAME] = {0}; // Array used for looping back samples
    // Config can be done remotely via i_i2c
    xk_audio_316_mc_ab_AudioHwInit(i_i2c, hw_config);

    while (1) {
    select {
        case i_i2s.init(i2s_config_t &?i2s_config, tdm_config_t &?tdm_config):
            i2s_config.mode = I2S_MODE_I2S;
            i2s_config.mclk_bclk_ratio = (MASTER_CLOCK_FREQUENCY / (SAMPLE_FREQUENCY * CHANS_PER_FRAME * DATA_BITS));
            xk_audio_316_mc_ab_AudioHwConfig(i_i2c, hw_config, SAMPLE_FREQUENCY, MASTER_CLOCK_FREQUENCY, 0, DATA_BITS, DATA_BITS);
            break;

        case i_i2s.receive(size_t n_chans, int32_t in_samps[n_chans]):
            for (int i = 0; i < n_chans; i++){
                samples[i] = in_samps[i]; // copy samples
            }
            break;

        case i_i2s.send(size_t n_chans, int32_t out_samps[n_chans]):
            for (int i = 0; i < n_chans; i++){
                out_samps[i] = samples[i]; // copy samples
            }
            break;

        case i_i2s.restart_check() -> i2s_restart_t restart:
            restart = I2S_NO_RESTART; // Keep on looping
            break;
        } // End select
    } // End while (1)
} // End i2s_loopback


int main(void)
{
    interface i2c_master_if i_i2c[1]; // Cross tile interface
        
    par {
        on tile[0]: {
            xk_audio_316_mc_ab_board_setup(hw_config); // Setup must be done on tile[0]
            xk_audio_316_mc_ab_i2c_master(i_i2c);      // Run I2C master server task to allow control from tile[1]
        }

        on tile[1]: {
            interface i2s_frame_callback_if i_i2s;

            par {
                // The application - loopback the I2S samples - note callbacks are inlined so does not take a thread
                [[distribute]] i2s_loopback(i_i2s, i_i2c[0]);
                i2s_frame_master(i_i2s, p_dac, NUM_I2S_LINES, p_adc, NUM_I2S_LINES, DATA_BITS, p_bclk, p_lrclk, p_mclk, bclk);
            }
        }
    }
    return 0;
}