PIC microcontroller tutorial

Gepost door Bastiaan Steenbergen op donderdag 4 december 2003

Het schrijven van een programma

Zonder software zal een µC weinig uithalen. Deze software moet eerst zelf worden ontworden en geschreven. Het schrijven van software gebeurt net zoals dat bijvoorbeeld in C++ programma's worden geschreven. Alleen zijn de commando's wat anders. C++ is een hogere programmeertaal. De taal waarmee je een µC programmeert is een lagere taal. Deze taal noemen ze assembler taal. Waarom het een lagere taal heet wordt je tijdens de uitleg van de taal wel duidelijk. Zodra de software klaar is moet het worden geassembleerd. Dit wordt gedaan door een assembler die afhankelijk is van het type en merk van de µC. Elk merk fabrikant heeft zijn eigen assembler. Een assembler zet het geschreven programma om in een string van hexadecimale tekens die de µC begrijpt.

Omdat een µC nu eenmaal digitaal werkt zul je heel veel bij het programmeren tegen bits en en bytes oplopen. Ik veronderstel dat je wel weet wat bits zijn en hoe de nummering werkt en wat de plaatswaardes zijn van bits. Microcontrollers kunnen alleen hele simpele bewerkingen uitvoeren. Toch kun je, met al die bewerkingen samen, een heel groot complex geheel maken. Het rekenen gaat met bits, het verplaatsen gaat met bits, alles gaat gewoon met bits.

Om deze tutorial wat leuker en meer praktischer te maken hebben we er een miniproject aan vastgeknoopt. We zullen voor dat project een programma schrijven dat uiteindelijk hardware zal aansturen. Ondanks dat de µC weinig commando's heeft bespreken we ze niet allemaal. De reden darvoor is dat veel commando's op elkaar lijken zodoende kun je ze zelf ontdekken.

Commando's heten in assembler taal "Instructies". De µC die we gebruiken heeft 35 van deze instructies. Hij valt daarom onder de categorie RISC processor. Dit betekent "Reduced Instruction Set Computer", wat inhoudt dat hij maar weinig instructies heeft. Er bestaan processors met veel meer instructies. Alle instructies worden sequentieel uitgevoerd, dus in de volgorde dat ze zijn geschreven. We zullen uitleggen hoe het uitvoeren van instructies werkt, zodat je een beter beeld hebt van het geheel en beter snapt waarom je iets op een bepaalde manier moet doen en niet anders. De chip heeft een permanent programmageheugen. Hierin staan jouw eigen instructies onder elkaar. Zodra de µC een voedings spanning krijgt aangeboden, en er een kloksignaal aanwezig is op zijn klok-input-pinnen, gaat die beginnen. Intern heeft de controller een teller die bijhoudt waar die is met het uitvoeren van zijn instructies. Deze teller noemt men "program counter". Helemaal in het begin is deze program counter 0000, wat dus inhoudt dat die de instructie op adresgeheugen nummer 0000 moet gaan uitvoeren. Deze counter wijst altijd naar de volgende uit te voeren instructie. Omdat het wijst naar 0000 wordt dit geheugenadres gelezen en wordt de instructie, zoals ze dat noemen, opgehaald. En wordt meteen de program counter verhoogd met 1 zodat die weer wijst naar de volgende uit te voeren instructie. Na het ophalen van een instructie wordt deze vertaald zodat de processor weet wat die ermee moet gaan doen. Zodra die gereed is met het uitvoeren van zijn instructie kan die zijn volgende instructie gaan ophalen, vertalen en uitvoeren, meer gebeurt er niet. Het is telkens ophalen, vertalen en uitvoeren van instructies. Dit gaat net zolang door tot het programma is afgelopen of totdat er een externe reset word gegeven.

