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..70b4985 --- /dev/null +++ b/boards/bare_metal/Makefile @@ -0,0 +1,156 @@ +# 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) + +# 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_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? +# 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 + # 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')); \ +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 + +# 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 + +# 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) + +# Just boot to user image (no programming) +boot: + tinyprog --boot + +clean: + rm -f $(PROJ).json $(PROJ).asc $(PROJ).rpt $(PROJ).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 +# 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 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..131816e --- /dev/null +++ b/boards/bare_metal/bootloader.v @@ -0,0 +1,319 @@ +// 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 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. +// +// 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 - 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) + output pin_led_middle, // white LED middle finger (pin 40) + output pin_led_pinky, // white LED pinky finger (pin 41) + + input pin_spi_miso, // SPI flash MISO (dedicated pin 17) + output pin_spi_cs, // SPI flash CS (dedicated pin 16) + 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 + output pin_spi_hold // SPI flash /HOLD (pin 19, IOB_29b) - drive high to disable hold +); + + // ============================================================================ + // Dedicated SPI pins (14-17) on UP5K + // ============================================================================ + // 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 + // ============================================================================ + // 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; + // 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_slot <= 2'b00; + stay_in_bootloader <= 0; + end else if (!bypass_sampled) begin + 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 + + // ============================================================================ + // 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 and not held in BL + if (!host_activity && !autoboot_trigger && !stay_in_bootloader) 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 + // ============================================================================ + // 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. + // 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; + + // 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 (warmboot_sel[1]), + .S0 (warmboot_sel[0]), + .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 (pin_spi_miso), + .spi_cs (pin_spi_cs), + .spi_mosi (pin_spi_mosi), + .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..abbd5c0 --- /dev/null +++ b/boards/bare_metal/bootmeta.json @@ -0,0 +1,12 @@ +{ + "bootmeta": { + "bootloader": "wenzellabs bare_metal USB bootloader", + "bver": "1.1", + "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..d0a2a0a --- /dev/null +++ b/boards/bare_metal/pins.pcf @@ -0,0 +1,35 @@ +# 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 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 + +# 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 +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 diff --git a/programmer/tinyprog/__init__.py b/programmer/tinyprog/__init__.py index 9b673e0..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)) @@ -315,6 +324,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 diff --git a/programmer/tinyprog/__main__.py b/programmer/tinyprog/__main__.py index 489a07d..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,13 +72,25 @@ 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 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(): @@ -172,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) @@ -312,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))