Skip to content

G4-Scope

This is an exploration into the combination of analog and digital "computing". The aim is to show electrical signals as graph on a screen, i.e. to implement an oscilloscope. The first constraint is that it must fit on a 7x3 or 7x5 cm PCB so that it can be turned into a module later. The second constraint is that it must be doable for me :) - I don't have any formal electronics design training. That's really the point of this: to learn how a "scope" works.

Hardware

A STM32G431CB µC is used as the brains: its two ADCs can sample at up to ≈ 5 Ms/s, the CPU clock goes up to 170 MHz, it has 32 KB RAM, and there's 128 KB of flash memory. There's enough here for an extensive exploration and implementation.

The display is a small 1.9" 320x170 pixel display. Just enough to display a "graticule" area with 12 divisions across and 6 divisions vertically.

The G4-Scope is controlled by buttons and one or two rotary encoders, but these will need to be on a separate module. There's no way to fit these on one PCB with the display.

For development, a Black Magic Probe is used, using an STM32F411 "Black Pill" board.

That, plus a breadboard and some wires, is about it. The total cost with parts from AliExpress was no more than about €30, as of mid-2026.

Once the basic design and build works, an analog front end of some kind will need to be added. But that's a bit further down the road ...

Software

On the software side, it's all based on PlatformIO and JeeH. I'm developing this on MacOS, but Linux and Windows should work equally well. See JeeH's getting started for details.

One step at a time

There are a lot of moving parts in this project. From figuring out a "good" development cycle to implementing and debugging the many different hardware details for high-speed signal acquisition and visualisation. This G4-Scope is bound to be a pretty long adventure.

All software development is based on the source code in this area: https://codeberg.org/jcw/dobb/src/branch/main/apps/g4-scope/.

Blinking LED

The first build verifies that the G431 has power and is properly connected to the Black Magic Probe (BMP), and that the LED blinks once compiled and uploaded. It's based on this code:

blink.cpp
#include <jee.h>
using namespace jeeh;

const Pin led ("C6","P");

int main () {
    while (true) {
        led.toggle();
        msWait(500);
    }
}

The command to build and upload is:

pio run -e blink -t upload

Console output via RTT

See the JeeH info about RTT for details. This needs a serial connection to the BMP to display messages. That connection can then be kept open across all uploads: very convenient!

console.cpp
#include <jee.h>
#include <jee/hal.h>
using namespace jeeh;
#include "defs.hpp"

int main () {
    initBoard();

    while (true) {
        led.toggle();
        msWait(500);
    }
}

Sample output on the debug console, i.e. the BMP's serial USB port:

console: STM32G431xx @ 170 MHz (v7.0.9-13-gc89bf101)

Note

This same source code structure will be used in all the following "gradual enhacement" examples.

DAC + ADC check

Next step: read out the ADC. The built-in DAC can feed different input voltages as a first test. It looks like in analog mode mode ADC and DAC can be inter-connected on one pin:

Unfortunatelt, JeeH's built-in ADC driver is not quite up to it. It's a simple implementation for ADC1, not for ADC2 (connected to DAC1's output). So for now, I'll jumper A4 to A0.

dac2adc.cpp
#include <jee.h>
#include <jee/hal.h>
using namespace jeeh;
#include "defs.hpp"

int main () {
    initBoard();

    Pin::config("A4:A");
    dac::init(); // currently hard-coded for DAC1, output pin A4
    adc::init(); // currently hard-coded for ADC1
    msWait(2); // TODO needs > 1 ms settling time, but why?

    // this code needs a jumper between DAC out (A4) and ADC in (A0)

    const uint16_t vals [] = {
        0, 1000, 2000, 3000, 4000, 4095, 4000, 3000, 2000, 1000, 0,
    };

    for (auto e : vals) {
        dac::set(e);
        msWait(1);

        logf("%4d => %4d %4d %4d", e,
            adc::read(1), adc::read(1), adc::read(1)); // A0 is ADC1 channel 1
    }

    while (true) { led.toggle(); msWait(500); }
}

Sample output, with different DAC values and three quick ADC conversions:

dac2adc: STM32G431xx @ 170 MHz (v7.0.9-13-gc89bf101)
   0 =>   11    0    0
1000 =>  960  927  931
2000 => 2016 1977 1982
3000 => 3087 2968 2969
4000 => 3936 3936 3936
4095 => 3983 3992 3992
4000 => 3936 3936 3936
3000 => 2951 2972 2969
2000 => 1927 1982 1980
1000 =>  927  927  931
   0 =>    0    0    0

It works: the DAC voltages are converted back to digital by the ADC. The measurements are a few percent off, and it looks like the top value is never reached. The DAC buffer is probably not strong enough to drive an ADC with a minimum sample time (it needs a low-impedance input to quickly charge/discharge the sample-and-hold capacitor).

Warning

With fastClock() in defs.hpp disabled, the results are a bit different. All ADC readings are now much closer to the expected values:

dac2adc: STM32G431xx @ 16 MHz (v7.0.9-13-gc89bf101)
   0 =>   45   47   47
1000 =>  997  998  997
2000 => 1997 1996 1997
3000 => 2997 2997 2997
4000 => 3997 3998 3997
4095 => 4057 4057 4058
4000 => 3999 3999 3999
3000 => 2998 2998 2998
2000 => 1997 1997 1997
1000 =>  997  997  997
   0 =>   48   47   47
There's definitely something going on at higher clock speeds ...

Continuous DAC output

For more tests involving the ADC, a stable signal of some kind would be useful. This can be generated with the DAC and a DMA channel in circular buffer mode to continuously feed it. A hardware timer can set the pace at which the DMA feeds new values to the DAC.

But first I need a sine wave ...

CORDIC

For the sake of using as much of the µC's built-in hardware as possible, I'm generating the sine wave using the built-in CORDIC hardware. From the reference manual:

The CORDIC coprocessor provides hardware acceleration of mathematical functions (mainly trigonometric ones) commonly used in motor control, metering, signal processing, and many other applications.

cordic.cpp
#include <jee.h>
#include <jee/hal.h>
using namespace jeeh;
#include "defs.hpp"

int main () {
    initBoard();

    cordic::init();

    for (auto i = 0; i <= 0x10000; i += 0x1000) {
        auto v = cordic::sine(i);
        logf("%6d %*c", v, v/1024+32, '+');
    }

    while (true) { led.toggle(); msWait(500); }
}

Sample output:

cordic: STM32G431xx @ 170 MHz (v7.0.9-14-g6699c955)
     0                                +
 12539                                            +
 23170                                                      +
 30273                                                             +
 32767                                                               +
 30273                                                             +
 23170                                                      +
 12539                                            +
     0                                +
-12539                    +
-23170          +
-30273   +
-32767 +
-30273   +
-23170          +
-12539                    +
     0                                +

Yep, it's a sine wave. And it's in full 16-bit resolution, more than enough for a 12-bit DAC.