Story & Motivation
SpectrumAnalyzer was created because, ever since childhood, I loved watching the "jumping bars" move in sync with music on my Technics SC-EH550 stereo system. Unfortunately, modern audio equipment with built-in hardware spectrum analyzers is practically non-existent.
I tested various display solutions—OLEDs, LCDs, etc.—and came to the conclusion that VFD (Vacuum Fluorescent Display) has a unique charm that no other technology can match. Unfortunately, acquiring a high-quality VFD display these days is not easy and requires a fairly substantial financial investment.
The choice fell on the Noritake GU256X64D-3900B VFD so I could recreate a visual shape similar to the classic Technics display. I specifically wanted the pixels not to be too small, which is why I didn't pick the 256x128 version—smaller pixels would have sacrificed that authentic retro vibe.

Technical Architecture & Controller Selection
The display features two interfaces: UART and Parallel. Unfortunately, UART would be too slow to achieve full frame display speeds, so I decided to use the parallel interface.
When selecting a control board, I initially considered the STM32F722, which theoretically should be sufficient to drive the display. However, it quickly became apparent that the biggest bottleneck for computing an 8k-sample FFT is the 256 kB RAM limit. While technically feasible, this was meant to be a hobby project, not a trial by ordeal! :D
Many people's first thought would be a Raspberry Pi. However, writing a driver to control the parallel port on a Pi would be problematic. Standard GPIO bit-banging is far too slow (on a Raspberry Pi 4B using `libgpiod`, I achieved around 3 µs per access). Even if you manage to achieve much lower latency, thread preemption in Linux remains a major issue. Resolving this would require writing a custom kernel driver that utilizes a hardware block to drive the parallel interface—ideally with DMA—which, given the limited hardware documentation, would be a challenge. Another alternative could have been using an external microcontroller to drive the VFD and receiving data over SPI.
Ultimately, I decided to test the STM32MP257, which has no memory constraints and presents a very interesting multi-core architecture: Linux runs on the Cortex-A35 cores, while a 400 MHz Cortex-M33 microcontroller is available on the exact same chip.
Software Implementation & Dual-Core Pipeline
Since I already had an open-source C++ application that computes FFT (https://github.com/sylwekkominek/SpectrumAnalyzer), I decided to reuse it.
I modified the application so that the rendering/drafter thread, instead of drawing to a computer screen via OpenGL, opens `/dev/rpmsg0` and sends the computed frequency-domain data directly to the Cortex-M33 core. The M33 core runs the VFD display driver and controls the parallel port bare-metal, completely without an operating system.
This approach solves two critical problems:
1. Deterministic Display Timing: Without an OS, the bare-metal M33 driver has plenty of time for fully deterministic display control. As a result, the display operates buttery smooth without any stutters or delays that would otherwise occur due to Linux thread preemption if I were toggling GPIOs directly from Linux without a hardware DMA controller.
2. Offloaded Computation: All heavy lifting (FFT calculations) is performed on Linux. Audio capture is handled by listening to the system loopback audio. In my setup, audio is streamed from my phone to the device via Bluetooth, and sound output goes to an external USB sound card. This way, I don't have to worry about complex audio processing logic.
Inter-Core Communication (RPMsg / OpenAMP)
Communication between the Cortex-A35 and Cortex-M33 cores is relatively simple to set up using ST's example code.
On the Linux side, you open a virtual UART / RPMsg character device (`/dev/ttyRPMSG`) and send data in packets, which land on the M33 side and trigger an interrupt from the interrupt controller upon arrival.
It is important to keep in mind that there is a predefined FIFO queue with a fixed depth and buffer size. In my case, the queue buffer holds 16 messages of 512 bytes each. This constraint is important as it dictates how data must be framed and sent. For my setup, this posed no issue at all, since I only transmit 25 frequency bar values 100 times per second.
Hardware Synchronization & Pin Ownership
Because GPIO pins can be assigned to either the Cortex-A35 (Linux) or the Cortex-M33 core, resource synchronization must be handled carefully.
On the Cortex-M33 side, calling ST library functions makes it easy to check whether a specific GPIO port/peripheral is free or currently locked by Linux. Once confirmed free, the M33 core can safely drive and reset the VFD hardware.
More information about the open-source application can be found here:
Sylwester