Zoals er bij het "Hardware" gedeelte is vertelt werkt deze controller met een working register, W genaamd. Dit register wordt gebruikt voor het transport van data, namelijk van dataregister tot dataregister. Er kunnen alleen complete bytes worden verplaatst, dus een reeks van 8 bits. Wel is het mogelijk om direct een bit in het geheugen te veranderen van 0 naar 1 of andersom. Zodra er een 1 staat en je wilt er een 0 van maken dan heet deze handeling het "clearen" van dat bit. Wil je van een 0 een 1 maken dan noemen ze dit het "setten" van een bit.
Stel dat je de data uit register 0Ah naar register 21h wilt verplaatsen dan dien je eerst de data uit 0Ah naar het W register te verplaatsen, en dan kan het W register het pas verplaatsen naar dataregister 21h. De adressen en getallen die worden gebruikt bij de assember taal zijn meestal hexadecimale getallen. Je dient dit aan te geven in je programma. Zo is 21h de hexadecimale variant van het decimale 33d. Die h geeft dus aan dat het om een hex-getal gaat en de d dat het om een decimaal getal gaat. Alle niet ingevulde bits voor je getal worden als een 0 verondersteld. Zet je bijvoorbeeld in de assembler 21h neer dan is dit eigenlijk 021h. Je kunt als je dat wilt ook een hexadecimaal getal neerzeten in de vorm van 0x021. In C is dit een veelgebruikte methode om hexgetallen weer te geven. Wil je nu een waarde boven 9Fh neerzetten dan dien je dit vooraf te laten gaan door een 0.
Dus A0h of hoger kent die niet en zal een error geven. Je moet dan neerzetten 0A0h. Bij de andere notatie 0x0A0 heb je hier uiteraard geen last van.

Hieronder zie je een lijst met alle instructies die er zijn:

Instructie Parameters Betekenis
ADDLW k Add literal and W
ADDWF f, d Add W and f
ANDLW k AND literal with W
ANDWF f, d AND W with f
BCF f, b Bit clear f
BSF f, b Bit set f
BCF f, b Bit clear f
BTFSC f, b Bit test f, Skip if clear
BTFSS f, b Bit test f, Skip if set
CALL k Call subroutine
CLRF f Clear f
CLRW - Clear W
CLRWDT - Clear Watchdog Timer
COMF f, d Complement f
DECF f, d Decrement f
DECFSZ f, d Decrement f, Skip if 0
GOTO k Go to address
INCF f, d Increment f
INCFSZ f, d Increment f, Skip if 0
IORLW k Inclusive OR literal with W
IORWF f, d Inclusive OR W with f
MOVF f, d Move f
MOVLW k Move literal to W
MOVWF f Move W to f
NOP - No operation
RETFIE - Return from interrupt
RETLW k Return with literal in W
RETURN - Return from subroutine
RLF f, d Rotate Left f through Carry
RRF f, d Rotate Right through Carry
SLEEP - Go into standby mode
SUBLW k Subtract W from literal
SUBWF f, d Subtract W from f
SWAPF f, d Swap nibbles in f
XORLW k Exclusive OR literal with W
XORWF f, d Exclusive OR W with f

Links staan de instructies, daarnaast staat wat je voor extra info bij elke instructie moet bijvoegen. En helemaal rechts zie je samengevat wat ze doen. Een literal is een getal.
De betekenis van de letters:

  • f staat voor registerfile, van 00h tot 4Fh
  • d voor destination select. 0 = sla het resultaat op in W, 1=sla het resultaat op in de file
  • b voor bit address
  • k voor constante data of een label
  • - is niks
  • W is working register

We zullen de werking van vele instructies toelichten. Je zult er misschien niet meteen veel van begrijpen, maar dat is niet erg. Als je er mee aan de slag gaat kun je hier naar terugkijken. Het is nu alleen even belangrijk dat je weet wat er ongeveer is en wat het doet. We zullen beginnen bij de bit manipulaties.

BCF f, b Bit clear f

Dit is een instructie waarmee je een bit kunt clearen, dus een 0 maken. Je moet als extra info opgeven in welke file het bit staat, en om welk bitnummer het gaat.
Voorbeeld: BCF 25h, 4h
Hierbij zal dus bitnummer 4 in datafile op locatie 25h, een 0 worden. Stel dat er in adres 25h de bitreeks 0111.0111 staat, dan zal na deze instructie de data op dat adres er als volgt uitzien: 0110.0111

BSF f, b Bit set f

Dit is dezelfde instructie als hierboven alleen dan wordt het bit op de gegevens locatie geset.

MOVLW k Move literal to W

Hiermee kun je in het W-register een getal plaatsen.
Voorbeeld: MOVLW A4h
Het W register zal dan na deze instructie de waarde A4h bevatten.

