gspr

Gadila Sai Prashanth Reddy

I build hardware that thinks, and I measure it.

B.Tech ECE · Shiv Nadar University · FPGA ML acceleration · embedded BLE · real-time audio DSP

01 / 07 FPGA ML acceleration · IIT Hyderabad, May–Jul 2026

33,497 inferences a second on a ZCU104

measured on the board

I built end-to-end CNN inference on a Xilinx ZCU104 (xczu7ev): training, then HLS and quantization, then a custom IP in the programmable logic, then benchmarking on the board.

33,497 inf/s
custom IP vs the Vitis AI DPU's 10,727 in the same matched-twin session: 3.1×source: measured on the board
4.4×
board-level energy efficiency: 3,177 vs 719 inferences per second per watt, separate power sweepsource: measured on the board
~30% · 64 · 16
LUTs · DSP slices · BRAM used by the custom IP

Custom IP: 33,497 inf/s against the DPU's 10,727 in the same session (3.1×). About 30% LUT, 64 DSP, 16 BRAM; 4.4× the DPU's inferences per watt.

Full story

Constraints

It began on an RFSoC 4x2 and moved to the ZCU104 so both toolchains build reproducibly on the free Vivado tier.

Investigation

To compare hls4ml and Vitis AI fairly I used a matched-twin method. The model, weights, board and test set are identical; only the toolchain changes. I ran it across three deployment pipelines.

held equaltwin Atwin B
model=
weights=
board= ZCU104
test set=
toolchainhls4mlVitis AI

Resolution

The custom AXI-DMA streaming IP keeps every layer on the PL. With K-sample TLAST batching it reached 33,497 inf/s against the Vitis AI DPU's 10,727 in the same matched-twin session: 3.1×, in about 30% of the LUTs, 64 DSP slices and 16 BRAM.

Consequences

In a separate power sweep it delivered 3,177 inferences per second per watt to the DPU's 719: 4.4× the board-level energy efficiency.

02 / 07 RISC-V + partial reconfiguration · PYNQ-Z2

A RISC-V core whose accelerator swaps in 2.5 ms

measured on the board · latest 2026-10-311 open issue

A small single-cycle RV32I core on a PYNQ-Z2 (Zynq-7020) reaches a custom function unit through four memory addresses: ordinary stores and loads, no new instructions. That unit sits in a reconfigurable partition, a fixed patch of the FPGA that can be rewritten while the rest of the chip keeps its configuration. Two modules fit the same frozen ports: a 32×32 multiplier and a popcount/bitwise unit.

2.50 ms
partial swap of the 111,105-byte bitstream through the Linux driver, against 66.3 ms for the full 4,045,672-byte bitstream (25.9×)source: medians of 20 swaps per tier, build B, run 2 (THEORY_SHEET.md / WEEK4_HARDWARE.md)
98 of 7,351
swaps ignored in the overnight soak: 1.33 %, 95 % CI 1.10–1.62 %source: soak/summary.json, on the board 2026-10-13/14
729 → 161
cycles for one benchmark, software loop vs through the slot (4.5×), 40 MHz buildsource: WEEK6_USABLE.md, on the board 2026-10-31

What went wrong

My predictions were wrong in useful ways. I expected about 0.8 ms from the configuration port's bandwidth; the Linux driver's fixed cost per load makes it 2.5 ms, and PYNQ's Python layer 62 ms. Halving the partition's height saved nothing, because a configuration frame is a whole clock region tall; halving its width halved the file. And asking Vivado for 50 MHz gave 31.25 MHz on the board until I pinned the PLLs.

A single-cycle RV32I core drives a custom function unit through four memory addresses. The unit sits in a reconfigurable partition that holds either a 32×32 multiplier or a popcount/bitwise unit. Swapping it loads a 111,105-byte partial bitstream in 2.50 ms through the Linux driver, against 66.3 ms for the full 4,045,672-byte bitstream (25.9×). The same program then reads 2 instead of 42.

