Tags

, , , , ,

This post explores interfacing a PDM microphone with an NXP LPC810 ARM CORTEX-M0 microprocessor, taking advantage of its existing peripherals to do the heavy lifting at the high bit rate, allowing there to be enough power to implement a practical calculation based on the audio input.

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

A PDM Microphone Stunt

As a stunt, we will implement the signal processing chain for a PDM microphone in an LPC810. This is a 32-bit ARM CORTEX-M0 CPU in an 8-pin DIP package priced under $3.50 in single pieces.

It has family members with higher pin counts and with additional FLASH and RAM, but this model is sufficient for this demonstration, and easy to use play with in a solderless breadboard due to its classic 8-pin DIP package.

The LPC810 has 1K of SRAM and 4K of FLASH on the die, runs the CPU core at 30 MHz from a 1.8V to 3.6V supply. It includes an SPI interface which we will (mis-)use to capture the PDM bitstream.

We will run our LPC810 at 3.3V, with a 30MHz core clock.

Obviously, in a system of this scale we are not planning to record the audio in any form. Instead, we will measure its “loudness”, and output that measure as a PWM signal which can be easily converted to an analog level.

Other simple outputs that could be computed would include comparing the loudness to a threshold, perhaps with hysteresis and outputing a simple digital result.

For the purpose of this demo, we will connect the PWM output directly to a classic analog voltmeter so that the needle could be marked in dB SPL.

Parts is parts

There are only two significant parts to this demonstration, aside from the usual collection of passive components and hookup wire.

LPC810

The LCP810 used here was purchased along with a suitable USB serial adapter, 3.3V regulator, and a handful of useful passive components as a starter kit from Adafruit.

This is the LPC810M021FN8 in the friendly and classic DIP-8 package. Get your first one from Adafruit for the handy accessories, but get them in quantity from Digi-Key or your favorite distributor.

Oddly, all of the family members from NXP that are available in smaller packages also have higher pin counts, more FLASH and more RAM. I am slightly surprised that this exact die isn’t also available in a much smaller 8-pin package.

Knowles Microphone

We purchased a Knowles SPM0437HD4H-B microphone from Digi-Key, and made a simple breakout board for it by hand with copper-clad perfboard as described in a previous post.

face back

Wiring

Power will be supplied via the USB serial adapter that allows the firmware to be reflashed using the ISP boot loader included in the LPC810. The adapter provides a connection to USB Vbus, which is nominally 5V (and limited to 100 mA unless the USB device negotiated for more, and bounded at 500 mA regardless).

Since both the LPC810 and microphone are 3.3V devices, a regulator is required. The kit included a Microchip MCP1700-3.3V regulator and bypass caps, so that is what we use.

Using a solderless breadboard, we wire the LPC810, power supply, and microphone as follows:

LPC810 Pins      Usage
  1 ~RESET       Reset button
  2 TXD          White wire to serial
  3 SCLK         Mic clock from SPI
  4 P2           PWM Analog SPL Output
  5 MISO/~ISP    Mic data input to SPI 
  6 Vdd          3V3
  7 Vss          GND
  8 RXD          Green wire to serial

MCP1700-3.3     
  1 GND          GND Black wire to serial
  2 Vin          5V (Vbus) Red wire to serial
  3 Vout         3V3

MIC Breakout
  Wht/Blu        Mic clock
  Wht/Grn        Mic data
  Blu            3V3
  Grn            GND

USB Serial Cable (PL2303HX based, TTL levels)
  Blk            GND
  Red            5V USB Vbus
  Wht            LPC810 Txd, PC Rxd
  Grn            LPC810 Rxd, PC Txd

Include bypass caps from both 5V and 3V3 to GND near the MCP1700.

Connect an analog meter from the PWM pin to ground. For best results, a simple 1 pole low-pass filter made from a 680 ohm resistor and 10 µF capacitor will block all the PWM carrier leaving just the measured SPL analog level.

Handling PDM

Microphone clock and PDM Data

