AdaCore: Build Software that Matters
Global communication network concept.
Sep 24, 2026

Ada-FPGA-Programmer: Configuring a Tang Nano 9K from Bare-Metal Ada, Built by Students

Four Rose-Hulman Institute of Technology students, Ellie Harshany, Kaden Nutter, Bill Scholten, and Connor Witkemper, built an open-source FPGA programmer in Ada.


The programmer runs on an STM32F070RB Nucleo. It configures the Gowin GW1NR-9C FPGA on a Tang Nano 9K with a NEORV32 RISC-V processor, then uploads a program to that processor inside the FPGA. The whole operation is driven from a plain Linux terminal.

That sentence crosses several layers of computing and two separate hardware boards, so let us first separate them.

An FPGA does not execute a program in the usual CPU sense. It contains logic that can be configured into a circuit. The file describing that circuit is called a bitstream. On an SRAM-based FPGA such as the GW1NR-9C, the configuration is volatile. Turn off the power, and the circuit disappears.

The circuit can include a processor. NEORV32 is a complete RISC-V CPU described in VHDL. Loading the bitstream therefore turns the blank FPGA fabric into a machine containing a CPU. That CPU can then receive and execute ordinary compiled firmware.

There are 2 separate boards in this project: the Tang Nano 9K, which has the FPGA that needs programming, and the STM32F070RB Nucleo, which performs the programming and is connected to the host computer running Linux. The students' task in this project was to make the entire chain work.

There are three distinct artifacts in this project:

  1. The Ada firmware is running on the STM32 microcontroller to perform the loading;
  2. The FPGA bitstream, which instantiates the NEORV32 processor;
  3. The RISC-V firmware executed by that processor.

In normal use, the host sends the last two through the first. This is what makes the project interesting. It exposes the entire path from a Linux terminal, through a bare-metal Ada program, into FPGA configuration logic, and finally into a processor that did not exist before the bitstream was loaded.

The Tang Nano 9K is inexpensive enough to teach this stack without turning the hardware budget into the project. What was missing was the glue: an open and understandable way to perform these operations without hiding everything behind vendor tooling.

Through the GNAT Academic Program, AdaCore sponsored the capstone, Ahmed Sammoud advised the team, and I acted as project mentor. There is still some work to finish to validate the programmer on the actual hardware board, but the team has done fantastic work. They had to make quite a few design decisions; the following blog post walks through these in greater detail.

Keep the host dumb

The design objective was simple. The host should know almost nothing.

Set the serial port to the correct baud rate, then send the file. All knowledge of the programming protocol lives in the STM32. The Nucleo receives bytes through its USB serial connection and drives the FPGA programming pins on the other side. All the logic is encapsulated in an Ada program that runs baremetail on the STM32 board; the Ada program is built using Alire.

This is a good boundary. The Linux side does not need a Gowin driver, a JTAG library, or a special application. The microcontroller owns the protocol because it also owns the pins and the timing. This is the innovative part: with many other solutions that tackle FPGA programming, you need software on the host's side or complex vendor tooling. With this configuration, the host just needs to communicate over a serial port.

JTAG is just a state machine

The FPGA is configured through JTAG. Most embedded developers have used JTAG. Far fewer have implemented it.

JTAG, formally IEEE 1149.1, uses four signals:

  • TCK provides the clock;
  • TMS selects the next state;
  • TDI carries data into the device;
  • TDO carries data out.

Inside the device is the TAP (Test Access Port). The TAP is a 16-state machine. On every TCK edge, it reads TMS and moves to another state. The right sequence leads to one of two important shift paths. In Shift-IR, the incoming bits select an instruction such as reading the device ID, erasing configuration memory, or starting configuration. In Shift-DR, the incoming bits provide the data for that instruction.

That is the protocol: a state machine and two shift registers.

Anything capable of controlling four GPIO pins can drive it. The direct implementation is called bit-banging. Software sets TMS and TDI, pulses TCK, reads TDO, and repeats.

