PIC 16F684 van knipperlicht naar... (pagina 7)

Servotester

Schema      JAL code


Modelbouwservo's, zoveel types, zoveel eigenschappen. Om de instellingen van een individuele servo te weten, is een servotester noodzakelijk.

De meeste (analoge) servo's werken volgens hetzelfde principe: een motor bedient over een tandwielreductie een arm. Op die arm is een tergkoppeling voorzien in de vorm van een regelbare weerstand. De servo krijgt een puls en de interne electronica probeert om de potmeter (en de arm) zo te verdraaien dat de inwendig opgewekte puls overeenkomt met de aangeboden puls. Zijn beide pulsen gelijk dan is de gewenste positie bereikt. De servo probeert nu beide pulsen gelijk te houden en bij mechanische belasting van de arm wordt de servo bijgeregeld.
Een servo verwacht een puls die tussen de 1ms (uiterst linkse positie van de arm) en 2ms (uiterst rechts) ligt. Een puls met een lengte van 1,5ms zet de servo in zijn middelste positie. De pulsen moeten herhaald worden met een frequentie van 50Hz (Wiki-servo).

De hier beschreven servotester kan een puls genereren tussen de 1ms en 2ms om zeker de uiterste standen van een servo te kunnen meten.
Servo's kunnen hogere en lagere waarden van die puls aan (500µs tot 2,5ms) maar bij die uiterste standen gaat de servo steeds proberen om die stand aan te houden, wat kan leiden tot een hoog stroomverbruik. Het is aangeraden om servo's te testen met een amperemeter opgenomen in de positieve voedingsspanning.

De servotester werkt als volgt: de pulsingang van de servo wordt verbonden met pen A2. Na het inschakelen van de voeding komt op het display de waarde '1500 us' te staan en gaat de servo in de 'neutrale' stand. Een druk op de knop en de servo gaat met stapjes van 1ms over neutraal (1,5ms) naar 2ms en blijft dit steeds herhalen (links - neutraal - rechts - neutraal -links - ...).
Nogmaals drukken op de knop en de servo kan bediend worden met de regelbare weerstand. De waarde van de puls (in microseconden) wordt op het display weergegeven.

Het programma is opgebouwd uit verschillende 'blokken'. Een groot deel van die programmablokken zijn al aan bod gekomen in vorige projecten zodat daar niet veel uitleg meer aan gegeven wordt.

Het eerste 'blok' is wat commentaar over wat het programma doet en de noodzakelijke processorinstellingen.

  ;-------------------------------------------------------------------------------
;16F684
;
;Servotester
;versie 20120806
;-------------------------------------------------------------------------------
include 16f684

pragma target clock 4_000_000 ;oscillator frequency
pragma target OSC XT ;4MHz xTal
pragma target WDT disabled ;no watchdog
pragma target MCLR internal ;reset internally

enable_digital_io() ;alle pinnen digitale IO

 

De instellingen de led, de servo-uitgang en de ingang voor de schakelaar.

  ;LED is via voorschakelweerstand aangesloten op pin A0
alias led is pin_A0
pin_A0_direction
= output
led
= off
alias servo is pin_A2
pin_A2_direction
= output
servo
= off
alias switch is pin_A3
pin_A3_direction
= input

 

De instellingen voor Timer2. Timer2 is het kloppend hart in dit project. Elke 20ms genereert de timer een interrupt. Die 20ms is zo gekozen om een 50Hz verversingspuls voor de servo te genereren.
Verder nog enkele vlaggen die door de interruptprocedure en het hoofdprogramma gebruikt worden om verschillende tijdstippen door te geven.
Ook enkele tellers worden hier geïnitialiseerd: een secondeteller (50 * 20ms), een teller voor het knipperen van de led (hier ingesteld op 400ms) en een teller om de servo een nieuwe waarde tegeven in de RUN-modus (ingesteld op 200ms; dus elke 200ms wordt de waarde van de servo aangepast - die aanpaswaarde wordt bij de servoinstellingen bepaald).

  ;-------------------------------------------------------------------------------
;instellingen voor Timer2
;Prescaler= 16
;PostScaler = 5
;PR2 = 250 - 1 voor herlaadtijd
;Freq = 50 Hz
;Tijd = 0.02 seconden
;-------------------------------------------------------------------------------
T2CON_TOUTPS = 0b0100 ;postscaler
T2CON_T2CKPS = 0b11
PR2 = 249 ;herlaadwaarde
PIE1_TMR2IE = high ;Timer2 interrupt enable
INTCON_PEIE = high ;perifierie interrupt enable
PIR1_TMR2IF = low ;wis interrupt vlag

