elomax

[ Techniek ] [ Shop ] [ Site map
Assembler ] [ Programmeren ] Foutmeldingen ]

Terug
Omhoog

De opbouw van een programma

Een programma bestaat uit een (lange) lijst instructies. Het is bijna niet mogelijk een correct werkend programma te schrijven, als er niet enige logische ordening in de instructies wordt gebracht. Hiervoor staan verschillende technieken ter beschikking. De tekst laten inspringen en van voldoende commentaar voorzien is daar één van, maar ook de manier waarop u het programma samenstelt is van belang voor de leesbaarheid en begrijpelijkheid.

De wijze waarop een programmeur zijn programma's indeelt is haast zo individueel als een handtekening. Op een grotere afdeling waar meerdere programmeurs werken, die elkaars werk goed kennen, kan men vaak in een oogopslag zien wie van de collega's een bepaald programma geschreven heeft.

Een goede manier om een programma in te delen, die ook in de praktijk heeft bewezen goed te werken, zal hieronder besproken worden.

Modulen

Om een programma overzichtelijk te houden, kunt u het programma opknippen in stukken die elk een gedeelte van de functionaliteit van het uiteindelijke programma voor hun rekening nemen. Zo'n stuk programma noemen we een module. Een module moet bij voorkeur een welomschreven functie vervullen. Als de module goed geschreven is, zou u hem in meerdere programma's moeten kunnen gebruiken.

Een module kan bijvoorbeeld allerlei rekenroutines bevatten, of alle code die nodig is om een bepaald type I/O te besturen, of code die nodig is voor het vervullen van een bepaalde functie (bijvoorbeeld een LED laten knipperen, een analoge meting te doen, etc..)

Een programma wordt opgebouwd uit modulen en aansluitend een hoofdprogramma. Het hoofdprogramma is in feite ook een module, maar dan één die alle andere modulen mag gebruiken om de uiteindelijke functionaliteit van het programma te realiseren. Wanneer de meeste code in modulen is geplaatst, is het hoofprogramma kort en overzichtelijk.

Een module is uiteindelijk niet meer dan een stuk tekst in het programma. Een module die u in meerdere programma's wilt toepassen, kunt u natuurlijk ook in een eigen file zetten. De module kan dan in een programma worden ingelezen door middel van de 'incl' pseudo-instructie.

Een module heeft gewoonlijk de volgende onderdelen:

  • initialisatie

  • interne routines

  • interface routines

Initialisatie

Tijdens initialisatie van een module worden de voor die module gebruikte variabelen geladen met een beginwaarde. Wanneer de module betrekking heeft op I/O wordt tijdens initialisatie ook de betreffende I/O op de gewenste manier ingesteld. Nadat de module geïnitialiseerd is, moet de module bruikbaar zijn voor toepassing door het hoofdprogramma.

Interne routines

Ook de werking van een module kan verder opgesplitst worden in nog kleinere stukjes programma, die er samen voor zorgen dat de functies die de module moet verrichten verzorgd worden. Deze stukjes kunt u het best opnemen in de vorm van subroutines. Een subroutine is ook een afgebakend stukje code met een welomschreven functie. Interne subroutines zijn alleen bedoeld om vanuit de module zelf te worden aangeroepen.

Interface routines

De functionaliteit die door een module verzorgd wordt, moet natuurlijk beschikbaar gemaakt worden aan het hoofdprogramma. Dit doet u door in de module bepaalde subroutines te 'promoveren' tot z.g. interface-routine. Een interface routine is een subroutine in een module, die door het hoofdprogramma mag worden aangeroepen. De interne subroutines zijn alleen voor intern gebruik (door de module zelf.) De interface routines mogen door de module zelf aangeroepen worden, maar zijn ook bedoeld voor extern gebruik. Dat een routine een interface routine is, kunt u aangeven met commentaar.

Als voorbeeld kiezen we het 'loopled' programma, dat u in het begin misschien al gebruikt hebt om de werking van de SIMPLEX te controleren. Dit programma laat de LED's in een bepaalde volgorde knipperen. De LED's zijn aangesloten op de Outputs van poort B. Poort B is een simpele digitale uitgang (8 bits.) De uitgangen kunnen '1' of '0' gemaakt worden door het gewenste bitpatroon in het I/O register voor poort B te schrijven. Dit register heeft het adres $1004, en werkt (vanuit het programma gezien) als een gewone geheugenplaats. Het adres van het I/O register van poort B (en de adressen van alle andere I/O registers) zijn voor elk programma hetzelfde. De adressen zijn gedefiniëerd in een 'include' file (map512.asm voor een SIMPLEX-512, of map2048.asm voor een SIMPLEX-2048.) Het voorbeeld gaat ervan uit dat U een standaard SIMPLEX hebt (SIMPLEX-512.) Wanneer u een uitgebreide SIMPLEX hebt, verandert alleen de naam van de 'include' file. Uiteraard komt bij een ander type SIMPLEX het programma ook op andere adressen terecht. De standaard SIMPLEX-512 heeft programmageheugen op adres $B600 t/m $B7FF.

