/** \file MicDemo.c \brief Demonstrate use of a PDM MEMS microphone with an ATTiny. This is a demonstration of sampling audio data from a digital MEMS microphone, delivered in PDM (Pulse Density Modulation) format at a 1 MHz sample rate. This code is built entirely independently of the Arduino framework, although it does run in an ATTiny85 on an Adafruit Trinket board which was built to be used as an inexpensive and small Arduino. The demonstration uses a Knowles SPM0437HD4H-B microphone and a 5V Adafruit Trinket board, along with some passive components. Visible outputs are provided by the LED already present on the Trinket, a 5V scale analog meter movement, and a standard sized hobby servo. Power is supplied by the USB cable that also supports flashing the firmware. 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 The microphone is a 3V nominal part. We could wire it directly to a 3V ATTiny85, there is a 3V model of the Trinket that suits, but when running at 3V the ATTiny85 is limitted to too low a core clock to keep up with the computation required per 8 audio bits. For a breadboad demonstration, resistor dividers are used to power the mic and adjust the level of the clock drive. In the finished PCB, those must be replaced by an actual 3.3V supply and level translators. 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. The microphone's clock is generated by Timer 1 based on the internal 64 MHz PLL. The clock output on PB4 is wired to the USI clock input on PB2, as well as to the microphone. The microphone's data is gathered on PB0 which is the DI pin to the USI peripheral. The USI provides an 8-bit shift register and can capture input data on either clock edge, and provide an interrupt once per 8 bits received. The interrupt will do all processing that must be done at the source sample rate, and hand off to foreground code to finish the processing and do something useful with the recovered audio. Through a pair of CIC filter structures, each 128 one-bit PDM samples will be downconverted to a single signed 15-bit PCM sample. Starting from the 1 MHz source clock, the PCM sample rate is then 7812.5 Hz. The PCM samples will be fed into an RMS calculation, which is a fair proxy for SPL at the microphone. We know the mic itself is specified to convert a 94 dB 1Hz tone into -26 dBFS in its PDM bit stream. By recovering a 15 bit PCM sample, we can in principle measure a 90 dB range, topping out at 0 dBFS or 120 dB SPL. */ #include #include #include #include /* Define the pin used for the PWM output proportional to measured SPL. */ #define SPLOUT PORTB1 /* Define the pin used for the Hobby Servo drive signal. Undefine to * not generate a hobby servo output at all. */ #define SERVOPIN PORTB3 /* Define the pin used for debug sonar. Note that there aren't any * spare pins once both the SPL and SERVO outputs are defined. It would * be bad if this collided with either of those, so don't do that. */ //#define SONARPIN PORTB3 /* * Macros lifted from wiring internals to make direct manipulation of * io register bits easier. While not obviously implemented in terms of * the AVR instructions SBI and CBI, it would be a reasonable optimization * for avr-gcc to do so, and inspection of the generated assembly shows * that it often does do so. */ #ifndef cbi #define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit)) #endif #ifndef sbi #define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit)) #endif /* * Macros to make a pin wiggle as a debug aid. To use these, place the * SONARSET() and SONARCLEAR() around areas of interest. Be careful * that each set is balanced by a clear, or you won't get a pulse that * can be observed. */ #ifdef SONARPIN #define SONARSET() do{sbi(PORTB,SONARPIN);}while(0) #define SONARCLR() do{cbi(PORTB,SONARPIN);}while(0) #else #define SONARSET() do{}while(0) #define SONARCLR() do{}while(0) #endif /** Global Variables */ // Set by the foreground to schedule the ISR to clear the pin volatile uint8_t pinset = 0; // Number of 8 bit samples captured by the USI shift register // to accumulate into a single audio PCM sample. #define WINBYTES 16 // Set by the ISR when a total is ready volatile uint8_t winflag = 0; // Total count of bits over the most recent WINBYTES interrupts volatile int16_t winval = 0; // working variables for the ISR, private to each sampling window uint8_t scount = WINBYTES+1; int16_t s1_sum = 0; int16_t s2_sum1 = 0; int16_t s2_comb1_1 = 0; int16_t s2_comb1_2 = 0; int16_t s2_sum2 = 0; int16_t s2_comb2_1 = 0; int16_t s2_comb2_2 = 0; #if defined(SPLOUT) && (SPLOUT == 1) /** 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) { #if defined(__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__) TIMSK = 0; // No interrupts TCCR0A = (0<>=1; ++r; } return (r<<3) | (v&0x7); } /** 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; SONARSET(); #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; } SONARCLR(); } /** Initialize peripherals. * * Called from main() after the core clock has been configured. Does * all the one-time initialization of peripherals. */ void setup() { #if defined(SPLOUT) && (SPLOUT == 1) init_timer_0(); sbi(DDRB,PORTB1); // PB.1 = output PWM #endif init_timer_1(); init_usi(); #ifdef SERVOPIN sbi(DDRB,SERVOPIN); // PB.3 = output SERVO Control #endif #ifdef SONARPIN sbi(DDRB,SONARPIN); // PB.3 = output debug sonar #endif sei(); } /** Foreground thread loop. * * If this function returns, it will be immediately called again from * an infinite loop in main(). This structure is borrowed from Wiring * as implemented on the Arduino platform. * * This is the forground thread of our SPL meter demonstration. We * loop until the half of the CIC filter implemented at interrupt * level has signaled that the decimator is ready to deliver a sample. * * When a sample is available, we finish the CIC filter by * implementing the combs, then feed the finished PCM sample into the * running windowed SPL calculation, here based on a mean absolute * value of the samples. * * We also maintain a windowed mean sample value to use to remove any * DC offset that might be present. Since the mic's data sheet * documents a typical offset of about 6% of full scale, removing this * offset is actually important in order to support sufficient * sensitivity to quieter rooms. */ void loop() { #define WINDOWSIZE 977 // samples at Fs=7812.5 Hz #define LOG2WINDOWSIZE 79 // nominally == 8*log2(WINDOWSIZE) uint16_t n = 0; int32_t sabs = 0; int16_t avg = 0; int32_t sum = 0; int16_t spl = 0; #ifdef SERVOPIN uint16_t servo; #ifdef SWEEPTEST uint8_t dir = 0; #endif #endif while(1) { if (winflag) { // collect the latest sample int16_t v = winval; winflag = 0; // Finish the second CIC filter by implementing the // remaining comb stages { 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 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's 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. 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). // // To convert that to dB FS, we need to compute // // 20. * log10(sabs/(8192*WINDOWSIZE)) + 3.010 // // ideally without actually computing a log or working // anywhere near floating point. // // Alternatively, we could simply work with the linear // scale, and use appropriate labelling on a meter face, // or similar downstream processing to convert to dB. // // It turns out that computing a fixed point log base 2 // is fast and easy, since it can be approximated by a // piece-wise linear function that can be computed by // searching for the highest order 1 bit in the // integer. The lg2 function does that, producing an // 8-bit result which can hold 8*log2 of any 32-bit // integer. Converting that result to dB is then just // simple arithmetic to scale and offset as needed. spl = lg2(sabs); #ifdef SPLOUT // 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 = (spl - LOG2WINDOWSIZE) * 2; if (spl < 0) spl = 0; else if (spl > 255) spl = 255; OCR0B = spl; // set the PWM width #endif #ifdef SERVOPIN // 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; #ifdef SWEEPTEST if (dir) { if (++servo > 250) dir = !dir; } else { if (--servo < 125) dir = !dir; } #endif if (servo < 125) servo = 125; if (servo > 250) servo = 250; pinset = servo; #endif // reset sum for next RMS window sabs = 0; #if defined(SERVOPIN) && defined(MULTIPULSE) } else if ((n==WINDOWSIZE/4)||(n==WINDOWSIZE/2)||(n==3*WINDOWSIZE/4)) { // make additional servo pulses identical to the last commanded value pinset = servo; #endif } } } } /** Entry point. * * The reset vector sets up the stack, initializes the data and bss * segments, then calls here. If we return, the processor will hang, so * we don't return. The structure of this is lifted shamelessly from * the Arduino platform, but without all of the baggage that Wiring * brings with it since we don't have room in an ATTiny85 for very much * baggage. * * We also support a compile-time directive to set the core clock to * 16Mhz. Note that this is only documented to work for devices running * with VCC at 5V. This faster core clock is required in order to have * enough instructions per USI interrupt in order to complete the audio * computation in time. */ int main(void) { #if defined(F_CPU) && (F_CPU==16000000L) // If requested at compile time, make the CPU core clock be 16 MHz // instead of the 8 MHz our fuses are likely set for. clock_prescale_set(clock_div_1); #endif // Do one-time initialization of our peripherals, as in Wiring. setup(); // Call the actual worker function in a loop, as in Wiring. for (;;) { loop(); } /*NOTREACHED*/ return 0; }