Arduino Advanced Oscilloscope
This example implements an oscilloscope with an Arduino Uno board. The oscilloscope has the following features:
- Sampling rate of 50 kSa/s
- Oscilloscope display of 100 samples
- Analog channel selectable from AN0 to AN5
- Vertical scale selectable from 10 mV/div to 1 V/div
- Time scale selectable from 200 µs/div to 100 ms/div
- Trigger modes: none, auto and normal
- Trigger edge: falling, rising
- Trigger level selectable
- Trigger indicator
Hardware
- Arduino UNO Board
- ESP-01 WiFi module (with µPanel Firmware)
- ESP-01 Breadboard adapter
- Breadboard wires (4 lines, Male-Female)
µPanel definition
The application implements 2 completely different panels: 1) splash screen, 2) oscilloscope panel.
1) Splash screen HCTML definition:
D!252;{^*30%80,100!282,141{ht2,000,1*14T:μPanel;}/3{*5T:Mobile Interactive;_T:Universal Panel;}_{*7_T:Oscilloscope v2;_*6T#3A3:for Arduino UNO;}}/20*15B0:START;
2) Oscilloscope panel HCTML definition:
D!252;/5G0A%95,70*0:0,99,0,5,10,8::::0F0:FFF:FFF:252:121;{d,-7%93S1o70!242#8F8^;|%33<M0s1:CH 0;|%33^M1s1:1 V/div;|%33>M2s1:10 ms/div;}S2!4A4,252-r20m%30,12;K1:{s2|.??I1.434%30;|T:?;|.??I1.430%30;}${^%95|J1(CH)|J1(Amp)|J1(Time)}/5{^%95|{s2T:DC;W0F*5:0;T:AC;}|{s2+.7T:Trigger;L1M:0:1.438,1.414,1.418;}|{s2fbT:NO;W1F*5:1;T:AU;}}/5{fb!272,252r20#AFA-%90*8|m&&&L2G:0;*12T:Trigger level: ;M3:2.5 V;_R0%98:0:1023:1:512:100;}
HCTML Code Explained in Detail
D!252; Define the background color to 252
/5 Insert an half height emply line
G0A%95,70*0:0,99,0,5,10,8::::0F0:FFF:FFF:252:121;
Define the oscilloscope display as a plot (G) with ID 0 and type A (auto X indexing). Which means that the user has to provide only the y-values. The plot area is set to 95% x 70%. The font size is set to 0 in order to hide title, values on x-axis and y-axis. Manually set the X range to [0,99] and the Y range to [0,5]. Manually set the grid to 10 divisions for the X and 8 division for the Y. Set empty fields for Title, X label and Y label. Set curve color to green (0F0), Title color white (FFF), Plot border to white (FFF), grid color to green (252) and plot background color to dark green (121).
{d,-7%93S1o70!242#8F8^;|%33<M0s1:CH 0;|%33^M1s1:1 V/div;|%33>M2s1:10 ms/div;}
Define the oscilloscope panel bar with channel, vertical and time scales. Displace the container up 7% in order to superimpose the bar on the oscilloscope display. Set the bar size to 93%. Define a style class (1) with opacity 70%, background color green (242), forecolor light green (8F8), content alignment center. Divide the container into 3 cells and assign to them the style 1. Insert the text “CH 0” into the first cell, the text “1 V/div” into the second one, and the text “10 ms/div” into the third one.
S2!4A4,252-r20m%30,12;
Define a style class (2) to be used to create the oscilloscope buttons with gradient background color from green (4A4) to dark green 252, border with radius of 20 eq. pixels, content middle aligned, and size 30% x 12%.
K1:{s2|.??I1.434%30;|T:?;|.??I1.430%30;}$
Define a macro (1) to create the oscilloscope buttons. The macro creates a container with style class 2, with 3 cells. It binds the click event to the first cell and inserts the picture 1.434 (symbol +) scaled to 30%. It insert the text of first macro parameter into the second cell. It binds the click event on the third cell and inserts the picture 1.430 (symbol -) scaled to 30%.
{^%95|J1(CH)|J1(Amp)|J1(Time)}/5
Create a container to contain the first three oscilloscope buttons. Set the container size to 95%. Create 3 cells and apply the macro 1 to create the buttons with texts: “CH”, “Amp” and “Time”. Insert an half height empty line under the container.
{^%95|{s2T:DC;W0F*5:0;T:AC;}|{s2+.7T:Trigger;L1M:0:1.438,1.414,1.418;}|{s2fbT:NO;W1F*5:1;T:AU;}}
Create a container with size of 95% to contain the second row of buttons. Divide the container into three cells. The first cell contains a sub-container with style 2, text “DC”, a switch (0) of type F, scaled 50%, in position off, and the text “AC”. The second cell contains a sub-container with style 2, click event 7, the text “Trigger” and the user defined LED with three states corresponding to the pictures 1.438, 1.414, 1.418. The third cells contains a sub container with style 2, font bold, text “NO”, switch (1) of type F, scaled 50%, state on, and text “AU”.
/5 Insert an half height emply line
{fb!272,252r20#AFA-%90*8|m&&&L2G:0;*12T:Trigger level: ;M3:2.5 V;_R0%98:0:1023:1:512:100;}
Insert a container to contain the trigger controls, with font bold, background gradient color from 272 to 252, radius with 20 eq. pixels, forecolor AFA and border. Set the container size to 90% scaling the objects to 80%. Set one container’s cell with middle aligned content, some space (&&&) a standard green led (off). Add the text “Trigger level:” with a dynamic message (3) set to 2.5 V. Add a row to the container and add a range (slider 0) with size 98%, range [0,1023], step 1, initial value 512 and refresh interval of 100 ms.
Arduino Code
#define FOREVER -1 // Constant for wait forever #define DISPLAY_POINTS 100 // Number of points to display #define HYST 3 // Trigger hysteresis #define NUMBER_OF_VERT_SCALES 8 // Number of vertical scales #define NUMBER_OF_TIME_SCALES 9 // Number of time scales // Vertical scales in mV/div const int VerticalScales_mV[NUMBER_OF_VERT_SCALES] = {10,20,50,100,200,500,625,1000}; // Time scales in us/div const long TimeBases_us[NUMBER_OF_TIME_SCALES] = {200,500,1000,2000,5000,10000,20000,50000,100000}; String Msg; // Received Message char Page = 0; // Current Application Panel char Channel = 0; // Selected analog input channel int Rate = 1000; // Selected sample rate volatile char Triggered = 0; // Trigger got char VerticalScaleNumber = 6; // Selected vertical scale (0.625 V/div) char TimeBaseNumber = 4; // Selected time scale (5 ms/div) char TriggerEdge = 0; // 0 = none, 1 = rising, 2 = falling unsigned int SampleBuffer[DISPLAY_POINTS]; // Buffer for samples volatile unsigned int NSample = 0; // Number of captured samples int AutoTriggerCounter; // Flag Auto Trigger int AutoTrigger_Time_Cycles = 50000 / 8; // 125 ms ---> 8 fps int SamplePrescaler; // Sample decimation to obtain time scale int TriggerThreshold; // Trigger Threshold in LSB void setup() { Serial.begin(115200); // Initialise serial at 115200 to speed-up delay(5000); // Let's the module start Serial.println(""); // Discarge old partial messages ADC_setup(); // Initialise AD Converter SetSamplingTimeBase(); // Set time scale and sampling } // Initialise AD Converter void ADC_setup() { TCCR1A = 0x00; // No outputs on compare math, no PWM mode TCCR1B = 2 | (1 << WGM12); // Enable CTC mode up to OCR1A and prescaler /8 -> Start TCCR1C = 0x00; // Nothing to set into C register TCNT1 = 0; // Clear Timer Counter OCR1A = 39; // Set Threshold A (Sampling f= 16e6/8/40 = 50 kHz) OCR1B = 1; // Set Threshold B to trigger AD (any value < 39 is ok) TIFR1 = (1 << OCIE1A) | (1 << OCIE1B); // Clear Interrupt flag Match A and B TIMSK1 = (0 << OCIE1A) | (0 << OCIE1B); // Put 1 to ename Interrapt flag Match A or B if required analogRead(A0); // Dummy Read to make Arduino setting all the AD's registers ADMUX = 0x40 | Channel; // Select "Channel" and VCC as ADC reference voltage ADCSRA = 0x93; // Prescaler 8 (2 MHz, yes, a little too fast...), Turn ON the ADC, Clear IF ADCSRA |= (1 << ADIE); // Enable AD Interrupt on conversion completed ADCSRB = 5; // Select Timer 1 Match B as trigger ADCSRA |= 1 << ADATE; // Enable Auto trigger } void SetSamplingTimeBase() { // The sampling is fixed to 50 kHz, we just change the decimator factor. // Please note that the trigger still works on the full sample rate // TimeBase_us is microseconds per division: // At 50 kS/s with 100 LCD Points, 10 Divisions // We have a time/div = 200 us SamplePrescaler = TimeBases_us[TimeBaseNumber] / 200; // Compute the decimator prescaler AutoTriggerCounter = AutoTrigger_Time_Cycles; // Set the Auto trigger interval NSample = 0; // Clear the sampler counter } /***************************************************** * This function waits a data message from the uPanel * * Input: timeout_ms time to wait for message * -1 for forever * Return: 0 = Timeout, 1 = Message received ******************************************************/ int WaitMessage(int timeout_ms) { int c; // the received byte unsigned long entrytime = millis(); // Read the time at the function entry static char KeepBuffer = 0; // This tells if we have a partial message in the buffer if (!KeepBuffer) Msg = ""; // If Keepbuffer is false than clear the old message KeepBuffer = 0; // in any case set now the keep buffer to false do { while ((c = Serial.read()) > '\n') Msg += (char) c; // Read incoming chars, if any, until new line if (c == '\n') // is message complete? { if (Msg.substring(0,1).equals("#")) return 1; // if it is a data message return 1 Msg = ""; // otherwise, wait for another one } } while ((timeout_ms < 0) || (millis()-entrytime < timeout_ms)); // has max time passed? KeepBuffer = 1; // Keep the partial buffer content return 0; // if time passed, return 0 } void DisplaySplashScreen() // Send the splash screen cointaining the Start button { Serial.print("$P:D!252;{^*30%80,100!282,141{ht2,000,1*14T:μPanel;}/3{*5T:Mobile Interactive;_T:Universal Panel;}_"); Serial.println("{*7_T:Oscilloscope v2;_*6T#3A3:for Arduino UNO;}}/20*15B0:START;"); while (WaitMessage(FOREVER)) { if (Msg.substring(0,4).equals("#B0P")) { Page = 1; break;} // has start been presed? Change page and exit } } void UpdateOscilloscopeTrigger() // Update the Trigger edge LED state { Serial.print("#L1"); // Update LED 1, which is a custom led with 3 states Serial.println(TriggerEdge,10); // X (for none), up arrow (for rising edge), down arrow (for falling edge) } void ChangeTrigger() // Change the tigger edge mode between: None, Rising, Falling { TriggerEdge = (TriggerEdge + 1) % 3; // Increase the trigger mode, module 3 UpdateOscilloscopeTrigger(); // Update the oscilloscope trigger image } void UpdateOscilloscopeBar() // Update the oscilloscore bar { int YS; // Used for vertical scale int VerticalScale_mV = VerticalScales_mV[VerticalScaleNumber]; // Get the vertical scale long TimeBase_us = TimeBases_us[TimeBaseNumber]; // Get the time scale Serial.print("#M0CH "); // Update Oscilloscope panel header Serial.println(Channel,DEC); // with the selected channel Serial.print("#M1"); // Update Oscilloscope panel header with the vertical scale YS = VerticalScale_mV; // Scale in mV if (YS >= 1000) YS = YS / 1000; // if greater than 1000 mV use V as unit Serial.print(YS,10); // Update the panel label if (VerticalScale_mV < 1000) // detect with unit has to be used Serial.println(" mv/div"); // display mV per division else // or Serial.println(" V/div"); // display V per division Serial.print("#M2"); // Update Oscilloscope panel header with the time scale if (TimeBase_us < 1000) // the time per division is less than 1000 us ? { Serial.print(TimeBase_us,10); // if yes, write the time scale value Serial.println(" µ/div"); // and the us/div label } else // otherwise if (TimeBase_us < 1000000) // is time scale less than 1 s ? { Serial.print(TimeBase_us/1000,10); // if yes, display the time base value Serial.println(" ms/div"); // and the ms/div label } } void ChangeCh(char d) // Change the selected channel { Channel += d; // Increase or decrease selected channel if (Channel < 0) Channel = 0; // Limit minimum channel to 0 (AN0) if (Channel > 5) Channel = 5; // Limit maximum channel to 5 (AN5) ADMUX = 0x40 | Channel; // Change the ADC selected channel UpdateOscilloscopeBar(); // Update the oscilloscope panel bar } void ChangeTime(char d) // Change the selected time scale { TimeBaseNumber += d; // Increase or descrease the selected time scale if (TimeBaseNumber < 0) TimeBaseNumber = 0; // Limit the minimum scale to the first one if (TimeBaseNumber >= NUMBER_OF_TIME_SCALES) // Limit the maximum scale to the number of scales TimeBaseNumber = NUMBER_OF_TIME_SCALES-1; SetSamplingTimeBase(); // Change the sampling schema accordingly UpdateOscilloscopeBar(); // Update the oscilloscope panel bar } void ChangeAmp(char d) // Change the selected vertical scale { VerticalScaleNumber += d; // Increase or descrease the selected scale if (VerticalScaleNumber < 0) VerticalScaleNumber = 0; // Limit the min selected scale to the first one if (VerticalScaleNumber >= NUMBER_OF_VERT_SCALES) // Limit the max selected scale to the last one VerticalScaleNumber = NUMBER_OF_VERT_SCALES-1; UpdateOscilloscopeBar(); // Update the Oscilloscope panel bar } void ChangeThreshold(int th) // Change the trigger threshold { TriggerThreshold = th; // Save the new threshold float thv = ((float)th)/1024.0*5.0; // Transform the threshold from LSB into V Serial.print("#M3"); // Update the value of trigger threshold Serial.print(thv,2); // on the panel Serial.println(" V"); // appending the V unit } void RefreshOscilloscopePlot() // Refresh the oscilloscope display plot { int n; // Sample counter float Voltage; // Voltage // Compute the scale factor to transform LSB into display range [0, 5] float k = 1/1024.0 * 5.0 * (5.0/8.0) * (1000.0/(float)VerticalScales_mV[VerticalScaleNumber]); Serial.print("#G0C:"); // Clear the old plot and get ready to receive the new for(n=0; n<DISPLAY_POINTS; n++) // Send all display points { Voltage = ((float) SampleBuffer[n]) * k; // Transform the sampled LSB into Voltage Serial.print(Voltage,3); // Send the acquired voltage Serial.print(","); // Append the separator for the next value } Serial.println(""); // Conclude the plot command } void ChangeTriggerAuto(char v) // Switch AutoTrigger / Normal modes { if (v) AutoTriggerCounter = AutoTrigger_Time_Cycles; // Set auto trigger interval else AutoTriggerCounter = 0; // Disable auto trigger } void DisplayOscilloscope() // Display the oscilloscope panel { static char LastTriggered = -1; // This is used to remember the trigger LED state // Send Oscilloscope panel Serial.print("$P:D!252;/5G0A%95,70*0:0,99,0,5,10,8::::0F0:FFF:FFF:252:121;"); Serial.print("{d,-7%93S1o70!242#8F8^;|%33<M0s1:CH 0;|%33^M1s1:1 V/div;|%33>M2s1:10 ms/div;}"); Serial.print("S2!4A4,252-r20m%30,12;K1:{s2|.??I1.434%30;|T:?;|.??I1.430%30;}$"); Serial.print("{^%95|J1(CH)|J1(Amp)|J1(Time)}"); Serial.print("/5{^%95|{s2T:DC;W0F*5:0;T:AC;}|{s2+.7T:Trigger;L1M:0:1.438,1.414,1.418;}|{s2fbT:NO;W1F*5:1;T:AU;}}"); Serial.println("/5{fb!272,252r20#AFA-%90*8|m&&&L2G:0;*12T:Trigger level: ;M3:;_R0%98:0:1023:1:512:100;}/5{>%90B0:Exit;}"); UpdateOscilloscopeBar(); // Update the oscilloscope panel bar UpdateOscilloscopeTrigger(); // Update the oscilloscope trigger ChangeThreshold(512); // Set the threshold in the middle of the range while (1) { if (WaitMessage(1)) // Wait until it's time for a new sample or incoming data { Msg.toUpperCase(); if (Msg.substring(0,4).equals("#R0:")) ChangeThreshold(Msg.substring(4).toInt()); // Settings button? exit if (Msg.substring(0,7).equals("#.EVT:0")) ChangeCh(1); // Manage button Channel + if (Msg.substring(0,7).equals("#.EVT:1")) ChangeCh(-1); // Manage button Channel - if (Msg.substring(0,7).equals("#.EVT:2")) ChangeAmp(1); // Manage button Vertical Scale + if (Msg.substring(0,7).equals("#.EVT:3")) ChangeAmp(-1); // Manage button Vertical Scale - if (Msg.substring(0,7).equals("#.EVT:4")) ChangeTime(1); // Manage button Time Scale + if (Msg.substring(0,7).equals("#.EVT:5")) ChangeTime(-1); // Manage button Time Scale - if (Msg.substring(0,7).equals("#.EVT:7")) ChangeTrigger(); // Manage Trigger Toggle Button if (Msg.substring(0,4).equals("#W10")) ChangeTriggerAuto(0); // Manage switch into Trigger Manual if (Msg.substring(0,4).equals("#W11")) ChangeTriggerAuto(1); // Manage switch into Trigger Auto if (Msg.substring(0,4).equals("#B0P")) {Page=0; return;} // Exit button? change to page 0 } if (NSample == DISPLAY_POINTS) // Has the ADC with the interrupt routing sample all points? { RefreshOscilloscopePlot(); // if yes, refresh the oscilloscope display NSample = 0; // Clear the sample counter to start a new acquisition cycle } if (Triggered != LastTriggered) // The trigger state changed ? { // if the trigger was not an auto-trigger, then turn on the trigger LED if (Triggered == 2) Serial.println("#L21"); else Serial.println("#L20"); // Update the trigger LED LastTriggered = Triggered; // Remember the new state } } } // This is the Arduino Main Loop! void loop() { // Display the correct panel page if (Page == 0) DisplaySplashScreen(); // Send Application Splash Screen if (Page == 1) DisplayOscilloscope(); // Send Oscilloscope panel } //------------------------------------------------------------------------------------ // ADC Interrupt Routine //------------------------------------------------------------------------------------ ISR(ADC_vect) { int x = ADC; // Read the sampled value static char TrigState = 0; // This remember the trigger state // 0 = waiting, 1 = , 2 =, 3 = Got. static int NPres = 0; // This remember the decimator counter TIFR1 = (1 << OCIE1B); // Clear Interrupt flag Match A and B if ((NSample == 0) && (TriggerEdge)) // Trigger is enabled and needed? { if (AutoTriggerCounter > 0) // is the autotrigger enabled? { AutoTriggerCounter--; // if yes, decrease the auto trigger timer if (!AutoTriggerCounter) // auto trigger exipred? { TrigState = 3; // if yes fire the trigger AutoTriggerCounter = AutoTrigger_Time_Cycles; // Restart a new autotrigger } } else // otherwise, if auto trigger is disabled { AutoTriggerCounter--; // Count down for the same trigger interval if (AutoTriggerCounter < -AutoTrigger_Time_Cycles) // has the interval expired? { Triggered = 0; // if yes, turn off the trigger condition (for LED) AutoTriggerCounter = 0; // restart another interval } } if (TriggerEdge == 2) // is the trigger into falling edge? { if (TrigState == 1) // if trigger state is into "signal was above" { if (x < TriggerThreshold - HYST) // signal is now below threshold and hysteresis { TrigState = 2; // fire the trigger! // reload auto trigger interval if enabled if (AutoTriggerCounter > 0) AutoTriggerCounter = AutoTrigger_Time_Cycles; // clear timer if ..... if (AutoTriggerCounter < 0) AutoTriggerCounter = 0; } } if (TrigState == 0) // if trigger sate is into "signal unknown" { if (x > TriggerThreshold + HYST) TrigState = 1; // set state "signal above" if above } } else // Otherwise if the trigger is into rising edge { if (TrigState == 1) // if trigger state is into "signal was under" { if (x > TriggerThreshold + HYST) // signal is now above threshold and hysteresis { TrigState = 2; // fire the trigger! // reload auto trigger interval if enabled if (AutoTriggerCounter > 0) AutoTriggerCounter = AutoTrigger_Time_Cycles; // clear timer if ..... if (AutoTriggerCounter < 0) AutoTriggerCounter = 0; } } if (TrigState == 0) // if trigger sate is into "signal unknown" { if (x < TriggerThreshold - HYST) TrigState = 1; // set state "signal under" if under } } if (TrigState < 2) return; // If trigger is not fired, then exit to prevent saving the sample } if (NSample < DISPLAY_POINTS) // Have all samples been acquired? { if (NPres == 0) // If not, is this the first decimator cycle= { SampleBuffer[NSample] = x; // if yes, save the sample into the buffer NSample++; // increment sample counter if (NSample == DISPLAY_POINTS) // All samples captured? { Triggered = TrigState; // Communicate the trigger source to the main TrigState = 0; // clear the trigger state } } NPres++; // Increase the decimator counter if (NPres >= SamplePrescaler) NPres = 0; // is the counter reached the maximum? clear it } }