Browse Source

fw: add ability to write a board ID string in ESP flash

Add the ability to write a board ID string in ESP flash, and add
beginning infrastructure for handling multiversion firmware images
(not quite there yet.)
H. Peter Anvin 2 years ago
parent
commit
75e32256b8

+ 16 - 4
Makefile

@@ -5,11 +5,13 @@ SUBDIRS   := esp32 tools rv32 fpga
 REVISIONS := v1 v2 bypass
 
 GIT_DIR   ?= .git
+TEMP      ?= /tmp
 
 MAX80_IP  ?= max80
 
 CURL	   = curl
 PERL	   = perl
+ESPTOOL   ?= esptool
 
 all clean spotless :
 	$(MAKE) local.$@ $(SUBDIRS) goal=$@
@@ -54,7 +56,7 @@ fpga: version.vh | rv32 esp32
 local.all:
 
 local.clean:
-	rm -f *~ ./\#* \# *.bak *.tmp
+	rm -f *~ ./\#* \# *.bak *.tmp stamp_*.bin
 
 local.spotless: local.clean
 	rm -f version.h version.vh
@@ -75,9 +77,19 @@ upload-esp:
 
 # Update via HTTP or serial port
 ip = $(MAX80_IP)
-upload-%:
+upload-v%:
 	if [ -z '$(PORT)' ]; then \
-		$(CURL) -v --data-binary @fpga/output/$*.fw http://$(ip)/sys/fwupdate ; \
+		$(CURL) -v --data-binary @fpga/output/v$*.fw http://$(ip)/sys/fwupdate ; \
 	else \
-		$(PERL) ./esp32/flashesp.pl fpga/output/$*.fw '$(PORT)' $(FLASHOPT); \
+		$(PERL) ./esp32/flashesp.pl fpga/output/v$*.fw '$(PORT)' $(FLASHOPT); \
 	fi
+
+# Set board revision in flash
+WRITEFLASH = [ ! -z '$(PORT)' ] && $(ESPTOOL) --before default_reset --after hard_reset --port $(PORT) write_flash -z
+
+stamp_max80_v%.bin:
+	$(PERL) -e '$$m = "MAX80 v$*\0"; print $$m, "\xff" x (4096 - length ($$m));' > $@
+
+setver-v%: stamp_max80_v%.bin
+	if [ -z '$(PORT)' ]; then echo 'Set PORT for this command' 2>&1; exit 1; fi
+	$(WRITEFLASH) 0 $< || sleep 1; $(WRITEFLASH) 0 $<

+ 25 - 0
common/boardinfo.h

@@ -0,0 +1,25 @@
+#ifndef BOARDINFO_H
+#define BOARDINFO_H
+
+#include "compiler.h"
+
+#define BOARDINFO_SIZE		4096
+
+#define BOARDINFO_MAGIC_1	0x6682df97
+#define BOARDINFO_MAGIC_2	0xe2a0d506
+
+#define IBLK_MAX_MAC_ADDR	8
+
+struct board_info {
+    uint32_t magic[2];
+    uint32_t len;
+    uint32_t crc;		/* 32-bit CRC calculated with crc = 0 */
+
+    char version_str[256];
+
+    uint8_t mac[IBLK_MAX_MAC_ADDR][6];
+};
+
+extern_c struct board_info board_info;
+
+#endif

+ 2 - 1
common/fwimg.h

