Let’s build an oscilloscope with #raspberrypi and #stm32

It’s possible to build a simple oscilloscope with an Arduino Uno, its standard serial port and the serial plotter. I wanted to see if I could take things to the next level and incorporate (the beginnings) of data capture and analysis, and be faster, too.

For this project you’re going to need a Raspberry Pi and an STM32 microcontroller. The Pi does the logging, and the STM32 acts as an ADC (Analogue to Digital Converter). You could use an off-the-shelf ADC module, but I happened to have an STM32 Blue Pill (STM32F103C8T6). You’ll also need the Arduino IDE with the stm32duino libraries installed.

Another good candidate would be an ESP32-WROOM in place of the STM32, but I have only provided code for the latter chip, not the former. You could also use an Arduino Uno/Nano, or some other flavour, but remember that these operate at 5V, and are therefore incompatible with the Pi unless you use logic level shifters, or maybe voltage dividers.

We use the SPI protocol to communicate, with the Pi acting as master, and the STM32 acting as slave. We can also use the Pi to generate tones to feed into the “capture card” (i.e the STM32).

To get the code, clone my project over on github:

git clone https://github.com/blippy/rpi.git

In directory stm32/adc-interrupts, there is an INO file. Compile it and upload it to the STM32. It’s quite a short program (although it took me long enough to figure out!) and uses interrupts to capture the ADC data.

In a terminal on the Pi, cd to directory rpi/oscope. All the action will happen there. Hookup the Pi to the STM32 pins as follows:

      Pi  STM32
MOSI  19  PA7
MISO  21  PA6
SCK   23  PA5
SS    24  PA4
SAMP  35  PB1

Make sure that the devices are also connected by a common ground. Plug a wire between one of the ground pins on the Pi with a ground pin on the STM32.

As you might expect, MOSI, MISO, SCK and SS are for the SPI communications. You can probably dispense with the MOSI connection.

The row labelled SAMP is the sampling pin on the STM32. That’s where it’s ADC is set up. Any external voltage source that you want to sample should be fed into this pin instead of pin 35 from the Pi.

There’s nothing special about pin 35. However,I have provided the file tone.py that generates a 440Hz squarewave with a 50% duty cycle. So connect pin 35 to PB1 and run

python3 tone.py

Let’s see what it looks like on our oscilloscope, shall we? Type


which both runs capture.py and plot.py. Later, you can experiment with both files. The plotter is actually quite slow, but after a few seconds you should get a plot that looks something like this:

Groovy! This was achieved by setting

spi.max_speed_hz = 500000

in capture.py. Let’s spice things up by creating an RC (resistor-capacitor circuit). An RC circuit looks something like this:

The bottom terminal points in the diagram represents ground, whilst the upper two are positive voltages. Take the lead out of PB1 and put it into “Vin”. Choose values for R and C. I chose 220 ohm for the former, and 2.2uF for the latter. It gives a nice result. Attach “Vout” to PB1. Remember to ground the capacitor.

Run ./oner again, and you’ll get an output that should look like this:

The signal now approaches a triangular wave. Experiment further, and see if you can produce a better triangular wave, or perhaps a sine wave. It will involve having two RC circuits in series with correctly-chosen values.

Further directions for the project would be:

  • to have continuous capture. However, I haven’t found any graphing software that’s fast enough yet.
  • to be able to set trigger points. It would be nice to see switch bouncing, for instance
  • Fourier transforms

Go and explore!


If you want to change the capture frequency, then the best way is likely through changing


in capture.py. Reduce the speed if you want a slower capture, increase it if you want more samples. It is also possible to introduce a delay between fetches. There are some limits on the capture speeds, bounded by:

  • ADC conversion time. I’ve discusses various tradeoffs here. It currently takes the sample rate is set with ADC_SMPR_55_5, which equates with 175kHz. Pretty good
  • Transfer rate between STM32 and Pi.
  • Speed of the python program itself. Capture frequencies maybe adversely affected by the fact that python is a slow interpreter