MOVWF f Move W to f

Hiermee kun je de waarde van W in een file zetten.
Voorbeeld: MOVWF 25h
Stel dat er in W de waarde A4h stond dan bevat na deze instructie de file op adres 25h ook deze waarde. De waarde die in W staat blijft er ook instaan. De MOV die in de instructie staat is wel wat verwarrend, want dit zou suggereren dat het wordt verplaatst. Maar in werkelijkheid wordt het alleen maar gekopieerd.

MOVF f, d Move f

Bij deze instructie wordt de data uit een file gekopieerd naar het W-register.
Voorbeeld: MOVF 25h, 0
Dit zorgt ervoor dat de data uit file 25h in het register W wordt geladen.
Voorbeeld: MOVF 25h, 1
Dit zorg ervoor dat de data niet wordt verplaatst. Wat heeft dat nu voor nut zou je zeggen. Nou toch kan het handig zijn. Het zit namelijk zo. Intern bevat de chip een ALU zoals we hebben toegelicht in het "Hardware" gedeelte. Deze ALU voert alle berekeningen uit. Na een berekening levert de ALU de uitkomst, maar ook geeft deze door middel van een "statusregister" extra informatie. Het statusregister is een dataregister dat zit op adres 03h en het ziet er als volgt uit:

Status registerStatus register

Nu gaat het ons alleen even om het Z-bit en het C-bit. Zoals je kunt zien zit het Z-bit op bitnummer 2, en het C-bit op nummer 0. De Z staat voor Zero. Zodra er een berekening/bewerking wordt uitgevoerd en de uitkomst is een 0, dan maakt de processor van bitnummer 2 een 1'tje. Zo kun je door na die berekening te kijken naar dat bit zien of de uitkomst een 0 was of niet. Het C-bit staat voor Carry. Dit is wel bekend uit de digitale wereld denk ik. Als er een berekening wordt uitgevoerd die een uitkomst heeft die groter is dan de 8bits die ter beschikking staan dan geeft dit bit dat aan. Dus zou je de binaire waarde 1000.0000 optellen bij 1000.0000 dan wordt de uitkomst 1.0000.0000. Maar aangezien de processor maar met 8 bits werkt blijft er als resultaat 0000.0000 over. En om dan dus aan te geven dat het allerlinkse bit buiten het bereik viel maar er wel was wordt het allereerste bit (dus bitnummer 0) van het statusregister een 1. Ook wordt dit bit gebruikt bij de omgekeerde bewerking. Dus bij aftrekkingen die een Borrow nodig hebben. In de datasheet kun je vinden welke instructie welk bit in het statusregister beïnvloed.

Om weer terug te komen bij de instructie MOVF 25h, 1. Deze bewerking laat het getal gewoon in de file zitten, maar werkt wel het statusregister bij. Zo kun je dus informatie krijgen over de waarde van de data in dat register.

CLRF f Clear f
CLRW - Clear W

Deze instructies lijken erg op elkaar en daarom behandel ik ze samen. Je kunt hiermee het register clearen. Dat wil zeggen van alle bits 0'len maken.
Omdat er maar 1 W-register is is het niet nodig extra info bij de instructie op te geven. Bij het clearen van een file moet de controller echter wel weten welke file het is.
Voorbeeld: CLRF 25h
Zo wordt de data in het dataregister op adres 25h binair gezien 0000.0000.

SWAPF f, d Swap nibbles in f

Een file bevat 1 byte aan data, dat zijn dus 8 bits. Nu noemen ze in de digitale wereld de linker en rechterhelft nibbles. Zou de file er als volgt uitzien in bits: 1010.1111
Dan is het linker nibble 1010 en het rechter nibble 1111. Met de bovenstaande instructie kun je die 2 helften met elkaar laten omwisselen, daarom heet het swap.
Zou je dit dus uitvoeren op het voorbeeld 1010.1111 dan zal de data erna 1111.1010 zijn.

COMF f, d Complement f

