21.12.2013 Views

Maskinnära programmering 6B2266

Maskinnära programmering 6B2266

Maskinnära programmering 6B2266

SHOW MORE
SHOW LESS

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

Hooray! Your file is uploaded and ready to be published.

Saved successfully!

Ooh no, something went wrong!