Tags

, , , , , , ,

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

We demonstrate the possibility of interfacing a PDM microphone with an Atmel ATTiny85 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 demo will compute the sound level and wave a flag.

A PDM Microphone Stunt

Here is a short video showing a small hobby servo scared by a bike horn. Notice the pink flag stuck to the servo jumping when honked at!

What we just did

As a stunt, we implement the signal processing chain to reconstruct PCM samples from a PDM microphone in an ATTiny85. This is an 8-bit CPU in the Atmel AVR family, a close relative of the CPU at the heart of the popular Arduino boards. By “tiny” they mean both size and capacity. It is usually seen in an 8-pin package (both classic DIP and SOIC packages are available) with at most 6 GPIO pins available. It has only 512 byte SRAM and 8 KB FLASH available. Running at 5V, it can be clocked up to 20 MHz.

We will run our ATTiny85 at 5V, with a 16MHz core clock. That will give us enough raw CPU power to handle the PDM conversion and the calculation of something useful related to the audio.

What to compute

Obviously, in a system of this scale we are not planning to record the audio in any form.

The simplest possible measurement we could make would be to track the peak sample value. Peak values are interesting if you are a recording engineer and want to avoid any possibility of clipping the recorded audio, but they are too sensitive to transient events to use for characterizing sound. Besides, the peak is too simple to compute to make an interesting demonstration all by itself.

Instead, we will measure its “loudness” as sound pressure level (SPL).

What to do with the result

We output the measured SPL as a PWM signal which can be easily converted to an analog level. 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.

At the same time, we control a common hobby servo to wave a flag when startled.

In principle, it should be possible to use USB to convey the measurement to a PC, however a combination of the specific allocation of peripheral devices to pins and the resources needed to implement USB entirely in software in this CPU make that impossible. It would be possible, however, to use a second ATTiny85 for the USB interface, with a simple signaling convention between the two devices to convey the loudness. PWM would work for that convention, since we are using the core of its only serial peripheral to receive the microphone’s data.

Is this useful?

Yes and no. As is, it could be used to animate a stuffed animal, as long as a single motion is sufficient, and a suitable mechanism is arranged for the servo to manipulate.

More practically, the analog output ranges from 0V for very quiet to 4V for extremely loud, as a 1 kHz PWM signal updated at about 8 Hz. That is easily sensed by an ADC, or perhaps by directly measuring the pulse width, in another CPU such as a Raspberry Pi or an Arduino. The sound level could then control something, or be combined with other sensors to control something.

With a small amount of pattern recognition, it would be easy to use the sound level output to notice things like phones ringing or alarms ringing, with the larger system tweeting or emailing a status.

Hardware or parts is parts

There are only two significant parts used in this demonstration: the CPU and the microphone. Naturally there is also the usual collection of passive components and hookup wire. Then there are a few other parts like the meter and servo that make the demo more visual.

Trinket for ATTiny85

This demo will take advantage of the Adafruit Trinket board, which provides an ATTiny85 in a solderless breadboard friendly form factor, along with a USB connector (and matching bootloader firmware) to support in-circuit programming. (For tasks where the ATTiny85 is sufficient, a $7 board is really easy to afford, even when the CPU it holds up only costs a buck of two.)

The Trinket can be programmed as if it were an Arduino, from the Arduino IDE. We will not be taking direct advantage of that feature, although the version of the IDE provided by Adafruit is the recommended way to get a complete cross-compiler toolchain running on a PC.

The IDE includes a working installation of GCC for the AVR CPU family, as well as supporting utilities making cross development for AVRs easier. Installer packages for Windows, Linux and Mac are available from Adafruit.

To enable the Arduino IDE, the Trinket is delivered with a boot loader installed that supports reflashing (most of) the program flash over a USB connection. The bootload emulates one of the popular AVR device programmers, and a matching command line utility is included in the IDE.

Knowles Microphone

We purchased a Knowles SPM0437HD4H-B microphone from Digi-Key. Since this is a surface-mount package we made a simple breakout board for it from copper-clad perfboard. Using a suitable Dremel tool, we cut an opening for the sound port, and separated the copper into six pads. The microphone was glued face down, taking care to not cover the port with glue. 30 AWG wire was soldered from the microphone’s pads to the board, and hookup wire soldered to the board allowed connection to the solderless breadboard without too much hassle.