The team's Ada firmware does exactly this for the control sequence. Six clocks with TMS high return the TAP to reset from any state. The firmware then walks through Select-IR and Capture-IR, shifts an 8-bit Gowin instruction, and raises TMS on the last bit to leave the shift state. From there, it follows Gowin's configuration sequence: read status, enable configuration, erase the FPGA's SRAM, initialize it, then stream the bitstream through Shift-DR.

Straightforward, but potentially very slow.

Bit-bang the protocol, use SPI for the data

Bit-banging a few command bits is fine. Bit-banging an entire FPGA bitstream, one GPIO operation at a time, is not.

The team noticed that once the TAP is in Shift-DR with TMS held low, the interesting part of JTAG temporarily disappears. What remains is simply this: put a bit on a wire and pulse a clock.

That is also what SPI hardware does.

So the firmware enters Shift-DR using GPIO, then transfers ownership of the same pins to the STM32 SPI peripheral. TMS remains low on a separate GPIO, so the TAP cannot leave Shift-DR, while SPI streams the payload at 12 MHz without software toggling every bit.

Near the end of the transfer, the firmware gives the pins back to GPIO. It bit-bangs the final byte and raises TMS on the final bit, leaving Shift-DR exactly when required.

The peripheral provides throughput. GPIO provides precise control of the state machine.

This is my favorite part of the design. The team did not treat "JTAG" and "SPI" as indivisible protocol boxes. They examined the electrical behavior of a JTAG state, recognized that it matched a capability already present in the microcontroller, and changed pin ownership mid-operation. That is sharp embedded engineering.

Silence marks the end

The host sends a raw file through the serial port. There is no header carrying its length, so the microcontroller does not know in advance how many bytes will arrive.

Incoming bytes are captured with DMA into a circular buffer. DMA allows the UART to write to memory without interrupting the CPU for each byte. The Ada firmware follows the DMA write position and drains new data as it appears.

Once data starts flowing, an unchanged write position for a short period indicates the end of the file.

The firmware also keeps the newest byte in reserve. Every byte except the last can be transmitted over SPI with TMS held low. The reserved final byte must be sent through GPIO because its final bit also carries the TMS transition out of Shift-DR.

Silence provides the delimiter. Holding back one byte makes the JTAG exit possible. No framing bytes need to be inserted into the payload.

Then the microcontroller becomes a wire

Once the bitstream is loaded, the FPGA contains a running NEORV32 processor. The Nucleo's job changes completely.

NEORV32 includes a UART bootloader. This is a small resident program that accepts an executable over a serial connection and starts it. The Nucleo lowers its host-facing UART to the bootloader's 19,200 baud rate, sends u to select upload mode, and begins forwarding traffic between its two serial ports.

Bytes from the Linux host go to the FPGA. Replies from the FPGA go back to the host. The Nucleo detects the end of the executable using the same silence-based rule, then sends it to execute it.

At that point, the terminal is talking to a RISC-V processor that did not physically exist a few moments earlier.

Do not debug the specification against silent silicon

The most professionally interesting part of the repository may not be the programmers themselves. It may be the test rigs.

FPGA programming specifications are exact, and the physical device is a terrible teacher. Gowin requires specific TAP transitions, a mandatory 4 ms pause between erase and initialization, and strict handshake and chip-select behavior for its alternative SSPI programming interface. When any of this is wrong, the FPGA often responds by doing nothing.

You can attach a logic analyzer, stare at waveforms, and try to infer which rule you violated. Or you can build a device that tells you.

The team built two such devices on an MSP432 LaunchPad. One impersonates the Gowin FPGA over JTAG. The other impersonates it over SSPI. Each receives the programmer's signals, checks them against the expected sequence, and reports any violations over UART, with LEDs providing an immediate pass-or-fail indication.

The JTAG rig implements the complete 16-state TAP. It checks reset, IDCODE, erase, initialization, and write operations, and returns the expected Gowin device ID. The SSPI rig checks the erase-to-initialize delay and the rules governing burst writes.

The real FPGA can say only "it worked" or "it did not." The emulator can say where the conversation stopped making sense.