var volatile byte TimeFlags = 0
var volatile bit FlagTimePassed at TimeFlags : 0
var volatile bit Flag20msPassed at TimeFlags : 1
var volatile bit FlagBlink at TimeFlags : 2
var volatile bit FlagBlinkState at TimeFlags : 3
var volatile bit FlagServoStep at TimeFlags : 4
var volatile bit FlagClear at TimeFlags : 7
;een seconde
const byte TimerCounterReload = 50
var byte TimerCounter = TimerCounterReload
;Led knipper frequentie
const byte BlinkTimerReload = 20
var byte BlinkTimer = BlinkTimerReload
;Servo update frequentie
const byte ServoStepReload = 10
var byte ServoStepTimer = ServoStepReload

 

De initialisatie van het display (idem als bij 'Hello world'). Vlak voor de initialisatie van de LCD wordt er 500ms gewacht (de processor doet niets) en dit om 'tragere' LCD modules zichzelf te laten initialiseren na een power-up.

  ;-------------------------------------------------------------------------------
;LCD declaratie en initialisatie
;-------------------------------------------------------------------------------
;Volgende constanten moeten gedeclareerd worden:
const byte LCD_ROWS = 2 -- 1, 2 or 4 lines
const byte LCD_CHARS = 16 -- 8, 16 or 20 chars per line
;Alisassen voor de handshake-lijnen:
alias lcd_rs is pin_c5 -- cmd/data select
alias lcd_en is pin_c4 -- trigger
pin_C5_direction = output
pin_C4_direction
= output
;Aliassen voor de vier datalijnen:
alias lcd_dataport is portC_low -- 4 databits
pin_C0_direction = output
pin_C1_direction
= output
pin_C2_direction
= output
pin_C3_direction
= output

;voor de aanduiding van 'micro' gebruiken we een 'u'
;het karakter 'mu' bestaat niet standaard op het display
;en kan eventueel zelf aangemaakt worden
const byte sTime[] = " us"

;laad de eigenlijke bibliotheek
include lcd_hd44780_4
;wacht even om trage LCD's te laten resetten
include delay
delay_100ms(5)
;en initialiseer het display
lcd_init() -- init the lcd controller
include print

;scherm wissen
lcd_clear_screen()

 

De ADC instellingen: High-resolution (10 bits) en RA1 als analoge input.

  ;-------------------------------------------------------------------------------
;ADC instellingen
;-------------------------------------------------------------------------------
const bit ADC_HIGH_RESOLUTION = true
const byte ADC_NVREF = 0
;de bibliotheek laden
include adc
;De ADC initialiseren
adc_init()
;en maak pen 12 (RA1/AN1) analoge ingang
set_analog_pin(1)
var word wADCValue

 

Timer1 instellingen. Timer 1 zorgt voor een tijdsduur van de servopuls. De timer is ingesteld op 'ticks' van 1µs zodat een herlaadwaarde van 1000 exact 1ms geeft (1500 geeft 1.5ms en 2000 geeft 2ms).
Timer1 is een 'upcounter'. De timer telt vanaf 0 tot zijn maximum waarde 65535 en bij de volgende telpuls gaat deze over van 65535 naar 0 (16 bits teller). Bij deze overgang wordt (indien ingesteld) een interrupt gegenereerd. Om nu de teller een puls van 1.5ms te laten genereren, moeten we wat rekenen. Van de maximale tellerstand (=65535) moeten we 1500 aftrekken om de herlaadwaarde te bekomen.
Een ander 'addertje onder het gras' is dat de herlaadwaarde uit 2 maal 8 bits registers bestaat. 1500 als waarde past al niet meer in een 8 bit register zodat we een 16 bit variabele moeten maken (word-variabele). Om deze 16 bit variabele aan 2 * 8 bits registers toe te kennen moeten we een trukje toepassen: op hetzelfde adres van de 16 bits variabele maken we een array van 2 * een 8 bit variabele. De ene 8 bits bevatten de hoge waarde, de andere de lage lage waarde (wServoReload = 16 bits, ServoReload[2] = 2 * 8 bits).
Met de individuele elementen van de array kunnen we de 8 bits registers laden (TMR1L = ServoReload[0]).

  ;-------------------------------------------------------------------------------