The SPI peripheral in master mode will generate a configured clock as long as the master has data to send. We just need to map SPI0_SCK to an external pin. For the demo, I chose pin 3.

The microphone data drives SPI0_MISO, which I mapped to pin 5. That pin is also the boot-time selector that chooses the ISP boot loader if held low at reset. Since SPI0_SCK is not operating at reset, the microphone is not driving its data output at all, and the CPU correctly interprets that as a normal boot. The microphone data is clocked into a 16-bit shift register in the SPI0 peripheral.

Note that we don’t need to map SPI0_MOSI to any pins at all. We may be sending data, but we don’t need it. It could be used as a kind of debug sonar if it were mapped. Various distinct status could be coded on it, and sampled with a logic analyzer or scope.

We configure the SPI clock for 1 MHz by dividing by 30.

The SPI receiver signals that data is available after 16 clocks. The data is then passed into the CIC filter 8 bits at a time to reconstruct lower sample rate, higher bit depth PCM samples.

CIC Filter Implementation

Our goal is to downsample the 1 MHz PDM bit stream input by a factor of 128, producing PCM samples at 7812.5 Hz. We do this in two stages. The first CIC stage downsamples by 8. The second by 16.

This is essentially the same filter model found in the ATTiny85 stunt. See that post and the following posts for explanation the gory details.

The simplest approach to a straight SPL meter in this platform does not use interrupts at all. Rather, it polls for the SPI to be ready, and does all the needed arithmetic on each block of PDM bits.

Testing shows that for SPL based on mean absolute sample value over reasonably sized windows and using the same log base 2 we used in the ATTiny85, it all works smoothly.

Software Tools

LPCXpresso

The LPC8xx family is well supported by the free development environment provided by NXP. Their LPCXpresso package provides a customized Eclipse IDE along with a GCC cross tool chain targeting ARM in a single installation. You do need to register with Code Red to download, but the download is free.

A good place to start for code samples and a nice tutorial for the LPC810 specifically is the help system at Adafruit. The tutorial covers collecting the tools, laying out the CPU, regulator, LED, and serial cable, and getting a simple blinky sample firmware to compile, load, and run.

If you need and want a complete IDE for NXP ARM devices, LPCXpresso is clearly the tool to have. However, you won’t be able to debug in the LPC8xx without a compatible JTAG or SWD interface. A compatible interface can be had as part of the eval board for the LPC812, priced on the street1 at under $27.

Of course, debugging in the LPC810 requires mapping SWDIO and SWCLK to specific pins in addition to ~RESET. That only leaves three free pins for in-circuit functions (two of which are the UART needed for using the boot loader for program loading), so we haven’t attempted to leave the debugger available on this breadboard.

One nice feature of the LPCXpresso IDE is the custom state machine editor provided by Code Red. It can generate configuration for the LPC810’s SCT peripheral (State Configurable Timer, a hardware implemented finite state machine that is tightly coupled with two timers/counters). I didn’t use it for this as a simple PWM output is easy to directly configure from C code, but it could be useful for more complex applications.

Loading Firmware

Loading firmware into the LPC8xx family can be done over the hardware debug channel (single wire debug, or SWD), or over an async serial connection by exercising a special version of the factory boot loader.

The serial ISP is particularly handy if you already plan to use the serial port with a PC. A simple TTL level USB serial adapter cable can power the LPC810 for both programming and running.

There seem to be two standard options for ISP programming from a PC, FlashMagic and lpc2isp, ignoring the third option of rolling your own programming tool based on data sheets and app notes from NXP.

FlashMagic is a free version of a commercial product. It is free for development use, costs money to license for use in a production line or field service setting, and can not be distributed independently. I used FlashMagic for playing with the LPC810 and it works slickly and does what I need done.

lpc2isp is an open source command-line utility, suitable for use in either a development or production environment. It may require some fussing to get it built right and to use it. I expect I will post about that once I’ve tried to use it.

