Aansturen van een servo met behulp van interrupts

Servo's zijn motoren die afhankelijk van het stuursignaal in een bepaalde positie gaan staan. Een servo heeft typisch drie aansluitingen: de negatieve voedingsspanning, de positieve voedingsspanning (5V) en het stuursignaal. Het Dwengo-bord heeft twee connectoren servo1 en servo2 waarop rechtstreeks servo's kunnen aangeschakeld worden. Op deze pagina leggen we in detail uit hoe het stuursignaal voor een servo op een efficiƫnte manier met de PIC18F4550 kan gegenereerd worden. Dwengo beschikt ook over een bibliotheek dwengoServo.h die deze details voor de gebruiker verbergt.

De gewenste positie wordt naar de servo gestuurd in de vorm van een PWM-signaal. PWM staat voor Pulse Width Modulation of pulsbreedtemodulatie in het Nederlands. Een PWM-signaal is een elektrisch signaal waar de spanning periodiek pulsjes vertoont. De breedte van de pulsjes geeft de gewenste servo-positie aan. Dus, als we de breedte van de pulsjes veranderen, verandert de positie van de servo mee. Dit is weergegeven in de figuur. Het PWM-signaal voor het aansturen van een sevo heeft typisch een periode van 20 ms en de breedte van de puls varieert tussen 0,7 en 2,3 ms.

PWM met servo-posities

Het Principe: Timer interrupts

We zouden het PWM-signaal voor de servo's kunnen genereren met behulp van delays zoals hieronder is weergegeven voor een PWM-signaal met pulsbreedte 1 ms en periode 20 ms. Dit is echter zeer inefficiƫnt aangezien de microcontroller het grootste deel van de tijd bezig is met het wachten en in die tijd niets nuttigs doet. Het is veel beter om in dit geval gebruik te maken van een Timer en de bijhorende interrupt.

  1. while(TRUE) {
  2. SERVO1 = 1;
  3. delay_ms(1);
  4. SERVO1 =0;
  5. delay_ms(19);
  6. }

Een timer is een stukje hardware op de chip. Je kan de timer instellen om na een bepaalde tijd af te lopen. Nadat de timer is ingesteld door de microcontroller begint hij af te tellen en kan de microcontroller verder werken aan een andere taak. Op het moment dat de timer afloopt stuurt hij een interrupt naar de microcontroller. Deze stopt dan met zijn normale werking en voert de Interrupt Service Routine (ISR) uit om daarna zijn werk te hervatten.

We kunnen dit mechanisme als volgt gebruiken om een hetzelfde PWM-signaal te genereren als de code hierboven:

  • Stel in het begin van de hoofdlus de servo1 gelijk aan 1 en stel de timer in op 1 ms.
  • Schrijf een ISR die:

-- servo1 gelijk stelt aan 0 en de timer instelt op 19 ms als servo1 gelijk is aan 1.
-- servo1 gelijk stelt aan 1 en de timer instelt op 1 ms als servo1 gelijk is aan 0.

De C-code