face back

For actual connection to the breadboard, and to isolate the microphone at least a little from mechanically coupled noise, I used about three inches of 24 AWG copper wire. This is long enough to hold the mic in the air, but not so long that we have to worry very much about transmitting the 1 MHz clock and data signals.

A more polished breakout board layout should include a bypass cap near the microphone. My mic seems to be happy enough without it, but it could be easily added between pins 1 and 6 of the mic. Adding such a cap to the handmade breakout would also serve to make identification of pin 1 easier.

Analog Meter Movement

The demo will convert the SPL value to an analog voltage level using a timer peripheral configured to drive a single pin with a pulse-width modulated square wave. If the base PWM frequency is high enough, that waveform will be filtered by an analog meter movement, which will indicate a voltage proportional to the PWM duty cycle.

I found a suitable meter movement at Amazon. All the usual electronics suppliers (Digi-Key, Mouser, etc.) wanted amazingly large amounts of money for this classical device. The cheap import at amazon cost me less than $10. Surplus houses and ebay will likely also yield genuine vintage meters if that better suits your project.

Connection is by screw terminals at the meter’s back, which I completed with more 24 AWG solid wire. I added a 0.1 µF bypass capacitor across the meter terminals as well.

Hobby Servo

I have a few “standard size” hobby servos laying around, but they draw more power than really should be taken off of a USB connection and I didn’t want to fiddle with batteries or additional power supplies. So I went looking for the smallest servos available. I found these tiny servos intended for small and light objects. The demo will drive one of these to wave a flag according to the sound level.

Another reason to use the tiny servo is that it is fairly quiet. The larger servos are often noisy enough for the demo firmware to hear the servo moving, causing some feedback, and continuing to move the servo.

The demo code will drive any standard hobby servo with the usual pulse width protocol, but the USB cable may not supply enough power for larger servos. If adapting the servo output to your own project, be sure to consider the power issues carefully.

Wiring

Power will be supplied via the same USB cable that allows the Triket firmware to be re-flashed.

Using a solderless breadboard, we wire the Trinket and microphone as follows:

  Trinket Pins    Usage
   0 PB0/DI        Mic data input to USI 
   1 PB1/DO/OC0B   PWM Analog SPL Output
   2 PB2/USCLK     Mic clock input to USI
   3 PB3           Sonar, Hobby Servo (USB D-)
   4 PB4/OC1B      Mic clock output   (USB D+)
   5 PB5/RESET     Reset button

For the purposes of the demo, we use a voltage divider to reduce the level of the clock signal from 5V logic to 3V, and a similar divider to provide approximately 3V power to the microphone.

Schematic diagram

The microphone clock is level shifted by a voltage divider made up of 2K and 3K resistors.

To make the breadboard work, we had to drive the mic at a higher than recommended voltage so that its data output would cross the Vih threshold for the ATTiny85’s input pins, which is 70% of Vcc or 3.5V when running at 5V.

I’ve wired the USB Vbus directly to the servo (shown as K1 on the schematic) so that the servo motor is not loading the Trinket’s regulator.

Handling PDM Audio

Master clock

The Timer 1 peripheral divides the CPU’s internal 64 MHz PLL by 64, producing the 1 MHz master clock to the microphone via pin PB4/OC1B.

Unfortunately, it is not possible to internally route the master clock to the USI peripheral that will be receiving the microphone’s data. So the master clock is also connected to the USI‘s clock input on pin PB2/USCLK.

/** Initialize Timer 1
 *
 *  Timer/Counter 1 will generate the 1MHz (or greater) clock for the
 *  MEMS PDM microphone on pin OC1B, which must be wired to the USCLK
 *  pin to actuall receive data.
 *
 *  Indirectly, it also controls the finished sample rate, as that is a
 *  direct decimation from this clock.
 *
 *  And it controls the resolution of the hobby servo output which is
 *  tied to the decimation of the audio samples because it is available
 *  and works.
 */
