ultramouse3d_mcu.c
// by Karl Gluck & David DeTomaso, 2009
#include <util/delay.h> // for the microsecond software-delay routine
#include <stdio.h> // fprintf
#include <stdlib.h> // C-standard library
#include <string.h> // string-manipulation routines
#include <inttypes.h> // for special integer type definitions
// These are the standard Atmel AVR include files
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
// Imports and initializes the serial-port communication library. This is
// how the program sends time-delays for each receiver to the client on the
// computer that actually moves the mouse.
#include "uart.h"
FILE uart_str = FDEV_SETUP_STREAM(uart_putchar, uart_getchar, _FDEV_SETUP_RW);
//=====================================================================//
//=================== HARDWARE CONFIGURATION ===================//
//=====================================================================//
/*
PORT B:
B.1 - driver circuit input for ultrasonic tx unit
PORT C:
C.0 - final output stage in rx 1's amplifier circuit
C.1 - final output stage in rx 2's amplifier circuit
C.2 - final output stage in rx 3's amplifier circuit
C.3 - button 0 (low when pressed, pull-up resistor)
*/
//=====================================================================//
//=================== GLOBAL VARIABLES ===================//
//=====================================================================//
volatile unsigned short trigger_times[3]; // stores timebase when triggered
volatile unsigned char trigger_mask; // how many rx's have recorded signal
volatile unsigned short timebase_160kHz; // counts up
volatile unsigned char comm_position; // use for serial comm. send
volatile unsigned char is_transmitting; // generate tx pulse while 1
//=====================================================================//
//=================== MACROS & DEFINITIONS ===================//
//=====================================================================//
// The time duration (nanoseconds) of each timebase count.
#define PERIOD_160kHz_nS (6250)
// How long to transmit the 40 kHz wave. This value is a safe amount of time
// to be sure that the receiver circuitry will be able to rise to a logic "1"
// Time is in nanoseconds.
#define TIME_TO_TRANSMIT_nS (1000000) /* 1 millisecond */
// How long to wait after transmission starts to reset the counter because
// the pulse wasn't received on all units. The value was derived by
// pulsing 40 kHz waves repeatedly on a distant transmitter while watching
// the receiver pins with an oscilloscope and seeing how long it takes for
// echoes and other resonance to decay.
// Time is in nanoseconds.
#define TIME_TO_MISSED_SIGNAL_nS (15000000) /* 15 milliseconds */
// The number of timebase counts at which to stop transmitting the 40 kHz pulse
#define TRANSMIT_COUNTER_CUTOFF (2 * TIME_TO_TRANSMIT_nS / PERIOD_160kHz_nS)
// The number of timebase counts that indicates a signal was lost
#define MISSED_SIGNAL_COUNTER_CUTOFF (2 * (TIME_TO_TRANSMIT_nS +\
TIME_TO_MISSED_SIGNAL_nS)\
/ PERIOD_160kHz_nS)
//=====================================================================//
//=================== INTERRUPTS ===================//
//=====================================================================//
//--------------------------------------------------------------------//
//--------- timer0 compare A interrupt, 160 kHz -----------//
//--------------------------------------------------------------------//
ISR (TIMER0_COMPA_vect) {
// This ISR has to be VERY short since it is executed so quickly:
// it must always have fewer than 100 instructions, otherwise it will
// livelock the main loop. The shorter it is, the better.
unsigned short local_timebase = ++timebase_160kHz;
// The is_transmitting flag is used instead of a more straightforward
// comparison (timebase_160kHz < X) because this requires 1 instruction,
// but a compare requires 4.
if (is_transmitting != 0) {
PORTB = local_timebase;
}
// poll the pins. Use a local variable instead of reading PINC
// repeatedly for speed. Make a local copy of the trigger mask
// since it's volatile, and check it first since the statement
// is only 2 instructions if testing returns false.
unsigned char pins = PINC;
unsigned char local_trigger_mask = trigger_mask;
if ((local_trigger_mask & (1<<0)) == 0) {
if ((pins & (1<<0)) != 0) {
trigger_times[0] = local_timebase;
trigger_mask |= (1<<0);
}
}
if ((local_trigger_mask & (1<<1)) == 0) {
if ((pins & (1<<1)) != 0) {
trigger_times[1] = local_timebase;
trigger_mask |= (1<<1);
}
}
if ((local_trigger_mask & (1<<2)) == 0) {
if ((pins & (1<<2)) != 0) {
trigger_times[2] = local_timebase;
trigger_mask |= (1<<2);
}
}
}
//--------------------------------------------------------------------//
//-------------- USART transmit interrupt -----------------//
//--------------------------------------------------------------------//
ISR (USART0_UDRE_vect) { // UART xmit-empty ISR
// // if we're done transmitting, turn off this interrupt
if (comm_position > 5) {
UCSR0B ^= (1<<UDRIE0);
return;
}
// send the next byte
UDR0 = ((unsigned char*)trigger_times)[comm_position++];
}
//--------------------------------------------------------------------//
//----------------------- main ----------------------------//
//--------------------------------------------------------------------//
int main(void) {
PORTB = 0b00000000; // Initialize port B to output on pin 1
DDRB = 0b00000010; // (pin 1 because the counter's 1 bit is used!)
DDRC = 0b00000000; // Read input from port C: pins 0, 1, 2 = rx 0, 1, 2
uart_init();
// Reset global variables
trigger_times[0] = 0x0;
trigger_times[1] = 0x0;
trigger_times[2] = 0x0;
timebase_160kHz = 0;
trigger_mask = 0;
is_transmitting = 1;
// Use timer0 as a 160 kHz timer.
// Now, the documentation for the ATMega644 says that
// the formula for the frequency at which a timer will be
// called is:
//
// fOCnA = fclk_IO / (2 * prescaler * (1 + OCRnA))
//
// HOWEVER, by scoping a pin that's toggled every time the
// interrupt is called, we've found that this is NOT the
// case. The interrupt is actually called TWICE that fast;
// i.e. the formula should be:
//
// fOCnA = fclk_IO / (prescaler * (1 + OCRnA))
//
// So to get a 160 kHz interrupt, we need the following
// parameters:
//
// prescaler: 1
// OCR0A: 99
// fclk_IO: 16 MHz (crystal freq.)
// -> fOCnA = 160 kHz (also hopefully?)
//
TCCR0A = (1<<WGM01); // CTC mode, which means max is OCR0A
TCCR0B = (1<<CS00); // run at prescalar = 1 (CS00 bit set)
OCR0A = 99; // reset counter every 100 increments
TIMSK0 = (1<<OCIE0A); // (1) turn on timer 0 overflow ISR
TIFR0 = (1<<OCF0A); // (2)
TCNT0 = 0; // reset the counter's current value
// Start up the interrupts
_delay_ms(500); // this isn't strictly necessary
sei();
while (1) { // execute forever
//DDRA |= 1; // debugging
if (timebase_160kHz >= TRANSMIT_COUNTER_CUTOFF) {
is_transmitting = 0;
PORTB = 0;
}
// If all 3 triggers have been captured, or the time since the signal
// was transmitted is longer than the missed signal threshold,
// calculate new data and restart the transmitter.
// It is the job of the receiving program to check to see if any of
// the trigger times are zero and throw out the signal.
if (timebase_160kHz > MISSED_SIGNAL_COUNTER_CUTOFF) {
TCCR0B = 0; // Turn off timer 0 by removing its clock
goto NEW_PULSE; // Skip coordinate transmission, and re-send
// an ultrasonic pulse.
//
More Information
Code written for the ATMega644 that runs the UltraMouse.