*************************************************
* definiëren van de geheugen map
*************************************************
incl "map512.asm"

*************************************************
* start van het programma
*************************************************
PROGRAM space |kies het programma﷓gebied

reset equ $ |na reset begint de micro op deze plaats
lds #stackend |begin met de stackpointer te laden

ldx #databeg |en zet dan het volledige datagebied op 00
clearram clr 0,x
inx
cpx #dataend
bls clearram

ldx #regsbeg |laat IX op de bank met I/O registers wijzen


*************************************************
* modulen
*************************************************


*************************************************
* LED's
*************************************************
PROGRAM space
ledstart equ $ |het beginadres van deze module

ledport equ portb
led0 equ bit0
led1 equ bit1
led2 equ bit2

DATA space
ledpattern rmb 1 |index naar het huidige patroon voor de led's

* initialisatie van de led's; begin met de eerste stap van het patroon
PROGRAM space

bsr ledstep
bra ledend |einde van de initialisatie

* de tabel met ledpatronen die stap﷓voor﷓stap wordt afgewerkt
ledtable fcb ( led0)
fcb ( led1 or led0)
fcb (led2 or led1 or led0)
fcb (led2 or led1 or led0)
fcb (led2 or led1 )
fcb (led2 )
fcb 0
fcb 0
steps equ $﷓ledtable |het aantal stappen in het patroon

* subroutine voor de afhandeling van een stap in het ledpatroon
PROGRAM space

ledstep equ $
pshb
pshx
ldx #ledtable |haal het adres van de patroontabel in IX
ldab ledpattern |en de index in de tabel in B
abx |IX wijst nu op het huidige patroon
ldab ledport |haal het bitpatroon van de output poort
andb #not(led2 or led1 or led0)
orab 0,x |zet eerst de led's uit, en voeg daarna
stab ledport |het nieuwe patroon toe
ldab ledpattern |zet de index daarna op een nieuw patroon
incb |maar blijf binnen de tabel, en zet de
cmpb #steps |index terug op 0 als alle patronen een keer
blo ledstep9 |geweest zijn
clrb
ledstep9 stab ledpattern
pulx
pulb
rts

ledend equ $ |einde van de led module

* het aantal bytes dat deze module nodig heeft in het programma gebied
ledsize equ ledend﷓ledstart



*************************************************
* het hoofdprogramma
*************************************************
PROGRAM space
jmp main0

main0 ldx #50000 |laad teller voor een vertraging

main1 dex |verlaag de teller
bne main1 |tot 0
jsr ledstep |en doe dan een stap van het looplicht
bra main0 |herhaal dit oneindig

end


Bovenin wordt de file 'map512.asm' ingelezen. Deze file zorgt voor het definiëren en laden van de pointers voor het programmageheugen (PROGRAM space) en het datageheugen (DATA space.) Daarnaast bevat de file de adressen en namen van alle I/O registers en de verschillende bits in de I/O registers. Dit is voor elk programma dat u schrijft steeds hetzelfde, dus is het erg handig deze gegevens in een aparte file op te nemen. In elk programma kunt u die file dan inlezen.

Daarna volgt de initialisatie van het hoofdprogramma. Dit is ook min- of meer standaard. De stackpointer wordt geladen, en alle bytes in het datageheugen worden met '0' gevuld.

Hierna volgen de verschillende modulen voor het programma. De eerste (en enige) module in dit programma verzorgt de besturing van de LED's op de SIMPLEX.

Er is code voor de initialisatie van de module, en één interface routine. Er zijn in dit eenvoudige geval geen extra interne routines nodig.

Na de LED module volgt het hoofdprogramma, ofwel 'main loop'. De main loop is zeer eenvoudig geworden en bestaat uit niet meer dan een wachtlusje, en daarna een aanroep van de LED module (die het ledpatroon een stapje verder zet.)

Hieronder staat de feitelijke LED module.

*************************************************
* LED's
*************************************************
PROGRAM space
ledstart equ $ |het beginadres van deze module

ledport equ portb
led0 equ bit0
led1 equ bit1
led2 equ bit2

