Week 10 ADC and the Microphone



To avoid aliasing, the sampling frequency of an Analog-to-Digital Converter (ADC) must be at least twice the highest frequency component of the analog signal you are trying to digitize. This is known as the Nyquist theorem, and the minimum sampling rate is the Nyquist rate. In practice, it is recommended to use a sampling rate that is significantly higher than the minimum and to use a low-pass filter (anti-aliasing filter) before the ADC to remove frequencies above half the sampling rate.

If you sample a voltage through an ISR, the frequency the code gets into the ISR will be the sampling frequency.
The best practice is to use the ADC Conversion Complete ISR. When the ADC conversion is completed, it interrupts and you can process and store the data inside the ISR. This keeps the code organized in a better way.

  // Configure ADC: A0, AVCC reference, left-justified result
  ADMUX = (1 << REFS0)| (1<<ADLAR);



When Mode REFS1:REFS0=0:1, AREF must not be connected to a voltage and AVCC voltage will be used as the reference. Ideally, connect a capacitor at AREF will be helpful but the current Skyboard doesn't have a cap on it, which is fine.





When an ADC conversion is complete, the result is found in these two registers. When ADCL is read, the ADC Data Register is not updated until ADCH is read. Consequently, if the result is left adjusted and no more than 8-bit precision is required, it is sufficient to read ADCH. Otherwise, ADCL must be read first, then ADCH. The ADLAR bit and the MUXn bits in ADMUX affect the way the result is read from the registers. If ADLAR is set (ADLAR=1), the result is left adjusted. If ADLAR is cleared (ADLAR=0 which is the default value), the result is right adjusted.



When there is no input, the voltage divider provides a 2.5 V bias to the Op Amp and the output should be 50% of the entire supply's voltage, which is 2.5 V or 128 out of 256.
You can just read ADCH and don't do anything to ADCL if you only need an 8-bit precision.
If you need higher precision, you need to read ADCL first then read ADCH. However, the question is if you only read ADCH without shifting it right by 2 bits, isn't it amplifying the result by 4 times? The answer is no, when you only read ADCH, it recognizes that you are using an 8-bit precision so a 1000 0000 will be 128 or half of 256 or 2.5 V. If you are using a 10-bit precision, a 10 0000 0000 is 512 or half of 1024, which is also 2.5 V but better precision.



  // Configure ADC: Enable, Prescaler 16
  // ADC clock = 16MHz / 16 = 1 MHz.
  ADCSRA = (1 << ADEN) | (1 << ADPS2);

Human whistling is between 2 - 4 kHz
A 25 us ISR for the ADC is giving you a 40 kHz sampling rate which should be sufficient.



16M/(40kx13) = 30.78. The prescaler should be 30 or less. The follow examples show you the results from prescalers to be 16, 8, 4, and 2 respectfully.



However, smaller prescalers will give you less accurate results, described as follows:



For the same whistling, if you choose anything equal to or slower than 500 kHz (prescaler equal to or larger than 32), you are getting results as follows (with the same whistling):





  // Set to CTC mode (WGM12), no prescaler (CS10)
  TCCR1B |= (1 << WGM12) | (1 << CS10);



This time we are using TIMER1's CTC mode, which counts to the value stored in OCR1A then interrupts.



  // Set OCR1A for 25µs (16MHz * 25µs - 1 = 400 - 1 = 399)
  OCR1A = 399;

Use a test pin to measure the ADC's sampling rate. My oscilloscope shows 26 us whcih is very close to 25 us. I pressed the Run/Stop button on the scope to capture the pulses. This could be a little tricky since it happens very fast. I used the Scale knob to zoom in to the pulses to measure the distance between two rising edges.



  // Enable Timer1 compare interrupt A
  TIMSK1 |= (1 << OCIE1A);



Here is the setup() function.

void setup() {
  pinMode(TEST_PIN, OUTPUT);
  Serial.begin(9600);
  // Configure ADC: A0, AVCC reference, left-justified result
  ADMUX = (1 << REFS0)| (1<<ADLAR);
  // Configure ADC: Enable, Prescaler=16M/(sampling rate x 13), 10-bit conversion takes about 13 cycles
  // Prescaler 10 or less should be fine
  ADCSRA = (1 << ADEN) | (1 << ADPS2);
  // Enable ADC Interrupt
  ADCSRA |= (1 << ADIE);
  // Configure Timer1 for CTC mode and interrupt
  cli(); // Disable interrupts
  TCCR1A = 0;
  TCCR1B = 0;
  // Set to CTC mode (WGM12), no prescaler (CS10)
  TCCR1B |= (1 << WGM12) | (1 << CS10);
  // Set OCR1A for 25µs (16MHz * 25µs - 1 = 400 - 1 = 399)
  OCR1A = 399;
  // Enable Timer1 compare interrupt A
  TIMSK1 |= (1 << OCIE1A);
  sei(); // Re-enable interrupts
}


// Timer1 Compare Match A Interrupt Service Routine
ISR(TIMER1_COMPA_vect) {
  digitalWrite(TEST_PIN, HIGH);
  // Start the ADC conversion
  ADCSRA |= (1 << ADSC);
  digitalWrite(TEST_PIN, LOW);
}



