/*******************************************************
RMS Voltage calculation using LOA (Lots of Arithmetic)
Detection of zero crossing and accumulation of RMS data
is all done with software using a single ADC channel collecting
at 50KHz.
Input circuit: 80K80K voltage divider and capacitivelycoupled input
80K Ohm Resistor3.3V
0.5 uF 
Signal Generator >Analog input A9

80K Ohm ResistorGND
Signal Generator >
MJB 03/27/2024
***************************************************************/
#include <ADC.h>
// instantiate a new ADC object
ADC *adc = new ADC(); // adc object;
const char compileTime [] = "RMS Voltage with LOA algorithm Compiled " __DATE__ " " __TIME__;
const int adcpin = A9;
const int ADCMARKpin = 2;
const int ledpin = 13;
// ADCMARKHI and ADCMARKLO are used to observe ADC IRQ timing on oscilloscope
#define ADCMARKHI digitalWriteFast(ADCMARKpin, HIGH);
#define ADCMARKLO digitalWriteFast(ADCMARKpin, LOW);
#define LEDON digitalWriteFast(ledpin, HIGH); // Also marks IRQ handler timing
#define LEDOFF digitalWriteFast(ledpin, LOW);
#define LEDTOGGLE digitalToggleFast(ledpin);
// buffers for histogram data
#define SAMPRATE 50000 // Sampling at 50KHZ
// Variables used by ADC interrupt handler to communicate
// with the foreground loop()
volatile uint32_t filtered_mean; // filtered mean in ADC counts
volatile uint32_t freq_samples; // number of ADC samples in full cycle
volatile uint32_t num_samples; // number of samples in RMS data
volatile uint32_t sum_squares; // sum of squared voltage counts
volatile bool data_ready; // set after hightolow zero crossing
void setup() {
Serial.begin(9600);
delay(500);
LEDON
Serial.println(compileTime);
pinMode(ADCMARKpin, OUTPUT);
pinMode(ledpin, OUTPUT);
pinMode(adcpin, INPUT_DISABLE);
InitADC();
}
void loop() { // just one option: show the collected data
char ch;
if (Serial.available()) {
ch = Serial.read();
if (ch == 'd') {
Serial.println("\nCollection Results");
ShowData();
}
}
}
void ShowData(void){
float period, frequency;
float rmscounts, rmsvolts;
while(data_ready){} // wait while data_ready
while(!data_ready){} // wait until next data_ready
Serial.printf("Filtered Mean: %lu\n", filtered_mean);
period = (float)freq_samples/SAMPRATE;
if(period >0) frequency = 1.0/period; else frequency = 1.0;
Serial.printf("Period: %8.2fmSec Frequency: %8.3fHz\n", 1000.0 *period,frequency);
Serial.printf("num_samples: %lu sum_squares: %lu\n",num_samples, sum_squares);
rmscounts = sqrt((float)sum_squares/num_samples);
rmsvolts = 3.32 * rmscounts/4096.0;
Serial.printf("RMS counts: %8.1f RMS Volts: %8.3f\n", rmscounts, rmsvolts);
}
/******************************************************
Initialize the ADC to automatically collect data
using a dedicated hardware timer.
*****************************************************/
void InitADC(void) {
Serial.println("Initializing ADC");
adc>adc0>setAveraging(1 ); // set number of averages
adc>adc0>setResolution(12); // set bits of resolution
adc>adc0>setConversionSpeed(ADC_CONVERSION_SPEED::MED_SPEED); // change the conversion speed
adc>adc0>setSamplingSpeed(ADC_SAMPLING_SPEED::MED_SPEED); // change the sampling speed
adc>adc0>stopTimer();
adc>adc0>startSingleRead(adcpin); // call this to setup everything before the Timer starts, differential is also possible
delay(1);
adc>adc0>readSingle();
// now start the ADC collection timer
adc>adc0>startTimer(SAMPRATE); //frequency in Hz
filtered_mean = 2048; // starting guess for filtered mean
adc>adc0>enableInterrupts(adc0_isr);
}
/***************************************************************************
This is the ADC interrupt handler. The ADC Timer hardware starts collecting
an ADC sample at the rate specified with the ADCTimer frequency (50KHz).
This handler is called at the end of the ADC collection. It has to do the
follwing arithmetic while collecting ADC Values continuously:
1. Note the sample at which the ADC value exceeds the filtered_mean of a complete
input cycle. Mean values are calculated as the signal mean between
two successive downward zero crossings.
NOTE: Positive zerocrossing is the point where the input exceeds the mean.
2. After the positive zero crossing, reset the sample counter,
NSamples. Also reset the sum of squared values, SUMSQUARES. Subtract the
filtered_mean value from the sample and square the difference.
Add this variable to the sum of squared values,SumSquares.
3. When in the top half of the input signal (above the mean), subtract filtered_mean
from the input value and square the result. Add that squared value to SumSquares
increment the sample counter, NSamples (includes the values just after zero crossing).
4. Continue step 3 until the input voltage descends below the mean. At this time
you have collected NSamples and the sum of the squared sample values.
5 Calculate the new_mean value from mean_samples and mean_sum.
Save the value of mean_samples in a separate variable, freq_samples.
Initialize the counter, mean_Count, to 1 and a sum, mean_Sum, to the ADC
value
6. Apply a simple low pass filter to the generated new_mean values to reduce
noise on the mean value. Save the filtered value in filtered_mean.
This updated value is not used until the next positive zero crossing.
7. Set a boolean flag, DataReady, to indicate to the foreground loop() that new
values of NSamples and SumSquares are available. The foreground loop will
use these values to calculate the RMS value for the top half of the input
cycle. The foreground can also use the values of filtered_mean and freq_samples
to display the mean value and the signal period (or frequency: 1/period).
NOTE: variables local to ISR are in form: VariableName (called CamelCase).
variables communicating outside ISR are in form: variable_name.
When collecting data, the ISR runs in about 100nanoSeconds (66 clock cycles).
The T4X can do a LOT of Arithmetic very quickly!
******************************************************************************/
void adc0_isr() {
uint32_t AdcVal, PlusValue;
static bool InUpperHalf = false; // used in detection of zerocrossing
static bool LastwasUpper = false; // used in detection of zerocrossing
static bool RisingEdge = false;
static bool FallingEdge = false;
static uint32_t NSamples = 1;
static uint32_t Hysteresis = 15; // static variable retain their values from
static uint32_t SumSquares = 0; // one interrupt to the next.
static uint32_t MeanSamples = 0;
static uint32_t MeanSum = 0;
static uint32_t FilteredSum = 0;
static uint32_t NewMean = 2048;
// Collect ADC value and do all the arithmetic
AdcVal = adc>adc0>readSingle();
MeanSamples++;
MeanSum += AdcVal;
RisingEdge = ((AdcVal > (NewMean + Hysteresis)) && !LastwasUpper);
FallingEdge = ((AdcVal < (NewMean  Hysteresis )) && LastwasUpper);
if(RisingEdge){
InUpperHalf = true;
ADCMARKHI // for oscilloscope measurement of time in handler
LastwasUpper = true;
NSamples = 0;
SumSquares = 0;
data_ready = false; // let the foreground know new data is being collected
}
if(InUpperHalf){// Now do the arithmetic for samples after the transition to upper half
NSamples++;
PlusValue = AdcVal  filtered_mean;
SumSquares += PlusValue * PlusValue;
}
if(FallingEdge){ // reached transition to lower half
LastwasUpper = false;
ADCMARKLO
InUpperHalf = false;
if(MeanSamples > 2){
NewMean = MeanSum / MeanSamples;
freq_samples = MeanSamples; // Set value for foreground use
}
MeanSum = 0; MeanSamples = 1; // set up for next sample
// Now do a simple filter that 'averages' means over 16 cycles
FilteredSum = (filtered_mean * 15) + NewMean;
filtered_mean = FilteredSum/16;
num_samples = NSamples; NSamples = 0; // Set external variable and reset NSamples
sum_squares = SumSquares; SumSquares = 0; // Set external variable and reset SumSquares
data_ready = true; // let the foreground know new data is available in num_samples and sum_squares
}
// after the transition to lower half, we don't do anything except collect data to
// update the mean. That is done at the very beginning.
// The following instruction makes sure that everything is
// completed before exiting the IRQ handler
#if defined(__IMXRT1062__) // Teensy 4.1
asm("DSB");
#endif
}