Fast Signal Sampling (Part 3)
Part 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 11, 12
Bugs and improvments! While testing some final applications, I captured one bug (improper calculation) and managed to improve the clock stability.
Here is the revised function for parameter settings
void PlainADC::setAcquisitionParameters(uint8_t channel, uint16_t samplesBufferSize, uint16_t frequency) { // Set acquisition parameters if (acqEngineStatus != ADC_ACQ_ENG_STOPPED) stopAcquisitionEngine(); _samplesBufferSize = samplesBufferSize; _frequency = frequency; uint16_t timeInterval = (1000000UL / frequency); // In us from 50000 (20 hz) to 16 (64 kHz) cli(); // set ADC // Clear control registers ADMUX = 0x00; ADCSRA = 0x00; ADCSRB = 0x00; // 8-Bit ADC in ADCH Register (left aligned data) ADMUX |= (1 << ADLAR); // VCC as a Reference ADMUX |= (1 << REFS0); // Set channel ADMUX |= channel; // Compute adc prescaler value automatically, allowed value are 2^1 to 2^7 _adcPrescaler = 128; // Decrease prescaler down to 16; for some reasons, decreasing below 16 drives to unaccurate timing while ((_adcPrescaler > uint16_t((timeInterval << 4) / 13)) && (_adcPrescaler > 16)) _adcPrescaler = (_adcPrescaler >> 1); // Set prescaler uint8_t adcPrescalerExponent = exponent(2, _adcPrescaler); ADCSRA |= (adcPrescalerExponent >> 1); // Enable ADC ADCSRA |= (1 << ADEN); // ADC Auto Trigger Enable ADCSRA |= (1 << ADATE); // ADC interrupt enable ADCSRA |= (1 << ADIE); // ADC Auto Trigger Source Selection ADCSRB |= ((1 << ADTS0) | (1 << ADTS2)); // Set Timer1 // Reset Timer/Counter1 control registers and Interrupt Mask Register TCCR1A = 0; TCCR1B = 0; TIMSK1 = 0; // Set Clear Timer on Compare Match (CTC) Mode TCCR1B |= (1 << WGM12); // Compute timer prescaler, allowed values are 1, 8 or 64 if (timeInterval < 4096) _timPrescaler = 1; else if (timeInterval < 32768) _timPrescaler = 8; else _timPrescaler = 64; uint8_t timPrescalerExponent = exponent(8, _timPrescaler); // Set prescaler TCCR1B |= (timPrescalerExponent + 0x01); // Set upper count value: (F_CPU*Interval)/Prescaler _timUpperCount = (16000000UL / (frequency * _timPrescaler)); OCR1A = _timUpperCount; // Reset Timer/Counter2 TIMSK2 = 0; // sei(); startAcquisitionEngine(); }
Deactivating unused timers/counters leads to better clock stability because the CPU no longer cares about handling their respective interrupts. In this function, assuming that the final application does not use timer/counter2, I decided to disable it.
This is an other story for timer/counter0 which is used for delays and serial communication. So that we have to manage it slightly differently within the acquisition engine:
void PlainADC::acquireData(uint8_t *vData) { if (acqEngineStatus == ADC_ACQ_ENG_STOPPED) startAcquisitionEngine(); uint8_t originalTIMSK0 = TIMSK0; // Record timer/counter0 mask TIMSK0 = 0x00; // Disable timer/counter0 // Reset number of acquired samples uint16_t samples = 0; dataAcqStatus = ADC_DAT_ACQ_WAITING; // Set acquisition status do { if (dataAcqStatus == ADC_DAT_ACQ_TRIGGERED) { PORTB ^= (1 << PINB5); // Comment in normal operation mode vData[samples] = adcValue; // Store data samples++; // Increment sample counts dataAcqStatus = ADC_DAT_ACQ_WAITING; // Toggle status } } while (samples != _samplesBufferSize); dataAcqStatus != ADC_DAT_ACQ_IDLE; // Reset status TIMSK0 = originalTIMSK0; // Restore timer/counter0 operation }
In this case, the content of the timer/counter0 interrupt mask register is recorded, nulled and restored on completion of data acquisition.
For testing purpose, I made prescaler variables global and accessible at run time through appropriate functions, giving, in the header file:
uint8_t adcPrescaler(void); // Get value void adcPrescaler(uint8_t value); // Set value uint8_t timPrescaler(void); // Get value void timPrescaler(uint8_t value); // Set value uint16_t timUpperCount(void); // Get value void timUpperCount(uint16_t value); // Set value
and in the code file
uint8_t PlainADC::adcPrescaler(void){ // Get _adcPrescaler value return(_adcPrescaler); } void PlainADC::adcPrescaler(uint8_t value){ // Set _adcPrescaler value _adcPrescaler= value; } uint8_t PlainADC::timPrescaler(void){ // Get _timPrescaler value return(_timPrescaler); } void PlainADC::timPrescaler(uint8_t value){ // Set _timPrescaler value _timPrescaler = value; } uint16_t PlainADC::timUpperCount(void){ // Get _timPrescaler value return(_timUpperCount); } void PlainADC::timUpperCount(uint16_t value){ // Set _timPrescaler value _timUpperCount = value; }
Simple but efficient!