Ver código fonte

projects/usb_audio: Initial import

This project was originally started during a stream.
This is the import into master with the cleanup and extended code.

Not perfect, but functional and good enough for people to use.

Signed-off-by: Sylvain Munaut <tnt@246tNt.com>
Sylvain Munaut 4 anos atrás
pai
commit
14c14af0a5

+ 45 - 0
projects/usb_audio/Makefile

@@ -0,0 +1,45 @@
+# Project config
+PROJ = usb_audio
+
+PROJ_DEPS := no2usb no2misc no2ice40
+PROJ_RTL_SRCS := $(addprefix rtl/, \
+	audio_pcm.v \
+	dfu_helper.v \
+	picorv32.v \
+	picorv32_ice40_regs.v \
+	soc_bram.v \
+	soc_picorv32_base.v \
+	soc_picorv32_bridge.v \
+	soc_spram.v \
+	soc_usb.v \
+	sysmgr.v \
+)
+PROJ_SIM_SRCS := $(addprefix sim/, \
+	spiflash.v \
+)
+PROJ_SIM_SRCS += rtl/top.v
+PROJ_TESTBENCHES := \
+	dfu_helper_tb \
+	top_tb
+PROJ_PREREQ = \
+	$(BUILD_TMP)/boot.hex
+PROJ_TOP_SRC := rtl/top.v
+PROJ_TOP_MOD := top
+
+# Target config
+BOARD ?= icebreaker
+DEVICE := $(shell awk '/^\#\# dev:/{print $$3; exit 1}' data/top-$(BOARD).pcf && echo up5k)
+PACKAGE := $(shell awk '/^\#\# pkg:/{print $$3; exit 1}' data/top-$(BOARD).pcf && echo sg48)
+
+YOSYS_SYNTH_ARGS = -dffe_min_ce_use 4 -dsp
+NEXTPNR_ARGS = --pre-pack data/clocks.py --seed 2
+
+# Include default rules
+include ../../build/project-rules.mk
+
+# Custom rules
+fw/boot.hex:
+	make -C fw boot.hex
+
+$(BUILD_TMP)/boot.hex: fw/boot.hex
+	cp $< $@

+ 20 - 0
projects/usb_audio/README.md

@@ -0,0 +1,20 @@
+USB Audio Class demo
+====================
+
+For the icebreaker, the USB hardware connections are :
+  * `P1B4`: USB DP
+  * `P1B3`: USB DN
+  * `P1B2`: Pull up. Resistor of 1.5 kOhm to USB DP 
+
+and the Audio PMOD on P1A.
+
+For the bitsy, refer to the PCF (or edit it) to know what pins
+are used for audio out.
+
+To run :
+  * Build and flash the bitstream
+      * This will build `fw/boot.hex` and include it as the BRAM initial data
+      * `make prog` or `make dfuprog`
+
+  * Flash the main application code in SPI at offset 1M
+      * `make -C fw prog` or `make -C fw dfuprog`

+ 2 - 0
projects/usb_audio/data/clocks.py

@@ -0,0 +1,2 @@
+ctx.addClock("clk_24m", 24)
+ctx.addClock("clk_48m", 48)

+ 36 - 0
projects/usb_audio/data/top-bitsy-v0.pcf

@@ -0,0 +1,36 @@
+# SPI
+set_io -nowarn spi_mosi 14
+set_io -nowarn spi_miso 17
+set_io -nowarn spi_clk 15
+set_io -nowarn spi_flash_cs_n 16
+set_io -nowarn spi_ram_cs_n 37
+
+# USB
+set_io -nowarn usb_dp 43
+set_io -nowarn usb_dn 42
+set_io -nowarn usb_pu 38
+
+# UART
+set_io -nowarn -pullup yes uart_rx 18
+set_io -nowarn uart_tx 19
+
+# Clock
+set_io -nowarn clk_in 35
+
+# Button
+set_io -nowarn -pullup yes btn 10
+
+# Leds
+set_io -nowarn rgb[0] 39
+set_io -nowarn rgb[1] 40
+set_io -nowarn rgb[2] 41
+
+# PCM Audio
+  # Random pick, assign as needed
+set_io -nowarn audio[0] 20
+set_io -nowarn audio[1] 13
+
+# MIDI
+  # Random pick, assign as needed
+set_io -nowarn -pullup yes midi_rx 21
+set_io -nowarn midi_tx 12

+ 60 - 0
projects/usb_audio/data/top-bitsy-v1.pcf

@@ -0,0 +1,60 @@
+# SPI
+set_io -nowarn spi_mosi 14
+set_io -nowarn spi_miso 17
+set_io -nowarn spi_clk 15
+set_io -nowarn spi_flash_cs_n 16
+set_io -nowarn spi_ram_cs_n 6
+
+# USB
+set_io -nowarn usb_dp 42
+set_io -nowarn usb_dn 38
+set_io -nowarn usb_pu 37
+
+# UART
+set_io -nowarn uart_rx 47
+set_io -nowarn uart_tx 44
+
+# Clock
+set_io -nowarn clk_in 35
+
+# Button
+set_io -nowarn btn 2
+
+# Leds
+set_io -nowarn rgb[0] 39
+set_io -nowarn rgb[1] 40
+set_io -nowarn rgb[2] 41
+
+# PCM Audio
+  # PMOD 1
+set_io -nowarn audio[0]  9
+set_io -nowarn audio[1] 10
+
+  # PMOD 2
+#set_io -nowarn audio[0] 28
+#set_io -nowarn audio[1] 34
+
+  # PMOD 3
+#set_io -nowarn audio[0] 11
+#set_io -nowarn audio[1] 46
+
+  # tnt's prototype
+set_io -nowarn audio[0] 43
+set_io -nowarn audio[1] 36
+
+# MIDI
+  # PMOD 1
+#set_io -nowarn -pullup yes midi_rx 9
+#set_io -nowarn midi_tx 10
+
+  # PMOD 2
+set_io -nowarn -pullup yes midi_rx 28
+set_io -nowarn midi_tx 34
+
+  # PMOD 3
+#set_io -nowarn -pullup yes midi_rx 11
+#set_io -nowarn midi_tx 46
+
+  # tnt's prototype
+#set_io -nowarn -pullup yes midi_rx 31
+#set_io -nowarn midi_tx 32

+ 35 - 0
projects/usb_audio/data/top-fomu-hacker.pcf

@@ -0,0 +1,35 @@
+## pkg: uwg30
+  
+# SPI
+set_io -nowarn spi_mosi F1
+set_io -nowarn spi_miso E1
+set_io -nowarn spi_clk D1
+set_io -nowarn spi_flash_cs_n C1
+
+# USB
+set_io -nowarn usb_dp A4
+set_io -nowarn usb_dn A2
+set_io -nowarn usb_pu D5
+
+# UART
+set_io -nowarn -pullup yes uart_rx E3	# Tied up
+set_io -nowarn uart_tx B1		# N/C
+
+# Clock
+set_io -nowarn clk_in F5
+
+# Button
+set_io -nowarn -pullup yes btn F4       # Pin 1
+
+# Leds
+set_io -nowarn rgb[0] A5
+set_io -nowarn rgb[1] B5
+set_io -nowarn rgb[2] C5
+
+# PCM Audio
+set_io -nowarn audio[0] E5		# Pin 2
+set_io -nowarn audio[1] E4		# Pin 3
+
+# MIDI
+set_io -nowarn -pullup yes midi_rx C3	# Tied up
+set_io -nowarn midi_tx F2		# Pin 4

+ 68 - 0
projects/usb_audio/data/top-icebreaker.pcf

@@ -0,0 +1,68 @@
+# SPI
+set_io -nowarn spi_mosi 14
+set_io -nowarn spi_miso 17
+set_io -nowarn spi_clk 15
+set_io -nowarn spi_flash_cs_n 16
+set_io -nowarn spi_ram_cs_n 37
+
+# USB
+ # PMOD 1A
+#set_io -nowarn usb_dp 47
+#set_io -nowarn usb_dn 45
+#set_io -nowarn usb_pu  4
+
+ # PMOD 1B
+set_io -nowarn usb_dp 34
+set_io -nowarn usb_dn 31
+set_io -nowarn usb_pu 43
+
+ # PMOD 2
+#set_io -nowarn usb_dp 21
+#set_io -nowarn usb_dn 19
+#set_io -nowarn usb_pu 27
+
+ # tnt's cable
+#set_io -nowarn usb_dp 31
+#set_io -nowarn usb_dn 34
+#set_io -nowarn usb_pu 38
+
+# UART
+set_io -nowarn -pullup yes uart_rx 6
+set_io -nowarn uart_tx 9
+
+# Clock
+set_io -nowarn clk_in 35
+
+# Button
+set_io -nowarn btn 10
+
+# Leds
+set_io -nowarn rgb[0] 39
+set_io -nowarn rgb[1] 40
+set_io -nowarn rgb[2] 41
+
+# PCM Audio
+ # PMOD 1A
+set_io -nowarn audio[0] 45
+set_io -nowarn audio[1] 44
+
+ # PMOD 1B
+#set_io -nowarn audio[0] 31
+#set_io -nowarn audio[1] 28
+
+ # PMOD 2
+#set_io -nowarn audio[0] 19
+#set_io -nowarn audio[1] 18
+
+# MIDI
+ # PMOD 1A
+#set_io -nowarn -pullup yes midi_rx 45
+#set_io -nowarn midi_tx 44
+
+ # PMOD 1B
+#set_io -nowarn -pullup yes midi_rx 31
+#set_io -nowarn midi_tx 28
+
+ # PMOD 2
+set_io -nowarn -pullup yes midi_rx 19
+set_io -nowarn midi_tx 18

+ 19 - 0
projects/usb_audio/doc/mem-map.md

@@ -0,0 +1,19 @@
+
+
+`0x00` CSR (R)
+
+
+`0x01` Volume (W)
+
+	[31:16] Volume channel 1
+	[15: 0] Volume channel 0
+
+	0x4000 is 0dB
+
+
+`0x02` Sample FIFO (W)
+
+	[31:16] Channel 1
+	[15: 0] Channel 0
+
+	PCM 16 bit signed

+ 5 - 0
projects/usb_audio/fw/.gitignore

@@ -0,0 +1,5 @@
+*.bin
+*.elf
+*.hex
+*.o
+*.gen.h

+ 88 - 0
projects/usb_audio/fw/Makefile

@@ -0,0 +1,88 @@
+BOARD ?= icebreaker
+CROSS ?= riscv-none-embed-
+CC = $(CROSS)gcc
+OBJCOPY = $(CROSS)objcopy
+ICEPROG = iceprog
+DFU_UTIL = dfu-util
+
+BOARD_DEFINE=BOARD_$(shell echo $(BOARD) | tr a-z\- A-Z_)
+CFLAGS=-Wall -Wextra -Wno-unused-parameter -Os -march=rv32i -mabi=ilp32 -ffreestanding -flto -nostartfiles -fomit-frame-pointer -Wl,--gc-section --specs=nano.specs -D$(BOARD_DEFINE) -I.
+
+
+# Common / Shared
+COMMON_PATH=../../riscv_usb/fw/
+CFLAGS += -I$(COMMON_PATH)
+
+HEADERS_common=$(addprefix $(COMMON_PATH), \
+	console.h \
+	led.h \
+	mini-printf.h \
+	spi.h \
+	utils.h \
+)
+
+SOURCES_common=$(addprefix $(COMMON_PATH), \
+	start.S \
+	console.c \
+	led.c \
+	mini-printf.c  \
+	spi.c \
+	utils.c \
+)
+
+
+# USB
+NO2USB_FW_VERSION=0
+include ../../../cores/no2usb/fw/fw.mk
+CFLAGS += $(INC_no2usb)
+
+SOURCES_common += $(SOURCES_no2usb)
+HEADERS_common += $(HEADERS_no2usb)
+
+
+# Local
+HEADERS_app=\
+	config.h \
+	audio.h \
+	usb_str_app.gen.h \
+	$(NULL)
+
+SOURCES_app=\
+	audio.c \
+	fw_app.c \
+	usb_desc_app.c \
+	$(NULL)
+
+
+all: boot.hex fw_app.bin
+
+
+boot.elf: $(COMMON_PATH)/lnk-boot.lds $(COMMON_PATH)/boot.S
+	$(CC) $(CFLAGS) -Wl,-Bstatic,-T,$(COMMON_PATH)/lnk-boot.lds,--strip-debug -DFLASH_APP_ADDR=0x000a0000 -o $@ $(COMMON_PATH)/boot.S
+
+fw_app.elf: $(COMMON_PATH)/lnk-app.lds $(HEADERS_app) $(SOURCES_app) $(HEADERS_common) $(SOURCES_common)
+	$(CC) $(CFLAGS) -Wl,-Bstatic,-T,$(COMMON_PATH)/lnk-app.lds,--strip-debug -o $@ $(SOURCES_common) $(SOURCES_app)
+
+
+%.hex: %.bin
+	$(COMMON_PATH)/bin2hex.py $< $@
+
+%.bin: %.elf
+	$(OBJCOPY) -O binary $< $@
+
+
+prog: fw_app.bin
+	$(ICEPROG) -o 640k $<
+
+dfuprog: fw_app.bin
+ifeq ($(DFU_SERIAL),)
+	$(DFU_UTIL) -R -a 1 -D $<
+else
+	$(DFU_UTIL) -R -S $(DFU_SERIAL) -a 1 -D $<
+endif
+
+
+clean:
+	rm -f *.bin *.hex *.elf *.o *.gen.h
+
+.PHONY: prog_app clean

+ 800 - 0
projects/usb_audio/fw/audio.c

