Maskinnära programmering 6B2266
Maskinnära programmering 6B2266
Maskinnära programmering 6B2266
Create successful ePaper yourself
Turn your PDF publications into a flip-book with our unique Google optimized e-Paper software.
Maskinnära <strong>programmering</strong> <strong>6B2266</strong><br />
Häfte: Interna enheter.<br />
Timers, CCP, Interrupt, AD-omvandlare.<br />
Programexempel för 16F84/16F628/16F874.<br />
• Anslutning av extern LCD-display<br />
• Avläsning av rotationspulsgivare<br />
• Interrupt, INT-interruptet, RB-interruptet<br />
• Timer0, Timer1 och Timer2<br />
• CCP-enheterna, Capture Compare (PWM)<br />
• Ex. Tändpunktsförställning hos förbränningsmotor.<br />
• CCP-Capture, frekvensmätning.<br />
• PWM, Pulse Width Modulation, pulsbreddsmodulering<br />
• PWM-lab, Varvtalsratt till LEGO-motorn, varvtal/riktningsratt till LEGOmotorn<br />
• Programmering av TIMER2 som samplingsklocka<br />
• PIC16F874 AD-omvandlare<br />
© 2005 William Sandqvist
LCD-display 16×1 egentligen (2×8)×1<br />
GTC-1601 TR6N0C ELFA 75-511-53<br />
GTC-1601 ser ut som en enkelradig display med 16 tecken,<br />
med består i själva verket av två rader om 8 tecken som placerats efter varandra.<br />
Bekvämt om man ska skriva ut tabeller, men besvärligt om det gäller långa meningar.<br />
( Se programexemplet ).<br />
Detta är ett exempelprogram med ELFA’s billigaste LCD-display (c:a 100:-).<br />
LCD-displayer finns av många olika typer. Kanske köper Du själv en för användning<br />
till din <strong>programmering</strong>suppgift?<br />
De flesta LCD-displayer använder samma styrkrets och därför kan nedanstående<br />
programexempel användas som en utgångspunkt även för andra displayer.<br />
Man måste emellertid vara beredd på att läsa ”manualen” för displayen, och att det<br />
kan behövas göra en del anpassningar av programmet.<br />
LCD-displayernas kontaktstandard<br />
Pin nr. Namn Funktion Pin nr. Namn Funktion<br />
1 V SS Gnd, jord 8 D1 (databit 1)<br />
2 V DD +5V, matningsp. 9 D2 (databit 2)<br />
3 V EE Kontrast 10 D3 (databit 3)<br />
4 RS Char/!Command 11 D4 databit 4<br />
5 RD/!WR Read/!Write 12 D5 databit 5<br />
6 E Enable, clock 13 D6 databit 6<br />
7 D0 (databit 0) 14 D7 databit 7<br />
Att mata spädbarn ...<br />
Att mata spädbarn går lättare när man ger smakbitar (nibble).<br />
Eftersom två smakbitar blir till en munsbit (byte), kan barnet ändå<br />
bli mätt till slut.<br />
När det gäller små PIC-processorer har man ofta ont om pinnar, och man vill då<br />
ogärna ansluta en LCD-display med många ledningar. Alla LCD-displayer har därför<br />
en finess som gör att tecken och kommandon kan "matas in" med 2 st 4-bitarstal<br />
(nibble) i följd, i stället för med ett 8-bitarstal (byte). I så fall räcker det med att man<br />
ansluter 4 dataledningar och 2 styrledningar ( om man avstår från att kunna läsa i<br />
LCD-kretsens register, med RD/!WR ).<br />
Det kommando som "ställer in" om 8 eller 4 dataledningar ska användas måste ges<br />
först. Detta kommando har naturligtvis valts så att 4 bitar räcker för att skilja det från<br />
de övriga kommandona!<br />
3
PIC-processorn har en instruktion för att byta plats på två nibble i en byte<br />
SWAPF f, d. Kompilatorn Cc5x har en inbyggd funktion för samma sak<br />
x = swap(x);. Instruktionen finns med för att underlätta hantering av 4-bitsdata.<br />
I programexemplet nedan plockas i stället nibblen ut "bit för bit". Det går då åt lite<br />
fler instruktioner än om man använder swap, men programmet blir enklare att<br />
anpassa ifall man skulle behöva koppla displayen till andra av PIC-processorns<br />
pinnar.<br />
Koppla så här ...<br />
Kopplingsdäcket med 16x1-display. Även seriekommunikationskretsen får plats. ( Den används inte av<br />
exempelprogrammet).<br />
OBS! Viktigt! Avstörningskondensatorn 470 µF ELFA 67-011-48.<br />
( LCD-displayen drar nämligen en hel del ström … )<br />
4
Testprogram lcd16x1 "Hello world! "<br />
/* lcd16x1.c LCD-display 4 bit data (nibble) conn. to PIC 16F628 */<br />
/* GTC-1601-TR6N0C 1 (2) line 2x8 example: "Hello wo" + "rld! "*/<br />
/* ELFA 75-511-53 c:a 110:- */<br />
/* Internal ic: KS0066U used pins: D4-D7, RS, EN, RD/!WR=0 */<br />
/*<br />
________ ________<br />
| \/ | RD/!WR = 0<br />
D6--|RA2 16F628 RA1|--D5 (only write, wait while busy)<br />
D7--|RA3<br />
RA0|--D4<br />
|RA4 RA7/OSC1|<br />
|RA5/MCLR RA6/OSC2|<br />
+5V--|Vss<br />
Vdd|--GND<br />
|RB0/INT (RB7)/PGD|<br />
|RB1/Rx (RB6)/PGC|<br />
|RB2/Tx<br />
RB5|--RS<br />
EN--|RB3/CCP (RB4)/PGM|<br />
|__________________|<br />
*/<br />
#include "16F628.h"<br />
#pragma config |= 0x3ff0<br />
// use internal 4MHz oscillator<br />
/* I/O-pin definitions */<br />
/* change if you need a pin for a different purpose */<br />
#pragma bit RS @ PORTB.5<br />
#pragma bit EN @ PORTB.3<br />
#pragma bit D7 @ PORTA.3<br />
#pragma bit D6 @ PORTA.2<br />
#pragma bit D5 @ PORTA.1<br />
#pragma bit D4 @ PORTA.0<br />
#include "delays.c"<br />
void delay( char ); // ms delay function from file delays.c<br />
void lcd_init( void );<br />
void lcd_putchar( char );<br />
char text1( char );<br />
char text2( char );<br />
void main( void)<br />
{<br />
CM0=1; CM1=1; CM2=1; // no comparators on PORTA pins<br />
/* I/O-pin direction in/out definitions, change if needed */<br />
TRISB = 0b011.0.1.0.111; // output to RS and EN<br />
TRISA = 0b1111.0000; // output to D4-D7<br />
char i;<br />
lcd_init();<br />
RS = 1; // LCD in character-mode<br />
// display the 8 char text1() sentence<br />
for(i=0; i
* ****************** FUNCTIONS ****************************** */<br />
char text1( char x) // this is the way to store a sentence<br />
{ skip(x); /* internal function CC5x. */<br />
#pragma return[] = "Hello wo" // 8 chars max!<br />
}<br />
char text2( char x) // this is the way to store a sentence<br />
{ skip(x); /* internal function CC5x. */<br />
#pragma return[] = "rld! " // 8 chars max!<br />
}<br />
void lcd_init( void ) // must be run once before using the display<br />
{ delay(40); // give LCD time to settle<br />
/* Function set */<br />
RS = 0; // LCD in command-mode<br />
// 0010.xxxx 4-bit mode<br />
// 0010.xxxx 4-bit mode<br />
lcd_putchar(0b0010.0010);<br />
// 0010.xxxx 4-bit mode three times!<br />
// The display is actual two lines after each other<br />
// 1.0.xxxxxx two line.display off.xxxxxx<br />
lcd_putchar(0b0010.1.0.00);<br />
/* Display ON/OFF Control */<br />
// 0000.xxxx<br />
// 1.1.0.0.xxxx 1.display on.cursor off.blink off.xxxx<br />
lcd_putchar(0b0000.1.1.0.0);<br />
/* Display clear */<br />
// 0000.xxxx<br />
// 0001.xxxx<br />
lcd_putchar(0b0000.0001);<br />
/* Entry mode set */<br />
// 0000.xxxx<br />
// 01.1.0.xxxx 01.increment mode.shift off.xxxx<br />
lcd_putchar(0b0000.01.1.0);<br />
// initialization is done!<br />
}<br />
void lcd_putchar( char data )<br />
{ // must set LCD-mode before calling this function!<br />
// RS = 1 LCD in character-mode<br />
// RS = 0 LCD in command-mode<br />
// upper Nibble<br />
D7 = data.7;<br />
D6 = data.6;<br />
D5 = data.5;<br />
D4 = data.4;<br />
EN = 0;<br />
EN = 1;<br />
delay(5);<br />
// lower Nibble<br />
D7 = data.3;<br />
D6 = data.2;<br />
D5 = data.1;<br />
D4 = data.0;<br />
EN = 0;<br />
EN = 1;<br />
delay(5);<br />
}<br />
6
Lösta programexempel med PIC16F628<br />
Avläsning av pulsgivare<br />
Pulsgivare ELFA 35-847-60<br />
Rotations-Pulsgivare har länge använts som digitala vinkelgivare i industrin, och de<br />
används numera även som inställningsrattar och vred i hemelektronik ( tex. för Jog<br />
up/down ).<br />
De senare typerna har mekaniska kontakter och masstillverkas till låga priser ( det<br />
finns pulsgivare från c:a 20:- ), så det finns all anledning att bekanta sig med<br />
givartypen.<br />
Givaren består av släpkontakter och en kodskiva. Den har tre anslutningar A B och C<br />
(Common). Vid vridning blir det kontakt mellan A-C, eller B-C, eller med både A-C<br />
B-C. Givaren spänningssätts genom att C-kontakten ansluts till jord och A och B -<br />
kontakterna via var sin "pullup"-resistor ( tex 10 kΩ ) till matningsspänningen 5 V.<br />
Utsignalen från givaren är en tvåbitars Gray-kod som för varja "klick" följer en<br />
sekvens bestående av fyra steg.<br />
ClockWise rotation -><br />
00 01 11 10 00<br />
Det finns flera sätt att avläsa pulsgivaren på. Antingen avsöker man givaren så ofta att<br />
man inte riskerar att missa någon tillståndsändring, eller så låter man alla<br />
tillståndsförändringar generera avbrott, och låter avbrottsrutinen avläsa givaren.<br />
PIC-processorer har en speciell finess. Man kan programera dem att generera avbrott<br />
vid varje förändring på någon av fyra fyra portpinnar RB4 ... RB7. Denna finess<br />
används bland annat till datormöss, som ju har två pulsgivare ( för x-led och y-led )<br />
och därmed fyra signaler.<br />
(Observera! man bör undvika att använda PORTB till något annat om man utnyttjar<br />
denna finess).<br />
Vid avläsningen av givaren ser man till att lagra det föregående tillståndet för att<br />
kunna jämföra detta med det nuvarande tillståndet.<br />
Varje pil i tillståndsdiagrammet består av ett sådant tillståndspar old.new.<br />
Ett enkelt sätt att avläsa givaren är att räkna upp positionen vid pilen 00.01 och ned<br />
positionen vid pilen 01.00. Även om kontakten studsar blir "nettoresultatet" det<br />
rätta, eftersom man ju alltid måste gå den ena vägen en gång mer än den andra för att<br />
byta tillstånd.<br />
Enkoderavläsningsprogram<br />
quad_A och quad_B är bitvariabler för de portpinnar A och B signalen anslutits till.<br />
Det avlästa tillståndet och föregående tillstånd samlas i fyra bitar i variabeln trans<br />
( transition, övergång ). Här får man god användning för att kompilatorn Cc5x har<br />
bitoperationer ( som tillägg till ANSI-C ). Cc5x har även binära konstanter och<br />
eftersom det är tillåtet att att avgränsa grupper av bitar med punkt-tecken så blir det<br />
enkelt och tydligt att ange vilkor för de olika pilarna ( transitions ) i<br />
tillståndsdiagrammet, tex. som if( trans == 0b00.01 ).<br />
8
while(1)<br />
{<br />
/* read encoder new value */<br />
trans.0 = quad_A;<br />
trans.1 = quad_B;<br />
}<br />
/* compare with old value */<br />
if( trans == 0b00.01 ) counter ++;<br />
if( trans == 0b01.00 ) counter --;<br />
delay(10);<br />
/* replace old value with new value */<br />
trans.2 = trans.0;<br />
trans.3 = trans.1;<br />
Enkoderavläsning. Testprogrammet quad62x.c<br />
Inkoppling av pulsgivarkontakterna<br />
När man kopplar in kontakter till portpinnar behöver man i allmänhet sk. "pullup"-<br />
motstånd. En sluten kontakt kan ge "0"-nivån, men när kontakten är öppen blir nivån<br />
obestämd utan pullup. PIC-processorerna har inbyggda "pullup"-motstånd ( c:a 10k )<br />
som kopplas in med kommandot:<br />
OPTION.7 = 0; /* internal pullup resistors is on */.<br />
I figuren finns också två 1k serie-resistorer för att "isolera" pulsgivaren från<br />
kretsprogrammeraren. Vid krets<strong>programmering</strong> används ju pinnarna RB6 och RB7<br />
( om vi inte hade haft behov av att omprogrammera PIC-kretsen hade dessa pinnar<br />
varit helt lediga och serieresistorerna onödiga ).<br />
I det följande programmet quad62x.c läses pulsgivaren av med hjälp av PCprogrammet<br />
Hyperterminalen.<br />
Därför tas funktionsfiler som innehåller initserial, putchar, string_out, inttoa och<br />
delay med i arbetsbiblioteket.<br />
9
* quad62x.c read rotary encoder with PC-terminal program */<br />
/* SERIAL COMMUNICATION (RS232) 9600 baud. */<br />
#include "16f628.h"<br />
#include "Math16.h"<br />
#pragma config |= 0x3ff0<br />
char s[11]; /* global string buffer */<br />
/* function prototypes */<br />
void initserial( void ); /* in seriF62x.c */<br />
bit putchar( char ); /* in seriF62x.c */<br />
void string_out( void ); /* in stringIO.c */<br />
void inttoa( int ); /* in ascnum.c */<br />
void delay( char ); /* in delays.c */<br />
#pragma bit quad_A @ PORTB.6<br />
#pragma bit quad_B @ PORTB.7<br />
#include "seriF62x.c"<br />
#include "stringIO.c"<br />
#include "ascnum.c"<br />
#include "delays.c"<br />
void main(void)<br />
{<br />
TRISB.6 = 1; /* quad_A is input */<br />
TRISB.7 = 1; /* quad_B is input */<br />
char trans; /* to store transition */<br />
trans = 0;<br />
int counter;<br />
counter = 0;<br />
delay( 10 );<br />
initserial(); /* Initialise serial port */<br />
putchar('\r');<br />
putchar('\n');<br />
while(1)<br />
{<br />
/* read encoder new value */<br />
trans.0 = quad_A;<br />
trans.1 = quad_B;<br />
/* compare with old value */<br />
if( trans == 0b00.01 ) counter ++;<br />
if( trans == 0b01.00 ) counter --;<br />
/* this is the delay between encoder readings */<br />
inttoa( counter );<br />
string_out();<br />
putchar('\r'); /* Write on the same line! */<br />
}<br />
}<br />
/* replace old value with new value */<br />
trans.2 = trans.0;<br />
trans.3 = trans.1;<br />
Ta med filerna: seriF62x.c, stringIO.c, ascnum.c, delays.c<br />
10
Interrupt, avbrottsprogramering av PIC<br />
midrangekretsarna (ex. med 16F87x)<br />
Interuptlogiken för 16F87x med 15 interuptkällor.<br />
Interruptlogiken för 16F62x med 10 interruptkällor.<br />
Interrupt och Polling<br />
Antag att Du sitter i en skön fåtölj och läser en bok. Plötsligt blir Du avbruten av att<br />
telefonen ringer, Du markerar med en blyertspenna var i boken Du befann dig och<br />
svarar.<br />
Under samtalet ringer det på dörrklockan och Du ber den Du talar med i telefonen<br />
med att dröja kvar medan Du går till dörren. När Du är färdig med ärendet vid dörren<br />
återupptar Du telefonsamtalet. När Du efter ett tag har talat färdigt i telefonen och<br />
avslutat telefonsamtalet kan Du återvända till fåtöljen och fortsätta med att läsa den<br />
goda boken - vid blyertsmärket.<br />
11
Denna scen avser att belysa hur ett huvudprogram ( Du läser en bok ) via en<br />
händelse ( telefonen ringer ) som genererar ett interrupt ( avbrott ) blir avbrutet så att<br />
ett annat program, avbrottsprogrammet, utför en lämplig avbrottsrutin ( Du talar i<br />
telefon ).<br />
Avbrottsprogrammet blir i sin tur avbrutet och får då i stället utföra en annan<br />
avbrottsrutin ( att ta ärendet vid ytterdörren ).<br />
Om interruptmöjligheten inte fanns, om telefonen saknade ringsignal och ytterdörren<br />
saknade dörrklocka, skulle Du gång på gång "oroligt" behöva avbryta läsningen av<br />
boken för att lyfta telefonluren och fråga "är det någon där?", "är det någon där?",<br />
och att öppna ytterdörren för att se efter om någon står utanför och väntar?<br />
Detta kallas för Polling, avsökning.<br />
Det är lätt att inse att Interrupt många gånger är att föredra framför Polling. De allra<br />
flesta mikrostyrkretsar har därför försetts med mer eller mindre utvecklade<br />
mekanismer för interrupt.<br />
Global och Local enable<br />
Om Du vill läsa din bok i lugn och ro utan att bli avbruten kan Du sätta på dig<br />
öronproppar. Telefonen eller dörrklockan kan då ringa hur mycket som helst utan att<br />
bli "betjänade". Du har omöjliggjort interrupt, Disable interrupt. Tar Du bort<br />
öronpropparna har Du åter möjliggjort interrupt, Enable interrupt. Detta kallas för<br />
att på global nivå enable/disable interrupt, Global enable.<br />
Du har även möjlighet att på lokal nivå enabla/disabla interrupt, Local enable. Till<br />
exempel kan Du på lokal nivå disabla telefonen genom att dra ur telefonjacket. Då<br />
kommer Du fortfarande att kunna höra dörrklockan.<br />
( detta målande exempel har jag lånat av en kollega, Robert Meyer )<br />
PIC-processorernas interruptmekanismer<br />
Interruptkällor, globalt, regionalt(?) och lokalt enable<br />
PIC16F87x processorerna har upp till 15 olika interruptkällor. PIC16F628 har tio. De<br />
enablas lokalt med "flaggor" som finns i registren INTCON, PIR1 och PIR2.<br />
Eftersom de alla finns definierade som bitvariabler i respektive processors includefil<br />
så behöver man inte hålla reda på i vilket register och i vilken bitposition de olika<br />
flaggorna finns.<br />
Den lokala enablebiten heter som källan, men med tillägg av bokstaven E på slutet<br />
( tex. external interrupt INT har enableflaggan INTE ).<br />
Interrupt enablas globalt med flaggan GIE ( Global interrupt enable ) som finns i<br />
registret INTCON. Vid start har processorn interrupt disablat.<br />
I INTCON finns även en flagga PEIE ( Peripheral interrupt enable ) som enablar<br />
registren PIE1 och PIE2. Man kanske kan kalla detta för "regionalt enable"?<br />
Observera således att tex CCP1E ( i PIE1 ) sammanlagt enablas på tre ställen.<br />
1) CCP1E=1;, 2) PEIE=1; och 3) GIE=1;.<br />
12
Några interruptkällor<br />
Namn Register Kommentar<br />
INT INTCON External interrupt, RB0/INT-pinnen<br />
TMR0 INTCON TMR0 Overflow interrupt<br />
RBI INTCON interrupt on change PORTB RB.0...RB.3<br />
ADI PIE1 AD-converter interrupt (PIC16F87x)<br />
TXI PIE1 USART Transmitter interrupt<br />
RCI PIE1 USART Reciever interrupt<br />
CCP1I PIE1 CCP1 Capture/Match interrupt<br />
CCP2I PIE2 CCP2 Capture/Match interrupt (PIC16F87x)<br />
TMR2I PIE1 TMR2 PR2 Match interrupt<br />
TMR1I PIE1 TMR1 Overflow interrupt<br />
Interruptrutin<br />
När en interruptkälla, som är enablad, begär interrupt så anropas interruptrutinen.<br />
Denna har alltid en fast adress i programminnet ( 0x0004 ). För att vara säker på att<br />
interruptrutinen får rätt adress skriver man den först, före main():<br />
Med raden interrupt int_server( void ) ger vi interruptrutinen namnet<br />
int_server och definierar den på samma sätt som en vanlig funktion.<br />
#include "16F874.h"<br />
// #include "16F628.h"<br />
#include "int16Cxx.h"<br />
#pragma origin 4<br />
interrupt int_server( void )<br />
{<br />
int_save_registers<br />
/* interrupt routine */<br />
int_restore_registers<br />
}<br />
void main( void )<br />
{<br />
/* the main program */<br />
/* local enable */<br />
/* global enable */<br />
/* something that causes the interrupt */<br />
}<br />
I interruptrutinen gäller automatiskt att fortsatta interrupt är disablade, GIE=0;. ( Om<br />
man skriver GIE=1; inuti interruptrutinen, kan nya händelser även avbryta<br />
interruptrutinen, så gör inte detta ).<br />
Till skillnad mot en vanlig funktion så vet man aldrig var i huvudprogrammet man<br />
befinner sig när interruptet sker och interruptrutinen anropas. Interruptet kanske sker<br />
mitt i ett vilkorsuttryck. När huvudprogrammet sedan får tillbaka kontrollen från<br />
interruptrutinen, och får chansen att utvärdera vilkorsuttrycket, så vill det till att<br />
interuptrutinen inte har förändrat några flaggor eller register!<br />
13
Det är därför viktigt att interruptrutinen börjar med att lagra undan innehållet W-<br />
registret mfl. på säker plats innan rutinen utför något som kan påverka dessa. Det sker<br />
här genom att man includerat filen #include "int16Cxx.h" och skrivit<br />
int_save_registers.<br />
Denna textsträng är ett uttryck som C:s preprocessor "byter ut" mot några, för<br />
processortypen, lämpliga instruktioner. Det är därför viktigt att man anger<br />
processortypen med #include "16F874.h" (#include "16F628.h")<br />
i början av programmet.<br />
Genom att avsluta interruptrutinen med textsträngen<br />
int_restore_registers<br />
så läggs de ursprungliga registerinnehållen tillbaka innan återhoppet till<br />
huvudprogrammet sker.<br />
Väl tillbaka i huvudprogrammet blir interrupt åter enablat.<br />
Interrupt flag bits<br />
Registren INTCON, PIR1 och PIR2 innehåller olika interruptflagbits. Bitarna heter<br />
som källan, men med tillägg av bokstaven F på slutet. Om en interruptflagbit är "1",<br />
så innebär det att motsvarande händelse har inträffat.<br />
Det är lämpligt att börja interruptrutinen med att undersöka vilken händelse det är<br />
som utlöst interruptet, för att sedan styra programflödet till rätt åtgärd. Observera att<br />
man måste 0-ställa aktuell interruptflagbit innan man lämnar interruptrutinen.<br />
Annars kommer man direkt tillbaka till interruptrutinen! ( dvs. programmet låser sig ).<br />
Även när man inte använder interrupt, när interruptet är disablat, kan programmet<br />
ändå avkänna dessa bitar, dvs "polla" bitarna.<br />
Bild på ett "klaffskåp med blänkare", en elektromekanisk signalanordning som förekom i början av<br />
1900 talet i paradvåningar. Från trycknappar i de olika rummen kunde man kalla på<br />
serveringspersonalen eller hembiträdet. Ringklockan i klaffskåpet ringde och motsvarande "blänkare"<br />
för respektive rum föll ned. När uppdraget utförts tryckte betjäningen på knappen under "blänkaren"<br />
så att den återställdes. - Är det månne härifrån Microchip fått idén till sitt interrupt?<br />
14
Lösta programexempel med PIC16F628<br />
INT Interrupt, RB0/INT-pinnen<br />
External interrupt, yttre interrupt på pinnen RB0/INT genereras av en positiv eller<br />
negativ flank ( 0..1 / 1..0 ). Valet av flanktyp gör man med bit INTEDG i OPTIONregistret.<br />
Positiv flank gäller om INTEDG=1; och negativ flank om INTEDG=0;.<br />
Tänds lysdioden när man trycker på knappen så fungerar interruptet ...<br />
( Finns det någon som idag vet vad ”Killroy was here” betyder? )<br />
/* exint62x.c External interrupt turns on light */<br />
#include "16F628.h"<br />
#include "int16Cxx.h"<br />
#pragma config |= 0x3ff0<br />
#pragma origin 4<br />
interrupt int_server( void )<br />
{<br />
int_save_registers<br />
if( INTF == 1 ) /* test if it is the INT-interrupt? */<br />
{ /* this time it's obvius that it is! */<br />
PORTA.2 = 1; /* Lightdiode on to show "Killroy was here" */<br />
INTF = 0; /* Reset INT-flag before leaving */<br />
}<br />
int_restore_registers<br />
}<br />
void main( void )<br />
{<br />
/* the main program */<br />
CM0=1; CM1=1; CM2=1; /* no comparators at PORTA */<br />
TRISB.0 = 1; /* RB0/INT is input */<br />
TRISA.2 = 0; /* RA2 output to lightdiode */<br />
PORTA.2 = 0;<br />
INTEDG = 0; /* interrupt on negative going edge */<br />
INTE = 1; /* local enable */<br />
GIE = 1; /* global enable */<br />
while( 1 ) nop();<br />
}<br />
15
Lösta programexempel med PIC16F628<br />
RB-Interrupt. Interrupt on change RB7,<br />
RB6, RB5, RB4 -pinnarna<br />
Fyra av PORTB's pinnar har den speciella egenskapen att de kan generera interrupt så<br />
fort någon förändring inträffar. Denna funktion är främst tänkt för avkodning av<br />
tangentbord eller för avläsning av datormöss. Det passar också utmärkt för avkodning<br />
av pulsgivare ( se avläsning av pulsgivare ). Man bör inte göra något annat med<br />
PORTB om man valt denna funktion, därför har vi kopplat lysdioden till PORTA i<br />
stället.<br />
( Grundinställningen för PORTA's pinnar är som komparatoringångar. Detta kopplar<br />
man bort med kommandot: CM0=1; CM1=1; CM2=1; ).<br />
Inkoppling av pulsgivarkontakterna<br />
När man kopplar in kontakter till portpinnar behöver man i allmänhet sk. "pullup"-<br />
motstånd. En sluten kontakten kan ge "0"-nivån, men när kontakten är öppen blir<br />
nivån obestämd utan pullup. PIC-processorerna har inbyggda "pullup"-motstånd som<br />
kopplas in med kommandot<br />
OPTION.7 = 0; /* internal pullup resistors is on */.<br />
I figuren fins två 1k serie-resistorer som behövs för att "isolera" pulsgivaren från<br />
kretsprogrammeraren. Vid krets<strong>programmering</strong> används ju pinnarna RB6 och RB7<br />
( om vi inte hade haft behov av att omprogrammera PIC-kretsen hade dessa pinnar<br />
varit helt lediga och serieresistorerna onödiga ).<br />
En lysdiod på portpinne RA2 ska lysa varje gång pulsgivaren står i 0-läget.<br />
Programmet kan kontrolleras genom att man vrider pulsgivaren lika många steg<br />
framåt som bakåt och ger akt på att lysdioden tänds i 0-läget. Eftersom programmet<br />
utnyttjar "interrupt on change" är det inte troligt att processorn "tappar bort"<br />
räkneläget hur snabbt man än roterar pulsgivaren! Pröva!<br />
16
* rbint62x.c RB-int reads rotary switch */<br />
#include "16F628.h"<br />
#include "int16Cxx.h"<br />
#pragma config |= 0x3ff0<br />
int counter; /* global pulsecounter */<br />
char trans; /* global for transitions */<br />
#pragma bit lightdiode @ PORTA.2<br />
#pragma bit quad_A @ PORTB.6<br />
#pragma bit quad_B @ PORTB.7<br />
#define ON 1<br />
#define OFF 0<br />
#pragma origin 4<br />
interrupt int_server( void )<br />
{<br />
int_save_registers<br />
if( RBIF == 1 ) /* is it the RB pin change-interrupt? */<br />
{ /* this time it's obvius that it is! */<br />
/* read encoder new value */<br />
trans.0 = quad_A;<br />
trans.1 = quad_B;<br />
/* compare with old value */<br />
if( trans == 0b00.01 ) counter ++;<br />
if( trans == 0b01.00 ) counter --;<br />
/* replace old value with new value */<br />
trans.2 = trans.0;<br />
trans.3 = trans.1;<br />
RBIF = 0; /* Reset RB-change flag before leaving */<br />
}<br />
int_restore_registers<br />
}<br />
void main( void )<br />
{<br />
/* the main program */<br />
CM0=1; CM1=1; CM2=1; /* no comparators on PORTA */<br />
OPTION.7 = 0; /* internal pullup resistors on */<br />
TRISA.2 = 0; /* PA2 is output to lightdiode */<br />
PORTA.2 = 0;<br />
TRISB = 0b11000000; /* only inputs quad_A and quad_B */<br />
trans = 0;<br />
counter = 0;<br />
RBIE = 1; /* local enable */<br />
GIE = 1; /* global enable */<br />
}<br />
while( 1 )<br />
{<br />
if ( counter == 0 ) lightdiode = ON;<br />
else lightdiode = OFF;<br />
}<br />
17
Timers<br />
Timer0<br />
Timer0 är den timer som alla PIC-kretsar har. Det är en 8-bitarsräknare.<br />
Den kan programmeras att räkna pulser från T0CKI-pinnen, eller de interna<br />
klockpulserna f osc /4.<br />
Den kan förses med en prescaler så att den räknar långsammare.<br />
När Timer0 "räknar runt" sätts en flagga T0IF, den kan även programmeras att då<br />
orsaka programavbrott ( de enklaste PIC-kretsarna saknar avbrottsmöjligheten ).<br />
Register TMR0 kan läsas och skrivas från programmet. Timer0 programmeras med<br />
OPTION-registret.<br />
OPTION-registret<br />
18
Timer1<br />
Timer1 är en 16-bitarsräknare. Den kan antingen räkna pulser från T1CKI-pinnen,<br />
räkna de interna klockpulserna f osc /4, eller räkna klockpulser från en egen<br />
kristalloscillator T1OSC. Det sista alternativet gäller användning med lågfrekventa<br />
klock-kristaller 32,7680 kHz som ger hög noggrannhet med låg strömförbrukning.<br />
Den egna kristalloscillatorn kan synkroniseras med processorns klocka ( Synk ). Om<br />
den inte synkroniseras ( Asynk ), så kan den fortsätta att gå även om processorn<br />
försätts i lågeffektläge, s.k. SLEEP-mode.<br />
En programmerbar prescaler ger tillsammans med räknarens 16 bitar Timer1 ett<br />
omfattande arbetsområde. När Timer1 "räknar runt" sätts en flagga TMR1IF, den kan<br />
även programmeras att då orsaka programavbrott.<br />
Register TMR1H och TMR1L kan läsas eller skrivas, ett i taget, från programmet.<br />
Detta kräver speciell teknik. Om Timer1 används i samverkan med CCP-enheterna får<br />
man tillgång till extra register som underlättar läsning/skrivning.<br />
Timer1 programmeras med T1CON.<br />
Register T1CON Timer1 control register<br />
b7 b6 b5 b4 b3 b2 b1 b0<br />
- - T1CKPS1 T1CKPS0 T1OSCEN !T1SYNK TMR1CS TMR1ON<br />
Timer1 input ClocK Prescale Select, T1CKPS: T1CKPS1 T1CKPS1<br />
11 = 1/8, 10 = 1/4, 01 = 1/2, 00 = 1/1<br />
Timer1 OSCillator Enable, T1OSCEN: T1OSCEN<br />
1 = oscillator enable, 0 = oscillator shut-off<br />
Timer1 external clock input SYNChronisation, T1SYNC: !T1SYNC<br />
1 = no synchronisation, 0 = synchronisation<br />
TiMeR1 Clock Source select bit, TMR1CS: TMR1CS<br />
1 = External clock RC0/T1OSO/T1CKI, 0 = internal clock f osc /4<br />
TiMeR1 On bit, TMR1ON: TMR1ON<br />
1 = enables Timer1, 0 = stops Timer1<br />
19
Timer2<br />
Timer2 är 8-bitarsräknare som främst används tillsammans med CCP-enheterna för att<br />
generera PWM-signaler. Om Timer2 inte behövs till PWM-generering kan den<br />
användas som intern "klocka" som med jämna mellanrum sätter en flagga TMR2IF. I<br />
samband med detta kan den programmeras att orsaka programavbrott. ( Observera att<br />
Timer2:s utgång inte är ansluten till någon yttre pinne ).<br />
Timer2 räknar tills den når det värde som står i periodregistret PR2 då den 0-ställs<br />
och börjar om. Timer2 använder klockpulser från f osc /4, men eftersom den både har en<br />
Prescaler och en Postscaler blir inställningsområdet stort ( lika stort som för Timer1<br />
16-bitsräknaren ).<br />
Timer2 ställs in med T2CON-registret. TMR2 kan läsas och skrivas från programmet.<br />
Hela Timer2 kan stängas av om den inte används, så att man minskar<br />
strömförbrukningen.<br />
Register T2CON Timer2 control register<br />
b7 b6 b5 b4 b3 b2 b1 b0<br />
- TOUTPS3 TOUTPS2 TOUTPS1 TOUTPS0 TMR2ON T2CKPS1 T2CKPS0<br />
Timer2 OUTput Postscale Select bits, TOUTPS:<br />
TOUTPS3 TOUTPS2 TOUTPS1 TOUTPS0<br />
0000 1/1 0100 1/5 1000 1/9 1100 1/13<br />
0001 1/2 0101 1/6 1001 1/10 1101 1/14<br />
0010 1/3 0110 1/7 1010 1/11 1110 1/15<br />
0011 1/4 0111 1/8 1011 1/12 1111 1/16<br />
Timer2 ON bit, TMR2ON: TMR2ON<br />
1 = Timer2 on, 0 = Timer2 off<br />
Timer2 ClocK Prescale Select bits, T2CKPS: T2CKPS1 T2CKPS0<br />
00 = 1/1, 01 = 1/4, 1x = 1/16<br />
20
Hur läser/skriver man till 16-bit<br />
"free running" Timer1?<br />
Timer1 är en 16-bitarsräknare. Den måste läsas av som två 8-bitarstal, de 8 mest<br />
signifikanta bitarna TMR1H och de 8 minst signifikana bitarna TMR1L. Detta kan vara<br />
ett problem eftersom Timern kan "slå runt" mellan avläsningarna av 8-bits talen.<br />
Följande kod visar det säkra sättet<br />
( men eventuellt interrupt måste vara avstängt under avläsningen):<br />
long unsigned int time;<br />
unsigned char TEMPH;<br />
unsigned char TEMPL;<br />
TEMPH = TMR1H;<br />
TEMPL = TMR1L;<br />
if (TEMPH == TMR1H) /* Timer1 has not rolled over = good value */<br />
{<br />
time = TEMPH*256;<br />
time += TEMPL;<br />
}<br />
else /* Timer1 has rolled over - no new rollover for some time */<br />
/* read a new good value */<br />
{<br />
time = TMR1H*256;<br />
time += TMR1L;<br />
}<br />
Det kan också vara problematiskt att skriva till en 16-bitarsräknare eftersom det måste<br />
ske som två 8-bitarstal. Detta är det säkra sättet (Interrupt måste vara avstängt under<br />
skrivningen):<br />
TMR1L = 0; /* clear low byte = no rollover for some time */<br />
TMR1H = 12345/256; /* high byte of constant 12345 */<br />
TMR1L = 12345%256; /* low byte of constant 12345 */<br />
Observera att Timer1 ofta används tillsammans med en CCP-enhet. Vid Capture läses<br />
Timer1 av på ett säkert sätt av hårdvaran och resultatet hamnar i registerparet CCPRH<br />
och CCPRL.<br />
long unsigned int time;<br />
time = CCPR1H*256 + CCPR1L<br />
21
CCP-enheterna,<br />
Capture/Compare/(PWM)<br />
PIC-processorena kan vara utrustade med en eller flera CCP-enheter<br />
(Capture/Compare/PWM). Varje CCP innehåller ett 16-bitarsregister ( 2×8 bit,<br />
CCPRH och CCPRL ) som kan användas som ett 16-bitars Capture-register, ett 16-<br />
bitars Compare-register, eller som ett 10-bitars PWM Master/Slave DutyCycleregister.<br />
CCP-enheten innehåller också en 16-bitars komparator. Varje CCP-enhet har<br />
en portpinne, CCP-pinnen, för anslutning till omvärlden.<br />
Man ställer in CCP-enheternas arbetssätt med CCPCON-register. Eftersom CCPenheterna<br />
antingen samverkar med Timer1 eller Timer2 måste man även göra timerinställningar.<br />
CCP-pinnen måste ställas in som ingång eller utgång med aktuellt TRIS-register.<br />
PIC16F628 har en CCP-enhet CCP1 ( pinne 9, RB3/CCP1 TRISB.3 ). Register<br />
CCP1CON, CCPR1H och CCPR1L.<br />
Register CCP1CON<br />
b7 b6 b5 b4 b3 b2 b1 b0<br />
- - CCP1X CCP1Y CCP1M3 CCP1M2 CCP1M1 CCP1M0<br />
CCP1X CCP1Y PWM Minst signifikanta bitar<br />
22
CCP1M3 CCP1M2 CCP1M1 CCP1M0 CCP mode select bits<br />
M3 M2<br />
M3 M2<br />
mode-Beskrivning<br />
M1 M0<br />
M1 M0<br />
mode-Beskrivning<br />
0000 Reset av CCP-modulen 1000 Compare, 1-ställ CCP-pinne, CCPIF-flagga<br />
Capture, varje neg flank,<br />
0100<br />
CCPIF-flagga<br />
1001 Compare, 0-ställ CCP-pinne, CCPIF-flagga<br />
Capture, var pos flank, CCPIFflagga<br />
0101<br />
1010 Compare, CCPIF-flagga<br />
Capture, var 4:e pos flank,<br />
Compare, speciell händelse, CCPIF-flagga<br />
0110<br />
CCPIF-flagga<br />
1011<br />
CCP1: 0-ställ Timer1<br />
Capture, var 16:e pos flank,<br />
0111<br />
CCPIF-flagga<br />
11xx PWM-mode<br />
CCPIF-flaggan ( CCP1IF ) kan användas till att orsaka avbrott, interrupt. Observera<br />
att flaggorna måste 0-ställas av programmet, att de inte 0-ställs automatiskt när de<br />
läses!<br />
Capture mode<br />
Capture innebär att Timer1:s ögonblicksvärde 16-bitar kopieras över till de två 8-<br />
bitarsregistren CCPR1H och CCPR1L, när en förutbestämd "händelse" inträffar på<br />
CCP1 - pinnen (pinne 9). De två 8-bitarstalen kan sammansättas till en 16-<br />
bitarsvariabel:<br />
unsigned long time;<br />
time = CCPR1H*256 ;<br />
time += CCPR1L ;<br />
Vid Capture sätts CCP1IF -flaggan i PIR1 -registret. Det går att programmera så att<br />
avbrott, Interrupt, utlöses av detta.<br />
• Ställ in Timer1, internal/external, prescaler, synk/asynk, on/off.<br />
• CCP1-mode.<br />
• TRISB.3 = 1;<br />
23
Compare mode<br />
Compare innebär att ett 16-bitars tal i CCPR1 -registren kontinuerligt jämförs med<br />
Timer1:s räknevärde. När överenstämmelse sker blir CCP1-pinnen<br />
hög/låg/oförändrad beroende på inställningen i CCP1CON.<br />
Överenstämmelsen kan programmeras att dessutom utlösa avbrott, interrupt.<br />
Förutom påverkan på CCP-pinnen, finns det möjlighet att välja en så kallad "speciell<br />
händelse" (special event). För CCP1 innebär denna 0-ställning av Timer1.<br />
Ställ in Timer1, internal/external, prescaler, synk/asynk, on/off.<br />
• CCP1-mode.<br />
• TRISB.3 = 0;<br />
Timer1, valbar period<br />
CCP-enheten kan 0-ställa Timer1 med special event, vid det tal som står i<br />
registerparet CCPR. Timer1:s periodtid förkortas (men inget avbrott sker<br />
/* fosc = 4 MHz, Timer1Period = 0,001 s (1000 Hz) */<br />
T1CON = 0b00.00.0.1.0.1;<br />
/* --.presc=1/1.osc=shut off.no sync.internal clk.Enable timer */<br />
/* CCPR = (fosc/4) * Timer1Period = 1000000*0,001 = 1000 */<br />
/* CCPR = 1000 = 0x3E8 CCPR1H = 0x3, CCPR1L = 0xE8 */<br />
CCPR1H = 0x3;<br />
CCPR1L = 0xE8;<br />
CCP1CON = 0b00.00.1011; /* --. 00. special event */<br />
/* Timer1Period is now 1 ms */<br />
24
Så här skulle PIC-kretsarnas CCP-enheter kunna<br />
användas i en bilmotor … ( exempel med PIC16F87x )<br />
Elektronisk styrning av<br />
förbränningsmotorer<br />
De flesta bilmotorer ( och många andra förbränningsmotorer ) är i dag elektroniskt<br />
styrda. Detta har gjort underverk med bilarna, men samtidigt har verkstäderna förlorat<br />
kontrollen över justeringen av dessa bilar, även märkesverkstäder kan ha problem<br />
med att justera det enklaste tomgångsproblem.<br />
Intresset för att lära sig hur motorstyrningarna egentligen fungerar är därför stort. På<br />
IT-universitetets Mekatronik-inriktning har genom åren flera projekt handlat om detta.<br />
Några teknologer har efter projekten kunnat söka sig till motorindustrin för liknande<br />
arbetsupgifter.<br />
De mikrostyrkretsar ( PIC16F87x ) som används i Mekatronik-inriktningens projekt<br />
har sitt ursprung i kretsar för bilindustrin, och det kan därför vara intressant att se<br />
hur de ingående enheterna kan användas för att lösa motorstyrningsuppgifter.<br />
Behovet av tändförställning<br />
När bränsle/luftblandningen antänds i kompressionskammaren, börjar<br />
förbränningen vid tändstiftet och sprids därifrån vidare i<br />
bränsle/luftblandningen. Det tar en given tid för hela blandningen att brinna,<br />
expandera och därmed tvinga kolven ner i loppet. Det är därför man måste<br />
börja tändningsprocessen ( Z ) innan kolven når övre dödläge (ÖD). Det är<br />
detta som kallas för tändförställning.<br />
Om motorns varvtal ökar så får blandningen kortare tid på sig att brinna.<br />
Man behöver därför öka tändförställningen med ökat varvtal.<br />
Nu är det så att den hastighet med vilken bränsle/luftblandningen brinner<br />
varierar med hur full cylindern är vid kompressionen ( cylinderfyllning ).<br />
Vid litet gaspådrag och höga varvtal fylls cylindern mindre än vid stort<br />
"gaspådrag" och låga varvtal.<br />
Det behövs således även olika tändförställningar vid samma varvtal<br />
beroende på "gaspådrag" ( egentligen menar vi motorbelastning i stället för<br />
"gaspådrag" ).<br />
• Z = f ( varvtal, motorbelastning )<br />
25
Givare för vinkel, varvtal, motorbelastning<br />
Vinkelläge<br />
Fordonsindustrin har hittat ett mycket billigt sätt<br />
att mäta motoraxelns vinkelläge. En induktiv<br />
givare avger pulser när motorns startkuggkrans<br />
roterar, och en annan induktiv givare avger en<br />
puls per varv då ett "indexstift" passerar. (<br />
Tekniken beskrivs i boken Mätgivare ).<br />
Antag att startkransen har 130 kuggar. För att<br />
grovt hålla reda på vinkelläget "räknar" man i<br />
princip de kuggar som passerar med en modulo-<br />
130-räknare.<br />
Mikrostyrkretsen PIC6F87x har tre interna räknare ( Timer0, Timer1 och Timer2).<br />
Timer0 är lämplig till denna uppgift, det är en åttabitarsräknare, dvs en modulo-256-<br />
räknare som "slår runt" vid talet 255 och då börjar om med talet 0. Timer0 kan<br />
programmeras att räkna pulser som inkommer på pinne T0CKI. Kugg-givaren ansluts<br />
till denna pinne.<br />
Man kan få ett avbrott (interrupt) i programkörningen var gång Timer0 "slår runt".<br />
Om man vid avbrotten passar på att direkt "ladda" Timer0 med talet 126<br />
(256-130=126) blir räknecykeln förkortad till 130 steg, dvs vi får en<br />
modulo-130-räknare. Aktuell kugge kan man få reda på genom att läsa av Timer0 och<br />
dra ifrån talet 126.<br />
Antag att indexstiftet är placerat vid kugge nr "0". Mikrostyrkretsen PIC6F87x har en<br />
yttre avbrottsingång INT. Indexpinnens givare ansluts till denna. Vid start av motorn<br />
"laddar" man Timer0 med talet 126 när index-pinnen passerar sin givare och<br />
genererar avbrott. Från denna stund håller Timer0 "reda på" kuggarna.<br />
26
Varvtal<br />
Varvtalsmätningen går i princip till så att man mäter den tid det tar för kuggarna att<br />
passera Kugg-givaren. Rotationen sker inte med konstant varvtal utan "ryckigt",<br />
eftersom förbränningsmotorer är explosionsmotorer.<br />
PIC-processorerna kan vara utrustade med en eller flera så kallade CCP-enheter<br />
(CaptureComparePWM). För tidmätning låter man CCP-enheten vara inställd på<br />
Capture, och använder Timer1. Timer1 är en 16-bitsräknare (2×8 bitar), den<br />
programmeras att räkna i takt med processorns klocka. CCP-enhetens ingångspinne<br />
ansluts till Kugg-givaren och programmeras att reagera på positiva pulsflanker. Vid<br />
varje kugge kommer då automatiskt Timer1 att "läsas av" ( = capture) och resultatet<br />
hamnar i CCP-enhetens register CCPRH och CCPRL. De två CCPR-registren kan<br />
hanteras som ett 16-bitarstal. Vid "Capture" anropas avbrottsrutinen ( interrupt ) och i<br />
denna kan tidsvärdet i CCPR-registren lagras undan. I princip beräknas tiden mellan<br />
två kuggpassager som skillnaden mellan två tidsvärden, och varvtalet som det<br />
inverterade värdet av denna tid.<br />
Motorbelastning<br />
Tryckgivare, varmtråds-massflödesmätare, trottelpotentiometer.<br />
Motorns belastning kan mätas på många olika sätt. Den klassiska metoden för<br />
förgasarmotorer är att mäta undertrycket i förgasaren med en tryckgivare. En annan<br />
metod är att mäta luftflödet till motorn med en varmtråds-massflödesmätare. Ett<br />
ungefärligt mått på motorns belastning kan man få genom att med en trottelpotentiometer<br />
mäta vilket "gaspådrag" föraren ger.<br />
( Givarna beskrivs i boken Mätgivare ).<br />
27
Dessa givare har alla en analog spänning som utstorhet, och ansluts därför enklast till<br />
PIC-processorernas inbyggda AD-omvandlare. ( PIC16F87x har AD-omvandlaren ).<br />
Tändförställningstabell<br />
Sambandet mellan tändpunktens förställning och varvtalet<br />
tillsammans med belastningen är komplicerat och beskrivs bäst<br />
som en "karta" ( mappad tändning ).<br />
Rent experimentellt kan man placera fordonet på en så kallad<br />
"rullande landsväg" och systematiskt ändra varvtal och<br />
belastning och för varje kombination "pröva fram" den bästa<br />
tändförställningen. Mätvärdena kan man samla i en<br />
Tändförställningstabell.<br />
Med Matlab kan man visualisera tändförställningstabellen.<br />
3-D plot med Matlab. (Klipp ut texten och prova den i Matlabs kommandofönster).<br />
Observera att tabellens värden är "fejkade" och är endast avsedda som principexempel.<br />
ignadv = [ 30 40 40 40 20 15 15 15<br />
20 40 40 40 25 20 20 20<br />
40 60 60 50 30 20 20 15<br />
40 70 70 60 40 25 25 20<br />
45 80 85 80 50 40 30 25<br />
50 85 85 70 60 50 35 30<br />
60 90 90 80 75 60 40 35<br />
70 95 95 80 75 65 50 40 ];<br />
surf(ignadv);<br />
28
Tabellfunktion i PIC-processorn.<br />
Så här kan tabellen programmeras som en funktion i PIC-processorn. När funktionen<br />
anropas med tex. ignitionmap(0,0) (rad 0 kolumn 0) returnerar den talet 30.<br />
/* speed 3 bits 0...7 ; torque 3 bits 0...7 */<br />
char ignitionmap(char speed, char torque)<br />
{<br />
char index;<br />
index = speed*8 + torque; /* index for 64 table entries */<br />
skip(index)<br />
return 30;<br />
return 40;<br />
... /* the next 60 table entries */<br />
...<br />
return 50;<br />
return 40;<br />
}<br />
Tändningen<br />
Med hjälp av Timer0 vet man motorns vinkel. När man närmar sig tändpunkten mäter<br />
man tiden mellan passagen av två kuggar med den första CCP-enhetens Capturefunktion,<br />
och motorns belastning med hjälp av AD-omvandlaren. Genom att anropa<br />
tabellfunktionen så får man veta vilken tändförställning som är den rätta.<br />
För att utföra tändningen vid rätt tidpunkt låter man PIC-processorns andra CCPenhet<br />
vara inställd på Compare.<br />
När motorn nått den kugge som innehåller intervallet för tändningen, laddar man<br />
CCP-registren med ett tal, en tidsfördröjning. En programmerbar "händelse" utförs<br />
med CCP-pinnen då Timer1 når ( compare ) detta tal. Tändspolens drivkrets har<br />
anslutits till CCP-pinnen på ett sådant sätt så att den programmerade "händelsen"<br />
utlöser gnistan.<br />
CCP-enheten är så konstruerad att "händelsen" åtföljs av att Timer1 0-ställs ( och i fallet med CCPenhet<br />
2 att AD-omvandlaren startas ). Ett anrop till avbrottsrutinen ( interrupt ) kan ske nu om så<br />
önskas.<br />
29
Lösta programexempel med PIC16F628<br />
Indirekt frekvensmätning, periodtid,<br />
multiperiodtid<br />
Frekvensmätning sker ofta indirekt som periodtidsmätning ( T ), varefter frekvensen<br />
( f ) beräknas som f = 1/T. Periodtiden är tiden mellan två lika flanker hos<br />
mätsignalen. Frekvensmätning kan ske inom ett brett intervall eftersom Timer1 har en<br />
Prescaler och det dessutom är möjligt att programmera mätning av, varje eller var<br />
fjärde eller var åttonde flank (multiperiodmätning).<br />
Vi antar att f osc = 4 MHz, och att Timer1 räknar T1CK = f osc /4 = 1 MHz.<br />
Timer1-ticket blir då 1 µs och Timer1-perioden 2 16 ×1µs = 66 ms ( 15,3 Hz ).<br />
Om en 100 kHz-signal ( med periodtiden 10 µs ) inkommer på CCP-pinnen hinner det<br />
gå 10 Timer1-tick mellan två positiva flanker. Om det i stället är en 10 kHz-signal går<br />
det 100 Timer1-tick mellan två flanker, och vid en 1 kHz-signal blir det 1000<br />
Timer1-tick. Vid 100 Hz blir det 10000 Timer1-tick.<br />
Är frekvensen 10 Hz hinner Timer1 räkna mer än ett helt varv mellan två flanker.<br />
Vid höga frekvenser som tex en 100 kHz-signalen kan man öka mätupplösningen<br />
genom att bara registrera var åttonde flank ( det går då 80 Timer1-tick mellan de<br />
registrerade flankerna). Vid låga frekvenser som 10 Hz-signalen måste man antingen<br />
hålla räkning på hur många gånger Timer1 räknar runt, eller låta Timer1 räkna<br />
långsammare genom att använda prescalern, tex. 1/8.<br />
30
Programexempel frekvensmätning med 16F628<br />
/* fmeas62x.c frequency measurement for 16F628 */<br />
/* input frequency 100 Hz - 10 kHz at CCP1-pin */<br />
#include "16f628.h"<br />
#include "math16.h"<br />
#pragma config |= 0x3ff1<br />
/* function prototypes */<br />
void unslongtoa( unsigned long ); /* in ascnum.c */<br />
void initserial(); /* in seriF62x.c */<br />
bit putchar( char ); /* in seriF62x.c */<br />
void delay( char ); /* in delays.c */<br />
void string_out( void ); /* in stringIO.c */<br />
char text1( char ); /* function prototype */<br />
char s[11]; /* Global char array for digits */<br />
#include "seriF62x.c"<br />
#include "ascnum.c"<br />
#include "stringIO.c"<br />
#include "delays.c"<br />
void main(void)<br />
{<br />
char i;<br />
uns16 T, f, t1, t2;<br />
initserial();<br />
TRISB.3 = 1; /* CCP1-pin is input */<br />
/* Settings */<br />
/* "--"."Prescale 1/1"."osc shut off"."synchr"."fosc/4"."on" */<br />
T1CON = 0b00.00.0.0.0.1 ;<br />
/* "--"."--"."Capture each pos edge" */<br />
CCP1CON = 0b00.00.0101 ;<br />
while(1)<br />
{<br />
CCP1IF = 0 ; /* reset flag */<br />
while (CCP1IF == 0 ); /* wait for capture */<br />
t1 = CCPR1H*256;<br />
t1 += CCPR1L;<br />
CCP1IF = 0 ; /* reset flag */<br />
while (CCP1IF == 0 ); /* wait for next capture */<br />
t2 = CCPR1H*256;<br />
t2 += CCPR1L;<br />
/* Calculations */<br />
T = t2 - t1; /* calculate period */<br />
f = 1000000/T; /* calculate frequency */<br />
31
* print textstring text1 */<br />
for(i = 0; putchar( text1(i)); i++) ;<br />
/* print variable f */<br />
unslongtoa(f);<br />
string_out();<br />
putchar('\r'); putchar('\n');<br />
}<br />
}<br />
delay( 10 );<br />
/* String to write */<br />
char text1( char x)<br />
{<br />
skip(x);<br />
#pragma return[] = "The frequency is [Hz]: " '\0'<br />
}<br />
Hjälpfunktioner finns i filerna seriF62x.c, ascnum.c,<br />
stringIO.c, delays.c<br />
Programmet utnyttjar funktioner som ligger i andra filer och som därför måste tas<br />
med i arbetsbiblioteket.<br />
SeriF62x.c ( funktionsprototyp seriF62x.h )<br />
ascnum.c ( funktionsprototyp ascnum.h )<br />
stringIO.c ( funktionsprototyp stringIO.h )<br />
delays.c ( funktionsprototyp delays.h )<br />
Förmodligen har Du ingen tongenerator hemma. Den som vill pröva<br />
frekvesmätningsprogrammet kan koppla frekvensdelarkretsen 74HC4040’s<br />
klockingång till PIC-processorns RA6/CLKOUT (1MHz). På 4040-kretsens ben<br />
återfinner man 1 MHz neddelat steg för steg, dvs. frekvenserna 500 kHz, 250 kHz,<br />
125kHz, … 244Hz. Vid labtillfället i skolan finns en tongenerator och FLUKE<br />
scopemeter-123.<br />
32
PWM, Pulse Width Modulation<br />
Medan mikrostyrkretsar ofta har inbyggda AD-omvandlare, är det mer sällan som de<br />
också har DA-omvandlare. Anledningen till detta är att det finns en genväg att digitalt<br />
framställa analoga signaler - PWM, Pulse Width Modulation, pulsbreddsmodulering.<br />
Pulsbreddsmodulerad spänning, många komponenter märker bara det analoga medelvärdet.<br />
En pulsbreddsmodulerad signal består av en snabb följd av pulser. Många<br />
komponenter är "tröga" och hinner inte uppfatta att signalen ändrar sig, utan reagerar<br />
på signalens medelvärde som om det vore en konstant spänning (medelspänningen).<br />
Det gäller tex motorer, glödlampor och värmeelement. Har man snabba komponenter<br />
som skulle kunna upptäcka variationerna så filtrerar man PWM-signalen för att få ett<br />
konstant värde.<br />
PWM-signalen har fast frekvens och periodtid, medan pulstiden ( t 1 ) varieras på<br />
pulsluckans ( t 0 ) bekostnad. Signalens DutyCycle = t 1 / ( t 1 + t 0 ) är direkt<br />
proportionell mot det analoga medelvärdet.<br />
Hur programmerar man en PWM-signal?<br />
( ett avskräckande exempel utan PWM-enhet )<br />
I detta exempel låter vi lysdioden styras med en PWM-signal så att den lyser med<br />
75% intensitet ( medelvärde ) då strömställaren är "1" och med 25% intensitet då<br />
strömställaren är "0".<br />
33
Alla PIC-processorer har åtminstone en inbyggd räknare, Timer0. Timer0 är en 8-<br />
bitsräknare, den räknar således hela tiden från 0 till 255 då den slår runt och börjar<br />
från 0 igen. Med OPTION - registret väljer man räknehastigheten.<br />
Med ett tal Duty mellan 0 och 255 styr man PWM-signalen. Så länge Timer0 räknar<br />
under talet Duty låter man PWM-utgångspinnen vara "1", för att 0-ställa den så fort<br />
Timer0 räknat förbi talet Duty.<br />
Eftersom programmet hela tiden ( åtminstone varje Timer0-tick ) måste "läsa av"<br />
Timer0 får man inte så mycket processortid över för annat med denna metod ( ju<br />
högre PWM-frekvens, desto mindre tid till övriga uppgifter ).<br />
Många PIC-processorer har inbyggda "PWM-enheter". De kan självständigt generera<br />
PWM-signaler utan allt för stor inblandning från "huvudprogrammet".<br />
PWM-signal med Timer0<br />
/*<br />
pwmpic.c Timer0-PWM-signal ( Bad example! )<br />
===========================================<br />
*/<br />
#include "16F628.h"<br />
#pragma config |= 0x3ff0<br />
#define ON 1<br />
#define OFF 0<br />
#pragma bit PWM_out @ PORTB.0<br />
#pragma bit Contact @ PORTB.1<br />
char MoreToDo(void);<br />
char Duty; /* global variable */<br />
void main( void)<br />
{<br />
Duty = 0; /* start with zero DutyCycle */<br />
TRISB = 0b11111110 ; /* PORTB.0 is output */<br />
/* PORTB.1 is input */<br />
OPTION = 0b11000111; /* Timer0 Prescaler divide by 256 */<br />
while (1) /* forever */<br />
{<br />
char i;<br />
if (TMR0 < Duty) PWM_out = 1;<br />
else PWM_out = 0;<br />
Duty = MoreToDo(); /* Be back in one TIMER0-tick ! */<br />
}<br />
}<br />
34
* Function*/<br />
char MoreToDo(void)<br />
{<br />
char Duty;<br />
if( Contact == ON ) Duty = 191; /* 191/255 = 75% Dutycycle */<br />
/* _______ __ */<br />
/* _| |__| */<br />
/* */<br />
else Duty = 64; /* 64/255 = 25% Dutycycle */<br />
/* __ __ */<br />
/* _| |_______| */<br />
/* */<br />
return Duty; /* Be back in one TIMER0-tick! */<br />
}<br />
I exemplet ovan antar vi att PIC-processorn har klockfrekvensen 4 MHz och att<br />
Timer0 använder prescalerns /256 och därför räknar med frekvensen ( f osc /4 )/256 =<br />
10 6 /256 = 3906 Hz, tiden mellan två Timer0-tick blir 0,25 ms.<br />
PWM-signalens periodtid blir (1/3906)×256 = 66 ms. PWM-signalens frekvens blir<br />
därför låg, 15 Hz.<br />
Denna signal duger till ett värmeelement, medan en glödlampa riskerar att "flimra"<br />
störande.<br />
Om Timer0 inte använder prescalern blir PWM-frekvensen 256 ggr högre dvs 3900<br />
Hz, men då blir utrymmet för andra programaktiviteter än själva genereringen av<br />
PWM-signalen obefintliga.<br />
Ofta vill man ha PWM-frekvenser över 20000 Hz så att man undviker att vibrationer i<br />
utrustningen blir "hörbara" (för människor).<br />
Även om det går att programmera en PWM-signal direkt, så finns det således ett stort<br />
behov av att i stället kunna använda inbyggda "PWM-enheter" som kan avlasta<br />
processorn.<br />
( Microchip visar i sin application note AN654 på bättre sätt att programmera PWM<br />
med Timer0 genom att utnyttja avbrott då timern "slår runt" ).<br />
35
PWM med CCP-enheter<br />
( CaptureComparePWM )<br />
CCP-enheterna kan användas till att generera PWM-signaler. Detta sker i samverkan<br />
med Timer2.<br />
Timer2 är en 8-bitars räknare, vid PWM kompletteras den internt i processorn med 2<br />
bitar till en 10-bitarsräknare. Därigenom kan PWM-signaler med upp till 10 bitars<br />
upplösning genereras. PWM-signalens periodtid kan ställas in med Timer2:s prescaler<br />
( T2CON ) och med periodregistret PR2. Inställningen i periodregistret påverkar hur<br />
långt Timer2 räknar, och därigenom även PWM-signalens upplösning. Högst<br />
upplösning får man om Timer2 räknar fullt, dvs PR2 = 255, och periodtiden bara<br />
justeras med prescalern T2CON.<br />
(Timer2 som "10-bitarsräknare" räknar f osc inte f osc /4 ).<br />
Timer2 har även en postscaler, men den används inte vid PWM. Eftersom denna kan<br />
generera regelbundna avbrott kan man använda den till att uppdatera PWM-enhetens<br />
DutyTime.<br />
Från CCP-enheten används två 8 bitarsregister CCPRL och CCPRH och en 16-bitars<br />
komparator ( bara 10 bitar används ). CCPRH programmeras inte av användaren utan<br />
är bara en intern buffert vid PWM.<br />
PWM-signalens DutyTime, T Duty , "beställs" som 8 mest signifikanta bitar med<br />
registret CCPRL ( trots registrets namn! ) och 2 extra minst signifikanta bitar från<br />
registret CCPCON bitarna CCPX och CCPY.<br />
Om man har 10 bitars upplösning, men nöjer sig med 8 bitars upplösning, räcker det<br />
att placera 8 bitar för önskad DutyTime, T Duty , i registret CCPRL, och man behöver i<br />
så fall inte bry sig om de två extra bitarna i register CCPCON utan låter dem vara 0.<br />
36
Funktion<br />
• Timer2 räknar tills den når talet i periodregistret PR2, då 0-ställs timern och<br />
samtidigt 1-ställs en SR-låskrets. Detta påbörjar PWM-pulsen på CCP-pinnen.<br />
• Nu laddas de 10 bitarna "DutyTime" från "CCPRL×4 + CCPX×2+ CCPY" till<br />
komparatorn.<br />
• 10-bitars komparatorn upptäcker när Timer2 räknat fram till talet för<br />
DutyTime.<br />
• Då 0-ställs SR-låskretsen och PWM-pulsen avslutas på CCP-pinnen.<br />
• När Timer2 räknat fram till periodvärdet 0-ställs timern på nytt och förloppet<br />
upprepas - allt detta utan medverkan från processorn.<br />
PWM-frekvens, upplösning och DutyCycle<br />
För upplösningen gäller:<br />
För DutyCycle gäller:<br />
Exempel (med PIC16F628)<br />
Antag att vi har en PIC16F628 med 10 MHz klocka. Högst upplösning får man om<br />
Timer2 räknar fullt, dvs PR2 = 255 och prescalern delar med 1/1 ( ställs in med<br />
T2CON ). Prescalern kan ställas in på: 1:1, 1:4, 1:16 {1 4 16}.<br />
f PWM = 10⋅10 6 / (256×4×1) = 9766 Hz<br />
Upplösning = log(10000000/9766) / log(2) = 3/0,3 = 10 bitar<br />
Om denna PWM-frekvens kan accepteras och om man nöjer sig med 8 bitars<br />
upplösning blir <strong>programmering</strong>en av DutyCycle enkel:<br />
37
* Settings CCP1 PWM-mode */<br />
TRISB.3 = 0; /* CCP1 is output (RB3 pin) */<br />
PR2 = 255; /* Timer2 counts full period */<br />
/* PWM DutyCycle */<br />
CCPR1L = 64; /* in this example 25% DutyCycle 65/256 = 0,25 */<br />
/* CCP1X and CCP1Y can be set to 0 for 8 bit resolution */<br />
/* 0, 0000 (1:1 postscale), 1 (TMR2 on), 00 (1:1 prescale) */<br />
T2CON = 0b00000100; /* Set CCP unit in PWM-mode */<br />
CCP1CON = 0b00.0.0.1111;<br />
/* --, 0 (CCP1X), 0 (CCP1Y), 1111 (PWMmode 11xx) */<br />
Vill man använda en högre PWM-frekvens, tex 20 kHz för att undvika hörbara<br />
vibrationer, laddar man PR2 med talet 124. Högsta upplösningen blir då 9 bitar.<br />
Löst programexempel med PIC16F628<br />
PWM-testsignal, 1000 Hz 50%<br />
dutycycle<br />
Detta mycket enkla testprogram visar hur PIC16F62x ställs in för att för att generera<br />
en 1000 Hz, 50% dutycycle PWM-signal. Hemma kan testsignalerna avlyssnas med<br />
kristall-hörluren och kanske jämföras med ett stämt musikinstrument, eller telefonens<br />
kopplingston som är 440 Hz..<br />
( Vid labtillfället i skolan mäter vi frekvensen med en FLUKE 123-scopmeter. )<br />
38
PR2-registret<br />
Om oscillatorfrekvensen är 4 MHz och Timer2's prescaler är inställd på 1/4<br />
( T2CON ) så ska periodregistret PR2 således laddas med talet 249.<br />
CCPR 10 bitar<br />
50% Duty-cycle ger:<br />
Enklaste sättet att variera Duty-cycle är nu att skriva tal mellan 0 och 249 till CCPRLregistret.<br />
( Skriver man ett tal i CCPRL-registret som är större än talet i PR2-registret, tex 250,<br />
så upphör PWM-signalen).<br />
Programmet pwm1k62x.c<br />
/* pwm1k62x.c PWM testprogram for 16f628 */<br />
/* PWM frequency = 1000 Hz. PWM DutyCycle = 50%. */<br />
#include "16f628.h"<br />
#pragma config |= 0x3ff0<br />
void main(void)<br />
{<br />
/* Settings CCP1 PWM-mode */<br />
PR2 = 249;<br />
/* 10 bit Duty 0b01111101.0.0 = 500 */<br />
CCPR1L = 0b01111101;<br />
/* CCP1X = 0 CCP1Y = 0 later on */<br />
TRISB.3 = 0; /* CCP1 is output at RB3 ( pin 9 ) */<br />
T2CON = 0b0.0000.1.01;<br />
/* 0, 0000 (1:1 postscale), 1 (TMR2 on), 01 (1:4 prescale) */<br />
CCP1CON = 0b00.0.0.1111;<br />
/* --, 0 (CCP1X), 0 (CCP1Y), 1111 (PWMmode 11xx) */<br />
while(1) nop();<br />
}<br />
39
Lösta programexempel med PIC16F628<br />
Varvtalsratt till<br />
LEGO-motor<br />
Pulsgivare och PWM<br />
En ratt med pulsgivare är ansluten till RB6 och RB7. I skolan ansluter vi en Legomotor<br />
mellan RB3/CCP1 (PWM-signalen) och RA1. ( Hemma får Du nöja dig med<br />
två antiparallella lysdioder vars ljusstyrka symboliserar motorns varvtal ).<br />
Pulsgivaren ska användas som varvtalsratt för legomotorn (öka/minska). Full fart<br />
uppnås när man vridit fram pulsgivaren 255 steg.<br />
Programmet måste ignorera om man vrider fram mer än 255 steg eller bakåt förbi ”0”.<br />
/* pwmspeed.c RB-int reads rotary switch and outputs pwm */<br />
#include "16F628.h"<br />
#include "int16Cxx.h"<br />
#pragma config |= 0x3ff0<br />
char count; /* global to accumulate pulses from sensor */<br />
char trans; /* global to store transitions */<br />
#pragma bit lightdiode @ PORTA.2<br />
#pragma bit quad_A @ PORTB.6<br />
#pragma bit quad_B @ PORTB.7<br />
#define ON 1<br />
#define OFF 0<br />
40
#pragma origin 4<br />
interrupt int_server( void )<br />
{<br />
int_save_registers<br />
if( RBIF == 1 ) /* is it the RB pin change-interrupt? */<br />
{ /* this time it's obvius that it is! */<br />
/* read encoder new value */<br />
trans.0 = quad_A;<br />
trans.1 = quad_B;<br />
/* compare with old value */<br />
if( (trans == 0b00.01) && (count != 255) ) count ++;<br />
if( (trans == 0b01.00) && (count != 0) ) count --;<br />
/* replace old value with new value */<br />
trans.2 = trans.0;<br />
trans.3 = trans.1;<br />
RBIF = 0; /* Reset RB-change flag before leaving */<br />
}<br />
int_restore_registers<br />
}<br />
void main( void )<br />
{<br />
/* the main program */<br />
char speed;<br />
CM0=1; CM1=1; CM2=1; /* no comparators on PORTA */<br />
OPTION.7 = 0; /* internal pullup resistors on */<br />
TRISA.1 = 0; /* PA1 is output. Motor GND. */<br />
PORTA.1 = 0; /* Motor GND = 0 */<br />
TRISB = 0b11000000; /* only inputs quad_A and quad_B */<br />
trans = 0;<br />
speed = 0;<br />
RBIE = 1; /* local enable */<br />
GIE = 1; /* global enable */<br />
/* Settings CCP1 PWM-mode */<br />
PR2 = 255;<br />
TRISB.3 = 0; /* CCP1 is output (RB3-pin) */<br />
T2CON = 0b0.0000.1.00;<br />
/* 0, 0000 (1:1 postscale), 1 (TMR2 on), 00 (1:1 prescale) */<br />
CCP1CON = 0b00.0.0.1111;<br />
/* --, 0 (CCP1X), 0 (CCP1Y), 1111 (PWMmode 11xx) */<br />
while(1)<br />
{<br />
speed = count;<br />
CCPR1L = speed; /* new PWM-value */<br />
}<br />
}<br />
41
Programuppgift med PIC16F628<br />
Varvtalsratt till<br />
LEGO-motorn,<br />
medurs/moturs<br />
rotation<br />
Pulsgivare och PWM<br />
Uppgiften är nu att med pulsgivarens ratt styra motorns varvtal i två rotationsriktningar.<br />
När ratten vrids åt ena hållet ska hastigheten öka, om den vrids tillbaka åt andra hållet<br />
ska den minska tills den blir ”0”. Fortsätter man nu vridningen ska motorn byta<br />
rotationsriktning och ratten ska styra hastigheten i den andra riktningen!<br />
Byte av rotationsriktning<br />
Byte av rotationsriktning kan ske genom att man ändrar spänningen vid motorns<br />
jordpunkt från ”0” till ”1”. Om jordspänningen är ”0” ger PWM-signalen en proportionell<br />
hastighet i ena rotationsriktningen. Om jordspänningen är ”1” ger PWMsignalen<br />
en omvänt proportionellt varvtal i den motsatta rotationsriktningen, här bör<br />
man således programmera DutyCycle’s inverterade värde ( se figur )!<br />
Detta låter komplicerat, men det är faktiskt enklare än det ser ut eftersom det stämmer<br />
överens med hur datorer lagrar tal med tecken.<br />
Om variabeln count inte är teckenlös (char) utan i stället deklareras som ett tal<br />
med tecken ( int count; ) har den talområdet –128 < 0 < +127. Max- och mingränserna<br />
i programmet måste då omdefinieras:<br />
if( (trans == 0b00.01) && (count != 127) ) count ++;<br />
if( (trans == 0b01.00) && (count != -128) ) count --;<br />
42
Om man studerar de binära talen -128 (10000000), -1 (11111111), 0 (00000000),<br />
+1 (00000001), +127 (01111111), ser man att den mest signifikanta biten count.7<br />
är teckenbiten och att denna kan användas för att styra motorns rotationsriktning.<br />
PORTA.1 = count.7; /* rotate cw or ccw */<br />
Talcirkel. Registeraritmetik innebär att talsystemet kan ses som en "ring" - som km-räknaren på en bil.<br />
För enkelhets skull är registerlängden i figuren fyra bitar, då räknar registret "runt" efter talet 15.<br />
Principen är densamma för ett 8-bitars register, men det räknar runt efter talet 255.<br />
Tal kan antingen betraktas som teckenlösa tal ( tex typ char i C ) eller som tal med tecken<br />
( typ int i C ).<br />
Om man fördubblar talet count får man ett lämpligt värde att styra PWM-enheten<br />
med, detta visar nedanstående tabell:<br />
count count.7 count*2 count count.7 count*2<br />
+127 0 +254 -128 1 0<br />
(01111111)<br />
(11111110) (10000000)<br />
(00000000)<br />
0<br />
0 0<br />
-1 1 +254<br />
(00000000)<br />
(00000000) (11111111)<br />
(11111110)<br />
char speed;<br />
speed = count*2;<br />
CCPR1L = speed; /* new PWM-value */<br />
Det hela passar som ”hand i handske”!<br />
Inför dessa ändringar i programmet pwmspeed.c . Provkör<br />
därefter programmet och styr motorn med olika varvtal och<br />
rotationsriktningar.<br />
43
Lösta programexempel med PIC16F628<br />
TickClock, samplingsklocka<br />
I mekatroniken styr man processer med hjälp av ställdon utifrån mätvärden från<br />
givare. En mikrostyrkrets beräknar lämpliga åtgärder som sätts in vid regelbundna<br />
tidpunkter, samplingstillfällen.<br />
Det är verkligen viktigt att samplingsfrekvensen är konstant, eftersom storleken på en<br />
åtgärd naturligtvis måste bero på hur ofta som den utförs.<br />
En mycket vanlig <strong>programmering</strong>suppgift är därför att generera regelbundna<br />
programavbrott, med en sk samplingsklocka. Man kan se detta som det mekatroniska<br />
systemets hjärtslag!<br />
PIC-processorerna är förberedda för att enkelt kunna utföra detta.<br />
TIMER2 som samplingsklocka<br />
TIMER2:s periodtid är mycket enkel att påverka med ett 8-bitars periodregister PR2.<br />
Den har dessutom både en postscaler ( 1/1 1/4 1/16 ) och en prescaler ( 1/1 ... 1/16 ).<br />
När postscalern "slår runt" sätts en flagga TMR2IF och då kan även interruprutinen<br />
köras om man programmerat detta.<br />
Ofta används TIMER2 för generering av PWM-signaler. Postscalern ingår inte för<br />
genereringen av PWM-frekvensen, men den kan ändå användas som<br />
samplingsklocka.<br />
44
Antag att vi har 4 MHz kristall och att vi behöver en 750 Hz samplingsklocka (tex för<br />
att uppdatera PWM-signalen). TIMER2:s prescaler är 1/1 och periodregistret<br />
PR2=255. ( Jämför med exemplet Varvtalsratt till LEGO-motorn ). PWMfrekvensen<br />
blir:<br />
10 6 /256 = 3906 Hz. För att få en 750 Hz samplingsklocka måste vi dela<br />
pwmfrekvensen med 5 med postscalern. 3906/5 = 750 Hz.<br />
Programmet kan provas genom att man kontrollerar att har en ton med frekvensen<br />
750/2 Hz på pinne RB0 och att man har en ton med frekvensen 3906 Hz från CCP1<br />
pinnen RB3.<br />
/* pwmclk.c sampleclock 750 Hz with TIMER2 and pwm 3000 Hz */<br />
#include "16F628.h"<br />
#include "int16Cxx.h"<br />
#pragma config |= 0x3ff0<br />
#pragma origin 4<br />
interrupt int_server( void )<br />
{<br />
int_save_registers<br />
if( TMR2IF == 1 ) /* test if it is the TMR2I-interrupt? */<br />
{ /* this time it's obvius that it is! */<br />
PORTB.0 = !PORTB.0; /* togle earphone, 750/2 Hz tone at RB0 */<br />
TMR2IF = 0; /* Reset TMR2IF-flag before leaving */<br />
}<br />
int_restore_registers<br />
}<br />
void main( void )<br />
{<br />
/* the main program */<br />
TRISB.0 = 0; /* RB0 output to earphone */<br />
PORTB.0 = 0;<br />
/* Settings CCP1 PWM-mode */<br />
PR2 = 255;<br />
/* 10 bit startup Duty 0b01111101.0.0 */<br />
CCPR1L = 127; /* startup value */<br />
/* CCP1X = 0 and CCP1Y = 0 later on in CCP1CON */<br />
TRISB.3 = 0; /* CCP1 is output (RB3-pin) 3000 Hz tone */<br />
T2CON = 0b0.0100.1.00;<br />
/* 0, 0100 (1:5 postscale), 1 (TMR2 on), 00 (1:1 prescale) */<br />
CCP1CON = 0b00.0.0.1111;<br />
/* --, 0 (CCP1X), 0 (CCP1Y), 1111 (PWMmode 11xx) */<br />
TMR2IE = 1; /* local enable */<br />
PEIE = 1; /* regional enable */<br />
GIE = 1; /* global enable */<br />
while( 1 ) nop(); /* main program has nothing to do ! */<br />
}<br />
45
PIC16F87x, AD-omvandlarens<br />
referensspänning<br />
Blockschema över PIC-midrangekretsarnas AD-enhet.<br />
Matningsspänningen som spänningsreferens<br />
För att kunna mäta analoga spänningar har PIC-midrangekretsarna en 10-bitars ADomvandlare.<br />
Med hjälp av en inbyggd sk. multiplexer har man möjlighet att ansluta<br />
omvandlaren till en i taget av 8 portpinnar. Som referens vid AD-omvandlingen<br />
används antingen kretsens matningsspänning, (V SS , V DD ), eller så kan man ansluta<br />
yttre spänningsreferenser genom två av portpinnarna. Det enklaste är naturligtvis att<br />
använda matningsspänningen som referens, men det kan finnas olika anledningar till<br />
att använda yttre spänningsreferens.<br />
Yttre precisionsreferens<br />
Antag att PIC-processorn ska användas som en noggrann voltmeter. Det är då viktigt<br />
att den uppmätta spänningen jämförs med en exakt referens. Om utrustningen<br />
tillexempel är batteridriven, så varierar matningspänningen för mycket för att vara<br />
lämplig som referensspänning.<br />
46
PIC-processor som Voltmeter.<br />
Man kan ansluta en yttre referensspänning till ingången AN3. Det finns utmärkta<br />
spänningsreferenskretsar att köpa, tex kretsen MAX6241, som har spänningen 4,096<br />
V. Detta "ovanliga" spänningsvärde kommer av att talet 4096 är en tvåpotens, 2 12 =<br />
4096. Med den referensspänningen får AD-omvandlaren skalfaktorn 4 mV/steg vilket<br />
innebär att mätvärdet kan presenteras direkt, utan att behöva skalas om med<br />
multiplikation/division. Division med "4" kan man utföra genom att "skifta" det<br />
binära talet två steg åt höger.<br />
(Multiplikationer och divisioner innebär mycket programkod för processorer som<br />
saknar inbyggd multiplikatorenhet).<br />
Högsta upplösning<br />
Vill man ha högsta upplösning vid mätning av givarsignaler måste man se till att<br />
givarens utspänningsområde och det spänningsområde AD-omvandlaren omvandlar<br />
helt "täcker" varandra. I allmänhet krävs det då att både AD-omvandlarens<br />
referensspänning och 0-nivå kan justeras.<br />
Hög upplösning kräver att AD-omvandlarens referensspänning och 0-nivå kan justeras efter givarens<br />
spänningsområde.<br />
Med de två potentiometrarna i figuren kan AD-omvandlarens referensspänning<br />
(V REF+ ) ställas in mellan 2,5-5V och 0-nivå (V REF- ) mellan 0-2,5V. Genom att<br />
potentiometrarna är seriekopplade riskerar man inte att av misstag kunna ställa in en<br />
referensnivå som är lägre än 0-nivån.<br />
47
Anslutning av en termistor<br />
Ett praktiskt exempel är temperaturmätning med en NTC-termistor. En termistor har<br />
hög känslighet, men olinjärt temperatursamband.<br />
I praktiken seriekopplar man termistorn med en fast resistor så att den ingår i en<br />
spänningsdelare. ( Tekniken beskrivs i boken Mätgivare ). Figuren visar sambandet<br />
mellan spänning och temperatur för en sådan spänningsdelare. Kurvan börjar vid 0,5<br />
V och slutar vid 3,5V och blir däremellan förvånadsvärt linjär.<br />
Genom att ställa in V REF- = 0,5V och V REF+ = 3,5V med potentiometrarna, så utnyttjar<br />
temperaturgivaren AD-omvandlarens hela spänningsområde.<br />
48
Ratiometrisk anslutning<br />
Ratiometrisk anslutning av potentiometergivare till en AD-omvandlare.<br />
Som referensspänning kan den ostabiliserade matningsspänningen användas!<br />
Ibland behöver man stabila referensspänningar både till givare och AD-omvandlare.<br />
Ett ofta använt konstgrepp är då ratiometrisk anslutning av givaren. Så länge<br />
givarens referensspänning och AD-omvandlarens referensspänning ändrar sig i<br />
samma proportioner så omvandlas samma värde.<br />
Om man har samma referensspänning till givare och AD-omvandlare så ställs det<br />
därför inga krav på att den ska vara stabil.<br />
AD-omvandlare förses av detta skäl ofta med separata anslutningar för<br />
matningspänning och jord till sina analoga respektive digitala delar.<br />
( V DD V SS V REF+ V REF- ).<br />
Det viktiga är att båda referensspänningarna ansluts till samma punkter så att de<br />
utsätts för samma "störningar".<br />
PIC-processorn har separat anslutning för AD-omvandlarens referensspänning genom<br />
pinnarna AN2 och AN3.<br />
( Tekniken beskrivs i boken Mätgivare ).<br />
49
PIC16F87x AD-omvandling, steg för<br />
steg (utan användning av interrupt)<br />
1. Konfigurera AD-modulen<br />
Konfigurera analoga ingångar och referensspänningar (eller digitital<br />
I/O). Register ADCON1.<br />
• Välj AD-ingångskanal. Register ADCON0.<br />
• Bestäm AD-omvandlarens klockfrekvens. Register ADCON0.<br />
• Sätt på AD-modulen! Register ADCON0.<br />
• Välj format på hur 10-bitstalet ska justeras. Register ADCON1.<br />
Exempel:<br />
ADCON0=0bxxxxxxxx; ADCON1=0bxxxxxxxx; TRISA=0bxxxxxxxx;<br />
TRISE=0bxxxxxxxx;<br />
2. Vänta inställningstiden ( acquistion time ).<br />
• Acquistion time t ACQ är av storleksordningen 20 µs. Exempel på hur man<br />
programmerar 20 µs ungefärlig fördröjningstid (tids-spill) vid 4 MHz<br />
klockfrekvens:<br />
char i = 3; do ; while ( --i > 0);<br />
3. Starta AD-omvandlingen.<br />
• Ettställ GO/DONE biten. Register ADCON0. Kodexempel:<br />
GO = 1;<br />
4. Vänta tills AD-omvandlingen är klar.<br />
• Vänta ut att biten GO/DONE blir 0. Exempel på programkod:<br />
while( GO );<br />
5. Läs av AD-omvandlaren.<br />
• 8 bitar, ADRES vänsterjusterat. Kodexempel:<br />
char answer;<br />
answer=ADRESH;<br />
• 10 bitar, ADRES högerjusterat. Kodexempel:<br />
unsigned long answer;<br />
answer = 256*ADRESH; answer += ADRESL;<br />
( ADRES finns fördefinierad i headerfilen 16f874.h som ADRESH och ADRESL,<br />
GO/DONE finns fördefinierad i headerfilen 16f874.h som GO )<br />
50
PIC16F874<br />
Blinkfrekvensen styrs av potentiometern<br />
/* ADtest.c testprogram for 16f874 */<br />
/* Blinkfrequency at RD0 varies with voltage on AN0 */<br />
#include "16f874.h"<br />
#pragma config |= 0x3ff2<br />
#pragma bit out @ PORTD.0<br />
void main(void)<br />
{<br />
char i, j, answer, blinktime;<br />
TRISD.0 = 0; /* lightdiode at RD0 is output */<br />
out = 1;<br />
/* AD Setup. Se help Script! */<br />
/* "fosc/8"."AN0"."go=0"."-"."on" */<br />
ADCON0 = 0b01.000.0.0.1;<br />
/* "leftjust"."fosc/8"."--"."internal ref, Vref+=Vcc, Vref-=Vss" */<br />
ADCON1 = 0b0.1.00.1110;<br />
TRISA.0 = 1; /* AN0 is input */<br />
while(1)<br />
{<br />
char i = 3; do ; while ( --i 0); /* wait tACQ */<br />
GO=1; /* start AD */<br />
while(GO); /* wait for done */<br />
blinktime = ADRESH; /* read result 8 msb */<br />
/* double loop i,j generates blinkfrequency */<br />
for( i=0;i < blinktime; i++)<br />
{<br />
for( j=0;j < 250; j++);<br />
}<br />
/* toggle lightdiode on/off */<br />
out = ! out;<br />
}<br />
}<br />
51
JSP-diagram för AD-testprogram. Med en potentiometer till AD-omvandlaren kan man styra en<br />
lysdiods blinkfrekvens.<br />
52