Getting Started

There are three models for how the mic array unit can be included in your application. The details of how to allocate, initialize and start the mic array will depend on the chosen model.

These are:

  • Vanilla Model - This is meant to be the simplest way to include the mic array. It is usually sufficient but offers relatively little flexibility with respect to configuration and run-time control. Using this model (mostly) means modifying an application’s build scripts.

  • Prefab Model - This model involves a little more work from the application developer, including writing a couple C++ wrapper functions, but gives the application access to any of the defined prefab mic array components.

  • General Model - Whenever the above are not used. This is necessary if an application wishes to use a customized mic array component.

The vanilla and prefab models for integrating the mic array into your application will be discussed in more detail below. The general model may involve customizing or extending the classes in lib_mic_array and is beyond the scope of this introduction. Whichever model is chosen, the first step is to identify your hardware resources.

Identify Resources

The key hardware resources to be identified are the ports and clock blocks that will be used by the mic array unit. The ports correspond to the physical pins on which clocks and sample data will be signaled. Clock blocks are a type of hardware resource which can be attached to ports to coordinate the presentation and capture of signals on the pins.

Clock Blocks

While clock blocks may be more abstract than ports, their implications for this library are actually simpler. First, the mic array unit will need a way of taking the audio master clock and dividing it to produce a PDM sample clock. This can be accomplished with a clock block. This will be the clock block which the API documentation refers to as “Clock A”.

Second, if (and only if) the PDM microphones are being used in a Dual Data Rate (DDR) configuration a second clock block will be required. In a DDR configuration 2 microphones share a physical pin for output data, where one signals on the rising edge of the PDM clock and the other signals on the falling edge. The second clock block required in a DDR configuration is referred to as “Clock B” in the API documentation.

Each tile on an xcore.ai device has 5 clock blocks available. In code, a clock block is identified by its resource ID, which are given as the preprocessor macros XS1_CLKBLK_1 through XS1_CLKBLK_5.

Unlike ports, which are tied to specific physical pins, clock blocks are fungible. Your application is free to use any clock block that has not already been allocated for another purpose. The vanilla component model defaults to using XS1_CLKBLK_1 and XS1_CLKBLK_2.

Ports

Three ports are needed for the mic array component. As mentioned above, ports are physically tied to specific device pins, and so the correct ports must be identified for correct behavior.

Note that while ports are physically tied to specific pins, this is _not_ a 1-to-1 mapping. Each port has a port width (measured in bits) which is the number of pins which comprise the port. Further, the pin mappings for different ports overlap, with a single pin potentially belonging to multiple ports. When identifying the needed ports, take care that both the pin map (see the documentation for your xcore.ai package) and port width are correct.

The first port needed is a 1-bit port on which the audio master clock is received. In the code and various places in the documentation, you may see this referred to as p_mclk.

The second port needed is a 1-bit port on which the PDM clock will be signaled to the PDM mics. This port is often referred to as p_pdm_clk.

The third port is that on which the PDM data is received. In an SDR configuration, the width of this port must be greater than or equal to the number of microphones. In a DDR configuration, this port width must be greater than or equal to the number of microphones. This port is referred to as p_pdm_mics.

XCore applications are typically compiled with an “XN” file (with a “.xn” file extension). An XN file is an XML document which describes some information about the device package as well as some other helpful board-related information. The identification of your ports may have already been done for you in your XN file. Here is a snippet from an XN file:

...
<Tile Number="1" Reference="tile[1]">
  <!-- MIC related ports -->
  <Port Location="XS1_PORT_1G"  Name="PORT_PDM_CLK"/>
  <Port Location="XS1_PORT_1F"  Name="PORT_PDM_DATA"/>
  <!-- Audio ports -->
  <Port Location="XS1_PORT_1D"  Name="PORT_MCLK_IN_OUT"/>
  <Port Location="XS1_PORT_1C"  Name="PORT_I2S_BCLK"/>
  <Port Location="XS1_PORT_1B"  Name="PORT_I2S_LRCLK"/>
  <!-- Used for looping back clocks -->
  <Port Location="XS1_PORT_1N"  Name="PORT_NOT_IN_PACKAGE_1"/>
</Tile>
...

The first 3 ports listed, PORT_PDM_CLK, PORT_PDM_DATA and PORT_MCLK_IN_OUT are respectively p_pdm_clk, p_pdm_mics and p_mclk. The value in the “Location” attribute (e.g. XS1_PORT_1G) is the port name as you will find it in your package documentation.

In this case, either PORT_PDM_CLK or XS1_PORT_1G can be used in code to identify this port.

Declaring Resources

Once the ports and clock blocks to be used have been indentified, these resources can be represented in code using a pdm_rx_resources_t struct. The following is an example of declaring resources in a DDR configuration. See pdm_rx_resources_t, PDM_RX_RESOURCES_SDR() and PDM_RX_RESOURCES_DDR() for more details.

pdm_rx_resources_t pdm_res = PDM_RX_RESOURCES_DDR(
                                PORT_MCLK_IN_OUT,
                                PORT_PDM_CLK,
                                PORT_PDM_DATA,
                                XS1_CLKBLK_1,
                                XS1_CLKBLK_2);

Note that this is not necessary when using the vanilla model, as it is done for you.

Other Resources

In addition to ports and clock blocks, there are also several other hardware resource types used by lib_mic_array which are worth considering. Running out of any of these will preclude the mic array from running correctly (if at all)

  • Threads - At least one hardware thread is required to run the mic array component. A second thread may also be used for modestly reduced MIPS consumption.

  • Computation - The mic array unit will require a fixed number of MIPS (millions of instructions per second) to perform the required processing. The exact amount will depend on the configuration used.

  • Memory - The mic array requires a modest amount of memory for code and data. (see @todo).

  • Chanends - At least 4 chanends must be available for signaling between threads/sub-components.