;Servo en timer1 instellingen
;-------------------------------------------------------------------------------
var volatile word wServoReload = 65535 - 1480
var volatile byte ServoReload[2] at wServoReload
var volatile word wServoValue = 1500
var byte NewServoValue = 0
var byte ServoDir = 0
var word wServoStep = 10

T1CON = 0b00000100
TMR1H = ServoReload[1]
TMR1L = ServoReload[0]

T1CON_T1CKPS = 0 ;geen prescaler
T1CON_T1OSCEN = high ;oscillator enable
T1CON_TMR1CS = low ;internal clock
T1CON_TMR1ON = low ;Timer1 off
T1CON_NT1SYNC = high ;do not synchronize clock
PIE1_TMR1IE = high
INTCON_PEIE = high

 

De interruptprocedure. Er zijn nu twee interruptbronnen: timer2 en timer1 die elk afzonderlijk (op verschillende of gelijke tijdstippen) een interrupt kunnen genereren. Zaak is nu om te kijken welke interrupt de hoogste prioriteit heeft.
Wanneer Timer1 een interrupt genereert, wil dit zeggen dat de servopuls is afgelopen. Servo's zijn niet heel echt kritisch wat betreft hun herhalingsfrequentie maar wel wa betreft de stabiliteit van hun pulsduur. Om die reden wordt de Timer1 interrupt als eerste aangepakt.
Direct wordt bij de interrupt het servosignaal op 'laag' gezet (einde van de servopuls). De interruptvlag wordt gewist en er wordt gekeken of er een nieuwe herlaadwaarde is voor de servo. Die herlaadwaarde wordt berekend (hierbij wordt rekening gehouden met de tijd die nodig is om tot op dit punt in de interruptroutine te komen - JAL saved bepaalde registers voor het uitvoeren van een interruptroutine). De eventuele nieuwe waarde wordt terug in de tellerregisters geplaats.

  ;-------------------------------------------------------------------------------
;INTERRUPT PROCEDURE
;-------------------------------------------------------------------------------
procedure TimerInterrupt is
  
pragma interrupt

  
;Timer 1 interrupt
  
if(PIR1_TMR1IF)then
    
;servo puls afgelopen
    
Servo = low
    
;stop de teller
    
T1CON_TMR1ON = low
    
;reset Timer1 interruptvlag
    
PIR1_TMR1IF = low
    
;Nieuwe waarde?
    
if(NewServoValue > 0)then
      
;De tijd dat de servopluls 'actief' moet zijn (tussen 1000 en 2000 µs)
      ;wordt berekend door de van de maximale tellerwaarde (0xFFFF = 65536)
      ;de werkelijke waarde af te trekken.
      ;De waarde 20 die extra bijgeteld wordt is om de 'verloren' microseconden
      ;te compenseren die nodig zijn om tot in de interruptprocedure te geraken
      ;(oa saven van registers).
      
wServoReload = 65535 - wServoValue + 20
    
end if
    
TMR1H = ServoReload[1]
    TMR1L = ServoReload[0]
end if


 

Timer2 interrupt: 20ms zijn gepasseerd en dit wil zeggen dat er een nieuwe servopuls moet opgewekt worden. De routine begint daarmee (Servo = high). En Timer1 wordt gestart.
Verder kijkt de routine nog naar de verschillende tellerstanden voor de verschillende tijdsafhankelijke handelingen in de main-loop (secondenteller, ledknipperteller en servo-update-teller).

    ;Timer 2 interrupt
  
if(PIR1_TMR2IF)then
    
;20ms zijn om: zet de servo en de teller aan
    
Servo = high
    
T1CON_TMR1ON = high
    
;reset Timer2 interrruptvlag
    
PIR1_TMR2IF = low
    
;meldt 20 ms gepasseerd
    
Flag20msPassed = true
    
;laat dit 50 keer gebeuren voor 1 seconde
    
TimerCounter = TimerCounter - 1
    
if(TimerCounter == 0)then
      
TimerCounter = TimerCounterReload ;herlaad de teller
      
FlagTimePassed = true ;tijd is verstreken
    
end if
    
;ServoStepTimer
    
ServoStepTimer = ServoStepTimer - 1
    
if(ServoStepTimer == 0)then
      
ServoStepTimer = ServoStepReload
      FlagServoStep
