PyScope - 1MHz Python Oscilloscope

by TsiamDev in Circuits > Arduino

1545 Views, 1 Favorites, 0 Comments

PyScope - 1MHz Python Oscilloscope

scope_demo.png

This is a rudimentary oscilloscope built with python for Arduino DUE

Supplies

Arduino Due, USB cable and 1 jumper wire

Showcase

scope_demo.png

Here is the sample output for a 1MHz signal.

You can find more information about me, and the tutorials I make, here:

tsiamdev.github.io


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

wiring.png

A simple jumper between the 2 pins is all that is needed.

The schematic was made with Fritzing.

Arduino Code

3.png
4.png
1.png
2.png

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

9.png
5.png
6.png
7.png
8.png

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!