PDM to PCM Filter Design#

Introduction#

Conversion of the 1 bit PDM signal at a high sample rate to a 32 bit PCM signal at a lower sample rate requires several decimation stages. Each filter should remove the frequencies that will alias into the final passband, this typically means attenuation by at least 80dB. There are 3 types of stage, all of which are implemented as FIR filters:

  • First stage: moving average filter, decimation of PDM signal by 32.

  • Middle stages (optional): half band filter, decimation by 2

  • Final stage: low pass filter, decimation to final sample rate.

A 2 stage filter has a simpler implementation, but a 3 or more stage filter needs fewer computations for the same performance. Python scripts to aid the design are provided in this folder.

Example designs#

Functions to design 2 and 3 stage decimation filters can be found in design_filters.py. This also contains several example designs:

  • good_2_stage_filter: decimation from 3.072 MHz to 16 kHz using 2 stages.

  • small_2_stage_filter: as above, but using fewer filter taps in stage 2. This saves memory, at the cost of reduced alias suppression and early passband rolloff.

  • good_3_stage_filter: similar performance to good_2_stage_filter, but over 3 stages instead of 2. This results in fewer computations.

  • good_32k_filter: decimation from 3.072 MHz to 32 kHz using 2 stages.

Each example design returns coefficients in a packed format of [stage][coefficients, decimation_ratio]. The filter coefficients are returned as either float or int. If int_coeffs=True, stage 1 will be int16 and subsequent stages will be int32.

Calling python ./python/filter_design/design_filter.py will generate a .pkl file for each example. These can be run though python/stage1.py to convert the coefficients to hexadecimal values for use in lib_mic_array/src/etc/stage1_fir_coef.c. More details on this process are in python/README.rst

Plotting utilities#

plot_coeffs.py contains filter response plotting utilities for individual stages (plot_stage) and the combined response (plot_filters). Both can input and output axis handles for comparing filter designs.

When plotting a single stage using plot_stage, the a plot is generated showing the filter taps in the time domain, and the frequency response of the filter. The filter response shows the frequency response of the filter at the input sampling frequency. Frequencies that will be discarded by subsequent stages are greyed out, frequencies that will alias into the final passband are left white. These frequencies should have the most attenuation.

The passband response shows the filter response at the output sampling frequency. The decimated response includes all the aliases, the summed level of these is shown in green. The alias level in the final passband should be as low as possible. The passband ripple is the filter response, shown on a zoomed scale.

plot_filters shows a summary of the individual stages, and the combined filter response. Passband roll off in stage 1 can be corrected stage 2, giving a flat combined response.

Designing custom filters#

Tools for designing custom decimation filter are provided. Generally, at least 80dB of alias attenuation is recommended. More taps is better, but will use more memory and CPU, and may have rounding issues in Stage 1. Some design considerations for each stage are detailed below

Stage 1#

The stage 1 filter starts with a 32 tap moving average filer (np.ones(32)), which is convolved with itself ma_stages times. This gives a frequency response with strong nulls at frequencies that will alias into the final passband. More stages will give more attenuation and widen the nulls. The number of taps for the final filter is (31*ma_stages) + 1.

The coefficient values for 4 or fewer stages fits within the range of int16, so no rounding is required. For 5 or more stages, the range of coefficient values exceeds int16 range, so scaling and rounding is required which will affect the filter response and increase the level of the aliases. As each stage convolves np.ones(32) with the previous stage, a good solution is to scale and round the coefficients at an intermediate stage, allowing the final stages to be convolved without additional rounding.

Final Stage#

The ideal final stage filter would be a brickwall antialiasing filter (i.e. 8kHz for a 16kHz output). A realistic implementation must balance the flatness of the passband with the amount of alias attenuation. More taps allows for a steeper slope, increased alias attenuation and less compromise, but requires more memory and compute.

The filter is designed using the window method, typically using a Kaiser window which allows for good control of the filter slopes (other windows can be specified). The target filter response compensates for any rolloff in previous stages to give a flat combined filter response, generally this results in around 1dB rise at higher frequencies. The C implementation requires the coefficients to be int32, scaling and rounding to this is not usually an issue.

Optional middle stages#

Extra decimation stages can be added after the initial MA filter. This means the final stage filter runs at a lower sample rate, and so needs fewer taps for the same performance. Middle stages are expected to decimate by 2, allowing a half band filter to be used. These filters have a very large transition band as only frequencies that alias into the final passband need to be attenuated, and so require very few taps to implement effectively.

If (n_taps+1) % 4 == 0, then every other tap can be zero. This is achieved by designing the filter at the output sample rate, then upsampling it to the input sample rate by inserting zeros between the taps. Coefficients are int32, so scaling and rounding is not usually an issue.