= true
    
end if
    
;Knipperteller
    
BlinkTimer = BlinkTimer - 1
    
if(BlinkTimer == 0)then
      
BlinkTimer = BlinkTimerReload
      FlagBlink
= true
    
end if
  end if
  
;Einde interrupt
end procedure

 

Instellingen voor de schakelaar: drie modi zijn beschikbaar: neutraal - run - manueel.
Om schakeldender te onderdrukken, wordt een hulpvlag gebruikt.

  ;-------------------------------------------------------------------------------
;Instellingen voor de schakelaar
;-------------------------------------------------------------------------------
const byte SW_MODE_NEUTRAL = 0
const byte SW_MODE_RUN = 1
const byte SW_MODE_MANUAL = 2
var byte SW_MODE = SW_MODE_NEUTRAL
var byte SW_MODE_CHANGE = 1 ;voor initialisatie!

const byte SW_ON = 1
const byte SW_OFF = 0
var byte SW_STATE = SW_OFF
var byte SW_PREVIOUS_STATE = SW_OFF

 

Vlak voor de main-loop worden de interrupts ge-enabled.

  ;-------------------------------------------------------------------------------
T2CON_TMR2ON = high ;turn timer2 on;
INTCON_GIE = high ;enable interrupts

 

En uiteindelijk de main-loop.

Die main-loop is eigenlijk niets meer dan een asynchrone afhandeling van de vlaggen die gezet zijn tijdens de interruptroutine. Asynchrone afhandeling omdat timing hierbij geen rol speelt.
Volgende zaken worden afgehandeld:
- het verstrijken van de 20ms
- het in het oog houden van de schakelaar
- eventueel reageren op een wijziging van de schakelaar
- in de RUN-modus de servo updaten
- de led laten knipperen
- de één seconde overgang in de gaten houden
Elk deeltje wordt afzonderlijk behandelt.

Het verstrijken van de 20ms (het belangrijkste gebeuren).
Als eerste wordt de vlag gereset.
In MANUL-MODE wordt elke 20ms de ADC uitgelezen en wordt de waarde toegekend als nieuwe waarde voor de servo en wordt deze op het display weergegegeven.

  ;-------------------------------------------------------------------------------
;Main loop
;-------------------------------------------------------------------------------
forever loop
  if
(Flag20msPassed)then
    
;20ms zijn gepasseerd, reset de vlag
    
Flag20msPassed = low
    
if(SW_MODE == SW_MODE_MANUAL)then
      
;De stand van de potmeter wordt gebruikt voor de servowaard.
      ;Lees de ADC (10 bits -> waarde van 0 tot 1023)
      
wADCValue = adc_read_high_res(1)
      ;de lsb hebben we niet nodig, zorgt voor jitter
      
wADCValue = wADCValue >> 1
      
;We hebben nu een waarde van 0 tot 511. Maal 2 geeft dit terug een
      ;waarde van 0 tot 1022 (die 1000 hebben we nodig voor 1ms)
      
wADCValue = wADCValue + wADCValue
      
;Tel de offset bij voor een waarde van 1ms tot 2ms
      
wServoValue = 1000 + wADCValue
      
;En zet die waarde op het display
      
lcd_cursor_position(1,0)
      print_word_dec(lcd, wServoValue)
      print_string(lcd, sTime)
      ;geef de interruptroutine te kennen dat er een nieuwe waarde is
      
NewServoValue = 1
    
end if
 

In die 20ms vlag wordt ook gekeken of de schakelaar gewijzigd is. De schakelaar is actie 'laag' (via pull-up naar Vcc). Om schakeldender te voorkomen wordt de toestand van de schakelaar pas als actief gemeld wanneer deze na 20ms nog steeds laag is.
Bij een 'actieve' schakelaar wordt de modusteller verhoogd en wordt er overgegaan naar een andere modi (neutraal - run - manueel) waarbij een vlag gezet wordt.

      ;Schakelaar gewijzigd? (laag waar)
    
if(Switch == high)then
      
;Als de vlaggen nog aan stonden dan afzetten
      
if(SW_STATE == SW_ON)then
        
SW_STATE = SW_OFF
        SW_PREVIOUS_STATE
= SW_OFF
      
end if
    end if
    if
(Switch == low)then
      
;Als het indrukken van de schakelaar nog niet gemeld is, melden
      
if(SW_STATE == SW_OFF)then
        