Vanilla Model

Mic array configuration with the vanilla model is achieve mostly through the application’s build system configuration.

In the /etc/vanilla directory of this repository are a source and header file which are not compiled with (or on the include path) of the library. Configuring the mic array using the vanilla model means adding those files to your application’s build (not the library target), and defining several compiler flags which tell it how to behave.

Vanilla - CMake Macro

To simplify this further, a CMake macro called mic_array_vanilla_add() has been included with the build system.

mic_array_vanilla_add() takes several arguments:

  • TARGET_NAME - The name of the CMake application target that the vanilla mode source should be added to.

  • MCLK_FREQ - The frequency of the master audio clock, in Hz.

  • PDM_FREQ - The desired frequency of the PDM clock, in Hz.

  • MIC_COUNT - The number of microphone channels to be captured.

  • SAMPLES_PER_FRAME - The size of the audio frames produced by the mic array unit (frames will be 2 dimensional arrays with shape (MIC_COUNT,SAMPLES_PER_FRAME)).

Vanilla - Optional Configuration

Though not exposed by the mic_array_vanilla_add() macro, several additional configuration options are available when using the vanilla model. These are all configured by adding defines to the application target.

Vanilla - Initializing and Starting

Once the configuration options have been chosen, initializing and starting the mic array at run-time is achieved easily. Two function calls are necessary, both can be included through mic_array_vanilla.h.

First, during application initialization, the function ma_vanilla_init(), which takes no arguments, must be called. This will configure the hardware resources and install the PDM rx service as an ISR, but will not actually start any threads or PDM capture.

Then, once initialization is complete, to begin PDM capture and processing, the vanilla thread entry point, ma_vanilla_task() is called. ma_vanilla_task() takes a single argument which is the chanend that will be used to transmit audio frames to subsequent stages of the processing pipline. Usually the call to ma_vanilla_task() will be placed directly in a par {...} block along with other threads do be started on the tile.

(Note that these functions must be called from the core which will host the decimation thread)

Prefab Model

The lib_mic_array library has a C++ namespace mic_array::prefab which contains class templates for typical mic array setups using common sub-components. The intention is to hide most of the complexity (and unneeded flexibility) from the application author, so they can focus only on pieces they care about.

(Note, at the time of this writing, only one prefab class template, mic_array::prefab::BasicMicArray has been defined.)

To configure the mic array using a prefab, you will need to add a C++ source file to your application. NB: This will end up looking a lot like the contents of mic_array_vanilla.cpp when you are through.

Prefab - Declare Resources

The example in this section will use 2 microphones in a DDR configuration with DC offset elimination enabled, and using 128-sample frames. The resource IDs used may differ than those required for your application.

pdm_res will be used to identify the ports and clocks which will be configured for PDM capture.

#include "mic_array/cpp/Prefab.hpp"
...
#define MIC_COUNT    2    // 2 mics
#define DCOE_ENABLE  true // DCOE on
#define FRAME_SIZE   128  // 128 samples per frame
...
pdm_rx_resources_t pdm_res = PDM_RX_RESOURCES_DDR(
                                PORT_MCLK_IN_OUT,
                                PORT_PDM_CLK,
                                PORT_PDM_DATA,
                                MIC_ARRAY_CLK1,
                                MIC_ARRAY_CLK2);

Prefab - Allocate MicArray

The C++ class template mic_array::MicArray is central to the mic array unit in this library. The class templates defined in the mic_array::prefab namespace each derive from mic_array::MicArray.

Define and allocate the specific implementation of MicArray to be used.

// Using the full name of the class could get cumbersome. Let's give it an
// alias.
using TMicArray = mic_array::prefab::BasicMicArray<
                      MIC_COUNT, FRAME_SIZE, DCOE_ENABLED>
// Allocate mic array
TMicArray mics = TMicArray();

Now the mic array unit has been defined and allocated. Because class templates were used, the mics object is self-contained, without the need of external data buffers. Additionally, class templates will ultimately allow unused features to be optimized out at build time. For example, if DCOE is disabled, it will be optimized out so that at run-time there won’t even be a check to see whether it’s enabled.

Prefab - Init and Start Functions

You’ll now need to implement a couple functions in your C++ file. In most cases these functions will need to be callable from C or XC, and so they should not be static, and they should be decorated with extern "C" (or the MA_C_API preprocessor macro provided by the library).

First, a function which initializes the MicArray object and configures the port and clock block resources. The documentation for mic_array::prefab::BasicMicArray will indicate any parts of the MicArray object that need to be initialized.

#define MCLK_FREQ   24576000
#define PDM_FREQ    3072000
...
MA_C_API
void app_init() {
  // Configure clocks and ports
  const unsigned mclk_div = mic_array_mclk_divider(MCLK_FREQ, PDM_FREQ);
  mic_array_resources_configure(&pdm_res, mclk_div);

  // Initialize the PDM rx service
  mics.PdmRx.Init(pdm_res.p_pdm_mics);
}

app_init() can be called from an XC main() during initialization.

For this example we’ll assume we want to run the PDM rx service as an ISR. We’ll start the PDM clock, install the ISR and enter the decimator thread.

MA_C_API
void app_mic_array_task(chanend_t c_audio_frames) {
  mics.SetOutputChannel(c_audio_frames);

  // Start the PDM clock
  mic_array_pdm_clock_start(&pdm_res);

  mics.InstallPdmRxISR();
  mics.UnmaskPdmRxISR();

  mics.ThreadEntry();
}

Now a call to app_mic_array_task() with the channel to send frames on can be placed inside a par {...} block to spawn the thread.