Prefab.hpp

template<unsigned MIC_COUNT, unsigned FRAME_SIZE, bool USE_DCOE, unsigned MICS_IN = MIC_COUNT>
class mic_array::prefab::BasicMicArray : public mic_array::MicArray<MIC_COUNT, TwoStageDecimator<MIC_COUNT, STAGE2_DEC_FACTOR, STAGE2_TAP_COUNT>, StandardPdmRxService<MIC_COUNT, MIC_COUNT, STAGE2_DEC_FACTOR>, std::conditional<USE_DCOE, DcoeSampleFilter<MIC_COUNT>, NopSampleFilter<MIC_COUNT>>::type, FrameOutputHandler<MIC_COUNT, FRAME_SIZE, ChannelFrameTransmitter>>

Class template for a typical bare-metal mic array unit.

This prefab is likely the right starting point for most bare-metal (i.e. not using an RTOS) applications.

With this prefab, the decimator will consume one device core, and the PDM rx service can be run either as an interrupt, or as an additional thread. The benefit of running PDM rx as a thread is decreased MIPS usage, but the drawback is that it consumes a core.

For the first and second stage decimation filters, this prefab uses the coefficients provided with this library. The first stage uses a (non-configurable) decimation factor of 32, and the second stage is configured to use a decimation factor of 6.

To get 16 kHz audio output from the BasicMicArray prefab, then, the PDM clock must be configured to 3.072 MHz (3.072 MHz / (32 * 6) = 16 kHz).

An instance of BasicMicArray has 4 sub-components responsible for different portions of the work being done. These sub-components are PdmRx, Decimator, SampleFilter and OutputHandler. See the documentation for MicArray for more details about these.

Sub-Components

The template parameter MIC_COUNT is the number of microphone channels to be processed and output.

Template Parameters Details

The template parameter FRAME_SIZE is the number of samples in each output frame produced by the mic array. Frame data is communicated using the API found in mic_array/frame_transfer.h. Typically ma_frame_rx() will be the right function to use in a receiving thread to retrieve audio frames. Note that calls to ma_frame_rx() will block until a frame becomes available on the specified chanend.

The boolean template parameter USE_DCOE indicates whether the DC offset elimination filter should be applied to the output of the second stage decimator. DC offset elimination is an IIR filter intended to ensure audio samples on each channel tend towards zero-mean.

See

Sample Filters

for more information about DC offset elimination.

If USE_DCOE is false, no further filtering of the second stage decimator’s output will occur.

The template parameter MICS_IN indicates the number of microphone channels to be captured by the PdmRx component of the mic array unit. This will often be the same as MIC_COUNT, but in some applications, MIC_COUNT microphones must be physically connected to an xCore port which is not MIC_COUNT (SDR) or MIC_COUNT/2 (DDR) bits wide.

In these cases, capturing the additional channels (likely not even physically connected to PDM microphones) is unavoidable, but further processing of the additional (junk) channels can be avoided by using MIC_COUNT < MICS_IN. The mapping which tells the mic array unit how to dervice output channels from input channels can be configured during initialization by calling StandardPdmRxService::MapChannels() on the PdmRx sub-component of the BasicMicarray.

If the application uses an SDR microphone configuration (i.e. 1 microphone per port pin), then MICS_IN must be the same as the port width. If the application is running in a DDR microphone configuration, MICS_IN must be twice the port width. MICS_IN defaults to MIC_COUNT.

Before a mic array unit can be started or initialized, it must be allocated.

Allocation

Instances of BasicMicArray are self-contained with respect to memory, needing no external buffers to be supplied by the application. Allocating an instance is most easily accomplished by simply declaring the mic array unit. An example follows.

#include "mic_array/cpp/Prefab.hpp"
...
using AppMicArray = mic_array:prefab::BasicMicArray<MICS,SAMPS,DCOE>;
AppMicArray mics;

Here, mics is an allocated mic array unit. The example (and all that follow) assumes the macros used for template parameters are defined elsewhere.

Before a mic array unit can be started, it must be initialized.

Initialization

BasicMicArray reads PDM samples from an xCore port, and delivers frames of audio data over an xCore channel. To this end, an instance of BasicMicArray needs to be given the resource IDs of the port to be read and the chanend to transmit frames over. This can be accomplished in either of two ways.

If the resource IDs for the port and chanend are available as the mic array unit is being allocated, one option is to explicitly construct the BasicMicArray instance with the required resource IDs using the two-argument constructor:

using AppMicArray = mic_array:prefab::BasicMicArray<MICS,SAMPS,DCOE>;
AppMicArray mics(PORT_PDM_MICS, c_frames_out);

Otherwise (typically), these can be set using BasicMicArray::SetPort(port_t) and BasicMicArray::SetOutputChannel(chanend_t) to set the port and channel respectively.

AppMicArray mics;
...
void app_init(port_t p_pdm_mics, chanend_t c_frames_out)
{
 mics.SetPort(p_pdm_mics);
 mics.SetOutputChannel(p_pdm_mics);
}

Next, the ports and clock block(s) used by the PDM rx service need to be configured appropriately. This is not accomplished directly through the BasicMicArray object. Instead, a pdm_rx_resources_t struct representing these hardware resources is constructed and passed to mic_array_resources_configure(). See the documentation for pdm_rx_resources_t and mic_array_resources_configure() for more details.

Finally, if running BasicMicArray’s PDM rx service within an ISR, before the mic array unit can be started, the ISR must be installed. This is accomplished with a call to BasicMicArray::InstallPdmRxISR(). Installing the ISR will not unmask it.

After it has been initialized, starting the mic array unit with the PDM rx service running as an ISR, three steps are required.

Begin Processing (PDM rx ISR)