Pblock sweep (multiplier): 12 columns full height 220,989 B; 12 columns half height 220,989 B; 8 columns plus one DSP column 111,101 B. Clock: the IP assumed a 1600 MHz IO PLL, the board boots at 1000 MHz, so 50 MHz came out as 31.25 MHz until the PLLs were pinned.

A colour clip, decoded by the core

Press Play, then Swap the slot. The picture needs the bitwise unit's XOR, so with the multiplier in the slot it breaks. This runs in your browser on the page's own RV32I model. The 96×64, eight-colour decoder has not run on the board yet; the board played an earlier one-bit clip on 31 October.

Open the full demo ↗

Full story

Investigation

One program runs on both. Swap the slot and its answer changes from 42 to 2, with nothing else touched. Simulation passes 14 of 14 checks for each module, Vivado's pr_verify confirms both configurations share an identical static region, and every swap on the board is checked by reading memory back.

Resolution

A later build made it a computer you can use. A loader writes new programs into the core with no new bitstream, and a small C toolchain sends * and popcount to the slot, so the same binary computes different things depending on the partial bitstream loaded.

A 64×48 1-bit video decodes on the core by XOR through the slot. Swap in the multiplier and the picture is gone within 0.3 s.

What went wrong, in full

Adding the program loader used up the timing margin. 50 MHz closed at +0.053 ns, and my fix, a smaller instruction memory, made it worse at −0.563 ns: builds at the same constraint landed about ±0.5 ns apart. 40 MHz closes with +2.868 ns, and swap time does not depend on the core clock.

Open: an overnight soak measured the fault. 98 of 7,351 swaps (1.33 %, 95 % CI 1.10–1.62 %) were ignored by the configuration engine with no error from the driver, and each needed a full reload. Direction, load path and the gap between swaps made no measurable difference. The demo detects a miss and recovers. The root cause is not found yet.

The soak ended when a recovery reload failed and left the fabric unconfigured; the board needed a power cycle. Whether that shares a cause with the ignored swaps is untested.

On the 40 MHz build the PYNQ path missed 3 of 20 swaps, well above the soak's 1.77 %, with different status registers. That is unexplained and not re-measured at scale, so the demo uses the driver path, which had no misses in 27.

03 / 07 active studio speakers · real-time DSP

The crossover is code, not copper

v1 completedv2 in build · last verified on hardware 2026-09-16

v1 is two tri-amplified 3-way cabinets (about 110 W per channel: 6.5-inch woofer, 4-inch mid, 1-inch dome) with a hand-wound toroidal-inductor crossover, voiced with an 11-band parametric EQ.

44 biquads
real time at 24-bit / 96 kHz on one 240 MHz ESP32-S3 coresource: custom-speakers amp/README.md, on hardware 2026-09-15
70.9 → 42.7
cycles per biquad after forcing -O3source: custom-speakers amp/DSP.md, measured on the ESP32-S3 2026-08-23
62% → 37.6%
CPU load of one core at 48 kHz, -Os → -O3source: measured (CPU meter in the figure)

What went wrong

At 96 kHz every USB packet was dropped: the ESP32-S3's USB receive FIFO is too small for TinyUSB's default rule, so the firmware now resizes it per interface.

My voicing preset through an LR4 crossover at 2.5 kHz. CPU: 62% → 37.6% of one core at 48 kHz after -O3; 74.9% measured at 96 kHz.

A desk with the two v1 three-way speakers either side of a monitor, with the macropad in front.
v1 on the desk, macropad in front
A v1 speaker baffle with dome tweeter, 4-inch midrange and 6.5-inch woofer.
v1 baffle: tweeter, mid, woofer
Full story

Investigation

A 44-biquad chain runs at 24-bit / 96 kHz on about 75% of one 240 MHz core. Forcing -O3 cut the cost from 70.9 to 42.7 cycles per biquad. I also wrote the USB Audio Class 1.0 device (24-bit, 48/96 kHz, asynchronous feedback) with zero steady-state underruns. USB → DSP → DAC → amp playback was verified by ear on 2026-09-16.

