Oscilloscopio Avanzato con Arduino
Questo esempio implementa un oscilloscopio con una scheda Arduino Uno. L’oscilloscopio ha le seguenti caratteristiche:
- frequenza di campionamento di 50 KSA / s
- Campionamenti visualizzazione: 100 campioni
- Canali analogici selezionabili da AN0 a AN5
- Scala verticale da 10 mV / div a 1 V / div
- Scala di tempo da 200 ms / div a 100 ms / div
- Modalità di trigger: nessuno, auto e normale
- edge trigger: in calo, in aumento
- Livello di trigger selezionabile
- indicatore trigger
Hardware
- Arduino UNO
- Modulo ESP-01 WiFi (con µPanel Firmware)
- Adattatore Breadboard ESP-01
- Cavi Breadboard (4 linee, Maschio-Femmina)
μPanel Definizione L’applicazione implementa 2 pannelli completamente diversi: 1) schermata iniziale, 2) Pannello oscilloscopio. 1) definizione dello schermo Splash HTML:
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 Definizione del pannello HTML:
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;}
Codice HTML spiegato in dettaglio
D!252; Definire il colore di sfondo a 252
/5 Inserire una riga vuota mezza altezza
G0A%95,70*0:0,99,0,5,10,8::::0F0:FFF:FFF:252:121;
Definire il display dell’oscilloscopio come una trama (G) con ID 0 e di tipo A (X indicizzazione automatica). Il che significa che l’utente deve fornire solo i valori y. L’area di stampa è impostata al 95% x 70%. La dimensione del carattere è impostato su 0 per nascondere il titolo, i valori su asse xe y. Impostare manualmente la gamma X a [0,99] e la gamma Y per [0,5]. Impostare manualmente la griglia a 10 divisioni per la X e 8 divisione per le Y. insieme vuoto campi per titolo, etichetta X e Y etichetta. Imposta colore curva verde (0F0), Titolo colore bianco (FFF), confine Plot al bianco (FFF), colore della griglia di verde (252) e il colore di sfondo trama di colore verde scuro (121).
{d,-7%93S1o70!242#8F8^;|%33<M0s1:CH 0;|%33^M1s1:1 V/div;|%33>M2s1:10 ms/div;}
Definire la barra del pannello oscilloscopio con scale di canale, verticali e di tempo. Spostare il contenitore in crescita del 7%, al fine di sovrapporre la barra sul display dell’oscilloscopio. Impostare la dimensione della barra al 93%. Definire una classe di stile (1) con l’opacità al 70%, colore di sfondo verde (242), forecolor verde chiaro (8F8), Centro contenuti allineamento. Dividere il contenitore in 3 celle e assegnare loro stile 1. Inserire il testo “CH 0” nella prima cella, il testo “1 V / div” nella seconda, e il testo “10 ms/div” in il terzo.
S2!4A4,252-r20m%30,12;
Definire una classe di stile (2) da utilizzare per creare i pulsanti oscilloscopio con colore gradiente di sfondo da verde (4A4) per verde scuro 252, confine con raggio di 20 eq. pixel, il contenuto centrale allineati, e le dimensioni del 30% x 12%.
K1:{s2|.??I1.434%30;|T:?;|.??I1.430%30;}$
Definire una macro (1) per creare i pulsanti oscilloscopio. La macro crea un contenitore con classe di stile 2, con 3 celle. Si lega l’evento click alla prima cella e inserisce l’immagine 1.434 (simbolo +) in scala al 30%. Si inserisce il testo del primo parametro macro in seconda cella. Si lega l’evento click sulla terza cella e inserisce l’immagine 1.430 (simbolo -) in scala al 30%.
{^%95|J1(CH)|J1(Amp)|J1(Time)}/5
Creare un contenitore per contenere i primi tre pulsanti oscilloscopio. Impostare la dimensione del contenitore al 95%. Creare 3 celle e applicare la macro 1 per creare i pulsanti con i testi: “CH”, “Amp” e “Ora”. Inserire una riga vuota mezza altezza sotto il contenitore.
{^%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;}}
Creare un contenitore con dimensioni di 95% per contenere la seconda fila di pulsanti. Dividere il contenitore in tre celle. La prima cella contiene un sub-contenitore con stile 2, testo “DC”, un interruttore (0) di tipo F, in scala 50%, in posizione di riposo, e il testo “AC”. La seconda cella contiene un sub-contenitore con stile 2, fare clic su evento 7, il testo “Trigger” e l’utente definito a LED con tre stati corrispondenti alle immagini 1.438, 1.414, 1.418. Il terzo cellule contiene un contenitore sub con stile 2, grassetto, testo “NO”, l’interruttore (1) di tipo F, in scala del 50%, lo stato, e il testo “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;}
Inserire un contenitore per contenere i controlli di trigger, con carattere grassetto, il colore di sfondo sfumato 272-252, il raggio di 20 eq. pixel, ForeColor AFA e di frontiera. Impostare la dimensione del contenitore al 90% scalare gli oggetti al 80%. Impostare una cella del contenitore con il contenuto centrale allineato, un pò di spazio (&&&) un verde principio ha (off). Aggiungere il “livello di trigger:” testo con un messaggio dinamico (3) impostato a 2,5 V. Aggiungere una riga al contenitore e aggiungere un intervallo (cursore 0), con dimensioni 98%, range [0,1023], punto 1, primo valore 512 e intervallo di aggiornamento di 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 } }