Either way, with only 4 kB of FLASH memory to load, a serial port loader is a perfectly sane and sensible way to go.

Building the Demo

This demo is my first LCP810 project, so I built it stages. I took the advice of the Adafruit help system and started with a known to work LED Blinky project to prove that the development tools were all working, then modified it to add the microphone handling and SPL calculation.

Get the Blinky Running

Work through the whole Adafruit tutorial. At the end, you’ll have a build environment and hardware running firmware you compiled and flashed.

Modify it to listen to the mic

Connect the mic to 3V3, GND, SCLK, and MISO. The mic will default to left channel timing, and that happens to match the timing that the provided framework code assumes for the SPI port.

Adjust the pin configuration in configurePins() found in main.c. I simply replaced its provided content with this. The magic numbers come from playing with NXP’s web-based pin mux tool. I’ve retained the LED output which we will use for PWM, and mapped SCK and MISO for the mic. I’ve also assumed that SWD is impractical, and simply dropped the #if that supported it in the sample code.

void configurePins()
{
    /* Enable SWM clock */
    //  LPC_SYSCON->SYSAHBCLKCTRL |= (1 << 7);  // this is already done in SystemInit()

    /* Pin setup from web-based mux tool
       http://www.lpcware.com/content/tools/lpc-initializer
    ------------------------------------------------
    1   PIO0_5 = RESET
    2   PIO0_4 = U0_TXD
    3   PIO0_3 = SPI0_SCK   Mic clock 1MHz
    4   PIO0_2 = CTOUT_0    LED
    5   PIO0_1 = SPI0_MISO  Mic data, boot flash select
    6   Vdd
    7   Vss
    8   PIO0_0 = U0_RXD
    ------------------------------------------------
    NOTE: SWD is disabled to free GPIO pins!
    ------------------------------------------------ */

    /* Pin Assign 8 bit Configuration */
    /* U0_TXD */
    /* U0_RXD */
    LPC_SWM->PINASSIGN0 = 0xffff0004UL;
    /* SPI0_SCK */
    LPC_SWM->PINASSIGN3 = 0x03ffffffUL;
    /* SPI0_MISO */
    LPC_SWM->PINASSIGN4 = 0xffff01ffUL;
    /* CTOUT_0 */
    LPC_SWM->PINASSIGN6 = 0x02ffffffUL;

    /* Pin Assign 1 bit Configuration */
    /* RESET */
    LPC_SWM->PINENABLE0 = 0xffffffbfUL;
}

Declare some variables and configure the hardware at the top of main():