void init_timer_1(void)
{
    sbi(PLLCSR, PCKE);          // Timer1 uses 64MHz PCK
    TIMSK = 0;              // No interrupts

    // prescale stopped, normal async mode, ignore PWMA
    TCCR1 = (1<<CTC1) | (0<<CS10);
    // Make OC1B toggle at count OCR1C, normal async mode, ignore PWMB
    GTCCR = (1<<COM1B0);
    OCR1C = 7;              // timer1 counts 32 PCKs
    OCR1B = 7;              // PIN 1 toggles in phase with USI click
    TCNT1  = 0x00;          // clear timer
    // prescale to PCK/1, normal async mode, ignore PWMA, counter stopped
    TCCR1 = (1<<CTC1) | (3<<CS10);
    sbi(DDRB,PORTB4);           // PB.4 = output
}

PDM Input

The ATTiny85 has a peripheral named USI which consists of an 8-bit shift register with some supporting logic and controls. Its intended use is as the core of an implementation of the I2C or SPI protocols, but with some care it can be used to capture arbitrary serial bit streams.

One quirk of the USI is that it cannot be usefully clocked by another internal peripheral, so its clock input is obtained by an external wire from the master clock output.

The USI is configured to interrupt once per 8 bits shifted. The interrupt handler reads the shifted data, feeding it into the first CIC stage.

Part of the USI configuration selects which clock edge latches the input data. Since the PDM microphone is designed to be used in a stereo pair clocked by a single clock and sharing a single data wire, this configuration effectively chooses whether the USI is listening to the left or right microphone.

Since there is only one USI peripheral, it isn’t possible to work with both the left and right channels, so we just pick one.

/** Initialize the USI peripheral.
 *
 *  We are using the 8-bit shift register of the USI to receive the PDM
 *  audio samples, clocked from a timer output running at 1 MHz. We
 *  normally only read from the USI, so the shifted data output can be
 *  configured to not drive any pin at all.
 *
 *  Note that since the ATTiny85 does not provide a complete enough pin
 *  mux fabric, we have to depend on an external wire to connect USCLK
 *  to OC1B.
 */
void init_usi(void)
{
  // initialize
    cbi(DDRB,PORTB2);   // PB.2 = input USCLK
    sbi(DDRB,PORTB1);   // PB.1 = output DO
    cbi(DDRB,PORTB0);   // PB.0 = input DI
    cbi(PORTB,PORTB0);  // turn OFF PB.0 pull up

    USISR = _BV(USIOIF); // clear interrupt flag before enabling interrupt
    // USIOIE=1 for Overflow interrupt
    // USIWM=1  for 3-wire SPI mode or 0 for no mode, allowing PB1 to be
    //          used for other things
    // USICS=2  for USCLK rising edge
    // USICLK=0 for USI count external both edges
    USICR = (1<<USIOIE) | (0<<USIWM0) | (2<<USICS0) | (0<<USICLK);
    USIDR = 0x55; // test pattern, but harmless if left in with a mic present
}

CIC Filter Implementation

Our goal is to downsample the 1 MHz input by a factor of 128, producing PCM samples at 7812.5 Hz. If we were receiving input bits individually for processing, we would have to average 16 core clocks per bit, with the hard requirement of reading the current bit before the next bit arrives.

That is a hard requirement to meet, without careful coding in assembly language. The resulting code would be quite fragile and difficult to integrate with the per PCM sample calculations and per RMS window calculations.

So we take advantage of the shift register in the USI to collect 8 PDM bits at a time, and an interrupt routine to collect them for processing. The interrupt will fire at 125 kHz (1 MHz / 8), allowing 128 clocks per interrupt. If we can keep the per-interrupt calculation plus the interrupt overheads small enough, the foreground thread will have time to do all the rest of the arithmetic.

Since a CIC filter is fundamentally a box-car average, we can pre-compute that average for each possible 8-bit pattern and do a single table-lookup as the complete implementation of a CIC that down-samples by 8. That sample stream just needs to be down sampled by a further factor of 16 to reach our target sample rate.