DATA space
ledpattern rmb 1 |index naar het huidige patroon voor de led's

* initialisatie van de led's; begin met de eerste stap van het patroon
PROGRAM space

bsr ledstep
bra ledend |einde van de initialisatie

* de tabel met ledpatronen die stap﷓voor﷓stap wordt afgewerkt
ledtable fcb ( led0)
fcb ( led1 or led0)
fcb (led2 or led1 or led0)
fcb (led2 or led1 or led0)
fcb (led2 or led1 )
fcb (led2 )
fcb 0
fcb 0
steps equ $﷓ledtable |het aantal stappen in het patroon

* subroutine voor de afhandeling van een stap in het ledpatroon
PROGRAM space

ledstep equ $
pshb
pshx
ldx #ledtable |haal het adres van de patroontabel in IX
ldab ledpattern |en de index in de tabel in B
abx |IX wijst nu op het huidige patroon
ldab ledport |haal het bitpatroon van de output poort
andb #not(led2 or led1 or led0)
orab 0,x |zet eerst de led's uit, en voeg daarna
stab ledport |het nieuwe patroon toe
ldab ledpattern |zet de index daarna op een nieuw patroon
incb |maar blijf binnen de tabel, en zet de
cmpb #steps |index terug op 0 als alle patronen een keer
blo ledstep9 |geweest zijn
clrb
ledstep9 stab ledpattern
pulx
pulb
rts

ledend equ $ |einde van de led module

* het aantal bytes dat deze module nodig heeft in het programma gebied
ledsize equ ledend-ledstart


Wanneer de processor gereset wordt, zal hij beginnen op adres $B600 (bij een SIMPLEX-512.) Dit is helemaal bovenin het programma. Hier wordt de stackpointer geladen en het datageheugen gevuld. Daarna komt het programma in de LED module terecht. Bovenin de LED module staat de initialisatie code voor deze module. Die wordt doorlopen, en daarna springt het programma naar het eind van de module. Wanneer u meerdere modulen zou hebben, zou het programma hierna dus in de initialisatie-code voor de volgende module terecht komen. Na initialisatie van deze volgende module wordt weer naar het eind van die module gesprongen, etc. Na de laatste module komt het programma in de main loop terecht. Wanneer de main loop start, is alle initialisatie achter de rug, en kunnen de modulen meteen gebruikt worden.

Becommentariëren

Het is niet eenvoudig een programma te voorzien van compact, volledig en duidelijk commentaar. Dit is vooral een kwestie van ervaring. Er zijn wel enkele vuistregels te geven.

  • Een programma moet te begrijpen zijn wanneer voornamelijk het commentaar gelezen wordt, en bijna niet naar de instructies wordt gekeken. De subroutine om het looplicht één stapje te laten doen gaat er zonder instructies als volgt uitzien:
* subroutine voor de afhandeling van een stap in het ledpatroon
ledstep equ $
|haal het adres van de patroontabel in IX
|en de index in de tabel in B
|IX wijst nu op het huidige patroon
|haal het bitpatroon van de output poort
|zet eerst de led's uit, en voeg daarna
|het nieuwe patroon toe
|zet de index daarna op een nieuw patroon
|maar blijf binnen de tabel, en zet de
|index terug op 0 als alle patronen een keer
|geweest zijn
  • Het is niet zinvol te beschrijven wat elke instructie precies doet. Dat zoekt u beter op in de beschrijving van de instructieset. Commentaar zoals in het volgende voorbeeld is gegeven helpt niet bij het begrijpen van een programma:
* subroutine voor de afhandeling van een stap in het ledpatroon
PROGRAM space

ledstep equ $
pshb                      |zet B op de stack
pshx                      |zet X op de stack
ldx #ledtable             |laad X met 'ledtable'
ldab ledpattern           |laad B met 'ledpattern'
abx                       |tel B op bij X
ldab ledport              |laad B met de ledpoort
andb #not(led2 or led1 or led0)
orab 0,x                  |OR B met waar X op wijst
stab ledport              |zet B in de ledpoort
ldab ledpattern           |laad B met 'ledpattern'
incb                      |verhoog B
cmpb #steps               |vergelijk B met 'steps'
blo ledstep9              |spring als kleiner
clrb                      |zet B op 0
ledstep9 stab ledpattern  |zet B in ledpattern
pulx                      |haal X van de stack
pulb                      |haal B van de stack
rts                       |return van deze subroutine

Probeert U de werking van deze subroutine eens te doorgronden wanneer U alleen het commentaart leest.