This does not prove that every electrical and timing detail will work on the target board. It does something more useful than pretending otherwise: it removes protocol errors from the list of mysteries before the real hardware enters the loop.

Ada without a superloop full of flags

The firmware does not use a large HAL, and it is not organized as a superloop polling a pile of global flags.

Peripheral access goes through Ada packages generated from the microcontroller's SVD description. Hardware registers are represented as typed records. Driving a pin low looks like this:

GPIOA_Periph.BSRR.BR.Arr (Pin) := 1;

There is no handwritten shift, mask, or magic hexadecimal constant obscuring which field is being changed. Ada handles these types of concepts more elegantly compared to C, C++, or even Rust.

The program also uses Ada concurrency directly on the Cortex-M0. One task interprets commands from the host: config, upload, help, and exit. Another task performs the FPGA operations. They coordinate via a protected object that contains a five-state enumeration.

That protected object is the complete shared coordination state. The mutual exclusion is part of the construct, not a convention that every future contributor must remember.

For students learning Ada, this matters. They did not merely reproduce a C superloop with Ada syntax. They used typed register views, tasks, and a protected object because those abstractions match the problem.

What is complete, and what is not

The repository is honest about its current state.

The serial JTAG programmer is complete and tested on the hardware. The SSPI implementation is complete and has been checked with a logic analyzer, but it has not yet been validated against the physical GW1NR-9C. The command-interpreter variant still needs changes on the host-terminal side to transfer data. The original plan also included having the programmer enumerate as its own USB serial device. For now, communication goes through the Nucleo's built-in debug interface.

The repository also keeps the intermediate work. Its supplementary_work directory contains the blinky, JTAG, SPI, and USB experiments that led to the final design. This is not garbage to hide before delivery. It is a map for the next person who needs to understand or extend the system.

Those limitations do not diminish the project. They define its actual boundary.

The students still had to read a 16-state protocol from a standard, drive it through register definitions they generated, exploit the equivalence between JTAG shifting and SPI to obtain useful throughput, coordinate the firmware with Ada tasks, and build specification-checking emulators before trusting real hardware.

More importantly, they had to decide where every responsibility belonged:

  • The host moves files;
  • The STM32 owns the protocol and timing;
  • SPI moves the large payload;
  • GPIO controls the exceptional transitions;
  • The emulator explains protocol failures;
  • The real FPGA confirms the physical implementation.

This is exactly what I want from a GNAT Academic Program project. Not a toy exercise wearing an embedded label. A real system, with real interfaces, real failure modes, and engineering decisions that can be explained and defended.

Continue the work

The AdaCore FPGA Programmer is open source. There are several concrete ways to extend it: validate the SSPI path on physical hardware, complete the USB device support, improve the host-side command interface, or adapt the emulator and programmer to another FPGA family.

Explore the project on GitHub, and learn more about the GNAT Academic Program.

Congratulations to Ellie Harshany, Kaden Nutter, Bill Scholten, and Connor Witkemper, and thank you to Ahmed Sammoud for advising the project.

FAQs

AdaCore sponsors a limited number of capstone projects at various universities per year. Feel free to reach out to your capstone director and point them to this article, and have them reach out.

For capstone projects, we typically use readily available, affordable hardware. You should be able to find these boards at your usual retailers.

Capstones rely on the open source Ada toolchain and the Alire package manager. You can download from here. The work of the academic projects is typically provided in open-source projects; links are provided in the text above.

Author

Olivier Henley

Portrait 2

The author, Olivier Henley, is a UX Engineer at AdaCore. His role is exploring new markets through technical stories. Prior to joining AdaCore, Olivier was a consultant software engineer for Autodesk. Prior to that, Olivier worked on AAA game titles such as For Honor and Rainbow Six Siege in addition to many R&D gaming endeavors at Ubisoft Montreal. Olivier graduated from the Electrical Engineering program at Polytechnique Montreal. He is a co-author of patent US8884949B1, describing the invention of a novel temporal filter implicating NI technology. An Ada advocate, Olivier actively curates GitHub’s Awesome-Ada list

Blog_

Latest Blog Posts