@@ -0,0 +1,800 @@
+/*
+ * audio.c
+ *
+ * USB Audio class firmware
+ *
+ * Copyright (C) 2020 Sylvain Munaut
+ * All rights reserved.
+ *
+ * LGPL v3+, see LICENSE.lgpl3
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <string.h>
+
+#include "console.h"
+#include "led.h"
+#include "mini-printf.h"
+#include "spi.h"
+#include <no2usb/usb.h>
+#include <no2usb/usb_ac_proto.h>
+#include <no2usb/usb_dfu_rt.h>
+#include <no2usb/usb_hw.h>
+#include <no2usb/usb_priv.h>
+#include "utils.h"
+#include "config.h"
+
+
+// Volume helpers
+// ---------------------------------------------------------------------------
+
+/* [round(256*(math.pow(2,i/256.0)-1)) for i in range(256)] */
+static const uint8_t vol_log2lin_lut[] = {
+	0x00, 0x01, 0x01, 0x02, 0x03, 0x03, 0x04, 0x05,
+	0x06, 0x06, 0x07, 0x08, 0x08, 0x09, 0x0a, 0x0b,
+	0x0b, 0x0c, 0x0d, 0x0e, 0x0e, 0x0f, 0x10, 0x10,
+	0x11, 0x12, 0x13, 0x13, 0x14, 0x15, 0x16, 0x16,
+	0x17, 0x18, 0x19, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
+	0x1d, 0x1e, 0x1f, 0x20, 0x20, 0x21, 0x22, 0x23,
+	0x24, 0x24, 0x25, 0x26, 0x27, 0x28, 0x28, 0x29,
+	0x2a, 0x2b, 0x2c, 0x2c, 0x2d, 0x2e, 0x2f, 0x30,
+	0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x35, 0x36,
+	0x37, 0x38, 0x39, 0x3a, 0x3a, 0x3b, 0x3c, 0x3d,
+	0x3e, 0x3f, 0x40, 0x41, 0x41, 0x42, 0x43, 0x44,
+	0x45, 0x46, 0x47, 0x48, 0x48, 0x49, 0x4a, 0x4b,
+	0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x51, 0x52,
+	0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a,
+	0x5b, 0x5c, 0x5d, 0x5e, 0x5e, 0x5f, 0x60, 0x61,
+	0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,
+	0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71,
+	0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79,
+	0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, 0x80, 0x81,
+	0x82, 0x83, 0x84, 0x85, 0x87, 0x88, 0x89, 0x8a,
+	0x8b, 0x8c, 0x8d, 0x8e, 0x8f, 0x90, 0x91, 0x92,
+	0x93, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b,
+	0x9c, 0x9d, 0x9f, 0xa0, 0xa1, 0xa2, 0xa3, 0xa4,
+	0xa5, 0xa6, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad,
+	0xaf, 0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb6, 0xb7,
+	0xb8, 0xb9, 0xba, 0xbc, 0xbd, 0xbe, 0xbf, 0xc0,
+	0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc8, 0xc9, 0xca,
+	0xcb, 0xcd, 0xce, 0xcf, 0xd0, 0xd2, 0xd3, 0xd4,
+	0xd6, 0xd7, 0xd8, 0xd9, 0xdb, 0xdc, 0xdd, 0xde,
+	0xe0, 0xe1, 0xe2, 0xe4, 0xe5, 0xe6, 0xe8, 0xe9,
+	0xea, 0xec, 0xed, 0xee, 0xf0, 0xf1, 0xf2, 0xf4,
+	0xf5, 0xf6, 0xf8, 0xf9, 0xfa, 0xfc, 0xfd, 0xff,
+};
+
+/*  [round(math.log2(1.0 + x / 256.0) * 256) for x in range(256)] */
+static const uint8_t vol_lin2log_lut[] = {
+	0x00, 0x01, 0x03, 0x04, 0x06, 0x07, 0x09, 0x0a,
+	0x0b, 0x0d, 0x0e, 0x10, 0x11, 0x12, 0x14, 0x15,
+	0x16, 0x18, 0x19, 0x1a, 0x1c, 0x1d, 0x1e, 0x20,
+	0x21, 0x22, 0x24, 0x25, 0x26, 0x28, 0x29, 0x2a,
+	0x2c, 0x2d, 0x2e, 0x2f, 0x31, 0x32, 0x33, 0x34,
+	0x36, 0x37, 0x38, 0x39, 0x3b, 0x3c, 0x3d, 0x3e,
+	0x3f, 0x41, 0x42, 0x43, 0x44, 0x45, 0x47, 0x48,
+	0x49, 0x4a, 0x4b, 0x4d, 0x4e, 0x4f, 0x50, 0x51,
+	0x52, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a,
+	0x5c, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63,
+	0x64, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c,
+	0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x74, 0x75,
+	0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d,
+	0x7e, 0x7f, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85,
+	0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d,
+	0x8e, 0x8f, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95,
+	0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b, 0x9b, 0x9c,
+	0x9d, 0x9e, 0x9f, 0xa0, 0xa1, 0xa2, 0xa3, 0xa4,
+	0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xa9, 0xaa, 0xab,
+	0xac, 0xad, 0xae, 0xaf, 0xb0, 0xb1, 0xb2, 0xb2,
+	0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xb9,
+	0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf, 0xc0, 0xc0,
+	0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc6, 0xc7,
+	0xc8, 0xc9, 0xca, 0xcb, 0xcb, 0xcc, 0xcd, 0xce,
+	0xcf, 0xd0, 0xd0, 0xd1, 0xd2, 0xd3, 0xd4, 0xd4,
+	0xd5, 0xd6, 0xd7, 0xd8, 0xd8, 0xd9, 0xda, 0xdb,
+	0xdc, 0xdc, 0xdd, 0xde, 0xdf, 0xe0, 0xe0, 0xe1,
+	0xe2, 0xe3, 0xe4, 0xe4, 0xe5, 0xe6, 0xe7, 0xe7,
+	0xe8, 0xe9, 0xea, 0xea, 0xeb, 0xec, 0xed, 0xee,
+	0xee, 0xef, 0xf0, 0xf1, 0xf1, 0xf2, 0xf3, 0xf4,
+	0xf4, 0xf5, 0xf6, 0xf7, 0xf7, 0xf8, 0xf9, 0xf9,
+	0xfa, 0xfb, 0xfc, 0xfc, 0xfd, 0xfe, 0xff, 0xff,
+};
+
+
+#define VOL_INVALID (-32768)
+
+/* 16384 * math.pow(10, x/(20*256)) */
+static int16_t
+vol_log2lin(int16_t log)
+{
+	uint16_t lin;
+	int s = 0;
+
+	/* Special cases */
+	if (log == VOL_INVALID)	/* Special value */
+		return 0x0000;
+
+	if (log >= 1541)	/* Max is ~6 dB */
+		return 0x7fff;
+
+	/* Integer part */
+	while (log < 0) {
+		log += 1541;
+		s += 1;
+	}
+
+	/* LUT */
+	lin = vol_log2lin_lut[(log * 680) >> 12];
+
+	/* Scaling */
+	lin = (lin << 6) | (lin >> 2) | 0x4000;
+	lin >>= s;
+
+	return lin;
+}
+
+/* 20 * 256 * math.log10(lin / 16384) */
+static int16_t
+vol_lin2log(int16_t lin)
+{
+	int32_t l = 0;
+
+	/* Special cases */
+	if (lin <= 0)
+		return VOL_INVALID;
+
+	/* Integer part */
+	while (lin < 0x4000) {
+		lin <<= 1;
+		l = l - 256;
+	}
+
+	/* LUT correct */
+	l += vol_lin2log_lut[(lin >> 6) & 0xff];
+
+	/* Final scaling */
+	l = (l * 1541) >> 8;
+
+	return (int16_t) l;
+}
+
+
+// PCM Audio
+// ---------------------------------------------------------------------------
+
+struct wb_audio_pcm {
+	uint32_t csr;
+	uint32_t volume;
+	uint32_t fifo;
+} __attribute__((packed,aligned(4)));
+
+static volatile struct wb_audio_pcm * const pcm_regs = (void*)(AUDIO_PCM_BASE);
+
+static struct {
+	bool active;
+	bool mute_all;
+
+	struct {
+		bool     mute;
+		int16_t  vol_log;
+		uint16_t vol_lin;
+	} chan[2];
+
+	uint8_t bdi;
+} g_pcm;
+
+
+static void
+pcm_hw_update_volume(void)
+{
+	pcm_regs->volume =
+		(((!g_pcm.mute_all && !g_pcm.chan[1].mute) ?
+			g_pcm.chan[1].vol_lin : 0) << 16) |
+		(((!g_pcm.mute_all && !g_pcm.chan[0].mute) ?
+			g_pcm.chan[0].vol_lin : 0));
+}
+
+static void
+pcm_set_volume(uint8_t chan, int16_t vol_log)
+{
+	printf("Volume set %d to %d\n", chan, vol_log);
+
+	if (g_pcm.chan[chan].vol_log == vol_log)
+		return;
+
+	g_pcm.chan[chan].vol_lin = vol_log2lin(vol_log);
+	g_pcm.chan[chan].vol_log = vol_lin2log(g_pcm.chan[chan].vol_lin);
+
+	pcm_hw_update_volume();
+}
+
+static void
+pcm_init(void)
+{
+	/* Local state */
+	memset(&g_pcm, 0x00, sizeof(g_pcm));
+
+	/* Audio enabled at -6 dB by default */
+	pcm_set_volume(0, -6*256);
+	pcm_set_volume(1, -6*256);
+}
+
+static int
+pcm_level(void)
+{
+	return (pcm_regs->csr >> 4) & 0xfff;
+}
+
+
+// Audio USB data
+// ---------------------------------------------------------------------------
+
+static void
+pcm_usb_fill_feedback_ep(void)
+{
+	/* FIXME figure this out */
+#if 0 
+	uint32_t val = 8192;
+
+	/* Prepare buffer */
+	usb_data_write(64, &val, 4);
+	usb_ep_regs[1].in.bd[0].ptr = 64;
+	usb_ep_regs[1].in.bd[0].csr = USB_BD_STATE_RDY_DATA | USB_BD_LEN(3);
+#endif
+}
+
+
+
+static void
+pcm_usb_flow_start(void)
+{
+	/* Reset Buffer index */
+	g_pcm.bdi = 0;
+
+	/* EP 1 OUT: Type=Isochronous, dual buffered */
+	usb_ep_regs[1].out.status = USB_EP_TYPE_ISOC | USB_EP_BD_DUAL;
+
+	/* EP1 OUT: Queue two buffers */
+	usb_ep_regs[1].out.bd[0].ptr = 1024;
+	usb_ep_regs[1].out.bd[0].csr = USB_BD_STATE_RDY_DATA | USB_BD_LEN(288);
+
+	usb_ep_regs[1].out.bd[1].ptr = 1024 + 288;
+	usb_ep_regs[1].out.bd[1].csr = USB_BD_STATE_RDY_DATA | USB_BD_LEN(288);
+
+	/* EP1 IN: Type=Isochronous, single buffered */
+	usb_ep_regs[1].in.status = USB_EP_TYPE_ISOC;
+
+	pcm_usb_fill_feedback_ep();
+}
+
+static void
+pcm_usb_flow_stop(void)
+{
+	/* EP 1 OUT: Disable */
+	usb_ep_regs[1].out.status = 0;
+
+	/* EP 1 IN: Disable */
+	usb_ep_regs[1].in.status = 0;
+
+	/* Stop playing audio */
+	pcm_regs->csr = 0;
+}
+
+static void
+pcm_usb_set_active(bool active)
+{
+	if (g_pcm.active == active)
+		return;
+
+	g_pcm.active = active;
+
+	if (active)
+		pcm_usb_flow_start();
+	else
+		pcm_usb_flow_stop();
+}
+
+static void
+pcm_poll(void)
+{
+	/* Check if enough space in FIFO */
+	if (pcm_level() >= 440)
+		return;
+
+	/* EP BD Status */
+	uint32_t ptr = usb_ep_regs[1].out.bd[g_pcm.bdi].ptr;
+	uint32_t csr = usb_ep_regs[1].out.bd[g_pcm.bdi].csr;
+
+	/* Check if we have a USB packet */
+	if ((csr & USB_BD_STATE_MSK) == USB_BD_STATE_RDY_DATA)
+		return;
+
+	/* Valid data ? */
+	if ((csr & USB_BD_STATE_MSK) == USB_BD_STATE_DONE_OK)
+	{
+		static uint32_t lt;
+		uint32_t ct;
+
+		volatile uint32_t __attribute__((aligned(4))) *src_u32 = (volatile uint32_t *)((USB_DATA_BASE) + ptr);
+		int len = (csr & USB_BD_LEN_MSK) - 2; /* Reported length includes CRC */
+
+		for (int i=0; i<len; i+=4)
+			pcm_regs->fifo = *src_u32++;
+
+		ct = usb_get_tick();
+		if ((ct-lt) > 1)
+			printf("%d %d %d %d\n", len, pcm_level(), ct-lt, ct);
+		lt = ct;
+
+		/* If we have enough in the FIFO, enable core */
+		if ((pcm_level() > 200) && !(pcm_regs->csr & 1))
+			pcm_regs->csr = 1;
+	}
+
+	/* Next transfer */
+	usb_ep_regs[1].out.bd[g_pcm.bdi].csr = USB_BD_STATE_RDY_DATA | USB_BD_LEN(288);
+	g_pcm.bdi ^= 1;
+}
+
+
+// PCM Audio USB control
+// ---------------------------------------------------------------------------
+
+static bool
+pcm_usb_mute_set(uint16_t wValue, uint8_t *data, int *len)
+{
+	uint8_t chan = wValue & 0xff;
+
+	if (chan >= 3)
+		return false;
+
+	if (chan == 0) {
+		g_pcm.mute_all = data[0];
+		pcm_hw_update_volume();
+	} else {
+		g_pcm.chan[chan-1].mute =data[0];
+		pcm_hw_update_volume();
+	}
+
+	return true;
+}
+
+static bool
+pcm_usb_mute_get(uint16_t wValue, uint8_t *data, int *len)
+{
+	uint8_t chan = wValue & 0xff;
+
+	if (chan >= 3)
+		return false;
+
+	if (chan == 0) {
+		data[0] = g_pcm.mute_all;
+	} else {
+		data[0] = g_pcm.chan[chan-1].mute;
+	}
+
+	return true;
+}
+
+
+static bool
+pcm_usb_volume_set(uint16_t wValue, uint8_t *data, int *len)
+{
+	uint8_t chan = wValue & 0xff;
+
+	if ((chan == 0) || (chan >= 3))
+		return false;
+
+	pcm_set_volume(chan, *((int16_t*)data));
+
+	return true;
+}
+
+static bool
+pcm_usb_volume_get(uint16_t wValue, uint8_t *data, int *len)
+{
+	uint8_t chan = wValue & 0xff;
+
+	if ((chan == 0) || (chan >= 3))
+		return false;
+
+	*((int16_t*)data) = g_pcm.chan[chan-1].vol_log;
+
+	return true;
+}
+
+static bool
+pcm_usb_volume_min(uint16_t wValue, uint8_t *data, int *len)
+{
+	uint8_t chan = wValue & 0xff;
+
+	if ((chan == 0) || (chan >= 3))
+		return false;
+
+	*((int16_t*)data) = (-80 * 256);
+
+	return true;
+}
+
+static bool
+pcm_usb_volume_max(uint16_t wValue, uint8_t *data, int *len)
+{
+	uint8_t chan = wValue & 0xff;
+
+	if ((chan == 0) || (chan >= 3))
+		return false;
+
+	*((int16_t*)data) = (5 * 256);
+
+	return true;
+}
+
+static bool
+pcm_usb_volume_res(uint16_t wValue, uint8_t *data, int *len)
+{
+	uint8_t chan = wValue & 0xff;
+
+	if ((chan == 0) || (chan >= 3))
+		return false;
+
+	*((int16_t*)data) = (256 / 2);
+
+	return true;
+}
+
+
+// MIDI
+// ---------------------------------------------------------------------------
+
+struct wb_uart {
+	uint32_t data;
+	uint32_t clkdiv;
+} __attribute__((packed,aligned(4)));
+
+static volatile struct wb_uart * const midi_regs = (void*)(MIDI_BASE);
+
+
+void
+midi_usb_set_conf(void)
+{
+	/* EP 2 OUT: Type=Bulk, single buffered */
+	usb_ep_regs[2].out.status = USB_EP_TYPE_BULK;
+
+	/* Fill a buffer */
+	usb_ep_regs[2].out.bd[0].ptr = 1536;
+	usb_ep_regs[2].out.bd[0].csr = USB_BD_STATE_RDY_DATA | USB_BD_LEN(64);
+}
+
+static const int
+midi_pkt[16] = {
+	-1,	/* 0x0 Miscellaneous function codes. Reserved for future extensions */
+	-1,	/* 0x1 Cable events. Reserved for future expansion */
+	2,	/* 0x2 Two-byte System Common messages like MTC, SongSelect, etc */
+	3,	/* 0x3 Three-byte System Common messages like SPP, etc */
+	3,	/* 0x4 SysEx starts or continues */
+	1,	/* 0x5 SysEx ends with following single byte */
+	2,	/* 0x6 SysEx ends with following two bytes */
+	3,	/* 0x7 SysEx ends with following three bytes */
+	3,	/* 0x8 Note-off */
+	3,	/* 0x9 Note-on */
+	3,	/* 0xa Poly-KeyPress */
+	3,	/* 0xb Control Change */
+	2,	/* 0xc Program Change */
+	2,	/* 0xd Channel Pressure */
+	3,	/* 0xe PitchBend Change */
+	1,	/* 0xf Single Byte */
+};
+
+static void
+midi_poll(void)
+{
+	/* EP BD Status */
+	uint32_t ptr = usb_ep_regs[2].out.bd[0].ptr;
+	uint32_t csr = usb_ep_regs[2].out.bd[0].csr;
+
+	/* Check if we have a USB packet */
+	if ((csr & USB_BD_STATE_MSK) == USB_BD_STATE_RDY_DATA)
+		return;
+
+	/* Valid data ? */
+	if ((csr & USB_BD_STATE_MSK) == USB_BD_STATE_DONE_OK)
+	{
+		uint32_t midi[64];
+		int len = (csr & USB_BD_LEN_MSK) - 2; /* Reported length includes CRC */
+
+		usb_data_read(midi, ptr, len);
+
+		for (int i=0; i<(len>>2); i++) {
+			uint32_t w = midi[i];
+			int bl = midi_pkt[w & 0xf];
+			w >>= 8;
+
+			while (bl-- > 0) {
+				midi_regs->data = (w & 0xff);
+				w >>= 8;
+			}
+		}
+	}
+
+	/* Next transfer */
+	usb_ep_regs[2].out.bd[0].csr = USB_BD_STATE_RDY_DATA | USB_BD_LEN(64);
+}
+
+static void
+midi_init(void)
+{
+	/* 31250 baud with 24MHz system clk */
+	midi_regs->clkdiv = 768;
+}
+
+
+
+// Shared USB driver
+// ---------------------------------------------------------------------------
+
+/* Control handler structs */
+
+typedef bool (*usb_audio_control_fn)(uint16_t wValue, uint8_t *data, int *len);
+
+struct usb_audio_control_handler {
+	int len;
+	usb_audio_control_fn set_cur;
+	usb_audio_control_fn get_cur;
+	usb_audio_control_fn get_min;
+	usb_audio_control_fn get_max;
+	usb_audio_control_fn get_res;
+};
+
+struct usb_audio_req_handler {
+	uint8_t rcpt;		/* USB_REQ_RCPT_INTF or USB_REQ_RCPT_EP */
+	uint8_t idx;		/* Interface or EP index */
+	uint8_t entity_id;
+	uint16_t val_match;
+	uint16_t val_mask;
+	const struct usb_audio_control_handler *h;
+};
+
+
+/* Control handlers for this implementation */
+
+static const struct usb_audio_control_handler _uac_mute = {	/* USB_AC_FU_CONTROL_MUTE */
+	.len		= 1,
+	.set_cur	= pcm_usb_mute_set,
+	.get_cur	= pcm_usb_mute_get,
+};
+
+static const struct usb_audio_control_handler _uac_volume = {	/* USB_AC_FU_CONTROL_VOLUME */
+	.len		= 2,
+	.set_cur	= pcm_usb_volume_set,
+	.get_cur	= pcm_usb_volume_get,
+	.get_min	= pcm_usb_volume_min,
+	.get_max	= pcm_usb_volume_max,
+	.get_res	= pcm_usb_volume_res,
+};
+
+#define INTF_AUDIO_CONTROL	1
+#define UNIT_FEATURE		2
+
+static const struct usb_audio_req_handler _uac_handlers[] = {
+	{ USB_REQ_RCPT_INTF, INTF_AUDIO_CONTROL, UNIT_FEATURE, (USB_AC_FU_CONTROL_MUTE   << 8), 0xff00, &_uac_mute   },
+	{ USB_REQ_RCPT_INTF, INTF_AUDIO_CONTROL, UNIT_FEATURE, (USB_AC_FU_CONTROL_VOLUME << 8), 0xff00, &_uac_volume },
+	{ 0 }
+};
+
+
+/* USB driver implemntation (including control handler dispatch */
+
+static struct {
+	struct usb_ctrl_req *req;
+	usb_audio_control_fn fn;
+} g_cb_ctx;
+
+static bool
+audio_ctrl_req_cb(struct usb_xfer *xfer)
+{
+	struct usb_ctrl_req *req = g_cb_ctx.req;
+	usb_audio_control_fn fn = g_cb_ctx.fn;
+	return fn(req->wValue, xfer->data, &xfer->len);
+}
+
+static enum usb_fnd_resp
+audio_ctrl_req(struct usb_ctrl_req *req, struct usb_xfer *xfer)
+{
+	const struct usb_audio_req_handler *rh;
+
+	/* Check it's a class request to an interface */
+	if (USB_REQ_TYPE(req) != USB_REQ_TYPE_CLASS)
+		return USB_FND_CONTINUE;
+	
+	/* Check R/W consitency */
+		/* The control request ID mirrors the read flag in the MSB */
+	if ((req->bmRequestType ^ req->bRequest) & 0x80)
+		return USB_FND_ERROR;
+
+	/* Find a matching handler */
+	for (rh=&_uac_handlers[0]; rh->rcpt; rh++)
+	{
+		usb_audio_control_fn fn = NULL;
+
+		/* Check recipient type and index */
+		if (USB_REQ_RCPT(req) != rh->rcpt)
+			continue;
+
+		if ((req->wIndex & 0xff) != rh->idx)
+			continue;
+
+		/* Check Entity ID */
+		if ((req->wIndex >> 8) != rh->entity_id)
+			continue;
+
+		/* Check control */
+		if ((req->wValue & rh->val_mask) != rh->val_match)
+			continue;
+
+		/* We have a match, first check it's not a NOP and check length */
+		if (!rh->h)
+			return USB_FND_ERROR;
+
+		if ((rh->h->len != -1) && (rh->h->len != req->wLength))
+			return USB_FND_ERROR;
+
+		/* Then grab appropriate function */
+		switch (req->bRequest)
+		{
+		case USB_REQ_AC_SET_CUR:
+			fn = rh->h->set_cur;
+			break;
+
+		case USB_REQ_AC_GET_CUR:
+			fn = rh->h->get_cur;
+			break;
+
+		case USB_REQ_AC_GET_MIN:
+			fn = rh->h->get_min;
+			break;
+
+		case USB_REQ_AC_GET_MAX:
+			fn = rh->h->get_max;
+			break;
+		
+		case USB_REQ_AC_GET_RES:
+			fn = rh->h->get_res;
+			break;
+
+		default:
+			fn = NULL;
+		}
+
+		if (!fn)
+			return USB_FND_ERROR;
+
+		/* And try to call it */
+		if (USB_REQ_IS_READ(req)) {
+			/* Request is a read, we can call handler immediately */
+			xfer->len = req->wLength;
+			return fn(req->wValue, xfer->data, &xfer->len) ? USB_FND_SUCCESS : USB_FND_ERROR;
+		} else {
+			/* Request is a write, we need to hold off until end of data phase */
+			g_cb_ctx.req = req;
+			g_cb_ctx.fn = fn;
+			xfer->len = req->wLength;
+			xfer->cb_done = audio_ctrl_req_cb;
+			return USB_FND_SUCCESS;
+		}
+	}
+
+	return USB_FND_ERROR;
+}
+
+static enum usb_fnd_resp
+audio_set_conf(const struct usb_conf_desc *conf)
+{
+	/* Default PCM interface is inactive */
+	pcm_usb_set_active(false);
+
+	/* MIDI EP config */
+	midi_usb_set_conf();
+
+	return USB_FND_SUCCESS;
+}
+
+static enum usb_fnd_resp
+audio_set_intf(const struct usb_intf_desc *base, const struct usb_intf_desc *sel)
+{
+	/* Check it's audio class */
+	if (base->bInterfaceClass != 0x01)
+		return USB_FND_CONTINUE;
+
+	/* Sub class */
+	switch (base->bInterfaceSubClass)
+	{
+	case USB_AC_SCLS_AUDIOCONTROL:
+	case USB_AC_SCLS_MIDISTREAMING:
+		return USB_FND_SUCCESS;
+
+	case USB_AC_SCLS_AUDIOSTREAMING:
+		pcm_usb_set_active(sel->bAlternateSetting != 0);
+		return USB_FND_SUCCESS;
+
+	default:
+		return USB_FND_ERROR;
+	}
+}
+
+static enum usb_fnd_resp
+audio_get_intf(const struct usb_intf_desc *base, uint8_t *alt)
+{
+	/* Check it's audio class */
+	if (base->bInterfaceClass != 0x01)
+		return USB_FND_CONTINUE;
+
+	/* Sub class */
+	switch (base->bInterfaceSubClass)
+	{
+	case USB_AC_SCLS_AUDIOCONTROL:
+	case USB_AC_SCLS_MIDISTREAMING:
+		*alt = 0;
+		return USB_FND_SUCCESS;
+
+	case USB_AC_SCLS_AUDIOSTREAMING:
+		*alt = g_pcm.active ? 1 : 0;
+		return USB_FND_SUCCESS;
+
+	default:
+		return USB_FND_ERROR;
+	}
+}
+
+static struct usb_fn_drv _audio_drv = {
+	.ctrl_req = audio_ctrl_req,
+	.set_conf = audio_set_conf,
+	.set_intf = audio_set_intf,
+	.get_intf = audio_get_intf,
+};
+
+
+// Exposed API
+// ---------------------------------------------------------------------------
+
+void
+audio_init(void)
+{
+	/* Init hardware */
+	pcm_init();
+	midi_init();
+
+	/* Register function driver */
+	usb_register_function_driver(&_audio_drv);
+}
+
+void
+audio_poll(void)
+{
+	pcm_poll();
+	midi_poll();
+}
+
+void
+audio_debug_print(void)
+{
+	uint32_t csr = pcm_regs->csr;
+
+	printf("Audio PCM tick       : %04x\n", csr >> 16);
+	printf("Audio PCM FIFO level : %d\n", (csr >> 4) & 0xfff);
+	printf("Audio PCM State      : %d\n", csr & 3);
+}

