PyScope - 1MHz Python Oscilloscope
by TsiamDev in Circuits > Arduino
1698 Views, 1 Favorites, 0 Comments
PyScope - 1MHz Python Oscilloscope
This is a rudimentary oscilloscope built with python for Arduino DUE
Supplies
Arduino Due, USB cable and 1 jumper wire
Showcase
Here is the sample output for a 1MHz signal.
You can find more information about me, and the tutorials I make, here:
Introduction
So, the other day I was playing around with a pet project involving electronics. And I came upon a situation where I needed to generate a very fast PWM pulse (with a frequency of 1MHz).
Thus, I came upon two problems.
1. Generate the pulse, and
2. check to see if it the pulse is generated correctly.
GitHub Repository
This is the GitHub repository dedicated to this project. https://github.com/TsiamDev/PyScope
Wiring
A simple jumper between the 2 pins is all that is needed.
The schematic was made with Fritzing.
Arduino Code
To tackle the first problem, I utilized an Arduino Due that I had lying around. The problem was that digitalWrite(), digitalRead() and analogRead() were too slow for my purposes. And so, I set up, and then, directly wrote to the PIO registers of the Atmel Chip.
First, initialize digital pin 2 as output:
void setup() { // initialize digital pin 2 as output pinMode(pwm_pin, OUTPUT); .... }
Then, write the corresponding value (HIGH, or LOW) to the pin, based on the duty cycle defined by the <cnt > counter:
void loop() { if (flag == true){ // Set digital pin 2 to HIGH and leave // the rest pin states unchanged PIOB->PIO_SODR |= 0x1<<25; }else{ // Clear digital pin 2 and leave // the rest pin states unchanged PIOB->PIO_CODR |= 0x1<<25; } // The following lines set the pulse's period // by (essentially) counting the nanoseconds that have passed // increment the period counter cnt++; // if the counter has reached the 15/20 = .75 = 75% of the pulse's period // change the flag (which essentially controls the duty cycle by changing // the state of the digital pin in the above code) if (cnt == 15){ flag = false; } if (cnt == 20){ flag = true; // reset counter cnt = 1; } ...... }
The bottleneck happened when reading the pin that generated the pulse. I modified somewhat (rather, I stripped down) the code that I found here:
https://forum.arduino.cc/t/arduino-due-adc-dma-channel-ordering-in-buffer/620520
First, set up the ADC on pin A0:
// these lines set free running mode on adc 7 (pin A0) ADC->ADC_MR |= 0x80; ADC->ADC_CR=2; ADC->ADC_CHER=0x80;
Then, simply wait until the ADC conversion happens. Finally, read the data from pin A0:
// wait for ADC conversion while((ADC->ADC_ISR & 0x80)==0); // read the data from pin A0 a0 = ADC->ADC_CDR[7];
Problem (1) solved!
Python Oscilloscope
And now, off to the second problem. How do I visualize correctly the data that ADC read? Well, with the trusty contemporary multitool of course! Which is none other than Python.
The code I wrote is rather simple and most of it is for visualization purposes rather than calculations.
Firstly, we open a connection to the Serial Port that our Arduino is connected to, we set the buffer size and clear the initial (garbage) input data:
# Open Serial Communication Port #ser = Serial('COM6', 115200, timeout=None) ser = Serial('COM7', 1000000, timeout=None) ser.set_buffer_size(rx_size = offset) # Until the correct initialization of the port at Arduino's side # Garbage exists on the input buffer, so clear the input ser.flushInput()
Afterwards, we read the input data whenever a threshold (in bytes) is reached:
i = [] data = [] int_data = [] while True: #if the osciloscope is NOT Paused then if eh.isPaused is False: # Clear the input cause it may have been overflown # with obsolete data in any given Pause-Play interval ser.flushInput() while True: # check to see how many bytes are waiting to be read bytesToRead = ser.inWaiting() #print(bytesToRead) # DEBUG # We avoid reading constantly the input by specifying # this offset (whihc is actually a threshold). # If more than the <offset> number of bytes are # waiting, then read stop waiting for more bytes and... if bytesToRead >= offset: break
Then, we convert the read data to a python string and parse it (while surrounding the code with a <try> block to catch the times when input is malformed, or some other type of exception is thrown):
try: # ...read <offset> bytes, and then convert them to python string temp = ser.read(offset).decode() # Some minimal parsing happens here... # remove \r\n temp = temp.split('\r\n') # ...and here # remove any empty elements from the list temp = [x for x in temp if x != ''] #print(temp) # DEBUG #print("offset: ", offset) # DEBUG
If the resulting list is not empty, we convert it to a list of integers, and finally we read the voltage amplitude with the calculation at line 81 (3.3 volts is the digital HIGH on Due and 4095 is the resolution of the ADC). Int_data now has the measured voltages:
if temp != "": buf = [int(float(x)) for x in temp] data = [] for b in buf: data.append(b * (3.3 / 4095.0)) #total_data.append(b * (3.3 / 1023.0)) int_data = [int(x) for x in data]
The final part of the calculations, concerns triggering. To do this, we search the converted array to find the first time where the voltage goes from LOW to HIGH, and toss the previous samples.
Int_data is now triggered. Pun intended 😊
# Triggering #""" index = int_data.index(0) indices = [i for i, x in enumerate(int_data) if x == 0] for i in range(0, len(indices)-1): if int_data[indices[i]] == 0 and int_data[indices[i+1]] > 0: index = i break int_data = int_data[index:] #print(int_data[index], index) #"""
The rest of the python code utilizes the matplotlib library to render the information to the screen and I consider it to be self-explanatory. Nevertheless, If you feel it should be further explained drop me a line and I will remedy that 😊.
Oh! Also don’t forget! At the end, we close the port we opened.
Till next time, have a good one!