bicycle-bell/firmware/bell.c

498 lines
13 KiB
C

/*
* Bicycle Bell/Horn firmware
* Copyright (C) 2015 Stuart Longland
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#define F_CPU 16000000UL
#include <avr/io.h>
#include <string.h>
#include <util/delay.h>
#include <stdint.h>
#include <avr/pgmspace.h>
#include <avr/interrupt.h>
#include "bellsnd.h"
#include "hornsnd.h"
/*
* Connections:
* - Port B Pin 4: External Source (active low)
* - Port B Pin 5: Green LED
* - Port B Pin 6: Blue LED
* - Port B Pin 7: Sound output (PWM)
* - Port C Pin 7: Red LED
* - Port D Pin 6: Power On (active low)
* - Port D Pin 7: Mode
* - Port E Pin 6: Bell (active low)
*
* Test points:
* - Port C Pin 6: PWM Next Sample
* - Port D Pin 4: Main loop
* - Port D Pin 3: PWM Buffer Wait
* - Port D Pin 2: PWM Buffer Switch
* - Port D Pin 1: Bell down
* - Port D Pin 0: Bell release
* - Port F Pin 7: Bell state (main loop)
*/
#define MODE_SW (PIND & (1 << DDB7))
#define BELL_SW (!(PINE & (1 << DDB6)))
#define EXT_SW (!(PINB & (1 << DDB4)))
#define PWR_ON_PORT PORTD
#define PWR_ON_PIN (1 << DDB6)
#define RED_LED_PORT PORTC
#define GREEN_LED_PORT PORTB
#define BLUE_LED_PORT PORTB
#define RED_LED_PIN (1 << DDB7)
#define GREEN_LED_PIN (1 << DDB5)
#define BLUE_LED_PIN (1 << DDB6)
/*
* Timer configuration. Freetronics picked GPIO B7 as their output for the
* sound, which means we're stuck with either using timers 0 (8-bit) or 1
* (16-bit). I could pick a different pin, but sod it, I've already wired up
* the board so there's no going back now.
*
* Timers 0 and 1 are fed off the system clock, so we're stuck with a maximum
* of 16MHz as the input frequency. (If we had Timer4 available to us, then
* we've got 64MHz.) Timer 1 *can* do 16-bits, but at that resolution, it'll
* give us a 244Hz carrier, which is utterly useless for audio. So we'll
* suffer with 8-bits, which gives us a 62.5kHz carrier.
*
* Timer 0 is probably our easiest choice for this. Timer 1 can also do 8-bit
* but why complicate things with 16-bit registers? Our output will be OC0A.
* We will feed the samples into OCR0A.
*/
#define T0_COMA (2) /* Output A: Clear on match */
#define T0_COMB (0) /* Output B: Not used */
#define T0_WGM (3) /* Fast PWM mode */
#define T0_CS (1) /* 16MHz clock (no prescale) */
#define T0_FCA (0) /* Do not force A */
#define T0_FCB (0) /* Do not force B */
#define TCCR0A_VAL \
( (T0_COMA << 6) \
| (T0_COMB << 4) \
| (T0_WGM & 0x3) )
#define TCCR0B_VAL \
( (T0_FCA << 7) \
| (T0_FCB << 6) \
| ((T0_WGM & 0x04) << 1) \
| T0_CS)
/*
* Timer 3 is well suited to give us a stable sample rate clock at sample
* rates up to 32kHz. Multiples of 8kHz should be smack on (according to the
* crystal), and multiples of 11025Hz should be less than 1% off. 48kHz will
* be off, but you'd be insane to expect DVD-quality audio out of the PWM
* output of an 8-bit micro.
*
* We set OCR3A according to the equation:
* OCR3A = (F_CPU / f_sample) - 1
*
* The interrupt vector name is TIMER3_COMPA.
*/
#define T3_COMA (0) /* Output A: Not used */
#define T3_COMB (0) /* Output B: Not used */
#define T3_COMC (0) /* Output C: Not used */
#define T3_WGM (4) /* Mode: CTC */
#define T3_CS (1) /* 16MHz clock (no prescale) */
#define T3_FCA (0) /* Do not force A */
#define T3_FCB (0) /* Do not force B */
#define T3_FCC (0) /* Do not force C */
#define T3_ICNC (0) /* Don't care */
#define T3_ICES (0) /* Don't care */
#define TCCR3A_VAL \
( (T3_COMA << 6) \
| (T3_COMB << 4) \
| (T3_COMC << 2) \
| (T3_WGM & 0x3) )
#define TCCR3B_VAL \
( (T3_ICNC << 7) \
| (T3_ICES << 6) \
| ((T3_WGM & 0xc) << 1) \
| T3_CS)
#define OCR3A_VAL(freq) \
((F_CPU / freq) - 1)
#define TIMSK3_VAL (1 << 1)
/*
* The following is our output sample buffers, two buffers that get rotated
* around (double-buffering) to ensure we're not writing to the one we're
* reading from, buffer selector and the buffer pointer.
*/
#define BUFFER_SZ 256
#define BUFFER_NUM 2
static volatile uint8_t pwm_buffer[BUFFER_NUM][BUFFER_SZ];
static volatile uint8_t buffer_ready = 0;
static volatile uint8_t buffer_sel = 0;
static volatile uint8_t buffer_ptr = 0;
static volatile uint8_t buffer_wait = 0;
static volatile uint8_t pwm_on = 0;
/*
* That leaves us timer 1 for a tick counter, which we can tune as needed.
* Best rate would be about 10Hz to give us 100msec time slices that we can
* give us a nice tick counter. The primary aim of this will be to delay the
* system power-down after the bell or external source signals are
* de-asserted.
*
* We set OCR1A according to the equation:
* OCR1A = (F_CPU / (1024*f_sample)) - 1
*
* This can be hard-coded since it will not be changing at runtime. The
* interrupt vector name is TIMER1_COMPA.
*/
#define T1_COMA (0) /* Output A: Not used */
#define T1_COMB (0) /* Output B: Not used */
#define T1_COMC (0) /* Output C: Not used */
#define T1_WGM (4) /* Mode: CTC */
#define T1_CS (5) /* 15.625kHz clock (16MHz / 1024) */
#define T1_FCA (0) /* Do not force A */
#define T1_FCB (0) /* Do not force B */
#define T1_FCC (0) /* Do not force C */
#define T1_ICNC (0) /* Don't care */
#define T1_ICES (0) /* Don't care */
#define T1_FREQ (10) /* 10Hz */
#define TCCR1A_VAL \
( (T1_COMA << 6) \
| (T1_COMB << 4) \
| (T1_COMC << 2) \
| (T1_WGM & 0x3) )
#define TCCR1B_VAL \
( (T1_ICNC << 7) \
| (T1_ICES << 6) \
| ((T1_WGM & 0xc) << 1) \
| T1_CS)
#define OCR1A_VAL \
((F_CPU / (1024*T1_FREQ)) - 1)
#define TIMSK1_VAL (1 << 1)
/* This is our global tick counter variable */
static volatile uint8_t system_tick = 0;
/* LED state information */
static volatile uint8_t led_colour = 0;
/* Audio set-up routine, buffer 0 better be ready! */
void start_audio(uint16_t sample_rate) {
/* Stop interrupts momentarily */
if (pwm_on) {
/* We already have PWM running??? */
RED_LED_PORT |= RED_LED_PIN;
GREEN_LED_PORT &= ~GREEN_LED_PIN;
BLUE_LED_PORT &= ~BLUE_LED_PIN;
while(1);
}
cli();
/* Set up buffer pointers */
buffer_sel = 1;
buffer_ptr = BUFFER_SZ-1;
/* Set up timer 0 */
OCR0A = UINT8_MAX/2;
TCCR0A = TCCR0A_VAL;
TCCR0B = TCCR0B_VAL;
/* Set up timer 3 */
OCR3A = OCR3A_VAL(sample_rate);
TCCR3A = TCCR3A_VAL;
TCCR3B = TCCR3B_VAL;
TIMSK3 = TIMSK3_VAL;
/* Resume interrupts */
sei();
/* Wait for audio interrupt to tick */
//led_colour = 0xc;
while(!pwm_on);
}
/* Audio tear-down routine */
void stop_audio() {
/* Stop interrupts momentarily */
cli();
//led_colour = 0xc;
/* Re-set buffer pointers and PWM state */
buffer_sel = 0;
buffer_ptr = 0;
buffer_ready = 0;
buffer_wait = 0;
pwm_on = 0;
/* Silence output */
OCR0A = UINT8_MAX/2;
/* Stop timer 3 */
TIMSK3 = 0;
TCCR3A = 0;
TCCR3B = 0;
OCR3A = 0;
/* Stop timer 0 */
TCCR0A = 0;
TCCR0B = 0;
/* Clear buffers */
memset(pwm_buffer, 0, sizeof(pwm_buffer));
/* Resume interrupts */
sei();
}
/* Write audio to the output buffer */
uint16_t write_audio(const uint8_t* audio, uint16_t offset,
uint16_t len, uint8_t is_ram, uint8_t loop) {
/* Wait until the interrupt handler switches buffers */
while(buffer_ready);
/* Pick the buffer not being read */
uint8_t buf_num = buffer_sel ? 0 : 1;
uint16_t buf_rem = BUFFER_SZ;
uint16_t buf_ptr = 0;
uint16_t in_rem = len - offset;
volatile uint8_t* out = pwm_buffer[buf_num];
const uint8_t* in = &audio[offset];
while(buf_rem && in_rem) {
if (is_ram)
*out = *in;
else
*out = pgm_read_byte(in);
out++;
buf_rem--;
in++;
in_rem--;
if (loop && (!in_rem)) {
in_rem = len;
in = audio;
}
}
/* Mark the buffer as ready */
buffer_ready = 1;
/* Return where we got to */
return len - in_rem;
}
/* The loop point in the bell effect */
#define BELL_LOOP_SZ (2054)
/* Bell states */
#define BELL_IDLE 0
#define BELL_DOWN 1
#define BELL_RELEASE 2
#define BELL_STOP 3
static volatile uint8_t bell_state = BELL_IDLE;
static volatile uint8_t bell_released = 0;
static volatile uint16_t bell_ptr = 0;
static uint8_t bell_mode = 0;
static const uint8_t* bell_snd = NULL;
static uint16_t bell_loop_sz = 0;
static uint16_t bell_sz = 0;
static uint8_t bell_loop = 0;
/* What do we do when the bell is idle? */
void bell_idle(void) {
if (BELL_SW) {
/*
* Someone has pressed the bell button, enter the "down"
* state, load the initial buffer then start the audio.
*/
bell_state = BELL_DOWN;
bell_mode = MODE_SW;
if (bell_mode) {
bell_snd = horn;
bell_loop_sz = HORN_LOOP_SZ;
bell_sz = HORN_SZ - HORN_LOOP_OFFSET;
} else {
bell_snd = bell;
bell_loop_sz = BELL_LOOP_SZ;
bell_sz = BELL_SZ;
}
bell_ptr = write_audio(
bell_snd, 0, bell_loop_sz, 0, 1);
start_audio(BELL_RATE);
}
}
/* What do we do while the button is held? */
void bell_down(void) {
if (BELL_SW) {
/* The button is still down, is there room? */
if (!buffer_ready) {
/* There is, put some more dinging noises in */
if (bell_mode && !bell_loop) {
/*
* We've played the initial part, now for
* the rest.
*/
bell_snd = &horn[HORN_LOOP_OFFSET];
bell_loop_sz = HORN_LOOP_SZ - HORN_LOOP_OFFSET;
bell_ptr -= HORN_LOOP_OFFSET;
bell_sz -= HORN_LOOP_OFFSET;
bell_loop = 1;
}
bell_ptr = write_audio(
bell_snd, bell_ptr, bell_loop_sz,
0, 1);
}
} else {
/* Button just released? Or switch bounce */
bell_state = BELL_RELEASE;
bell_released = system_tick;
}
}
/* What do we do when the button is released? */
void bell_release(void) {
if (BELL_SW) {
/* The button is bouncing */
bell_state = BELL_DOWN;
bell_down();
} else if ((system_tick - bell_released) > 2) {
/* I'll call this released. */
bell_state = BELL_STOP;
} else if (!buffer_ready) {
/* Keep making the dinging noises in the meantime */
bell_ptr = write_audio(
bell_snd, bell_ptr, bell_loop_sz,
0, 1);
}
}
/* Waiting for the final ding */
void bell_stop(void) {
if (bell_ptr < bell_sz) {
if (!buffer_ready) {
/* One more ding since there's room */
bell_ptr = write_audio(
bell_snd, bell_ptr, bell_sz, 0, 0);
}
} else {
/* We're done, wait for the buffer to finish */
while(!buffer_wait);
stop_audio();
bell_state = BELL_IDLE;
}
}
/* Our main loop */
int main(void) {
/* Our last-activity time */
uint8_t last_act = 0;
/* Ensure interrupts are off */
cli();
/* Ensure audio is not running */
pwm_on = 0;
stop_audio();
PORTB |= (1 << DDB0);
/* Set up inputs */
PORTE |= (1 << 6);
PORTD |= (1 << 6);
/* Set up outputs */
DDRB |= (1 << DDB5)|(1 << DDB6)|(1 << DDB7)|(1 << DDB0);
DDRC |= (1 << DDB7)|(1 << DDB6);
DDRD |= (1 << DDB6)|(1 << DDB5)|
(1 << DDB3)|(1 << DDB2)|(1 << DDB1)|(1 << DDB0);
/* Hold the power on */
PWR_ON_PORT |= PWR_ON_PIN;
/* Set up Timer 1 */
OCR1A = OCR1A_VAL;
TIMSK1 = TIMSK1_VAL;
TCCR1A = TCCR1A_VAL;
TCCR1B = TCCR1B_VAL;
/* Enable interrupts */
sei();
while(1) {
if (bell_state == BELL_IDLE) {
led_colour = 1;
bell_idle();
} else if (bell_state == BELL_DOWN) {
led_colour = 3;
bell_down();
PORTD ^= (1 << DDB1);
} else if (bell_state == BELL_RELEASE) {
led_colour = 2;
bell_release();
PORTD ^= (1 << DDB0);
} else {
led_colour = 5;
bell_stop();
}
/* Turn power off if self-powered */
if (pwm_on)
PWR_ON_PORT |= PWR_ON_PIN;
else
PWR_ON_PORT &= ~PWR_ON_PIN;
}
}
ISR(TIMER1_COMPA_vect) {
/*
* Executed every tick to time things like shutdown delay and
* to blink the LEDs.
*/
uint8_t c = led_colour;
system_tick++;
if ((c & 8) && (!(system_tick & 0x02)))
c = 0;
if (c & 1)
RED_LED_PORT |= RED_LED_PIN;
else
RED_LED_PORT &= ~RED_LED_PIN;
if (c & 2)
GREEN_LED_PORT |= GREEN_LED_PIN;
else
GREEN_LED_PORT &= ~GREEN_LED_PIN;
if (c & 4)
BLUE_LED_PORT |= BLUE_LED_PIN;
else
BLUE_LED_PORT &= ~BLUE_LED_PIN;
}
ISR(TIMER3_COMPA_vect) {
/*
* Executed to pull data from the buffer and stuff it into
* the PWM output. We begin by reading the sample at the current
* buffer location and writing that to PWM.
*/
OCR0A = pwm_buffer[buffer_sel][buffer_ptr];
/* Is this the end of the buffer? */
if (buffer_ptr < (BUFFER_SZ-1)) {
/* No, move on */
buffer_ptr++;
//led_colour = 0x4;
PORTC ^= (1 << DDB6);
/* It is, is the other buffer ready? */
} else if (buffer_ready) {
/* Swap */
buffer_sel = buffer_sel ? 0 : 1;
buffer_ptr = 0;
buffer_ready = 0;
buffer_wait = 0;
PORTD ^= (1 << DDB2);
/* We're waiting on a buffer */
} else if (!buffer_wait) {
buffer_wait = 1;
//led_colour = 0x9;
PORTD ^= (1 << DDB3);
}
pwm_on = 1;
}