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:&#956Panel;}/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;}


 
Application splash screen
 
Oscilloscope screenshot


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:&#956Panel;}/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(" &#181s/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
  }
  
}