diff --git a/tiny_scope/adc.h b/tiny_scope/adc.h index 495b241..ba8f83c 100644 --- a/tiny_scope/adc.h +++ b/tiny_scope/adc.h @@ -61,6 +61,11 @@ class ADCBase { *buffer++ = ADCBase::readFast(); } }; + // current sampling mode (0 - getModeCount()-1) + uint8_t getMode(){ return curMode; } + // low-noise bulk read. AVR overrides this with a CPU-idle version; the + // portable fallback just defers to readMulti() so other platforms are unaffected. + void readMultiQuiet(uint16_t *buffer, unsigned size){ readMulti(buffer, size); } // calibration function, return corrected AREF based on Vbg measured against it. uint16_t calibrateAREF(){ return 0; diff --git a/tiny_scope/adc_avr.cpp b/tiny_scope/adc_avr.cpp index 0eb2607..0c4799a 100644 --- a/tiny_scope/adc_avr.cpp +++ b/tiny_scope/adc_avr.cpp @@ -24,8 +24,16 @@ */ #ifdef __AVR__ #include "adc_avr.h" +#include +#include #if defined(ADCSRA) && defined(ADCL) +// ADC conversion-complete ISR: intentionally empty. It exists only so the CPU +// wakes from sleep when a conversion finishes (readMultiQuiet sets ADIE). +// analogRead() in the Arduino AVR core only polls ADIF and never enables ADIE, +// so there is no conflict with the normal read path. +EMPTY_INTERRUPT(ADC_vect); + // Actual voltage of Vbg (internal 1.1V ref) in mV. This is different from chip to chip. // = VrefMeasured*1100/5.000 (with accurate multimeter). VrefMeasured*1100/3.300 for 3.3V systems. #define INTERNAL_REF_MV 1090 @@ -98,6 +106,42 @@ void ADCInput::readMulti(uint16_t *buffer, unsigned size){ cbi(ADCSRA, ADATE); // Disable free running } +/* + * Low-noise variant of readMulti() (issue #13). + * + * Instead of free-running back-to-back conversions, take one single conversion + * at a time and put the CPU to sleep for the duration of each. With the core + * halted the AVR's ADC noise-canceler suppresses digital switching noise that + * would otherwise couple into the sample - a benefit free-running readMulti() + * cannot get because it never sleeps. Intended for the voltmeter/steady-signal + * display where per-sample sleep latency is irrelevant. + * + * SLEEP_MODE_IDLE (not SLEEP_MODE_ADC) is deliberate: ADC noise-reduction mode + * halts clkIO, which would stop Timer0 (micros/millis) and the PWM test signals. + * IDLE keeps clkIO running, so other interrupts (e.g. Timer0) may wake us early; + * the inner loop re-checks ADSC and sleeps again until the conversion is done. + * This uses the canonical race-free avr-libc sleep pattern (cli/sleep_enable/sei). + */ +void ADCInput::readMultiQuiet(uint16_t *buffer, unsigned size){ + set_sleep_mode(SLEEP_MODE_IDLE); + sbi(ADCSRA, ADIF); // clear stale completion flag + sbi(ADCSRA, ADIE); // ISR wakes the CPU when conversion completes + for (; size; size--){ + sbi(ADCSRA, ADSC); + cli(); + while (bit_is_set(ADCSRA, ADSC)){ + sleep_enable(); + sei(); + sleep_cpu(); // other interrupts (Timer0) may wake us early; loop re-checks + sleep_disable(); + cli(); + } + sei(); + *buffer++ = ADCL | (ADCH << 8); + } + cbi(ADCSRA, ADIE); +} + /* * Read internal reference voltage. */ diff --git a/tiny_scope/adc_avr.h b/tiny_scope/adc_avr.h index e0c98c0..409a6a5 100644 --- a/tiny_scope/adc_avr.h +++ b/tiny_scope/adc_avr.h @@ -28,7 +28,8 @@ class ADCInput : public ADCBase { protected: - uint8_t curMode; + // curMode is inherited from ADCBase; do not redeclare it here or it would + // shadow the base member that getMode() reads. bool setPrescaler(uint8_t mode); public: static const uint8_t prescalers[]; @@ -40,6 +41,7 @@ class ADCInput : public ADCBase { uint32_t getClock(); // Hz for information purposes only uint32_t getSampleRate(); // Hz for information purposes only void readMulti(uint16_t *buffer, unsigned size); // multi-sample read + void readMultiQuiet(uint16_t *buffer, unsigned size); // low-noise multi-sample read inline uint16_t read() __attribute__((always_inline)){ return analogRead(input); diff --git a/tiny_scope/capture.cpp b/tiny_scope/capture.cpp index acc8449..12988b5 100644 --- a/tiny_scope/capture.cpp +++ b/tiny_scope/capture.cpp @@ -28,7 +28,7 @@ int Capture::init(ADCInput adc_in, unsigned samples_in, uint16_t rangemV_in){ * Capture the requested number of samples. * on return, data contains the raw capture data. */ -void Capture::capture(){ +void Capture::capture(bool quiet){ uint32_t start; uint16_t *dataCur; uint16_t v; @@ -57,7 +57,7 @@ void Capture::capture(){ } } start = micros(); - adc.readMulti(dataCur, samples); + quiet ? adc.readMultiQuiet(dataCur, samples) : adc.readMulti(dataCur, samples); elapsedus = micros() - start; } diff --git a/tiny_scope/capture.h b/tiny_scope/capture.h index 88a47db..5e88067 100644 --- a/tiny_scope/capture.h +++ b/tiny_scope/capture.h @@ -21,7 +21,7 @@ class Capture { int init(ADCInput adc, unsigned samples, uint16_t rangemV); void calibrate(); - void capture(); + void capture(bool quiet=false); void tomV(); }; diff --git a/tiny_scope/tiny_scope.ino b/tiny_scope/tiny_scope.ino index 5169a47..f76bdb8 100644 --- a/tiny_scope/tiny_scope.ino +++ b/tiny_scope/tiny_scope.ino @@ -179,6 +179,9 @@ void setADCMode(){ void loop(){ static Scope scope = Scope(display, 0, SCREEN_WIDTH, 0, 47); + // Use low-noise quiet capture once the signal is steady (voltmeter) or at + // the slowest ADC mode, where the per-sample sleep latency doesn't matter. + static bool quietCapture = false; if (!capture.samples){ // leave "Out of memory" printed by setup() onscreen. @@ -186,12 +189,16 @@ void loop(){ return; } - capture.capture(); + capture.capture(quietCapture); capture.tomV(); + // isFlatLine() mutates a rolling-average state, so call it exactly once per + // loop and reuse the result (this also fixes a preexisting double-sample quirk). + bool flatLine = scope.isFlatLine(capture); + display.clearDisplay(); // Enable voltmeter mode if line is flat - if (scope.isFlatLine(capture)){ + if (flatLine){ scope.displayVoltMeter(capture); } else { scope.displayScope(capture); @@ -204,11 +211,15 @@ void loop(){ Serial.print(F("sim: capture samples=")); Serial.println(capture.samples); Serial.print(F("sim: flatline ")); - Serial.println(scope.isFlatLine(capture) ? F("YES") : F("NO")); + Serial.println(flatLine ? F("YES") : F("NO")); Serial.println(F("sim: TESTS COMPLETE")); } #endif + // mode 0 is the slowest prescaler on AVR; on portable builds getMode() is + // always 0 and readMultiQuiet falls back to readMulti, so behavior is unchanged. + quietCapture = flatLine || capture.adc.getMode() == 0; + setADCMode(); // displaying at max 20fps delay(50);