PIC 16F684 van knipperlicht naar... (pagina 7)
Servotester
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