In het algemeen kunt U het commentaar scheiden in twee stukken:

  1. Wàt de routine doet. Dit zet U in één of meer regels boven de subroutine.

  2. Hoé de routine dit verwezenlijkt. Dit kunt U beschrijven in de ruimte rechts van de instructies.

Subroutines

Net als modulen moeten goede subroutines een welomschreven functie vervullen. Als het moeilijk is de functie van een subroutine in één kernachtige zin te omschrijven, heeft u waarschijnlijk geen goede subroutine geschreven.

Er zijn een aantal 'gewoonten' bij het schrijven van subroutines, die in de praktijk hebben bewezen erg handig te zijn.

  • Geef de routine een goede naam, waaruit de functie van de routine goed blijkt. Wanneer de routine ergens anders in het programma wordt aangeroepen, heeft u dan tenminste een idee van wat er in de routine gebeurt.

  • Zorg ervoor dat de routine niet meer doet dan wat hij moet doen. De meeste routines gebruiken bijvoorbeeld intern een aantal van de registers in de processor. U kunt registers gebruiken om gegevens aan de routine door te geven, of om gegevens uit de routine terug te krijgen. Omschrijf in het commentaar duidelijk welke registers hiervoor gebruikt worden, en zorg er ook voor dat de andere registers door de routine niet veranderd worden. Hiervoor kunt u de registers die door de routine intern gebruikt worden aan het begin van de routine op de stack bewaren, en juist voor het verlaten van de routine weer van de stack afhalen.

Het is vooral een kwestie van ervaring, en veel (goede) voorbeelden lezen om duidelijke en gestructureerde programma's te schrijven. Wanneer u een programma goed probeert te structureren en becommentariëren doet u dit niet alleen om een programma leesbaar voor iemand anders te houden. Een helder programma is ook door uzelf eenvoudiger te controleren op fouten, en zal in het algemeen om te beginnen al minder fouten bevatten dan een 'brei'. Denk voordat u een programma gaat schrijven altijd eerst na over de opbouw en de modulen die er in gebruikt kunnen worden.

Labels

Elk label moet een unieke naam hebben. U bent natuurlijk volkomen vrij elke naam te kiezen die u wilt. Er zijn echter een paar goede gewoonten te geven, die in de praktijk hebben bewezen goed te werken.

Bij een subroutine zijn er twee 'soorten' labels te herkennen.

  • Label voor het begin van de subroutine.

  • Labels die intern in de routine worden gebruikt.

Wanneer een label wordt toegekend voor het begin van een subroutine, dan is het handig dit label te definiëren met een 'equ $' pseudo-instructie. Bijvoorbeeld:

ledstep equ $

Dit is hetzelfde als wanneer het label voor de eerste instructie van de routine wordt geplaatst, maar door het label op deze wijze te definiëren is het eenvoudig te onderscheiden van andere labels, die van buiten de routine niet aangeroepen kunnen of mogen worden.

Labels die intern in een routine worden gebruikt, krijgen de naam van de routine plus een volgnummer. Dit vereenvoudigt het verzinnen van een naam voor een label. Daarnaast zal het onwaarschijnlijk zijn dat het label al eerder gebruikt werd (labels moeten uniek zijn.)

Vaak wordt in een routine een stuk (conditioneel) overgeslagen. Een label aan het eind van een routine zal dan ook vaak voorkomen. Een handige gewoonte is, om dit label het volgnummer 9 te geven (of 99 als de routine erg veel interne labels heeft.)

Registers

Vaak werkt een subroutine met getallen die bij aanroep aan de routine moeten worden doorgegeven. Andere routines voeren een actie uit die een getal oplevert. Ook een combinatie van deze twee komt natuurlijk voor.

De gebruikelijke manier om gegevens naar een subroutine te transporteren, is deze gegevens in een register te laden voordat de routine wordt aangeroepen. Een routine die een getal teruggeeft laat kan het resultaat achterlaten in een register. Geheugenplaatsen kunnen ook gebruikt worden voor het gegevens transport, maar wanneer dit via de registers gebeurt levert dit meestal snellere code op.

Het is handig wanneer er een soort standaard is voor het transport van de gegevens. Als alle routines op een soortgelijke manier hun gegevens ontvangen en weer teruggeven, is de kans op vergissingen kleiner. De manier waarop in de voorbeelden in deze cursus gegevens van- en naar subroutines worden doorgegeven is:

  • 8-bits waarden worden doorgegeven via accumulator B

  • 16-bits waarden worden doorgegeven via register D

  • adressen worden doorgegeven via het IX register

Vorige ] Volgende ]

© 2005...2008 Elomax [Voorwaarden ]