An open-source, ECC-based internal scrubber for Xilinx UltraScale FPGAs.
Mr. Scrub is a small RISC-V soft core (Minimax, ~2k LUTs) that reads
configuration frames through ICAP, uses the FRAME_ECCE3 primitive to detect
upsets, and corrects single-bit errors in place. It is designed to be dropped
into a larger FPGA design as a TMR'd sub-module.
This code was developed and deployed on Vivado 2023.2, though the open-source release has been re-targeted to Vivado 2025.1 for portability. No simulation harness (just a GUI launcher) or top-level RTL is included here -- this is an "over-the-fence" release.
You are welcome to contact me (gsmecher@threespeedlogic.com) with questions.
Makefile Top-level build: firmware + TMR'd EDIF
rtl/
scrubber.vhd Top-level: BRAMs, ICAPE3, FRAME_ECCE3, streaming FIFO, testbench
minimax.v RVC-optimized RISC-V core (https://github.com/gsmecher/minimax)
src/
scrubber.c Main loop: ECC decode, keyhole command dispatch, upset notify
hw.c ICAP driver primitives
util.S Hand-tuned assembly (tx_stream, popcount, ctz, deinter4, ...)
microcode.S Minimax microcode ROM (linked into the firmware image)
scrubber.h Register map and function prototypes
crt0.S Startup + BSS/data relocation
bare.ld Linker script (matches the BRAM layout in scrubber.vhd)
tcl/
export_edif.tcl Out-of-context synthesis -> EDIF
update_mmi.xslt Splits the generated MMI for three TMR'd BRAM copies
scrubber_tb.tcl Simulation launcher
bin/
apply_tmr.py SpyDrNet-TMR voter insertion
bin2mem .bin -> .mem converter
spydrnet/ BYU SpyDrNet (git submodule)
spydrnet-tmr/ BYU SpyDrNet-TMR (git submodule)
- Vivado: 2025.1
- Python. SpyDrNet and
SpyDrNet-TMR are vendored as git
submodules; fetch them after cloning with
git submodule update --init. - Saxon-B (
saxonb-xslt) for MMI transformation.
From the repo root:
make
make simgui
make clean
The default target produces the two artifacts you integrate into a parent FPGA project:
scrubber_tmr.edif: triplicated netlist; add to your project as a source.scrubber.mem: firmware image; used downstream byupdatememto patch the post-P&R bitstream without re-running synthesis.
The default make output gives you two artifacts to drop into a parent
FPGA project; everything downstream of that is design- and
board-specific, and the Makefile deliberately stops there. This section
sketches the full integration flow so you can script it yourself.
1. Instantiate the scrubber in your parent project. Add
scrubber_tmr.edif as a synthesis source so the triplicated netlist
is linked in, and instantiate the scrubber entity (from
rtl/scrubber.vhd) at the point in your hierarchy where you want it.
Wire the control, alert-stream, and flash ports to your host and
configuration flash.
2. Place and route as usual. Vivado emits a .bit and, with
write_mem_info, a .mmi describing the layout of BRAMs containing
memory-initialization data.
3. Patch the firmware into the bitstream with updatemem. Because
the scrubber's ROM is triplicated, the MMI lists it as a single
MemoryArray with three BRAM children; updatemem needs to see it as
three separate arrays. tcl/update_mmi.xslt performs that split:
saxonb-xslt -xsl:tcl/update_mmi.xslt \
-s:parent_project.mmi \
-o:patched.mmi \
instpath=<path/to/scrubber_rom/xpm_memory_base_inst> \
part=xcku060-ffva1517-1-c
instpath is the hierarchical path to the scrubber's
xpm_memory_base_inst ROM as it appears in your parent project's
synthesized hierarchy.
Then invoke updatemem with one -data/-proc pair per TMR copy:
updatemem -force -meminfo patched.mmi \
-bit parent_project.bit -out patched.bit \
-data scrubber.mem -proc <instpath>-1 \
-data scrubber.mem -proc <instpath>-2 \
-data scrubber.mem -proc <instpath>-3
The patched bitstream contains the updated firmware without re-running
synthesis or P&R. Generate a flash image (.mcs) from it with
write_cfgmem in whatever geometry your board expects.
The shipped RTL targets the Kintex UltraScale KU060 (xcku060-ffva1517-1-c).
Porting to another UltraScale part requires:
- Updating
PARTin the Makefile. - Updating the IDCODE check in
scrubber.c(currently0x03919093for KU060, per UG570 Table 1-5). - Re-confirming the ECC/SECDED syndrome decoding in
calculate_ecc(), which follows the UltraScale configuration-frame layout.
The scrubber exposes two host-facing interfaces at the VHDL top:
- Control port (
ctl_*): a host-initiated request/ack interface for keyhole commands, memory inspection, and scrubber-state readout. - Alert stream (
str_*): an 8-bit AXI-Stream used to emit unsolicited packets (state changes, upset notifications). The firmware'stx_state()andtx_notify_upset()wrap an example packet framing; swap in your own framing by redefiningPKT_HDR_*inscrubber.c.
BSD 3-Clause. See LICENSE.
Minimax (rtl/minimax.v, src/microcode.S) is separately copyrighted by
Three-Speed Logic, Inc. and Sean Cross; it is vendored here under its
original terms.