After getting the FLIR Lepton running on the Forgix, I wanted a demo which needs no additional hardware at all but shows how we can develop projects for the board.
If you have a Forgix and a laptop, you can run this project in a couple of minutes. The laptop captures audio from its microphone, the RP2354 carries the samples to the Efinix Trion T8, and the FPGA performs the spectral analysis in real hardware, returning eight frequency band magnitudes which the laptop plots live. As a bonus, the Forgix RGB LED becomes a tiny Volume Unit (VU) meter, showing bass energy in red, midrange in green and treble in blue.
The point of the project is not that an FPGA is the only way to compute an eight-band spectrum. Your laptop could do it in a microsecond. The point is that it demonstrates the complete Forgix development flow in one small, verifiable design, a VHDL processing engine with self-checking simulations, a runtime SPI transport between the RP2354 and the T8, firmware which layers cleanly on the official Forgix loader protocol, and a host application which ties it all together.
It is exactly the skeleton I would start from for a real signal processing application on this board, and everything is in the repository: RTL, test benches, firmware, host software and build scripts.
Signal PathThe path for each frame is:
- The laptop captures roughly 32 ms of mono audio and resamples it to 8 ksample/s.
- It applies a Hann window, scales to Q15 and sends 256 signed 16-bit samples in a single CRC32 protected USB frame.
- The RP2354 forwards those 512 bytes to the FPGA over the Forgix three-wire SPI link.
- The Trion T8 computes eight DFT bins using one time-multiplexed hardware multiplier.
- The RP2354 reads back a 20-byte response containing the eight 16-bit magnitudes, a frame counter and status, and returns it over USB.
- The laptop converts the magnitudes to dBFS and updates the bar display, while the FPGA drives the RGB LED directly from the same magnitudes.
The eight analyzed bands are centered on 62.5, 125, 250, 500, 1000, 1500, 2250 and 3000 Hz.
With a 256-sample frame at 8 ksample/s these correspond to DFT bins 2, 4, 8, 16, 32, 48, 72 and 96, which gives a musically useful, roughly logarithmic spacing from a strictly linear-resolution transform.
The obvious way to build a spectrum analyzer is an FFT, but an FFT computes all 128 usable bins when I only want eight of them. On a device the size of the T8, with a handful of embedded multipliers, the better trade is a direct DFT evaluated only at the bins of interest, which is essentially the same observation that underpins the Goertzel algorithm.
For each incoming sample, the engine must compute a cosine and a sine product for each of the eight bins, sixteen multiplications in total. At the 32 MHz board clock there are hundreds of cycles available between samples, so there is no need for sixteen multipliers, or even two. A single signed 16x16 multiplier is shared across everything.
A small state machine walks each sample through the eight bins, spending six states per bin: load the cosine coefficient, multiply, accumulate into the real sum, load the sine coefficient, multiply, accumulate into the imaginary sum. The complete work for one sample takes 48 clock cycles, comfortably below the rate at which samples can arrive over USB and SPI.
The twiddle factors come from a 65-entry quarter-wave sine table in Q15 format. An 8-bit phase accumulator per bin steps by the bin number each sample, and quarter-wave symmetry unfolds the table into a full sine and cosine:
function sine_q15(phase : unsigned(7 downto 0)) return signed is
variable p : natural range 0 to 255;
variable offset : natural range 0 to 63;
begin
p := to_integer(phase);
offset := p mod 64;
case p / 64 is
when 0 => return C_SINE_QUARTER(offset);
when 1 => return C_SINE_QUARTER(64 - offset);
when 2 => return -C_SINE_QUARTER(offset);
when others => return -C_SINE_QUARTER(64 - offset);
end case;
end function;Each bin accumulates into 40-bit signed registers, which provides guard bits well beyond the worst-case growth of 256 full-scale products. When the 256th sample completes, the engine sequences through the bins once more to form the output magnitudes. Rather than spend multipliers on a true root of sum of squares, it uses the classic multiplier-free approximation |real| + |imaginary|, whose worst-case error of about 3 dB at 45 degrees is invisible in a bar graph display. Six guard bits are then removed so that a full-scale Hann-windowed tone lands near 0 dBFS, with saturation to full scale if anything overflows. Finally the accumulators and phases are cleared, the frame counter increments and a frame-ready flag is raised.
The engine also keeps score on itself: if a new sample arrives while the state machine is still busy, an overrun flag latches, travels back in the status byte and is displayed as a warning in the host viewer. In practice it never fires, but a real-time design should tell you when its assumptions break rather than silently drop data.
The Forgix routes a three-wire SPI link between the RP2354 and the T8, with a single shared SDIO data line rather than separate MOSI and MISO. The FPGA side implements a compact command protocol on top of it:
0x10followed by 512 bytes delivers 256 little-endian signed PCM samples.0x11reads the 20-byte spectrum response.0x9freads a one-byte design ID,0xA8.
The SPI slave: synchronizes SCK, CS and SDIO through three-stage shift registers into the 32 MHz clock domain; samples data on rising edges; and changes data on falling edges. The interesting part of any shared-data-line protocol is the turnaround. When the FPGA sees a read command it takes over SDIO, but it must ignore the falling edge which terminates the command byte itself before it starts advancing its transmit bit counter; getting that one edge wrong shifts the entire response by a bit. On the other side, the RP2354 releases its drive on the line before clocking in the reply.
The response to 0x11 begins with the design ID and a status byte carrying the frame-ready and overrun flags, then the 16-bit frame counter, then the eight magnitudes in little-endian order. Importantly, the magnitudes and status are snapshotted into holding registers at the moment the command byte is decoded, so the DFT engine can carry on processing the next frame while the previous results are still being clocked out, and the reader can never see a torn, half-updated result.
The RP2354 firmware extends the official Forgix USB loader protocol rather than inventing a new one. The framing, CRC32 protection, sequence numbers and the HELLO, STATUS, START, DATA, END and ABORT commands are exactly the standard loader; the spectrum bridge adds a 0x10 command carrying one 512-byte PCM frame from the laptop, and a 0x90 response carrying the 20-byte spectrum back. Anything that already speaks to the Forgix loader protocol can drive this firmware.
The same pins also lead two lives. During configuration the firmware drives the T8 over hardware SPI0 at 8 MHz, streaming the bitstream with CRC verification and watching for CDONE. Once the FPGA is up, the firmware de-initializes the SPI peripheral and re-registers the identical pins as GPIO, because the runtime link is a three-wire protocol with a mid-transaction direction change on SDIO, which the hardware SPI block cannot do. The runtime transfers are bit-banged, and the firmware confirms the design is alive by reading the ID register before reporting success:
fpga_runtime_prepare();
uint8_t design_id = fpga_runtime_ping();
if (design_id != FPGA_RUNTIME_DESIGN_ID) {
respond_error(frame, ctx, FORGE_ERR_RUNTIME, design_id, "spectrum core did not answer");
return;
}That ping matters. It converts "the bitstream loaded" into "the design I expect is actually running and answering", which is a far stronger statement, and it catches a stale or wrong bitstream immediately rather than through mysterious garbage data later.
Host SoftwareThe host side is a single Python application using pySerial, sounddevice and Matplotlib. On startup it finds the Forgix automatically by USB VID, programs the FPGA image over the loader protocol, pings the design and then enters the streaming loop: capture, resample to 8 ksample/s, Hann window, send, plot. An exponential moving average smooths the bars just enough to be readable without feeling laggy, and a status line shows the FPGA frame counter, the per-frame USB round-trip time and any microphone or FPGA overrun warnings.
Because asking readers to grant microphone permission before they know the hardware works is unkind, there is also a synthetic mode. Running the visualizer with --source sweep sends a logarithmic sine sweep from 62.5 Hz to 3 kHz through the full USB, SPI and FPGA path, and you can watch the peak walk across all eight hardware bins in turn. It proves every element of the chain with no audio hardware involved, and it is the first thing to run on a fresh setup.
The build closes timing at the 32 MHz board clock with 3.27 ns of positive setup slack, using 1, 875 LUT4s, 1, 180 flip-flops and a single embedded multiplier on the Trion T8. Each sample costs 48 cycles of engine time, 1.5 microseconds at 32 MHz, so the DFT result for a frame is ready within a microsecond of the final SPI byte arriving. The live display updates at the USB round-trip rate with the round trip time shown on screen, the overrun flag stays clear, and the RGB LED pulses along with whatever the microphone hears, which is a surprisingly satisfying way to confirm that your audio really is being processed by a few hundred flip-flops on a tiny FPGA.
Wrap UpThis project is the Forgix workflow in miniature: a self-contained VHDL engine proven in simulation before it ever met silicon, a runtime protocol layered on the standard loader, pins that switch personality between configuration and runtime, and a host application that makes the whole thing usable in one command. The techniques it demonstrates — time-multiplexing a scarce multiplier, quarter-wave coefficient storage, snapshot registers to prevent torn reads, and a design ID handshake before trusting a link — scale directly to more serious signal processing on this board and on much larger devices.
The complete project, including the pre-built UF2 firmware and FPGA image so you can run it without installing any FPGA tools, is in the repository below. Grab a Forgix, plug it in, run the sweep, then put some music on.





Comments