Skip to main content



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.

           // 
           //                    *** Note on GOTO ***
           // Being as this source code is on the internet, we should
           // mention that the use of GOTO here is out of desire for
           // a concise and efficient program.  Any other solution will
           // require a dozen or more instructions (ex. add'l IF statements,
           // writing a worker function, etc...) that are deteremental to
           // the very sensitive performance requirements of this program.
           // GOTO is not evil, but bad programmers often misuse it.
           //
           // 
     }
     if (trigger_mask == 0b00000111) {  // the > is just for safety
       TCCR0B = 0;     // Turn off timer 0 by removing its clock

       // The highest bits of each timer time are used for button states, since
       // it is highly unlikely that any will have a value as high as 2^15, which
       // would correspond to 70 meters.
       // There is only 1 button on the current mouse, so only 1 bit is set.
       // Low means the button is pressed.
       if ((PINC & (1<<4)) == 1) {
         trigger_times[0] |= (unsigned short)(1<<15);
       }

       // send coordinates to the computer via serial cable
       comm_position = 0;
       UCSR0B |= (1<<UDRIE0);      // enable USART transmission
       while (comm_position <= 5); // wait until the transmit finished

NEW_PULSE:

       // reset the global state and send a new pulse
       PORTB = 0;            // make sure the output port is low
       trigger_mask = 0;
       trigger_times[0] = 0x0;
       trigger_times[1] = 0x0;
       trigger_times[2] = 0x0;
       is_transmitting = 1;  // when the ISR is called, create the 40 kHz wave
       timebase_160kHz = 0;


       _delay_ms(5);
       // drive input zero
       DDRC  = 0b00000111;
       PORTC = 0b00000000;
       DDRC  = 0b00000000;
       _delay_ms(25);


       TCNT0 = 0;            // reset timer 0 counter value
       TCCR0B = (1<<CS00);   // turn on timer 0 at prescalar = 1 (CS00)
     }
   }
}




More Information

Code written for the ATMega644 that runs the UltraMouse.