Experimentation would be required to see how far the data capture rates could be pushed. Suffice it to say that you do not get anywhere near the 175kHz sample rate quoted above. Reimplementing the python program in C++ might be a good first step in upping the rates.

Update 2020-07-23

Choosing the SPI rate

As of today, you can choose the SPI capture rate in python.py. The question is: what SPI rate should you choose for what “real” frequency?

Rate table
       -r    us   Actual     FF
    1,000  4755    210Hz    4.8
    5,000  3596    278Hz   18.0
   10,000  1842    543Hz   18.4
   50,000   389   2570Hz   19.5
  100,000   210   4751Hz   21.0
  200,000   121   8264Hz   24.1
  300,000  90.8    11kHz   27.3
  400,000  81.5  12.2kHz   32.8
  500,000  68.5  14.6kHz   34.2
1,000,000  36.5  27.4kHz   36.5

The above table was compiled from actual samplings on a Pi3. They are indicative, and are probably inapplicable on something like a Pi4 or Pi2.

-r is the SPI speed parameter passed to either oner or capture.py The default speed is 1,000,000 Hz, but you can slow down the speed of SPI by issuing a command like:

oner -r 50000

which, obviously, sets the SPI rate to 50kHz. Slowing down the rate will obviously reduce the number of samples you take per second, which is actually an advantage if you want to sample lower frequencies.

us is the actual sampling time in microoseconds. So, setting the SPI rate higher reduces the time for each sample, as indicated in the table.

Actual is the actual sampling rate, as measured in Hz. It’s just the us converted into an equivalent frequency.

FF is the fudge factor that you need to apply to convert your desired actual frequency to the SPI value.

You will notice that there is not a linear relationship between the actual frequency of measurement and the SPI rate. This is because the python code takes time to process a loop, the Linux kernel may interrupt processes, and so forth.

The table is just a guide. If you want 100 samples at around 11kHz, then you would use a value of -r or 300000. I have given some examples below.

Example 1

You want to capture input at a rate of 8kHz, which may be generated from, say, audio output. A value of -r of 200000 would be suitable. Look at the actual rate column, and cross-reference it with a -r value. Alternatively, you can apply a fudge factor of, let’s say, 25, which will give you the same value. You will end up with a sequence of 100 samples (the default) taken at approx 8kHz.

Example 2

Suppose you have a signal which you estimate has a frequency of 440Hz. You want to take 20 samples over that period. In other words, you want a sampling frequency of 20*440 = 8800Hz. So, here again, choosing a SPI rate of 200000 is about right.

You should be able to capture approx 100/20 = 5 waves for a 100-sample capture. You can increase the -r value for a finer-grained measurement of the wave, which will reduce the number of waves you capture. And vice versa.

I hope that makes sense.

Update 2 2020-07-23: investigating speedups

Dissatisfied with the speed as it stood, I decided to try a test using C++. At the 1MHz speed, I was getting reads in the range 37us-53us. The results were highly variable! It means that the Python code wasn’t likely to be the problem. Who knew Python could be so fast?

It turns out that reading in words (uint16_t) in blocks of 100 instead of a word at a time approximately doubles the transfer rate. I estimate that the time per read is 17us, maybe going down to 16us if one reads in blocks of 200.

This is close to the theoretical maximum of SPI transferring at a rate of 1MHz, which is 1E6/16 (bits) = 62.5kHz. A time per read of a sample 17us implies a sampling rate of 1E6/17 = 59kHz.

So, reading in blocks of 100 effectively maxes out the SPI, and the only way you can increase the sample rate is to increase the SPI rate. I haven’t yet found out if this is feasible.

In the STM32 I have set the ADC sample rate to ADC_SMPR_55_5 (see here), giving an ADC conversion rate of 5.7us, which equates with 175kHz.

So maybe there could be more efficiency gains on top of that. More work would be required.

About mcturra2000

Computer programmer living in Scotland.
This entry was posted in Uncategorized. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s