Resolution

v2 replaces the passive crossover with DSP on an ESP32-S3: voicing EQ → Linkwitz–Riley LR4 crossover → per-driver EQ, feeding two I2S DACs split by band into four class-D channels.

Consequences

Into 8 Ω on a 24 V rail the TPA3118 is voltage-limited to about 25–30 W per channel. That, not the drivers, is the system's loudness ceiling.

What went wrong, in full

One DAC gave no output while every counter looked perfect. Its LCK and DIN wires were swapped.

04 / 07 ESP32 BLE HID macropad · v5

A macropad with a face

v3 shipped · v5 verified on hardware

Twelve keys, a 240×320 display and a wireless rotary-encoder puck that talks to the pad over ESP-NOW and takes firmware updates over the air through the pad.

98.24 × 116 mm
case footprint, the size the 3D drawing prints on its dimension linessource: measured from the v5 case STL
20°
display mount tilt, with its M2.5 holes on a 56.5 × 30.67 mm patternsource: measured by slicing v5_display_mount.stl

Play with it

Interactive pad: press its keys to fire the firmware's real face triggers. (Needs JavaScript.)

Drawn from the CAD

Line drawings of the v5 case, switch plate, display bezel and keycaps, rendered from my CAD.

On the desk

The v5 macropad: white 3D-printed case, a 4 by 3 grid of alternating black and white spiral-top keycaps, and a 2-inch display.
v5 on the desk (earlier face firmware)
The wireless AS5600 encoder puck, a white dial, in front of the v5 pad.
The ESP-NOW encoder puck

The macropad, in 3D

This is my macropad.

Every layer designed and printed.

Then it got a face.

Go on, play with it.

Line drawing of the v5 macropad exploded into keycaps, display bezel, top plate, switch plate and case.
Full story

Problem

One pad has to work with several computers, and a dual-boot PC counts as two: Windows and Linux on the same machine have to pair with it as separate hosts.

Constraints

Everything sits in a 3D-printed case 98.24 × 116 mm across: twelve MX switches on a 20 mm grid in a printed switch plate, under a top plate held by M3 countersunk screws.

The 2-inch IPS display module (ST7789, board 61 × 35 mm) stands on M2.5 × 6 standoffs on a printed mount tilted 20° toward you.

Resolution

Three Easy-Switch BLE host slots each get their own Bluetooth address and bond keys, so Windows and Linux on the same dual-boot PC pair as two separate hosts.

The puck is an AS5600 magnetic encoder with 50 detents per turn. On the pad it sets the volume.

The face on the display is drawn from numbers, not stored pictures: 14 expressions, each a set of shape parameters, blended by spring physics on a fixed 30 Hz step. Twelve pad events (a host connecting, a typing burst, a new track) can each trigger an expression, with its own chance and cooldown. The face on this page runs the same expression table and spring physics as the firmware.

MacroPadDeck, the companion app, turns it into a Stream Deck: a shared .NET 9 core with WinRT (Windows) and BlueZ (Linux) front ends over a custom GATT protocol, with per-app presets, now-playing and one-click OTA.

Consequences

v3 ran 1–2 months on one 18650 and used a PCB printed on the university's conductive silver-ink printer.

05 / 07 GPS-denied navigation · hackathon proof of concept

Sightline: mapping and driving without GPS

measured on KITTI + in Gazebono robot hardware yet

The brief: show that a ground robot can map and navigate where GPS stops working, without buying any hardware.

12.2–14.4 m
KITTI 06 APE, C++ core, four runs on 6 Sep (1.0–1.17 % of the path); the 5 Sep reference run scored 49.5 m (4.05 %)source: public repo ugv-camera-slam-sih, README.md and RUNBOOK.md at 599d437
10–11 s
Nav2 goal reached, 2 of 2 runs, after the use_sim_time fix (aborted at 168 s before)source: public repo ugv-camera-slam-sih, RUNBOOK.md §11

What went wrong

