PIC-programmeringsuppgift (PROA) Ugnsklocka digital klocka med en timer-funktion Tillämpad Digitalteknik med PIC-processor IL131V (HT12) Karl-Axel Zander 920208-5594 28-01-2013 underskrift
Uppgift & beskrivning Ursprunglig uppgift (28-10-2012) En timer, "ugnsklocka", på en 4-digit 7-segmentdisplay (min:sek) som man ställer in en tid med hjälp av en/flera knapp(ar), ev vridomkopplare, den ska räkna ner. Timern startas med en knapp och när tiden når 00:00 görs ett lämpligt ljud ifrån en hörlur under ett par sekunder. Efteråt kan användaren ställa in och starta en ny nedräkning. Utökad uppgift (05-12-2012) jag utökar min uppgift med att lägga in en klockfunktion som kan visa vad klockan är när timern inte används. Håller det enkelt genom att den får ställas in på nytt när kretsen startas efter power-off och utan extern kristall (för bättre noggrannhet). Denna produkt beskriven ovan ska byggas och redovisas prototypaktigt med hjälp av en pic-processor och de nödvändiga elektroniska komponenter som krävs för att uppgiftsidén ska vara demonstrerbar. Hårdvara Jag har använt mig av PIC-processorn 16F628A med 4MHz intern oscillator, 3,5KB program minne och 16st input/output pinnar (1 input only, 1 input half output). Fig 1. Pinnkarta för PIC16F628A (och liknande modeller)
Jag har valt att koppla in så mycket som tre knappar för att styra produkten för att användaren lättsamt ska kunna förstå och använda den. För att göra ett hörbart alarmljud sitter en hörlur kopplad till en port som kan spela upp ljud genom att med olika hastigheter skifta porten mellan 1 och 0. Displayen är en enkel 4-siffrig 7-segment display med 16 pinnar. För att koppla samman dessa komponenter och dessutom smidigt kunna införa spänningskälla och ledningar in för att kunna programmera och hantera PIC-processorn så är allt detta monterat på ett kopplingsdäck av lagom storlek. I figur 2 ses ett förenklat blockdiagram över hur komponenterna är kopplade från processorn. Fig 2. Blockdiagram I figur 3 ses produkten färdigmonterad på kopplingsdäcket.
Fig 3 Kopplingsdäcket Komponentlista - PIC-processor 16F628A - Motstånd 240Ω (segmentenkopplingarna till displayen) - Mostånd 1,5KΩ (till transistorerna) - Motstånd 10KΩ, (i förslutningen av matningsspänningen och pull-up motstånd till knapparna) - PNP-transistorer BC557B, Ic max (A) 0,2A (stödförsörning till displayen, kopplingarna digit1-digit4) - Diod 100V 150mA - Kondensator 2,7µF - Trycknappar - 4-digit 7-segment display (modell ATA3492BW) - Keramisk hörlur utan plugg - Sladdar, kopplingstråd, ISCP-kabel till Microchip PICkit 2 programmerare En kodensator är kopplad mellan + och i mattningsspänningen, se längst ner till vänster i figur 3. Denna fungerar som ett utjämnande filter för eventuella spänningsfall från matningskällan. Pullup motstånd är kopplade i varje tryckknapp krets för att signalen ska hålla sig till hög (5V) och inte fladdrar till vilket läge som helst av sig själv när knappen inte trycks. Hur funkar 7-segment displayen? Bara en siffra kan användas åt gången då alla de 4 displayerna delar ut-pinnar, segmentpinnar. Se kopplingsscehmat i figur 4. Detta är en fördel då vi skär ner på antalet ledningar, samtidigt som vi kan få alla lampor att till synes att lysa samtidigt, genom att med sk multiplexing mycket snabbt tända en siffra, släcka den,
ställa om segmentpinnarna, tända nästa siffra, osv och konstant så loopa runt alla siffror. Gör man detta väldigt snabbt (1000 gånger i sekunden) så ser en människa inte att det blinkar utan som att alla lampor faktiskt är tända samtidigt. Fig 4. Displayens kopplingsschema Det uppmätta spänningsfallet över displayen är 3V, om räknar med att vi har 5V från spänningkällan så har vi 2V kvar. Varje led i varje siffra (8st) får således strömmen 2/240 = 8mA då dessa är kopplade genom individuella 240Ω motstånd, vilket är tillräckligt för att lysa upp en LED synbart. PNP-transistorer används till displayen för att pic-processorn bara kan sourca 25mA från portarna (även agera jord, sinka 25mA). Då 8 LEDS drar ~64mA så klarar inte porten alla dessa lampor. Nu slår porten på och av spänningen som kommer direkt från matningskällan genom en transistor (kopplade genom 1,5KΩ motstånd), som i sin tur är kopplad till displayen. Mjukvara I figur 5 ses ett sturkturdiagram (JSP-diagram) över programflödet med sparsamt med detaljoperationer för att göra det överskådligt. En del förstå till logiken och hur programmet fungerar faller tyvärr bort i denna förenklade variant, och kan behöva kompletterande förklaring. Programmet börjar i main metoden med att ställa in portar och timers, och initiering av programvariabler sker. Dessa steg är väl kommenterade och förklarade i programkoden. Komparatorer och dyl stängs av så att alla portar kan utnyttjas för out eller input då denna hårdvarukonstruktion kräver det. Två timers ställs in för att göra interrupt med olika frekvens, vilket är centralt för programmet, användas för klockans & timerns tidräkning och för att kunna göra en fördröjningsfunktioner i programmet utan låsning. Programmet går sedan in i en oändlig loop, programmet körs till användaren rycker sladden. Programmet har 4 tillstånd, states eller som kallat modes. Dessa är:
1: set clock 2: display clock 3: set timer 4: timer running Button1, button2 och button3 gör olika saker beroende på vilket tillstånd man befinner sig. Loopen består av ett antal partier kod omringande av en villkorsats var, se strukturdiagrammet. Uppfylls villkoret körs kodpartiet, annars hoppas det över och kollas först igen vid nästa vända. Tex så kollas Timer2 och Timer0s interrupt flagga således en gång per loop, dvs dessa bevakas inte under konstant låst polling utan lite annan kod får köra imellan. Eftersom dessa timrar triggar ofta så undviks helt att använda låsta programdelayer (programmet fastnar på ett ställe för att vänta ut en bestämd tid). Delays är i stället gjorda med countervariabler som ökas på i samband med timerinterrupten, värdena på dessa kan kollas på övriga ställen i programmen och räknas om till hur lång riktig tid som har förflutit sedan senaste nollställningen. Ett problem jag blev osäker på är noggrannheten för tidräkningen. Timer2 är inställd för att ge interrupt ungefär 1/244 sekund enligt denna beräkning: Count = fclk / (4*Prescaler*(PR2-TMR2)*Postscaler*fout = 4MHz / (4*1*(256-0)*16*1Hz) = 244,14 Efter att timern har triggat ungefär 244 gånger så bör det således gått en sekund. Detta är tydligen inte noggrant nog då klockan drog sig märkbart. Då jag var osäker hur jag skulle lösa och testa detta så lämnade jag detta som en parantes i projektet. Jag testade dock att ändra detta värde till 246 efter manuellt testande vilket verkade bli klart bättre, jag är dock osäker på matematiken och eventuella andra faktorer som behövs ta hänsyn till. Sedan är jag även osäker på hur programmets struktur påverkar noggrannheten på hur snabbt dessa interrupts upptäcks. Optimalt vore om timerns flagga bevakades med låst polling, medan nu körs en massa annan kod och flaggan kollas bara ibland. Men jag antar att det fungerar dugglig i nuläget som en prototyp för demonstration, på 10 minuter manuellt testande kan jag inte se att klockan drar sig märkbart efter den manuella korrigeringen.
Fig 5. Förenklat strukturdiagram (JSP) för programkoden
Programkod Kompilator: C-kompilatorn Cc5x 3.4A (C-dialekt för PIC) av Bengt Knudsen /* owenclock.c B Knudsen Cc5x C-compiler - not ANSI-C An ordinary digital clock on a with a countdown alarm timer function - Processor "pic16f628a" - 4-digit 7-segment display - 3 buttons: "mode", "increase", "set/start/stop" - Ceramic earphone for alarm \/ Button1 -- RA2 16F628A RA1 -- Digit1 Digit4 -- RA3 RA0 -- Digit2 Digit3 -- RA4-od RA7/OSC1 -- Earphone Button2 -- RA5/MCLR RA6/OSC2 -- Segment A GND -- Vss Vdd -- +5V Button3 -- RB0/INT (RB7)/PGD -- Segment G Segment E -- RB1/Rx (RB6)/PGC -- Segment C Segment B -- RB2/Tx RB5 -- Colon Segment D -- RB3/CCP (RB4)/PGM -- Segment F 7-segment digit: A F B G E C */ D #include "16F628A.h" #pragma config = 0x3F10 //Low Volt Programming off (RB4), RA5 I/O internal MCLR (RA5 can only be used as input) void setsegments(char number); void main(void) //configure ports CMCON = 0b111; //turn off comparators (port RA3 RA4) TRISA = 0b00100100; //port RA2 RA5 input, port RA0 RA1 RA3 RA4 RA6 RA7 output TRISB = 0b00000001; //port RB0 input, port RB1-RB7 output //Timer0, inbuilt 8-bit interrupt timer TMR0 = 0; //TMR0 initiation T0CS = 0; //use internal CLK T0SE = 0; //react on Low2High edge PSA = 0; //use presacler PS0 = 0; PS1 = 0; //presacler PS2 PS1 PS0 bits set to "000" = 1:2 PS2 = 0;
//Timer2, another inbuilt 8-bit interrupt timer T2CON = 0b01111100; //Timer2 initalization: postscaler = 1111 (1:16), timer2 set to ON, prescaler = 00 (1:1) TMR2 = 0; //start value = 0 PR2 = 0xFF; //stop value = 255 RB1 = 1; RB2 = 1; RB3 = 1; RB4 = 1; RB5 = 1; RB6 = 1; RB7 = 1; RA6 = 1; RA0 = 1; RA1 = 1; RA3 = 1; RA4 = 1; //turn off display char timer0counter = 0; //count number of times timer0 has interrupted char timer2counterclock = 0; //count number of times timer2 has interrupted char timer2countertimer = 0; //count number of times timer2 has interrupted //button 1 ("mode") state variables (single signal on press) char button1counter = 0; //time counter used for debounce char button1pressed = 0; //boolean, sets to true when the press is detected on port char button1pressedalready = 0; //boolean, sets to true when waiting on deboucning on pressdown (study button handling code to see how this and following state variables are used) char button1waitforrelease = 0; //boolean, sets to true when debouncing is done char button1releasedalready = 0; //boolean, sets to true when waiting on debouncing on release //button 2 ("increase") state variables (continous signals (with delay between them) on hold down) char button2counter = 0; char button2pressed = 0; char button2pressedalready = 0; char button2delay = 35; //amount of delay between each signal (decreasing slowly when button hold down long for faster stepping) //button 3 ("set/start/stop") state variables (same as button 1) char button3counter = 0; char button3pressed = 0; char button3pressedalready = 0; char button3waitforrelease = 0; char button3releasedalready = 0; //earphone state variables char soundcounter = 0; //time counter used for duration of a sound char notecounter = 0; //time counter used for make diffrent long vibrations (notes) char timerstartsound = 0; //boolean, true if sound "Timer start sound" is playing char timerendsound = 0; //boolean, true if sound "Timer end sound" is playing char soundfirsttime = 1; //boolean, keep track of current state in loop //state variables for blinking digits char displayblinkcounter = 0; char displayblinkactivated = 0; char displayblinkactivatedalready = 0; //the timer "time", signed data type to handle negative values (handy when counting down) signed char timerdigit1 = 0; signed char timerdigit2 = 0; signed char timerdigit3 = 0; signed char timerdigit4 = 0; //least significant digit char timerstarted = 0; //boolean, true if timer is running //the clock "time" char clockdigit1 = 0; char clockdigit2 = 0; char clockdigit3 = 0; char clockdigit4 = 0; //least significant digit char clockstarted = 0; //boolean, true if clock is running
char clockminutecounter = 0; //counter used to count interrupts (to 60 seconds) char setclockchoosendigit = 1; //state variable 1-4, selected digit when setting clock char mode = 1; //program states 1-4, see list below /* modes: 1: set clock 2: display clock 3: set timer 4: timer running */ while(1) //program loop, "semi polling" used for program updates, buttons, sounds etc i.e non blocking if (TMR2IF) //check if timer2 has interrupted (every 1/244 sec) TMR2IF = 0; //reset flag //update diffrent counters used everywhere timer2counterclock++; timer2countertimer++; button1counter++; button2counter++; button3counter++; soundcounter++; displayblinkcounter++; //blink display function, toogle a boolean every 1/2 sec if (displayblinkactivatedalready == 0) displayblinkactivatedalready = 1; displayblinkcounter = 0; if (displayblinkcounter >= 122) displayblinkactivatedalready = 0; displayblinkactivated =!displayblinkactivated; // if (timer2counterclock >= 246) //every 1 sec (to fix: Count should be ~244 to simualte 1 second but it triggers slighty too fast, why? Manually adjusted value used for now) if (clockstarted) //is clock running? clockminutecounter++; if (clockminutecounter == 60) //one minute passed yet? clockminutecounter = 0; //update clock "time" clockdigit4++; if (clockdigit4 > 9) clockdigit4 = 0; clockdigit3++; if (clockdigit3 > 5) clockdigit3 = 0; clockdigit2++; if (clockdigit1 == 2 && clockdigit2 > 3) clockdigit2 = 0; clockdigit1 = 0; if (clockdigit2 > 9)
clockdigit2 = 0; clockdigit1++; timer2counterclock = 0; if (timer2countertimer >= 246) //every 1 sec (to fix: Count should be ~244 to simualte 1 second but it triggers slighty too fast, why? Manually adjusted value used for now) if (timerstarted) //is timer running? //update timer "time" timerdigit4--; if (timerdigit4 < 0) timerdigit4 = 9; timerdigit3--; if (timerdigit3 < 0) timerdigit3 = 5; timerdigit2--; if (timerdigit2 < 0) timerdigit2 = 9; timerdigit1--; if (timerdigit1 == 0 && timerdigit2 == 0 && timerdigit3 == 0 && timerdigit4 == 0) //00:00? then make alarm and stop timerendsound = 1; timerstarted = 0; if (mode == 1) //if in "set clock" mode, set clock as of the selection it was (when timer triggerd) and start clock clockstarted = 1; //start clock timer2counterclock = 0; mode = 3; timer2countertimer = 0; if (T0IF) //check if timer0 has interrupted (every 1/2000 sec) T0IF = 0; //rested flag //update counters used everywhere notecounter++; timer0counter++; if (timer0counter >= 2) //every 1/1000 sec timer0counter = 0;
//update display, multiplex digits (light them one at the time) with ~1/1000 sec delay to avoid flicker (must due to shared segment pins on each digit on display) if (mode == 1) //"set clock": blink non selected digits, light colon and selected digit RB5 = 0; //colon if (!displayblinkactivated) if (RA1 == 0) //digit 1 setsegments(clockdigit2); RA1 = 1; RA0 = 0; if (RA0 == 0) //digit 2 setsegments(clockdigit3); RA0 = 1; RA4 = 0; if (RA4 == 0) //digit 3 setsegments(clockdigit4); RA4 = 1; RA3 = 0; if (RA3 == 0) //digit 4 setsegments(clockdigit1); RA3 = 1; RA1 = 0; setsegments(clockdigit1); RA1 = 0; //selected digit should be lit without blinking RA1 = 1; RA0 = 1; RA4 = 1; RA3 = 1; if (setclockchoosendigit == 1) setsegments(clockdigit1); RA1 = 0; if (setclockchoosendigit == 2) setsegments(clockdigit2); RA0 = 0; if (setclockchoosendigit == 3) setsegments(clockdigit3); RA4 = 0; if (setclockchoosendigit == 4) setsegments(clockdigit4); RA3 = 0; if (mode == 2) //"display clock": blink colon, light digits
if (!displayblinkactivated) RB5 = 0; //colon RB5 = 1; if (RA1 == 0) //digit 1 setsegments(clockdigit2); RA1 = 1; RA0 = 0; if (RA0 == 0) //digit 2 setsegments(clockdigit3); RA0 = 1; RA4 = 0; if (RA4 == 0) //digit 3 setsegments(clockdigit4); RA4 = 1; RA3 = 0; if (RA3 == 0) //digit 4 setsegments(clockdigit1); RA3 = 1; RA1 = 0; setsegments(clockdigit1); RA1 = 0; if (mode == 3 mode == 4) //"set timer" and "timer running": light digits and colon, no blinking RB5 = 0; //colon if (RA1 == 0) //digit 1 setsegments(timerdigit2); RA1 = 1; RA0 = 0; if (RA0 == 0) //digit 2 setsegments(timerdigit3); RA0 = 1; RA4 = 0; if (RA4 == 0) //digit 3 setsegments(timerdigit4); RA4 = 1; RA3 = 0; if (RA3 == 0) //digit 4 setsegments(timerdigit1); RA3 = 1; RA1 = 0; setsegments(timerdigit1); RA1 = 0;
//"semi polling" of buttons //button 1 "mode" (single signal on press) if (RA2 == 0) button1pressed = 1; if (button1pressed) if (button1pressedalready == 0) //first time we get here while button1pressed = ture? if (mode == 1) //"set clock": select next digit setclockchoosendigit++; if (setclockchoosendigit > 4) setclockchoosendigit = 1; if (mode == 2) //"display clock": switch to timer if (timerstarted) mode = 4; mode = 3; if (mode == 3 mode == 4) //"set timer" and "timer running": switch to clock mode = 2; button1pressedalready = 1; button1counter = 0; if (button1counter >= 5) //delay ~20ms to debounce button after press button1waitforrelease = 1; if (button1waitforrelease == 1 && RA2 == 1 && button1releasedalready == 0) //released button yet? button1releasedalready = 1; button1counter = 0; if (button1releasedalready == 1 && button1counter >= 5) //delay ~20ms to debounce button after release //button is released and debounced, reset all state variables for next press button1pressedalready = 0; button1pressed = 0; button1waitforrelease = 0; button1releasedalready = 0; //button 2 "increase" (continous signals (with delay between them) on hold down) if (RA5 == 0) button2pressed = 1; button2delay = 35; //delay ~150ms delay between each signal on holddown, reseted to this value if button is released if (button2pressed) if (button2pressedalready == 0) if (mode == 1) //"set clock": increase selected digit by 1
if (setclockchoosendigit == 1) clockdigit1++; if (clockdigit1 == 2 && clockdigit2 > 3) clockdigit1 = 0; if (clockdigit1 > 2) clockdigit1 = 0; if (setclockchoosendigit == 2) clockdigit2++; if (clockdigit1 == 2 && clockdigit2 > 3) clockdigit2 = 0; if (clockdigit2 > 9) clockdigit2 = 0; if (setclockchoosendigit == 3) clockdigit3++; if (clockdigit3 > 5) clockdigit3 = 0; if (setclockchoosendigit == 4) clockdigit4++; if (clockdigit4 > 9) clockdigit4 = 0; //mode 2 "display clock": do nothing time 90:00 if (mode == 3) //"set timer": increase time with 1 second at the timerdigit4++; if (timerdigit4 > 9) timerdigit4 = 0; timerdigit3++; if (timerdigit3 > 5) timerdigit3 = 0; timerdigit2++; if (timerdigit2 > 9) timerdigit2 = 0; timerdigit1++; if (timerdigit1 > 9) //to fix: max number (99:59) goes to timerdigit1 = 9; //mode 4 "timer running": do nothing button2pressedalready = 1; button2counter = 0; if (button2counter >= button2delay) //delay button2pressedalready = 0; button2pressed = 0;
if (RA5 == 0) //button2delay equals amount of delay between each signal (decreasing slowly when button hold down long for faster stepping) button2delay--; if (button2delay < 1) button2delay = 1; //button 3 "set/start/stop" (single signal on press) if (RB0 == 0) button3pressed = 1; if (button3pressed) if (button3pressedalready == 0) //first time we get here when button3pressed = ture? if (mode == 1) //"set clock": set selected time and enter mode 2 clockstarted = 1; //start clock timer2counterclock = 0; mode = 2; //exit "set clock" mode and enter "display clock" mode if (mode == 2) //"display clock": stop clock and enter mode 1 clockstarted = 0; mode = 1; if (mode == 3) //"set timer": start timer and enter mode 4 if (timerdigit1 == 0 && timerdigit2 == 0 && timerdigit3 == 0 && timerdigit4 == 0) //if no time is set (00:00), just play sound and do nothing timerendsound = 1; timerstartsound = 1; timerstarted = 1; timer2countertimer = 0; mode = 4; if (mode == 4) //"timer running": stop timer, reset it and enter mode 3 timerstarted = 0; timerdigit1 = 0; timerdigit2 = 0; timerdigit3 = 0; timerdigit4 = 0; mode = 3; button3pressedalready = 1; button3counter = 0; if (button3counter >= 5) //delay ~20ms to debounce button after press button3waitforrelease = 1; if (button3waitforrelease == 1 && RB0 == 1 && button3releasedalready == 0) //released button yet? button3releasedalready = 1; button3counter = 0; if (button3releasedalready == 1 && button3counter >= 5) //delay ~20ms to debounce button after release //button is released and debounced, reset all state variables for next press
button3pressedalready = 0; button3pressed = 0; button3waitforrelease = 0; button3releasedalready = 0; //"Timer start sound" if (timerstartsound) if (soundfirsttime) soundfirsttime = 0; soundcounter = 0; //start counting time if (soundcounter <= 122) //play half a second if (notecounter >= 2) //bright tune, lower value give faster vibrations (changes on port RA7) notecounter = 0; RA7 =!RA7; soundfirsttime = 1; timerstartsound = 0; //"Timer end sound" if (timerendsound) if (soundfirsttime) soundfirsttime = 0; soundcounter = 0; //start counting time if (soundcounter <= 244) //play 1 second if (notecounter >= 4) //dark tune, higher value give slower vibrations (changes on port RA7) notecounter = 0; RA7 =!RA7; soundfirsttime = 1; timerendsound = 0; void setsegments(char number) //enable segments to form argument number switch (number) //0 = on, 1 = off <segment order: A B C D E F G> case 0: RA6 = 0; RB2 = 0; RB6 = 0; RB3 = 0; RB1 = 0; RB4 = 0; RB7 = 1; break; case 1: RA6 = 1; RB2 = 0; RB6 = 0; RB3 = 1; RB1 = 1; RB4 = 1; RB7 = 1; break; case 2: RA6 = 0; RB2 = 0; RB6 = 1; RB3 = 0; RB1 = 0; RB4 = 1; RB7 = 0; break; case 3: RA6 = 0; RB2 = 0; RB6 = 0; RB3 = 0; RB1 = 1; RB4 = 1; RB7 = 0; break; case 4: RA6 = 1; RB2 = 0; RB6 = 0; RB3 = 1; RB1 = 1; RB4 = 0; RB7 = 0; break; case 5: RA6 = 0; RB2 = 1; RB6 = 0; RB3 = 0; RB1 = 1; RB4 = 0; RB7 = 0; break;
case 6: RA6 = 0; RB2 = 1; RB6 = 0; RB3 = 0; RB1 = 0; RB4 = 0; RB7 = 0; break; case 7: RA6 = 0; RB2 = 0; RB6 = 0; RB3 = 1; RB1 = 1; RB4 = 1; RB7 = 1; break; case 8: RA6 = 0; RB2 = 0; RB6 = 0; RB3 = 0; RB1 = 0; RB4 = 0; RB7 = 0; break; case 9: RA6 = 0; RB2 = 0; RB6 = 0; RB3 = 1; RB1 = 1; RB4 = 0; RB7 = 0; break; default: RA6 = 1; RB2 = 1; RB6 = 1; RB3 = 1; RB1 = 1; RB4 = 1; RB7 = 1; break; //turn off all segments if number!= 0..9