+ 30 - 0
projects/usb_audio/fw/audio.h

@@ -0,0 +1,30 @@
+/*
+ * audio.h
+ *
+ * USB Audio class firmware
+ *
+ * Copyright (C) 2020 Sylvain Munaut
+ * All rights reserved.
+ *
+ * LGPL v3+, see LICENSE.lgpl3
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#pragma once
+
+void audio_init(void);
+void audio_poll(void);
+void audio_debug_print(void);

+ 32 - 0
projects/usb_audio/fw/config.h

@@ -0,0 +1,32 @@
+/*
+ * config.h
+ *
+ * Copyright (C) 2019 Sylvain Munaut
+ * All rights reserved.
+ *
+ * LGPL v3+, see LICENSE.lgpl3
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#pragma once
+
+#define UART_BASE	0x81000000
+#define SPI_BASE	0x82000000
+#define LED_BASE	0x83000000
+#define USB_CORE_BASE	0x84000000
+#define USB_DATA_BASE	0x85000000
+#define AUDIO_PCM_BASE	0x86000000
+#define MIDI_BASE	0x87000000

+ 153 - 0
projects/usb_audio/fw/fw_app.c

@@ -0,0 +1,153 @@
+/*
+ * fw_app.c
+ *
+ * Copyright (C) 2019 Sylvain Munaut
+ * All rights reserved.
+ *
+ * LGPL v3+, see LICENSE.lgpl3
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <string.h>
+
+#include <no2usb/usb.h>
+#include <no2usb/usb_dfu_rt.h>
+#include <no2usb/usb_hw.h>
+#include <no2usb/usb_priv.h>
+
+#include "audio.h"
+#include "console.h"
+#include "led.h"
+#include "mini-printf.h"
+#include "spi.h"
+#include "utils.h"
+#include "config.h"
+
+
+extern const struct usb_stack_descriptors app_stack_desc;
+
+static void
+serial_no_init()
+{
+	uint8_t buf[8];
+	char *id, *desc;
+	int i;
+
+	flash_manuf_id(buf);
+	printf("Flash Manufacturer : %s\n", hexstr(buf, 3, true));
+
+	flash_unique_id(buf);
+	printf("Flash Unique ID    : %s\n", hexstr(buf, 8, true));
+
+	/* Overwrite descriptor string */
+		/* In theory in rodata ... but nothing is ro here */
+	id = hexstr(buf, 8, false);
+	desc = (char*)app_stack_desc.str[1];
+	for (i=0; i<16; i++)
+		desc[2 + (i << 1)] = id[i];
+}
+
+static void
+boot_dfu(void)
+{
+	/* Force re-enumeration */
+	usb_disconnect();
+
+	/* Boot firmware */
+	volatile uint32_t *boot = (void*)0x80000000;
+	*boot = (1 << 2) | (1 << 0);
+}
+
+void
+usb_dfu_rt_cb_reboot(void)
+{
+        boot_dfu();
+}
+
+
+void
+main()
+{
+	int cmd = 0;
+
+	/* Init console IO */
+	console_init();
+	puts("Booting Audio image..\n");
+
+	/* LED */
+	led_init();
+	led_color(48, 96, 5);
+	led_blink(true, 200, 1000);
+	led_breathe(true, 100, 200);
+	led_state(true);
+
+	/* SPI */
+	spi_init();
+
+	/* Enable USB directly */
+	serial_no_init();
+	usb_init(&app_stack_desc);
+	usb_dfu_rt_init();
+
+	/* Audio */
+	audio_init();
+
+	/* Connect */
+	usb_connect();
+
+	/* Main loop */
+	while (1)
+	{
+		/* Prompt ? */
+		if (cmd >= 0)
+			printf("Command> ");
+
+		/* Poll for command */
+		cmd = getchar_nowait();
+
+		if (cmd >= 0) {
+			if (cmd > 32 && cmd < 127) {
+				putchar(cmd);
+				putchar('\r');
+				putchar('\n');
+			}
+
+			switch (cmd)
+			{
+			case 'p':
+				audio_debug_print();
+				break;
+			case 'b':
+				boot_dfu();
+				break;
+			case 'c':
+				usb_connect();
+				break;
+			case 'd':
+				usb_disconnect();
+				break;
+			default:
+				break;
+			}
+		}
+
+		/* USB poll */
+		usb_poll();
+		audio_poll();
+	}
+}

+ 330 - 0
projects/usb_audio/fw/usb_desc_app.c