Met deze instructie is de data te complementeren, wat wil zeggen inverteren. Dus alle bits omdraaien van 0 naar 1 en andersom. Je behoort mee te geven om welke file het gaat en of het resultaat in de file weer moet worden opgeslagen of dat het in W moet worden geplaatst.
Voorbeeld: COMF 25h, 0
Met deze instructie worden de bits in file 25h gecomplementeerd en wordt de uitkomst in W geplaatst. Stel dat er 1000.1111 in de file staat. Na deze instructie staat er dan in het W-register 0111.0000. Let op!, de data in die file blijft onveranderd, het antwoordt werd namelijk niet weer in die file opgeslagen.
Die instructie zorgt ervoor dat de data uit file 25h in de ALU wordt gedaan. De ALU complementeert deze bits. Dus aan de data in de file is niks verandert. En door de optie 0 in de instructie wordt het resultaat in het W-register geplaatst zonder nog aan de file te zitten.

NOP - No operation

Een van de eenvoudigste instructies. Deze doet namelijk helemaal niks. Wat is dan het nut zou je zeggen. Je kunt zo een vertraging in je programma zetten. Stel dat je een extern geheugen vraagt om data. Dit is bijvoorbeeld niet direct beschikbaar. Door een of meerdere NOP's te plaatsen kun je de processor even in tijd laten wachten. Stopzetten kan namelijk niet, immers de klok blijft doorlopen, en daar loopt de µC op.

DECF f, d Decrement f
INCF f, d Increment f

Deze twee instructies nemen we samen aangezien ze simpel zijn en vrijwel identiek qua toepassing. Decrement staat voor vermindering. Deze vermindering houdt in dat de data die in de file staat met één omlaag gaat. De andere instructie doet het omgekeerde, deze zal de waarde van de inhoudt van de file met één ophogen. Het resultaat wordt afhankelijk van de gekozen waarde voor d in W of in de file geplaatst.

ADDLW k Add literal and W

Een instructie die je zo nu en dan eens nodig zult hebben. Je kunt hiermee getallen optellen bij de inhoud van W.
Voorbeeld: ADDLW 04h
Dus stel dat je 04h wilt optellen bij de waarde 24h, dan moet je zorgen dat bv. de 24h in W zit en dan deze instructie uitvoeren met als parameter 04h. Na deze instructie zal W het antwoordt bevatten en dat is in ons voorbeeld dus 28h.

ADDWF f, d Add W and f

Deze functie is vergelijkbaar met die van hierboven. Het enigste verschil is dat nu W wordt opgeteld met de data die in een file zit.
Je kunt door voor d een 0 te maken het resultaat in W laten zetten. De file zal dan alleen worden gebruikt om de data te lezen en zal niet veranderen. Kies je echter voor d de waarde 1, dan is het weer net andersom. Dan zal het antwoordt in de file worden geplaatst en zal W niet wijzigen.

SUBLW k Subtract W from literal

Dit is een instructie waar je snel de mist in kan gaan. Hiermee kun je namelijk W van een getal aftrekken, dus dat is de volgende bewerking: k - W = W. Let dus op, het is dus NIET W - k = W.
Voorbeeld: SUBLW 26h
Stel nu dat er 14h in W staat dan wordt het antwoordt van deze som: 26h - 14h = 12h. Het antwoord wordt altijd in W geplaatst.

SUBWF f, d Subtract W from f

Deze instructie is weer wat meer logisch dan de vorige. Hiermee wordt namelijk het volgende berekend: f - W. Dus de inhoud van W wordt van de inhoud van de file afgetrokken.
Het resultaat wordt geplaatst in W of in f afhankelijk van wat er wordt gekozen voor de waarde van d in de instructie.
Voorbeeld: SUBWF 21h, 1
Door dit uit te voeren zal de ALU een bewerking gaan uitvoeren op de file, het zal de inhoud van W eraf halen en het antwoord terugzetten op de locatie van de file die net is gebruikt.

De volgende serie instructies maken gebruik van digitale logica zoals de functies AND, OR en EXOR.

ANDLW k AND literal with W
ANDWF f, d AND W with f
IORLW k Inclusive OR literal with W
IORWF f, d Inclusive OR W with f
XORLW k Exclusive OR literal with W
XORWF f, d Exclusive OR W with f

