commit:42072f3f151c473c1e37e72ab0fe41faa79994ef
author:Chip
committer:Chip
date:Tue May 20 23:19:35 2025 -0500
parents:
Initial commit
diff --git a/Makefile b/Makefile
line changes: +17/-0
index 0000000..1796a38
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,17 @@
+targets = sqwak_box.bin
+
+all: $(targets)
+
+sqwak_box.elf: sqwak_box.c
+	avr-gcc -Os -mmcu=attiny84a sqwak_box.c -o sqwak_box.elf
+
+sqwak_box.bin: sqwak_box.elf
+	avr-objcopy -I elf32-avr -O binary sqwak_box.elf sqwak_box.bin
+
+.PHONY: readfuses
+readfuses:
+	minipro -p ATTINY84A@DIP16 -c config -r fuses
+
+.PHONY: writefuses
+writefuses:
+	minipro -p ATTINY84A@DIP16 -c config -w fuses

diff --git a/README.md b/README.md
line changes: +40/-0
index 0000000..106813e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,40 @@
+This is the software for the [Sqwak Box sound
+board](https://dominionofawesome.com/vca/sqwak-box/).
+
+# Building
+
+The firmware needs `avr-gcc` and
+[`avr-libc`](https://www.nongnu.org/avr-libc/user-manual/) to compile.
+Just type `make`, and you should have a `sqwak_box.bin` you can flash to
+an ATtiny84.
+
+# Fuses
+
+Included is a `minipro` format fuse definition in the `fuses` file. If
+you're using a different tool, set the fuses to:
+
+```
+low fuse byte: 0xE2 (this is the default except CKDIV8 is disabled)
+high fuse byte: 0xDF (default value)
+extended fuse byte: 0xFF (default value)
+```
+
+# Generating the sample ROM
+
+The samples are 31250Hz mono, and the script `convert.sh` uses `ffmpeg`
+to convert any sound file to a raw file in that format.
+
+```
+$ ./convert.sh sound.mp3 sound.raw
+```
+
+Once you have a set of RAW files, you can build that into a ROM image
+with `rombuild.pl`.
+
+```
+$ ./rombuild.pl -o sound.bin sound1.raw sound2.raw ...
+```
+
+Then flash `sound.bin` to your flash chip.
+
+The firmware will randomly select a sample on playback.

diff --git a/convert.sh b/convert.sh
line changes: +8/-0
index 0000000..985ba34
--- /dev/null
+++ b/convert.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+if [ -z "$1" -o -z "$2" ]; then
+	echo "$0 <media file> <output file>"
+	return 1
+fi
+
+ffmpeg -i $1 -f u8 -c pcm_u8 -ar 31250 $2

diff --git a/fuses b/fuses
line changes: +12/-0
index 0000000..c94750b
--- /dev/null
+++ b/fuses
@@ -0,0 +1,12 @@
+lfuse = 0xe2
+hfuse = 0xdf
+efuse = 0xff
+user_id0 = 0xe2
+user_id1 = 0xdf
+user_id2 = 0xff
+user_id3 = 0x37
+user_id4 = 0x33
+user_id5 = 0x30
+user_id6 = 0x31
+user_id7 = 0x36
+lock = 0xff

diff --git a/rombuild.pl b/rombuild.pl
line changes: +63/-0
index 0000000..bb02552
--- /dev/null
+++ b/rombuild.pl
@@ -0,0 +1,63 @@
+#!/usr/bin/perl
+use Getopt::Long qw/:config auto_help/;
+use strict;
+use v5.10;
+
+# The ROM starts with a single byte defining how many samples are present
+# 
+# Following this, there is one sample definition slot for each sample. Each
+# sample definition looks like this:
+#
+# struct {
+#     uint8_t start[3];
+#     uint8_t len[3];
+# }
+#
+# If start is zero, the sample slot is empty (though that should not happen
+# normally)
+
+my ($output, $help);
+GetOptions(
+    "output|o=s", \$output,
+    "help|h",     \$help,
+) or die "Invalid options";
+
+if ($help) {
+    print <<'HELP';
+rombuild -o rom.bin snd1.raw [snd2.raw snd3.raw ...]
+
+rombuild assembles a series of raw samples into a binary that can be
+flashed to a chip. You can specify up to 16 samples.
+HELP
+    exit 1;
+}
+
+my $n_samples = @ARGV;
+
+die "You must specify an output file" unless defined $output;
+die "Too many samples" if $n_samples > 255;
+die "You must specify at least one sample" if $n_samples == 0;
+
+my $samples_start = 6 * $n_samples + 1;
+
+open OUTPUT, '>', $output;
+print OUTPUT pack("C", $n_samples);
+my $c = $samples_start;
+foreach my $bin (@ARGV) {
+    my $s = -s $bin;
+    print OUTPUT pack("CCC", ($c >> 16) & 0xFF, ($c >> 8) & 0xFF, $c & 0xFF);
+    print OUTPUT pack("CCC", ($s >> 16) & 0xFF, ($s >> 8) & 0xFF, $s & 0xFF);
+    say "$bin size ", sprintf("%06x", $s), " @ ", sprintf("%06x", $c);
+    $c += $s;
+}
+
+# SLUUURRRRRP
+$/ = undef;
+
+foreach my $bin (@ARGV) {
+    open F, $bin;
+    print OUTPUT <F>;
+    close F;
+}
+
+close OUTPUT;

diff --git a/sqwak_box.c b/sqwak_box.c
line changes: +247/-0
index 0000000..e035d45
--- /dev/null
+++ b/sqwak_box.c
@@ -0,0 +1,247 @@
+#include <avr/io.h>
+#include <avr/interrupt.h>
+#include <avr/sleep.h>
+#define F_CPU 8000000UL
+#include <util/delay.h>
+
+#define USI_PULSE_0 (1<<USIWM0)|(1<<USITC)
+#define USI_PULSE_1 (1<<USIWM0)|(1<<USICLK)|(1<<USITC)
+#define SAMPLE_DEF_SIZE 6
+
+register struct {
+	uint8_t tick: 1;
+	uint8_t play: 1;
+} flags asm("r15");
+register uint8_t sample_buffer asm("r14");
+
+uint8_t n_samples;
+uint8_t button_state;
+uint8_t channels;
+uint16_t button_debounce;
+uint32_t sample_count;
+
+void usi_init() {
+	// Set CS high initially
+	PORTA = 0b10000000;
+	// Set PA7 (CS) PA5 (DO) and PA4 (USCK) to outputs
+	DDRA = 0b10110000;
+}
+
+void usi_deinit() {
+	PORTA = 0;
+	DDRA = 0;
+}
+
+#define spi_cs_enable() (PORTA &= ~0b10000000)
+#define spi_cs_disable() (PORTA |= 0b10000000)
+
+uint8_t spi_transfer(uint8_t d) {
+	cli();
+	USIDR = d;
+	USICR = USI_PULSE_0;
+	USICR = USI_PULSE_1;
+	USICR = USI_PULSE_0;
+	USICR = USI_PULSE_1;
+	USICR = USI_PULSE_0;
+	USICR = USI_PULSE_1;
+	USICR = USI_PULSE_0;
+	USICR = USI_PULSE_1;
+	USICR = USI_PULSE_0;
+	USICR = USI_PULSE_1;
+	USICR = USI_PULSE_0;
+	USICR = USI_PULSE_1;
+	USICR = USI_PULSE_0;
+	USICR = USI_PULSE_1;
+	USICR = USI_PULSE_0;
+	USICR = USI_PULSE_1;
+	sei();
+	return USIDR;
+}
+
+void flash_id() {
+	spi_cs_enable();
+	spi_transfer(0x9F);
+	spi_transfer(0x00);
+	spi_transfer(0x00);
+	spi_transfer(0x00);
+	spi_cs_disable();
+}
+
+void begin_playback(uint8_t sample) {
+	sample_buffer = 0x7F;
+	uint8_t addr[3];
+	uint16_t dir_addr = sample * SAMPLE_DEF_SIZE + 1;
+	uint8_t* sc = (uint8_t*) &sample_count;
+	sc[3] = 0;
+
+	usi_init();
+
+	// Fetch directory information
+	spi_cs_enable();
+	spi_transfer(0x03);
+	spi_transfer(0);
+	spi_transfer(dir_addr >> 8);
+	spi_transfer(dir_addr & 0xFF);
+	addr[0] = spi_transfer(0);
+	addr[1] = spi_transfer(0);
+	addr[2] = spi_transfer(0);
+	sc[2] = spi_transfer(0);
+	sc[1] = spi_transfer(0);
+	sc[0] = spi_transfer(0);
+	spi_cs_disable();
+
+	// Begin SPI transfer
+	spi_cs_enable();
+	spi_transfer(0x03);
+	spi_transfer(addr[0]);
+	spi_transfer(addr[1]);
+	spi_transfer(addr[2]);
+
+	if (channels == 0) {
+		// Wait for the amp to power up
+		_delay_ms(200);
+	}
+	channels = 1;
+
+	// Clear OC0A on compare match, Fast PWM
+	TCCR0A = 0b10000011;
+	// Reset counter
+	TCNT0 = 0;
+	// Enable overflow interrupt
+	TIMSK0 = _BV(TOIE0);
+	// Match at sample
+	OCR0A = sample_buffer;
+	// Enable timer - Fast PWM, no prescaler
+	TCCR0B = 0b00000001;
+
+	// Switch to idle sleep because we need the timer/counter to function
+	set_sleep_mode(SLEEP_MODE_IDLE);
+}
+
+void end_playback() {
+	// Disable overflow interrupt
+	TIMSK0 = 0;
+	// disable timer
+	TCCR0B = 0;
+	// End SPI transfer
+	spi_cs_disable();
+	// Return USI pins to Hi-Z
+	usi_deinit();
+
+	// Return to power-off sleep
+	set_sleep_mode(SLEEP_MODE_PWR_DOWN);
+
+	channels = 0;
+	button_debounce = 0;
+}
+
+uint8_t adc_read() {
+	// VCC reference, select PA0
+	ADMUX = 0;
+	// Enable, Start conversion, Prescaler divisor 16
+	ADCSRA = _BV(ADEN) | _BV(ADSC) | 0b100;
+
+	// Wait for conversion to finish
+	while (!(ADCSRA & _BV(ADIF)));
+	// Clear interrupt flag
+	ADCSRA = _BV(ADIF);
+	// Both registers must be read
+	uint8_t tmp = ADCL;
+	ADCH;
+	// Shut off the ADC
+	ADCSRA = 0;
+	return tmp;
+}
+
+uint16_t lfsr_state;
+
+void lfsr_init() {
+	lfsr_state = adc_read();
+}
+
+uint8_t lfsr_read() {
+	lfsr_state ^= lfsr_state >> 7;
+	lfsr_state ^= lfsr_state << 9;
+	lfsr_state ^= lfsr_state >> 13;
+	return lfsr_state & 0xFF;
+}
+
+void main() {
+	// Set PB2 to output
+	DDRB = 0b00000100;
+	// Set pull-up on PB0 (button)
+	PORTB = 0b00000001;
+	// Enable interrupts for pin changes on PORTB
+	GIMSK = _BV(PCIE1);
+	// Enable pin change interrupt on PB0
+	PCMSK1 = _BV(PCINT8);
+
+	button_state = 0b00000001;
+	button_debounce = 0;
+	channels = 0;
+	flags.tick = flags.play = 0;
+
+	lfsr_init();
+
+	// Grab the number of samples from the first byte of flash
+	usi_init();
+	spi_cs_enable();
+	spi_transfer(0x03);
+	spi_transfer(0x00);
+	spi_transfer(0x00);
+	spi_transfer(0x00);
+	n_samples = spi_transfer(0);
+	spi_cs_disable();
+	usi_deinit();
+
+	// Enable interrupts
+	sei();
+
+	set_sleep_mode(SLEEP_MODE_PWR_DOWN);
+	while (1) {
+		sleep_mode();
+
+		if (button_debounce > 0) {
+			button_debounce--;
+		}
+
+		if (flags.tick) {
+			// fetch the next sample
+			sample_buffer = spi_transfer(0x00);
+
+			if (--sample_count == 0) {
+				end_playback();
+			}
+			flags.tick = 0;
+		}
+
+		if (flags.play) {
+			spi_cs_disable();
+
+			uint8_t n = lfsr_read() % n_samples;
+			begin_playback(n);
+
+			flags.play = 0;
+		}
+	}
+}
+
+ISR(TIM0_OVF_vect) {
+	OCR0A = sample_buffer;
+	flags.tick = 1;
+}
+
+ISR(PCINT1_vect) {
+	uint8_t current_state = PINB & 0b00000001;
+	if (current_state == button_state) {
+		return;
+	}
+
+	uint8_t pressed = button_state & ~current_state;
+	if (button_debounce == 0 && (pressed & _BV(PB0))) {
+		flags.play = 1;
+		button_debounce = 4000;
+	}
+
+	button_state = current_state;
+}