// ADC Conversion Complete Interrupt Service Routine
ISR(ADC_vect) {
  // Read the 8-bit result (we only need the high byte)
  // Read ADCL then ADCH to ensure consistency
  uint8_t low_byte = ADCL; // The Arduino ADC requires ADCL to be read first
  uint8_t high_byte = ADCH;  
  if (sample_index < NUM_SAMPLES) {
    adc_reading[sample_index] = high_byte; // Use high byte for 8-bit result
    sample_index++;
  } else {
    // We have collected all samples, so stop the timer and set flag
    TCCR1B &= 0; // Stop timer
    sampling_complete = true;
    sample_index = 0;
  }
}



Task 1 grading rubric:
1. TIMER 1 CTC mode ISR handles ADC conversion. (20 points)
2. Oscilloscope sampling rate verification - around 25 us. (20 points)
3. 64 whistling samples ploted - close to a sine wave. (20 points)




Task 2: Frequency Measurement

Log 256 samples when the user presses 'f' to fill the buffer. You must whistle very close the microphone, and whistle it as loud as you can. The successful pattern looks as follows, although these are just numbers but you can still see an oscillating/periodic pattern in it. You should have 8 - 12 periods for these 256 points and the frequency of human whistle is 2 - 4 kHz, but I constantly whistled at a lower pitch but it was still close to 2 kHz.



I copied and pasted the whole data block from Putty and pasted them into a spreadsheet. You can use the Data - Text to Columns option to split them into different columns and plot them. In the following figure, I manually did it for the first fours lines and plotted them. It showed about 3 periods so you can predict that for the entire 16 rows, it'll be around 12 periods for the 256 points. If you did it right, you should have about 10 - 12 periods.

The dashed line in the ploted figure shows the uppper threshold and lower threshold, which were defined in the code. Again, the bias/DC offset should be at 128 or 50% of the range, but it was at around 160 for me so I just used 160 +- an error for the upperthreshold and lowerthreshold valudes.



Here is how I define/declare the constants:

#define TEST_PIN 14
#define SAMPLE_PERIOD 25                          // in terms of microseconds
#define SAMPLE_FREQUENCY 1000000/SAMPLE_PERIOD       // 1/SAMPLE_PERIOD
#define ISR_COUNTS 16*SAMPLE_PERIOD  // 25us * 16M/s * 10^-6
#define NUM_SAMPLES 256

I found that when it is quiet, the output is around 160. I use +-10 LSBs as the error range to create the upper and lower thresholds.


volatile uint8_t upperThreshold = 160 + 10;
volatile uint8_t lowerThreshold = 160 - 10;

The setup() function is the same as the one for Task 1.

To print 16 data points per line, when the index of the sample being ANDed with 0x0F is 0, it means that there were just 16 points printed so you can start a new line, print a line tag there, such as s[0], s[16], ... ..., s[240].

 for (i = 0; i < NUM_SAMPLES; i++) { // print-out samples
                        if ((i & 0x0F) == 0){                        
                          sprintf(buff,"\r\nS[%2d]", i);
                          Serial.print(buff);
                        }
                        sprintf(buff, "%3d ", adc_reading[i]);
                        Serial.print(buff);
                    }

In your loop() function, when the current data is lower than 160 and the next is higher than 160 that means there is one zero cross event. This current index should be logged into array cross[i]



As long as your signal crosses 0 (160) here for at least twice, we'll have at least one period of the signal logged.

                    if (crossCount>=2){
                      for (i = 0; i < crossCount - 1; i++) {
                           // Your Code //;


                      }
                      // Calculate the period and frequency based on the average period
                      period = // Your code//; // every two points is 25 us, times how many points is the total time in a period
                      frequency =  // Your code //;                ; 
                      sprintf(buff, "average period = %u us", period);
                      Serial.println(buff);
                      sprintf(buff, "average frequency = %d Hz", frequency);
                      Serial.println(buff);
                    }

We'll need one ISR to start the conversion and another ISR (ADC conversion complete ISR) to log the data

// Timer1 Compare Match A Interrupt Service Routine
ISR(TIMER1_COMPA_vect) {
  digitalWrite(TEST_PIN, HIGH);
  // Start the ADC conversion
// Your Code //;
  digitalWrite(TEST_PIN, LOW);
}

typedef enum {
    MIC_IDLE, MIC_WAIT_FOR_TRIGGER, MIC_ACQUIRE
} myADCstates_t;

// ADC Conversion Complete Interrupt Service Routine
ISR(ADC_vect) {
//  uint8_t low_byte = ADCL; // The Arduino ADC requires ADCL to be read first if you need a 10-bit precision
  uint8_t high_byte = ADCH;
  static myADCstates_t myADCstate = MIC_IDLE;
  static uint16_t index = 0;
  switch (myADCstate) { // and do what it tells you to do
        case MIC_IDLE:
            if (fillBuffer == true) {
               // Your Code //;
            }
            break;
        case MIC_WAIT_FOR_TRIGGER:              

                // Your Code //;


                }          
            break;
        case MIC_ACQUIRE:
                // Your Code //;




            }
            break;
        default:
            myADCstate = MIC_IDLE;
            break;
    }
}



Task 2 grading rubric:
1. Period and frequency calculation (20 points)
2. TIMER1 ISR (5 points)
3. ADC conversion completion ISR (60 points)