From 8cb9cb043d1008ebc1367229b6c9adb4ef9c02c3 Mon Sep 17 00:00:00 2001 From: "claude-fable-5 (Claude Code)" Date: Sat, 11 Jul 2026 20:25:37 -0700 Subject: [PATCH] Add low-noise quiet capture for voltmeter and slowest ADC mode Rescoped from issue #13: instead of free-running conversions, the new ADCInput::readMultiQuiet() takes single conversions with the CPU sleeping in SLEEP_MODE_IDLE during each one, engaging the AVR ADC noise canceler to keep core switching noise out of the sample. IDLE rather than SLEEP_MODE_ADC so clkIO (Timer0/micros, PWM test signals) keeps running. The quiet path activates when the previous frame was flat (voltmeter display, reusing the rolling isFlatLine state) or when the slowest ADC mode is selected. The free-running readMulti() hot path is untouched, and non-AVR platforms fall back to it via the ADCBase default. Also fixes a latent curMode shadow in adc_avr.h (ADCInput redeclared the ADCBase member, so base accessors would have read a stale copy) and a double isFlatLine() call in loop() that skewed the rolling Vpp average. Verified: arduino:avr:uno and adafruit:samd:adafruit_feather_m0 build clean with --warnings all (globals unchanged at 490 bytes); make sim-test passes 8/8 against the golden baseline in the devcontainer. Co-Authored-By: Claude Fable 5 --- tiny_scope/adc.h | 5 +++++ tiny_scope/adc_avr.cpp | 44 +++++++++++++++++++++++++++++++++++++++ tiny_scope/adc_avr.h | 4 +++- tiny_scope/capture.cpp | 4 ++-- tiny_scope/capture.h | 2 +- tiny_scope/tiny_scope.ino | 17 ++++++++++++--- 6 files changed, 69 insertions(+), 7 deletions(-) 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);