First, the PDM clock must be started. This is accomplished with a call to mic_array_pdm_clock_start(). The same pdm_rx_resources_t that was passed to mic_array_resources_configure() is given as an argument here.

Second, the PDM rx ISR that was installed during initialization must be unmasked. This is accomplished by calling BasicMicArray::UnmaskPdmRxISR() on the mic array unit.

Finally, the mic array processing thread must be started. The entry point for the mic array thread is BasicMicArray::ThreadEntry().

A typical pattern will include all three of these steps in a single function which wraps the mic array thread entry point.

AppMicArray mics;
pdm_rx_resources_t pdm_res;
...
MA_C_API  // alias for 'extern "C"'
void app_mic_array_task()
{
 mic_array_pdm_clock_start(&pdm_res);
 mics.UnmaskPdmRxISR();
 mics.ThreadEntry();
}

Using this pattern, app_mic_array_task() is a C-compatible function which can be called from a multi-tile main() in an XC file. Then, app_mic_array_task() is called directly from a par {...} block. For example,

main(){
 ...
 par {
   on tile[1]: {
     ... // Do initialization stuff
     
     par {
       app_mic_array_task();
       ...
       other_thread_on_tile1(); // other threads
     }
   }
 }
}

The procedure for running the mic array unit with the PDM rx component running as a stand-alone thread is much the same with just a couple key differences.

Begin Processing (PDM Rx Thread)

When running PDM rx as a thread, no call to BasicMicArray::UnmaskPdmRxISR() is necessary. Instead, the application spawns a second thread (the first being the mic array processing thread) using BasicMicArray::PdmRxThreadEntry() as the entry point.

mic_array_pdm_clock_start() must still be called, but here the requirement is that it be called from the hardware thread on which the PDM rx component is running (which, of course, cannot be the mic array thread).

A typical application with a multi-tile XC main() will provide two C-compatible functions &#8212; one for each thread:

MA_C_API
void app_pdm_rx_task()
{
 mic_array_pdm_clock_start(&pdm_res);
 mics.PdmRxThreadEntry();
}

MA_C_API
void app_mic_array_task()
{
 mics.ThreadEntry();
}

The threads are spawned from XC main using a par {...} block:

main(){
 ...
 par {
   on tile[1]: {
     ... // Do initialization stuff
     
     par {
       app_mic_array_task();
       app_pdm_rx_task();
       ...
       other_thread_on_tile1(); // other threads
     }
   }
 }
}

Once the PDM rx thread is launched or the PDM rx interrupt has been unmasked, PDM data will start being collected and reported to the decimator thread. The application then must start the decimator thread within one output sample time (i.e. sample time for the output of the second stage decimator) to avoid issues.

Real-Time Constraint

Once the mic array processing thread is running, the real-time constraint is active for the thread consuming the mic array unit’s output, and it must waiting to receive an audio frame within one frame time.

This library comes with examples which demonstrate how a mic array unit is used in an actual application. If you are encountering difficulties getting BasicMicArray to work, studying the provided examples may help.

Examples

Todo:

link to example apps?

Todo:
Hardware resource usage

Warning

If the receiving thread is not waiting to retrieve the audio frame from the mic array when it becomes available, the pipeline may back up and cause samples to be dropped. It is the responsibility of the application developer to ensure this does not happen.

Note

BasicMicArray::InstallPdmRxISR() installs the ISR on the hardware thread that calls the method. In most cases, installing it in the same thread as the decimator is the right choice.

Note

Notice that app_mic_array_task() above is a thin wrapper for mics.ThreadEntry(). Unfortunately, because the type of mics is a C++ class, mics.ThreadEntry() cannot be called directly from an XC file (including the one containing main()). Further, because a C++ class template was used, this library cannot provide a generic C-compatible call wrapper for the methods on a MicArray object. This unfortunately means it is necessary in some cases to create a thin wrapper such as app_mic_array_task().

Template Parameters
  • MIC_COUNT – Number of microphone channels.

  • FRAME_SIZE – Number of samples in each output audio frame.

  • USE_DCOE – Whether DC offset elimination should be used.

Public Functions

inline constexpr BasicMicArray() noexcept

No-argument constructor.

This constructor allocates the mic array and nothing more.

Call BasicMicArray::Init() to initialize the decimator.

Subsequent calls to BasicMicArray::SetPort() and BasicMicArray::SetOutputChannel() will also be required before any processing begins.

void Init()

Initialize the decimator.

BasicMicArray(port_t p_pdm_mics, chanend_t c_frames_out)

Initialzing constructor.

If the communication resources required by BasicMicArray are known at construction time, this constructor can be used to avoid further initialization steps.

This constructor does not install the ISR for PDM rx, and so that must be done separately if PDM rx is to be run in interrupt mode.

Parameters
  • p_pdm_mics – Port with PDM microphones

  • c_frames_out – (non-streaming) chanend used to transmit frames.

void SetPort(port_t p_pdm_mics)

Set the PDM data port.

This function calls this->PdmRx.Init(p_pdm_mics).

This should be called during initialization.

Parameters

p_pdm_mics – The port to receive PDM data on.

void SetOutputChannel(chanend_t c_frames_out)

Set the audio frame output channel.

This function calls this->OutputHandler.FrameTx.SetChannel(c_frames_out).

This must be set prior to entrying the decimator task.

Parameters

c_frames_out – The channel to send audio frames on.

void PdmRxThreadEntry()

Entry point for PDM rx thread.

This function calls this->PdmRx.ThreadEntry().

Note

This call does not return.

void InstallPdmRxISR()

Install the PDM rx ISR on the calling thread.

This function calls this->PdmRx.InstallISR().

void UnmaskPdmRxISR()

Unmask interrupts on the calling thread.

This function calls this->PdmRx.UnmaskISR().