Hieronder vind je de C-code die, met behulp van interrupts, een PWM-signaal genereert met een pulsbreedte van 1 ms en een periode van 20 ms. We zullen nu stap voor stap uitleggen wat deze code juist betekent.

  1. #pragma config PLLDIV = 5 // Divide by 5 (20 MHz oscillator input)
  2. #pragma config FOSC = HSPLL_HS // HS oscillator, PLL enabled, HS used by USB
  3. #pragma config IESO = OFF // Oscillator Switchover mode disabled
  4. #pragma config PWRT = OFF // PWRT disabled
  5. #pragma config BOR = OFF // Brown-out Reset enabled in hardware only (SBOREN is disabled)
  6. #pragma config WDT = OFF // HW Disabled - SW Controlled
  7. #pragma config WDTPS = 32768 // 1:32768
  8. #pragma config MCLRE = ON // MCLR pin enabled; RE3 input pin disabled
  9. #pragma config LVP = OFF // Disable low-voltage programming
  10. #pragma config CCP2MX = ON // CCP2 is multiplexed to RC1 and not to RB3
  11. #pragma config PBADEN = OFF // PORB digital IO on powerup
  12.  
  13. #include <p18f4550.h>
  14.  
  15. #pragma interrupt ISR
  16. void ISR() {
  17. if (PIR1bits.TMR1IF == 1) { // TIMER1 interrupt?
  18. if (PORTBbits.RB5 == 1) {
  19. PORTBbits.RB5 = 0;
  20. TMR1L = 37036 & 0x00FF;
  21. TMR1H = (37036 & 0xFF00) >> 8;
  22. } else {
  23. PORTBbits.RB5 = 1;
  24. TMR1L = 64036 & 0x00FF;
  25. TMR1H = (64036 & 0xFF00) >> 8;
  26. }
  27. PIR1bits.TMR1IF = 0; // Reenable TIMER1 interrupt
  28. }
  29. }
  30.  
  31. #pragma code high_vector=0x08
  32. void high_vector() {
  33. _asm
  34. goto ISR
  35. _endasm
  36. }
  37. #pragma code
  38.  
  39. void main(void) {
  40.  
  41. // Initialise servo pin
  42. TRISBbits.TRISB5 = 0; // Set RB5 as output
  43. PORTBbits.RB5 = 1; // Initialise with 1
  44.  
  45. // Initialise prescaler to 8
  46. T1CONbits.T1CKPS0 = 1;
  47. T1CONbits.T1CKPS1 = 1;
  48.  
  49. // Set TIMER1 to 1 ms
  50. TMR1L = 64036 & 0x00FF;
  51. TMR1H = (64036 & 0xFF00) >> 8;
  52.  
  53. T1CONbits.TMR1ON = 1;
  54.  
  55. // Allow TIMER1 interrupts
  56. INTCONbits.GIE = 1;
  57. INTCONbits.PEIE = 1;
  58. PIE1bits.TMR1IE = 1;
  59.  
  60. PIR1bits.TMR1IF = 0;
  61.  
  62. // Wait forever
  63. while (1) {
  64. }
  65. }

Initialisatie van pin servo1

Aangezien we het PWM-signaal willen genereren op de connector servo1 van het Dwengo-bord moeten we de signaal-pin van deze servo-connector, RB5, instellen als uitgangspin. We stellen de uitgangswaarde ook onmiddellijk in op 1. Dit gebeurt met de volgende code (lijn 42-43)

  1. TRISBbits.TRISB5 = 0; // set RB5 as output
  2. PORTBbits.RB5 = 1; // initialize with 0

Initialiseren van TIMER 1

Zoals we hierboven al zeiden is een timer een stukje hardware op de chip van de microcontroler dat een interrupt genereert na een vooropgestelde tijd. Om deze tijd in te stellen moeten we in een aantal registers de juiste waarden schrijven. In plaats van rechtstreeks de tijd in te stellen, stellen we twee waarden in:

  • Het aantal tellen voordat de timer afloopt en dus een interrupt genereert.
  • De tijd tussen twee tellen.

We stellen de tijd tussen twee tellen van TIMER 1 in op 883,33 ns = 666,66 ns. De timmer moet dus elke 8 instructiecycli (=83,33 ns) 1 maal tellen. Hiervoor kan gezorgd worden door de prescaler van de timer in te stellen op 8. Indien je aandachtig de PIC18F4550 datasheet External link bekijkt, zal je zien dat hiervoor de bits T1CKPS1 en T1CKPS0 van het register *T1CON moeten ingesteld worden op waarde 1.

  1. T1CONbits.T1CKPS0 = 1;
  2. T1CONbits.T1CKPS1 = 1;

