+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
+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.
+#!/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
+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
+#!/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;
+#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;
+}