int main(void)
{
    uint8_t scount = CIC2_R/2;         // 2nd CIC decimation counter
    uint8_t pcount = WINDOWSPERPRINT;  // SPLs to wait between UART prints
    uint32_t n = 0;                    // PCM samples in SPL window
    uint32_t sabs = 0;                 // sum of absolute valued samples
    int32_t sum = 0;                   // sum of signed samples
    int avg = 0 ;                      // mean of previous SPL window

    /* Initialise the GPIO block */
    gpioInit();

    /* Initialise the UART0 block for printf output */
    uart0Init(115200);

    /* Configure the multi-rate timer for 1ms ticks */
    mrtInit(__SYSTEM_CLOCK/1000);


    /* Configure SPI0 for PDM microphone at 1 MHz */
    spiInit(LPC_SPI0, 29, 0);

    /* Configure PWM based on the SCT */
    pwmInit();

    /* Configure the switch matrix (setup pins for UART0 and GPIO) */
    configurePins();

Add code for a read-only transfer of 16 bits to the framework’s spi.h:

uint16_t spiRead16  (LPC_SPI_TypeDef *SPIx);

and spi.c. This function keeps sending 16-bit long words as long as the transmitter is ready, and returns as soon as the receiver is ready. This keeps the clock output running, while polling for the next available mic data.

/* receive 16 bits from the SPI bus */
uint16_t spiRead16(LPC_SPI_TypeDef *SPIx)
{
  if (SPIx->STAT & SPI_STAT_TXRDY)
      SPIx->TXDATCTL = SPI_TXDATCTL_FSIZE(16-1) | 0;
  while ( (SPIx->STAT & SPI_STAT_RXRDY) == 0 )
      if (SPIx->STAT & SPI_STAT_TXRDY)
          SPIx->TXDATCTL = SPI_TXDATCTL_FSIZE(16-1) | 0;
  return SPIx->RXDAT;
}

We also need the implementation of lg2() and the first stage CIC table in main.c:

/** Fixed point log base 2 of an unsigned 32 bit integer.
 *
 *  Return an unsigned 8-bit value ranging from 0 to 255 which includes
 *  3 bits of fraction. Except for some special case returns for inputs
 *  less than 8, the fraction bits are exactly the next three
 *  significan bits of the input value below its most significant set
 *  bit. The integer part is the bit number of the most significant set
 *  bit. The output values approximate the log curve to within a
 *  few percent over the whole range.
 *
 *  \param v Value to compute logarithm of.
 *
 *  \return Returns 8*log2(v). Returns 0 if v is 0.
 */

uint8_t lg2(uint32_t v) {
    static const uint8_t log2table[]  = {
            0,0,8,13,16,19,21,22
    };
    if (!(v&~7)) {
        return log2table[v];
    }
    uint8_t r = 3;     // r will be 8*log2(v)
    while (v & ~0xF) {
        v>>=1;
        ++r;
    }
    return (r<<3) | (v&0x7);
}

#define WINDOWSIZE 781 // Fs=7812.5 Hz
#define PWM_OFFSET 75  // nominally == 8*log2(WINDOWSIZE)
#define WINDOWSPERPRINT 20

const int8_t pdmsum8[256] = {
#   define S(n) (2*(n)-8)
#   define B2(n) S(n),  S(n+1),  S(n+1),  S(n+2)
#   define B4(n) B2(n), B2(n+1), B2(n+1), B2(n+2)
#   define B6(n) B4(n), B4(n+1), B4(n+1), B4(n+2)
        B6(0), B6(1), B6(1), B6(2)
};

The CIC filters need some global variables for their state in main.c:

typedef int16_t CICREG;
CICREG s2_sum1 = 0;
CICREG s2_comb1_1 = 0;
CICREG s2_comb1_2 = 0;
CICREG s2_sum2 = 0;
CICREG s2_comb2_1 = 0;
CICREG s2_comb2_2 = 0;

#define CIC2_R 16

Finally, the bulk of main() becomes this infinite loop, that polls for 16 PDM bits to be received, then processes them. We've left the blink's configuration of the UART in place, and freely use it to announce values on the serial port occasionally. Since we haven't added any interrupt handling for the UART, any strings printed will interrupt the sample collection process, including the microphone's clock. As long as those interruptions are short enough, the mic doesn't seem to care and we don't care much either since we are only computing SPL and a few noisy PDM bits cannot disrupt that calculation by enough to matter.

    printf("LPC810 PDM Mic Demo!rn");
    while(1)
    {
        uint16_t pdm = spiRead16(LPC_SPI0);

As in the ATTiny85 code, the first stage of PDM to PCM is to count the set bits in each of the captured bytes, with each bit rescaled to a signed +/- 1 range. This is the equivalent of an order-1 CIC filter with R=8, M=1, N=1. The bit growth of the output of this filter is then N  log_2 R M or 3, for 4 total significant bits out from the single bit in. The numeric range at this stage is -8 to 8, so it can't fit in a 4-bit 2's complement variable, but that is moot.

The actual counting is done by lookup in the pdmsum8[] table, which is done for each of the two bytes in the 16 bit SPI receive register. Then we feed the 4 bit result to a second CIC with N=2, R=16, M=2 which has bit growth of 10, for a total of 14 significant bits out. The counter scount is used to implement the decimation, which must be by a multiple of 2 (which was also a requirement of the CIC filter) since scount is actually counting byte pairs here.

        s2_sum1 += pdmsum8[pdm&0xff] ;
        s2_sum2 += s2_sum1;
        s2_sum1 += pdmsum8[pdm>>8] ;
        s2_sum2 += s2_sum1;

        if (!--scount) {
            CICREG Rout2 = s2_sum2;
            CICREG stage1, stage2; 

            scount = CIC2_R/2;

            stage1 = Rout2 - s2_comb1_2;
            s2_comb1_2 = s2_comb1_1;
            s2_comb1_1 = Rout2;

            stage2 = stage1 - s2_comb2_2;
            s2_comb2_2 = s2_comb2_1;
            s2_comb2_1 = stage1;

The finished PCM sample is in the variable stage2. We add it to the accumulators to implement SPL via mean of absolute values, also keeping a signed mean value to use to eliminate any DC offset. (Eliminating the DC offset is actually required because the mic is documented to have a 6% of full scale offset which swamps anything but really loud noises if not removed.) The samples in the SPL window are counted by n.

            sum += stage2;
            sabs += (stage2-avg) > 0 ? (stage2-avg) : -(stage2-avg);
            ++n;

After a window's worth of samples have been accumulated, we finish the SPL calculation and set the PWM output unless this is the first window after we've printed a result according to pcount which runs from WINDOWSPERPRINT down to 0

            if (n == WINDOWSIZE) {
                int spl;

                spl = (lg2(sabs) - PWM_OFFSET) * 2;
                // set the PWM unless we just interrupted the mic's data for printing
                if (pcount != WINDOWSPERPRINT)
                    pwmSet(spl);

                avg = sum / WINDOWSIZE;
                sum = 0;
                sabs = 0;
                n = 0;
                if (!--pcount) {
                    pcount = WINDOWSPERPRINT;
                    printf("%d %drn", spl, avg);
                }
            }
        }
    } // bottom of while(1) loop
} // end of main()

