, , , , , , , ,

Since porting the PDM microphone SPL measurement code base from the ATTiny85 to the ARM based LPC8xx family, it would make sense to verify that the calculations are performing as expected. This post will run through some test cases and the results in an LPC812 CPU.

This is part of a series of articles on the general subject of audio signal processing from air to information.

Previous installments include:

Yet another CPU?

We are playing with a variety of members of the NXP LPC8xx family. The 8 pin DIP packaged LPC810 is amusing in its nostalgic package, but the higher pin counts available for the LPC812 provide 4 times as much memory and fit in a much smaller volume at essentially no difference in price.

The firmware discussed in this post is similar to that seen in the previous post with some mostly cosmetic changes.

The most visible change is that the firmware has been ported to the LPCXpresso framework. This is practical in the LPC812 since there is a very generous 16 KB of flash available for code, compared to the 4 KB in the LPC810. The advantage of this is that the resulting firmware will be easier for others to extend and modify.

The real functional change is that given the framework we have switched the SPI code to work entirely at interrupt level. This means that the microphone clock is not interrupted when the UART prints, and the window immediately after a print is not corrupted. A framework implementation of a FIFO is used to pass finished PCM samples from the interrupt handler to the foreground loop where the SPL calculation, updates to the PWM outputs (an LED and the analog levels get separate PWM outputs), and the occasional print to the UART can happen at their own pace.

This iteration of the source code has not yet put under revision control, but that will happen “soon”. Watch the blog for an announcement.

Test signals

I hand-constructed two PDM bitstream fragments that represent conditions with levels that can be predicted.

I’ve built the test vectors as 128 bit fragments, stored 16 bits at a time. This representation matches well with the SPI peripheral configured for 16-bit frames. If the test vectors are enabled at compile time, then they replace the value read from the LPC_SPI0->RXDAT register each time the RXRDY interrupt is handled. A free-running counter increments on each receive value, and is used to index the array of test vectors. A run-time parameter controls the number of bits by which the counter is shifted before indexing, this allows for easily varying the test signal’s fundamental frequency by whole octaves.

The first signal is a fully clipped 100% full scale square wave. This is easily represented by long sequences of 1 bits alternating with equal length sequences of 0 bits. At low enough frequencies, we can predict that the absolute sample value from our CIC filter will be exactly 8192. Averaged over a 781 sample window, we can predict the expected scaled log2 SPL will be 208.

The second signal is a stepped triangle wave, peaking at 100% full scale. The 8 step levels are equivalent to PCM samples of 0, 4096, 8192, 4096, 0, -4096, -8192, and -4096. It has an ideal mean absolute value of 4096, which predicts the expected scaled log2 SPL will be 192.

The implementation allows the shift to be varied by using the debugger to adjust the value of the TESTSHIFT variable. The choice of which wave form is used is made by commenting out all the cases except the desired one from the initialization of testsamples[].

#ifdef PDMTEST
uint16_t testcount = 0;
const uint16_t testsamples[8] = {
//      0xffff, 0xffff, 0xffff, 0xffff, 0x0000, 0x0000, 0x0000, 0x0000, // 100%FS
        0x5555, 0xf5f5, 0xffff, 0x5f5f, 0x5555, 0x5050, 0x0000, 0x0505, // Aztec
uint8_t TESTSHIFT=3;
#define TESTMASK (((sizeof testsamples) / (sizeof testsamples[0]))-1)

In the implementation of SPI0_IRQHandler(), we must read the received data from the SPI0 peripheral in order to signal that we handled the interrupt. But if the test case is compiled in, we replace the value read with the appropriate value from the test vector.

        // ...
        if (status & SPI_STAT_RXRDY) {
            uint16_t pdm = LPC_SPI0->RXDAT;
#ifdef PDMTEST
            pdm = testsamples[((++testcount)>>TESTSHIFT) & TESTMASK];
        // ...


Running the firmware with both test vectors at a variety of frequencies produces the following results as reported on the serial port.

shift    100%FS   Aztec    F0, Hz
   0     -150     -150     7812.5       
   1     -150     -150     3906.2                   
   2      184      172     1953.1                   
   3      202      190      976.6                   
   4      206      192      488.3               
   5      208      194      244.1               
   6      208      194      122.1           
   7      208      194       61.0           
   8      210      194       30.5           
   9      210      196       15.2    

Some observations:

  1. The first two octaves are exactly at and above the Nyquist point. The CIC filters are producing PCM samples at exactly 7812.5 Hz, so the case with shift==1 is exactly at the Nyquist point. It isn’t surprising that the measured SPL effectively minus infinity for these rows since there isn’t any energy available below the Nyquist point to measure.

  2. The result of -150 is consistent with the earlier point. SPL is computed from a mean absolute sample value. The code sums the absolute sample value, computes SPL as 2 * (lg2(sum_abs) - WINDOWOFFSET) where lg2() is computing eight times log base 2, clipped at 0. If sum_abs is sufficiently small, then lg2() will return 0, and the SPL will be reported as -2 * WINDOWOFFSET, or exactly 75 in the firmware as tested.

  3. WINDOWOFFSET should be exactly 8 * lg2(WINDOWSIZE), where WINDOWSIZE is 781 samples, or 76. The code as tested has it set to 75.

  4. The 100% FS square wave measures 210, not the expected 208. This is a difference of 2, or exactly twice the error in the value of WINDOWSIZE used.

  5. The stepped triangle measures 192, 194, or 196 depending on the shift applied, but at reasonable frequencies it is 194. This is also exactly 2 counts higher than predicted, which is also exactly twice the error in the value of WINDOWSIZE used.


The test signals produce SPL results that exactly match expectations when quirks of the implementation are accounted for.

A meter on the 3.3V scale PWM output moves nicely when the bike horn is honked. The red LED also varies nicely in brightness as the SPL varies. (Blow on the mic and the ember gets brighter.)

For my next trick, I want to use the UART to transfer 8-bit PCM samples (or even better, 8-bit uLAW or aLaw) at our full sample rate to the PC. This will allow us to use the microphone to record telephone quality mono audio and verify that the resulting PCM samples sound “right”.

(Written with StackEdit.)