Interrupts, aansturen van een servo: een lichtvolger
In deze tutorial leggen we stap voor stap uit hoe je een PWM-signaal (Pulse Width Modulation) kan generen met behulp van een timer-gestuurde interrupt routine. Dit PWM-signaal kan je dan bijvoorbeeld gebruiken om een servo mee aan te sturen. Na het voltooien van deze tutorial heb je naast meer kennis over de microcontroller, een werkende lichtvolger!
Benodigdheden
- Een Dwengo-bord
- Een Dwengo-programmer
- De bijbehorende kabels
- Twee fototransistoren met bijhorende 22 kOhm weerstanden
- Eventueel een Dwengo-breadbord voor het gemakkelijk bouwen van de analoge schakeling en wat draadjes
- Een servo-motor
- Een soldeerbout met wat tin voor het ineenknutselen van de sensor
PWM-signalen
Pulse Width Modulation, afgekort PWM, is een manier om informatie te coderen. Hierbij wordt de breedte van een blokgolf-puls aangepast (gemoduleerd). Een voorbeeld hiervan zie je op de figuur. Wanneer we een PWM-signaal als voeding wensen te gebruiken biedt de techniek de mogelijkheid om toestanden tussen volledig uit en volledig aan weer te geven. Met andere woorden, de gemiddelde spanning kan gradueel geregeld worden. Anderzijds kan de breedte van de blokgolf-puls ook gebruikt worden om informatie te coderen. Het PWM-signaal wordt dan gebruikt als controle-signaal voor een elektronische component.
In deze tutorial gebruiken we een PWM-signaal om de stand van een servomotor te regelen. Voor servo's gebruikt men typisch een blokgolf met een periode van 20 ms. De breedte van de puls ligt typisch tussen de 0,7 ms en 2,3 ms en bepaalt de stand van de servo zoals aangegeven op de figuur.
De microcontroller die gebruikt wordt op het Dwengo-bord heeft een ingebouwde PWM-module die het mogelijk maakt om snel en gemakkelijk een PWM-signaal te genereren. Deze is aangesloten op de motor-driver van het Dwengo-bord zodat gemakkelijk de spanning van bijvoorbeeld motoren geregeld kan worden. Dit wordt uitgelegd in de tutorial over de licht-etende robot. Uiteraard kan je deze ingebouwde module ook gebruiken voor het aansturen van de servo's. Maar om het mogelijk te maken zowel de spanning te regelen van motoren als servo's aan te sturen bieden we hier een alternatieve methode om een PWM-signaal te genereren met behulp van timer interrupts.
Timer interrupts
In de vorige tutorials hebben we enkel programma's geschreven die sequentieel uitgevoerd worden. Op deze wijze kan je heel wat leuke programma's schrijven maar je bent steeds gebonden aan het sequentiële verloop van het programma. Soms wil je echter kleine taken uitvoeren zonder dat de hoofdlus van het programma veel verstoord wordt. Dit kan je oplossen met behulp van interrupts.
De microcontroller voorziet met interrupts een (in hardware ingebouwd) mechanisme om te reageren op gebeurtenissen die zich voordoen terwijl de hoofdlus van het programma loopt. Typische voorbeelden van zulke gebeurtenissen zijn drukknoppen die ingedrukt worden, analoge sensoren die een bepaalde waarde bereiken, een timer die afloopt, ...
Daarnaast heeft de microcontroller ook een ingebouwd mechanisme om tijd bij te houden. Dit mechanisme noemt men een timer. De timer verhoogt op vaste tijdstippen de waarde van een specifiek register. De snelheid waarmee dit gebeurt, is instelbaar in software. Vaak bieden microcontrollers de mogelijkheid om een interrupt te genereren van zodra dit register zijn maximale waarde bereikt. Het is dit mechanisme dat we gebruiken om een PWM-signaal mee te genereren.
Aansturen van een servo
De microcontroller kan zo geprogrammeerd worden dat hij een bepaalde opdracht uitvoerd wanneer er een interrupt optreedt. Daarvoor moet ons programma uitgebreid worden met een interrupt handler. De programmeur moet ervoor zorgen dat deze interrupt handler zo efficient mogelijk geschreven is teneinde de hoofdlus van het programma niet te veel te verstoren. Immers de hoofdlus wordt steeds onderbroken wanneer er een interrupt optreedt. Om diezelfde reden is het ook goed erover te waken niet overdreven veel interrupts te laten voorkomen.
Bibliotheken, macro's en globale variabelen
Naast het inladen van de bibliotheken en het zetten van de configuratiebits, definiëren we eerst een aantal macro's die de code leesbaarder maken en maken we een globale variabele aan. Dit is een variabele die ook buiten de hoofdlus zichtbaar zal zijn.
#define TRUE 1 #define FALSE 0 #define MAX_TIMER_SERVO 64536 // maximal left position #define MIN_TIMER_SERVO 62086 // maximal right position #define SERVO_STEP 10 // defines servo precision #define THRESHOLD 30 // defines light sensitivity unsigned int timerServo;
Interrupt handlers
Daarna voegen we de prototypes voor de interrupt handlers toe.
void YourHighPriorityISRCode(); void YourLowPriorityISRCode();
Merk op dat we twee interrupt handlers aanmaken. Zoals de namen van de functies aangeven voorzien we een interrupt handler met hoge prioriteit en eentje met lage prioriteit. Dit is vooral van belang wanneer je meerdere soorten interrupts zou willen gebruiken afkomstig van verschillende soorten hardwarebronnen. Door ze in de één of de andere handler te steken kan je dan hun prioriteit aangeven wat van belang is wanneer er twee interrupts tegelijkertijd zouden optreden. In deze tutorial is dit echter van geen belang en zullen we enkel code schrijven in de interrupt handler met hoge prioriteit.
Vervolgens maak je de eigenlijke interrupt handlers aan. Hierbij zal je met behulp van het woord pragma interrupt en pragma interruptlow aangeven dat het stuk code op de juiste plaats in het programmageheugen moet komen te staan. Dit verschilt typisch van het adres waar de hoofdlus van het programma komt. Wanneer er een interrupt optreedt, zal de microcontroller de code op deze adressen beginnen uitvoeren. Daarnaast moeten de code van de interrupts ook op de juiste plaats in het geheugen gemapt worden. Dit gebeurt door het adres voor de high_vector en low_vector mee te geven.
#pragma code high_vector=0x08 void high_vector() { _asm goto YourHighPriorityISRCode _endasm } #pragma code #pragma code low_vector=0x18 void low_vector() { _asm goto YourLowPriorityISRCode _endasm } #pragma code // interrupt handler routines #pragma interrupt YourHighPriorityISRCode void YourHighPriorityISRCode() { if (PIR1bits.TMR1IF == TRUE) { // check interrupt flag: from timer? if (PORTBbits.RB5 == 0) { TMR1H = (timerServo & 0xFF00) >> 8; // keep most significant byte TMR1L = timerServo & 0x00FF; // keep least significant byte PORTBbits.RB5 = 1; // high pulse } else { TMR1H = 0x8A; // timer 1 interrupt after 83.333ns*8*(65536-35536) = +/- 20ms TMR1L = 0xD0; // 35536 = 0x8AD0 PORTBbits.RB5 = 0; // low pulse } PIR1bits.TMR1IF = FALSE; // reenable TMR1 interrupt } } #pragma interruptlow YourLowPriorityISRCode void YourLowPriorityISRCode() { }
De code van de functie YourHighPriorityISRCode() volgt het volgende stramien:
1. Kijken vanwaar de interrupt afkomstig is, hiervoor moet je de verschillende interrupt-vlaggen controleren van de verschillende voorzieningen op de microcontroller die mogelijk een interrupt genereren. Wij gebruiken enkel een timer1-interrupt en kijken dus de interrupt-vlag na van timer1.
2. Uitvoeren van de code die hoort bij de interrupt, deze code schrijf je zo kort en efficiënt mogelijk. In ons geval houdt dit in dat we pin RB5 hoog of laag zetten naargelang de toestand waarin pin RB5 zich bevond. Vervolgens wordt het timer1-register op de juiste manier ingesteld. Hierover zullen we meer vertellen bij het bespreken van de hoofdlus. Merk op dat we gebruik maken van de globale variabele timerServo die we in de hoofdlus op de juiste manier zullen instellen.
3. Heractiveren van de interrupt. In ons geval heractiveren we de timer1-interrupt door het juiste register op 0 te zetten.
Initialisaties
In de hoofdlus maken we eerst een aantal variabelen aan waarin we de sensor-waarden zullen opslaan van de linker en rechter lichtsensor. We houden eveneens de vorige waardes bij. Daarnaast configureren we de ADC-module van de microcontroller.
void main(void) { int rightSensor, leftSensor, previousRightSensor, previousLeftSensor; /* ADC_FOSC_64: conversion clock, table 21-1, 1TAD = 1,33 us (has to be between 0,7 us and 25 us) ADC_RIGHT_JUST: least significant bits ADC_6_TAD: acquisition time, 6 TAD's used for good conversion, minimal 1,4 us required ADC_INT_OFF: no adc-interrupts ADC_VREFPLUS_VDD and ADC_VREFMINUS_VSS: use PIC ground and source as voltage reference 0b1010: pins AN0-AN4 configured as analog input pins */ OpenADC(ADC_FOSC_64 & ADC_RIGHT_JUST & ADC_6_TAD, ADC_CH0 & ADC_INT_OFF & ADC_VREFPLUS_VDD & ADC_VREFMINUS_VSS, 0b1010); // Configuring ADC
We beslissen om het PWM-signaal te genereren op pin RB5. Deze pin is verbonden met connector servo1 op het Dwengo-bord. Daarom configureren we pin RB5 als uitgang en initialiseren we de pin met 0.
// servo pins TRISBbits.TRISB5 = 0; // set RB5 as output PORTBbits.RB5 = 0; // initialize with 0
We stellen de globale variabele timerServo in met de waarde 62086. Het wordt later duidelijk waar deze waarde vandaan komt. Belangrijk is te onthouden dat we met deze variabele de positie van de servo zullen bepalen.
timerServo = MIN_TIMER_SERVO; // maximal right
Vervolgens stellen we de nodige registers om de ingebouwde timer1-module te activeren met bijhorende interrupt. Dit begint met het op 0 zetten van de timer1 interrupt flag:
PIR1bits.TMR1IF = 0;
Aangezien we eerder een onderscheid maakten tussen lage en hoge prioriteiten bij het optreden van een interrupt, activeren we interrupt prioriteiten met het register IPEN.
RCONbits.IPEN = 1;
We deactiveren Brown-out Reset.
RCONbits.SBOREN = 0; // BOR = off
Wanneer een timer geactiveerd wordt zal er een register met 1 verhoogd worden op vaste tijdstippen. Voor timer1 is dit een 16-bit register waarvan TMR1H de meest significante byte voorstelt en TMR1L de minst significante byte. Wanneer dit 16-bit register zijn maximale waarde 0xFFFF bereikt zal er een interrupt optreden. Door het register op voorhand te initialiseren met een bepaalde waarde kan je de tijd tot het optreden van een interrupt versnellen. We initialiseren daartoe het register TMR1H met de 8 meest significante bits en register TMR1L met de minst significante bits van de variabele timerServo. We gebruiken een binaire AND-operatie en een schuifoperatie om de juist bits te selecteren.
TMR1H = (timerServo & 0xFF00) >> 8; // keep most significant byte TMR1L = timerServo & 0x00FF; // keep least significant byte
Vervolgens stellen we timer1 zo in dat deze iedere 883.333 ns het *timer1-register zal verhogen. Dit gebeurt met behulp van het T1CON-register zo in te stellen dat de gebruikte instructiecyclus TCYx (= 83.333 ns) met factor 8 vertraagd wordt. We gebruiken dus een prescaler van 8. Tegelijkertijd activeren we timer1. Indien je aandachtig de PIC18F4550 datasheet
bekijkt zal je zien dat we het register T1CON moeten instellen met 0b00110001 om een prescaler van 8 te selecteren en timer1 te activeren.
T1CON = 0b00110001; // 48Mhz/4, prescaler 1/8
Hiermee is het dan ook meteen duidelijk vanwaar de gekozen waarden voor de uiterste posities MIN_TIMER_SERVO en MAX_TIMER_SERVO voor de servo komen. Voor het ene uiterste willen we een wachttijd van 0.7 ms. We zoeken dus een getal waarmee we het 16-bits register van timer1 moeten initialiseren zodat deze na 0.7 ms op zijn maximale waarde 65536 komt te staan. Indien we de prescaler van 8 en de TCYx van 83.333 ns in achting nemen bekomen we dus 0.7 ms = 83.333 * 8 * (65536-x) ns met x het getal dat we zoeken. Als je dit uitrekent bekom je voor x 64536. Met andere woorden om een pulsbreedte van 0.7 ms te verkrijgen moeten we het timer1-register met de waarde 64536 initialiseren. We kennen dus aan de variabele timerServo deze waarde toe. Hetzelfde geldt voor de andere uiterste positie: 2.3 ms = 83.333ns * 8 * (65536-62086).
Na de puls in het PWM-signaal staat komt er een rust van ongeveer 20 ms. Daartoe wordt het timer1-register in de interrupt handler YourHighPriorityISRCode() geïnitialiseerd op 35536 want 83.333ns * 8 * (65536-35536) is ongeveer 20ms.
Achtereenvolgens zetten we nog het Global Interrupt Enable-bit (GIEH) aan, activeren we het Overflow Interrupt Priority-bit (TMR1IP) en het TMR1 Overflow Interrupt Enable-bit (TMR1IE). Wat allemaal nodig is om timer1-interrupts te kunnen gebruiken. Voor meer details verwijs ik naar de PIC18F4550 datasheet
.
INTCON = 0b10000000; // GIEH = TRUE IPR1bits.TMR1IP = 1; PIE1bits.TMR1IE = 1;
Ten slotte initialiseren we ook reeds de variabele leftSensor en rightSensor met een zinnige waarde.
leftSensor = 512; rightSensor = 512;
Volgen van licht
Na het initialiseren van de nodige zaken zijn we gekomen tot de eigenlijke logica van ons programma. Dit bestaat zoals vaak uit een lus die een oneindig aantal keren uitgevoerd wordt waarin we achtereenvolgens volgende stappen ondernemen:
1. Bewaren van de oude waarden van de twee lichtsensoren
2. Uitlezen van de huidige sensor-waardes
3. Het gemiddelde nemen van de huidige en oude sensor-waardes
4. Vergelijken van de twee sensor-waardes en de positie veranderen
5. Controleren of er een uiterste positie werd bereikt
6. Een beetje wachten
Als we dit in code omzetten maken we eerst de oneindige lus aan en voeren we stap één uit.
while (TRUE) { // do this forever // save old previousLeftSensor = leftSensor; previousRightSensor = rightSensor;
Vervolgens lezen we in de tweede stap de sensoren uit.
// reading right light sensor SetChanADC(ADC_CH0); // choose ADC channel (override ADC_CH0) Delay10TCYx(150); // wait a bit ConvertADC(); // start conversion while (BusyADC()); // wait for conversion rightSensor = ReadADC(); // read result // reading left light sensor SetChanADC(ADC_CH1); // choose ADC channel (override ADC_CH1) Delay10TCYx(150); // wait a bit ConvertADC(); // start conversion while (BusyADC()); // wait for conversion leftSensor = ReadADC(); // read result
In een derde stap middelen we de sensor-waardes uit. De meeste sensoren zijn immers onderhevig aan ruis. Deze ruis kan je wegfilteren. De meest eenvoudige manier om dit te doen is door het gemiddelde te nemen van de huidige en vorige sensor-waarde.
// filtering leftSensor = (leftSensor + previousLeftSensor)/2; rightSensor = (rightSensor + previousRightSensor)/2;
In de vierde stap laten we het programma kijken vanwaar het licht komt. Hierbij dien je in acht te nemen dat hoe groter de sensor-waarde is hoe minder licht er aanwezig is. De positie kunnen we veranderen door een waarde bij de globale variabele timerServo op te tellen of af te trekken. We doen dit steeds met vaste sprongen die je kan instellen met de macro SERVO_STEP. We maken eveneens gebruik van een drempelwaarde zodat wanneer beide sensoren ongeveer evenveel licht ontvangen de servo zijn positie blijft aanhouden. Wanneer straks je lichtvolger af is, is het interessant eens verschillende waarden voor SERVO_STEP en THRESHOLD uit te proberen en de effecten ervan te evalueren.
if ((rightSensor-leftSensor) > THRESHOLD) timerServo -= SERVO_STEP; else if ((leftSensor-rightSensor) > THRESHOLD) timerServo += SERVO_STEP;
In de vijfde stap kijken we of er één van de uiterste posities bereikt werd. Is dit het geval dan wensen we de waarde timerServo niet meer aan te passen omdat dit er kan toe leiden dat de servo het PWM-signaal niet meer kan interpreteren.
if (timerServo > MAX_TIMER_SERVO) timerServo = MAX_TIMER_SERVO; if (timerServo < MIN_TIMER_SERVO) timerServo = MIN_TIMER_SERVO;
Tot slot wachten we telkens een beetje. Je kan deze waarde verkleinen of vergroten om de lichtvolger sneller of trager te laten reageren. Merk hierbij op dat je uiteraard voor de analoge sensoren uit te meten ook een beetje moet wachten en dat de code zelf ook wat tijd in beslag neemt om uit te voeren. Je lichtvolger zal dus nooit oneindig snel zijn.
Delay10KTCYx(6); // wait 5 ms } }
Vooraleer je de code zal kunnen testen moet je echter wel nog een kleine constructie in elkaar knutselen. Dit staat uitgelegd in de volgende twee secties.
Aansluiten van een servo
Een servo heeft typisch drie draden verbonden met een connector. Twee van deze draden, de voedingsdraden, voorzien de servo van de juiste spanning en met één ervan kan je de positie instellen. De twee voedingsdraden zijn steeds zwart (aanduiding grond) en rood (aanduiding 5 V) van kleur. Om gemakkelijk servo's aan te sluiten is het Dwengo-bord voorzien van twee connectoren: servo1 en servo2. Hierop kan je servo's aansluiten die maximaal zo'n 200 à 400 mA stroom vragen. Sluit de servo zo aan dat de donkerbruine of zwarte draad overeenkomt met het "-"-teken zoals op de foto is weergegeven.

In deze tutorial gaan we ervan uit dat er een servo is aangesloten op de connector met naam servo1.
Aansluiten van de sensoren
In de tutorial over ADC toonden we hoe je met een weerstand en een fototransistor een lichtsensor kan bouwen. Voor de lichtvolger moet je de schakeling twee maal bouwen. Nu is het echter de bedoeling dat je de sensoren soldeert zodat deze bevestigd kunnen worden op de servo. Net zoals in de vorige tutorial sluit je de ene sensor aan op AN0 van het Dwengo-bord. De tweede sensor sluit je aan op AN1 van het Dwengo-bord. Let erop dat je het lange beentje van de fototransistor (de emittor) aansluit op de grond. Het korte beentje (de collector) sluit je aan op de weerstand die loopt naar de 5 V-pin. Het aansluiten van de draden kan je gemakkelijk doen met het Dwengo-experimenteerbord. Je krijgt dan een schakeling zoals op de foto.
Wanneer alles goed is aangesloten en geprogrammeerd heb je nu een werkende lichtvolger!
- Key words:
- Type:

Uw winkelwagen