;dender onderdrukken
        
if(SW_PREVIOUS_STATE == SW_OFF)then
          
;zet die vlag al en na 20 ms de eigenlijke vlag
          
SW_PREVIOUS_STATE = SW_ON
        
else
          
;de vorige keer was de schakelaar al ingedrukt, geen dender
          
SW_STATE = SW_ON
          
;verhoog de MODE
          
SW_MODE = SW_MODE + 1
          
if(SW_MODE > SW_MODE_MANUAL)then
            
SW_MODE = SW_MODE_NEUTRAL
          
end if
          
;en meldt dit
          
SW_MODE_CHANGE = 1
        
end if
      end if
    end if
  end if

 

Telkens wanneer de modus gewijzigd wordt, wordt er een initialisatie uitgevoerd.
Bij 'neutraal' wordt de servopuls op 1500 gezet en wordt die waarde op het display gezet.
Bij mode 'run' wordt enkel aangegeven dat de waarde moet verhoogd worden. Bij het omschakelen van 'neutraal' naar 'run' staat de servo reeds in 'neutraal' en staat de waarde 1500us reeds op het display.
Bij mode 'manual' wordt enkel het display gewist (na 20 ms wordt dan de effectieve waarde van de potmeter op het display gezet).

    if(SW_MODE_CHANGE != 0)then
    
;Mode is gewijzigd!
    ;initialiseer
    
SW_MODE_CHANGE = 0
    
if(SW_MODE == SW_MODE_NEUTRAL)then
      
;1500 us
      ;zet 1500 op de bovenste regel
      
lcd_clear_screen()
      wServoValue = 1500
      
lcd_cursor_position(0,0)
      print_word_dec(lcd, wServoValue)
      print_string(lcd, sTime)
      NewServoValue = 1
    
end if
    if
(SW_MODE == SW_MODE_RUN)then
      
;begin neutraal en verhoog; de juiste stand staat al op de LCD
      
wServoValue = 1500
      
ServoDir = 1
    
end if
    if
(SW_MODE == SW_MODE_MANUAL)then
      
;Wis het display en vanaf nu is er elke 20 ms een update
      
lcd_clear_screen()
    end if
  end if

 

In de run-modus wordt bij de huidige waarde van de servo telkens de waarde wServoStep bij de huidige servowaarde (in wServoValue ) bijgeteld of afgetrokken. Het optellen gebeurd tot de waarde groter zou worden dan 2000, waarna de waarde afgetrokken wordt dot dat deze kleiner zou worden dan 1000 waarna de cyclus zich herhaalt.
De huidige waarde wordt telkens op het display gezet.

    if(FlagServoStep)then
    if
(SW_MODE == SW_MODE_RUN)then
      
;pas de servo aan (enkel in RUN modus)
      
if(ServoDir == 0)then
        
;aflopend
        
if((wServoValue - wServoStep) >= 1000)then
          
wServoValue = wServoValue - wServoStep
          NewServoValue
= 1
        
else
          
ServoDir = 1
        
end if
      else
        
;oplopend
        
if((wServoValue + wServoStep) <= 2000)then
          
wServoValue = wServoValue + wServoStep
          NewServoValue
= 1
        
else
          
ServoDir = 0
        
end if
      end if
      
lcd_cursor_position(0,0)
      print_word_dec(lcd, wServoValue)
      print_string(lcd, sTime)
    end if
    
FlagServoStep = low
  
end if
 

Om de 400ms wordt vlag 'FlagBlink' gezet. Hiet wordt de led omgeschakeld (met een eenvoudige NOT operator) en wordt de vlag terug gewist.

    if(FlagBlink)then
    
;knipper de led
    
Led = !Led
    FlagBlink
= low
  
end if

 

Elke seconde wordt de vlag 'FlagTimePassed' gezet. Hier wordt niets uitgevoerd.

    if(FlagTimePassed)then
    
;1 seconde
    
FlagTimePassed = low
  
end if
  
;Einde main loop
end loop
;-------------------------------------------------------------------------------

 

Einde van het programma.

 


(pagina 6) ...PIC 16F684 van knipperlicht naar... (pagina 8)



Links

JALEdit de editor om JAL code te schrijven
JALlib de bibliotheken (en voorbeelden) en de compiler
MPLab IDE de ontwikkel- en PicProgrammeromgeving
Microchip de site van de PIC's
JalList Hulp en hopen voorbeelden