Je kunt op deze manier zeer eenvoudig de controller digitale logica laten uitvoeren op data. Inclusive OR is de gewone OR die wij kennen. De Exclusive OR is de EXOR die we wel kennen uit de digitale wereld.
Voorbeeld: ANDWF 24h, 1
Stel in W staat de waarde 3Ah, en in de file op locatie 24h staat 17h. Het antwoord wordt dan 12h, dit zal in de file worden geplaatst omdat d=1 is.
Voorbeeld: XORLW 4Eh
Op deze wijze kun je een getal met de inhoud van W EXOR'ren. Staat er 1Bh in W dan zal na deze instructie W de waarde 55h bevatten.

BTFSC f, b Bit test f, Skip if clear
BTFSS f, b Bit test f, Skip if set

De twee bovenstaande instructies zijn zeer nuttig. Ze testen allebei een bit in een file. Je dient op de plaatst van f aan te geven om welke file het gaat en op de plek van b om welk bit het gaat. De bovenste instructie zal, als het bit dat is getest 0 (dus clear) is, de instructie die volgt overslaan. De BTFSS instructie doet het omgekeerde en zal de opvolgende instructie overslaan in het geval dat het te testen bit 1 (dus geset) was. Deze instructies zijn goed te gebruiken bij het testen van een I/O poort. Door een I/O pin telkens te testen op zijn waarde kun je het programma bij het detecteren bv naar een bepaald stuk programma laten springen.

INCFSZ f, d Increment f, Skip if 0
DECFSZ f, d Decrement f, Skip if 0

Dit zijn 2 instructies die gemakkelijk zijn bij het gebruik van tellers. Ze verhogen namelijk (INCFSZ) of verlagen (DECFSZ) een variabele en zullen zodra de variabele de waarde 0 krijgt de volgende instructie overslaan. Is de waarde 0 nog niet bereikt dan wordt de waarde gewoon opgeslagen en wordt er verder gegaan met de direct daaropvolgende instructies zoals dat gebruikelijk is.

Assembleren

Nu gaan we wat meer uitleggen over wat er nog meer voor mogelijkheden zijn bij het schrijven van een programma.
Het programma dat je schrijft zal aan het einde worden geassembleerd. Dit wil zeggen dat een speciaal programma jouw geschreven code omzet in begrijpbare taal voor de µC.
Het speciale programma dat dit kan heet de 'assembler'. De assembler is specifiek gemaakt voor de µC die je gebruikt. Voordat een assembler je code om gaat zetten zal het eerst je complete code gaan controleren op fouten, aangezien het natuurlijk geen begrijpbare code voor de controller kan maken als jouw eigen programma fouten bevat. Dit controleren gebeurt op basis van de instructie set. De assembler moet daarom zijn gemaakt voor je controller omdat het moet weten welke instructies er bestaan en hoe ze werken. Om nog meer gebruikersgemak in het ontwikkelen van een programma te krijgen zijn er opties toegevoegd aan de instructies die je mag gebruiken. Zo is er een code bijgevoegd genaamd EQU. Dit is een niet bestaande µC instructie maar toch kun je het gebruiken. Met een voorbeeld zullen we verduidelijken wat je ermee kunt.
Stel je wilt de volgende instructie uitvoeren: MOVWF 2Dh (dit zal de waarde van W in de file op adres 2Dh zetten).
Nu zal het beste vaak voorkomen dat je deze instructie gebruikt op ook nog eens telkens hetzelfde adres. Ook zul je deze instructie in je programma meerdere keren gebruiken alleen dan met andere file's. Je zult dus telkens moeten onthouden welk file nummer de waarde van bijvoorbeeld je teller bevatte. Computers zullen dan wel makkelijk met getallen rekenen en doen, wij mensen zijn qua getallen toch wat minder goed. Nu kun je dit vereenvoudigen door de functie EQU te gebruiken. En dat doe je als volgt.
Stel dat je een teller in je programma bijhoudt, die is opgeslagen in de file op adres 2Dh. Je kunt dan helemaal boven in je programma de volgende regel intypen:

Teller	EQU	2Dh

Dit betekent dan dat overal waar jij in je programma invult Teller, dat daar de waarde 2Dh moet komen te staan. Let op, dit is hoofdletter gevoelig.
Zoals we al eerder hebben vertelt is dit geen instructie maar toch kun je het gebruiken omdat dit een speciale optie is die bij de assembler is toegevoegd. Als je je programma gaat assembleren zal de assembler overal waar Teller staat het vervangen door 2Dh en vervolgens deze EQU regel verwijderen omdat het niks voor de µC betekent. Op deze manier kun je veel beter en sneller je programma schrijven, omdat je de locatie van de Teller niet meer hoeft te onthouden.
De instructie van het voorbeeld MOVLW 2Dh, kun je nu dus schrijven als MOVLW Teller.

