How I’ve turned a Picoscope USB oscilloscope into an MCA



A few months ago I purchased a PicoScope 2204A USB oscilloscope purely for my teaching duties. I would plug it into my laptop, project live waveforms onto the classroom screen, and use it to illustrate analog signals in real time. One day I came across Dr. Max Fomitchev-Zamilov’s MCA kits on eBay, which turn a PicoScope into a multi-channel analyzer via a .NET interface and the PicoSDK. I thought it would be a fun experiment to reproduce that functionality in Python.

In practice it proved fairly straightforward, but I discovered two important hardware limitations:

  • 8-bit ADC – with only 256 quantization levels, the spectral resolution is coarse.
  • Quirky hardware trigger – setting a threshold that works at one voltage range (e.g. 40 mV in ±500 mV) may fail at another (±1 V), because the internal DAC or comparator resolution isn’t fine enough to generate the trigger voltage accurately. In such cases raising the threshold (e.g. to 80 mV) restores reliable triggering.

Below I sketch the core acquisition algorithm for pulse spectroscopy, and then describe the Python GUI implementation.

Acquisition Algorithm

  • Initialize
    Record the start time and zero the event counter.
  • Read User Settings
    • Full-scale input range (volts)
    • Lower-level discriminator (LLD) threshold (volts)
    • Polarity (“Positive” or “Negative” pulses)
    • Time-per-division (s/div)
    • Number of samples per block
  • Configure Scope
    Set the selected channel A range and coupling (AC for MCA), then apply the hardware trigger at the LLD threshold with the chosen edge.
  • Acquire Waveform
    Call run_block(), which:

    • Queries the timebase to get the sampling interval
    • Arms the scope and waits for the block to complete
    • Reads back a buffer of raw ADC counts
    • Converts counts to millivolts and then to volts
    • Returns time (t) and voltage (v) arrays
  • Detect Pulse Peak
    If polarity is positive, take peak = max(v); if negative, take peak = –min(v).
  • Compute Histogram Bin
    Normalize: raw_idx = (peak / full_scale_range) * 255
    Clamp: idx = min(max(int(raw_idx), 0), 255)
  • Update Statistics
    Increment histogram[idx], increment the total event count, then compute elapsed time since start and the event rate (events/sec).
  • Emit Updates
    • Send the raw waveform to the GUI for a “last-pulse” preview
    • Send the bin index to increment the bar or line histogram
    • Send the updated count and rate to the status display
  • Loop
    Continue steps 2–8 until the user stops the acquisition.

Software Structure

  • SimplePicoScope2000
    Wraps all PicoSDK calls: opening/closing the unit, setting channels and triggers, querying timebases, and retrieving blocks of data.
  • AcqThread
    A Qt thread that continuously acquires oscilloscope waveforms and emits them to the main GUI.
  • McaThread
    A Qt thread that runs the pulse-spectroscopy loop described above, building a 256-bin histogram and streaming updates back to the GUI.
  • MainWindow
    A PyQt5 application window with two tabs:

    • Oscilloscope: real-time trace display, noise measurement, and start/stop controls
    • MCA: spectrum acquisition controls, last-pulse preview, and dynamic histogram with rate and elapsed-time readouts

Source code

The source code of the script cannot be posted here.
I’ve published it on GitHub: https://github.com/l-papadopol/PyMCA

Updates
The code is actively under development. I’m trying to improve the pulse acquisition algorithm and also the UI.
I’ve also refactored the code with modularity in mind, splicing the app in various files. Results are promising.