Automating the goal exposed a Nav2 bug. The planner ran on wall-clock time while everything else used simulated time, so goals aborted after 168 s with an error that looked like a robot boxed in by walls. Setting the planner's use_sim_time fixed it: 2 of 2 runs reached the goal in 10–11 s.

Videos: monocular SLAM on a phone walk, the same pipeline against ground truth on KITTI 06, and the sparse 3D map. They need JavaScript to switch between.

KITTI 06: each run's estimated path drawn against ground truth, frame by frame, after sim3 alignment. Scrubbing it needs JavaScript.

Top-down, rotated onto the route's long axis with one scale on both axes. Monocular SLAM has no metric scale, so each run is fitted to ground truth with one rotation, translation and scale before its error is measured. KITTI data © Geiger et al., CC BY-NC-SA 3.0.

KITTI 06, C++ core: four runs on 6 Sep scored 12.2–14.4 m APE (1.0–1.17 % of the path); the 5 Sep reference run scored 49.5 m (4.05 %). Python core: 132.7 m. Live Gazebo run: map at about 8 s, goal sent at about 20 s, reached at about 31 s. Planner clock bug: aborted 2/2 at 168 s before the fix, reached 2/2 in 10–11 s after.

Three phone takes run through the same SLAM settings, drawn at one scale. Take A keeps 96.7 % of the clip with no discontinuities; take B collapses to a small knot at 43.7 %; take C keeps 26 % and ends in a tangle.
Three phone takes, identical settings, one shared scale. A, the shortest and lowest-resolution take, is the only one that holds together: steadiness beat both length and pixel count.
Full story

Problem

Sightline was our entry for an overnight university hackathon feeding into Smart India Hackathon, built over 1–6 Sep 2026. The brief: show that a ground robot can map and navigate where GPS stops working, without buying any hardware.

Resolution

It has two halves. Perception on real footage: monocular visual SLAM (pySLAM, C++ core with GTSAM) recovers a camera path and a sparse 3D map from a phone walk. pySLAM wrote an empty trajectory until two source patches went in, and a third saves the map when it runs headless. The autonomy loop, live: in Gazebo, RTAB-Map builds an occupancy grid from the robot's RGB-D camera and Nav2 plans and drives to a goal, with no GPS anywhere in the loop. The whole demo runs from one command and falls back to recordings on its own if the live run fails.

What went wrong, in full

What it does not show: monocular SLAM recovers no metric scale, so the phone map is qualitative. Loop closure never fires; it is traced to the RANSAC Sim3 geometry check but not fixed. The simulated robot uses wheel odometry, so that half is GPS-denied but not camera-only, and simulated depth has no noise. The phone clips were shot before sunrise on a covered walkway and in a corridor, not in the outdoor daylight the brief asked for.

06 / 07 connectome · paused 2026-10-13

A fruit-fly brain on my GPU

stages 0–2 passedstage 3 of 6 · gate failedrobot stage · gate failed

The FlyWire connectome maps every neuron and synapse in an adult fruit-fly brain: 138,639 neurons and 54.5 million synapses. Following Shiu et al. (Nature 2024), I run it as leaky integrate-and-fire neurons, so the wiring is the whole model and nothing is trained.

Jaccard 1.0000
GPU port against Brian2 on 17,193 spikessource: my runs of the model
8.1 s vs ~230 s
per 30-trial experiment, GPU port vs Brian2 (28×)source: my runs of the model
p = 0.004 · p = 0.075
CartPole: fly wiring beat random, not the echo-state network, over 5 seedssource: my runs of the model

What went wrong

The viral "fly brain plays a game" demos freeze the wiring and train a small readout, so the readout does the learning. My rule: the fly's wiring has to beat a degree-matched random network and an echo-state network over 5 seeds. On CartPole it beat random (p = 0.004) but not the echo-state network (p = 0.075). The pre-registered test failed, and that report stands.

