From 3a21c85498ea02f4b963178c5a93d044677fba36 Mon Sep 17 00:00:00 2001 From: matthias wenzel Date: Thu, 16 Apr 2026 08:52:05 +0200 Subject: [PATCH 01/12] start of a working bootloader for bare_metal Signed-off-by: matthias wenzel --- .gitignore | 3 + boards/bare_metal/Makefile | 131 +++++++++++ boards/bare_metal/boardmeta.json | 10 + boards/bare_metal/bootloader.v | 371 +++++++++++++++++++++++++++++++ boards/bare_metal/bootmeta.json | 12 + boards/bare_metal/pins.pcf | 28 +++ 6 files changed, 555 insertions(+) create mode 100644 boards/bare_metal/Makefile create mode 100644 boards/bare_metal/boardmeta.json create mode 100644 boards/bare_metal/bootloader.v create mode 100644 boards/bare_metal/bootmeta.json create mode 100644 boards/bare_metal/pins.pcf diff --git a/.gitignore b/.gitignore index f1af15c..e4039a4 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ synlog.tcl gerbers/ build/ *.pyc +next_serial.txt +_boardmeta_prod.json +bootloader_copy.bin diff --git a/boards/bare_metal/Makefile b/boards/bare_metal/Makefile new file mode 100644 index 0000000..c2c1e92 --- /dev/null +++ b/boards/bare_metal/Makefile @@ -0,0 +1,131 @@ +# bare_metal USB bootloader - Makefile +# based on TinyFPGA BX bootloader build flow + +PROJ = bootloader +PIN_DEF = pins.pcf +DEVICE = up5k +PKG = sg48 + +# Path to TinyFPGA bootloader common Verilog +COMMON = ../../common + +# All source files +SRC = $(PROJ).v $(wildcard $(COMMON)/*.v) + +USER_BITSTREAM ?= bootloader_copy.bin + +# flash memory map + +# 0x000_0000 - 0x000_009f =160b, multiboot header, slot table +# 0x000_00a0 - 0x001_ffff ~124kB, slot 0, bare_metal_bootloader +# 0x001_f000 - 0x001_ffff =4kB, bootmetadata +# 0x002_0000 - 0x003_ffff =128kB, slot 1, user bitstream, hackerstacker_demo by default +# 0x004_0000 - 0x005_ffff =128kB, slot 2, transputers anyone? +# 0x006_0000 - 0x007_ffff ~128kB, slot 3, hooray for transputers +# 0x008_0000 - 0x100_0000 user data, images, fonts, text, sounds, etc + +# UP5K bitstream is always ~104 KB, so we need 2^17 = 128 KB alignment +ALIGN = 17 + +all: $(PROJ).rpt multiboot.bin + +$(PROJ).json: $(SRC) + yosys -p 'synth_ice40 -top $(PROJ) -json $@' $(SRC) + +$(PROJ).asc: $(PIN_DEF) $(PROJ).json + nextpnr-ice40 --$(DEVICE) --package $(PKG) --pcf $(PIN_DEF) --json $(PROJ).json --asc $@ + +$(PROJ).bin: $(PROJ).asc + icepack $< $@ + +# Metadata offset within slot 0 (must be within the 128KB slot, after bitstream) +META_ADDR = 0x1F000 + +# Build multiboot image: +# -p0 = power-on image is slot 0 (bootloader) +# -a17 = each image aligned at 128 KB boundary +# For now, slot 1 is a copy of the bootloader (placeholder). +# Replace bootloader_copy.bin with your user design when ready. +# After icemulti, inject minified bootmeta.json at META_ADDR. +multiboot.bin: $(PROJ).bin bootmeta.json + cp $(PROJ).bin bootloader_copy.bin + icemulti -v -o $@ -a$(ALIGN) -p0 bootloader.bin $(USER_BITSTREAM) + python3 -c "\ +import json; \ +d = json.load(open('bootmeta.json')); \ +blob = json.dumps(d, separators=(',',':')).encode(); \ +assert len(blob) <= 4096, f'bootmeta too large: {len(blob)} bytes (max 4096)'; \ +f = open('$@', 'r+b'); \ +f.seek($(META_ADDR)); \ +f.write(b'\\xff' * 4096); \ +f.seek($(META_ADDR)); \ +f.write(blob); \ +f.close(); \ +print(f' Injected {len(blob)} bytes of bootmeta at $(META_ADDR)')" + +$(PROJ).rpt: $(PROJ).asc + icetime -d $(DEVICE) -mtr $@ $< + +# erase SPI flash (required before 'make prog' on non-empty flash) +# leaving manually as it's time-consuming +erase: + ch341prog -v -e + +# Flash multiboot image via CH341A clip programmer +prog: multiboot.bin + ch341prog -v -w multiboot.bin + +# Flash multiboot image via iceprog (if available) +iceprog: multiboot.bin + iceprog $< + +# User address in flash - must match icemulti -a$(ALIGN) +USER_ADDR = 0x20000 + +# Build a multiboot image with a real user design +# Usage: make multiboot-user USER_BIN=path/to/user.bin +multiboot-user: $(PROJ).bin + icemulti -v -o multiboot.bin -a$(ALIGN) -p0 $(PROJ).bin $(USER_BIN) + +# Flash user bitstream over USB (bootloader must be active) +# Usage: make flash-user USER_BIN=path/to/user.bin +flash-user: + python3 tinyprog_user.py -a $(USER_ADDR) $(USER_BIN) + +# Just boot to user image (no programming) +boot: + python3 tinyprog_user.py --boot + +clean: + rm -f $(PROJ).json $(PROJ).asc $(PROJ).rpt $(PROJ).bin + rm -f bootloader_copy.bin multiboot.bin + +# production: provision one board via CH341 +# reads serial from next_serial.txt, generates a UUID, writes boardmeta +# to security register page 1, flashes multiboot.bin to address 0. +CH341PROG ?= ch341prog +SECREG_PAGE = 1 + +produce_one_board: multiboot.bin + @echo "=== Producing board ===" + @SERIAL=$$(cat next_serial.txt) && \ + UUID=$$(python3 -c "import uuid; print(uuid.uuid4())") && \ + echo " Serial: $$SERIAL" && \ + echo " UUID: $$UUID" && \ + python3 -c "\ +import json, sys; \ +d = json.load(open('boardmeta.json')); \ +d['boardmeta']['serial'] = int(sys.argv[1]); \ +d['boardmeta']['uuid'] = sys.argv[2]; \ +open('_boardmeta_prod.json','w').write(json.dumps(d, separators=(',',':')))" \ + "$$SERIAL" "$$UUID" && \ + echo " Minified: $$(wc -c < _boardmeta_prod.json) bytes (max 256)" && \ + test $$(wc -c < _boardmeta_prod.json) -le 256 || { echo "ERROR: boardmeta > 256 bytes!"; rm -f _boardmeta_prod.json; exit 1; } && \ + $(CH341PROG) -W $(SECREG_PAGE) _boardmeta_prod.json && \ + $(CH341PROG) -L $(SECREG_PAGE) && \ + $(CH341PROG) -w multiboot.bin && \ + echo $$(($$SERIAL + 1)) > next_serial.txt && \ + echo "=== Board $$SERIAL done. Next serial: $$(cat next_serial.txt) ===" && \ + rm -f _boardmeta_prod.json + +.PHONY: all prog iceprog multiboot-user flash-user boot clean erase produce_one_board diff --git a/boards/bare_metal/boardmeta.json b/boards/bare_metal/boardmeta.json new file mode 100644 index 0000000..592176c --- /dev/null +++ b/boards/bare_metal/boardmeta.json @@ -0,0 +1,10 @@ +{ + "boardmeta":{ + "name": "bare_metal", + "fpga": "ice40up5k-sg48", + "hver": "0.1", + "serial": 12345, + "uuid": "00000000-1111-2222-3333-444444444444", + "url": "https://wenzellabs.de/bare_metal" + } +} diff --git a/boards/bare_metal/bootloader.v b/boards/bare_metal/bootloader.v new file mode 100644 index 0000000..b253bed --- /dev/null +++ b/boards/bare_metal/bootloader.v @@ -0,0 +1,371 @@ +// bare_metal USB bootloader - bootloader.v +// based on TinyFPGA BX bootloader +// iCE40UP5K-SG48ITR, 12 MHz crystal +// +// Mechanism: +// Slot 0 = this bootloader +// Slot 1 = user design +// Slot 2 = user design +// Slot 3 = user design +// SB_WARMBOOT S1=0 S0=1 -> boots to slot 1 +// +// btn_ok bypass: +// If btn_ok (active low) is held during the first ~4 ms after config, +// we skip USB entirely and warmboot straight to the user image. +// +// Otherwise we run the TinyFPGA bootloader (USB CDC-ACM + SPI bridge). +// If no USB host sends SOF within ~16 s, we auto-warmboot to user image. +// +// USB pull-up: 1k5 on D+ controlled by USB_DET (pin 37, active high). +// Drive high to enumerate on the bus, low to disconnect. + +module bootloader ( + input pin_clk_12M, // 12 MHz crystal on dedicated clock pin 35 + + inout pin_usbp, // USB D+ (pin 42, IOT_51a) + inout pin_usbn, // USB D- (pin 38, IOT_50b) + output pin_usb_det, // USB_DET (pin 37, IOT_36b) - high enables 1k5 pull-up on D+ + + input pin_btn_ok, // btn_ok, active low with external pull-up + + output pin_led_index, // white LED index finger (pin 39) + output pin_led_middle, // white LED middle finger (pin 40) + output pin_led_pinky, // white LED pinky finger (pin 41) + + inout pin_spi_miso, // SPI flash MISO (dedicated pin 17) - needs SB_IO on UP5K + output pin_spi_cs, // SPI flash CS (dedicated pin 16) + inout pin_spi_mosi, // SPI flash MOSI (dedicated pin 14) - needs SB_IO on UP5K + output pin_spi_sck, // SPI flash SCK (dedicated pin 15) + + output pin_spi_wp, // SPI flash /WP (pin 18, IOB_31b) - drive high to disable write-protect + output pin_spi_hold // SPI flash /HOLD (pin 19, IOB_29b) - drive high to disable hold +); + + // ============================================================================ + // SB_SPI hard IP - instantiate and disable to release dedicated SPI pins + // ============================================================================ + // The iCE40 UP5K has a hardened SPI block (SB_SPI) physically connected to + // pins 14/15/16/17. Even without instantiation, its output enables may + // default active and cause bus contention with our soft SPI master. + // We instantiate it here with all active signals tied off so its output + // enables are deasserted, freeing the pins for GPIO use. + wire spi_hard_so, spi_hard_soe; + wire spi_hard_mo, spi_hard_moe; + wire spi_hard_scko, spi_hard_sckoe; + wire spi_hard_mcsno0, spi_hard_mcsnoe0; + + SB_SPI #( + .BUS_ADDR74 ("0b0000") + ) spi_hard_ip ( + .SBCLKI (1'b0), + .SBRWI (1'b0), + .SBSTBI (1'b0), + .SBADRI7 (1'b0), + .SBADRI6 (1'b0), + .SBADRI5 (1'b0), + .SBADRI4 (1'b0), + .SBADRI3 (1'b0), + .SBADRI2 (1'b0), + .SBADRI1 (1'b0), + .SBADRI0 (1'b0), + .SBDATI7 (1'b0), + .SBDATI6 (1'b0), + .SBDATI5 (1'b0), + .SBDATI4 (1'b0), + .SBDATI3 (1'b0), + .SBDATI2 (1'b0), + .SBDATI1 (1'b0), + .SBDATI0 (1'b0), + .MI (1'b0), + .SI (1'b0), + .SCKI (1'b0), + .SCSNI (1'b1), // chip select active low - deassert + .SO (spi_hard_so), + .SOE (spi_hard_soe), + .MO (spi_hard_mo), + .MOE (spi_hard_moe), + .SCKO (spi_hard_scko), + .SCKOE (spi_hard_sckoe), + .MCSNO3 (), + .MCSNO2 (), + .MCSNO1 (), + .MCSNO0 (spi_hard_mcsno0), + .MCSNOE3 (), + .MCSNOE2 (), + .MCSNOE1 (), + .MCSNOE0 (spi_hard_mcsnoe0), + .SBDATO7 (), + .SBDATO6 (), + .SBDATO5 (), + .SBDATO4 (), + .SBDATO3 (), + .SBDATO2 (), + .SBDATO1 (), + .SBDATO0 (), + .SBACKO (), + .SPIIRQ (), + .SPIWKUP () + ); + + // ============================================================================ + // SB_IO for dedicated SPI pins - required on UP5K after SB_SPI is disabled + // ============================================================================ + // Without SB_IO, plain input/output on pins 14/17 does not work reliably + // after the SB_SPI hard IP output enables are deasserted. + wire spi_mosi_internal; // from bootloader core -> flash + wire spi_miso_internal; // from flash -> bootloader core + + SB_IO #( + .PIN_TYPE(6'b101001), // tristate output + simple input + .PULLUP(1'b0) + ) spi_mosi_iob ( + .PACKAGE_PIN (pin_spi_mosi), + .OUTPUT_ENABLE (1'b1), + .D_OUT_0 (spi_mosi_internal), + .D_IN_0 () + ); + + SB_IO #( + .PIN_TYPE(6'b101001), // tristate output + simple input + .PULLUP(1'b1) + ) spi_miso_iob ( + .PACKAGE_PIN (pin_spi_miso), + .OUTPUT_ENABLE (1'b0), + .D_OUT_0 (1'b0), + .D_IN_0 (spi_miso_internal) + ); + + // ============================================================================ + // PLL: 12 MHz -> 48 MHz + // ============================================================================ + // Pin 35 is a dedicated clock pad, so we use SB_PLL40_PAD (not _CORE). + // Parameters from: icepll -i 12 -o 48 + wire clk_48mhz; + wire lock; + wire reset = !lock; + + SB_PLL40_PAD #( + .DIVR (4'b0000), // DIVR = 0 + .DIVF (7'b0111111), // DIVF = 63 + .DIVQ (3'b100), // DIVQ = 4 + .FILTER_RANGE(3'b001), + .FEEDBACK_PATH("SIMPLE"), + .DELAY_ADJUSTMENT_MODE_FEEDBACK("FIXED"), + .FDA_FEEDBACK(4'b0000), + .DELAY_ADJUSTMENT_MODE_RELATIVE("FIXED"), + .FDA_RELATIVE(4'b0000), + .SHIFTREG_DIV_MODE(2'b00), + .PLLOUT_SELECT("GENCLK"), + .ENABLE_ICEGATE(1'b0) + ) usb_pll_inst ( + .PACKAGEPIN (pin_clk_12M), + .PLLOUTCORE (clk_48mhz), + .PLLOUTGLOBAL (), + .EXTFEEDBACK (), + .DYNAMICDELAY (), + .RESETB (1'b1), + .BYPASS (1'b0), + .LATCHINPUTVALUE(), + .LOCK (lock), + .SDI (), + .SDO (), + .SCLK () + ); + + // ============================================================================ + // Flash /WP and /HOLD - drive high to keep flash fully operational + // ============================================================================ + // After FPGA configuration, these GPIO pins float. If /HOLD goes low, + // the flash freezes its SPI output - causing all-zero reads. + assign pin_spi_wp = 1'b1; + assign pin_spi_hold = 1'b1; + + // ============================================================================ + // Clock divider: 48 -> 24 -> 12 MHz + // ============================================================================ + // The TinyFPGA bootloader expects both clk_48mhz and a slower 'clk'. + // The host_presence_timer counts at 'clk' rate; timeout is 196 000 000 + // which gives ~16.3 s at 12 MHz. + reg clk_24mhz = 0; + reg clk_12mhz = 0; + always @(posedge clk_48mhz) clk_24mhz <= !clk_24mhz; + always @(posedge clk_24mhz) clk_12mhz <= !clk_12mhz; + + wire clk = clk_12mhz; + + // ============================================================================ + // btn_ok bypass - skip bootloader, go straight to user image + // ============================================================================ + // Sample btn_ok once after PLL lock. If held low -> warmboot to user design. + // No debounce needed: button is either held during power-on/reset or not, + // and PLL lock time (~100 µs) already provides a stable sampling point. + reg bypass_sampled = 0; + reg bypass_trigger = 0; + + always @(posedge clk) begin + if (reset) begin + bypass_sampled <= 0; + bypass_trigger <= 0; + end else if (!bypass_sampled) begin + bypass_sampled <= 1; + bypass_trigger <= !pin_btn_ok; // active low: pressed = go to user + end + end + + // ============================================================================ + // Auto-boot timer - warmboot to user if no host / tinyprog activity + // ============================================================================ + // After ~1 s with no USB host or SPI activity, warmboot to user image. + // Resets when: + // - SPI CS goes low (tinyprog doing a transaction), OR + // - USB TX fires (host is enumerating / talking to us) + // Once any activity is seen the timer is permanently disarmed. + reg spi_cs_prev = 1; + reg usb_tx_prev = 0; + reg host_activity = 0; // latches on any USB or SPI activity + reg [23:0] autoboot_cnt = 0; // 2^23 / 12 MHz ≈ 0.7 s - close to 1 s + reg autoboot_trigger = 0; + + always @(posedge clk) begin + if (reset) begin + spi_cs_prev <= 1; + usb_tx_prev <= 0; + host_activity <= 0; + autoboot_cnt <= 0; + autoboot_trigger <= 0; + end else begin + spi_cs_prev <= pin_spi_cs; + usb_tx_prev <= usb_tx_en; + + // Detect SPI CS falling edge or USB TX rising edge + if ((spi_cs_prev && !pin_spi_cs) || (!usb_tx_prev && usb_tx_en)) + host_activity <= 1; + + // Count up if no activity has ever been seen + if (!host_activity && !autoboot_trigger) begin + if (autoboot_cnt[23]) + autoboot_trigger <= 1; + else + autoboot_cnt <= autoboot_cnt + 1'b1; + end + end + end + + // ============================================================================ + // 3-LED breathing with 120° phase shift + // ============================================================================ + // Triangle wave: 8-bit PWM value ramps 0->255->0 over ~512 µs steps. + // We use a 10-bit microsecond counter (from 12 MHz clk) and a 9-bit + // phase accumulator. Three LEDs are offset by 170 (≈ 512/3 ≈ 120°). + reg [5:0] breath_ns = 0; // divide clk by ~48 -> 1 µs ticks (shared with BL core idea) + wire breath_ns_rst = (breath_ns == 47); + always @(posedge clk) breath_ns <= breath_ns_rst ? 0 : breath_ns + 1'b1; + + reg [9:0] breath_us = 0; + wire breath_us_rst = (breath_us == 999); + always @(posedge clk) if (breath_ns_rst) breath_us <= breath_us_rst ? 0 : breath_us + 1'b1; + + reg [8:0] breath_phase = 0; // 0..511 triangle base + always @(posedge clk) if (breath_ns_rst && breath_us_rst) breath_phase <= breath_phase + 1'b1; + + // Triangle function: phase 0..255 -> ramp up, 256..511 -> ramp down + function [7:0] triangle; + input [8:0] ph; + triangle = ph[8] ? ~ph[7:0] : ph[7:0]; + endfunction + + wire [7:0] pwm_index = triangle(breath_phase); + wire [7:0] pwm_middle = triangle(breath_phase + 9'd170); + wire [7:0] pwm_pinky = triangle(breath_phase + 9'd341); + + reg [7:0] pwm_cnt = 0; + always @(posedge clk) pwm_cnt <= pwm_cnt + 1'b1; + + assign pin_led_index = pwm_index > pwm_cnt; + assign pin_led_middle = pwm_middle > pwm_cnt; + assign pin_led_pinky = pwm_pinky > pwm_cnt; + + // ============================================================================ + // USB_DET - control the 1k5 pull-up on D+ + // ============================================================================ + // Only assert (enumerate) after the bypass window has closed without + // btn_ok being held - i.e. we committed to running the bootloader. + // While bypass_armed is still 1 we keep USB_DET low so we don't glitch + // onto the bus if we're about to warmboot straight to the user image. + // Also deassert when the bootloader core requests boot (tinyprog "boot" + // command or host timeout) so the host sees a clean disconnect. + assign pin_usb_det = bypass_sampled && !bypass_trigger && !autoboot_trigger && !boot_from_bootloader; + + // ============================================================================ + // SB_WARMBOOT - interface to multiboot + // ============================================================================ + // S1=0, S0=1 -> selects image slot 1 (user design) + // BOOT pulse triggers the reconfig + wire boot_from_bootloader; // driven by tinyfpga_bootloader (timeout or USB cmd) + + wire boot = boot_from_bootloader || bypass_trigger || autoboot_trigger; + + SB_WARMBOOT warmboot_inst ( + .S1 (1'b0), + .S0 (1'b1), + .BOOT (boot) + ); + + // ============================================================================ + // USB tristate buffers - SB_IO primitives + // ============================================================================ + wire usb_p_tx; + wire usb_n_tx; + wire usb_p_rx; + wire usb_n_rx; + wire usb_p_rx_io; + wire usb_n_rx_io; + wire usb_tx_en; + + // When transmitting, feed back the idle state to RX (J state: D+=1, D-=0) + assign usb_p_rx = usb_tx_en ? 1'b1 : usb_p_rx_io; + assign usb_n_rx = usb_tx_en ? 1'b0 : usb_n_rx_io; + + SB_IO #( + .PIN_TYPE(6'b1010_01) // tristatable output + ) usbp_buf ( + .PACKAGE_PIN (pin_usbp), + .OUTPUT_ENABLE (usb_tx_en), + .D_IN_0 (usb_p_rx_io), + .D_OUT_0 (usb_p_tx) + ); + + SB_IO #( + .PIN_TYPE(6'b1010_01) // tristatable output + ) usbn_buf ( + .PACKAGE_PIN (pin_usbn), + .OUTPUT_ENABLE (usb_tx_en), + .D_IN_0 (usb_n_rx_io), + .D_OUT_0 (usb_n_tx) + ); + + // ============================================================================ + // TinyFPGA bootloader core + // ============================================================================ + tinyfpga_bootloader tinyfpga_bootloader_inst ( + .clk_48mhz (clk_48mhz), + .clk (clk), + .reset (reset), + + .usb_p_tx (usb_p_tx), + .usb_n_tx (usb_n_tx), + .usb_p_rx (usb_p_rx), + .usb_n_rx (usb_n_rx), + .usb_tx_en (usb_tx_en), + + .led (), // LED now driven by our 3-LED breather + + .spi_miso (spi_miso_internal), + .spi_cs (pin_spi_cs), + .spi_mosi (spi_mosi_internal), + .spi_sck (pin_spi_sck), + + .boot (boot_from_bootloader) + ); + +endmodule diff --git a/boards/bare_metal/bootmeta.json b/boards/bare_metal/bootmeta.json new file mode 100644 index 0000000..fc221b7 --- /dev/null +++ b/boards/bare_metal/bootmeta.json @@ -0,0 +1,12 @@ +{ + "bootmeta": { + "bootloader": "wenzellabs bare_metal USB bootloader", + "bver": "1.0", + "update": "https://wenzellabs.de/bare_metal", + "addrmap": { + "bootloader": "0x000a0-0x20000", + "userimage": "0x20000-0x80000", + "userdata": "0x80000-0x1000000" + } + } +} diff --git a/boards/bare_metal/pins.pcf b/boards/bare_metal/pins.pcf new file mode 100644 index 0000000..5a3b752 --- /dev/null +++ b/boards/bare_metal/pins.pcf @@ -0,0 +1,28 @@ +# bare_metal - USB bootloader +# iCE40UP5K SG48 (QFN 48) + +# 12 MHz crystal oscillator (dedicated clock pad) +set_io -nowarn pin_clk_12M 35 # IOT_46b_G0 + +# USB +set_io -nowarn pin_usbp 42 # IOT_51a - USB D+ +set_io -nowarn pin_usbn 38 # IOT_50b - USB D- +set_io -nowarn pin_usb_det 37 # IOT_36b - USB_DET, active-high enables 1k5 pull-up on D+ + +# btn_ok - skip bootloader, jump to user bitstream +set_io -nowarn pin_btn_ok 23 # IOT_37a + +# LED indicators - three white LEDs +set_io -nowarn pin_led_index 39 # white_index +set_io -nowarn pin_led_middle 40 # white_middle +set_io -nowarn pin_led_pinky 41 # white_pinky + +# SPI configuration flash (dedicated pins - usable as GPIO post-config) +# Matches ICEBreaker Bitsy (also UP5K-SG48) pin assignments. +# SB_SPI hard block must be instantiated and disabled to release these pins. +set_io -nowarn pin_spi_mosi 14 # SPI_SO (IOB_32a) - MOSI +set_io -nowarn pin_spi_miso 17 # SPI_SI (IOB_33b) - MISO +set_io -nowarn pin_spi_sck 15 # SPI_SCK (IOB_34a) +set_io -nowarn pin_spi_cs 16 # SPI_SS (IOB_35b) +set_io -nowarn pin_spi_wp 18 # IOB_31b - flash /WP (active low, drive high) +set_io -nowarn pin_spi_hold 19 # IOB_29b - flash /HOLD (active low, drive high) From 90c0bfe0a140d03fff48ff4c1bf55c4c2df53da0 Mon Sep 17 00:00:00 2001 From: matthias wenzel Date: Thu, 16 Apr 2026 10:03:22 +0200 Subject: [PATCH 02/12] bare_metal: establish two stage BL update mechanism Signed-off-by: matthias wenzel --- boards/bare_metal/Makefile | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/boards/bare_metal/Makefile b/boards/bare_metal/Makefile index c2c1e92..8efb6e6b 100644 --- a/boards/bare_metal/Makefile +++ b/boards/bare_metal/Makefile @@ -79,6 +79,33 @@ prog: multiboot.bin iceprog: multiboot.bin iceprog $< +# stage_two.bin: slot 0 with valid multiboot header, for OTA update. +# Written to address 0x0 by tinyprog during stage two of bootloader update. +# Uses icemulti with TWO images (so the header has a valid slot 1 entry), +# injects bootmeta, then truncates to 0x20000 (slot 0 boundary). +stage_two.bin: $(PROJ).bin bootmeta.json + cp $(PROJ).bin bootloader_copy.bin + icemulti -v -o $@ -a$(ALIGN) -p0 bootloader.bin bootloader_copy.bin + python3 -c "\ +import json; \ +d = json.load(open('bootmeta.json')); \ +blob = json.dumps(d, separators=(',',':')).encode(); \ +assert len(blob) <= 4096, f'bootmeta too large: {len(blob)} bytes (max 4096)'; \ +f = open('$@', 'r+b'); \ +f.seek($(META_ADDR)); \ +f.write(b'\\xff' * 4096); \ +f.seek($(META_ADDR)); \ +f.write(blob); \ +f.close(); \ +# truncate to slot 0 boundary - keeps header + bootloader + bootmeta \ +data = open('$@','rb').read()[:0x20000]; \ +# strip trailing 0xff, round up to 256B \ +end = len(data); \ +while end > 0 and data[end-1] == 0xff: end -= 1; \ +end = (end + 255) & ~255; \ +open('$@','wb').write(data[:end]); \ +print(f' stage_two.bin: {end} bytes (bootmeta at $(META_ADDR))')" + # User address in flash - must match icemulti -a$(ALIGN) USER_ADDR = 0x20000 @@ -98,7 +125,7 @@ boot: clean: rm -f $(PROJ).json $(PROJ).asc $(PROJ).rpt $(PROJ).bin - rm -f bootloader_copy.bin multiboot.bin + rm -f bootloader_copy.bin multiboot.bin stage_two.bin # production: provision one board via CH341 # reads serial from next_serial.txt, generates a UUID, writes boardmeta From a7e680a190383256f92a15af0b8991b2a8a00178 Mon Sep 17 00:00:00 2001 From: matthias wenzel Date: Thu, 16 Apr 2026 10:04:18 +0200 Subject: [PATCH 03/12] bare_metal: stay in BL even when not enumerated with btn_down Signed-off-by: matthias wenzel --- boards/bare_metal/bootloader.v | 23 ++++++++++++++++------- boards/bare_metal/bootmeta.json | 2 +- boards/bare_metal/pins.pcf | 3 +++ 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/boards/bare_metal/bootloader.v b/boards/bare_metal/bootloader.v index b253bed..c1d2057 100644 --- a/boards/bare_metal/bootloader.v +++ b/boards/bare_metal/bootloader.v @@ -10,9 +10,14 @@ // SB_WARMBOOT S1=0 S0=1 -> boots to slot 1 // // btn_ok bypass: -// If btn_ok (active low) is held during the first ~4 ms after config, +// If btn_ok (active low) is held at power-on / reset, // we skip USB entirely and warmboot straight to the user image. // +// btn_down stay: +// If btn_down (active low) is held at power-on / reset, +// the auto-boot timer is disabled - bootloader stays up indefinitely, +// useful for programming without a USB host present at power-on. +// // Otherwise we run the TinyFPGA bootloader (USB CDC-ACM + SPI bridge). // If no USB host sends SOF within ~16 s, we auto-warmboot to user image. // @@ -27,6 +32,7 @@ module bootloader ( output pin_usb_det, // USB_DET (pin 37, IOT_36b) - high enables 1k5 pull-up on D+ input pin_btn_ok, // btn_ok, active low with external pull-up + input pin_btn_down, // btn_down, active low - hold to stay in bootloader output pin_led_index, // white LED index finger (pin 39) output pin_led_middle, // white LED middle finger (pin 40) @@ -201,14 +207,17 @@ module bootloader ( // and PLL lock time (~100 µs) already provides a stable sampling point. reg bypass_sampled = 0; reg bypass_trigger = 0; + reg stay_in_bootloader = 0; // btn_down held at boot -> inhibit autoboot always @(posedge clk) begin if (reset) begin - bypass_sampled <= 0; - bypass_trigger <= 0; + bypass_sampled <= 0; + bypass_trigger <= 0; + stay_in_bootloader <= 0; end else if (!bypass_sampled) begin - bypass_sampled <= 1; - bypass_trigger <= !pin_btn_ok; // active low: pressed = go to user + bypass_sampled <= 1; + bypass_trigger <= !pin_btn_ok; // active low: pressed = go to user + stay_in_bootloader <= !pin_btn_down; // active low: pressed = stay in BL end end @@ -241,8 +250,8 @@ module bootloader ( if ((spi_cs_prev && !pin_spi_cs) || (!usb_tx_prev && usb_tx_en)) host_activity <= 1; - // Count up if no activity has ever been seen - if (!host_activity && !autoboot_trigger) begin + // Count up if no activity has ever been seen and not held in BL + if (!host_activity && !autoboot_trigger && !stay_in_bootloader) begin if (autoboot_cnt[23]) autoboot_trigger <= 1; else diff --git a/boards/bare_metal/bootmeta.json b/boards/bare_metal/bootmeta.json index fc221b7..abbd5c0 100644 --- a/boards/bare_metal/bootmeta.json +++ b/boards/bare_metal/bootmeta.json @@ -1,7 +1,7 @@ { "bootmeta": { "bootloader": "wenzellabs bare_metal USB bootloader", - "bver": "1.0", + "bver": "1.1", "update": "https://wenzellabs.de/bare_metal", "addrmap": { "bootloader": "0x000a0-0x20000", diff --git a/boards/bare_metal/pins.pcf b/boards/bare_metal/pins.pcf index 5a3b752..9ca02ab 100644 --- a/boards/bare_metal/pins.pcf +++ b/boards/bare_metal/pins.pcf @@ -12,6 +12,9 @@ set_io -nowarn pin_usb_det 37 # IOT_36b - USB_DET, active-high enables # btn_ok - skip bootloader, jump to user bitstream set_io -nowarn pin_btn_ok 23 # IOT_37a +# btn_down - stay in bootloader even when not enumerated by host +set_io -nowarn pin_btn_down 11 # IOB_20a + # LED indicators - three white LEDs set_io -nowarn pin_led_index 39 # white_index set_io -nowarn pin_led_middle 40 # white_middle From af2f9298b250cd797189a48c1f321f2ab56ffbab Mon Sep 17 00:00:00 2001 From: matthias wenzel Date: Thu, 16 Apr 2026 11:02:26 +0200 Subject: [PATCH 04/12] fix the tinyprog programmer to handle Winbond flashes Signed-off-by: matthias wenzel --- programmer/tinyprog/__init__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/programmer/tinyprog/__init__.py b/programmer/tinyprog/__init__.py index 9b673e0..64319f4 100644 --- a/programmer/tinyprog/__init__.py +++ b/programmer/tinyprog/__init__.py @@ -315,6 +315,13 @@ def __init__(self, ser, progress=None): self.security_page_read_cmd = 0x68 self.security_page_erase_cmd = 0x64 + elif flash_id[0] == 0xEF: + # Winbond + self.security_page_bit_offset = 4 + self.security_page_write_cmd = 0x42 + self.security_page_read_cmd = 0x48 + self.security_page_erase_cmd = 0x44 + else: # Adesto self.security_page_bit_offset = 0 From 93a429f6544fe9a26456f6b5b59908621d2b01fd Mon Sep 17 00:00:00 2001 From: matthias wenzel Date: Thu, 16 Apr 2026 11:02:58 +0200 Subject: [PATCH 05/12] fix the tinyprog programmer to handle two staged BL updates Signed-off-by: matthias wenzel --- programmer/tinyprog/__main__.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/programmer/tinyprog/__main__.py b/programmer/tinyprog/__main__.py index 489a07d..e73af74 100644 --- a/programmer/tinyprog/__main__.py +++ b/programmer/tinyprog/__main__.py @@ -75,7 +75,17 @@ def get_port_by_uuid(device, uuid): def check_for_new_bootloader(): - return [] + boards_needing_update = [] + ports = get_ports("1d50:6130") + + for port in ports: + with port: + p = TinyProg(port) + m = p.meta.root + if isinstance(m, dict) and u"bootmeta" in m and u"update" in m[u"bootmeta"]: + boards_needing_update.append(port) + + return boards_needing_update def check_for_wrong_tinyfpga_bx_vidpid(): From e89af5374be295680a0f39b414ed4e2fc2e5242f Mon Sep 17 00:00:00 2001 From: matthias wenzel Date: Thu, 16 Apr 2026 12:32:05 +0200 Subject: [PATCH 06/12] fix 'make boot' Signed-off-by: matthias wenzel --- boards/bare_metal/Makefile | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/boards/bare_metal/Makefile b/boards/bare_metal/Makefile index 8efb6e6b..2b71dd1 100644 --- a/boards/bare_metal/Makefile +++ b/boards/bare_metal/Makefile @@ -114,14 +114,9 @@ USER_ADDR = 0x20000 multiboot-user: $(PROJ).bin icemulti -v -o multiboot.bin -a$(ALIGN) -p0 $(PROJ).bin $(USER_BIN) -# Flash user bitstream over USB (bootloader must be active) -# Usage: make flash-user USER_BIN=path/to/user.bin -flash-user: - python3 tinyprog_user.py -a $(USER_ADDR) $(USER_BIN) - # Just boot to user image (no programming) boot: - python3 tinyprog_user.py --boot + tinyprog --boot clean: rm -f $(PROJ).json $(PROJ).asc $(PROJ).rpt $(PROJ).bin From b2c48e8cbcd61c758d2967edb143e8b940c90d5b Mon Sep 17 00:00:00 2001 From: matthias wenzel Date: Sat, 18 Apr 2026 07:39:17 +0200 Subject: [PATCH 07/12] don't bail out with an AttributeError when metadata is missing Signed-off-by: matthias wenzel --- programmer/tinyprog/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/programmer/tinyprog/__init__.py b/programmer/tinyprog/__init__.py index 64319f4..d672d61 100644 --- a/programmer/tinyprog/__init__.py +++ b/programmer/tinyprog/__init__.py @@ -274,6 +274,15 @@ def userdata_addr_range(self): return self._get_addr_range(u"userdata") def _get_addr_range(self, name): + # If we couldn't read metadata, present a clear error instead of + # raising an AttributeError when trying to access self.root. + if self.root is None: + raise Exception( + "Missing device metadata (flash reads returned no metadata). " + "Either specify the target address with -a/--addr, fix the " + "bootloader/flash state, or use a programmer that can access " + "the chip directly.") + # get the bootmeta's addrmap or fallback to the root's addrmap. addr_map = self.root.get(u"bootmeta", {}).get( u"addrmap", self.root.get(u"addrmap", None)) From ac4dbbd32fed80533d7cbbcd0a5e308ef7bb5482 Mon Sep 17 00:00:00 2001 From: matthias wenzel Date: Sat, 18 Apr 2026 07:41:23 +0200 Subject: [PATCH 08/12] log instead of crashing, tighter timeout loop with two staged BL updates Signed-off-by: matthias wenzel --- programmer/tinyprog/__main__.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/programmer/tinyprog/__main__.py b/programmer/tinyprog/__main__.py index e73af74..f1e1ea7 100644 --- a/programmer/tinyprog/__main__.py +++ b/programmer/tinyprog/__main__.py @@ -60,8 +60,11 @@ def strict_query_user(question): return valid.get(choice, False) -def get_port_by_uuid(device, uuid): +def get_port_by_uuid(device, uuid, verbose=False): ports = get_ports(device) + get_ports("1209:2100") + if verbose and ports: + sys.stdout.write("[ports: %s]" % ", ".join(str(p) for p in ports)) + sys.stdout.flush() for port in ports: try: with port: @@ -69,8 +72,10 @@ def get_port_by_uuid(device, uuid): if p.meta.uuid().startswith(uuid): return port - except Exception: - pass + except Exception as e: + if verbose: + sys.stdout.write("[%s: %s]" % (port, e)) + sys.stdout.flush() return None @@ -182,15 +187,23 @@ def perform_bootloader_update(port): new_port = None - for x in range(20): - time.sleep(1) + for x in range(40): + time.sleep(0.5) sys.stdout.write(".") sys.stdout.flush() + + # Look for the board by UUID. Stage one should present + # readable metadata now that the SB_SPI issue is fixed, so + # prefer the higher-level lookup that matches the UUID. new_port = get_port_by_uuid("1d50:6130", uuid) if new_port is not None: print("connected!") break + if new_port is None: + print("\n Failed to find stage one bootloader.") + return False + with new_port: p = TinyProg(new_port) @@ -322,6 +335,8 @@ def parse_int(str_value): with port: p = TinyProg(port) m = p.meta.root + if m is None: + m = {"error": "no metadata found"} m["port"] = str(port) meta.append(m) print(json.dumps(meta, indent=2)) From c794099e48ac3497518ffff5537ee8213e90ada7 Mon Sep 17 00:00:00 2001 From: matthias wenzel Date: Sat, 18 Apr 2026 07:45:44 +0200 Subject: [PATCH 09/12] bare_metal doesn't use iceprog Signed-off-by: matthias wenzel --- boards/bare_metal/Makefile | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/boards/bare_metal/Makefile b/boards/bare_metal/Makefile index 2b71dd1..6e7ee6d 100644 --- a/boards/bare_metal/Makefile +++ b/boards/bare_metal/Makefile @@ -75,10 +75,6 @@ erase: prog: multiboot.bin ch341prog -v -w multiboot.bin -# Flash multiboot image via iceprog (if available) -iceprog: multiboot.bin - iceprog $< - # stage_two.bin: slot 0 with valid multiboot header, for OTA update. # Written to address 0x0 by tinyprog during stage two of bootloader update. # Uses icemulti with TWO images (so the header has a valid slot 1 entry), @@ -150,4 +146,4 @@ open('_boardmeta_prod.json','w').write(json.dumps(d, separators=(',',':')))" \ echo "=== Board $$SERIAL done. Next serial: $$(cat next_serial.txt) ===" && \ rm -f _boardmeta_prod.json -.PHONY: all prog iceprog multiboot-user flash-user boot clean erase produce_one_board +.PHONY: all prog multiboot-user flash-user boot clean erase produce_one_board From 39be0bacfd9e5e7b5158964a40f18eebde841c5d Mon Sep 17 00:00:00 2001 From: matthias wenzel Date: Sat, 18 Apr 2026 07:47:21 +0200 Subject: [PATCH 10/12] don't use the SB_SPI hard IP, go like Fomu. stay in BL with btn_down Signed-off-by: matthias wenzel --- boards/bare_metal/bootloader.v | 112 +++++---------------------------- 1 file changed, 16 insertions(+), 96 deletions(-) diff --git a/boards/bare_metal/bootloader.v b/boards/bare_metal/bootloader.v index c1d2057..0e76062 100644 --- a/boards/bare_metal/bootloader.v +++ b/boards/bare_metal/bootloader.v @@ -38,9 +38,9 @@ module bootloader ( output pin_led_middle, // white LED middle finger (pin 40) output pin_led_pinky, // white LED pinky finger (pin 41) - inout pin_spi_miso, // SPI flash MISO (dedicated pin 17) - needs SB_IO on UP5K + input pin_spi_miso, // SPI flash MISO (dedicated pin 17) output pin_spi_cs, // SPI flash CS (dedicated pin 16) - inout pin_spi_mosi, // SPI flash MOSI (dedicated pin 14) - needs SB_IO on UP5K + output pin_spi_mosi, // SPI flash MOSI (dedicated pin 14) output pin_spi_sck, // SPI flash SCK (dedicated pin 15) output pin_spi_wp, // SPI flash /WP (pin 18, IOB_31b) - drive high to disable write-protect @@ -48,98 +48,12 @@ module bootloader ( ); // ============================================================================ - // SB_SPI hard IP - instantiate and disable to release dedicated SPI pins + // Dedicated SPI pins (14-17) on UP5K // ============================================================================ - // The iCE40 UP5K has a hardened SPI block (SB_SPI) physically connected to - // pins 14/15/16/17. Even without instantiation, its output enables may - // default active and cause bus contention with our soft SPI master. - // We instantiate it here with all active signals tied off so its output - // enables are deasserted, freeing the pins for GPIO use. - wire spi_hard_so, spi_hard_soe; - wire spi_hard_mo, spi_hard_moe; - wire spi_hard_scko, spi_hard_sckoe; - wire spi_hard_mcsno0, spi_hard_mcsnoe0; - - SB_SPI #( - .BUS_ADDR74 ("0b0000") - ) spi_hard_ip ( - .SBCLKI (1'b0), - .SBRWI (1'b0), - .SBSTBI (1'b0), - .SBADRI7 (1'b0), - .SBADRI6 (1'b0), - .SBADRI5 (1'b0), - .SBADRI4 (1'b0), - .SBADRI3 (1'b0), - .SBADRI2 (1'b0), - .SBADRI1 (1'b0), - .SBADRI0 (1'b0), - .SBDATI7 (1'b0), - .SBDATI6 (1'b0), - .SBDATI5 (1'b0), - .SBDATI4 (1'b0), - .SBDATI3 (1'b0), - .SBDATI2 (1'b0), - .SBDATI1 (1'b0), - .SBDATI0 (1'b0), - .MI (1'b0), - .SI (1'b0), - .SCKI (1'b0), - .SCSNI (1'b1), // chip select active low - deassert - .SO (spi_hard_so), - .SOE (spi_hard_soe), - .MO (spi_hard_mo), - .MOE (spi_hard_moe), - .SCKO (spi_hard_scko), - .SCKOE (spi_hard_sckoe), - .MCSNO3 (), - .MCSNO2 (), - .MCSNO1 (), - .MCSNO0 (spi_hard_mcsno0), - .MCSNOE3 (), - .MCSNOE2 (), - .MCSNOE1 (), - .MCSNOE0 (spi_hard_mcsnoe0), - .SBDATO7 (), - .SBDATO6 (), - .SBDATO5 (), - .SBDATO4 (), - .SBDATO3 (), - .SBDATO2 (), - .SBDATO1 (), - .SBDATO0 (), - .SBACKO (), - .SPIIRQ (), - .SPIWKUP () - ); - - // ============================================================================ - // SB_IO for dedicated SPI pins - required on UP5K after SB_SPI is disabled - // ============================================================================ - // Without SB_IO, plain input/output on pins 14/17 does not work reliably - // after the SB_SPI hard IP output enables are deasserted. - wire spi_mosi_internal; // from bootloader core -> flash - wire spi_miso_internal; // from flash -> bootloader core - - SB_IO #( - .PIN_TYPE(6'b101001), // tristate output + simple input - .PULLUP(1'b0) - ) spi_mosi_iob ( - .PACKAGE_PIN (pin_spi_mosi), - .OUTPUT_ENABLE (1'b1), - .D_OUT_0 (spi_mosi_internal), - .D_IN_0 () - ); - - SB_IO #( - .PIN_TYPE(6'b101001), // tristate output + simple input - .PULLUP(1'b1) - ) spi_miso_iob ( - .PACKAGE_PIN (pin_spi_miso), - .OUTPUT_ENABLE (1'b0), - .D_OUT_0 (1'b0), - .D_IN_0 (spi_miso_internal) - ); + // The iCE40 UP5K has an SB_SPI hard block on these pads, but if we do NOT + // instantiate it, the pads are available as regular GPIO - same approach as + // the Fomu (also UP5K) bootloader. Instantiating SB_SPI (even "disabled") + // connects its output enables to the pads and creates bus contention. // ============================================================================ // PLL: 12 MHz -> 48 MHz @@ -312,7 +226,13 @@ module bootloader ( // BOOT pulse triggers the reconfig wire boot_from_bootloader; // driven by tinyfpga_bootloader (timeout or USB cmd) - wire boot = boot_from_bootloader || bypass_trigger || autoboot_trigger; + // When stay_in_bootloader is set, ignore the core's internal timeout. + // The core also fires boot_from_bootloader on a USB "boot" command from + // tinyprog - we still want that, but can't distinguish here. Acceptable + // trade-off: with btn_down held, tinyprog "boot" command also won't work. + // In practice, tinyprog always programs then boots, so if btn_down is held + // the user explicitly wants to stay. + wire boot = (!stay_in_bootloader && boot_from_bootloader) || bypass_trigger || autoboot_trigger; SB_WARMBOOT warmboot_inst ( .S1 (1'b0), @@ -369,9 +289,9 @@ module bootloader ( .led (), // LED now driven by our 3-LED breather - .spi_miso (spi_miso_internal), + .spi_miso (pin_spi_miso), .spi_cs (pin_spi_cs), - .spi_mosi (spi_mosi_internal), + .spi_mosi (pin_spi_mosi), .spi_sck (pin_spi_sck), .boot (boot_from_bootloader) From edfb6f49486c68e00dec5532f368799d02584d9c Mon Sep 17 00:00:00 2001 From: matthias wenzel Date: Sat, 18 Apr 2026 07:56:21 +0200 Subject: [PATCH 11/12] clean up PCF, rm outdated comments Signed-off-by: matthias wenzel --- boards/bare_metal/pins.pcf | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/boards/bare_metal/pins.pcf b/boards/bare_metal/pins.pcf index 9ca02ab..26c68f7 100644 --- a/boards/bare_metal/pins.pcf +++ b/boards/bare_metal/pins.pcf @@ -20,12 +20,10 @@ set_io -nowarn pin_led_index 39 # white_index set_io -nowarn pin_led_middle 40 # white_middle set_io -nowarn pin_led_pinky 41 # white_pinky -# SPI configuration flash (dedicated pins - usable as GPIO post-config) -# Matches ICEBreaker Bitsy (also UP5K-SG48) pin assignments. -# SB_SPI hard block must be instantiated and disabled to release these pins. -set_io -nowarn pin_spi_mosi 14 # SPI_SO (IOB_32a) - MOSI -set_io -nowarn pin_spi_miso 17 # SPI_SI (IOB_33b) - MISO -set_io -nowarn pin_spi_sck 15 # SPI_SCK (IOB_34a) -set_io -nowarn pin_spi_cs 16 # SPI_SS (IOB_35b) -set_io -nowarn pin_spi_wp 18 # IOB_31b - flash /WP (active low, drive high) -set_io -nowarn pin_spi_hold 19 # IOB_29b - flash /HOLD (active low, drive high) +# SPI configuration flash +set_io -nowarn pin_spi_mosi 14 # IOB_32a - SPI_SO, MOSI +set_io -nowarn pin_spi_miso 17 # IOB_33b - SPI_SI, MISO +set_io -nowarn pin_spi_sck 15 # IOB_34a - SPI_SCK +set_io -nowarn pin_spi_cs 16 # IOB_35b - SPI_SS +set_io -nowarn pin_spi_wp 18 # IOB_31b - SPI_WPn +set_io -nowarn pin_spi_hold 19 # IOB_29b - SPI_HOLDn From 426e7b5a2469ce7575e7822f5660492a2594453a Mon Sep 17 00:00:00 2001 From: matthias wenzel Date: Thu, 23 Apr 2026 15:04:44 +0200 Subject: [PATCH 12/12] let the user choose any of her three bitstreams upon boot Signed-off-by: matthias wenzel --- boards/bare_metal/Makefile | 13 +++++++++--- boards/bare_metal/bootloader.v | 37 +++++++++++++++++++++++++--------- boards/bare_metal/pins.pcf | 8 +++++++- 3 files changed, 45 insertions(+), 13 deletions(-) diff --git a/boards/bare_metal/Makefile b/boards/bare_metal/Makefile index 6e7ee6d..70b4985 100644 --- a/boards/bare_metal/Makefile +++ b/boards/bare_metal/Makefile @@ -12,12 +12,16 @@ COMMON = ../../common # All source files SRC = $(PROJ).v $(wildcard $(COMMON)/*.v) -USER_BITSTREAM ?= bootloader_copy.bin +# Support up to three user bitstreams (slots 1..3). By default use the +# placeholder copy of the bootloader so all slots contain something. +USER_BITSTREAM_1 ?= bootloader_copy.bin +USER_BITSTREAM_2 ?= bootloader_copy.bin +USER_BITSTREAM_3 ?= bootloader_copy.bin # flash memory map # 0x000_0000 - 0x000_009f =160b, multiboot header, slot table -# 0x000_00a0 - 0x001_ffff ~124kB, slot 0, bare_metal_bootloader +# 0x000_00a0 - 0x001_efff ~124kB, slot 0, bare_metal_bootloader # 0x001_f000 - 0x001_ffff =4kB, bootmetadata # 0x002_0000 - 0x003_ffff =128kB, slot 1, user bitstream, hackerstacker_demo by default # 0x004_0000 - 0x005_ffff =128kB, slot 2, transputers anyone? @@ -49,7 +53,10 @@ META_ADDR = 0x1F000 # After icemulti, inject minified bootmeta.json at META_ADDR. multiboot.bin: $(PROJ).bin bootmeta.json cp $(PROJ).bin bootloader_copy.bin - icemulti -v -o $@ -a$(ALIGN) -p0 bootloader.bin $(USER_BITSTREAM) + # Build a multiboot image with the bootloader (slot 0) followed by up to + # three user bitstreams (slots 1..3). Defaults point to the placeholder + # copy so the image is valid even if you don't provide real user bitfiles. + icemulti -v -o $@ -a$(ALIGN) -p0 bootloader.bin $(USER_BITSTREAM_1) $(USER_BITSTREAM_2) $(USER_BITSTREAM_3) python3 -c "\ import json; \ d = json.load(open('bootmeta.json')); \ diff --git a/boards/bare_metal/bootloader.v b/boards/bare_metal/bootloader.v index 0e76062..131816e 100644 --- a/boards/bare_metal/bootloader.v +++ b/boards/bare_metal/bootloader.v @@ -31,7 +31,9 @@ module bootloader ( inout pin_usbn, // USB D- (pin 38, IOT_50b) output pin_usb_det, // USB_DET (pin 37, IOT_36b) - high enables 1k5 pull-up on D+ - input pin_btn_ok, // btn_ok, active low with external pull-up + input pin_btn_ok, // btn_ok, active low - skip to user bitstream 1 + input pin_btn_left, // btn_left, active low - skip to user bitstream 2 + input pin_btn_right, // btn_right, active low - skip to user bitstream 3 input pin_btn_down, // btn_down, active low - hold to stay in bootloader output pin_led_index, // white LED index finger (pin 39) @@ -120,17 +122,28 @@ module bootloader ( // No debounce needed: button is either held during power-on/reset or not, // and PLL lock time (~100 µs) already provides a stable sampling point. reg bypass_sampled = 0; - reg bypass_trigger = 0; + // 0 = no bypass, 1 = slot1, 2 = slot2, 3 = slot3 + reg [1:0] bypass_slot = 2'b00; + wire bypass_trigger = (bypass_slot != 2'b00); reg stay_in_bootloader = 0; // btn_down held at boot -> inhibit autoboot always @(posedge clk) begin if (reset) begin bypass_sampled <= 0; - bypass_trigger <= 0; + bypass_slot <= 2'b00; stay_in_bootloader <= 0; end else if (!bypass_sampled) begin - bypass_sampled <= 1; - bypass_trigger <= !pin_btn_ok; // active low: pressed = go to user + bypass_sampled <= 1; + // Priority: btn_ok (slot1) > btn_left (slot2) > btn_right (slot3) + if (!pin_btn_ok) + bypass_slot <= 2'b01; + else if (!pin_btn_left) + bypass_slot <= 2'b10; + else if (!pin_btn_right) + bypass_slot <= 2'b11; + else + bypass_slot <= 2'b00; + stay_in_bootloader <= !pin_btn_down; // active low: pressed = stay in BL end end @@ -222,8 +235,10 @@ module bootloader ( // ============================================================================ // SB_WARMBOOT - interface to multiboot // ============================================================================ - // S1=0, S0=1 -> selects image slot 1 (user design) - // BOOT pulse triggers the reconfig + // SB_WARMBOOT - interface to multiboot + // S1/S0 select which slot to boot. We sample button state at reset to + // decide whether to warmboot to slot 1..3. If no bypass button was + // pressed, the default target is slot 1. wire boot_from_bootloader; // driven by tinyfpga_bootloader (timeout or USB cmd) // When stay_in_bootloader is set, ignore the core's internal timeout. @@ -234,9 +249,13 @@ module bootloader ( // the user explicitly wants to stay. wire boot = (!stay_in_bootloader && boot_from_bootloader) || bypass_trigger || autoboot_trigger; + // Select warmboot slot: if a bypass button was sampled use that slot, + // otherwise default to slot 1 (2'b01). + wire [1:0] warmboot_sel = (bypass_slot != 2'b00) ? bypass_slot : 2'b01; + SB_WARMBOOT warmboot_inst ( - .S1 (1'b0), - .S0 (1'b1), + .S1 (warmboot_sel[1]), + .S0 (warmboot_sel[0]), .BOOT (boot) ); diff --git a/boards/bare_metal/pins.pcf b/boards/bare_metal/pins.pcf index 26c68f7..d0a2a0a 100644 --- a/boards/bare_metal/pins.pcf +++ b/boards/bare_metal/pins.pcf @@ -9,9 +9,15 @@ set_io -nowarn pin_usbp 42 # IOT_51a - USB D+ set_io -nowarn pin_usbn 38 # IOT_50b - USB D- set_io -nowarn pin_usb_det 37 # IOT_36b - USB_DET, active-high enables 1k5 pull-up on D+ -# btn_ok - skip bootloader, jump to user bitstream +# btn_ok - skip bootloader, jump to user bitstream 1 set_io -nowarn pin_btn_ok 23 # IOT_37a +# btn_left - skip bootloader, jump to user bitstream 2 +set_io -nowarn pin_btn_left 44 # IOB_3b_G6 + +# btn_left - skip bootloader, jump to user bitstream 3 +set_io -nowarn pin_btn_right 10 # IOB_18a + # btn_down - stay in bootloader even when not enumerated by host set_io -nowarn pin_btn_down 11 # IOB_20a