@@ -0,0 +1,330 @@
+/*
+ * usb_desc_app.c
+ *
+ * Copyright (C) 2020 Sylvain Munaut
+ * All rights reserved.
+ *
+ * LGPL v3+, see LICENSE.lgpl3
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#include <no2usb/usb_proto.h>
+#include <no2usb/usb_ac_proto.h>
+#include <no2usb/usb_cdc_proto.h>
+#include <no2usb/usb_dfu_proto.h>
+#include <no2usb/usb.h>
+
+
+usb_ac_ac_hdr_desc_def(1);
+usb_ac_ac_feature_desc_def(6);
+usb_ac_as_fmt_type1_desc_def(3);
+usb_ac_ms_ep_general_desc_def(1);
+usb_ac_ms_out_jack_desc_def(1);
+
+static const struct {
+	/* Configuration */
+	struct usb_conf_desc conf;
+
+	/* DFU Runtime */
+	struct {
+		struct usb_intf_desc intf;
+		struct usb_dfu_func_desc func;
+	} __attribute__ ((packed)) dfu;
+
+	/* Audio Control Interface */
+	struct {
+		struct usb_intf_desc intf;
+		struct usb_ac_ac_hdr_desc__1 hdr;
+		struct usb_ac_ac_input_desc input;
+		struct usb_ac_ac_feature_desc__6 feat;
+		struct usb_ac_ac_output_desc output;
+	} __attribute__ ((packed)) audio_ctl;
+
+	/* Audio Streaming Interface */
+	struct {
+		struct usb_intf_desc intf[2];
+		struct usb_ac_as_general_desc general;
+		struct usb_ac_as_fmt_type1_desc__3 fmt;
+		struct usb_cc_ep_desc ep_data;
+		struct usb_ac_as_ep_general_desc ep_gen;
+		struct usb_cc_ep_desc ep_sync;
+	} __attribute__ ((packed)) audio_stream;
+
+	/* MIDI Streaming Interface */
+	struct {
+		struct usb_intf_desc intf;
+		struct usb_ac_ms_hdr_desc hdr;
+		struct usb_ac_ms_in_jack_desc input;
+		struct usb_ac_ms_out_jack_desc__1 output;
+		struct usb_cc_ep_desc ep_data;
+		struct usb_ac_ms_ep_general_desc__1 ep_gen;
+	} __attribute__ ((packed)) midi_stream;
+
+} __attribute__ ((packed)) _app_conf_desc = {
+	.conf = {
+		.bLength                = sizeof(struct usb_conf_desc),
+		.bDescriptorType        = USB_DT_CONF,
+		.wTotalLength           = sizeof(_app_conf_desc),
+		.bNumInterfaces         = 4,
+		.bConfigurationValue    = 1,
+		.iConfiguration         = 4,
+		.bmAttributes           = 0x80,
+		.bMaxPower              = 0x32, /* 100 mA */
+	},
+	.dfu = {
+		.intf = {
+			.bLength		= sizeof(struct usb_intf_desc),
+			.bDescriptorType	= USB_DT_INTF,
+			.bInterfaceNumber	= 0,
+			.bAlternateSetting	= 0,
+			.bNumEndpoints		= 0,
+			.bInterfaceClass	= 0xfe,
+			.bInterfaceSubClass	= 0x01,
+			.bInterfaceProtocol	= 0x01,
+			.iInterface		= 5,
+		},
+		.func = {
+			.bLength		= sizeof(struct usb_dfu_func_desc),
+			.bDescriptorType	= USB_DFU_DT_FUNC,
+			.bmAttributes		= 0x0d,
+			.wDetachTimeOut		= 1000,
+			.wTransferSize		= 4096,
+			.bcdDFUVersion		= 0x0101,
+		},
+	},
+	.audio_ctl = {
+		.intf = {
+			.bLength		= sizeof(struct usb_intf_desc),
+			.bDescriptorType	= USB_DT_INTF,
+			.bInterfaceNumber	= 1,
+			.bAlternateSetting	= 0,
+			.bNumEndpoints		= 0,
+			.bInterfaceClass	= 0x01,
+			.bInterfaceSubClass	= USB_AC_SCLS_AUDIOCONTROL,
+			.bInterfaceProtocol	= 0x00,
+			.iInterface		= 6,
+		},
+		.hdr = {
+			.bLength		= sizeof(struct usb_ac_ac_hdr_desc__1),
+			.bDescriptorType	= USB_CS_DT_INTF,
+			.bDescriptorSubtype	= USB_AC_AC_IDST_HEADER,
+			.bcdADC			= 0x0100,
+			.wTotalLength		= sizeof(_app_conf_desc.audio_ctl) - sizeof(struct usb_intf_desc),
+			.bInCollection		= 1,
+			.baInterfaceNr		= { 0x02 },
+		},
+		.input = {
+			.bLength		= sizeof(struct usb_ac_ac_input_desc),
+			.bDescriptortype	= USB_CS_DT_INTF,
+			.bDescriptorSubtype	= USB_AC_AC_IDST_INPUT_TERMINAL,
+			.bTerminalID		= 1,
+			.wTerminalType		= 0x0101,
+			.bAssocTerminal		= 0,
+			.bNrChannels		= 2,
+			.wChannelConfig		= 0x0003,
+			.iChannelNames		= 7,
+			.iTerminal		= 9,
+		},
+		.feat = {
+			.bLength		= sizeof(struct usb_ac_ac_feature_desc__6),
+			.bDescriptortype	= USB_CS_DT_INTF,
+			.bDescriptorSubtype	= USB_AC_AC_IDST_FEATURE_UNIT,
+			.bUnitID		= 2,
+			.bSourceID		= 1,
+			.bControlSize		= 2,
+			.bmaControls		= {
+				U16_TO_U8_LE(0x0001),	// Mute
+				U16_TO_U8_LE(0x0003),	// Mute + Volume
+				U16_TO_U8_LE(0x0003),	// Mute + Volume
+			},
+			.iTerminal		= 0,
+		},
+		.output = {
+			.bLength		= sizeof(struct usb_ac_ac_output_desc),
+			.bDescriptortype	= USB_CS_DT_INTF,
+			.bDescriptorSubtype	= USB_AC_AC_IDST_OUTPUT_TERMINAL,
+			.bTerminalID		= 3,
+			.wTerminalType		= 0x0302,
+			.bAssocTerminal		= 0,
+			.bSourceID		= 2,
+			.iTerminal		= 10,
+		},
+	},
+	.audio_stream = {
+		.intf[0] = {
+			.bLength		= sizeof(struct usb_intf_desc),
+			.bDescriptorType	= USB_DT_INTF,
+			.bInterfaceNumber	= 2,
+			.bAlternateSetting	= 0,
+			.bNumEndpoints		= 0,
+			.bInterfaceClass	= 0x01,
+			.bInterfaceSubClass	= USB_AC_SCLS_AUDIOSTREAMING,
+			.bInterfaceProtocol	= 0x00,
+			.iInterface		= 11,
+		},
+		.intf[1] = {
+			.bLength		= sizeof(struct usb_intf_desc),
+			.bDescriptorType	= USB_DT_INTF,
+			.bInterfaceNumber	= 2,
+			.bAlternateSetting	= 1,
+			.bNumEndpoints		= 2,
+			.bInterfaceClass	= 0x01,
+			.bInterfaceSubClass	= USB_AC_SCLS_AUDIOSTREAMING,
+			.bInterfaceProtocol	= 0x00,
+			.iInterface		= 12,
+		},
+		.general = {
+			.bLength		= sizeof(struct usb_ac_as_general_desc),
+			.bDescriptortype	= USB_CS_DT_INTF,
+			.bDescriptorSubtype	= USB_AC_AS_IDST_GENERAL,
+			.bTerminalLink		= 1,
+			.bDelay			= 0,
+			.wFormatTag		= 0x0001,	/* PCM */
+		},
+		.fmt = {
+			.bLength		= sizeof(struct usb_ac_as_fmt_type1_desc__3),
+			.bDescriptortype	= USB_CS_DT_INTF,
+			.bDescriptorSubtype	= USB_AC_AS_IDST_FORMAT_TYPE,
+			.bFormatType		= 1,
+			.bNrChannels		= 2,
+			.bSubframeSize		= 2,
+			.bBitResolution		= 16,
+			.bSamFreqType		= 1,
+			.tSamFreq		= {
+				U24_TO_U8_LE(48000),
+			},
+		},
+		.ep_data = {
+			.bLength		= sizeof(struct usb_cc_ep_desc),
+			.bDescriptorType	= USB_DT_EP,
+			.bEndpointAddress	= 0x01,		/* 1 OUT */
+			.bmAttributes		= 0x05,		/* Data, Async, Isoc */
+			.wMaxPacketSize		= 288,
+			.bInterval		= 1,
+			.bRefresh		= 0,
+			.bSynchAddress		= 0x81,
+		},
+		.ep_gen = {
+			.bLength		= sizeof(struct usb_ac_as_ep_general_desc),
+			.bDescriptortype	= USB_CS_DT_EP,
+			.bDescriptorSubtype	= USB_AC_EDST_GENERAL,
+			.bmAttributes		= 0x00,
+			.bLockDelayUnits	= 0,
+			.wLockDelay		= 0,
+		},
+		.ep_sync = {
+			.bLength		= sizeof(struct usb_cc_ep_desc),
+			.bDescriptorType	= USB_DT_EP,
+			.bEndpointAddress	= 0x81,		/* 1 IN */
+			.bmAttributes		= 0x11,		/* Feedback, Isoc */
+			.wMaxPacketSize		= 8,
+			.bInterval		= 1,
+			.bRefresh		= 1,
+			.bSynchAddress		= 0,
+		},
+	},
+	.midi_stream = {
+		.intf = {
+			.bLength		= sizeof(struct usb_intf_desc),
+			.bDescriptorType	= USB_DT_INTF,
+			.bInterfaceNumber	= 3,
+			.bAlternateSetting	= 0,
+			.bNumEndpoints		= 1,
+			.bInterfaceClass	= 0x01,
+			.bInterfaceSubClass	= USB_AC_SCLS_MIDISTREAMING,
+			.bInterfaceProtocol	= 0x00,
+			.iInterface		= 0,
+		},
+		.hdr = {
+			.bLength		= sizeof(struct usb_ac_ms_hdr_desc),
+			.bDescriptorType	= USB_CS_DT_INTF,
+			.bDescriptorSubtype	= USB_AC_MS_IDST_HEADER,
+			.bcdADC			= 0x0100,
+			.wTotalLength		= sizeof(_app_conf_desc.midi_stream) - sizeof(struct usb_intf_desc) - sizeof(_app_conf_desc.midi_stream.ep_data) - sizeof(_app_conf_desc.midi_stream.ep_gen),
+		},
+		.input = {
+			.bLength		= sizeof(struct usb_ac_ms_in_jack_desc),
+			.bDescriptorType	= USB_CS_DT_INTF,
+			.bDescriptorSubtype	= USB_AC_MS_IDST_MIDI_IN_JACK,
+			.bJackType		= USB_AC_MS_JACK_TYPE_EMBEDDED,
+			.bJackID		= 1,
+			.iJack			= 0,
+		},
+		.output = {
+			.bLength		= sizeof(struct usb_ac_ms_out_jack_desc__1),
+			.bDescriptorType	= USB_CS_DT_INTF,
+			.bDescriptorSubtype	= USB_AC_MS_IDST_MIDI_OUT_JACK,
+			.bJackType		= USB_AC_MS_JACK_TYPE_EXTERNAL,
+			.bJackID		= 2,
+			.bNrInputPins		= 1,
+			.sources		= {
+				{
+					.baSourceID	= 1,
+					.baSourcePin	= 1,
+				},
+			},
+			.iJack			= 0,
+		},
+		.ep_data = {
+			.bLength		= sizeof(struct usb_cc_ep_desc),
+			.bDescriptorType	= USB_DT_EP,
+			.bEndpointAddress	= 0x02,		/* 2 OUT */
+			.bmAttributes		= 0x02,		/* Bulk */
+			.wMaxPacketSize		= 64,
+			.bInterval		= 0,
+			.bRefresh		= 0,
+			.bSynchAddress		= 0,
+		},
+		.ep_gen = {
+			.bLength		= sizeof(struct usb_ac_ms_ep_general_desc__1),
+			.bDescriptortype	= USB_CS_DT_EP,
+			.bDescriptorSubtype	= USB_AC_EDST_GENERAL,
+			.bNumEmbMIDIJack	= 1,
+			.baAssocJackID		= { 1 },
+		},
+	},
+};
+
+static const struct usb_conf_desc * const _conf_desc_array[] = {
+	&_app_conf_desc.conf,
+};
+
+static const struct usb_dev_desc _dev_desc = {
+	.bLength		= sizeof(struct usb_dev_desc),
+	.bDescriptorType	= USB_DT_DEV,
+	.bcdUSB			= 0x0200,
+	.bDeviceClass		= 0,
+	.bDeviceSubClass	= 0,
+	.bDeviceProtocol	= 0,
+	.bMaxPacketSize0	= 64,
+	.idVendor		= 0x1d50,
+	.idProduct		= 0x6147,
+	.bcdDevice		= 0x0001,	/* v0.1 */
+	.iManufacturer		= 2,
+	.iProduct		= 3,
+	.iSerialNumber		= 1,
+	.bNumConfigurations	= num_elem(_conf_desc_array),
+};
+
+#include "usb_str_app.gen.h"
+
+const struct usb_stack_descriptors app_stack_desc = {
+	.dev = &_dev_desc,
+	.conf = _conf_desc_array,
+	.n_conf = num_elem(_conf_desc_array),
+	.str = _str_desc_array,
+	.n_str = num_elem(_str_desc_array),
+};

+ 12 - 0
projects/usb_audio/fw/usb_str_app.txt

@@ -0,0 +1,12 @@
+0000000000000000
+osmocom
+!{"bitsy-v0": "iCEBreaker bitsy v0.x Audio demo", "bitsy-v1": "iCEBreaker bitsy v1.x Audio Demo", "icebreaker": "iCEBreaker Audio Demo", "": "iCE40 USB Audio Demo"}
+Main
+DFU runtime
+bitsy Audio Control
+One Ear
+Other Ear
+USB Stream
+PMOD Jack
+bitsy Audio Stream (off)
+bitsy Audio Stream (on)

+ 258 - 0
projects/usb_audio/rtl/audio_pcm.v

