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_rx and uart_tx handle the serial link, pkt_ctrl is 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

Share this project:

Updates

Submission history