In the fly-brain model, driving the 21 sugar-taste neurons at 200 Hz makes the feeding motor neuron MN9 fire at 94.5 Hz and lights up 452 neurons. At 10 Hz only 49 respond and MN9 doesn't fire. Sugar plus water drives MN9 at 23.6 Hz, more than either alone; adding bitter cuts it to 10.6 Hz.

GPU port: identical spikes to Brian2 (Jaccard 1.0000 on 17,193 spikes), 8.1 s vs about 230 s per experiment (28×). CartPole: fly wiring 27.6–171.7 vs random 9.3–13.3 and echo-state 36.7–49.3 mean episode length over 5 seeds.

Full story

Investigation

I first reproduced the paper's feeding and grooming circuits (7 of 8 pre-set checks pass, one near miss kept as a fail). Then I ported the model to sparse PyTorch on my laptop GPU. The port matches the Brian2 reference spike for spike and runs 28× faster.

What went wrong, in full

Then I found a flaw in my own method. Contiguous cross-validation folds were biasing the readout's fit. With whole-episode folds, a plain linear readout with no network at all reaches 500 of 500, past the pre-set void bar of 400. So the task cannot separate networks, and scientifically this stage is inconclusive.

So I set a task that needs memory: the connectome drives a simulated Gazebo robot up an odour plume. That gate failed too. The fly reached the source in 31, 19 and 21 % of episodes (with a readout chosen after seeing stage 3), against 66 and 81 % for the echo-state network and 41 % for input alone. Two echo-state seeds beating all three fly seeds already rules out a pass (best case p = 0.111), so I stopped the run there. In a separate ungated diagnostic on a numpy twin of the task (two other seeds), feeding the wind into the fly's own wind sensors made it worse: 7 and 2 % against 18 and 18 % for the same setup without that change.

07 / 07 codec tested on a laptop · BLE link not built

Hi-res BLE audio

codec round trip on a laptop · 2026-09-01BLE link designed, not built

LC3plus at 24-bit / 96 kHz over BLE L2CAP, gated on a sustained 600–768 kbps throughput test before any audio work.

46.0 dB
aligned SNR, 96 kHz / 24-bit stereo round trip at 320 kbps, 0 samples clippedsource: hires-bt-lc3plus PHASE0_RESULTS.md, 2026-09-01
400 B
stereo frame at 320 kbps and 10 ms, parsed from the bitstreamsource: hires-bt-lc3plus PHASE0_RESULTS.md, 2026-09-01
Full story

Investigation

The first phase tested the codec alone, on a laptop. LC3plus (ETSI TS 103 634 V1.7.1) built and passed its conformance vectors, which stop at 48 kHz. A separate 96 kHz / 24-bit stereo round trip at 320 kbps came back at the same length with 0 samples clipped, no lag and 46.0 dB aligned SNR. That is a health check, not a claim of bit-exactness.

Parsing the bitstream showed frames are exactly constant in size, so the packet plan is simple arithmetic: at 320 kbps, 10 ms frames are 400 bytes, one frame per packet at an MTU of 512. 192 kbps at 2.5 ms cannot be encoded, so any bitrate ladder has to avoid that corner.

Consequences

No audio has gone over BLE. These results size the transport; they say nothing about the radio link.

readme

From a trained model to a board that runs it

I'm a B.Tech Electrical & Computer Engineering student at Shiv Nadar University (2024–2028). My research is FPGA acceleration of neural networks with Prof. Amit Acharyya's group at IIT Hyderabad, supervised by Dr. Rakesh Palisetty. I take a CNN from training through HLS and quantization to a custom IP on a Xilinx ZCU104, then benchmark it on the board.

Outside the lab I build the hardware I use every day: a multi-host BLE macropad with its own companion app, and active studio speakers whose crossover is code instead of copper.

The little face following you around this site lives on that macropad's screen. It's the same expression table and spring physics as the firmware, running in your browser.

open a connection

Let's build something

I'm open to research collaborations, internships and interesting hardware problems, especially FPGA / ASIC design, edge AI hardware, embedded systems and audio DSP.

gs663@snu.edu.in

pad · 7 chapters