Operating on 8 input bits at a time, we simulate a single complete CIC stage by rescaling the samples to -1 or 1 and simply summing eight samples in the box. This is equivalent to a CIC stage with R=8, N=1, and M=1, which has a bit growth of 3, for a four-bit output.

Since there are only 256 input bit patterns and each produces a four bit result, this scaling and summing can be fully simulated by a single precomputed table, in a table that only requires 256 bytes of memory, and only requires a couple of clock cycles to access.

The equivalent code to count the set bits manually would require at minimum 24 instructions just to implement the loop over the input bits, plus additional instructions to compute a useful result. While the code would be smaller than the table, the loop overhead alone uses up 20% of the available time.

Since there is a great deal of structure in the table itself, a trick with C macros makes it possible to precompute and specify the entire table at compile time by defining and using four macros:

    const int8_t setbits[256] PROGMEM = {
    #   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)
    };

When the macros are expanded, the first and last entries of the table look like:

    const int8_t setbits[256] PROGMEM = {
        -8, -6, -6, -4, -6, -4, -4, -2, -6, -4, -4, -2, -4, -2, -2, 0,
        // ... skipped over 224 entries
        0, 2, 2, 4, 2, 4, 4, 6, 2, 4, 4, 6, 4, 6, 6, 8
    };

The second CIC stage has R=16, N=2, and M=2 for a bit growth of 10, and is implemented by adding the output of the first stage to the first integrator, then cascading to the second integrator. Every 16th interrupt, the second integrator is passed out of interrupt level for handling in the main loop.

/** USI receiver interrupt.
 *
 *  Capture 8 bits of PDM samples, and do the high sample rate work.
 *
 *  This interrupt effectively implements the decimation by 8 initial
 *  CIC filter.
 *
 *  Periodically supply the second CIC filter's integrator to the
 *  foreground thread to implement the decimation for the second CIC
 *  filter. The foreground will do the comb stages to complete the
 *  filter.
 *
 *  Implement the hobby servo control pulse by counting interrupts
 *  until the command pulse with is done, and setting or clearing the
 *  pin as indicated. This does couple the hobby servo control to the
 *  audio sample rate.
 */
ISR(USI_OVF_vect) {
    // Capture the mic data on entry
    uint8_t pdm = USIBR;
    int8_t tmp;

#ifdef SERVOPIN
    // If the pin is set, see if it is time to clear the pin.
    if (pinset) {
    if (!--pinset)  
        cbi(PORTB, SERVOPIN);
    else
        sbi(PORTB, SERVOPIN);

    }
#endif

    // First stage of PDM to PCM is to count the set bits in the
    // captured byte, rescale to a signed +/- 1 range, and add that to
    // the integrator stage 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*log2(R*M)
    // or 3, for 4 total significant bits out from the single bit in.
    tmp = pgm_read_byte(setbits + pdm);

    // Now 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.
    s2_sum1 += tmp;
    s2_sum2 += s2_sum1;
    if (--scount == 0) {
        // toss the R=16 downsampled sum to the foreground
        // to complete the CIC cascade.
        winval = s2_sum2;
        scount = WINBYTES;
        winflag = 1;
    }
}

We switch from interrupt level after each 16th interrupt to implement the decimation by 16, and to move calculation out of the interrupt handler as soon as possible. It would be desirable to include a third cascade in this CIC stage, but experimentation shows that the interrupt routine does not have enough time to execute the required additional arithmetic.