EQU is niet het enigste ongewone commando dat je kunt gebruiken.
Ook het commando END behoort niet tot de instructieset maar dien je wel te gebruiken. Helemaal aan het einde hoor je END neer te zetten. Zo weet de assembler dat dat de plek is waar het programma ophoudt, en dat die dus klaar is met assembleren want er is geen code meer.

Zodra de controller wordt gestart, of gereset, zal deze als eerste de instructie op adres 0000h gaan bekijken. We hebben dit al eerder uitgelegd in het stukje over de program counter. Je moet er dus voor zorgen dat je allereerste uit te voeren instructie op dat adres begint. Dit doe je door de assembler extra informatie aan te bieden. Door het commando ORG kun je dit duidelijk maken.

	ORG	00h

Als je dit helemaal aan het begin van je instructies plaatst dan weet de assembler dat de code die volgt na ORG op adres 00h moet beginnen.

Labels

Niet alleen EQU is extra gemaakt om het jouw te vergemakkelijken. Ook heb je de mogelijkheid om labels te gebruiken. Een label is een naam die je kunt toekennen aan een bepaalde locatie/regel in je geheugen. Wil je bv het commando GOTO gebruiken dan moet je kunnen vertellen waarheen. Hiervoor moet je de regel waar naartoe moet worden gesprongen een naam geven.

		ORG	00h
Teller  	EQU	2Dh
		GOTO	opnieuw
		MOVLW	14h
		ANDLW	40h
opnieuw	        MOVLW	teller
		INCF	teller, 1
		END

Hierboven zie je een klein voorbeeld dat gebruik maakt van labels, van het commando ORG, EQU en END. Het programma slaat nergens op maar het is alleen om de commando's te verduidelijken. GOTO betekent dat er ergens naartoe moet worden gesprongen. Wij vertellen via regel 3 dat er naar 'opnieuw' moet worden gesprongen. Door nu 'opnieuw' voor de regel waar je naar toe wilt springen te zetten weet de controller dat hij daar naartoe moet. In dit geval zal hij dus na het uitvoeren van regel 3 springen naar de regel MOVLW teller en zal vervolgens, zoals normaal, weer regel voor regel verder afwerken. De regels 4 en 5 zullen dus nooit worden uitgevoerd omdat hij daar simpelweg overheen springt.

De wijze van schrijven

Het intypen van je programma is zeer gemakkelijk en net zoals je bent gewent. Gewoon alles achter elkaar typen met een spatie tussen de verschillende onderdelen.
Er is echter een klein verschil. Je programmeerveld is opgedeeld in twee stukken, gescheiden door een TAB. Je moet in het meest linkse gebied de labels plaatsen, heb je geen label dan dien je een TAB vooraf aan je instructie te geven. Met een klein voorbeeld zullen we dit toelichten:

Optellen INCF teller, 1

Dit is met label, namelijk het label Optellen en dus kun je direct erachter na de spatie je instructie typen.
Stel nou dat deze regel geen label had dan werd het als volgt:

   INCF teller,1

Dus eerst een TAB en daarna pas begin je met je commando. De assembler verwacht namelijk in de 1e kolom een label. Je mag, dit hoeft dus niet, tussen het label en tussen je instructienaam een TAB geven. Het maakt het stukje programma namelijk wat overzichtelijker. Zelf doen we dit graag voor de overzichtelijkheid. Voor de assembler maakt het niks uit of je nou:

Optellen INCF teller,1

doet met alleen spatie's of:

Optellen	INCF	Teller, 1

met TAB's ertussen. De keuze is aan jouw.

Nu heb je genoeg kennis om te beginnen met het schrijven van een programma. Het schrijven van een programma kun je simpelweg gewoon in Notepad of een andere teksteditor doen. Je dient het alleen nog wel te assembleren en je wilt het misschien ook nog wel eerst simuleren. Hiervoor heb je een speciaal programma nodig. Dit staat in het volgende stuk van deze tutorial uitgelegd.