@@ -0,0 +1,258 @@
+/*
+ * audio_pcm.v
+ *
+ * vim: ts=4 sw=4
+ *
+ * Copyright (C) 2020  Sylvain Munaut <tnt@246tNt.com>
+ * SPDX-License-Identifier: CERN-OHL-P-2.0
+ */
+
+`default_nettype none
+
+module audio_pcm (
+	// Audio output
+	output wire [1:0] audio,
+
+	// Wishbone slave
+	input  wire [ 1:0] wb_addr,
+	output reg  [31:0] wb_rdata,
+	input  wire [31:0] wb_wdata,
+	input  wire        wb_we,
+	input  wire        wb_cyc,
+	output wire        wb_ack,
+
+	// USB
+	input  wire usb_sof,
+
+	// Clock / Reset
+	input  wire clk,
+	input  wire rst
+);
+
+	// Signals
+	// -------
+
+	// Wishbone
+	reg  b_ack;
+	reg  b_we_csr;
+	reg  b_we_volume;
+	reg  b_we_fifo;
+	wire b_rd_rst;
+
+	reg         run;
+	reg  [15:0] volume[0:1];
+
+	// FSM
+	localparam
+		ST_IDLE  = 0,
+		ST_RUN   = 1,
+		ST_FLUSH = 2;
+
+	reg  [ 1:0] state;
+	reg  [ 1:0] state_nxt;
+
+	wire running;
+
+	// Timebase
+	wire        tick;
+	reg  [ 9:0] tick_cnt;
+
+	reg  [15:0] tpf_cnt;
+	reg  [15:0] tpf_cap;
+
+	// FIFO
+    wire [31:0] fw_data;
+    wire        fw_ena;
+    wire        fw_full;
+
+    wire [31:0] fr_data;
+    wire        fr_ena;
+    wire        fr_empty;
+
+	reg  [ 9:0] f_lvl;
+	wire [ 9:0] f_mod;
+
+	// Audio pipeline
+	reg  [15:0] av_volume [0:1];
+	reg  [15:0] av_sample [0:1];
+	reg  [31:0] av_scaled [0:1];
+
+	wire [15:0] av_out[0:1];
+
+
+	// Wishbone interface
+	// ------------------
+
+	// Ack
+	always @(posedge clk)
+		b_ack <= wb_cyc & ~b_ack;
+
+	assign wb_ack = b_ack;
+
+	// Write
+	always @(posedge clk)
+	begin
+		if (b_ack) begin
+			b_we_csr    <= 1'b0;
+			b_we_volume <= 1'b0;
+			b_we_fifo   <= 1'b0;
+		end else begin
+			b_we_csr    <= wb_cyc & wb_we & (wb_addr == 2'b00);
+			b_we_volume <= wb_cyc & wb_we & (wb_addr == 2'b01);
+			b_we_fifo   <= wb_cyc & wb_we & (wb_addr == 2'b10);
+		end
+	end
+
+	always @(posedge clk)
+		if (rst)
+			run <= 1'b0;
+		else if (b_we_csr)
+			run <= wb_wdata[0];
+
+	always @(posedge clk or posedge rst)
+		if (rst)
+			{ volume[1], volume[0] } <= 32'h00000000;
+		else if (b_we_volume)
+			{ volume[1], volume[0] } <= wb_wdata;
+
+	assign fw_data = wb_wdata;
+	assign fw_ena  = b_we_fifo & ~fw_full;
+
+	// Read
+	assign b_rd_rst = ~wb_cyc | b_ack;
+
+	always @(posedge clk)
+		if (b_rd_rst)
+			wb_rdata <= 32'h00000000;
+		else
+			wb_rdata <= { tpf_cap, 2'b00, f_lvl, 2'b00, running, run };
+
+
+	// FSM
+	// ---
+
+	// State register
+	always @(posedge clk)
+		if (rst)
+			state <= ST_IDLE;
+		else
+			state <= state_nxt;
+
+	// Next state
+	always @(*)
+	begin
+		// Default is to stay
+		state_nxt = state;
+
+		// Transitions
+		case (state)
+		ST_IDLE:
+			if (run)
+				state_nxt = ST_RUN;
+
+		ST_RUN:
+			if (~run)
+				state_nxt = ST_FLUSH;
+
+		ST_FLUSH:
+			if (fr_empty)
+				state_nxt = ST_IDLE;
+		endcase
+	end
+
+	// Misc
+	assign running = (state == ST_RUN) | (state == ST_FLUSH);
+
+
+	// Timebase
+	// --------
+
+	// Tick counter
+	always @(posedge clk or posedge rst)
+		if (rst)
+			tick_cnt <= 0;
+		else
+			tick_cnt <= tick ? 10'd498 : (tick_cnt - 1);
+
+	assign tick = tick_cnt[9];
+
+	// Tick-per-usb frame counter
+	always @(posedge clk or posedge rst)
+		if (rst)
+			tpf_cnt <= 16'h0000;
+		else
+			tpf_cnt <= tpf_cnt + tick;
+
+	always @(posedge clk or posedge rst)
+		if (rst)
+			tpf_cap <= 16'h0000;
+		else if (usb_sof)
+			tpf_cap <= tpf_cnt;
+
+
+	// FIFO
+	// ----
+
+	// Instance
+	fifo_sync_ram #(
+		.DEPTH(512),
+		.WIDTH(32)
+	) fifo_I (
+		.wr_data  (fw_data),
+		.wr_ena   (fw_ena),
+		.wr_full  (fw_full),
+		.rd_data  (fr_data),
+		.rd_ena   (fr_ena),
+		.rd_empty (fr_empty),
+		.clk      (clk),
+		.rst      (rst)
+	);
+
+	// Read
+	assign fr_ena = ~fr_empty & tick & running;
+
+	// Level counter
+	always @(posedge clk)
+		if (rst)
+			f_lvl <= 0;
+		else
+			f_lvl <= f_lvl + f_mod;
+
+	assign f_mod = { {9{fr_ena & ~fw_ena}}, fr_ena ^ fw_ena };
+
+
+	// Volume & Mute
+	// -------------
+
+	always @(posedge clk)
+	begin : outpipe
+		integer i;
+
+		for (i=0; i<2; i=i+1)
+		begin
+			av_volume[i] <= volume[i];
+			av_sample[i] <= fr_empty ? 0 : fr_data[16*i+:16];
+			av_scaled[i] <= $signed(av_volume[i]) * $signed(av_sample[i]);
+		end
+	end
+
+
+	// PDM output
+	// ----------
+
+	assign av_out[0] = av_scaled[0][30:15] ^ 16'h8000;
+	assign av_out[1] = av_scaled[1][30:15] ^ 16'h8000;
+
+	pdm #(
+		.WIDTH(16),
+		.DITHER("NO"),
+		.PHY("ICE40")
+	) pdm_I[1:0] (
+		.pdm     (audio),
+		.cfg_val ({av_out[1], av_out[0]}),
+		.cfg_oe  (1'b1),
+		.clk     (clk),
+		.rst     (rst)
+	);
+
+endmodule // audio_pcm

+ 37 - 0
projects/usb_audio/rtl/boards.vh

@@ -0,0 +1,37 @@
+/*
+ * boards.vh
+ *
+ * vim: ts=4 sw=4 syntax=verilog
+ *
+ * Copyright (C) 2020  Sylvain Munaut <tnt@246tNt.com>
+ * SPDX-License-Identifier: CERN-OHL-P-2.0
+ */
+
+`ifdef BOARD_BITSY_V0
+	// 1bitsquared iCEbreaker bitsy prototypes (v0.x)
+	`define HAS_PSRAM
+`elsif BOARD_BITSY_V1
+	// 1bitsquared iCEbreaker bitsy prod (v1.x)
+	`define HAS_PSRAM
+`elsif BOARD_ICEBREAKER
+	// 1bitsquared iCEbreaker
+	`define HAS_PSRAM
+`elsif BOARD_FOMU_HACKER
+	// FOMU clock is 48M
+	`define PLL_CORE
+	`define PLL_CUSTOM
+	`define PLL_DIVR 4'b0000
+	`define PLL_DIVF 7'b0001111
+	`define PLL_DIVQ 3'b100
+	`define PLL_FILTER_RANGE 3'b100
+`endif
+
+
+// Defaults
+	// PLL params 12M input, 48M output
+`ifndef PLL_CUSTOM
+	`define PLL_DIVR 4'b0000
+	`define PLL_DIVF 7'b0111111
+	`define PLL_DIVQ 3'b100
+	`define PLL_FILTER_RANGE 3'b001
+`endif

+ 162 - 0
projects/usb_audio/rtl/dfu_helper.v

@@ -0,0 +1,162 @@
+/*
+ * dfu_helper.v
+ *
+ * vim: ts=4 sw=4
+ *
+ * Copyright (C) 2020  Sylvain Munaut <tnt@246tNt.com>
+ * SPDX-License-Identifier: CERN-OHL-P-2.0
+ */
+
+`default_nettype none
+
+module dfu_helper #(
+	parameter integer TIMER_WIDTH = 24,
+	parameter integer BTN_MODE = 3,		// [2] Use btn_tick, [1] Include IO buffer, [0] Invert (active-low)
+	parameter integer DFU_MODE = 0		// 0 = For user app, 1 = For bootloader
+)(
+	// External control
+	input  wire [1:0] boot_sel,
+	input  wire boot_now,
+
+	// Button
+	input  wire btn_pad,
+	input  wire btn_tick,
+
+	// Outputs
+	output wire btn_val,
+	output reg  rst_req,
+
+	// Clock
+	input  wire clk,
+	input  wire rst
+);
+
+	// Signals
+	// -------
+
+	// Button
+	wire btn_iob;
+	wire btn_v;
+	wire btn_r;
+	wire btn_f;
+
+	// Timer and arming logic
+	reg armed;
+	reg [TIMER_WIDTH-1:0] timer;
+	(* keep="true" *) wire timer_act;
+
+	// Boot logic
+	reg [1:0] wb_sel;
+	reg wb_req;
+	reg wb_now;
+
+
+	// Button logic
+	// ------------
+
+	// IOB
+	generate
+		if (BTN_MODE[1])
+			SB_IO #(
+				.PIN_TYPE(6'b000000),	// Reg input, no output
+				.PULLUP(1'b1),
+				.IO_STANDARD("SB_LVCMOS")
+			) btn_iob_I (
+				.PACKAGE_PIN(btn_pad),
+				.INPUT_CLK  (clk),
+				.D_IN_0     (btn_iob)
+			);
+		else
+			assign btn_iob = btn_pad;
+	endgenerate
+
+	// Deglitch
+	glitch_filter #(
+		.L(BTN_MODE[2] ? 2 : 4),
+		.RST_VAL(BTN_MODE[0]),
+		.WITH_SYNCHRONIZER(1),
+		.WITH_SAMP_COND(BTN_MODE[2])
+	) btn_flt_I (
+		.in       (btn_iob ^ BTN_MODE[0]),
+		.samp_cond(btn_tick),
+		.val      (btn_v),
+		.rise     (btn_r),
+		.fall     (btn_f),
+		.clk      (clk),
+`ifdef SIM
+		.rst      (rst)
+`else
+		// Don't reset so we let the filter settle before
+		// the rest of the logic engages
+		.rst      (1'b0)
+`endif
+	);
+
+	assign btn_val = btn_v;
+
+
+	// Arming & Timer
+	// --------------
+
+	assign timer_act = btn_v ^ armed;
+
+	always @(posedge clk or posedge rst)
+		if (rst)
+			armed <= 1'b0;
+		else
+			armed <= armed | timer[TIMER_WIDTH-2];
+
+	always @(posedge clk or posedge rst)
+		if (rst)
+			timer <= 0;
+		else
+			timer <= timer_act ? { TIMER_WIDTH{1'b0} } : (timer + { { (TIMER_WIDTH-1){1'b0} }, ~timer[TIMER_WIDTH-1] });
+
+
+	// Boot Logic
+	// ----------
+
+	// Decision
+	always @(posedge clk or posedge rst)
+		if (rst) begin
+			wb_sel  <= 2'b00;
+			wb_req  <= 1'b0;
+			rst_req <= 1'b0;
+		end else if (~wb_req) begin
+			if (boot_now) begin
+				// External boot request
+				wb_sel  <= boot_sel;
+				wb_req  <= 1'b1;
+				rst_req <= 1'b0;
+			end else begin
+				if (DFU_MODE == 1) begin
+					// We're in a DFU bootloader, any button press results in
+					// boot to application
+					wb_sel  <= 2'b10;
+					wb_req  <= wb_now | (armed & btn_f);
+					rst_req <= 1'b0;
+				end else begin
+					// We're in user application, short press resets the
+					// logic, long press triggers DFU reboot
+					wb_sel  <= 2'b01;
+					wb_req  <= wb_now  | (armed & btn_f &  timer[TIMER_WIDTH-1]);
+					rst_req <= rst_req | (armed & btn_f & ~timer[TIMER_WIDTH-1]);
+				end
+			end
+		end
+
+	// Ensure select bits are set before the boot pulse
+	always @(posedge clk or posedge rst)
+		if (rst)
+			wb_now <= 1'b0;
+		else
+			wb_now <= wb_req;
+
+	// IP core
+	SB_WARMBOOT warmboot (
+		.BOOT(wb_now),
+		.S0(wb_sel[0]),
+		.S1(wb_sel[1])
+	);
+
+endmodule // dfu_helper

Diferenças do arquivo suprimidas por serem muito extensas
+ 2979 - 0
projects/usb_audio/rtl/picorv32.v


+ 43 - 0
projects/usb_audio/rtl/picorv32_ice40_regs.v

@@ -0,0 +1,43 @@
+/*
+ * picorv32_ice40_regs.v
+ *
+ * vim: ts=4 sw=4
+ *
+ * Implementation of register file for the PicoRV32 on iCE40
+ *
+ * Copyright (C) 2020  Sylvain Munaut <tnt@246tNt.com>
+ * SPDX-License-Identifier: CERN-OHL-P-2.0
+ */
+
+`default_nettype none
+
+module picorv32_ice40_regs (
+	input  wire        clk,
+	input  wire        wen,
+	input  wire  [5:0] waddr,
+	input  wire  [5:0] raddr1,
+	input  wire  [5:0] raddr2,
+	input  wire [31:0] wdata,
+	output wire [31:0] rdata1,
+	output wire [31:0] rdata2
+);
+
+	ice40_ebr #(
+		.READ_MODE(0),
+		.WRITE_MODE(0),
+		.MASK_WORKAROUND(0),
+		.NEG_WR_CLK(0),
+		.NEG_RD_CLK(1)
+	) regs[3:0] (
+		.wr_addr ({ 4{2'b00, waddr} }),
+		.wr_data ({ 2{wdata} }),
+		.wr_mask (64'h0000000000000000),
+		.wr_ena  (wen),
+		.wr_clk  (clk),
+		.rd_addr ({2'b00, raddr2, 2'b00, raddr2, 2'b00, raddr1, 2'b00, raddr1}),
+		.rd_data ({rdata2, rdata1}),
+		.rd_ena  (1'b1),
+		.rd_clk  (clk)
+	);
+
+endmodule // picorv32_ice40_regs

+ 38 - 0
projects/usb_audio/rtl/soc_bram.v

@@ -0,0 +1,38 @@
+/*
+ * soc_bram.v
+ *
+ * vim: ts=4 sw=4
+ *
+ * Copyright (C) 2020  Sylvain Munaut <tnt@246tNt.com>
+ * SPDX-License-Identifier: CERN-OHL-P-2.0
+ */
+
+`default_nettype none
+
+module soc_bram #(
+	parameter integer AW = 8,
+	parameter INIT_FILE = ""
+)(
+	input  wire [AW-1:0] addr,
+	output reg    [31:0] rdata,
+	input  wire   [31:0] wdata,
+	input  wire   [ 3:0] wmsk,
+	input  wire          we,
+	input  wire          clk
+);
+
+	reg [31:0] mem [0:(1<<AW)-1];
+
+	initial
+		if (INIT_FILE != "")
+			$readmemh(INIT_FILE, mem);
+
+	always @(posedge clk) begin
+		rdata <= mem[addr];
+		if (we & ~wmsk[0]) mem[addr][ 7: 0] <= wdata[ 7: 0];
+		if (we & ~wmsk[1]) mem[addr][15: 8] <= wdata[15: 8];
+		if (we & ~wmsk[2]) mem[addr][23:16] <= wdata[23:16];
+		if (we & ~wmsk[3]) mem[addr][31:24] <= wdata[31:24];
+	end
+
+endmodule // soc_bram

+ 158 - 0
projects/usb_audio/rtl/soc_picorv32_base.v

@@ -0,0 +1,158 @@
+/*
+ * soc_picorv32_base.v
+ *
+ * vim: ts=4 sw=4
+ *
+ * Copyright (C) 2019-2020  Sylvain Munaut <tnt@246tNt.com>
+ * SPDX-License-Identifier: CERN-OHL-P-2.0
+ */
+
+`default_nettype none
+`include "boards.vh"
+
+module soc_picorv32_base #(
+	parameter integer WB_N  =  6,
+	parameter integer WB_DW = 32,
+	parameter integer WB_AW = 16,
+	parameter integer SPRAM_AW = 14,	/* 14 => 64k, 15 => 128k */
+
+	/* auto */
+	parameter integer WB_MW = WB_DW / 8,
+	parameter integer WB_RW = WB_DW * WB_N,
+	parameter integer WB_AI = $clog2(WB_MW)
+)(
+	// Wishbone
+	output wire [WB_AW-1:0] wb_addr,
+	input  wire [WB_RW-1:0] wb_rdata,
+	output wire [WB_DW-1:0] wb_wdata,
+	output wire [WB_MW-1:0] wb_wmsk,
+	output wire             wb_we,
+	output wire [WB_N -1:0] wb_cyc,
+	input  wire [WB_N -1:0] wb_ack,
+
+	// Clock / Reset
+	input  wire clk,
+	input  wire rst
+);
+
+	// Signals
+	// -------
+
+	// Memory bus
+	wire        mem_valid;
+	wire        mem_instr;
+	wire        mem_ready;
+	wire [31:0] mem_addr;
+	wire [31:0] mem_rdata;
+	wire [31:0] mem_wdata;
+	wire [ 3:0] mem_wstrb;
+
+	// RAM
+		// BRAM
+	wire [ 7:0] bram_addr;
+	wire [31:0] bram_rdata;
+	wire [31:0] bram_wdata;
+	wire [ 3:0] bram_wmsk;
+	wire        bram_we;
+
+		// SPRAM
+	wire [14:0] spram_addr;
+	wire [31:0] spram_rdata;
+	wire [31:0] spram_wdata;
+	wire [ 3:0] spram_wmsk;
+	wire        spram_we;
+
+
+	// CPU
+	// ---
+
+	picorv32 #(
+		.PROGADDR_RESET(32'h 0000_0000),
+		.STACKADDR(32'h 0000_0400),
+		.BARREL_SHIFTER(0),
+		.COMPRESSED_ISA(0),
+		.ENABLE_COUNTERS(0),
+		.ENABLE_MUL(0),
+		.ENABLE_DIV(0),
+		.ENABLE_IRQ(0),
+		.ENABLE_IRQ_QREGS(0),
+		.CATCH_MISALIGN(0),
+		.CATCH_ILLINSN(0)
+	) cpu_I (
+		.clk       (clk),
+		.resetn    (~rst),
+		.mem_valid (mem_valid),
+		.mem_instr (mem_instr),
+		.mem_ready (mem_ready),
+		.mem_addr  (mem_addr),
+		.mem_wdata (mem_wdata),
+		.mem_wstrb (mem_wstrb),
+		.mem_rdata (mem_rdata)
+	);
+
+
+	// Bus interface
+	// -------------
+
+	soc_picorv32_bridge #(
+		.WB_N (WB_N),
+		.WB_DW(WB_DW),
+		.WB_AW(WB_AW),
+		.WB_AI(WB_AI)
+	) pb_I (
+		.pb_addr     (mem_addr),
+		.pb_rdata    (mem_rdata),
+		.pb_wdata    (mem_wdata),
+		.pb_wstrb    (mem_wstrb),
+		.pb_valid    (mem_valid),
+		.pb_ready    (mem_ready),
+		.bram_addr   (bram_addr),
+		.bram_rdata  (bram_rdata),
+		.bram_wdata  (bram_wdata),
+		.bram_wmsk   (bram_wmsk),
+		.bram_we     (bram_we),
+		.spram_addr  (spram_addr),
+		.spram_rdata (spram_rdata),
+		.spram_wdata (spram_wdata),
+		.spram_wmsk  (spram_wmsk),
+		.spram_we    (spram_we),
+		.wb_addr     (wb_addr),
+		.wb_wdata    (wb_wdata),
+		.wb_wmsk     (wb_wmsk),
+		.wb_rdata    (wb_rdata),
+		.wb_cyc      (wb_cyc),
+		.wb_we       (wb_we),
+		.wb_ack      (wb_ack),
+		.clk         (clk),
+		.rst         (rst)
+	);
+
+
+	// Local memory
+	// ------------
+
+	// Boot memory
+	soc_bram #(
+		.INIT_FILE("boot.hex")
+	) bram_I (
+		.addr  (bram_addr),
+		.rdata (bram_rdata),
+		.wdata (bram_wdata),
+		.wmsk  (bram_wmsk),
+		.we    (bram_we),
+		.clk   (clk)
+	);
+
+	// Main memory
+	soc_spram #(
+		.AW(SPRAM_AW)
+	) spram_I (
+		.addr  (spram_addr[SPRAM_AW-1:0]),
+		.rdata (spram_rdata),
+		.wdata (spram_wdata),
+		.wmsk  (spram_wmsk),
+		.we    (spram_we),
+		.clk   (clk)
+	);
+
+endmodule // soc_picorv32_base

+ 186 - 0
projects/usb_audio/rtl/soc_picorv32_bridge.v

@@ -0,0 +1,186 @@
+/*
+ * soc_picorv32_bridge.v
+ *
+ * vim: ts=4 sw=4
+ *
+ * Copyright (C) 2020  Sylvain Munaut <tnt@246tNt.com>
+ * SPDX-License-Identifier: CERN-OHL-P-2.0
+ */
+
+`default_nettype none
+
+module soc_picorv32_bridge #(
+	parameter integer WB_N   =  8,
+	parameter integer WB_DW  = 32,
+	parameter integer WB_AW  = 16,
+	parameter integer WB_AI  =  2,
+	parameter integer WB_REG =  0	// [0] = cyc / [1] = addr/wdata/wstrb / [2] = ack/rdata
+)(
+	/* PicoRV32 bus */
+	input  wire [31:0] pb_addr,
+	output wire [31:0] pb_rdata,
+	input  wire [31:0] pb_wdata,
+	input  wire [ 3:0] pb_wstrb,
+	input  wire        pb_valid,
+	output wire        pb_ready,
+
+	/* BRAM */
+	output wire [ 7:0] bram_addr,
+	input  wire [31:0] bram_rdata,
+	output wire [31:0] bram_wdata,
+	output wire [ 3:0] bram_wmsk,
+	output wire        bram_we,
+
+	/* SPRAM */
+	output wire [14:0] spram_addr,
+	input  wire [31:0] spram_rdata,
+	output wire [31:0] spram_wdata,
+	output wire [ 3:0] spram_wmsk,
+	output wire        spram_we,
+
+	/* Wishbone buses */
+	output wire [WB_AW-1:0]        wb_addr,
+	input  wire [(WB_DW*WB_N)-1:0] wb_rdata,
+	output wire [WB_DW-1:0]        wb_wdata,
+	output wire [(WB_DW/8)-1:0]    wb_wmsk,
+	output wire                    wb_we,
+	output wire [WB_N-1:0]         wb_cyc,
+	input  wire [WB_N-1:0]         wb_ack,
+
+	/* Clock / Reset */
+	input  wire clk,
+	input  wire rst
+);
+
+	// Signals
+	// -------
+
+	wire ram_sel;
+	reg  ram_rdy;
+	wire [31:0] ram_rdata;
+
+	(* keep *) wire [WB_N-1:0] wb_match;
+	(* keep *) wire wb_cyc_rst;
+
+	reg  [31:0] wb_rdata_or;
+	wire [31:0] wb_rdata_out;
+	wire wb_rdy;
+
+
+	// RAM access
+	// ----------
+	// BRAM  : 0x00000000 -> 0x000003ff
+	// SPRAM : 0x00020000 -> 0x0003ffff
+
+	assign bram_addr  = pb_addr[ 9:2];
+	assign spram_addr = pb_addr[16:2];
+
+	assign bram_wdata  = pb_wdata;
+	assign spram_wdata = pb_wdata;
+
+	assign bram_wmsk  = ~pb_wstrb;
+	assign spram_wmsk = ~pb_wstrb;
+
+	assign bram_we  = pb_valid & ~pb_addr[31] & |pb_wstrb & ~pb_addr[17];
+	assign spram_we = pb_valid & ~pb_addr[31] & |pb_wstrb &  pb_addr[17];
+
+	assign ram_rdata = ~pb_addr[31] ? (pb_addr[17] ? spram_rdata : bram_rdata) : 32'h00000000;
+
+	assign ram_sel = pb_valid & ~pb_addr[31];
+
+	always @(posedge clk)
+		ram_rdy <= ram_sel && ~ram_rdy;
+
+
+	// Wishbone
+	// --------
+	// wb[x] = 0x8x000000 - 0x8xffffff
+
+	// Access Cycle
+	genvar i;
+	for (i=0; i<WB_N; i=i+1)
+		assign wb_match[i] = (pb_addr[27:24] == i);
+
+	if (WB_REG & 1) begin
+		// Register
+		reg [WB_N-1:0] wb_cyc_reg;
+		always @(posedge clk)
+			if (wb_cyc_rst)
+				wb_cyc_reg <= 0;
+			else
+				wb_cyc_reg <= wb_match & ~wb_ack;
+		assign wb_cyc = wb_cyc_reg;
+	end else begin
+		// Direct connection
+		assign wb_cyc = wb_cyc_rst ? { WB_N{1'b0} } : wb_match;
+	end
+
+	// Addr / Write-Data / Write-Mask / Write-Enable
+	if (WB_REG & 2) begin
+		// Register
+		reg [WB_AW-1:0] wb_addr_reg;
+		reg [WB_DW-1:0] wb_wdata_reg;
+		reg [(WB_DW/8)-1:0] wb_wmsk_reg;
+		reg wb_we_reg;
+
+		always @(posedge clk)
+		begin
+			wb_addr_reg  <= pb_addr[WB_AW+WB_AI-1:WB_AI];
+			wb_wdata_reg <= pb_wdata[WB_DW-1:0];
+			wb_wmsk_reg  <= ~pb_wstrb[(WB_DW/8)-1:0];
+			wb_we_reg    <= |pb_wstrb;
+		end
+
+		assign wb_addr  = wb_addr_reg;
+		assign wb_wdata = wb_wdata_reg;
+		assign wb_wmsk  = wb_wmsk_reg;
+		assign wb_we    = wb_we_reg;
+	end else begin
+		// Direct connection
+		assign wb_addr  = pb_addr[WB_AW+WB_AI-1:WB_AI];
+		assign wb_wdata = pb_wdata[WB_DW-1:0];
+		assign wb_wmsk  = pb_wstrb[(WB_DW/8)-1:0];
+		assign wb_we    = |pb_wstrb;
+	end
+
+	// Ack / Read-Data
+	always @(*)
+	begin : wb_or
+		integer i;
+		wb_rdata_or = 0;
+		for (i=0; i<WB_N; i=i+1)
+			wb_rdata_or[WB_DW-1:0] = wb_rdata_or[WB_DW-1:0] | wb_rdata[WB_DW*i+:WB_DW];
+	end
+
+	if (WB_REG & 4) begin
+		// Register
+		reg wb_rdy_reg;
+		reg [31:0] wb_rdata_reg;
+
+		always @(posedge clk)
+			wb_rdy_reg <= |wb_ack;
+
+		always @(posedge clk)
+			if (wb_cyc_rst)
+				wb_rdata_reg <= 32'h00000000;
+			else
+				wb_rdata_reg <= wb_rdata_or;
+
+		assign wb_cyc_rst = ~pb_valid | ~pb_addr[31] | wb_rdy_reg;
+		assign wb_rdy = wb_rdy_reg;
+		assign wb_rdata_out = wb_rdata_reg;
+	end else begin
+		// Direct connection
+		assign wb_cyc_rst = ~pb_valid | ~pb_addr[31];
+		assign wb_rdy = |wb_ack;
+		assign wb_rdata_out = wb_rdata_or;
+	end
+
+
+	// Final data combining
+	// --------------------
+
+	assign pb_rdata = ram_rdata | wb_rdata_out;
+	assign pb_ready = ram_rdy | wb_rdy;
+
+endmodule // soc_picorv32_bridge

+ 43 - 0
projects/usb_audio/rtl/soc_spram.v

@@ -0,0 +1,43 @@
+/*
+ * soc_spram.v
+ *
+ * vim: ts=4 sw=4
+ *
+ * Copyright (C) 2020  Sylvain Munaut <tnt@246tNt.com>
+ * SPDX-License-Identifier: CERN-OHL-P-2.0
+ */
+
+`default_nettype none
+
+module soc_spram #(
+	parameter integer AW = 14
+)(
+	input  wire [AW-1:0] addr,
+	output wire   [31:0] rdata,
+	input  wire   [31:0] wdata,
+	input  wire   [ 3:0] wmsk,
+	input  wire          we,
+	input  wire          clk
+);
+
+	wire [7:0] msk_nibble = {
+		wmsk[3], wmsk[3],
+		wmsk[2], wmsk[2],
+		wmsk[1], wmsk[1],
+		wmsk[0], wmsk[0]
+	};
+
+	ice40_spram_gen #(
+		.ADDR_WIDTH(AW),
+		.DATA_WIDTH(32)
+	) spram_I (
+		.addr(addr),
+		.rd_data(rdata),
+		.rd_ena(1'b1),
+		.wr_data(wdata),
+		.wr_mask(msk_nibble),
+		.wr_ena(we),
+		.clk(clk)
+	);
+
+endmodule // soc_spram

+ 157 - 0
projects/usb_audio/rtl/soc_usb.v

@@ -0,0 +1,157 @@
+/*
+ * soc_usb.v
+ *
+ * vim: ts=4 sw=4
+ *
+ * Copyright (C) 2020  Sylvain Munaut <tnt@246tNt.com>
+ * SPDX-License-Identifier: CERN-OHL-P-2.0
+ */
+
+`default_nettype none
+
+module soc_usb #(
+	parameter integer DW = 32
+)(
+	// USB
+	inout  wire usb_dp,
+	inout  wire usb_dn,
+	output wire usb_pu,
+
+	// Wishbone slave
+	input  wire [  11:0] wb_addr,
+	output wire [DW-1:0] wb_rdata,
+	input  wire [DW-1:0] wb_wdata,
+	input  wire          wb_we,
+	input  wire    [1:0] wb_cyc,
+	output wire    [1:0] wb_ack,
+
+	// Misc
+	output wire usb_sof,
+
+	// Clock / Reset
+	input  wire clk_sys,
+	input  wire clk_48m,
+	input  wire rst
+);
+
+	// Signals
+	// -------
+
+	// Bus OR
+	wire [DW-1:0] wb_rdata_i[0:1];
+
+	// Wishbone in 48 MHz domain
+	wire [11:0] ub_addr;
+	wire [15:0] ub_wdata;
+	wire [15:0] ub_rdata;
+	wire        ub_cyc;
+	wire        ub_we;
+	wire        ub_ack;
+
+	// SoF in 48 MHz domain
+	wire        usb_sof_48m;
+
+	// EP Buffer
+	wire [ 8:0] ep_tx_addr_0;
+	wire [31:0] ep_tx_data_0;
+	wire        ep_tx_we_0;
+
+	wire [ 8:0] ep_rx_addr_0;
+	wire [31:0] ep_rx_data_1;
+	wire        ep_rx_re_0;
+
+	reg ack_ep;
+
+
+	// Cross-clock
+	// -----------
+		// Bring control reg wishbone to 48 MHz domain
+
+	xclk_wb #(
+		.DW(16),
+		.AW(12)
+	)  wb_48m_xclk_I (
+		.s_addr  (wb_addr[11:0]),
+		.s_wdata (wb_wdata[15:0]),
+		.s_rdata (wb_rdata_i[0][15:0]),
+		.s_cyc   (wb_cyc[0]),
+		.s_ack   (wb_ack[0]),
+		.s_we    (wb_we),
+		.s_clk   (clk_sys),
+		.m_addr  (ub_addr),
+		.m_wdata (ub_wdata),
+		.m_rdata (ub_rdata),
+		.m_cyc   (ub_cyc),
+		.m_ack   (ub_ack),
+		.m_we    (ub_we),
+		.m_clk   (clk_48m),
+		.rst     (rst)
+	);
+
+	if (DW != 16)
+		assign wb_rdata_i[0][DW-1:16] = 0;
+
+	xclk_strobe sof_xclk_I (
+		.in_stb  (usb_sof_48m),
+		.in_clk  (clk_48m),
+		.out_stb (usb_sof),
+		.out_clk (clk_sys),
+		.rst     (rst)
+	);
+
+
+	// Core
+	// ----
+
+	usb #(
+		.EPDW(32)
+	) usb_I (
+		.pad_dp       (usb_dp),
+		.pad_dn       (usb_dn),
+		.pad_pu       (usb_pu),
+		.ep_tx_addr_0 (ep_tx_addr_0),
+		.ep_tx_data_0 (ep_tx_data_0),
+		.ep_tx_we_0   (ep_tx_we_0),
+		.ep_rx_addr_0 (ep_rx_addr_0),
+		.ep_rx_data_1 (ep_rx_data_1),
+		.ep_rx_re_0   (ep_rx_re_0),
+		.ep_clk       (clk_sys),
+		.wb_addr      (ub_addr),
+		.wb_rdata     (ub_rdata),
+		.wb_wdata     (ub_wdata),
+		.wb_we        (ub_we),
+		.wb_cyc       (ub_cyc),
+		.wb_ack       (ub_ack),
+		.sof          (usb_sof_48m),
+		.clk          (clk_48m),
+		.rst          (rst)
+	);
+
+
+	// EP data
+	// -------
+
+	assign ep_tx_addr_0 = wb_addr[8:0];
+	assign ep_rx_addr_0 = wb_addr[8:0];
+
+	assign ep_tx_data_0 = wb_wdata;
+	assign wb_rdata_i[1] = ack_ep ? ep_rx_data_1 : 32'h00000000;
+
+	assign ep_tx_we_0 = wb_cyc[1] & wb_we & ~ack_ep;
+	assign ep_rx_re_0 = 1'b1;
+
+	assign wb_ack[1] = ack_ep;
+
+	always @(posedge clk_sys or posedge rst)
+		if (rst)
+			ack_ep <= 1'b0;
+		else
+			ack_ep <= wb_cyc[1] & ~ack_ep;
+
+
+	// Bus read data
+	// -------------
+
+	assign wb_rdata = wb_rdata_i[0] | wb_rdata_i[1];
+
+endmodule // soc_usb

+ 89 - 0
projects/usb_audio/rtl/sysmgr.v

@@ -0,0 +1,89 @@
+/*
+ * sysmgr.v
+ *
+ * vim: ts=4 sw=4
+ *
+ * Copyright (C) 2019-2020  Sylvain Munaut <tnt@246tNt.com>
+ * SPDX-License-Identifier: CERN-OHL-P-2.0
+ */
+
+`default_nettype none
+`include "boards.vh"
+
+module sysmgr (
+	input  wire clk_in,
+	input  wire rst_in,
+	output wire clk_24m,
+	output wire clk_48m,
+	output wire rst_out
+);
+
+	// Signals
+	wire pll_lock;
+	wire pll_reset_n;
+
+	wire clk_24m_i;
+	wire clk_48m_i;
+	wire rst_i;
+	reg [3:0] rst_cnt;
+
+	// PLL instance
+`ifdef PLL_CORE
+	SB_PLL40_2F_CORE #(
+`else
+	SB_PLL40_2F_PAD #(
+`endif
+		.DIVR(`PLL_DIVR),
+		.DIVF(`PLL_DIVF),
+		.DIVQ(`PLL_DIVQ),
+		.FILTER_RANGE(`PLL_FILTER_RANGE),
+		.FEEDBACK_PATH("SIMPLE"),
+		.DELAY_ADJUSTMENT_MODE_FEEDBACK("FIXED"),
+		.FDA_FEEDBACK(4'b0000),
+		.SHIFTREG_DIV_MODE(2'b00),
+		.PLLOUT_SELECT_PORTA("GENCLK"),
+		.PLLOUT_SELECT_PORTB("GENCLK_HALF"),
+		.ENABLE_ICEGATE_PORTA(1'b0),
+		.ENABLE_ICEGATE_PORTB(1'b0)
+	) pll_I (
+`ifdef PLL_CORE
+		.REFERENCECLK(clk_in),
+`else
+		.PACKAGEPIN(clk_in),
+`endif
+		.PLLOUTCOREA(),
+		.PLLOUTGLOBALA(clk_48m_i),
+		.PLLOUTCOREB(),
+		.PLLOUTGLOBALB(clk_24m_i),
+		.EXTFEEDBACK(1'b0),
+		.DYNAMICDELAY(8'h00),
+		.RESETB(pll_reset_n),
+		.BYPASS(1'b0),
+		.LATCHINPUTVALUE(1'b0),
+		.LOCK(pll_lock),
+		.SDI(1'b0),
+		.SDO(),
+		.SCLK(1'b0)
+	);
+
+	assign clk_24m = clk_24m_i;
+	assign clk_48m = clk_48m_i;
+
+	// PLL reset generation
+	assign pll_reset_n = ~rst_in;
+
+	// Logic reset generation
+	always @(posedge clk_24m_i or negedge pll_lock)
+		if (!pll_lock)
+			rst_cnt <= 4'h0;
+		else if (~rst_cnt[3])
+			rst_cnt <= rst_cnt + 1;
+
+	assign rst_i = ~rst_cnt[3];
+
+	SB_GB rst_gbuf_I (
+		.USER_SIGNAL_TO_GLOBAL_BUFFER(rst_i),
+		.GLOBAL_BUFFER_OUTPUT(rst_out)
+	);
+
+endmodule // sysmgr

+ 302 - 0
projects/usb_audio/rtl/top.v

@@ -0,0 +1,302 @@
+/*
+ * top.v
+ *
+ * vim: ts=4 sw=4
+ *
+ * Copyright (C) 2019-2020  Sylvain Munaut <tnt@246tNt.com>
+ * SPDX-License-Identifier: CERN-OHL-P-2.0
+ */
+
+`default_nettype none
+`include "boards.vh"
+
+module top (
+	// SPI
+	inout  wire spi_mosi,
+	inout  wire spi_miso,
+	inout  wire spi_clk,
+	output wire spi_flash_cs_n,
+`ifdef HAS_PSRAM
+	output wire spi_ram_cs_n,
+`endif
+
+	// USB
+	inout  wire usb_dp,
+	inout  wire usb_dn,
+	output wire usb_pu,
+
+	// Debug UART
+	input  wire uart_rx,
+	output wire uart_tx,
+
+	// Button
+	input  wire btn,
+
+	// LED
+	output wire [2:0] rgb,
+
+	// Audio output
+	output wire [1:0] audio,
+
+	input  wire midi_rx,
+	output wire midi_tx,
+
+	// Clock
+	input  wire clk_in
+);
+
+	localparam integer SPRAM_AW = 14; /* 14 => 64k, 15 => 128k */
+	localparam integer WB_N  =  8;
+
+	localparam integer WB_DW = 32;
+	localparam integer WB_AW = 16;
+	localparam integer WB_RW = WB_DW * WB_N;
+	localparam integer WB_MW = WB_DW / 8;
+
+	genvar i;
+
+
+	// Signals
+	// -------
+
+	// Wishbone
+	wire [WB_AW-1:0] wb_addr;
+	wire [WB_DW-1:0] wb_rdata [0:WB_N-1];
+	wire [WB_RW-1:0] wb_rdata_flat;
+	wire [WB_DW-1:0] wb_wdata;
+	wire [WB_MW-1:0] wb_wmsk;
+	wire [WB_N -1:0] wb_cyc;
+	wire             wb_we;
+	wire [WB_N -1:0] wb_ack;
+
+	// USB
+	wire usb_sof;
+
+	// WarmBoot
+	reg boot_now;
+	reg [1:0] boot_sel;
+
+	// Clock / Reset logic
+	wire clk_24m;
+	wire clk_48m;
+	wire rst;
+
+
+	// SoC
+	// ---
+
+	soc_picorv32_base #(
+		.WB_N    (WB_N),
+		.WB_DW   (WB_DW),
+		.WB_AW   (WB_AW),
+		.SPRAM_AW(SPRAM_AW)
+	) base_I (
+		.wb_addr (wb_addr),
+		.wb_rdata(wb_rdata_flat),
+		.wb_wdata(wb_wdata),
+		.wb_wmsk (wb_wmsk),
+		.wb_we   (wb_we),
+		.wb_cyc  (wb_cyc),
+		.wb_ack  (wb_ack),
+		.clk     (clk_24m),
+		.rst     (rst)
+	);
+
+	for (i=0; i<WB_N; i=i+1)
+		assign wb_rdata_flat[i*WB_DW+:WB_DW] = wb_rdata[i];
+
+
+	// UART [1]
+	// ----
+
+	uart_wb #(
+		.DIV_WIDTH(12),
+		.DW(WB_DW)
+	) uart_I (
+		.uart_tx  (uart_tx),
+		.uart_rx  (uart_rx),
+		.wb_addr  (wb_addr[1:0]),
+		.wb_rdata (wb_rdata[1]),
+		.wb_we    (wb_we),
+		.wb_wdata (wb_wdata),
+		.wb_cyc   (wb_cyc[1]),
+		.wb_ack   (wb_ack[1]),
+		.clk      (clk_24m),
+		.rst      (rst)
+	);
+
+
+	// SPI [2]
+	// ---
+
+	ice40_spi_wb #(
+`ifdef HAS_PSRAM
+		.N_CS(2),
+`else
+		.N_CS(1),
+`endif
+		.WITH_IOB(1),
+		.UNIT(0)
+	) spi_I (
+		.pad_mosi (spi_mosi),
+		.pad_miso (spi_miso),
+		.pad_clk  (spi_clk),
+`ifdef HAS_PSRAM
+		.pad_csn  ({spi_ram_cs_n, spi_flash_cs_n}),
+`else
+		.pad_csn  (spi_flash_cs_n),
+`endif
+		.wb_addr  (wb_addr[3:0]),
+		.wb_rdata (wb_rdata[2]),
+		.wb_wdata (wb_wdata),
+		.wb_we    (wb_we),
+		.wb_cyc   (wb_cyc[2]),
+		.wb_ack   (wb_ack[2]),
+		.clk      (clk_24m),
+		.rst      (rst)
+	);
+
+
+	// RGB LEDs [3]
+	// --------
+
+	ice40_rgb_wb #(
+		.CURRENT_MODE("0b1"),
+		.RGB0_CURRENT("0b000001"),
+		.RGB1_CURRENT("0b000001"),
+		.RGB2_CURRENT("0b000001")
+	) rgb_I (
+		.pad_rgb    (rgb),
+		.wb_addr    (wb_addr[4:0]),
+		.wb_rdata   (wb_rdata[3]),
+		.wb_wdata   (wb_wdata),
+		.wb_we      (wb_we),
+		.wb_cyc     (wb_cyc[3]),
+		.wb_ack     (wb_ack[3]),
+		.clk        (clk_24m),
+		.rst        (rst)
+	);
+
+
+	// USB [4 & 5]
+	// ---
+
+	soc_usb #(
+		.DW(WB_DW)
+	) usb_I (
+		.usb_dp   (usb_dp),
+		.usb_dn   (usb_dn),
+		.usb_pu   (usb_pu),
+		.wb_addr  (wb_addr[11:0]),
+		.wb_rdata (wb_rdata[4]),
+		.wb_wdata (wb_wdata),
+		.wb_we    (wb_we),
+		.wb_cyc   (wb_cyc[5:4]),
+		.wb_ack   (wb_ack[5:4]),
+		.usb_sof  (usb_sof),
+		.clk_sys  (clk_24m),
+		.clk_48m  (clk_48m),
+		.rst      (rst)
+	);
+
+	assign wb_rdata[5] = 0;
+
+
+	// Audio PCM [6]
+	// ---------
+
+	audio_pcm pcm_I (
+		.audio    (audio),
+		.wb_addr  (wb_addr[1:0]),
+		.wb_rdata (wb_rdata[6]),
+		.wb_wdata (wb_wdata),
+		.wb_we    (wb_we),
+		.wb_cyc   (wb_cyc[6]),
+		.wb_ack   (wb_ack[6]),
+		.usb_sof  (usb_sof),
+		.clk      (clk_24m),
+		.rst      (rst)
+	);
+
+
+	// Midi [7]
+	//---------
+
+	uart_wb #(
+		.DIV_WIDTH(12),
+		.DW(WB_DW)
+	) midi_I (
+		.uart_tx  (midi_tx),
+		.uart_rx  (midi_rx),
+		.wb_addr  (wb_addr[1:0]),
+		.wb_rdata (wb_rdata[7]),
+		.wb_we    (wb_we),
+		.wb_wdata (wb_wdata),
+		.wb_cyc   (wb_cyc[7]),
+		.wb_ack   (wb_ack[7]),
+		.clk      (clk_24m),
+		.rst      (rst)
+	);
+
+
+	// Warm Boot
+	// ---------
+
+	// Bus interface
+	always @(posedge clk_24m or posedge rst)
+		if (rst) begin
+			boot_now <= 1'b0;
+			boot_sel <= 2'b00;
+		end else if (wb_cyc[0] & wb_we & (wb_addr[2:0] == 3'b000)) begin
+			boot_now <= wb_wdata[2];
+			boot_sel <= wb_wdata[1:0];
+		end
+
+	assign wb_rdata[0] = 0;
+	assign wb_ack[0] = wb_cyc[0];
+
+	// Helper
+	dfu_helper #(
+		.TIMER_WIDTH(24),
+		.BTN_MODE(3),
+		.DFU_MODE(0)
+	) dfu_helper_I (
+		.boot_now(boot_now),
+		.boot_sel(boot_sel),
+		.btn_pad(btn),
+		.btn_val(),
+		.rst_req(),
+		.clk(clk_24m),
+		.rst(rst)
+	);
+
+
+	// Clock / Reset
+	// -------------
+
+`ifdef SIM
+	reg clk_48m_s = 1'b0;
+	reg clk_24m_s = 1'b0;
+	reg rst_s = 1'b1;
+
+	always #10.42 clk_48m_s <= !clk_48m_s;
+	always #20.84 clk_24m_s <= !clk_24m_s;
+
+	initial begin
+		#200 rst_s = 0;
+	end
+
+	assign clk_48m = clk_48m_s;
+	assign clk_24m = clk_24m_s;
+	assign rst = rst_s;
+`else
+	sysmgr sys_mgr_I (
+		.clk_in(clk_in),
+		.rst_in(1'b0),
+		.clk_48m(clk_48m),
+		.clk_24m(clk_24m),
+		.rst_out(rst)
+	);
+`endif
+
+endmodule // top

+ 82 - 0
projects/usb_audio/sim/dfu_helper_tb.v

@@ -0,0 +1,82 @@
+/*
+ * dfu_helper_tb.v
+ *
+ * vim: ts=4 sw=4
+ *
+ * Copyright (C) 2019  Sylvain Munaut <tnt@246tNt.com>
+ * All rights reserved.
+ *
+ * BSD 3-clause, see LICENSE.bsd
+ *
+ * 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 <organization> 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 <COPYRIGHT HOLDER> 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.
+ */
+
+`default_nettype none
+
+module dfu_helper_tb;
+
+	// Signals
+	// -------
+
+	reg clk = 1'b0;
+	reg rst = 1'b1;
+	reg btn = 1'b0;
+
+
+	// Setup recording
+	// ---------------
+
+	initial begin
+		$dumpfile("dfu_helper_tb.vcd");
+		$dumpvars(0,dfu_helper_tb);
+		# 2000000 $finish;
+	end
+
+	always #10 clk <= !clk;
+
+	initial begin
+		#200 rst = 0;
+		#10000 btn = 1;
+		#200000 btn = 0;
+		#100000 btn = 1;
+	end
+
+
+	// DUT
+	// ---
+
+	dfu_helper #(
+		.TIMER_WIDTH(12),
+		.BTN_MODE(3),
+		.DFU_MODE(0)
+	) dut_I (
+		.boot_sel(2'b00),
+		.boot_now(1'b0),
+		.btn_pad(btn),
+		.btn_val(),
+		.rst_req(),
+		.clk(clk),
+		.rst(rst)
+	);
+
+endmodule // dfu_helper_tb

+ 425 - 0
projects/usb_audio/sim/spiflash.v

@@ -0,0 +1,425 @@
+/*
+ *  PicoSoC - A simple example SoC using PicoRV32
+ *
+ *  Copyright (C) 2017  Clifford Wolf <clifford@clifford.at>
+ *
+ *  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.
+ *
+ */
+
+`timescale 1 ns / 1 ps
+
+//
+// Simple SPI flash simulation model
+//
+// This model samples io input signals 1ns before the SPI clock edge and
+// updates output signals 1ns after the SPI clock edge.
+//
+// Supported commands:
+//    AB, B9, FF, 03, BB, EB, ED
+//
+// Well written SPI flash data sheets:
+//    Cypress S25FL064L http://www.cypress.com/file/316661/download
+//    Cypress S25FL128L http://www.cypress.com/file/316171/download
+//
+// SPI flash used on iCEBreaker board:
+//    https://www.winbond.com/resource-files/w25q128jv%20dtr%20revb%2011042016.pdf
+//
+
+module spiflash (
+	input csb,
+	input clk,
+	inout io0, // MOSI
+	inout io1, // MISO
+	inout io2,
+	inout io3
+);
+	localparam verbose = 1;
+	localparam integer latency = 8;
+
+	reg [7:0] buffer;
+	integer bitcount = 0;
+	integer bytecount = 0;
+	integer dummycount = 0;
+
+	reg [7:0] spi_cmd;
+	reg [7:0] xip_cmd = 0;
+	reg [23:0] spi_addr;
+
+	reg [7:0] spi_in;
+	reg [7:0] spi_out;
+	reg spi_io_vld;
+
+	reg powered_up = 1;
+
+	localparam [3:0] mode_spi         = 1;
+	localparam [3:0] mode_dspi_rd     = 2;
+	localparam [3:0] mode_dspi_wr     = 3;
+	localparam [3:0] mode_qspi_rd     = 4;
+	localparam [3:0] mode_qspi_wr     = 5;
+	localparam [3:0] mode_qspi_ddr_rd = 6;
+	localparam [3:0] mode_qspi_ddr_wr = 7;
+
+	reg [3:0] mode = 0;
+	reg [3:0] next_mode = 0;
+
+	reg io0_oe = 0;
+	reg io1_oe = 0;
+	reg io2_oe = 0;
+	reg io3_oe = 0;
+
+	reg io0_dout = 0;
+	reg io1_dout = 0;
+	reg io2_dout = 0;
+	reg io3_dout = 0;
+
+	assign #1 io0 = io0_oe ? io0_dout : 1'bz;
+	assign #1 io1 = io1_oe ? io1_dout : 1'bz;
+	assign #1 io2 = io2_oe ? io2_dout : 1'bz;
+	assign #1 io3 = io3_oe ? io3_dout : 1'bz;
+
+	wire io0_delayed;
+	wire io1_delayed;
+	wire io2_delayed;
+	wire io3_delayed;
+
+	assign #1 io0_delayed = io0;
+	assign #1 io1_delayed = io1;
+	assign #1 io2_delayed = io2;
+	assign #1 io3_delayed = io3;
+
+	// 16 MB (128Mb) Flash
+	reg [7:0] memory [0:16*1024*1024-1];
+
+	reg [1023:0] firmware_file;
+	initial begin
+		if (!$value$plusargs("firmware=%s", firmware_file))
+			firmware_file = "firmware.hex";
+		$readmemh(firmware_file, memory);
+	end
+
+	task spi_action;
+		begin
+			spi_in = buffer;
+
+			if (bytecount == 1) begin
+				spi_cmd = buffer;
+
+				if (spi_cmd == 8'h ab)
+					powered_up = 1;
+
+				if (spi_cmd == 8'h b9)
+					powered_up = 0;
+
+				if (spi_cmd == 8'h ff)
+					xip_cmd = 0;
+			end
+
+			if (powered_up && spi_cmd == 'h 03) begin
+				if (bytecount == 2)
+					spi_addr[23:16] = buffer;
+
+				if (bytecount == 3)
+					spi_addr[15:8] = buffer;
+
+				if (bytecount == 4)
+					spi_addr[7:0] = buffer;
+
+				if (bytecount >= 4) begin
+					buffer = memory[spi_addr];
+					spi_addr = spi_addr + 1;
+				end
+			end
+
+			if (powered_up && spi_cmd == 'h 0b) begin
+				if (bytecount == 2)
+					spi_addr[23:16] = buffer;
+
+				if (bytecount == 3)
+					spi_addr[15:8] = buffer;
+
+				if (bytecount == 4)
+					spi_addr[7:0] = buffer;
+
+				if (bytecount >= 5) begin
+					buffer = memory[spi_addr];
+					spi_addr = spi_addr + 1;
+				end
+			end
+
+			if (powered_up && spi_cmd == 'h bb) begin
+				if (bytecount == 1)
+					mode = mode_dspi_rd;
+
+				if (bytecount == 2)
+					spi_addr[23:16] = buffer;
+
+				if (bytecount == 3)
+					spi_addr[15:8] = buffer;
+
+				if (bytecount == 4)
+					spi_addr[7:0] = buffer;
+
+				if (bytecount == 5) begin
+					xip_cmd = (buffer == 8'h a5) ? spi_cmd : 8'h 00;
+					mode = mode_dspi_wr;
+					dummycount = latency;
+				end
+
+				if (bytecount >= 5) begin
+					buffer = memory[spi_addr];
+					spi_addr = spi_addr + 1;
+				end
+			end
+
+			if (powered_up && spi_cmd == 'h eb) begin
+				if (bytecount == 1)
+					mode = mode_qspi_rd;
+
+				if (bytecount == 2)
+					spi_addr[23:16] = buffer;
+
+				if (bytecount == 3)
+					spi_addr[15:8] = buffer;
+
+				if (bytecount == 4)
+					spi_addr[7:0] = buffer;
+
+				if (bytecount == 5) begin
+					xip_cmd = (buffer == 8'h a5) ? spi_cmd : 8'h 00;
+					mode = mode_qspi_wr;
+					dummycount = latency;
+				end
+
+				if (bytecount >= 5) begin
+					buffer = memory[spi_addr];
+					spi_addr = spi_addr + 1;
+				end
+			end
+
+			if (powered_up && spi_cmd == 'h ed) begin
+				if (bytecount == 1)
+					next_mode = mode_qspi_ddr_rd;
+
+				if (bytecount == 2)
+					spi_addr[23:16] = buffer;
+
+				if (bytecount == 3)
+					spi_addr[15:8] = buffer;
+
+				if (bytecount == 4)
+					spi_addr[7:0] = buffer;
+
+				if (bytecount == 5) begin
+					xip_cmd = (buffer == 8'h a5) ? spi_cmd : 8'h 00;
+					mode = mode_qspi_ddr_wr;
+					dummycount = latency;
+				end
+
+				if (bytecount >= 5) begin
+					buffer = memory[spi_addr];
+					spi_addr = spi_addr + 1;
+				end
+			end
+
+			spi_out = buffer;
+			spi_io_vld = 1;
+
+			if (verbose) begin
+				if (bytecount == 1)
+					$write("<SPI-START>");
+				$write("<SPI:%02x:%02x>", spi_in, spi_out);
+			end
+
+		end
+	endtask
+
+	task ddr_rd_edge;
+		begin
+			buffer = {buffer, io3_delayed, io2_delayed, io1_delayed, io0_delayed};
+			bitcount = bitcount + 4;
+			if (bitcount == 8) begin
+				bitcount = 0;
+				bytecount = bytecount + 1;
+				spi_action;
+			end
+		end
+	endtask
+
+	task ddr_wr_edge;
+		begin
+			io0_oe = 1;
+			io1_oe = 1;
+			io2_oe = 1;
+			io3_oe = 1;
+
+			io0_dout = buffer[4];
+			io1_dout = buffer[5];
+			io2_dout = buffer[6];
+			io3_dout = buffer[7];
+
+			buffer = {buffer, 4'h 0};
+			bitcount = bitcount + 4;
+			if (bitcount == 8) begin
+				bitcount = 0;
+				bytecount = bytecount + 1;
+				spi_action;
+			end
+		end
+	endtask
+
+	always @(csb) begin
+		if (csb) begin
+			if (verbose) begin
+				$display("");
+				$fflush;
+			end
+			buffer = 0;
+			bitcount = 0;
+			bytecount = 0;
+			mode = mode_spi;
+			io0_oe = 0;
+			io1_oe = 0;
+			io2_oe = 0;
+			io3_oe = 0;
+		end else
+		if (xip_cmd) begin
+			buffer = xip_cmd;
+			bitcount = 0;
+			bytecount = 1;
+			spi_action;
+		end
+	end
+
+	always @(csb, clk) begin
+		spi_io_vld = 0;
+		if (!csb && !clk) begin
+			if (dummycount > 0) begin
+				io0_oe = 0;
+				io1_oe = 0;
+				io2_oe = 0;
+				io3_oe = 0;
+			end else
+			case (mode)
+				mode_spi: begin
+					io0_oe = 0;
+					io1_oe = 1;
+					io2_oe = 0;
+					io3_oe = 0;
+					io1_dout = buffer[7];
+				end
+				mode_dspi_rd: begin
+					io0_oe = 0;
+					io1_oe = 0;
+					io2_oe = 0;
+					io3_oe = 0;
+				end
+				mode_dspi_wr: begin
+					io0_oe = 1;
+					io1_oe = 1;
+					io2_oe = 0;
+					io3_oe = 0;
+					io0_dout = buffer[6];
+					io1_dout = buffer[7];
+				end
+				mode_qspi_rd: begin
+					io0_oe = 0;
+					io1_oe = 0;
+					io2_oe = 0;
+					io3_oe = 0;
+				end
+				mode_qspi_wr: begin
+					io0_oe = 1;
+					io1_oe = 1;
+					io2_oe = 1;
+					io3_oe = 1;
+					io0_dout = buffer[4];
+					io1_dout = buffer[5];
+					io2_dout = buffer[6];
+					io3_dout = buffer[7];
+				end
+				mode_qspi_ddr_rd: begin
+					ddr_rd_edge;
+				end
+				mode_qspi_ddr_wr: begin
+					ddr_wr_edge;
+				end
+			endcase
+			if (next_mode) begin
+				case (next_mode)
+					mode_qspi_ddr_rd: begin
+						io0_oe = 0;
+						io1_oe = 0;
+						io2_oe = 0;
+						io3_oe = 0;
+					end
+					mode_qspi_ddr_wr: begin
+						io0_oe = 1;
+						io1_oe = 1;
+						io2_oe = 1;
+						io3_oe = 1;
+						io0_dout = buffer[4];
+						io1_dout = buffer[5];
+						io2_dout = buffer[6];
+						io3_dout = buffer[7];
+					end
+				endcase
+				mode = next_mode;
+				next_mode = 0;
+			end
+		end
+	end
+
+	always @(posedge clk) begin
+		if (!csb) begin
+			if (dummycount > 0) begin
+				dummycount = dummycount - 1;
+			end else
+			case (mode)
+				mode_spi: begin
+					buffer = {buffer, io0};
+					bitcount = bitcount + 1;
+					if (bitcount == 8) begin
+						bitcount = 0;
+						bytecount = bytecount + 1;
+						spi_action;
+					end
+				end
+				mode_dspi_rd, mode_dspi_wr: begin
+					buffer = {buffer, io1, io0};
+					bitcount = bitcount + 2;
+					if (bitcount == 8) begin
+						bitcount = 0;
+						bytecount = bytecount + 1;
+						spi_action;
+					end
+				end
+				mode_qspi_rd, mode_qspi_wr: begin
+					buffer = {buffer, io3, io2, io1, io0};
+					bitcount = bitcount + 4;
+					if (bitcount == 8) begin
+						bitcount = 0;
+						bytecount = bytecount + 1;
+						spi_action;
+					end
+				end
+				mode_qspi_ddr_rd: begin
+					ddr_rd_edge;
+				end
+				mode_qspi_ddr_wr: begin
+					ddr_wr_edge;
+				end
+			endcase
+		end
+	end
+endmodule

+ 100 - 0
projects/usb_audio/sim/top_tb.v

@@ -0,0 +1,100 @@
+/*
+ * top_tb.v
+ *
+ * vim: ts=4 sw=4
+ *
+ * Copyright (C) 2019  Sylvain Munaut <tnt@246tNt.com>
+ * All rights reserved.
+ *
+ * BSD 3-clause, see LICENSE.bsd
+ *
+ * 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 <organization> 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 <COPYRIGHT HOLDER> 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.
+ */
+
+`default_nettype none
+
+module top_tb;
+
+	// Signals
+	// -------
+
+	wire spi_mosi;
+	wire spi_miso;
+	wire spi_flash_cs_n;
+	wire spi_clk;
+
+	wire usb_dp;
+	wire usb_dn;
+	wire usb_pu;
+
+	wire uart_rx;
+	wire uart_tx;
+
+
+	// Setup recording
+	// ---------------
+
+	initial begin
+		$dumpfile("top_tb.vcd");
+		$dumpvars(0,top_tb);
+		# 2000000 $finish;
+	end
+
+
+	// DUT
+	// ---
+
+	top dut_I (
+		.spi_mosi(spi_mosi),
+		.spi_miso(spi_miso),
+		.spi_flash_cs_n(spi_flash_cs_n),
+		.spi_clk(spi_clk),
+		.usb_dp(usb_dp),
+		.usb_dn(usb_dn),
+		.usb_pu(usb_pu),
+		.uart_rx(uart_rx),
+		.uart_tx(uart_tx),
+		.rgb(),
+		.clk_in(1'b0)
+	);
+
+
+	// Support
+	// -------
+
+	pullup(usb_dp);
+	pullup(usb_dn);
+
+	pullup(uart_tx);
+	pullup(uart_rx);
+
+	spiflash flash_I (
+		.csb(spi_flash_cs_n),
+		.clk(spi_clk),
+		.io0(spi_mosi),
+		.io1(spi_miso),
+		.io2(),
+		.io3()
+	);
+
+endmodule // top_tb