// foreground thread combs go here
    if (winflag) {
        // collect the latest sample
        int16_t v = winval;
        winflag = 0;

        // Finish the second CIC filter by implementing the
        // remaining comb stages, with the finished PCM sample
        // in v. Due to bit depth growth in the filters, v
        // ranges from -8192 to +8192, corresponding to a 
        // positive real sample ranging from -1.0 to +1.0 with
        // 13 bits of precision.
        {
            int tmp1;//, tmp2;
            tmp1 = v - s2_comb1_2;
            s2_comb1_2 = s2_comb1_1; 
            s2_comb1_1 = v;
            v = tmp1 - s2_comb2_2;
            s2_comb2_2 = s2_comb2_1; 
            s2_comb2_1 = tmp1;
        }

        // Add the reconstructed sample to the RMS window.  Each raw
        // PCM sample is the output of a CIC cascade with 13 bits of
        // precision and 
        // ...

The main loop collects the integrator’s total and passes it through the required comb stages to get a finished 14-bit signed integer PCM sample value (actually ranging from -8192 to +8192) at 1 Mhz / 128 or 7812.5 Hz sample rate, useful for further processing.

SPL or how loud is that really?

About the simplest property of an audio signal that we could measure is its volume or loudness. Acoustically, this would be performed by measuring the pressure level, compared to a reference level of a reference tone. Since human hearing responds roughly logarithmically, the result is usually expressed on a logarithmic scale, in the form of decibels, or dB.

Several scales are used in practice, along with several different filters modeling human hearing response to broad-spectrum sounds. The most common scale is SPL (Sound Pressure Level), defined as the ratio between the root mean squared (RMS) pressure expressed in Pascals and the reference of level 20 µPa. This ratio is then expressed in dB:

dB SPL = 20 * log(Prms/P0)

The reference pressure was chosen because it is the threshold of hearing. Studies have shown that under normal conditions, pressure waves below 20 µPa cannot be heard, and waves above that level can be heard.

A sound wave with an RMS pressure of 20 µPa is then 0 dB SPL. Many audio systems are specified and tested with a reference tone: a 1 kHz sine wave with 1 Pa RMS amplitude, which is 94.00 dB SPL. (This pure tone is a remarkably unpleasant sound, even at relatively low SPL.) At the extreme, since the standard atmospheric pressure is 101325 Pa the loudest possible undistorted sound is 194 dB SPL which is a sound you do not want to experience as it would be highly likely to cause death.

Some more familiar sound levels are:

RMS pressure of -20 µPa         0.00 dB
flying mosquito at 3 m          ~0 dB
quiet room ambient level        30 dB
normal conversation at 1 m      40-60 dB
passenger car at 10 m           60-80 dB
busy traffic at 10 m            80-90 dB
long term hearing damage        85 dB
RMS pressure of 1 Pa            94.00 dB
risk of permanent damage        120 dB

Commercial SPL meters usually have two settings, slow and fast. The fast setting produces SPL results filtered with a time constant of 125 ms, and the slow is filtered with a 1 second time constant. As a practical demonstration, we will use 977 sample points (125 ms at the PCM sample rate of 7812.5 Hz) for each RMS value, and do the division by subtracting the logarithm of the window’s length from the logarithm of the sum over the window.

We will also approximate the RMS value with the easier to compute mean of absolute values. That avoids the multiplications to compute squares and the square root at the end, and introduces only a small error in the result, on the order of a few tenths of a dB.

In practice, since we plan to compute a logarithm in any case, the savings of mean absolute value over RMS may not be as large as that as the square root can be computed by simply dividing the log by 2, leaving only the cost of the squares. But in an 8-bit processor, there is also good reason to save the time those squares would cost (both the multiply and the need for a larger accumulator matter) so the demo code uses the mean absolute value of the PCM samples.

Note that our microphone also is documented to have a DC offset baked into the sample values, typically on the order of 6% of full scale. We can remove that with any high-pass filter. A cheap way to achieve that is to simple compute an average sample value of each box to be subtracted from the samples in the subsequent box.

In the code fragment that follows, avg is the signed average sample value of the previous RMS window, sum is the signed sum of raw sample, sabs is the unsigned sum of the absolute value of each raw sample offset by avg, and n is the count of samples in the window.

#define WINDOWSIZE 977         // samples at Fs=7812.5 Hz  
#define LOG2WINDOWSIZE 79      // nominally == 8*log2(WINDOWSIZE)

    // ... in the foreground thread loop ...
    if (winflag) {
        // ... finish the CIC filters to compute a PCM sample 
        // in v ...

        // Add the reconstructed sample to the RMS window.  Each raw
        // PCM sample is the output of a CIC cascade with 13 bits of
        // precision and logically ranges from -1 to +1.

        // Compute an average of this window to use to remove DC
        // offset from the next window.
        sum += v;

        // Take the absolute value for our RMS estimate based on
        // mean absolute value, after removing the DC offset based
        // on the previous window average sample.
        v -= avg;
        if (v < 0) { v = -v; }
        sabs += v;

        // Count samples in this window
        ++n; 
        if (n == WINDOWSIZE) {
            n = 0;
            // Once per RMS window, based on WINDOWSIZE samples at
            // 1 MHz / 128 = 7812.5 Hz sample rate.

            // Compute the mean sample value to offset the next window
            avg = sum / WINDOWSIZE;
            sum = 0;

            // Compute the mean absolute sample value to estimate
            // SPL. We can treat sabs directly as a fixed point
            // average sample value with log2(WINDOWSIZE) bits of
            // fraction, and ranging from 0.000 (0) to 8192.00
            // (8192*WINDOWSIZE).

            spl = lg2(sabs) - LOG2WINDOWSIZE;

            // Now use the SPL to do something.....
        }
    }

Another simplification is to compute the logarithm base 2 of the sum, rather than either the common or natural log. Log base two of a positive integer is easily computed by locating the position of the highest order set bit. Additional resolution can be had (and will be needed) by interpolating between exact powers of 2. The bits just below the highest set bit are convenient for this purpose. Putting this together, the function lg2() takes any positive 32-bit integer and returns an 8-bit fixed-point approximation of the log base 2 of that integer with 3 bits of fraction. As a special case for continuity, an input of 0 returns 0 rather than negative infinity, and values less than 8 are handled by a look up table.

/** 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
 *  significant 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[] PROGMEM = {
        0,0,8,13,16,19,21,22
    };
    if (!(v&~7)) {
        return pgm_read_byte(&log2table[v&7]);
    }
    uint8_t r = 3;     // r will be 8*log2(v)
    while (v & ~0xF) {
        v>>=1;
        ++r;
    }
    return (r<<3) | (v&0x7);
}

To relate the finished SPL value in internal logarithmic units to dB SPL, we have to note that the microphone data sheet claims that a 1 kHz tone at 94 dB SPL will typically register as -26 dB FS, where 0 dB FS is largest amplitude sine wave that can be represented in a PCM sample without clipping. A full scale square wave is then +3 dB FS, and which would measure as 8 * log2(8192) or 104 which can be rescaled to dB by multiplying by 20 * log10(2) / 8 or about 0.75 to get 78. Subtract the 3 dB for a sine wave, and we find that the offset is about -75 dB to dB FS, or +19 to dB SPL. Putting this all together, if we wanted to output true db SPL we would need the following expression in terms of our computed variable spl:

dB SPL = (3 * spl / 4) + 19

For the demo, we take the computed spl and convert it to an analog output and servo position more or less directly.

Analog Output by PWM

An analog level is created from the computed SPL by configuring Timer 0 as a pulse width modulator (PWM), and controlling the duty cycle of the pulse train to be proportional to the measured SPL. The duty cycle is controlled with an 8-bit unsigned register.

/** Initialize Timer 0
 *
 *  Timer 0 is used to generate the PWM signal output on pin OC0B. Once
 *  this has been called, the duty cycle is controlled from 0% to 100%
 *  with values ranging from 0 to 255 writen to register OCR0B.
 *
 *  The output pin should be low-pass filtered to change the PWM to an
 *  analog level.
 */
void init_timer_0(void)
{
    TIMSK = 0;          // No interrupts
    TCCR0A = (0<<COM0A0) | (2<<COM0B0) | (3<<WGM00); //OC0A=none OC0B=CLEAR WGM=FAST PWM
    TCCR0B = (5<<CS00);   //  CS0 = Fclk/1024 or 15625 Hz
    sbi(DDRB,PORTB1);   // PB.1 = output
    OCR0B = 0x7f;       // PIN 1 is a PWM output, initially 50%
    TCNT0  = 0x00;      // clear timer
}

As noted earlier, the computed SPL level in spl is an integer scaled as 8 times log base 2, and ranges in practice from 0 to 104. So simply scaling that by 2 gives values ranging from 0 to 208, or duty cycles ranging from 0% to 208/255 or 81%. This will correspond to voltages ranging from 0V to 4.08V.

The only quirk in the calculation is that we want to make sure that we never load the PWM control register with values outside its working range, so we are careful to clip the calculated result.

        // Make SPLOUT be a pulse with width proportional to log
        // mean abs level.  We scale and offset spl to fill in
        // the range 0 to 255 for controlling the PWM. We are
        // careful here to use saturation arithmetic so that
        // over and underflows do not wrap.
        spl *= 2;
        if (spl < 0)
            spl = 0;
        else if (spl > 255)
            spl = 255;

        OCR0B = spl; // set the PWM width

Hobby Servo Control

The USI receive interrupt is also used to time the pulse width that commands a hobby servo. Hobby servos are controlled by the width of a periodic pulse. This is not a conventional PWM where the variable is encoded by duty cycle.

Rather, each pulse causes the servo position to be updated. If you stop sending pulses, the servo sits still. The position is encoded in the width of the pulse, which can vary from about 1 ms to 2 ms, centered on 1.5 ms.

        // Proper hobby servo control is also based on pulse
        // width, where a neutral position is commanded by a
        // pulse width of 1.5 ms, and an overal motion of 180
        // degrees ranging from 1 ms to 2 ms. The USI interrupts
        // are at 125 kHz, so there are 125 counts per ms. We
        // will scale the level so that it ranges from 125 to
        // 250.
        servo = spl + 125;
        if (servo < 125) servo = 125;
        if (servo > 250) servo = 250;
        pinset = servo;

The variable servo is held from window to window, and could be used to schedule additional pulses at other points during a window simply by repeating the pinset = servo; line at suitable times. It was calculated from the doubled spl produced as a side effect of the PWM output, so the servo is fairly twitchy in a quiet room.

The variable pinset communicates with the USI interrupt routine seen earlier, which will decrement pinset if it is non-zero, and set the output pin as long as pinset remains non-zero.

And what comes next?

A full source kit ready to compile with avr-gcc and flash with avrdude will be made available soon.

A future installment will return to the filter math, and attempt to give a more complete understanding of how and why it works.

The stunt has already been ported to another inexpensive and tiny CPU, and I expect to post about that as well.

The goal with this device was to do something interesting as a result of loud noises in the ambient environment, such as make an art piece respond to sound, or to send you an email when your home's smoke alarm is ringing. Expect there to be posts in that direction eventually.

References and Further Reading

  • Decibel at Wikipedia. The meaning and usage of the not-really-a-unit symbol "dB" is often a source of confusion. This article provides a good overview.
  • Sound Pressure Level at Wikipedia. Measuring sound is an interesting challenge, starting with defining what you are actually measuring. One choice is Sound Pressure Level, but there are others. Start here, but read the other related articles for a broad overview.
  • Sound level meter at Wikipedia. Sound level meters have their own jargon and usage.
  • Sound level meters: 1928 to 2012 by Alan Marsh for publication in the Japanese Research Journal on Aviation Environment, March, 2012.
  • Background on Hobby Servos from Pololu. Start with the earliest post and read everything if you want lots of good background on on using and controlling these inexpensive, common, and powerful devices.
  • Selector guide to servos for sale at Pololu. Marvel at motors ranging in weight from 0.1 oz to more than 2 pounds with torque ranging from 4 oz-inch to 100 pound-inch. Price varies a bit too.

Update

A fair number of words have been added to this thread of articles. You can find a handy collection of all of them at the PDM Microphone category page. We've demonstrated the signal processing chain in breadboards based on the ATTiny85 (in this post), LPC810, LPC812, and in our LPC812 with microphone future product, which we call the SPLear™.


If you have a project involving embedded systems, micro-controllers, electronics design, audio, video, or more we can help. Check out our [main site][cec] and call or [email][cecmail] us with your needs. No project is too small!

+1 626 303-1602
Cheshire Engineering Corp.
120 W Olive Ave
Monrovia, CA 91016

[cec]: http://www.cheshireeng.com/ "Cheshire Engineering Corp."
[cecmail]: mailto:Inquiry@cheshireeng.com?subject=Help%20me%20with%20my%20project "email Cheshire about a project"


(Written with StackEdit.)

Advertisements