The PWM output is implemented with the state controlled timer (SCT) peripheral. It is a general purpose finite state machine engine that can control one 32-bit or two 16-bit timers. We use it for a simple PWM output by setting it to divide the core clock by 30000 to get a base rate of 1 kHz. We use one SCT event to turn on the output, and a second to turn it off. The pulse width is controlled by the number of counts between the on and off events, and if phase mattered it could be controlled by moving the position of both events together.

The implementation is in pwm.c:

#include "pwm.h"
#include "LPC8xx.h"

#define SCT_CONFIG_UNIFY   (1<<0)
#define SCT_CONFIG_NORELOAD_L  (1<<7)
#define SCT_CONFIG_NORELOAD_H  (1<<8)
#define SCT_CONFIG_AUTOLIMIT_L (1<<17)
#define SCT_CONFIG_AUTOLIMIT_H (1<<18)


#define SCT_CTRL_DOWN_L        (1<<0)
#define SCT_CTRL_STOP_L        (1<<1)
#define SCT_CTRL_HALT_L        (1<<2)
#define SCT_CTRL_CLRCTR_L  (1<<3)
#define SCT_CTRL_BIDIR_L   (1<<4)
#define SCT_CTRL_PRE_L(n)  (((n)&0xff)<<5)
#define SCT_CTRL_DOWN_H        (1<<16)
#define SCT_CTRL_STOP_H        (1<<17)
#define SCT_CTRL_HALT_H        (1<<18)
#define SCT_CTRL_CLRCTR_H  (1<<19)
#define SCT_CTRL_BIDIR_H   (1<<20)
#define SCT_CTRL_PRE_H(n)  (((n)&0xff)<<21)