@@ -29,7 +29,8 @@ enum fw_data_type {
     FDT_FPGA_INIT,		/* FPGA bitstream for update */
     FDT_ESP_PART,		/* ESP32 partition table */
     FDT_ESP_SYS,		/* ESP32 boot loader, OTA control, etc */
-    FDT_ESP_TOOL		/* esptool.py options for serial flashing */
+    FDT_ESP_TOOL,		/* esptool.py options for serial flashing */
+    FDT_BOARDINFO		/* Board information flash address */
 };
 enum fw_data_flags {
     FDF_OPTIONAL     = 0x0001	/* Ignore if chunk data type unknown */

+ 192 - 0
common/matchver.c

@@ -0,0 +1,192 @@
+/*
+ * Compare project/version strings of the form:
+ *
+ * PROJECT v<num>.<num>...[ <flags>]
+ *
+ * Pattern is of the form:
+ *
+ * PROJECT[ v[<num>.<num>...][-[<num>.<num>...]] ][+-]flags...
+ *
+ * Chunks must be separated by exactly one space. Flags are A-Za-z0-9.
+ * Any control character is treated as end of string.
+ */
+
+#include <stdlib.h>
+#include <stdbool.h>
+#include <inttypes.h>
+
+static int flag_val(char c)
+{
+    if (c < '0')
+	return -1;
+    else if (c <= '9')
+	return c-'0';
+    else if (c < 'A')
+	return -1;
+    else if (c <= 'Z')
+	return c-'A'+10;
+    else if (c < 'a')
+	return -1;
+    else if (c <= 'z')
+	return c-'a'+36;
+    else
+	return -1;
+}
+
+static uint64_t flag_mask(char c)
+{
+    int v = flag_val(c);
+    return (v < 0) ? 0 : UINT64_C(1) << v;
+}
+
+static inline bool is_digit(char c)
+{
+    return (unsigned int)(c - '0') < 10;
+}
+
+/*
+ * Compare numeric strings with components separated by dots, return the end
+ * of each string.
+ */
+static int compare_numbers(const char **ap, const char **bp,
+			   unsigned long bmissing)
+{
+    bool adig, bdig;
+    unsigned long an, bn;
+    int result = 0;
+    const char *a = *ap, *b = *bp;
+
+    for (;;) {
+	adig = is_digit(*a);
+	bdig = is_digit(*b);
+
+	if (!adig && !bdig)
+	    break;
+
+	if (adig) {
+	    an = strtoul(a, (char **)&a, 10);
+	    if (*a == '.')
+		a++;
+	} else {
+	    an = 0;
+	}
+
+	if (bdig) {
+	    bn = strtoul(b, (char **)&b, 10);
+	    if (*b == '.')
+		b++;
+	} else {
+	    bn = bmissing;
+	}
+
+	/* If result is set, the answer is already known, just find the end */
+	if (!result) {
+	    result = (an < bn) ? -1 : (an > bn) ? 1 : 0;
+	}
+    }
+
+    *ap = a; *bp = b;
+    return result;
+}
+
+static const char *parse_flags(const char *str, uint64_t *flags, uint64_t *mask)
+{
+    uint64_t polarity = -1;
+    uint64_t f = 0, m = 0;
+
+
+    while (1) {
+	char c = *str++;
+	uint64_t bit;
+
+	if (c == '+') {
+	    polarity = -1;
+	} else if (c == '-') {
+	    polarity = 0;
+	} else {
+	    bit = flag_mask(c);
+	    if (!bit)
+		break;
+
+	    m |= bit;
+	    f = (f & ~bit) | (polarity & bit);
+	}
+    }
+
+    *flags = f; *mask = m;
+    return str;
+}
+
+bool match_version(const char *version, const char *pattern)
+{
+    char v, p;
+    const char *vstart, *pstart;
+    uint64_t vflags, pflags, pmask;
+
+    while (1) {
+	v = *version++;
+	p = *pattern++;
+
+	if (v <= ' ' && p <= ' ')
+	    break;
+
+	if (v != p)
+	    return false;
+    }
+
+    if (p < ' ')
+	return true;		/* Project-only pattern */
+
+    if (v != ' ' || *version++ != 'v')
+	return false;		/* Invalid/unparsable version string */
+
+    if (*pattern++ != 'v')
+	return false;		/* Invalid pattern */
+
+    vstart = version;
+    pstart = pattern;
+    if (compare_numbers(&version, &pattern, 0UL) < 0)
+	return false;
+
+    if (*pattern == '-')
+	pattern++;
+    else
+	pattern = pstart;
+    if (compare_numbers(&vstart, &pattern, -1UL) > 0)
+	return false;
+
+    v = *version++;
+    vflags = 0;
+    if (v == ' ') {
+	uint64_t dummy;
+	parse_flags(version, &vflags, &dummy);
+    } else if (v > ' ') {
+	return false;
+    }
+
+    p = *pattern++;
+    pflags = pmask = 0;
+    if (p == ' ') {
+	parse_flags(pattern, &pflags, &pmask);
+    } else if (p > ' ') {
+	return false;
+    }
+
+    return (vflags & pmask) == pflags;
+}
+
+#ifdef COMMAND
+
+#include <stdio.h>
+
+int main(int argc, char *argv[])
+{
+    if (argc != 3) {
+	fprintf(stderr, "Usage: %s version pattern\n", argv[0]);
+	return 127;
+    }
+
+    return !match_version(argv[1], argv[2]);
+}
+
+#endif

+ 80 - 33
esp32/flashesp.pl

@@ -12,6 +12,8 @@ use v5.10;			# For "state"
 my $esptool = ($^O eq 'MSWin32') ? 'esptool.exe' : 'esptool.py';
 $esptool = $ENV{'ESPTOOL'} || $esptool;
 
+my $esp_retries = 5;
+
 my $FW_MAGIC = 0x7a07fbd6;
 
 my %datatypes = (
@@ -23,7 +25,8 @@ my %datatypes = (
     'fpgainit'  => 5,		# FPGA bypass (transient) image during update
     'esppart'   => 6,		# ESP32 partition table
     'espsys'    => 7,		# ESP32 boot loader, OTA control partition...
-    'esptool'   => 8		# esptool.py options for flashing
+    'esptool'   => 8,		# esptool.py options for flashing
+    'boardinfo' => 9		# board_info base address
     );
 my @type;
 foreach my $t (keys(%datatypes)) {
@@ -115,6 +118,79 @@ sub unquote_cmd($) {
     return @a;
 }
 
+# Similar to grep, but for a hash; also filters out 
+sub hgrep(&%) {
+    my($mfunc, %hash) = @_;
+
+    return map { $_ => $hash{$_} } grep(&$mfunc, keys %hash);
+}
+
+# Wrapper for running esptool, returns a hash with info output
+# or dies on failure
+sub hash2opt($)
+{
+    my($h) = @_;
+
+    return () unless (defined($h));
+    return map { $h->{$_} ne '' ? ('--'.$_, $h->{$_}) : () } sort keys %{$h};
+}
+
+sub run_esptool($$$$@)
+{
+    my($port,$common_options,$cmd,$cmd_options,@args) = @_;
+
+    my @espcmd = unquote_cmd($esptool);
+    push(@espcmd, '--p', $port);
+    push(@espcmd, hash2opt($common_options));
+    push(@espcmd, unquote_cmd($cmd));
+    push(@espcmd, hash2opt($cmd_options));
+    push(@espcmd, @args);
+
+    my $retries = $esp_retries;
+    my $ok = 0;
+    my %outinfo;
+    my @output;
+
+    while (!$ok && $retries--) {
+	%outinfo = ();
+	@output  = ();
+
+	print STDERR 'Command: ', join(' ', @espcmd), "\n";
+	print STDERR "Running $espcmd[0] $cmd... ";
+
+	my $esp;
+	if (!open($esp, '-|', @espcmd)) {
+	    print STDERR $!, "\n";
+	    exit 1;
+	}
+
+	while (defined(my $l = <$esp>)) {
+	    if ($l =~ /^Chip is (\S+)/) {
+		$outinfo{'chip'} = $1;
+	    } elsif ($l =~ /^MAC: ([0-9a-f:]+)/) {
+		$outinfo{'mac'} = $1;
+	    }
+	    push(@output, $l);
+	}
+
+	$ok = close($esp);
+	if ($ok) {
+	    print STDERR "ok\n";
+	    last;
+	} elsif ($retries) {
+	    print STDERR "failed, retrying\n";
+	    usleep(1000000);
+	} else {
+	    print STDERR "failed, giving up\n";
+	}
+    }
+
+    print STDERR @output;
+
+    die "$0: $espcmd[0] $cmd failed\n" unless ($ok);
+    return %outinfo;
+}
+
 my @args = @ARGV;
 my $esponly = 0;
 my $file;
@@ -232,38 +308,9 @@ foreach my $e (keys %espopt) {
     }
 }
 
-$espopt{'port'} = $port;
-
-my @espcmd = unquote_cmd($esptool);
-foreach my $o (sort grep (!/^flash_/, keys %espopt)) {
-    if (defined($espopt{$o})) {
-	push(@espcmd, "--$o", $espopt{$o});
-    }
-}
-push(@espcmd, 'write_flash', '-z');
-foreach my $o (sort grep (/^flash_/, keys %espopt)) {
-    if (defined($espopt{$o})) {
-	push(@espcmd, "--$o", $espopt{$o});
-    }
-}
-
-push(@espcmd, @espfiles);
-
-print STDERR join(' ', @espcmd), "\n";
-my $retries = 4;
-my $err = 0;
-while ($retries--) {
-    # Sometimes esptool tanks out for no reason, with error 256
-    $err = system(@espcmd);
-    last if ($err != 256);
-    usleep(1000000);
-}
-if ($err == -1) {
-    print STDERR "$0: $espcmd[0]: $!\n";
-} elsif ($err) {
-    print STDERR "$0: $espcmd[0]: exit $err\n";
-}
-exit $err if ($err || $esponly);
+run_esptool($port, { hgrep sub {!/^flash_/}, %espopt },
+	    'write_flash', { hgrep sub {/^flash_/}, %espopt },
+	    '-z', @espfiles);
 
 my $SerialPort = eval {
     require Device::SerialPort;

+ 169 - 0
esp32/max80/boardinfo.c

@@ -0,0 +1,169 @@
+#include "common.h"
+#include "boardinfo_esp.h"
+
+#include <esp_attr.h>
+#include <esp_flash.h>
+#include <esp_spi_flash.h>
+#include <esp_flash_internal.h>	/* For esp_flash_app_disable_protect() */
+#include <rom/crc.h>
+
+#define BOARDINFO_ADDR	0	/* Flash address on ESP */
+
+union board_info_block {
+    struct board_info i;
+    uint8_t b[BOARDINFO_SIZE];
+};
+
+static const union board_info_block *board_info_flash;
+struct board_info DRAM_ATTR board_info;
+
+static spi_flash_mmap_handle_t board_info_handle;
+
+static const union board_info_block *board_info_map(void)
+{
+    if (!board_info_flash) {
+	const void *bifp;
+
+	if (spi_flash_mmap(BOARDINFO_ADDR, BOARDINFO_SIZE, SPI_FLASH_MMAP_DATA,
+			   &bifp, &board_info_handle)
+	    == ESP_OK)
+	    board_info_flash = bifp;
+    }
+    return board_info_flash;
+}
+
+static void board_info_unmap(void)
+{
+    if (board_info_flash) {
+	spi_flash_munmap(board_info_handle);
+	board_info_flash = NULL;
+	board_info_handle = 0;
+    }
+}
+
+/* Returns 1 if the flash was modified, 0 if unmodified, -1 on error */
+int board_info_init(void)
+{
+    int err = -1;
+    uint32_t crc;
+    const union board_info_block *bif;
+
+    bif = board_info_map();
+    if (!bif)
+	goto unmapped;
+
+    if (bif->i.magic[0] != BOARDINFO_MAGIC_1 ||
+	bif->i.magic[1] != BOARDINFO_MAGIC_2 ||
+	bif->i.len < 16 || bif->i.len > BOARDINFO_SIZE ||
+	board_info.version_str[sizeof board_info.version_str - 1])
+	goto bad;
+
+    memcpy(&board_info, bif, sizeof board_info);
+
+    board_info.crc = 0;
+    crc = crc32_le(0, (const uint8_t *)&board_info, 16);
+    crc = crc32_le(crc, &bif->b[16], bif->i.len - 16);
+    board_info.crc = bif->i.crc;
+
+    if (crc != bif->i.crc)
+	goto bad;
+
+    printf("Board ID/version: %s\n", board_info.version_str);
+    
+    err = 0;
+    goto done;
+
+bad:
+    if (!memcmp(board_info_flash->b, "MAX80 ", 6) &&
+	strnlen(board_info_flash->b, sizeof board_info.version_str)
+	< sizeof board_info.version_str) {
+	/*
+	 * Contains board version string but nothing else; this
+	 * is allowed to simplify the initial programming.
+	 * Convert it to a proper structure and write it back.
+	 */
+	printf("NOTE: updating board information block in flash\n");
+	return board_info_set((const char *)board_info_flash->b);
+    }
+
+unmapped:
+    printf("WARNING: no board ID/version string set in flash\n");
+    board_info.len  = 0;
+    
+done:
+    if (board_info.len < sizeof board_info)
+	memset((char *)&board_info + board_info.len, 0,
+	       sizeof board_info - board_info.len);
+
+    board_info_unmap();
+    return err;
+}
+
+static int board_info_generate(const char *board_id_string)
+{
+    memset(&board_info, 0, sizeof board_info);
+    board_info.magic[0] = BOARDINFO_MAGIC_1;
+    board_info.magic[1] = BOARDINFO_MAGIC_1;
+    board_info.len      = sizeof board_info;
+
+    strncpy(board_info.version_str, board_id_string,
+	    sizeof board_info.version_str - 1);
+
+    memcpy(board_info.mac[0], efuse_default_mac, 6);
+
+    board_info.crc = crc32_le(0, (const uint8_t *)&board_info,
+			      sizeof board_info);
+
+    return 0;			/* For tailcalling convenience */
+}
+
+/* Must be in IRAM to allow flash operations */
+static int __noinline IRAM_ATTR board_info_update_flash(void)
+{
+    esp_err_t err;
+    int rv = -1;
+
+    /* The board_info table is in protected memory */
+    err = esp_flash_app_disable_protect(true);
+    if (err) {
+	printf("board_info_set: failed to unprotect flash (err 0x%x)\n", err);
+	goto err1;
+    }
+    
+    err = esp_flash_erase_region(NULL, BOARDINFO_ADDR, BOARDINFO_SIZE);
+    if (err) {
+	printf("board_info_set: failed to erase board info flash region (err 0x%x)\n", err);
+	goto err2;
+    }
+
+    err = esp_flash_write(NULL, &board_info, BOARDINFO_ADDR, sizeof board_info);
+    if (err) {
+	printf("board_info_set: failed to write board info flash region (err 0x%x)\n", err);
+	goto err2;
+    }
+
+    rv = 0;
+    
+err2:
+    esp_flash_app_disable_protect(false);
+err1:
+    return rv;
+}
+
+int board_info_set(const char *board_id_string)
+{
+    const union board_info_block *bif;
+
+    board_info_generate(board_id_string);
+    bif = board_info_map();
+    if (!bif)
+	return -1;		/* Could not map board info */
+    
+    bool unchanged = !memcmp(bif, &board_info, sizeof board_info);
+    board_info_unmap();
+
+    if (unchanged)
+	return 0;		/* Not modified, so don't reflash */
+
+    board_info_update_flash();
+}

+ 7 - 0
esp32/max80/boardinfo_esp.h

@@ -0,0 +1,7 @@
+#pragma once
+
+#include "common.h"
+#include "boardinfo.h"
+
+extern_c int board_info_init(void);
+extern_c int board_info_set(const char *board_id);

+ 2 - 2
esp32/max80/common.h

@@ -75,9 +75,9 @@ extern_c void reboot_now(void);
 extern_c int reboot_delayed(void);
 
 /*
- * Board version
+ * Main MAC address from efuses
  */
-extern_c uint8_t max80_board_version;
+extern_c uint8_t efuse_default_mac[6];
 
 /*
  * Time sync status

+ 7 - 1
esp32/max80/max80.ino

@@ -9,6 +9,7 @@
 #include "config.h"
 #include "led.h"
 #include "tty.h"
+#include "boardinfo_esp.h"
 
 #include <freertos/task_snapshot.h>
 #include <esp_heap_caps.h>
@@ -21,9 +22,11 @@
 #define PIN_USB_PWR_EN		7
 #define PIN_USB_PWR_SINK	8
 
+uint8_t efuse_default_mac[6];
+
 void setup_usb_ids()
 {
-    uint8_t mac[8];
+    uint8_t * const mac = efuse_default_mac;
     char serial[16];
 
     esp_efuse_mac_get_default(mac);
@@ -65,6 +68,9 @@ static void init_hw()
     for (int i = 1; i <= 18; i++)
 	pinMode(i, INPUT);
 
+    // Query board info
+    board_info_init();
+
     // Make sure FPGA nCE input (board 2.1+) is not accidentally pulled high
     fpga_enable_nce();
 

+ 3 - 5
esp32/max80/tty.cpp

@@ -7,11 +7,11 @@
 #include "config.h"
 #include "fw.h"
 
-#include "rom/crc.h"
-
 #include <USB.h>
 #include <HardwareSerial.h>
 
+#include <rom/crc.h>
+
 #define SOH  '\001'
 #define STX  '\002'
 #define ETX  '\003'
@@ -36,7 +36,7 @@
 #define STREAMBUF_SIZE	2048
 #define BUF_SLACK       (STREAMBUF_SIZE >> 1)
 
-static char enq_str[] = "\026\035MAX80 v0\004\r\n";
+static char enq_str[] = "\026\035MAX80 ready\004\r\n";
 static const char fwupload_start[] =
     "\034\001: /// MAX80 FW UPLOAD ~@~ $\r\n\035";
 
@@ -446,8 +446,6 @@ static void uart_flush()
 
 void TTY::init()
 {
-    enq_str[sizeof(enq_str)-5] += max80_board_version;
-
     uart_tty = new TTY(Serial0);
     uart_tty->_flush = uart_flush;
     Serial0.begin(BAUD_RATE);

+ 4 - 2
esp32/max80/wifi.cpp

@@ -384,8 +384,10 @@ static void wifi_config_ap(void)
 
     WiFi.softAPmacAddress(mac);
     setenv_mac("status.net.ap.mac", mac);
-    /* The last two bytes of the MAC */
-    snprintf(ap_ssid, sizeof ap_ssid, "MAX80_%02X%02X", mac[4], mac[5]);
+
+    /* The last two bytes of the efuse MAC */
+    snprintf(ap_ssid, sizeof ap_ssid, "MAX80_%02X%02X",
+	     efuse_default_mac[4], efuse_default_mac[5]);
 
     printf("[WIFI] AP SSID %s IP %s netmask %s channel %u\n",
 	       ap_ssid, AP_IP.toString(), AP_Netmask.toString(), channel);

BIN
esp32/output/max80.ino.bin


+ 43 - 13
fpga/Makefile

@@ -1,10 +1,13 @@
 MAKEFLAGS      += -R -r
 
 PROJECT		= max80
-REVISIONS	= v1 v2 bypass
+REVISIONS	= v1 v2
+EXTRAREVS       = bypass
 VARIANTS        =
 QU		= quartus
 
+ALLREVS         = $(REVISIONS) $(EXTRAREVS)
+
 # Common options for all Quartus tools
 QPRI		= --lower_priority
 QCPF		= $(QU)_cpf $(QPRI)
@@ -32,7 +35,7 @@ BUILDDATE      := $(shell LC_ALL=C date)
 
 SUBDIRS     = usb
 PREREQFILES = $(mifdir)/sram.mif \
-	      $(foreach rev,$(REVISIONS),$(foreach coffmt,jic pof, \
+	      $(foreach rev,$(ALLREVS),$(foreach coffmt,jic pof, \
 		$(outdir)/$(rev).$(coffmt).cof))
 
 alltarg := sof pof jic svf svf.gz xsvf xsvf.gz rbf rbf.gz rpf rpf.gz \
@@ -45,6 +48,10 @@ allout   = $(foreach o,$(alltarg),$(outdir)/$(1).$(o))
 
 sram_src = ../rv32/
 
+MKFW		  = ../tools/mkfwimage.pl
+
+RPF_ADDR	 := 0
+
 DRAM_ADDR        := 0x100000
 DRAM_IMAGE       := ../rv32/dram.bin
 
@@ -60,6 +67,10 @@ ESP_TOOL_OPT     := ../esp32/esptool.opt
 
 IDCODE     := 0x020f20dd		# JTAG IDCODE of FPGA
 
+BOARDINFO_ADDR   := 0xff000
+
+FWDEPS = $(MKFW) $(DRAM_IMAGE) $(ESP_IMAGE) $(ESP_BOOT_IMAGE) \
+	 $(ESP_PART_IMAGE) $(ESP_OTACTL_IMAGE) 
 
 .SUFFIXES:
 
@@ -68,18 +79,18 @@ IDCODE     := 0x020f20dd		# JTAG IDCODE of FPGA
 .DELETE_ON_ERROR:
 
 all: prereq
-	$(MAKE) $(REVISIONS:=.targets)
+	$(MAKE) $(ALLREVS:=.targets)
 	$(MAKE) fwimages
 #	$(MAKE) $(VARIANTS)
-#	$(MAKE) $(REVISIONS:=.update)
+#	$(MAKE) $(ALLREVS:=.update)
 
--include $(REVISIONS:=.deps)
+-include $(ALLREVS:=.deps)
 
 .PHONY: fwimages
-fwimages: $(patsubst %,$(outdir)/%.fw,$(filter-out bypass,$(REVISIONS)))
+fwimages: $(patsubst %,$(outdir)/%.fw,$(REVISIONS) $(PROJECT))
 
-.PHONY: $(REVISIONS)
-$(REVISIONS): prereq
+.PHONY: $(ALLREVS)
+$(ALLREVS): prereq
 	$(MAKE) $@.targets
 
 .PHONY: %.targets
@@ -99,7 +110,7 @@ $(VARIANTS):
 	$(MAKE) --old-file=$(outdir)/$*.fit.rpt $(call varout,$*)
 
 $(outdir)/%/variant.stamp: mif/%.mif \
-			   $(foreach rev,$(REVISIONS),$(outdir)/$(rev).fit.rpt)
+			   $(foreach rev,$(ALLREVS),$(outdir)/$(rev).fit.rpt)
 	-rm -rf var/$* $(outdir)/$*
 	mkdir -p var/$* var/$*/mif $(outdir)/$*
 	( cd $(outdir)/$* && ln -sf ../*.asm.rpt ../*.fit.rpt . )
@@ -107,7 +118,7 @@ $(outdir)/%/variant.stamp: mif/%.mif \
 		db incremental_db var/$*/
 	ln -f $< var/$*/mif/sram.mif
 	( cd var/$* && ln -sf ../../$(outdir)/$* ./output )
-	$(MAKE) -C var/$* $(REVISIONS:=.vtargets)
+	$(MAKE) -C var/$* $(ALLREVS:=.vtargets)
 	echo '$(BUILDDATE)' > $@
 
 $(outdir)/%.map.rpt: %.qsf | $(mifdir)/sram.mif
@@ -139,7 +150,7 @@ $(outdir)/%.sta.rpt: $(outdir)/%.fit.rpt | $(outdir)/%.sof
 $(outdir)/%.pow.rpt: $(outdir)/%.sta.rpt | $(outdir)/%.sof
 	$(QPOW) $(PROJECT) -c $*
 
-$(foreach rev,$(REVISIONS),$(outdir)/$(rev).%.cof): %.cof.xml $(outdir)
+$(foreach rev,$(ALLREVS),$(outdir)/$(rev).%.cof): %.cof.xml $(outdir)
 	$(SED) -e 's/@@PROJECT@@/$(@F:.$*.cof=)/g' $< > $@
 
 $(outdir)/%.jic: $(outdir)/%.jic.cof $(outdir)/%.sof ../rv32/dram.hex
@@ -180,14 +191,33 @@ $(outdir)/%.rpf: $(outdir)/%.rpd $(outdir)/%.z.rbf
 $(outdir)/%.gz: $(outdir)/%
 	$(GZIP) -9 < $< > $@
 
+# New-style combined firmware image
+ALLRPFS   = $(foreach rev,$(REVISIONS),$(outdir)/$(rev).rpf)
+everyrev  = $(shell n=1; for r in $(REVISIONS); do echo "$(1)" -ver 0,0,$$n,$$n "$(2)"; n=$$((n+1)); done)
+$(outdir)/$(PROJECT).fw: $(ALLRPFS) $(FWDEPS)
+	$(PERL) $(MKFW) -o - -fwmin 2 \
+		$(call everyrev,-type target,-str 'MAX80 $$r') \
+		-type fpgainit -addr $(IDCODE) -file $(outdir)/bypass.rbf \
+		$(call everyrev,-type data -addr $(RPF_ADDR),-file $(outdir)/$$r.rpf) \
+		-type data -addr $(DRAM_ADDR) -file $(DRAM_IMAGE) \
+		-type boardinfo -addr $(BOARDINFO_ADDR) -empty \
+		-type esptool -optional -file $(ESP_TOOL_OPT) \
+		-type espota -addr $(ESP_ADDR) -file $(ESP_IMAGE) \
+		-type esppart -addr $(ESP_PART_ADDR) -file $(ESP_PART_IMAGE) \
+		-type espsys -addr $(ESP_BOOT_ADDR) -file $(ESP_BOOT_IMAGE) \
+		-type espsys -addr $(ESP_OTACTL_ADDR) -file $(ESP_OTACTL_IMAGE) \
+	| $(GZIP) -9 > $@
+
+# Old-style single version firmware images
 $(outdir)/%.fw: $(outdir)/%.rpf $(outdir)/bypass.rbf \
 		$(DRAM_IMAGE) $(ESP_IMAGE) \
 		../tools/mkfwimage.pl
 	$(PERL) ../tools/mkfwimage.pl -o - \
 		-type target -str 'MAX80 $*' \
 		-type fpgainit -addr $(IDCODE) -optional -file $(outdir)/bypass.rbf \
-		-type data -addr 0 -file $< \
+		-type data -addr $(RPF_ADDR) -file $< \
 		-type data -addr $(DRAM_ADDR) -file $(DRAM_IMAGE) \
+		-type boardinfo -optional -addr $(BOARDINFO_ADDR) -empty \
 		-type esptool -optional -file $(ESP_TOOL_OPT) \
 		-type espota -addr $(ESP_ADDR) -file $(ESP_IMAGE) \
 		-type esppart -optional -addr $(ESP_PART_ADDR) -file $(ESP_PART_IMAGE) \
@@ -235,7 +265,7 @@ clean:
 	for d in $(SUBDIRS); do $(MAKE) -C $$d clean; done
 	rm -rf db incremental_db var simulation/modelsim \
 	greybox_tmp */greybox_tmp iodevs.vh output_files $(mifdir)
-	for d in $(REVISIONS); do \
+	for d in $(ALLREVS); do \
 		rm -f $$d/*.rpt $$d/*.rpt $$d/*.summary $$d/*.smsg  \
 		$$d/*.htm $$d/*.htm_files $$d/*.map $$d/*.eqn $$d/*.sld \
 		$$d/*.done ; \

+ 2 - 2
fpga/max80.qpf

@@ -19,12 +19,12 @@
 #
 # Quartus Prime
 # Version 21.1.0 Build 842 10/21/2021 SJ Lite Edition
-# Date created = 11:36:55  August 08, 2022
+# Date created = 12:16:48  August 18, 2022
 #
 # -------------------------------------------------------------------------- #
 
 QUARTUS_VERSION = "21.1"
-DATE = "11:36:55  August 08, 2022"
+DATE = "12:16:48  August 18, 2022"
 
 # Revisions
 

BIN
fpga/output/bypass.jic


BIN
fpga/output/bypass.rpd.gz


BIN
fpga/output/max80.fw


BIN
fpga/output/v1.fw


BIN
fpga/output/v1.jic


BIN
fpga/output/v1.rbf.gz


BIN
fpga/output/v1.rpd.gz


BIN
fpga/output/v1.sof


BIN
fpga/output/v1.svf.gz


BIN
fpga/output/v1.xsvf.gz


BIN
fpga/output/v2.fw


BIN
fpga/output/v2.jic


BIN
fpga/output/v2.rbf.gz


BIN
fpga/output/v2.rpd.gz


BIN
fpga/output/v2.sof


BIN
fpga/output/v2.svf.gz


BIN
fpga/output/v2.xsvf.gz


+ 1 - 1
rv32/checksum.h

@@ -1,4 +1,4 @@
 #ifndef CHECKSUM_H
 #define CHECKSUM_H
-#define SDRAM_SUM 0xa53dbdef
+#define SDRAM_SUM 0x1ce2ddf4
 #endif

+ 8 - 2
rv32/spiflash.c

@@ -627,6 +627,7 @@ static int spiflash_process_chunk(spz_stream *spz)
 	return spiflash_flash_chunk(spz);
     case FDT_TARGET:
 	str = spiflash_read_chunk_str(spz);
+	/* XXX: replace with proper matching algorithm */
 	if (!str || strcmp(str, spz->flash->target)) {
 	    MSG("update: this firmware file targets \"%s\", need \"%s\"\n",
 		str, spz->flash->target);
@@ -637,12 +638,17 @@ static int spiflash_process_chunk(spz_stream *spz)
 	str = spiflash_read_chunk_str(spz);
 	MSG("update: %s\n", str);
 	break;
-    case FDT_FPGA_INIT:		/* Used only when flashing from ESP32 */
-	return spiflash_skip_chunk(spz);
     case FDT_ESP_OTA:
 	if (!spz->flash->ops)
 	    goto skip;
 	return esp_ota_chunk(spz);
+    case FDT_FPGA_INIT:
+    case FDT_ESP_PART:
+    case FDT_ESP_SYS:
+    case FDT_ESP_TOOL:
+    case FDT_BOARDINFO:
+	/* Used only when flashing from ESP32 */
+	goto skip;
     default:
 	if (spz->header.flags & FDF_OPTIONAL)
 	    goto skip;

+ 93 - 52
tools/mkfwimage.pl

@@ -3,18 +3,19 @@
 use strict;
 use integer;
 
-my $FW_MAGIC = 0x7a07fbd6;
+my @FW_MAGIC = (undef, 0x7a07fbd6, 0xa924ed0b);
 
 my %datatypes = (
     'end'       => 0,		# End of data
     'data'      => 1,		# FPGA flash data
-    'target'    => 2,		# Firmware target string
+    'target'    => 2,		# Firmware target mask and string
     'note'      => 3,		# Informative string
     'espota'    => 4,		# ESP32 OTA image
     'fpgainit'  => 5,		# FPGA bypass (transient) image during update
     'esppart'   => 6,		# ESP32 partition table
     'espsys'    => 7,		# ESP32 boot loader, OTA control partition...
-    'esptool'   => 8		# esptool.py options for flashing
+    'esptool'   => 8,		# esptool.py options for flashing
+    'boardinfo' => 9		# board_info block address (FPGA)
     );
 my @type;
 foreach my $t (keys(%datatypes)) {
@@ -25,29 +26,15 @@ my $FDF_OPTIONAL = 0x0001;
 
 my $STRING_MAX_LEN = 4095;
 
+my %int_shifts = ('' => 0, 'k' => 10, 'm' => 20, 'g' => 30,
+		  't' => 40, 'p' => 50, 'e' => 60);
 sub getint($) {
     my($s) = @_;
 
     return undef
-	unless ($s =~ /^(([1-9][0-9]+)|(0(x[0-9a-f]+|[0-7]*)))([kmgtpe]?)$/i);
-
-    my $o = oct($3) + $2;
-    my $p = lc($5);
-
-    if ($p eq 'k') {
-	$o <<= 10;
-    } elsif ($p eq 'm') {
-	$o <<= 20;
-    } elsif ($p eq 'g') {
-	$o <<= 30;
-    } elsif ($p eq 't') {
-	$o <<= 40;
-    } elsif ($p eq 'p') {
-	$o <<= 50;
-    } elsif ($p eq 'e') {
-	$o <<= 60;
-    }
-    return $o;
+	unless ($s =~ /^(([1-9][0-9]*)|(0(x[0-9a-f]+|[0-7]*)))([kmgtpe]?)$/i);
+
+    return (oct($3) + $2) << $int_shifts{lc($5)};
 }
 sub filelen($) {
     my($f) = @_;
@@ -55,31 +42,78 @@ sub filelen($) {
 
     return $s[7];
 }
-sub output_chunk($$$) {
-    my($out,$data,$options) = @_;
-
-    print $out pack("VvvVV", $FW_MAGIC,
-		    $options->{'type'}, $options->{'flags'},
-		    length($data), $options->{'addr'});
-    printf STDERR "chunk: type %s (%u) flags 0x%x length %u addr 0x%x\n",
-	$type[$options->{'type'}], $options->{'type'}, $options->{'flags'},
-	length($data), $options->{'addr'};
+
+our $outfile;
+
+my %default_options = (
+    'fw'      => 1,		# Default and minimum firmware version
+    'type'    => $datatypes{'data'},
+    'addr'    => 0,
+    'flags'   => 0,
+    'vmatch'  => 0,
+    'vmask'   => 0,
+    'vmin'    => 0,
+    'vmax'    => 0xffff
+);
+my %need_versions = (
+    'vmatch' => 2, 'vmask' => 2, 'vmin' => 2, 'vmax' => 2
+);
+
+sub output_chunk($$%) {
+    my($out,$data,%opts) = @_;
+
+    foreach my $o (keys(%default_options)) {
+	$opts{$o} = $default_options{$o} unless (defined($opts{$o}));
+    }
+
+    $opts{'vmatch'} &= 0xffffffff;
+    $opts{'vmask'}  &= 0xffffffff;
+    $opts{'vmin'}   &= 0xffff;
+    $opts{'vmax'}   &= 0xffff;
+
+    my $version = $opts{'fw'};
+    foreach my $o (keys(%need_versions)) {
+	if ($opts{$o} ne $default_options{$o} &&
+	    $need_versions{$o} > $version) {
+		$version = $need_versions{$o};
+	}
+    }
+
+    if (!defined($FW_MAGIC[$version])) {
+	die "$0: $outfile: invalid firmware format version: $version\n";
+    }
+
+    print $out pack('VvvVV',
+		    $FW_MAGIC[$version],
+		    $opts{'type'}, $opts{'flags'},
+		    length($data), $opts{'addr'});
+    if ($version >= 2) {
+	print $out pack('VVvvV',
+			$opts{'vmatch'}, $opts{'vmask'},
+			$opts{'vmin'}, $opts{'vmax'},
+			0);
+    }
+    printf STDERR "chunk: fw %u type %s (%u) flags 0x%x length %u addr 0x%x ver 0x%x/0x%x,%u:%u\n",
+	$version, $type[$opts{'type'}], $opts{'type'},
+	$opts{'flags'},
+	length($data), $opts{'addr'},
+	$opts{'vmatch'}, $opts{'vmask'}, $opts{'vmin'}, $opts{'vmax'};
     print $out $data;
 }
 
 if (!scalar(@ARGV)) {
-    die "Usage: $0 [-o outfile] [options command]...\n".
+    die "Usage: $0 [-o outfile] [-fwmin ver] [options command]...\n".
 	"Options:\n".
 	"\t-type datatype\n".
 	"\t-addr address (or equivalent)\n".
 	"\t-optional\n".
 	"\t-required\n".
+	"\t-ver match,mask,min,max\n".
 	"Commands:\n".
 	"\t-file data_file\n".
 	"\t-str data_string\n";
 }
 
-our $outfile;
 our $out;
 
 sub delete_out {
@@ -106,13 +140,8 @@ if ($outfile ne '' && $outfile ne '-') {
 
 binmode $out;
 
-my %default_options = {
-    'type'  => $datatypes{'data'},
-    'addr'  => 0,
-    'flags' => 0
-};
 my $err;
-my %options = %default_options;
+my %options = ();
 while (1) {
     my $what = shift @ARGV;
 
@@ -120,13 +149,24 @@ while (1) {
 
     if ($what eq '-type') {
 	my $arg = lc(shift @ARGV);
-	$options{'type'} = $datatypes{$arg} || getint($arg);
-	if (!$arg) {
+	my $type = defined($datatypes{$arg}) ? $datatypes{$arg} : getint($arg);
+	if (!defined($type)) {
 	    die "$0: invalid data type: $arg";
 	}
-    } elsif ($what eq '-addr') {
-	my $arg = shift @ARGV;
-	$options{'addr'} = getint($arg);
+	$options{'type'} = $type;
+    } elsif ($what =~ /^\-(addr|flags|vmatch|vmask|vmin|vmax)$/) {
+	my $opt = $1;
+	my $arg = getint(shift @ARGV);
+	$options{$opt} = $arg if (defined($arg));
+    } elsif ($what eq '-fwmin') {
+	my $arg = getint(shift @ARGV);
+	$default_options{'fw'} = $arg if (defined($arg));
+    } elsif ($what eq '-ver') {
+	my @vp = split(/(?:[,:\/]|\.+)/, shift @ARGV);
+	foreach my $opt (qw(vmatch vmask vmin vmax)) {
+	    my $arg = getint(shift @vp);
+	    $options{$opt} = $arg if (defined($arg));
+	}
     } elsif ($what eq '-optional') {
 	$options{'flags'} |= $FDF_OPTIONAL;
     } elsif ($what eq '-required') {
@@ -144,23 +184,24 @@ while (1) {
 	my $dlen = read($in, $data, $is[7]);
 	close($in);
 
-	output_chunk($out, $data, \%options);
-	undef $data;
-
-	%options = %default_options;
+	output_chunk($out, $data, %options);
+	%options = ();
     } elsif ($what eq '-str') {
 	my $str = shift @ARGV;
 
 	if (length($str) > $STRING_MAX_LEN) {
-	    die "$0: string too long\n";
+	    die "$0: $outfile: string too long\n";
 	}
 	
-	output_chunk($out, $str, \%options);
-	%options = %default_options;
+	output_chunk($out, $str, %options);
+	%options = ();
+    } elsif ($what eq '-empty') {
+	output_chunk($out, '', %options);
+	%options = ();
     } else {
 	die "$0: unknown argument: $what\n";
     }
 }
 
-output_chunk($out, '', {'type' => $datatypes{'end'}});
+output_chunk($out, '', ('type' => $datatypes{'end'}));
 close($out);