From 20eab52f73b4d5b448956706860b52f2da932c7e Mon Sep 17 00:00:00 2001 From: Pat Thoyts Date: Sun, 21 Feb 2016 22:46:48 +0000 Subject: [PATCH 1/1] Simple sound synthesis using ATtiny85 timers. --- .gitignore | 3 + Makefile | 77 ++++++++++++++++++++ filter.sch | 98 +++++++++++++++++++++++++ tiny_sound.c | 199 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 377 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 filter.sch create mode 100644 tiny_sound.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c41c150 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.hex +*.elf +*.o diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c04613f --- /dev/null +++ b/Makefile @@ -0,0 +1,77 @@ +# -*- Makefile -*- + +PROJECT := tiny_sound +DEVICE := attiny85 +F_CPU := 8000000UL +INC := -I. +#AVRDUDE := avrdude -c usbasp -p $(DEVICE) +AVRDUDE := avrdude -c atmelice_isp -p $(DEVICE) + +CSRCS = $(PROJECT).c +OBJS = $(CSRCS:.c=.o) + +uname_S := $(shell sh -c 'uname -s 2>/dev/null || echo not') + +CC := avr-gcc +LD := avr-gcc +OBJCOPY := avr-objcopy +ifeq ($(uname_S),Linux) +RM := rm -f +else +RM := del >NUL +endif +CFLAGS :=-Wall -Wcast-align -Wshadow \ + -std=gnu99 -fshort-enums -pedantic-errors -Os -mcall-prologues \ + -mmcu=$(DEVICE) -DF_CPU=$(F_CPU) +LDFLAGS := -Wall -mmcu=$(DEVICE)# -Wl,-v +LIBS := + +V := @ +Q := $(V:1=) +QUIET_CC = $(Q:@=@echo CC $@ &)$(CC) +QUIET_LD = $(Q:@=@echo LD $@ &)$(LD) +QUIET_OBJCOPY = $(Q:@=@echo OBJCOPY $@ &)$(OBJCOPY) +QUIET_AVRDUDE = $(Q:@=@echo AVRDUDE $@ &)$(AVRDUDE) + +all: $(PROJECT).hex + +%.hex: %.elf + $(QUIET_OBJCOPY) -j .text -j .data -O ihex $< $@ + +%.elf: $(OBJS) + $(QUIET_LD) $(LDFLAGS) $^ $(LIBS) -o $@ + +%.o: %.c + $(QUIET_CC) $(CFLAGS) $(INC) -c $< + +flash: $(PROJECT).hex + $(QUIET_AVRDUDE) -U flash:w:$<:i + +clean: + -@$(RM) $(addprefix $(PROJECT), .elf .hex .net .bom .cmd) + +.PHONY: clean +.SECONDARY: $(addsuffix .elf, $(PROJECT)) $(OBJS) + +# Fuse high byte: +# 0xdd = 1 1 0 1 1 1 0 1 +# ^ ^ ^ ^ ^ \-+-/ +# | | | | | +------ BODLEVEL 2..0 (brownout trigger level -> 2.7V) +# | | | | +---------- EESAVE (preserve EEPROM on Chip Erase -> not preserved) +# | | | +-------------- WDTON (watchdog timer always on -> disable) +# | | +---------------- SPIEN (enable serial programming -> enabled) +# | +------------------ DWEN (debug wire enable) +# +-------------------- RSTDISBL (disable external reset -> enabled) +# +# Fuse low byte: +# 0xe1 = 1 1 1 0 0 0 0 1 +# ^ ^ \+/ \--+--/ +# | | | +------- CKSEL 3..0 (clock selection -> HF PLL) +# | | +--------------- SUT 1..0 (BOD enabled, fast rising power) +# | +------------------ CKOUT (clock output on CKOUT pin -> disabled) +# +-------------------- CKDIV8 (divide clock by 8 -> don't divide) +# For 16.5MHz internal: -U hfuse:w:0xdd:m -U lfuse:w:0xe1:m +# For 12MHz crystal: -U hfuse:w:0xdd:m -U lfuse:w:0b11111111:m +# For 8MHz internal: -U hfuse:w:0xd7:m -U lfuse:w:0xe2:m +fuse: + $(AVRDUDE) -e -U hfuse:w:0xd7:m -U lfuse:w:0xe2:m diff --git a/filter.sch b/filter.sch new file mode 100644 index 0000000..eef1ccf --- /dev/null +++ b/filter.sch @@ -0,0 +1,98 @@ +v 20140308 2 +C 55800 41700 0 0 0 title-A4-2.sym +{ +T 63200 43000 15 30 1 1 0 4 1 +Title=Low pass audio filter +T 62300 42000 15 16 1 1 0 4 1 +filename=filter.sch +T 66750 43200 15 16 1 1 0 4 1 +revision=1 +T 66950 41800 15 16 1 1 0 6 1 +page=1 +T 67100 41800 15 16 1 1 0 0 1 +number_of_pages=1 +T 66750 42550 15 12 1 1 0 4 1 +date=21/02/2016 +T 65350 42150 15 16 1 1 0 4 1 +author=Pat Thoyts +T 56000 50700 15 8 0 0 0 0 1 +symversion=1.0 +} +C 59400 46800 1 0 0 res_horiz.sym +{ +T 59700 49800 5 8 0 0 0 0 1 +device=resistor +T 59700 47000 5 10 1 1 0 0 1 +refdes=R1 +T 59700 46800 5 10 1 1 0 0 1 +value=1k +T 59700 46600 5 8 0 1 0 0 1 +footprint=0805 +T 59700 48800 5 8 0 0 0 0 1 +symversion=1.0 +} +C 60500 46100 1 0 0 cap_vert.sym +{ +T 60900 46500 5 10 1 1 0 0 1 +value=100nF +T 60900 46300 5 8 0 1 0 0 1 +footprint=0805 +T 60800 48200 5 8 0 0 0 0 1 +symversion=1.0 +T 60900 46900 5 10 1 1 0 0 1 +refdes=C1 +} +C 63400 46600 1 90 0 elko.sym +{ +T 61200 46900 5 8 0 0 90 0 1 +device=POLARIZED_CAPACITOR +T 63300 47100 5 8 0 1 90 0 1 +footprint=elko_RM5_D10 +T 62900 47100 5 10 1 1 0 0 1 +refdes=C2 +T 62900 46400 5 10 1 1 0 0 1 +value=10uF +T 61800 46900 5 8 0 0 90 0 1 +symversion=2 +} +C 61600 46300 1 0 0 potentiometer_vert.sym +{ +T 61700 49300 5 8 0 0 0 0 1 +device=potentiometer +T 61900 46900 5 8 0 1 0 0 1 +footprint=panel_mount_BI898 +T 61900 46900 5 10 1 1 0 0 1 +refdes=R3 +T 61700 48300 5 8 0 0 0 0 1 +symversion=1.0 +T 61900 46500 5 10 1 1 0 0 1 +value=10k +} +C 57900 47200 1 0 0 input-2.sym +{ +T 57900 47400 5 10 0 0 0 0 1 +net=AUDIO:1 +T 58500 47900 5 10 0 0 0 0 1 +device=none +T 59000 47500 5 10 1 1 0 7 1 +value=AUDIO +} +N 59300 47300 59600 47300 4 +N 60200 47300 61700 47300 4 +N 60800 47300 60800 47100 4 +N 61700 47300 61700 47100 4 +C 61200 45600 1 0 0 gnd-1.sym +N 61700 46500 61700 45900 4 +N 60800 45900 61700 45900 4 +N 60800 46500 60800 45900 4 +N 62800 46800 62000 46800 4 +C 63700 46700 1 0 0 output-2.sym +{ +T 64600 46900 5 10 0 0 0 0 1 +net=OUTPUT:1 +T 63900 47400 5 10 0 0 0 0 1 +device=none +T 63900 47000 5 10 1 1 0 1 1 +value=OUTPUT +} +N 63700 46800 63400 46800 4 diff --git a/tiny_sound.c b/tiny_sound.c new file mode 100644 index 0000000..243641d --- /dev/null +++ b/tiny_sound.c @@ -0,0 +1,199 @@ +/* Copyright (C) 2016 Pat Thoyts + * + * Demonstration of simple sound output using PWM on an ATtiny85. + * + * Based on the example at: + * http://www.technoblogy.com/show?QVN + * Modified to play a tune. + * + * The sound output is on PB4 and should be passed through a low + * pass filter. + * + */ + +#include +#include +#include +#include +#include +#include +#include + +/* + * counter values to generate specific notes + * tone_count = (freq * 65536) / 20000 + */ +unsigned int Tones[] = { + /* freq */ + /* A3 220.00 */ 721, + /* B3 246.94 */ 809, + /* C4 261.63 */ 857, + /* D4 293.00 */ 960, + /* E4 329.63 */ 1080, + /* F4 349.23 */ 1144, + /* G4 392.00 */ 1285, + /* A4 440.00 */ 1442, +}; + +volatile unsigned int acc; +static unsigned int note = 857; /* middle C */ +static int tune_index = 0; +static char tune[] = "CCGGAAG FFEEDDC GGFFEED GGFFEED CCGGAAG FFEEDDC "; + +#define SQUARE_WAVE +//#define SAWTOOTH_WAVE +ISR(TIMER0_COMPA_vect) +{ + acc += note; + +#ifdef SQUARE_WAVE + OCR1B = (acc >> 8) & 0x80; +#elif defined(SAWTOOTH_WAVE) + OCR1B = acc >> 8; +#else + signed char temp, mask; + temp = acc >> 8; + mask = temp >> 7; + OCR1B = temp ^ mask; +#endif +} + +ISR(TIMER0_OVF_vect) +{ + PORTB ^= _BV(PB1); +} + +ISR(TIMER1_OVF_vect) +{ + PORTB ^= _BV(PB0); +} + +/* Section 12.3.9: enable the 64Mhz PLL as asynchronous clock source + * for timer 1 allowing the PLL to settle. + */ +static void enable_pll() +{ + PLLCSR = _BV(PLLE); + _delay_us(100); + while (!(PLLCSR & _BV(PLOCK))) + ; + PLLCSR |= _BV(PCKE); +} + +/* use timer 0 and PWM on OC1B for sound generation */ +static void init_sound() +{ + cli(); + + DDRB |= _BV(DDB4); + PORTB &= ~_BV(PB4); + + enable_pll(); + + /* + * Configure timer 0 to call the COMPA interrupts at 20 kHz + * In the compare match interrupt handler, OCR1B gets set to + * a value defined by the current note + + * WGM = 0b111: Fast PWM (0->OCRA) + * CS = 0b010: CLK / 8 + * OCR0A = 49 causes a further /50 so 8e6/8/50 == 20 kHz + */ + TCCR0A = _BV(WGM01) | _BV(WGM00); + TCCR0B = _BV(WGM02) | _BV(CS01); + OCR0A = 49; + + /* + * CTC1 = 0: continuous counter (does not restart on compare match) + * COM1A = 0b00: OC1A disconnected + * COM1B = 0b01: OC1B clear on compare match, set at BOTTOM, OC1B# enabled + * PWM1A = 0: disabled modulator A (attached to PB0 and PB1) + * PWM1B = 1: enable PWM mode for OCR1B and 0 when match OCR1C + * CS = 0b0001: PCK/1 + * + * timer 1 has PWM enabled for B and toggles the output line on + * a match with OCR1B which is set by the note. + * OCR1B sets the duty cycle (when to switch the line low) and + * OCR1C (defaults to 0xFF) sets the TOP value and controls the frequency + * so in this case, 64e6/1/256 == 250kHz + * + * The tone is created in the OCR1B interrupt handler by setting + * the 16bit accumulator and using the top bit to control the PWM + */ + TCCR1 = _BV(CS10); + GTCCR = _BV(PWM1B) | /*_BV(COM1B1) |*/ _BV(COM1B0); + + /* Enable the compare match interrupt for A and add a overflow + * interrupt for testing the clock frequency with the oscilloscope + * (could use this as a 20kHz counter instead of _delay_ms later) + * + * For some reason, setting TOIE1 causes continual resets. + */ + TIMSK = _BV(OCIE0A) | _BV(TOIE0); /* | _BV(TOIE1); */ + + sei(); +} + +/* flash the hearbeat led (PB2) on startup */ +static void indicate_startup() +{ + DDRB |= _BV(PB2); + PORTB &= ~(PB2); + + for (int n = 0; n < 20; ++n) + { + PORTB ^= _BV(PB2); + _delay_ms(50); + } +} + +/* emit silence for ms milliseconds by disabling the output line */ +#define PAUSE(x) \ + DDRB &= ~_BV(DDB4); \ + _delay_ms((x)); \ + DDRB |= _BV(DDB4) + +int +main(void) +{ + const int tempo = 400; + + wdt_enable(WDTO_1S); + + power_adc_disable(); + power_usi_disable(); + + indicate_startup(); + init_sound(); + + /* some test outputs for oscilloscope probing */ + DDRB |= _BV(DDB1) | _BV(DDB0); + PORTB &= ~(_BV(PB1) | _BV(PB0)); + + for(;;) + { + wdt_reset(); + + int x = (int)tune[tune_index++]; + if (x == 0) + { + tune_index = 0; + continue; + } + if (x == 32) /* space means silence for 1 note */ + { + PAUSE(tempo); + continue; + } + + x = x - (int)'A'; + if (x < 2) + x = x + 7; + note = Tones[x]; + + PAUSE(50); /* provide a break after each note */ + + _delay_ms(tempo); /* play each note for tempo milliseconds */ + PORTB ^= _BV(PB2); + } +} -- 2.23.0