#define SCT_EVCTRL_MATCHSEL(n) (((n)&0x0f)<<0)
#define SCT_EVCTRL_COMBMODE(n) (((n)&0x03)<SYSAHBCLKCTRL |= (1 <PRESETCTRL    &= ~(1 <PRESETCTRL    |=  (1 <CTRL_U = (0
            | SCT_CTRL_HALT_L
            );
    LPC_SCT->CONFIG = (0
            | SCT_CONFIG_UNIFY
            | SCT_CONFIG_AUTOLIMIT_L
            );
    LPC_SCT->REGMODE_L = 0;

    // 30 MHz Count to 30000 for 1 kHz PWM base rate
    LPC_SCT->MATCH[0].U = 30000-1;
    LPC_SCT->MATCHREL[0].U = 30000-1;

    // Match values for the events, default 50% duty
    LPC_SCT->MATCH[1].U = 0;     // PWM Pulse Start
    LPC_SCT->MATCH[2].U = 15000; // PWM Pulse End
    LPC_SCT->MATCHREL[1].U = 0;      // PWM Pulse Start
    LPC_SCT->MATCHREL[2].U = 15000;  // PWM Pulse End

    // Event 0 creates the rising edge of the pulse
    // at MATCH[1], which will be count 0 unless
    // phase control is desired
    LPC_SCT->EVENT[0].CTRL = (0
            | SCT_EVCTRL_MATCHSEL(1)
            | SCT_EVCTRL_COMBMODE(1)
            );
    LPC_SCT->EVENT[0].STATE = 1; // active on state 0

    // Event 1 creates the falling edge of the pulse
    // at MATCH[2], which can vary based on the desired
    // pulse width.
    LPC_SCT->EVENT[1].CTRL = (0
            | SCT_EVCTRL_MATCHSEL(2)
            | SCT_EVCTRL_COMBMODE(1)
            );
    LPC_SCT->EVENT[1].STATE = 1; // active on state 0

    // remaining events are unused
    LPC_SCT->EVENT[2].STATE = 0;
    LPC_SCT->EVENT[3].STATE = 0;
    LPC_SCT->EVENT[4].STATE = 0;
    LPC_SCT->EVENT[5].STATE = 0;

    // Set CTOUT_0 on Event 0 and clear it on Event 1
    LPC_SCT->OUT[0].SET = (1<OUT[0].CLR = (1<OUT[1].SET = 0;
    LPC_SCT->OUT[1].CLR = 0;
    LPC_SCT->OUT[2].SET = 0;
    LPC_SCT->OUT[2].CLR = 0;
    LPC_SCT->OUT[3].SET = 0;
    LPC_SCT->OUT[3].CLR = 0;

    LPC_SCT->COUNT_U = 0;
    LPC_SCT->CTRL_U = (0
            | SCT_CTRL_CLRCTR_L
            //| SCT_CTRL_CLRCTRL_H
            | SCT_CTRL_PRE_L(0)
            );
}

void pwmSet(int val) {
    int v = (30000*val/255);
    if (v = 30000) {
        v = 30000;
    }
    LPC_SCT->MATCHREL[2].U = v-1;    // PWM Pulse End
}

With the public interface declared in pwm.h. This is not a completely general PWM implementation, but it is sufficient for my needs here.

extern void pwmInit(void);
extern void pwmSet(int v);

Demo and Next Steps

After flashing the firmware and letting it run, the PWM output will modulate the LED based on SPL, and periodic SPL values will be output on the serial port.

A future post will provide some video of the demo code jumping in response to various noisy objects.

A future post will document the placement of the source code into the fossil repository, along with instructions for building and flashing it with GCC and lpc2isp instead of the LPCXpresso IDE.

One refinement would be to use a running average SPL value for the serial port output, with the average set long enough to properly antialias the values printed. This would make the printed value roughly equivalent to a standard SPL meter’s slow setting, while the PWM output is roughly equivalent to the fast setting.

Another refinement would be to introduce interrupt handlers for both the UART transmitter and the SPI. That would allow the SPI clock to run without interruption and for data to collect and be reduced to PCM samples in parallel with the UART printing.


(Written with StackEdit.)


  1. Priced at Digi-Key on 2015-02-06. You’ll need to cut some jumpers to use the SWD/JTAG interface for your target hardware, or you can just debug your firmware in the provided 20-pin LPC812 package. 
Advertisements