firmware/ice40-riscv: Import common parts to all iCE40/RISC-V firmwares

Signed-off-by: Sylvain Munaut <tnt@246tNt.com>
diff --git a/firmware/ice40-riscv/common/bin2hex.py b/firmware/ice40-riscv/common/bin2hex.py
new file mode 100755
index 0000000..43ba263
--- /dev/null
+++ b/firmware/ice40-riscv/common/bin2hex.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python3
+#
+# Converts binary into something that can be used by `readmemh`
+#
+# Copyright (C) 2020 Sylvain Munaut <tnt@246tNt.com>
+# SPDX-License-Identifier: MIT
+#
+
+import struct
+import sys
+
+
+def main(argv0, in_name, out_name):
+	with open(in_name, 'rb') as in_fh, open(out_name, 'w') as out_fh:
+		while True:
+			b = in_fh.read(4)
+			if len(b) < 4:
+				break
+			out_fh.write('%08x\n' % struct.unpack('<I', b))
+
+if __name__ == '__main__':
+	main(*sys.argv)
diff --git a/firmware/ice40-riscv/common/console.c b/firmware/ice40-riscv/common/console.c
new file mode 100644
index 0000000..c5a9136
--- /dev/null
+++ b/firmware/ice40-riscv/common/console.c
@@ -0,0 +1,76 @@
+/*
+ * console.c
+ *
+ * Copyright (C) 2019-2020  Sylvain Munaut <tnt@246tNt.com>
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#include <stdint.h>
+
+#include "config.h"
+#include "mini-printf.h"
+
+
+struct wb_uart {
+	uint32_t data;
+	uint32_t clkdiv;
+} __attribute__((packed,aligned(4)));
+
+static volatile struct wb_uart * const uart_regs = (void*)(UART_BASE);
+
+
+static char _printf_buf[128];
+
+void console_init(void)
+{
+#ifdef BOARD_E1_TRACER
+	uart_regs->clkdiv = 22;	/* ~1 Mbaud with clk=24MHz */
+#else
+	uart_regs->clkdiv = 29;	/* ~1 Mbaud with clk=30.72MHz */
+#endif
+}
+
+char getchar(void)
+{
+	int32_t c;
+	do {
+		c = uart_regs->data;
+	} while (c & 0x80000000);
+	return c;
+}
+
+int getchar_nowait(void)
+{
+	int32_t c;
+	c = uart_regs->data;
+	return c & 0x80000000 ? -1 : (c & 0xff);
+}
+
+void putchar(char c)
+{
+	uart_regs->data = c;
+}
+
+void puts(const char *p)
+{
+	char c;
+	while ((c = *(p++)) != 0x00) {
+		if (c == '\n')
+			uart_regs->data = '\r';
+		uart_regs->data = c;
+	}
+}
+
+int printf(const char *fmt, ...)
+{
+        va_list va;
+        int l;
+
+        va_start(va, fmt);
+        l = mini_vsnprintf(_printf_buf, 128, fmt, va);
+        va_end(va);
+
+	puts(_printf_buf);
+
+	return l;
+}
diff --git a/firmware/ice40-riscv/common/console.h b/firmware/ice40-riscv/common/console.h
new file mode 100644
index 0000000..2645927
--- /dev/null
+++ b/firmware/ice40-riscv/common/console.h
@@ -0,0 +1,16 @@
+/*
+ * console.h
+ *
+ * Copyright (C) 2019-2020  Sylvain Munaut <tnt@246tNt.com>
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+void console_init(void);
+
+char getchar(void);
+int  getchar_nowait(void);
+void putchar(char c);
+void puts(const char *p);
+int  printf(const char *fmt, ...);
diff --git a/firmware/ice40-riscv/common/dma.c b/firmware/ice40-riscv/common/dma.c
new file mode 100644
index 0000000..a2c81cc
--- /dev/null
+++ b/firmware/ice40-riscv/common/dma.c
@@ -0,0 +1,69 @@
+/*
+ * dma.c
+ *
+ * Copyright (C) 2019-2020  Sylvain Munaut <tnt@246tNt.com>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "config.h"
+#include "dma.h"
+
+
+struct dma {
+	uint32_t csr;
+	uint32_t _rsvd;
+	uint32_t addr_e1;
+	uint32_t addr_usb;
+} __attribute__((packed,aligned(4)));
+
+#define DMA_CSR_GO		(1 << 15)
+#define DMA_CSR_BUSY		(1 << 15)
+#define DMA_DIR_E1_TO_USB	(0 << 14)
+#define DMA_DIR_USB_TO_E1	(1 << 14)
+#define DMA_CSR_LEN(x)		(((x)-2) & 0x1fff)
+
+static volatile struct dma * const dma_regs = (void*)(DMA_BASE);
+
+static struct {
+	bool pending;
+	dma_cb cb_fn;
+	void *cb_data;
+} g_dma;
+
+
+bool
+dma_ready(void)
+{
+	return !(dma_regs->csr & DMA_CSR_BUSY);
+}
+
+void
+dma_exec(unsigned addr_e1, unsigned addr_usb, unsigned len, bool dir,
+         dma_cb cb_fn, void *cb_data)
+{
+	dma_regs->addr_e1  = addr_e1;
+	dma_regs->addr_usb = addr_usb;
+	dma_regs->csr =
+		DMA_CSR_GO |
+		(dir ? DMA_DIR_USB_TO_E1 : DMA_DIR_E1_TO_USB) |
+		DMA_CSR_LEN(len);
+
+	g_dma.pending = true;
+	g_dma.cb_fn = cb_fn;
+	g_dma.cb_data = cb_data;
+}
+
+bool
+dma_poll(void)
+{
+	if (g_dma.pending && dma_ready()) {
+		g_dma.pending = false;
+		if (g_dma.cb_fn)
+			g_dma.cb_fn(g_dma.cb_data);
+	}
+
+	return g_dma.pending;
+}
diff --git a/firmware/ice40-riscv/common/dma.h b/firmware/ice40-riscv/common/dma.h
new file mode 100644
index 0000000..1793750
--- /dev/null
+++ b/firmware/ice40-riscv/common/dma.h
@@ -0,0 +1,23 @@
+/*
+ * dma.h
+ *
+ * Copyright (C) 2019-2020  Sylvain Munaut <tnt@246tNt.com>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <stdbool.h>
+
+typedef void (*dma_cb)(void *);
+
+/* Direction
+ *  0 is E1  to USB
+ *  1 is USB to E1
+ */
+
+bool dma_ready(void);
+void dma_exec(unsigned addr_e1, unsigned addr_usb, unsigned len, bool dir,
+              dma_cb cb_fn, void *cb_data);
+
+bool dma_poll(void);
diff --git a/firmware/ice40-riscv/common/led.c b/firmware/ice40-riscv/common/led.c
new file mode 100644
index 0000000..3ae298a
--- /dev/null
+++ b/firmware/ice40-riscv/common/led.c
@@ -0,0 +1,167 @@
+/*
+ * led.c
+ *
+ * Copyright (C) 2019-2020  Sylvain Munaut <tnt@246tNt.com>
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "config.h"
+#include "led.h"
+
+
+struct ledda_ip {
+	uint32_t _rsvd0;
+	uint32_t pwrr;		/* 0001 LEDDPWRR - Pulse Width Register Red   */
+	uint32_t pwrg;		/* 0010 LEDDPWRG - Pulse Width Register Green */
+	uint32_t pwrb;		/* 0011 LEDDPWRB - Pulse Width Register Blue  */
+	uint32_t _rsvd1;
+	uint32_t bcrr;		/* 0101 LEDDBCRR - Breathe Control Rise Register */
+	uint32_t bcfr;		/* 0101 LEDDBCFR - Breathe Control Fall Register */
+	uint32_t _rsvd2;
+	uint32_t cr0;		/* 1000 LEDDCR0  - Control Register 0 */
+	uint32_t br;		/* 1001 LEDDBR   - Pre-scale Register */
+	uint32_t onr;		/* 1010 LEDONR   - ON  Time Register */
+	uint32_t ofr;		/* 1011 LEDOFR   - OFF Time Register */
+} __attribute__((packed,aligned(4)));
+
+#define LEDDA_IP_CR0_LEDDEN		(1 << 7)
+#define LEDDA_IP_CR0_FR250		(1 << 6)
+#define LEDDA_IP_CR0_OUTPOL		(1 << 5)
+#define LEDDA_IP_CR0_OUTSKEW		(1 << 4)
+#define LEDDA_IP_CR0_QUICK_STOP		(1 << 3)
+#define LEDDA_IP_CR0_PWM_LINEAR		(0 << 2)
+#define LEDDA_IP_CR0_PWM_LFSR		(1 << 2)
+#define LEDDA_IP_CR0_SCALE_MSB(x)	(((x) >> 8) & 3)
+
+#define LEDDA_IP_BR_SCALE_LSB(x)	((x) & 0xff)
+
+#define LEDDA_IP_ONOFF_TIME_MS(x)	(((x) >> 5) & 0xff)	/*  32ms interval up to 8s */
+
+#define LEDDA_IP_BREATHE_ENABLE		(1 << 7)
+#define LEDDA_IP_BREATHE_MODULATE	(1 << 5)
+#define LEDDA_IP_BREATHE_TIME_MS(x)	(((x) >> 7) & 0x0f)	/* 128ms interval up to 2s */
+
+
+struct led {
+	uint32_t csr;
+	uint32_t _rsvd[15];
+	struct ledda_ip ip;
+} __attribute__((packed,aligned(4)));
+
+#define LED_CSR_LEDDEXE		(1 << 1)
+#define LED_CSR_RGBLEDEN	(1 << 2)
+#define LED_CSR_CURREN		(1 << 3)
+
+
+static volatile struct led * const led_regs = (void*)(LED_BASE);
+
+static const uint32_t led_cr0_base =
+	LEDDA_IP_CR0_FR250 |
+	LEDDA_IP_CR0_OUTSKEW |
+	LEDDA_IP_CR0_QUICK_STOP |
+	LEDDA_IP_CR0_PWM_LFSR |
+	LEDDA_IP_CR0_SCALE_MSB(480);
+
+
+void
+led_init(void)
+{
+	led_regs->ip.pwrr = 0;
+	led_regs->ip.pwrg = 0;
+	led_regs->ip.pwrb = 0;
+
+	led_regs->ip.bcrr = 0;
+	led_regs->ip.bcfr = 0;
+
+	led_regs->ip.onr = 0;
+	led_regs->ip.ofr = 0;
+
+	led_regs->ip.br = LEDDA_IP_BR_SCALE_LSB(480);
+	led_regs->ip.cr0 = led_cr0_base;
+
+	led_regs->csr = LED_CSR_LEDDEXE | LED_CSR_RGBLEDEN | LED_CSR_CURREN;
+}
+
+void
+led_color(uint8_t r, uint8_t g, uint8_t b)
+{
+#if defined(BOARD_ICE1USB)
+	// icE1usb
+	led_regs->ip.pwrr = b;
+	led_regs->ip.pwrg = g;
+	led_regs->ip.pwrb = r;
+#elif defined(BOARD_ICE1USB_PROTO_ICEBREAKER)
+	// iCEBreaker v1.0b tnt
+	led_regs->ip.pwrr = r;
+	led_regs->ip.pwrg = b;
+	led_regs->ip.pwrb = g;
+/*
+	// iCEBreaker v1.0c+
+	led_regs->ip.pwrr = b;
+	led_regs->ip.pwrg = g;
+	led_regs->ip.pwrb = r;
+ */
+#elif defined(BOARD_ICE1USB_PROTO_BITSY)
+	// iCEBreaker bitsy v0 (RGB led 'hacked on')
+	led_regs->ip.pwrr = g;
+	led_regs->ip.pwrg = r;
+	led_regs->ip.pwrb = b;
+#elif defined(BOARD_E1_TRACER)
+	// E1 tracer
+	led_regs->ip.pwrr = b;
+	led_regs->ip.pwrg = g;
+	led_regs->ip.pwrb = r;
+#else
+	// Default / Unknown
+	led_regs->ip.pwrr = r;
+	led_regs->ip.pwrg = g;
+	led_regs->ip.pwrb = b;
+#endif
+}
+
+void
+led_state(bool on)
+{
+	if (on)
+		led_regs->ip.cr0 = led_cr0_base | LEDDA_IP_CR0_LEDDEN;
+	else
+		led_regs->ip.cr0 = led_cr0_base;
+}
+
+void
+led_blink(bool enabled, int on_time_ms, int off_time_ms)
+{
+	/* Disable EXE before doing any change */
+	led_regs->csr = LED_CSR_RGBLEDEN | LED_CSR_CURREN;
+
+	/* Load new config */
+	if (enabled) {
+		led_regs->ip.onr = LEDDA_IP_ONOFF_TIME_MS(on_time_ms);
+		led_regs->ip.ofr = LEDDA_IP_ONOFF_TIME_MS(off_time_ms);
+	} else {
+		led_regs->ip.onr = 0;
+		led_regs->ip.ofr = 0;
+	}
+
+	/* Re-enable execution */
+	led_regs->csr = LED_CSR_LEDDEXE | LED_CSR_RGBLEDEN | LED_CSR_CURREN;
+}
+
+void
+led_breathe(bool enabled, int rise_time_ms, int fall_time_ms)
+{
+	if (enabled) {
+		led_regs->ip.bcrr = LEDDA_IP_BREATHE_ENABLE |
+		                    LEDDA_IP_BREATHE_MODULATE |
+		                    LEDDA_IP_BREATHE_TIME_MS(rise_time_ms);
+		led_regs->ip.bcfr = LEDDA_IP_BREATHE_ENABLE |
+		                    LEDDA_IP_BREATHE_MODULATE |
+		                    LEDDA_IP_BREATHE_TIME_MS(fall_time_ms);
+	} else {
+		led_regs->ip.bcrr = 0;
+		led_regs->ip.bcfr = 0;
+	}
+}
diff --git a/firmware/ice40-riscv/common/led.h b/firmware/ice40-riscv/common/led.h
new file mode 100644
index 0000000..a48c8d2
--- /dev/null
+++ b/firmware/ice40-riscv/common/led.h
@@ -0,0 +1,16 @@
+/*
+ * led.h
+ *
+ * Copyright (C) 2019-2020  Sylvain Munaut <tnt@246tNt.com>
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <stdbool.h>
+
+void led_init(void);
+void led_color(uint8_t r, uint8_t g, uint8_t b);
+void led_state(bool on);
+void led_blink(bool enabled, int on_time_ms, int off_time_ms);
+void led_breathe(bool enabled, int rise_time_ms, int fall_time_ms);
diff --git a/firmware/ice40-riscv/common/lnk-app.lds b/firmware/ice40-riscv/common/lnk-app.lds
new file mode 100644
index 0000000..27e51fa
--- /dev/null
+++ b/firmware/ice40-riscv/common/lnk-app.lds
@@ -0,0 +1,52 @@
+MEMORY
+{
+    SPRAM (xrw) : ORIGIN = 0x00020000, LENGTH = 0x10000
+    BRAM  (xrw) : ORIGIN = 0x00000010, LENGTH = 0x03f0
+}
+ENTRY(_start)
+SECTIONS {
+    .text :
+    {
+        . = ALIGN(4);
+        *(.text.start)
+        *(.text)
+        *(.text*)
+        *(.rodata)
+        *(.rodata*)
+        *(.srodata)
+        *(.srodata*)
+        . = ALIGN(4);
+        _etext = .;
+        _sidata = _etext;
+    } >SPRAM
+    .data : AT ( _sidata )
+    {
+        . = ALIGN(4);
+        _sdata = .;
+        _ram_start = .;
+        . = ALIGN(4);
+        *(.data)
+        *(.data*)
+        *(.sdata)
+        *(.sdata*)
+        . = ALIGN(4);
+        _edata = .;
+    } >SPRAM
+    .bss :
+    {
+        . = ALIGN(4);
+        _sbss = .;
+        *(.bss)
+        *(.bss*)
+        *(.sbss)
+        *(.sbss*)
+        *(COMMON)
+        . = ALIGN(4);
+        _ebss = .;
+    } >SPRAM
+    .heap :
+    {
+        . = ALIGN(4);
+        _heap_start = .;
+    } >SPRAM
+}
diff --git a/firmware/ice40-riscv/common/mini-printf.c b/firmware/ice40-riscv/common/mini-printf.c
new file mode 100644
index 0000000..53cfe99
--- /dev/null
+++ b/firmware/ice40-riscv/common/mini-printf.c
@@ -0,0 +1,208 @@
+/*
+ * The Minimal snprintf() implementation
+ *
+ * Copyright (c) 2013,2014 Michal Ludvig <michal@logix.cz>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in the
+ *       documentation and/or other materials provided with the distribution.
+ *     * Neither the name of the auhor nor the names of its contributors
+ *       may be used to endorse or promote products derived from this software
+ *       without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * ----
+ *
+ * This is a minimal snprintf() implementation optimised
+ * for embedded systems with a very limited program memory.
+ * mini_snprintf() doesn't support _all_ the formatting
+ * the glibc does but on the other hand is a lot smaller.
+ * Here are some numbers from my STM32 project (.bin file size):
+ *      no snprintf():      10768 bytes
+ *      mini snprintf():    11420 bytes     (+  652 bytes)
+ *      glibc snprintf():   34860 bytes     (+24092 bytes)
+ * Wasting nearly 24kB of memory just for snprintf() on
+ * a chip with 32kB flash is crazy. Use mini_snprintf() instead.
+ *
+ */
+
+#include "mini-printf.h"
+
+static unsigned int
+mini_strlen(const char *s)
+{
+	unsigned int len = 0;
+	while (s[len] != '\0') len++;
+	return len;
+}
+
+static unsigned int
+mini_itoa(int value, unsigned int radix, unsigned int uppercase, unsigned int unsig,
+	 char *buffer, unsigned int zero_pad)
+{
+	char	*pbuffer = buffer;
+	int	negative = 0;
+	unsigned int	i, len;
+
+	/* No support for unusual radixes. */
+	if (radix > 16)
+		return 0;
+
+	if (value < 0 && !unsig) {
+		negative = 1;
+		value = -value;
+	}
+
+	/* This builds the string back to front ... */
+	do {
+		int digit = value % radix;
+		*(pbuffer++) = (digit < 10 ? '0' + digit : (uppercase ? 'A' : 'a') + digit - 10);
+		value /= radix;
+	} while (value > 0);
+
+	for (i = (pbuffer - buffer); i < zero_pad; i++)
+		*(pbuffer++) = '0';
+
+	if (negative)
+		*(pbuffer++) = '-';
+
+	*(pbuffer) = '\0';
+
+	/* ... now we reverse it (could do it recursively but will
+	 * conserve the stack space) */
+	len = (pbuffer - buffer);
+	for (i = 0; i < len / 2; i++) {
+		char j = buffer[i];
+		buffer[i] = buffer[len-i-1];
+		buffer[len-i-1] = j;
+	}
+
+	return len;
+}
+
+struct mini_buff {
+	char *buffer, *pbuffer;
+	unsigned int buffer_len;
+};
+
+static int
+_putc(int ch, struct mini_buff *b)
+{
+	if ((unsigned int)((b->pbuffer - b->buffer) + 1) >= b->buffer_len)
+		return 0;
+	*(b->pbuffer++) = ch;
+	*(b->pbuffer) = '\0';
+	return 1;
+}
+
+static int
+_puts(char *s, unsigned int len, struct mini_buff *b)
+{
+	unsigned int i;
+
+	if (b->buffer_len - (b->pbuffer - b->buffer) - 1 < len)
+		len = b->buffer_len - (b->pbuffer - b->buffer) - 1;
+
+	/* Copy to buffer */
+	for (i = 0; i < len; i++)
+		*(b->pbuffer++) = s[i];
+	*(b->pbuffer) = '\0';
+
+	return len;
+}
+
+int
+mini_vsnprintf(char *buffer, unsigned int buffer_len, const char *fmt, va_list va)
+{
+	struct mini_buff b;
+	char bf[24];
+	char ch;
+
+	b.buffer = buffer;
+	b.pbuffer = buffer;
+	b.buffer_len = buffer_len;
+
+	while ((ch=*(fmt++))) {
+		if ((unsigned int)((b.pbuffer - b.buffer) + 1) >= b.buffer_len)
+			break;
+		if (ch!='%')
+			_putc(ch, &b);
+		else {
+			char zero_pad = 0;
+			char *ptr;
+			unsigned int len;
+
+			ch=*(fmt++);
+
+			/* Zero padding requested */
+			if (ch=='0') {
+				ch=*(fmt++);
+				if (ch == '\0')
+					goto end;
+				if (ch >= '0' && ch <= '9')
+					zero_pad = ch - '0';
+				ch=*(fmt++);
+			}
+
+			switch (ch) {
+				case 0:
+					goto end;
+
+				case 'u':
+				case 'd':
+					len = mini_itoa(va_arg(va, unsigned int), 10, 0, (ch=='u'), bf, zero_pad);
+					_puts(bf, len, &b);
+					break;
+
+				case 'x':
+				case 'X':
+					len = mini_itoa(va_arg(va, unsigned int), 16, (ch=='X'), 1, bf, zero_pad);
+					_puts(bf, len, &b);
+					break;
+
+				case 'c' :
+					_putc((char)(va_arg(va, int)), &b);
+					break;
+
+				case 's' :
+					ptr = va_arg(va, char*);
+					_puts(ptr, mini_strlen(ptr), &b);
+					break;
+
+				default:
+					_putc(ch, &b);
+					break;
+			}
+		}
+	}
+end:
+	return b.pbuffer - b.buffer;
+}
+
+
+int
+mini_snprintf(char* buffer, unsigned int buffer_len, const char *fmt, ...)
+{
+	int ret;
+	va_list va;
+	va_start(va, fmt);
+	ret = mini_vsnprintf(buffer, buffer_len, fmt, va);
+	va_end(va);
+
+	return ret;
+}
diff --git a/firmware/ice40-riscv/common/mini-printf.h b/firmware/ice40-riscv/common/mini-printf.h
new file mode 100644
index 0000000..99a9519
--- /dev/null
+++ b/firmware/ice40-riscv/common/mini-printf.h
@@ -0,0 +1,50 @@
+/*
+ * The Minimal snprintf() implementation
+ *
+ * Copyright (c) 2013 Michal Ludvig <michal@logix.cz>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above copyright
+ *       notice, this list of conditions and the following disclaimer in the
+ *       documentation and/or other materials provided with the distribution.
+ *     * Neither the name of the auhor nor the names of its contributors
+ *       may be used to endorse or promote products derived from this software
+ *       without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+
+#ifndef __MINI_PRINTF__
+#define __MINI_PRINTF__
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdarg.h>
+
+int mini_vsnprintf(char* buffer, unsigned int buffer_len, const char *fmt, va_list va);
+int mini_snprintf(char* buffer, unsigned int buffer_len, const char *fmt, ...);
+
+#ifdef __cplusplus
+}
+#endif
+
+#define vsnprintf mini_vsnprintf
+#define snprintf mini_snprintf
+
+#endif
diff --git a/firmware/ice40-riscv/common/spi.c b/firmware/ice40-riscv/common/spi.c
new file mode 100644
index 0000000..9072d27
--- /dev/null
+++ b/firmware/ice40-riscv/common/spi.c
@@ -0,0 +1,246 @@
+/*
+ * spi.c
+ *
+ * Copyright (C) 2019-2020  Sylvain Munaut <tnt@246tNt.com>
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "config.h"
+#include "spi.h"
+
+
+struct spi {
+	uint32_t _rsvd0[6];
+	uint32_t irq;		/* 0110 - SPIIRQ   - Interrupt Status Register  */
+	uint32_t irqen;		/* 0111 - SPIIRQEN - Interrupt Control Register */
+	uint32_t cr0;		/* 1000 - CR0      - Control Register 0 */
+	uint32_t cr1;		/* 1001 - CR1      - Control Register 1 */
+	uint32_t cr2;		/* 1010 - CR2      - Control Register 2 */
+	uint32_t br;		/* 1011 - BR       - Baud Rate Register */
+	uint32_t sr;		/* 1100 - SR       - Status Register    */
+	uint32_t txdr;		/* 1101 - TXDR     - Transmit Data Register */
+	uint32_t rxdr;		/* 1110 - RXDR     - Receive Data Register  */
+	uint32_t csr;		/* 1111 - CSR      - Chip Select Register   */
+} __attribute__((packed,aligned(4)));
+
+#define SPI_CR0_TIDLE(xcnt)	(((xcnt) & 3) << 6)
+#define SPI_CR0_TTRAIL(xcnt)	(((xcnt) & 7) << 3)
+#define SPI_CR0_TLEAD(xcnt)	(((xcnt) & 7) << 0)
+
+#define SPI_CR1_ENABLE		(1 << 7)
+#define SPI_CR1_WKUPEN_USER	(1 << 6)
+#define SPI_CR1_TXEDGE		(1 << 4)
+
+#define SPI_CR2_MASTER		(1 << 7)
+#define SPI_CR2_MCSH		(1 << 6)
+#define SPI_CR2_SDBRE		(1 << 5)
+#define SPI_CR2_CPOL		(1 << 2)
+#define SPI_CR2_CPHA		(1 << 1)
+#define SPI_CR2_LSBF		(1 << 0)
+
+#define SPI_SR_TIP		(1 << 7)
+#define SPI_SR_BUSY		(1 << 6)
+#define SPI_SR_TRDY		(1 << 4)
+#define SPI_SR_RRDY		(1 << 3)
+#define SPI_SR_TOE		(1 << 2)
+#define SPI_SR_ROE		(1 << 1)
+#define SPI_SR_MDF		(1 << 0)
+
+
+static volatile struct spi * const spi_regs[] = {
+	(void*)(SPI_FLASH_BASE),
+#ifdef BOARD_E1_TRACER
+	(void*)(SPI_LIU_BASE),
+#endif
+};
+
+
+void
+spi_init(void)
+{
+	/* Channel 0: Flash */
+	spi_regs[0]->cr0 = SPI_CR0_TIDLE(3) |
+	                   SPI_CR0_TTRAIL(7) |
+	                   SPI_CR0_TLEAD(7);
+
+	spi_regs[0]->cr1 = SPI_CR1_ENABLE;
+	spi_regs[0]->cr2 = SPI_CR2_MASTER | SPI_CR2_MCSH;
+	spi_regs[0]->br  = 3;
+	spi_regs[0]->csr = 0xf;
+
+#ifdef BOARD_E1_TRACER
+	/* Channel 1: LIU */
+	spi_regs[1]->cr0 = SPI_CR0_TIDLE(3) |
+	                   SPI_CR0_TTRAIL(7) |
+	                   SPI_CR0_TLEAD(7);
+	spi_regs[1]->cr1 = SPI_CR1_ENABLE | SPI_CR1_TXEDGE;
+	spi_regs[1]->cr2 = SPI_CR2_MASTER | SPI_CR2_LSBF | SPI_CR2_MCSH | SPI_CR2_CPHA;
+	spi_regs[1]->br  = 3;
+	spi_regs[1]->csr = 0xf;
+#endif
+}
+
+void
+spi_xfer(unsigned cs, const struct spi_xfer_chunk *xfer, unsigned n)
+{
+	unsigned chan = (cs >> 2);
+	cs &= 3;
+
+	/* Setup CS */
+	//spi_regs[chan]->cr2 |= SPI_CR2_MCSH;
+	spi_regs[chan]->csr = 0xf ^ (1 << cs);
+
+	/* Run the chunks */
+	while (n--) {
+		for (int i=0; i<xfer->len; i++)
+		{
+			spi_regs[chan]->txdr = xfer->write ? xfer->data[i] : 0x00;
+			while (!(spi_regs[chan]->sr & SPI_SR_RRDY));
+			if (xfer->read)
+				xfer->data[i] = spi_regs[chan]->rxdr;
+		}
+		xfer++;
+	}
+
+	/* Clear CS */
+	//spi_regs[chan]->cr2 &= ~SPI_CR2_MCSH;
+	spi_regs[chan]->csr = 0xf;
+}
+
+
+#define FLASH_CMD_DEEP_POWER_DOWN	0xb9
+#define FLASH_CMD_WAKE_UP		0xab
+#define FLASH_CMD_WRITE_ENABLE		0x06
+#define FLASH_CMD_WRITE_ENABLE_VOLATILE	0x50
+#define FLASH_CMD_WRITE_DISABLE		0x04
+
+#define FLASH_CMD_READ_MANUF_ID		0x9f
+#define FLASH_CMD_READ_UNIQUE_ID	0x4b
+
+#define FLASH_CMD_READ_SR1		0x05
+#define FLASH_CMD_WRITE_SR1		0x01
+
+#define FLASH_CMD_READ_DATA		0x03
+#define FLASH_CMD_PAGE_PROGRAM		0x02
+#define FLASH_CMD_CHIP_ERASE		0x60
+#define FLASH_CMD_SECTOR_ERASE		0x20
+
+void
+flash_cmd(uint8_t cmd)
+{
+	struct spi_xfer_chunk xfer[1] = {
+		{ .data = (void*)&cmd, .len = 1, .read = false, .write = true,  },
+	};
+	spi_xfer(SPI_CS_FLASH, xfer, 1);
+}
+
+void
+flash_deep_power_down(void)
+{
+	flash_cmd(FLASH_CMD_DEEP_POWER_DOWN);
+}
+
+void
+flash_wake_up(void)
+{
+	flash_cmd(FLASH_CMD_WAKE_UP);
+}
+
+void
+flash_write_enable(void)
+{
+	flash_cmd(FLASH_CMD_WRITE_ENABLE);
+}
+
+void
+flash_write_enable_volatile(void)
+{
+	flash_cmd(FLASH_CMD_WRITE_ENABLE_VOLATILE);
+}
+
+void
+flash_write_disable(void)
+{
+	flash_cmd(FLASH_CMD_WRITE_DISABLE);
+}
+
+void
+flash_manuf_id(void *manuf)
+{
+	uint8_t cmd = FLASH_CMD_READ_MANUF_ID;
+	struct spi_xfer_chunk xfer[2] = {
+		{ .data = (void*)&cmd,  .len = 1, .read = false, .write = true,  },
+		{ .data = (void*)manuf, .len = 3, .read = true,  .write = false, },
+	};
+	spi_xfer(SPI_CS_FLASH, xfer, 2);
+}
+
+void
+flash_unique_id(void *id)
+{
+	uint8_t cmd = FLASH_CMD_READ_UNIQUE_ID;
+	struct spi_xfer_chunk xfer[3] = {
+		{ .data = (void*)&cmd, .len = 1, .read = false, .write = true,  },
+		{ .data = (void*)0,    .len = 4, .read = false, .write = false, },
+		{ .data = (void*)id,   .len = 8, .read = true,  .write = false, },
+	};
+	spi_xfer(SPI_CS_FLASH, xfer, 3);
+}
+
+uint8_t
+flash_read_sr(void)
+{
+	uint8_t cmd = FLASH_CMD_READ_SR1;
+	uint8_t rv;
+	struct spi_xfer_chunk xfer[2] = {
+		{ .data = (void*)&cmd, .len = 1, .read = false, .write = true,  },
+		{ .data = (void*)&rv,  .len = 1, .read = true,  .write = false, },
+	};
+	spi_xfer(SPI_CS_FLASH, xfer, 2);
+	return rv;
+}
+
+void
+flash_write_sr(uint8_t sr)
+{
+	uint8_t cmd[2] = { FLASH_CMD_WRITE_SR1, sr };
+	struct spi_xfer_chunk xfer[1] = {
+		{ .data = (void*)cmd, .len = 2, .read = false, .write = true,  },
+	};
+	spi_xfer(SPI_CS_FLASH, xfer, 1);
+}
+
+void
+flash_read(void *dst, uint32_t addr, unsigned len)
+{
+	uint8_t cmd[4] = { FLASH_CMD_READ_DATA, ((addr >> 16) & 0xff), ((addr >> 8) & 0xff), (addr & 0xff)  };
+	struct spi_xfer_chunk xfer[2] = {
+		{ .data = (void*)cmd, .len = 4,   .read = false, .write = true,  },
+		{ .data = (void*)dst, .len = len, .read = true,  .write = false, },
+	};
+	spi_xfer(SPI_CS_FLASH, xfer, 2);
+}
+
+void
+flash_page_program(void *src, uint32_t addr, unsigned len)
+{
+	uint8_t cmd[4] = { FLASH_CMD_PAGE_PROGRAM, ((addr >> 16) & 0xff), ((addr >> 8) & 0xff), (addr & 0xff)  };
+	struct spi_xfer_chunk xfer[2] = {
+		{ .data = (void*)cmd, .len = 4,   .read = false, .write = true, },
+		{ .data = (void*)src, .len = len, .read = false, .write = true, },
+	};
+	spi_xfer(SPI_CS_FLASH, xfer, 2);
+}
+
+void
+flash_sector_erase(uint32_t addr)
+{
+	uint8_t cmd[4] = { FLASH_CMD_SECTOR_ERASE, ((addr >> 16) & 0xff), ((addr >> 8) & 0xff), (addr & 0xff)  };
+	struct spi_xfer_chunk xfer[1] = {
+		{ .data = (void*)cmd, .len = 4,   .read = false, .write = true,  },
+	};
+	spi_xfer(SPI_CS_FLASH, xfer, 1);
+}
diff --git a/firmware/ice40-riscv/common/spi.h b/firmware/ice40-riscv/common/spi.h
new file mode 100644
index 0000000..3a95358
--- /dev/null
+++ b/firmware/ice40-riscv/common/spi.h
@@ -0,0 +1,36 @@
+/*
+ * spi.h
+ *
+ * Copyright (C) 2019-2020  Sylvain Munaut <tnt@246tNt.com>
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <stdbool.h>
+
+struct spi_xfer_chunk {
+	uint8_t *data;
+	unsigned len;
+	bool write;
+	bool read;
+};
+
+#define SPI_CS_FLASH	0
+#define SPI_CS_LIU(n)	(4+(n))
+
+void spi_init(void);
+void spi_xfer(unsigned cs, const struct spi_xfer_chunk *xfer, unsigned n);
+
+void flash_cmd(uint8_t cmd);
+void flash_deep_power_down(void);
+void flash_wake_up(void);
+void flash_write_enable(void);
+void flash_write_disable(void);
+void flash_manuf_id(void *manuf);
+void flash_unique_id(void *id);
+uint8_t flash_read_sr(void);
+void flash_write_sr(uint8_t sr);
+void flash_read(void *dst, uint32_t addr, unsigned len);
+void flash_page_program(void *src, uint32_t addr, unsigned len);
+void flash_sector_erase(uint32_t addr);
diff --git a/firmware/ice40-riscv/common/start.S b/firmware/ice40-riscv/common/start.S
new file mode 100644
index 0000000..cc7abaf
--- /dev/null
+++ b/firmware/ice40-riscv/common/start.S
@@ -0,0 +1,110 @@
+/*
+ * start.S
+ *
+ * Startup code taken from picosoc/picorv32 and adapted for use here
+ *
+ * Copyright (C) 2017 Clifford Wolf <clifford@clifford.at>
+ * Copyright (C) 2019 Sylvain Munaut <tnt@246tNt.com>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+	.section .text.start
+	.global _start
+_start:
+
+	// zero-initialize register file
+	addi  x1, zero, 0
+	// x2 (sp) is initialized by reset
+	addi  x3, zero, 0
+	addi  x4, zero, 0
+	addi  x5, zero, 0
+	addi  x6, zero, 0
+	addi  x7, zero, 0
+	addi  x8, zero, 0
+	addi  x9, zero, 0
+	addi x10, zero, 0
+	addi x11, zero, 0
+	addi x12, zero, 0
+	addi x13, zero, 0
+	addi x14, zero, 0
+	addi x15, zero, 0
+	addi x16, zero, 0
+	addi x17, zero, 0
+	addi x18, zero, 0
+	addi x19, zero, 0
+	addi x20, zero, 0
+	addi x21, zero, 0
+	addi x22, zero, 0
+	addi x23, zero, 0
+	addi x24, zero, 0
+	addi x25, zero, 0
+	addi x26, zero, 0
+	addi x27, zero, 0
+	addi x28, zero, 0
+	addi x29, zero, 0
+	addi x30, zero, 0
+	addi x31, zero, 0
+
+#ifdef BOOT_DEBUG
+	// Set UART divisor
+	li a0, 0x81000000
+	li a1, 28
+	sw a1, 4(a0)
+
+	// Output '1'
+	li a1, 49
+	sw a1, 0(a0)
+#endif
+
+	// copy data section
+	la a0, _sidata
+	la a1, _sdata
+	la a2, _edata
+	bge a1, a2, end_init_data
+loop_init_data:
+	lw a3, 0(a0)
+	sw a3, 0(a1)
+	addi a0, a0, 4
+	addi a1, a1, 4
+	blt a1, a2, loop_init_data
+end_init_data:
+
+#ifdef BOOT_DEBUG
+	// Output '2'
+	li a0, 0x81000000
+	li a1, 50
+	sw a1, 0(a0)
+#endif
+
+	// zero-init bss section
+	la a0, _sbss
+	la a1, _ebss
+	bge a0, a1, end_init_bss
+loop_init_bss:
+	sw zero, 0(a0)
+	addi a0, a0, 4
+	blt a0, a1, loop_init_bss
+end_init_bss:
+
+#ifdef BOOT_DEBUG
+	// Output '3'
+	li a0, 0x81000000
+	li a1, 51
+	sw a1, 0(a0)
+#endif
+
+	// call main
+	call main
+loop:
+	j loop
diff --git a/firmware/ice40-riscv/common/utils.c b/firmware/ice40-riscv/common/utils.c
new file mode 100644
index 0000000..67b292f
--- /dev/null
+++ b/firmware/ice40-riscv/common/utils.c
@@ -0,0 +1,31 @@
+/*
+ * utils.c
+ *
+ * Copyright (C) 2019-2020  Sylvain Munaut <tnt@246tNt.com>
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#include <stdint.h>
+#include <stdbool.h>
+
+char *
+hexstr(void *d, int n, bool space)
+{
+	static const char * const hex = "0123456789abcdef";
+	static char buf[96];
+	uint8_t *p = d;
+	char *s = buf;
+	char c;
+
+	while (n--) {
+		c = *p++;
+		*s++ = hex[c >> 4];
+		*s++ = hex[c & 0xf];
+		if (space)
+			*s++ = ' ';
+	}
+
+	s[space?-1:0] = '\0';
+
+	return buf;
+}
diff --git a/firmware/ice40-riscv/common/utils.h b/firmware/ice40-riscv/common/utils.h
new file mode 100644
index 0000000..fc898f8
--- /dev/null
+++ b/firmware/ice40-riscv/common/utils.h
@@ -0,0 +1,12 @@
+/*
+ * utils.h
+ *
+ * Copyright (C) 2019-2020  Sylvain Munaut <tnt@246tNt.com>
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <stdbool.h>
+
+char *hexstr(void *d, int n, bool space);