Bij elke tel (in ons geval elke 666,66 ns) wordt het 16-bit register TMR1 met 1 verhoogd. Bij het bereiken van de maximale waarde van het register stuurt de timer een interrupt en telt verder vanaf 0. De maximale waarde van een 16-bit register is 65536. In principe loopt de timer dus elke 65536*666,66 ns = 43,69 ms af. We kunnen de timer echter ook instellen om na een vooropgestelde tijd t af te lopen. Dit doen we door in het TMR1-register een waarde w te schrijven zodanig dat het nog t seconden zal duren alvorens de waarde 65536 bereikt wordt. We kunnen deze waarde als volgt berekenen:

w = 65536 - t / 666,66 ns

Voor een tijd van 1 ms krijgen we een waarde w = 65536 - 1 ms/ 666,66 ns = 65536 - 1500 = 64036. Voor een tijd van 19 ms krijgen we een waarde van w = 65536 - 19 ms/ 666,66 ns = 65536 - 28500 = 37036. We willen dat de puls 1 ms duurt, dus moeten we de waarde 64036 in het TMR1-register schrijven. Omdat de PIC18F4550 een 8-bit microcontroller is moet, het schrijven van een 16-bit register in twee stappen gebeuren. Eerst schrijven we de minst significante 8 bits van het register (TMR1L) en daarna schrijven we de meest significante 8 bits van het register (TMR1H). In de C-code ziet dat er als volgt uit.

  1. TMR1L = 64036 & 0x00FF;
  2. TMR1H = (64036 & 0xFF00) >> 8;

We moeten TIMER 1 nog activeren. Dit doen we door het TMR1ON-bit van het T1CON-register hoog te maken.

  1. T1CONbits.TMR1ON = 1;

Initialisatie interrupts

In de volgende stap stellen we de microcontroller zodanig in dat die op de TIMER1-interrupt gaat reageren door de ISR uit te voeren. Hiervoor moeten we achtereenvolgens

  • Interrupts toelaten door het Global Interrupt Enable-bit (GIE) op 1 te zetten;
  • Interrupts van randapparaten zoals TIMER1 toelaten door het Peripheral Interrupt Enable-bit (PEIE) op 1 te zetten;
  • Interrupts van TIMER1 toelaten door het TMR1IE-bit op 1 te zetten en het TMR1IF-bit op 0 te zetten.

Meer details vind je in de PIC18F4550 datasheet External link.

  1. INTCONbits.GIE = 1;
  2. INTCONbits.PEIE = 1;
  3. PIE1bits.TMR1IE = 1;
  4. PIR1bits.TMR1IF = 0;

Interrupt Service Routine

De Interrupt Service Routine (lijn 15-29) wordt opgeroepen telkens als er een interrupt optreedt, dus niet enkel voor interrupts die afkomstig zijn van TIMER1. Daarom controleren we aan het begin van van de ISR of de opgetreden interrupt afkomstig is van TIMER1 (lijn 17). Daarna komt de code voor het behandelen van de TIMER1-interrupt (lijn 18-27). Als de servo1-pin op 1 staat, dan zetten we de servo1-pin op 0 en stellen we de timer in op 19 ms. Als de servo1-pin op 0 staat, dan zetten we de servo1-pin op 1 en stellen we de timer in op 1 ms. Op het einde (lijn 27) zetten we de TMR1IF-bit op 0 zodat er opnieuw TIMER1-interrupts toegelaten worden.

Als laatste bespreken we lijn 31-37. Als een interrupt optreedt, reageert de microcontroller hierop door niet te de volgende instructie uit te voeren maar de instructie op een vastgelegd adres. In het geval van de PIC18F4550 is dit het adres 0x08. We moeten ervoor zorgen dat de ISR uitgevoerd wordt als er een interrupt optreedt. Lijnen 31-37 zorgen hiervoor door op adres 0x08 een sprong instructie naar de ISR te plaatsen.

Inhoud syndiceren