/*********************************************************** ***** ECE 476 Spring 2004 Final Project ******* ***** The "Sober Up!" Breath-o-Matic Alcohol Sensor ******* ***** ******* ***** By Dan Golden and Alex Averbukh ******* ***** 04/09/2004 ******* ***********************************************************/ /************************************ ***** Port Description ******* ************************************ A.0 Alcohol Sensor A.1 Pressure Sensor (Diff Amp output) A[2:7] NOT USED B.0 Keyboard Clock B.1 Power B.3 Power button B.3 NOT USED B[4:5] Breathing LEDs [Red Green] B[6:7] Buttons [Blue Green] C[0:7] LCD D.0 Keyboard Data D[1:6] ETOH LEDs D.7 Speaker */ /************************************ ***** Includes ******* ************************************/ #include #include #include #include #include /************************************* ***** Definitions ******* *************************************/ #define VCC 4.86 #define R0 6584 #define R1 1970 #define numkbdCodes 39 #define hsnamelength 12 #define numsamples 10 // broken pressure gradient #define bpg 0.30 // MainState Definitions #define WarmingUp 0 #define MainMenu 1 #define TakingSample 2 #define EnteringHighScore 3 #define HighScoreList 4 // Port Definitions #define bLEDr PORTB.4 #define bLEDg PORTB.5 #define bluebutton PINB.6 #define greenbutton PINB.7 #define ETOHLED1 PORTD.1 // g #define ETOHLED2 PORTD.2 // g #define ETOHLED3 PORTD.3 // y #define ETOHLED4 PORTD.4 // y #define ETOHLED5 PORTD.5 // r #define ETOHLED6 PORTD.6 // r // ETOH level definitions // Given values are the MAX BACs for each level #define sober 0.04 #define happy 0.06 #define confused 0.09 #define tipsy 0.12 #define incomprehensible 0.15 #define borisyeltsin /************************************* ***** LCD Initialization ******* *************************************/ #define LCDwidth 16 //characters // LCD is on PORT C #asm .equ __lcd_port=0x15 #endasm #include // LCD driver routines /************************************* ***** Global Variables ******* *************************************/ unsigned int ms = 0, tempcount = 0; unsigned char MainState, TestBreathState, EnterHighScoreState, HighScoreState; unsigned char ButtonState; unsigned char bluebuttonPressed, greenbuttonPressed; unsigned char buttonsPressed; unsigned char thishighscorename[hsnamelength+1]; // 12 chars + null unsigned char thishighscore; float BAC; float thispressure, lastpressure; float voltage, tempfloat; float BAC_samples[30]; char count = 0, count2 = 0; // High Scores unsigned char highscorenames[10][hsnamelength+1]; float highscores[10]; unsigned char numhighscores = 0; // SCANCODES unsigned char kbdCodes[]={0x1C,0x32,0x21,0x23,0x24,0x2B,0x34,0x33, 0x43,0x3B,0x42,0x4B,0x3A,0x31,0x44,0x4D, 0x15,0x2D,0x1B,0x2C,0x3C,0x2A,0x1D,0x22, 0x35,0x1A,0x45,0x16,0x1E,0x26,0x25,0x2E, 0x36,0x3D,0x3E,0x46,0x29,0x66,0x5A}; // letters, numbers,space,backspace,ENTER //kbd2ascii unsigned char code; bit throw=0; int i; //UART unsigned char char_count; bit cmd_ready; char kbd_str[20]; unsigned char c; //lcd char lcd_buffer[17]; /************************************* ***** Function Prototypes ******* *************************************/ void initialize(void); // state machines void MainStateMachine(void); void TestBreathStateMachine(void); void EnterHighScoreStateMachine(void); void HighScoreStateMachine(void); void ButtonStateMachine(void); void DeleteHighScores(void); // change ADC channel - 0:ETOH, 1:Pressure void ADC_channel(unsigned char); // puts MCU to sleep to get a measurement from ADC & returns voltage float ADC_measure(void); // returns BAC from ETOH PPM measurement float PPMtoBAC(float); // returns character corresponding to the scancode of keyboard unsigned char kbd2ascii(void); //initializes UART for receive void UARTInit(void); // convert ETOH sensor voltage to BAC float VstoBAC(float); // make speaker noises during and after breath sample void Speaker(unsigned char); void ETOHLevelOutput(unsigned char); // turn off LEDs void LEDsoff(void); // Wait 6 seconds for sensor reading to stabalize; return value = BAC float ETOHStabalize(void); /************************************* ***** Interrupts ******* *************************************/ interrupt [TIM0_COMP] void timer0_compare(void) { // millisecond timers ms++; tempcount++; if ((ms & 31) == 0) // every 32 ms, run button state machine ButtonStateMachine(); } /***************************************************/ interrupt [USART_RXC] void uartreceive(void) { code = UDR; // get character from UART c = kbd2ascii(); if(c != 0) { // if character is not return or backspace... if (c != '\r' && c != '\b' ) { // if the name is not too long... if(char_count < hsnamelength) { kbd_str[char_count++] = c; kbd_str[char_count] = 0; //legal C string } } // if character is backspace... else if(c == '\b') { // and the name has a positive length... if(char_count > 0) { //lcd_clear(); //kbd_str[--char_count] = 0; kbd_str[--char_count] = ' '; } } // if character is return... else { kbd_str[char_count] = 0; // end string in a happy C way cmd_ready = 1; // command is ready to be executed } } } // this interrupt exists to wake the CPU from ADC sleep mode interrupt [ADC_INT] void ADCconvcompl(void) { } /************************************* ****** Main ******* *************************************/ void main(void) { initialize(); // get initial pressure ADC_channel(1); lastpressure = ADC_measure(); thispressure = lastpressure; // used to toggle LCD messages tempcount=0; while(1) { MainStateMachine(); } } /************************************* ***** Function Definitions ******* *************************************/ void initialize(void) { // set initial variables to 0 voltage = 0; MainState = MainMenu; TestBreathState = 0; EnterHighScoreState = 0; HighScoreState = 0; ButtonState = 0; bluebuttonPressed = 0; greenbuttonPressed = 0; buttonsPressed = 0; ms = 0; // initialize UART UARTInit(); // initialize timers // Timer 0 is a ms timer TIMSK=2; //turn on timer 0 cmp match ISR OCR0 = 249; //set the compare register to 250 time ticks TCCR0=0b00001011; //prescalar to 64 and turn on clear-on-match // Timer 2 is speaker output // FOC off, CTC, OC2 off, CLK/128 TCCR2 = 0b00001110; // initialize ADC ADMUX = 0b01100000; // reference VCC, channel 0, left adjust // ADC enable, start conversion, interrupt disable, CLK/128 ADCSRA = 0b11001111; // initialize sleep mode MCUCR = 0b10010000; // enable ADC sleep mode // initialize ports DDRA = 0b00000000; /* [7:2] Not Used 1: Pressure ADC input 0: ETOH ADC input */ PORTA = 0b00000000; // No pullups DDRB = 0b00110010; /* 7: Green button 6: Blue button 5: Green breathing LED 4: Red breathing LED 3: Not Used 2: Not Used 1: Circuit enable (1 = on) 0: Keyboard clock */ PORTB = 0b00000010; // circuit on, breathing LEDs off, no pullups // PORT C is LCD DDRD = 0b11111110; // RXD input,output ETOH LEDs,output speaker /* 7: Speaker 6: ETOH LED 6 (r) 5: ETOH LED 5 (r) 4: ETOH LED 4 (y) 3: ETOH LED 3 (y) 2: ETOH LED 2 (g) 1: ETOH LED 1 (g) 0: Keyboard data */ PORTD = 0b10000000; // All off, no pullups // initialize LCD, display welcome message lcd_init(LCDwidth); lcd_clear(); lcd_gotoxy(0,0); lcd_putsf("Welcome to the"); lcd_gotoxy(0,1); lcd_putsf("Breath-o-Matic!"); delay_ms(2000); lcd_clear(); // turn on interrupts #asm("sei"); } void ADC_channel(unsigned char channel) // 0: ETOH, 1: Pressure { ADMUX = 0b01100000 | channel; // set channel ADCSRA = ADCSRA | 0b01000000; // start conversion } float ADC_measure(void) { #asm("sleep"); // sleep to start conversion return((float) (ADCH / 256.0 * VCC)); // convert to voltage } float PPMtoBAC(float PPM) { /* BAC of 0.01 equals 0.01g ETOH in 210 L air PPM is g ETOH for every 10^6 g Air At STP, air has a density of 1.29 g/L thus BAC = (g ETOH) / (10^6 g Air) * (1.29 g Air / L air) * 210 * 10^-6 */ return (PPM * 1.29 * 210 / 1000000); } float VstoBAC(float Vs) { float Rs, PPM; /* VCC - ETOH sensor - 2k resistor - GND Measuring volage Vs between ETOH sensor and 2k resistor Rs = Rl*(VCC/Vs - 1) Region 1 (2 <= Rs/R0 < 4): PPM = 244.8 * (R0/Rs)^1.304 Region 2 (Rs/R0 < 2): PPM = 248 * ((R0/Rs)^1.323 */ // Region 1 readings have been experimentally determined to be junk! Rs = R1 * (VCC / Vs - 1); if (Rs/R0 < 2) // Region 2 PPM = 248 * pow((R0/Rs), 1.323); // else if (Rs/R0 < 4) // Region 1 // PPM = 244.8 * pow((R0/Rs),1.304); else PPM = 0; return PPMtoBAC(PPM); } unsigned char kbd2ascii(void) { /* 1) accept keyboard code 2) lookup ascii value 3) throw away F0 and following code 4) update the number of alpha-numeric characters. */ if(code!=0xf0 && !throw) { // search through the character code list for the received character for(i=0; i= 0) { ETOHLED1 = 1; OCR2 = 120; TCCR2 = TCCR2 | 0b00010000; delay_ms(100); TCCR2 = TCCR2 & 0b11101111; delay_ms(100); } if (level >= 1) { ETOHLED2 = 1; OCR2 = 110; TCCR2 = TCCR2 | 0b00010000; delay_ms(100); TCCR2 = TCCR2 & 0b11101111; delay_ms(100); } if (level >= 2) { ETOHLED3 = 1; OCR2 = 100; TCCR2 = TCCR2 | 0b00010000; delay_ms(100); TCCR2 = TCCR2 & 0b11101111; delay_ms(100); } if (level >= 3) { ETOHLED4 = 1; OCR2 = 90; TCCR2 = TCCR2 | 0b00010000; delay_ms(100); TCCR2 = TCCR2 & 0b11101111; delay_ms(100); } if (level >= 4) { ETOHLED5 = 1; OCR2 = 80; TCCR2 = TCCR2 | 0b00010000; delay_ms(100); TCCR2 = TCCR2 & 0b11101111; delay_ms(100); } if (level >= 5) { ETOHLED6 = 1; OCR2 = 70; TCCR2 = TCCR2 | 0b00010000; delay_ms(100); TCCR2 = TCCR2 & 0b11101111; delay_ms(100); } // play two more notes, to sound more musical OCR2 -= 5; TCCR2 = TCCR2 | 0b00010000; delay_ms(100); TCCR2 = TCCR2 & 0b11101111; delay_ms(100); OCR2 -= 5; TCCR2 = TCCR2 | 0b00010000; delay_ms(200); TCCR2 = TCCR2 & 0b11101111; } void LEDsoff(void) { ETOHLED1 = 0; ETOHLED2 = 0; ETOHLED3 = 0; ETOHLED4 = 0; ETOHLED5 = 0; ETOHLED6 = 0; bLEDr = 0; bLEDg = 0; } float ETOHStabalize(void) { float ETOH3, ETOH2, ETOH1, result; lcd_clear(); lcd_gotoxy(0,0); lcd_putsf("Wait"); ADC_channel(0); ETOH3 = ADC_measure(); // current measure ETOH2 = -1; // one second ago ETOH1 = -1; // two seconds ago tempcount = 0; count = 0; // wait six seconds, or until the sensor reading stabalizes, whichever is first while ((tempcount < 6000) && !((ETOH1 < ETOH3 + 0.005) && (ETOH1 > ETOH3 - 0.005))) { if ((tempcount & 1023) == 0) { ETOH1 = ETOH2; ETOH2 = ETOH3; ETOH3 = VstoBAC(ADC_measure()); lcd_gotoxy(12,0); sprintf(lcd_buffer, "%4.2f", ETOH3); // print current BAC lcd_puts(lcd_buffer); lcd_gotoxy(count,1); lcd_putsf("."); // put progress dots on screen Speaker(4); // beep delay_ms(150); Speaker(0); count++; } } if (tempcount == 6000) result = -1; // no stabalization; bad sample else result = ETOH3; // good sample delay_ms(1000); return result; } void ButtonStateMachine(void) { switch(ButtonState) { case 0: // no press if (bluebutton || greenbutton) { if (bluebutton) buttonsPressed += 2; else if (greenbutton) buttonsPressed += 1; ButtonState = 1; } break; case 1: // maybe press ButtonState = 2; if (bluebutton && (buttonsPressed & 0x02)) { bluebuttonPressed = 1; } else if (greenbutton && (buttonsPressed & 0x01)) { greenbuttonPressed = 1; } else { ButtonState = 0; } break; case 2: // press if (!(bluebutton || greenbutton)) ButtonState = 3; break; case 3: // maybe no press if (!(bluebutton || greenbutton)) { buttonsPressed = 0; greenbuttonPressed = 0; bluebuttonPressed = 0; ButtonState = 0; } else ButtonState = 2; break; default: // uh oh ButtonState = 0; } } void TestBreathStateMachine(void) { // INITIAL PRECONDITION: tempcount = 0; count = 0; TestBreathState = 0; switch (TestBreathState) { case 0: // breathing // every ~1/2 second... if (((tempcount & 511) == 0) && (count < numsamples)) { ADC_channel(0); // ETOH channel BAC_samples[count] = ADC_measure(); // display current voltage lcd_gotoxy(9,1); sprintf(lcd_buffer,"%5.3f",BAC_samples[count]); lcd_puts(lcd_buffer); // beep, blink red LED Speaker(1); delay_ms(100); Speaker(0); count++; // print seconds to LCD if (count & 1) // every second { lcd_gotoxy((count >> 1),1); sprintf(lcd_buffer, "%d", (count >> 1)+1); lcd_puts(lcd_buffer); } } else if (count >= numsamples) TestBreathState = 1; // determine goodness of sample break; case 1: // determine goodness of sample // make good sound, light green LED delay_ms(250); Speaker(3); delay_ms(1000); BAC = ETOHStabalize(); if (BAC >= 0) // good sample { TestBreathState = 2; // Light appropriate amount of LEDs if (BAC < sober) ETOHLevelOutput(0); else if (BAC < happy) ETOHLevelOutput(1); else if (BAC < confused) ETOHLevelOutput(2); else if (BAC < tipsy) ETOHLevelOutput(3); else if (BAC < incomprehensible) ETOHLevelOutput(4); else ETOHLevelOutput(5); lcd_clear(); lcd_gotoxy(0,0); sprintf(lcd_buffer, "BAC: %5.3f", BAC); // display BAC lcd_puts(lcd_buffer); lcd_gotoxy(0,1); lcd_putsf("G: Continue"); } else // bad sample { // make bad sound, light red LED Speaker(2); TestBreathState = 4; } break; case 2: // consistent samples // if drunky presses the green button... if (greenbuttonPressed) { LEDsoff(); while(greenbuttonPressed); // wait for release for (count = 0; count < numhighscores; count++) // determine if this high score beats a current one { if (BAC > highscores[count]) // high score! { MainState = EnteringHighScore; EnterHighScoreState = 0; thishighscore = count; UCSRB = 0b10010000; // enable keyboard tempcount = 0; // used to toggle LCD break; } } if ((numhighscores < 10) && (count == numhighscores)) // this is the lowest high score { MainState = EnteringHighScore; EnterHighScoreState = 0; thishighscore = numhighscores; UCSRB = 0b10010000; // enable keyboard tempcount = 0; // used to toggle LCD } else if (count == numhighscores) // no high score :( { MainState = HighScoreList; HighScoreState = 0; tempcount = 0; // used to toggle LCD } } break; case 4: // inconsistent samples LEDsoff(); lcd_clear(); lcd_gotoxy(0,0); lcd_putsf("Bad Sample!"); delay_ms(3000); lcd_clear(); lcd_gotoxy(0,0); lcd_putsf("Returning to"); lcd_gotoxy(0,1); lcd_putsf("Main Menu."); delay_ms(2000); // turn off red LED bLEDr = 0; MainState = MainMenu; // back to Main Menu // get initial pressure lastpressure = ADC_measure(); thispressure = lastpressure; tempcount = 0; break; default: TestBreathState = 0; } } void EnterHighScoreStateMachine(void) { // INITIAL PRECONDITION: EnterHighScoreState = 0; tempcount = 0; switch(EnterHighScoreState) { case 0: // happy messages if (tempcount == 0) { lcd_clear(); lcd_gotoxy(0,0); lcd_putsf("You have a new"); lcd_gotoxy(0,1); lcd_putsf("high score!"); } else if (tempcount == 2048) { lcd_clear(); lcd_gotoxy(0,0); lcd_putsf("Enter your name"); lcd_gotoxy(0,1); sprintf(lcd_buffer, "%d char max", hsnamelength); lcd_puts(lcd_buffer); } else if (tempcount == 4096) { lcd_clear(); lcd_gotoxy(0,0); lcd_putsf("G: Skip"); } else if (tempcount == 6144) tempcount = 0; // first character recieved; show character and move to next state if (char_count == 1) { lcd_clear(); lcd_gotoxy(0,0); sprintf(lcd_buffer, "%02d: ", thishighscore + 1); lcd_puts(lcd_buffer); lcd_gotoxy(4,0); sprintf(lcd_buffer,"%s",kbd_str); lcd_puts(lcd_buffer); EnterHighScoreState = 1; count = 1; } // green button skips high score name entry if (greenbuttonPressed) { while (greenbuttonPressed); HighScoreState = 0; MainState = HighScoreList; tempcount = 0; // used to toggle LCD } break; case 1: // Drunky is typing their name /* put score number and name, as entered, on LCD; backspace deletes char, and enter signals correct name. Space and alphanumerics are the ONLY permissible characters Save name to "thishighscorename" */ // if a new character is received... if (char_count != count) { lcd_gotoxy(4,0); sprintf(lcd_buffer,"%s",kbd_str); lcd_puts(lcd_buffer); count = char_count; } // Enter key pressed (and at least one char on screen) if (cmd_ready) { UCSRB = 0b10000000; // disable keyboard // name entered into thishighscorename for(i=0;kbd_str[i] != 0;i++) thishighscorename[i]=kbd_str[i]; thishighscorename[i]=0; // end in happy C way // if there is at least one character... if (i > 0) { if (numhighscores < 10) numhighscores++; // shift high scores to make room for new high score for (count = min(numhighscores - 1,8); (signed)count >= (signed)thishighscore; count--) { for (count2 = 0; count2 < hsnamelength; count2++) { highscorenames[count+1][count2] = highscorenames[count][count2]; } highscores[count+1] = highscores[count]; } // enter this high score for (count = 0; count < hsnamelength; count++) { highscorenames[thishighscore][count] = thishighscorename[count]; } if (BAC < 0.2) highscores[thishighscore] = BAC; else highscores[thishighscore] = 0.2; // reset keyboard stuff char_count = 0; cmd_ready = 0; } // move to High Score List HighScoreState = 0; MainState = HighScoreList; tempcount = 0; // used to toggle LCD } break; default: EnterHighScoreState = 0; } } void HighScoreStateMachine(void) { // INITIAL PRECONDITION: tempcount = 0; HighScoreState = 0; // if there is at least one high score... if (numhighscores > 0) { if (tempcount == 0) { // print number of high score to lcd_buffer and high score name to lcd_buffer sprintf(lcd_buffer, "%0d: %s", HighScoreState + 1,highscorenames[HighScoreState]); // print name and number to LCD lcd_clear(); lcd_gotoxy(0,0); lcd_puts(lcd_buffer); // print score to LCD lcd_gotoxy(0,1); sprintf(lcd_buffer, "BAC: %5.3f", highscores[HighScoreState]); lcd_puts(lcd_buffer); } // display next high score if (numhighscores > HighScoreState + 1 && tempcount == 2048) { HighScoreState++; tempcount = 0; } // display command list else if (numhighscores == HighScoreState + 1 && tempcount == 2048) { lcd_clear(); lcd_gotoxy(0,0); lcd_putsf("G: Main Menu"); lcd_gotoxy(0,1); lcd_putsf("B: Delete scores"); } else if (tempcount >= 4096) { HighScoreState = 0; tempcount = 0; } } // if there are no high scores... else if (numhighscores == 0) { if (tempcount == 0) { lcd_clear(); lcd_gotoxy(0,0); lcd_putsf("No high scores"); lcd_gotoxy(0,1); lcd_putsf("yet!"); } else if (tempcount == 2048) { lcd_clear(); lcd_gotoxy(0,0); lcd_putsf("G: Main Menu"); lcd_gotoxy(0,1); lcd_putsf("B: Delete scores"); } else if (tempcount >= 4096) tempcount = 0; } // to quit the high scores, press green if (greenbuttonPressed) { while (greenbuttonPressed); // wait for release MainState = MainMenu; // back to main menu ADC_channel(1); // set ADC to pressure channel // get initial pressure lastpressure = ADC_measure(); thispressure = lastpressure; tempcount = 0; // tempcount is used to toggle LCD } // to delete high scores, press blue else if (bluebuttonPressed) { while(bluebuttonPressed); // wait for release DeleteHighScores(); tempcount = 0; HighScoreState = 0; } } void MainStateMachine(void) { switch(MainState) { // this case is not currently implemented case WarmingUp: if (ADC_measure() < tempfloat) { tempfloat = ADC_measure(); lcd_gotoxy(0,1); sprintf(lcd_buffer, "%5.3f", tempfloat - 0.7); lcd_puts(lcd_buffer); } if ((tempfloat < 0.700) || greenbuttonPressed) // ETOH voltage has settled down after initial spike { while (greenbuttonPressed); // wait for release MainState = MainMenu; // move to "Main Menu" state ADC_channel(1); // set ADC to pressure channel // get initial pressure lastpressure = ADC_measure(); thispressure = lastpressure; tempcount = 0; // tempcount is used to toggle LCD } break; case MainMenu: // INITIAL PRECONDITION: tempcount = 0; ADC_channel(1); // initial pressure measured; // measure pressure if ((tempcount & 511) == 0) { lastpressure = thispressure; thispressure = ADC_measure(); } if (tempcount == 0) { lcd_clear(); lcd_gotoxy(0,0); lcd_putsf("Main Menu"); } else if (tempcount == 1024) { lcd_clear(); lcd_gotoxy(0,0); lcd_putsf("G: High Scores"); lcd_gotoxy(0,1); lcd_putsf("B: Force Sample"); ADC_channel(0); tempfloat = ADC_measure(); // get ETOH voltage sample ADC_channel(1); } else if ((tempcount == 2048) && (tempfloat <= 0.32)) { lcd_clear(); lcd_gotoxy(0,0); lcd_putsf("Or just start"); lcd_gotoxy(0,1); lcd_putsf("blowing!"); } else if ((tempcount == 2048)) { lcd_clear(); lcd_gotoxy(0,0); lcd_putsf("Warning"); lcd_gotoxy(0,1); sprintf(lcd_buffer, "%4.2f > 0.31", tempfloat); lcd_puts(lcd_buffer); } else if (tempcount >= 3072) { tempcount = 0; } if (greenbuttonPressed) { // wait for button release while(greenbuttonPressed); MainState = HighScoreList; // move to high score list HighScoreState = 0; tempcount = 0; // used to toggle LCD } // if the pressure has changed on our broken sensor... // or if the blue button is pressed... // someone is breathing into the Breath-o-Matic else if (thispressure > lastpressure + bpg || bluebuttonPressed) { while(bluebuttonPressed); // wait for release MainState = TakingSample; TestBreathState = 0; lcd_clear(); lcd_gotoxy(0,0); lcd_putsf("Breathe, Monkey!"); tempcount = 0; // used to time breath samples count = 0; // used for sample number } break; case TakingSample: TestBreathStateMachine(); break; case EnteringHighScore: EnterHighScoreStateMachine(); break; case HighScoreList: HighScoreStateMachine(); break; default: MainState = MainMenu; } }