Inspiration
Real trading firms put their fastest decision logic in FPGAs, because hardware reacts in a fixed, tiny number of clock cycles with no operating system in the way. We wanted to build a small version of that ourselves: a complete trading-signal engine with no CPU and no software, just logic on a $25-class FPGA board, and then see how small we could make it without losing a single correct answer.
What it does
A host PC sends 8-byte requests over UART (115200 baud), each carrying a price for two items. For each item the FPGA keeps the last 16 prices and their moving average, and answers every request with an 8-byte response: BUY, SELL, or the previous action if nothing changed.
With \( S \) the sum of the last 16 prices, the average is \( \bar{p} = \lfloor S / 16 \rfloor \) (a 4-bit shift in hardware). For a new price \( p_t \) and the previous price
$$ \text{BUY if } p_{t-1} \le \bar{p}_{\text{old}} \text{ and } p_t > \bar{p}_{\text{new}}, \qquad \text{SELL if } p_{t-1} \ge \bar{p}_{\text{old}} \text{ and } p_t < \bar{p}_{\text{new}} $$
Prices are full 16-bit values (0 to 65535), and the first 16 packets are a warm-up that always answers NONE.
How we built it
- Board and tools: Sipeed Tang Nano 20K (Gowin GW2AR-18), Gowin EDA V1.9.11.03, SystemVerilog, QuestaSim for simulation, Python for tooling.
- Design: five small modules.
uart_rxanduart_txhandle the serial link,pkt_ctrlis a single state machine that tbytes in into two engine commands and 8 bytes out,tem's window, running sum, last price and lastaction. - Version 1 worked first time on the board, bu4 registers. - Version 2 focused on size, since the ranking rewards the lowest logic count:
- The price windows and sums moved into the FPGA's block RAM, which doesn't count toward logic.
- The engine became bit-serial: it processes one bit per clock instead of using full-width adders and comparators. That's slower in clock cycles, but still nothing next to UART time.
- The UART and packet timers became LFSRs (shift registers with an XOR) instead of binary counters, which removes theiradders.
- Result: 214 logic cells and 197 registers, down from 386 and 704, with timing met at 27 MHz (Fmax 164 MHz).
- Verification: expected answers never came from our own reading of the algorithm. They came from the organizer's Python reference model. We generated test vectors in fivetremes at 0 and 65535) and replayed them through the full design in simulation with randomized UART timing, using SystemVerilog assertions and required coverage points. Final checks: - 100/100 system-simulation seeds passed.
- 18/18 deliberately injected bugs (mutants) were caught.
- The new UART transmitter was run clock-for-clock in lockstep against the old one to prove its behavior didn't change.
Challenges we ran into
- The USB bridge drops back-to-back bytes. The board's USB-serial chip lost bytes when our responses were sent with no gap, sothe transmitter adds a few idle bit-times after ea it needs on the board instead of guessing.
- Optimizations that made things worse. Synthesis optimizes across module boundaries. When we replaced the UART's shift register with a simpler latch, the UART shrank from 60 to 21 cells, but the packet controller grew by 34, because its byte-select logic had been folded into the shift register for y the total from a full build.
- Noisy numbers. Changing one module shifts other modules' counts by a few cells from build to build, so every claimed saving had to be measured on the whole design.
- Latency. Simulation says a round trip takes about 2 ms on the wire, but the board measures about 17 ms. Almost all of that the PC and USB stack, so we focused on correctnessclock cycles. lity.** The judges rebuild from our repository, so we made the Gowin project the single build description and checked that a fresh clone rebuilds the identical bitstream (only the timestamp differs).
Accomplishments that we're proud of
- 100% correct on the board: 84/84 scored packets and 168/168 actions in every robust and full-range run, with 0 timeouts.
- The design shrank by 45% in logic and 72% in registers between v1 and v2 without changing a single answer.
What we learned
- Measure, don't estimate. Several "obvious" optimizations didn't help, and the biggest win (block RAM plus bit-serial math) came from rethinking where the state lives rather
- Test against an oracle, and test the tests. Mutation testing showed us whether our testbenches would actually notice a broken design.
- Treat the build as part of the deliverable. A design is only finished when someone else can rebuild it from the repo and get the same bitstream.
Built With
- python
- systemverilog
- tcl

Log in or sign up for Devpost to join the conversation.