© Harry Broeders.
Deze pagina is bestemd voor studenten van de THRijswijk.
Dit voorbeeld laat een aantal manieren zien waarop de I/O ports van de 68HC11 vanuit C en/of C++ benaderd kunnen worden.
Het is "netjes" om de gebruikte methode te verbergen achter een "abstractie". Dit heeft 2 voordelen:
Er zijn een aantal verschillende methoden om de gebruikte methode te verbergen:
Het type byte
is met behulp van een typedef
als
volgt gedefinieerd:
typedef unsigned
char byte;
Tot slot worden al deze methode met elkaar vergeleken voor wat betreft snelheid en geheugengebruik.
volatile
byte*
variabele.
Als voorbeeld nemen we een programma dat de schakelaars kruislings
verbind met de leds. Dus PB0=PC7
, PB1=PC6
,
PB2=PC5
, ... PB7=PC0
. In het onderstaande programma
worden de schakelaars 1 voor 1 getest met behulp van de variabele
l
. Deze variabele krijgt achtereenvolgens de waarden:
00000001
, 00000010
, 00000100
, ...
10000000
(de 1
in de variabele l
schuift
dus steeds naar links). Als een schakelaar hoog is wordt de juiste led aangezet.
Dit gebeurd met de variabele r
. Deze variabele krijgt
achtereenvolgens de waarden: 10000000
, 01000000
,
00100000
, ... 00000001
(de 1
in de
variabele r
schuift dus steeds naar rechts).
int main() {
typedef unsigned char byte;
volatile byte* portc=(byte*)0x1003;
volatile byte* portb=(byte*)0x1004;
byte in, out, l, r;
while (1) {
in=*portc;
out=0;
for (l=0x01, r=0x80; l!=0; l<<=1, r>>=1)
if (in&r)
out|=l;
*portb=out;
}
return 0;
}
Als je niet meer weet wat het keyword volatile
betekend dan
kun je dat hier nog eens nalezen.
Het lezen en schrijven van de I/O ports wordt (als we optimaliseren voor
minimale programmagrootte, met de gcc opties -Os
-fomit-frame-pointer
-mshort
) als volgend naar
assemblercode vertaald:
Je ziet dat de compiler de index registers X
en Y
gebruikt om de pointers naar de ports in op te slaan. Bij het schrijven naar
poort B blijkt dat het X
register ook gebruikt wordt om de waarde
van de variabele out
op te halen.
In THRSim11 kun je deze poorten in het HLL variables window vinden:
Door in het HLL variables window op de rechter muisknop te klikken en daarna voor de pop-up menu optie "Show Locations" te kiezen worden de locaties van de variabelen zichtbaar.
const
.
Om programmeerfouten te voorkomen is het beter om met het keyword
const
aan te geven welke waarden alleen gelezen kunnen worden.
De code zou nu eventueel ook verder geoptimaliseerd kunnen worden. In de
oorspronkelijke versie van C (vaak K&R C genoemd naar de bedenkers van
C Brian Kernighan en
Dennis Ritchie) kwam het
keyword const
niet voor. Het idee voor het keyword
const
is afkomstig van
Bjarne Stroustrup
de bedenker van C++ (in overleg met Dennis Ritchie). Bij het standaardiseren
van C (1989) is const
ook in de C standaard opgenomen.
Voor wie echt alles wil weten: Leuk om te weten is dat het keywordconst
door de C standaardisatie commissie is bedacht. Stroustrup gebruikte in eerste instantie het keywordreadonly
. Stroustrup heeft ook het keywordwriteonly
voorgesteld maar dat is afgewezen door de C standaardisatie commissie. Stroustrup heeft toen besloten om ook in C++ (om compatibel te blijven met C) alleen het keywordconst
te gebruiken. Vreemd genoeg is er wel een verschil tussenconst
in C enconst
in C++. In C89 mag een alsconst
gedefinieerde variabele niet in een constante expressie gebruikt worden:int main() {
const int i=3;
int array[i];
return 0;
}
Geeft in ANSI C89 de volgende foutmelding:
error: ISO C90 forbids variable-size array `array'
Bij gcc moet je de opties:
-ansi
en-pedantic-errors
gebruiken om aan te geven dat je ANSI C89 wilt gebruiken (zie hier).In C++ kun je een als
const
gedefinieerde variabele wel in een constante expressie gebruiken:int main() {
const int i(3);
int array[i];
return 0;
}
Geeft (gecompileerd met de opties:
-c++98
en-pedantic-errors
) geen foutmelding.
int main() {
typedef unsigned char byte;
volatile const byte* const portc=(byte*)0x1003;
volatile byte* const portb=(byte*)0x1004;
byte in, out, l, r;
while (1) {
in=*portc;
out=0;
for (l=0x01, r=0x80; l!=0; l<<=1, r>>=1)
if (in&r)
out|=l;
*portb=out;
}
return 0;
}
In de regel:
volatile
const byte* const
portc=(byte*)0x1003;
geeft de eerste const
aan dat de waarde waar de pointer naar
wijst alleen gelezen mag worden. Als we toch proberen om naar de schakelaars
te schrijven:
*portc=out;
krijgen we de volgende foutmelding:
error: assignment of read-only location
De tweede const
geeft aan dat de variabele (de pointer)
portc
niet mag veranderen. Als we dat toch proberen bijvoorbeeld:
portc=portb;
dan krijgen we de volgende foutmelding:
error: assignment of read-only variable `portc'
Het lezen en schrijven van de I/O ports wordt (als we optimaliseren voor
minimale programmagrootte, met de gcc opties -Os
-fomit-frame-pointer
-mshort
) als volgend naar
assemblercode vertaald:
Je ziet dat nu alleen register X
wordt gebruikt maar dit register
wordt telkens opnieuw geladen. De variabelen portc
en
portb
zijn helemaal weggeoptimaliseerd. Dit betekent dat je
deze variabelen bij het debuggen van geoptimaliseerde code niet meer kunt
bekijken.
Het is daarom beter om tijdens het debuggen de code nog niet te optimaliseren. Als het programma wordt vertaald zonder optimalisatie opties dan kunnen we alle variabelen in de debugger bekijken:
Chris Werkmeester heeft een methode bedacht om const op een efficiente manier te kunnen gebruiken.
int main() {
typedef unsigned char byte;
extern volatile const byte* const portc;
extern volatile byte* const portb;
byte in, out, l, r;
while (1) {
in=*portc;
out=0;
for (l=0x01, r=0x80; l!=0; l<<=1, r>>=1)
if (in&r)
out|=l;
*portb=out;
}
return 0;
}
De pointers portc
en portb
worden nu in een andere
source file gedefinieerd:
typedef unsigned char byte;
volatile const byte* const portc=(byte*)0x1003;
volatile byte* const portb=(byte*)0x1004;
Beide source files worden onafhankelijk van elkaar gecompileerd en daarna met behulp van de linker samengevoegd tot één a.out file.
Het lezen en schrijven van de I/O ports wordt (als we optimaliseren voor
minimale programmagrootte, met de gcc opties -Os
-fomit-frame-pointer
-mshort
) als volgend naar
assemblercode vertaald:
Deze code is exact net zo efficient als de code zonder gebruik van
const
. Het programma is wel 4 bytes groter omdat het
X
en Y
register met behulp van de extended addressing
mode in plaats van met de immediate addressing mode worden geïnitialiseerd.
volatile
byte*
cast.
Bij deze methode worden geen pointer variabelen aangemaakt maar wordt meteen
"de inhoud van" adres 0x1003
(of 0x1004
) gebruikt.
int main() {
typedef unsigned char byte;
byte in, out, l, r;
while (1) {
in=*(volatile byte*)0x1003;
out=0;
for (l=0x01, r=0x80; l!=0; l<<=1, r>>=1)
if (in&r)
out|=l;
*(volatile byte*)0x1004=out;
}
return 0;
}
Het lezen en schrijven van de I/O ports wordt (als we optimaliseren voor
minimale programmagrootte, met de gcc opties -Os
-fomit-frame-pointer
-mshort
) als volgend naar
assemblercode vertaald:
Je ziet dat nu alleen register X
wordt gebruikt maar dit register
wordt telkens opnieuw geladen. Deze methode levert exact dezelfde machine
code op als methode 1 met gebruik van const
.
We willen dat 1 index register gebruikt wordt om de poorten te benaderen.
De verschillende registers kunnen dan met verschillende offsets geadresseerd
worden. Op de een of andere manier moeten we de compiler "vertellen" dat
portc
en portb
via opeenvolgende adressen te bereiken
zijn. De hierna beschreven methode 3, 4
en 5 maken dit (op verschillende manieren) mogelijk.
volatile
struct ports*
variabele.
int main() {
typedef unsigned char byte;
struct ports {
byte c;
byte b;
};
volatile struct ports* port=(struct ports*)0x1003;
byte in, out, l, r;
while (1) {
in=port->c;
out=0;
for (l=0x01, r=0x80; l!=0; l<<=1, r>>=1) {
if (in&r)
out|=l;
}
port->b=out;
}
return 0;
}
Het lezen en schrijven van de I/O ports wordt (als we optimaliseren voor
minimale programmagrootte, met de gcc opties -Os
-fomit-frame-pointer
-mshort
) als volgend naar
assemblercode vertaald:
Je ziet dat nu alleen register X
wordt gebruikt en dat dit register
maar 1 maal wordt geladen.
In THRSim11 kun je deze poorten in het HLL variables window vinden:
Helaas is het niet gegarandeerd dat deze methode werkt in standaard C. Een
standaard C compiler is namelijk niet verplicht om de velden van een
struct
aansluitend in het geheugen te plaatsen. Bepaalde
processoren kunnen bijvoorbeeld een int
variabele sneller
lezen/schrijven als die variabele op een even adres begint. We noemen dit
allignment (uitlijning) van variabelen. Het is in ons geval dus niet gegarandeerd
dat het veld b
op adres $1004
terechtkomt. In gcc
kunnen we met een attribute
aangeven dat de velden in een struct
aansluitend in het geheugen moeten worden geplaatst. We kunnen dit programma
dan echter niet meer compileren met een andere compiler.
int main() {
typedef unsigned char byte;
struct ports {
byte c;
byte b;
} __attribute__((packed));
volatile struct ports* port=(struct ports*)0x1003;
byte in, out, l, r;
while (1) {
in=port->c;
out=0;
for (l=0x01, r=0x80; l!=0; l<<=1, r>>=1) {
if (in&r)
out|=l;
}
port->b=out;
}
return 0;
}
const
.
Om programmeerfouten te voorkomen is beter om met het keyword
const
aan te geven welke waarden alleen gelezen kunnen worden.
int main() {
typedef unsigned char byte;
struct ports {
const byte c;
byte b;
} __attribute__((packed));
volatile struct ports* const port=(struct ports*)0x1003;
byte in, out, l, r;
while (1) {
in=port->c;
out=0;
for (l=0x01, r=0x80; l!=0; l<<=1, r>>=1) {
if (in&r)
out|=l;
}
port->b=out;
}
return 0;
}
Het gebruik van const
heeft in dit geval echter ook 2 nadelen:
const
. Alleen register X
wordt
gebruikt maar dit register wordt telkens opnieuw geladen.
port
wordt weggeoptimaliseerd en is dus niet meer
beschikbaar tijdens het debuggen. Dit is natuurlijk alleen maar een probleem
als je geoptimaliseerde code wilt debuggen. Meestal wordt eerst de niet
geoptimalisserde code gedebugged en dan kun je de variabele port
gewoon "zien" tijdens het debuggen.
De timer van de 68HC11 bevat ook een aantal 16 bits I/O registers. De timer
heeft daarnaast ook enkele 8 bits registers. Dat er zowel 8 als 16 bits registers
worden gebruikt is geen enkel probleem bij het gebruik van een
struct
omdat de velden van een struct
van verschillende
types mogen zijn. Het onderstaande programma gebruikt de timer van de 68HC11
om een blokvormig signaal met een frequentie van 1 kHz te genereren op pin
PA3 via Output Compare 5.
int main() {
typedef unsigned char byte;
typedef unsigned short word;
struct Timer {
byte cforc;
byte oc1m;
byte oc1d;
word tcnt;
word tic1;
word tic2;
word tic3;
word toc1;
word toc2;
word toc3;
word toc4;
word toc5;
byte tctl1;
byte tctl2;
byte tmsk1;
byte tflg1;
byte tmsk2;
byte tflg2;
} __attribute__((packed));
volatile struct Timer* timer=(struct Timer*)0x100b;
timer->tctl1=0x01; /* toggle oc5 = PA3 */
timer->toc5=timer->tcnt+50;
while (1) {
while ((timer->tflg1&0x08)==0x00) /* wacht op OC5F */;
timer->tflg1=0x08; /* reset OC5F */
timer->toc5+=1000;
}
return 0;
}
Het lezen en schrijven van de timer registers wordt (als we optimaliseren
voor minimale programmagrootte, met de gcc opties -Os
-fomit-frame-pointer
-mshort
) als volgend naar
assemblercode vertaald:
De verschillende registers van de timer kunnen in THRSim11 bekeken/veranderd
worden via de variabele timer
:
volatile
byte
array.
#define C 0
#define B 1
int main() {
typedef unsigned char byte;
volatile byte* port=(byte*)0x1003;
byte in, out, l, r;
while (1) {
in=port[C];
out=0;
for (l=0x01, r=0x80; l!=0; l<<=1, r>>=1)
if (in&r)
out|=l;
port[B]=out;
}
return 0;
}
Het lezen en schrijven van de I/O ports wordt (als we optimaliseren voor
minimale programmagrootte, met de gcc opties -Os
-fomit-frame-pointer
-mshort
) als volgend naar
assemblercode vertaald:
Je ziet dat nu alleen register X
wordt gebruikt en dat dit register
maar 1x wordt geladen. Deze methode levert exact dezelfde machinecode
als methode 3.
In THRSim11 kun je alleen de eerste poort in het HLL variables window vinden:
Deze methode werkt gegarandeerd in standaard C. Een standaard C compiler
is namelijk wel verplicht om de velden van een array
aansluitend in het geheugen te plaatsen. Verklaring: Als er een pointer
p
naar een array element wijst dan moet die pointer nadat die
bewerking p=p+1
is uitgevoerd naar het volgende array element
wijzen. Dit kan alleen maar werken als alle array elementen
aansluitend in het geheugen zijn geplaatst. Bij een
struct
is het niet mogelijk om via een pointer naar een veld
de overige velden te bereiken. Vandaar dat het niet nodig is om de velden
van een struct aansluitend in het geheugen te plaatsen.
Het is handiger om de constanten B
en C
in plaats
van met #define
met een enum
te definiëren.
int main() {
typedef unsigned char byte;
enum {C, B};
volatile byte* port=(byte*)0x1003;
byte in, out, l, r;
while (1) {
in=port[C];
out=0;
for (l=0x01, r=0x80; l!=0; l<<=1, r>>=1)
if (in&r)
out|=l;
port[B]=out;
}
return 0;
}
Dit maakt voor de gegenereerde machine code geen verschil.
Alle elementen van een array moeten van hetzelfde type zijn. Als in een programma zowel 8 als 16 bits registers worden gebruikt dan moeten we dus 2 array's definiëren. Het onderstaande programma gebruikt de timer van de 68HC11 om een blokvormig signaal met een frequentie van 1 kHz te genereren op pin PA3 via Output Compare 5.
int main() {
enum {PORTA, PIOC=0x02, PORTC, PORTB, PORTCL, DDRC=0x07, PORTD,
DDRD, PORTE, CFORC, OC1M, OC1D, TCTL1=0x20, TCTL2, TMSK1,
TFLG1, TMSK2, TFLG2, PACTL, PACNT, SPCR, SPSR, SPDR, BAUD,
SCCR1, SCCR2, SCSR, SCDR, ADCTL, ADR1, ADR2, ADR3, ADR4,
OPTION=0x39, COPRST, PPROG, HPRIO, INIT, CONFIG=0x3f};
enum {TCNT=0x07, TIC1, TIC2, TIC3, TOC1, TOC2, TOC3, TOC4, TOC5};
typedef unsigned char Byte;
typedef unsigned short Word;
volatile Byte* byte=(Byte*)0x1000;
volatile Word* word=(Word*)0x1000;
byte[TCTL1]=0x01; /* toggle oc5 = PA3 */
word[TOC5]=word[TCNT]+50;
while (1) {
while ((byte[TFLG1]&0x08)==0x00) /* wacht op OC5F */;
byte[TFLG1]=0x08; /* reset OC5F */
word[TOC5]+=1000;
}
return 0;
}
Het lezen en schrijven van de timer registers wordt (als we optimaliseren
voor minimale programmagrootte, met de gcc opties -Os
-fomit-frame-pointer
-mshort
) als volgend naar
assemblercode vertaald:
Methode 3 heeft 2 voordelen ten opzichte van methode 4:
Methode 3 heeft 1 nadeel ten opzichte van methode 4:
__attribute__ ((packed))
te gebruiken.
volatile
byte*
pointer arithmetic.
Bij deze methode wordt 1 pointer variabele gedefinieerd en worden de verschillende registers via pointer arithmetic benaderd:
#define C 0
#define B 1
int main() {
typedef unsigned char byte;
volatile byte* port=(byte*)0x1003;
byte in, out, l, r;
while (1) {
in=*(port+C);
out=0;
for (l=0x01, r=0x80; l!=0; l<<=1, r>>=1)
if (in&r)
out|=l;
*(port+B)=out;
}
return 0;
}
Het lezen en schrijven van de I/O ports wordt (als we optimaliseren voor
minimale programmagrootte, met de gcc opties -Os
-fomit-frame-pointer
-mshort
) als volgend naar
assemblercode vertaald:
Deze methode levert exact dezelfde machine code als methode 3 en methode 4. Deze methode heeft dezelfde nadelen als methode 4.
In THRSim11 kun je alleen de eerste poort in het HLL variables window vinden:
Het is handiger om de constanten B
en C
in plaats
van met #define
met een enum
te definiëren.
int main() {
typedef unsigned char byte;
enum {C, B};
volatile byte* port=(byte*)0x1003;
byte in, out, l, r;
while (1) {
in=*(port+C);
out=0;
for (l=0x01, r=0x80; l!=0; l<<=1, r>>=1)
if (in&r)
out|=l;
*(port+B)=out;
}
return 0;
}
Dit maakt voor de gegenereerde machine code geen verschil.
Als in een programma zowel 8 als 16 bits registers worden gebruikt dan kunnen we 2 pointers definiëren. Dit levert dan exact dezelfde code op als bij methode 4 met 2 array's. We kunnen ook 1 pointer definiëren en een extra cast toepassen.
Het onderstaande programma gebruikt de timer van de 68HC11 om een blokvormig signaal met een frequentie van 1 kHz te genereren op pin PA3 via Output Compare 5.
int main() {
enum {PORTA, PIOC=0x02, PORTC, PORTB, PORTCL, DDRC=0x07,
PORTD, DDRD, PORTE, CFORC, OC1M, OC1D, TCNT, TIC1=0x10,
TIC2=0x12, TIC3=0x14, TOC1=0x16, TOC2=0x18, TOC3=0x1A,
TOC4=0x1C, TOC5=0x1E, TCTL1=0x20, TCTL2, TMSK1, TFLG1,
TMSK2, TFLG2, PACTL, PACNT, SPCR, SPSR, SPDR, BAUD,
SCCR1, SCCR2, SCSR, SCDR, ADCTL, ADR1, ADR2, ADR3, ADR4,
OPTION=0x39, COPRST, PPROG, HPRIO, INIT, CONFIG=0x3f};
typedef unsigned char byte;
typedef unsigned short word;
volatile byte* port=(byte*)0x1000;
*(port+TCTL1)=0x01; /* toggle oc5 = PA3 */
*(word*)(port+TOC5)=*(word*)(port+TCNT)+50;
while (1) {
while ((*(port+TFLG1)&0x08)==0x00) /* wacht op OC5F */;
*(port+TFLG1)=0x08; /* reset OC5F */
*(word*)(port+TOC5)+=1000;
}
return 0;
}
Het lezen en schrijven van de timer registers wordt (als we optimaliseren
voor minimale programmagrootte, met de gcc opties -Os
-fomit-frame-pointer
-mshort
) als volgend naar
assemblercode vertaald:
Methode 5 is precies zo efficient als methode 3. Methode 3 heeft 2 voordelen ten opzichte van methode 5:
Methode 3 heeft 1 nadeel ten opzichte van methode 5:
__attribute__ ((packed))
te gebruiken.
volatile
struct ports*
variabele met union en bitfields om
afzonderlijke bits te benaderen.
int main() {
typedef unsigned char Byte;
struct ports {
union {
Byte byte;
struct {
Byte c7:1,c6:1,c5:1,c4:1,c3:1,c2:1,c1:1,c0:1;
} bits;
} c;
union {
Byte byte;
struct {
Byte b7:1,b6:1,b5:1,b4:1,b3:1,b2:1,b1:1,b0:1;
} bits;
} b;
};
volatile struct ports* port=(struct ports*)0x1003;
port->b.byte=0;
while (1) {
port->b.bits.b7=port->c.bits.c0;
port->b.bits.b6=port->c.bits.c1;
port->b.bits.b5=port->c.bits.c2;
port->b.bits.b4=port->c.bits.c3;
port->b.bits.b3=port->c.bits.c4;
port->b.bits.b2=port->c.bits.c5;
port->b.bits.b1=port->c.bits.c6;
port->b.bits.b0=port->c.bits.c7;
}
return 0;
}
Het lezen en schrijven van de I/O ports wordt (als we optimaliseren voor
minimale programmagrootte, met de gcc opties -Os
-fomit-frame-pointer
-mshort
) als volgend naar
assemblercode vertaald:
Deze methode heeft dezelfde voordelen als methode 3 maar heeft als extra
dat de bits ook afzonderlijk benaderd kunnen worden. Een test om te kijken
of schakelaar PC3
hoog is wordt nu:
if (port->c.bits.c3) {
/*...*/
}
In THRSim11 kun je deze poorten in het HLL variables window vinden:
Het is ook mogelijk om zogenaamde anonieme unions en structs (dat zijn unions en structs zonder naam) te gebruiken:
int main() {
typedef unsigned char byte;
struct ports {
union {
byte c;
struct {
byte c7:1,c6:1,c5:1,c4:1,c3:1,c2:1,c1:1,c0:1;
};
};
union {
byte b;
struct {
byte b7:1,b6:1,b5:1,b4:1,b3:1,b2:1,b1:1,b0:1;
};
};
};
volatile struct ports* port=(struct ports*)0x1003;
port->b=0;
while (1) {
port->b7=port->c0;
port->b6=port->c1;
port->b5=port->c2;
port->b4=port->c3;
port->b3=port->c4;
port->b2=port->c5;
port->b1=port->c6;
port->b0=port->c7;
}
return 0;
}
Door het gebruik van anonieme unions en structs wordt het veel eenvoudiger
om de afzonderlijke bits te benaderen. Een test om te kijken of schakelaar
PC3
hoog is wordt nu:
if (port->c3) {
/*...*/
}
De machine code veranderd niet door het gebruik van anonieme unions en structs:
THRSim11 (versie 5.22) heeft op dit moment problemen met het weergeven van deze anonieme unions en structs:
Om problemen met allignment te voorkomen (zie methode 3) kunnen we in gcc
met een attribute
aangeven dat de velden in een struct of union
aansluitend in het geheugen moeten worden geplaatst.
int main() {
typedef unsigned char byte;
struct ports {
union {
byte c;
struct {
byte c7:1,c6:1,c5:1,c4:1,c3:1,c2:1,c1:1,c0:1;
} __attribute__ ((packed));
} __attribute__ ((packed));
union {
byte b;
struct {
byte b7:1,b6:1,b5:1,b4:1,b3:1,b2:1,b1:1,b0:1;
} __attribute__ ((packed));
} __attribute__ ((packed));
} __attribute__ ((packed));
volatile struct ports* port=(struct ports*)0x1003;
port->b=0;
while (1) {
port->b7=port->c0;
port->b6=port->c1;
port->b5=port->c2;
port->b4=port->c3;
port->b3=port->c4;
port->b2=port->c5;
port->b1=port->c6;
port->b0=port->c7;
}
return 0;
}
volatile
union ports
array met bitfields om afzonderlijke
bits te benaderen.
#define c 0
#define b 1
int main() {
typedef unsigned char Byte;
union ports {
Byte byte;
struct {
Byte b7:1,b6:1,b5:1,b4:1,b3:1,b2:1,b1:1,b0:1;
} bits;
};
volatile union ports* port=(union ports*)0x1003;
port[b].byte=0;
while (1) {
port[b].bits.b7=port[c].bits.b0;
port[b].bits.b6=port[c].bits.b1;
port[b].bits.b5=port[c].bits.b2;
port[b].bits.b4=port[c].bits.b3;
port[b].bits.b3=port[c].bits.b4;
port[b].bits.b2=port[c].bits.b5;
port[b].bits.b1=port[c].bits.b6;
port[b].bits.b0=port[c].bits.b7;
}
return 0;
}
Het lezen en schrijven van de I/O ports wordt (als we optimaliseren voor
minimale programmagrootte, met de gcc opties -Os
-fomit-frame-pointer
-mshort
) als volgend naar
assemblercode vertaald:
Deze methode levert exact dezelfde machinecode als methode 5. Deze methode
heeft dezelfde voordelen als methode 4 maar heeft als extra dat de bits ook
afzonderlijk benaderd kunnen worden. Een test om te kijken of schakelaar
PC3
hoog is wordt nu:
if (port[c].bits.b3) {
/*...*/
}
In THRSim11 kun je alleen de eerste poort in het HLL variables window vinden:
Het is hier ook mogelijk om anonieme structs te gebruiken:
#define c 0
#define b 1
int main() {
typedef unsigned char Byte;
union ports {
Byte byte;
struct {
Byte b7:1,b6:1,b5:1,b4:1,b3:1,b2:1,b1:1,b0:1;
};
};
volatile union ports* port=(union ports*)0x1003;
port[b].byte=0;
while (1) {
port[b].b7=port[c].b0;
port[b].b6=port[c].b1;
port[b].b5=port[c].b2;
port[b].b4=port[c].b3;
port[b].b3=port[c].b4;
port[b].b2=port[c].b5;
port[b].b1=port[c].b6;
port[b].b0=port[c].b7;
}
return 0;
}
Door het gebruik van anonieme unions en structs wordt het eenvoudiger om
de afzonderlijke bits te benaderen. Een test om te kijken of schakelaar
PC3
hoog is wordt nu:
if (port[c].b3) {
/*...*/
}
De machine code veranderd niet door het gebruik van anonieme unions en structs:
THRSim11 (versie 5.22) heeft op dit moment problemen met het weergeven van deze anonieme unions en structs:
Om problemen met allignment te voorkomen (zie methode 3) kunnen we in gcc
met een attribute
aangeven dat de velden in een struct of union
aansluitend in het geheugen moeten worden geplaatst.
#define c 0
#define b 1
int main() {
typedef unsigned char Byte;
union ports {
Byte byte;
struct {
Byte b7:1,b6:1,b5:1,b4:1,b3:1,b2:1,b1:1,b0:1;
} __attribute__ ((packed));
} __attribute__ ((packed));
volatile union ports* port=(union ports*)0x1003;
port[b].byte=0;
while (1) {
port[b].b7=port[c].b0;
port[b].b6=port[c].b1;
port[b].b5=port[c].b2;
port[b].b4=port[c].b3;
port[b].b3=port[c].b4;
port[b].b2=port[c].b5;
port[b].b1=port[c].b6;
port[b].b0=port[c].b7;
}
return 0;
}
volatile
byte&
variabele.
In C++ kun je in plaats van een pointer ook een reference gebruiken:
int main() {
typedef unsigned char byte;
volatile byte& portc=*reinterpret_cast<byte*>(0x1003);
volatile byte& portb=*reinterpret_cast<byte*>(0x1004);
byte in, out, l, r;
while (1) {
in=portc;
out=0;
for (l=0x01, r=0x80; l!=0; l<<=1, r>>=1)
if (in&r)
out|=l;
portb=out;
}
return 0;
}
De definitie van de variabelen portb
en portc
is
iets ingewikkelder dan bij methode 1 maar het gebruik is eenvoudiger (je
hoeft geen *
meer te gebruiken).
Deze methode levert voor wat betreft het lezen van portc
en
het schrijven naar portb
exact dezelfde machinecode als methode
1:
THRSim11 (versie 5.22) heeft op dit moment nog geen support voor het weergeven van references. Hierdoor zijn de reference variabelen niet zichtbaar in het HLL variables window:
In C kunnen we de gebruikte methode om de poorten te benaderen verbergen achter functies.
enum reg {PORTA, PIOC=0x02, PORTC, PORTB, PORTCL, DDRC=0x07,
PORTD, DDRD, PORTE, CFORC, OC1M, OC1D, TCNT, TIC1=0x10,
TIC2=0x12, TIC3=0x14, TOC1=0x16, TOC2=0x18, TOC3=0x1A,
TOC4=0x1C, TOC5=0x1E, TCTL1=0x20, TCTL2, TMSK1, TFLG1,
TMSK2, TFLG2, PACTL, PACNT, SPCR, SPSR, SPDR, BAUD,
SCCR1, SCCR2, SCSR, SCDR, ADCTL, ADR1, ADR2, ADR3, ADR4,
OPTION=0x39, COPRST, PPROG, HPRIO, INIT, CONFIG=0x3f};
typedef unsigned char byte;
typedef unsigned short word;
volatile byte* port=(byte*)0x1000;
byte readByte(enum reg r) {
return *(port+r);
}
word readWord(enum reg r) {
return *(word*)(port+r);
}
void writeByte(enum reg r, byte b) {
*(port+r)=b;
}
void writeWord(enum reg r, word w) {
*(word*)(port+r)=w;
}
int main() {
writeByte(TCTL1, 0x01); /* toggle oc5 = PA3 */
writeWord(TOC5, readWord(TCNT)+50);
while (1) {
while ((readByte(TFLG1)&0x08)==0x00) /* wacht op OC5F */;
writeByte(TFLG1, 0x08); /* reset OC5F */
writeWord(TOC5, readWord(TOC5)+1000);
}
return 0;
}
#define
.
In C kunnen we de gebruikte methode om de poorten te benaderen verbergen achter macro's.
#define PORTA (*(volatile byte*)(port+0x00))
#define PIOC (*(volatile byte*)(port+0x02))
#define PORTC (*(volatile byte*)(port+0x03))
#define PORTB (*(volatile byte*)(port+0x04))
#define PORTCL (*(volatile byte*)(port+0x05))
#define DDRC (*(volatile byte*)(port+0x07))
#define PORTD (*(volatile byte*)(port+0x08))
#define DDRD (*(volatile byte*)(port+0x09))
#define PORTE (*(volatile byte*)(port+0x0A))
#define CFORC (*(volatile byte*)(port+0x0B))
#define OC1M (*(volatile byte*)(port+0x0C))
#define OC1D (*(volatile byte*)(port+0x0D))
#define TCNT (*(volatile word*)(port+0x0E))
#define TIC1 (*(volatile word*)(port+0x10))
#define TIC2 (*(volatile word*)(port+0x12))
#define TIC3 (*(volatile word*)(port+0x14))
#define TOC1 (*(volatile word*)(port+0x16))
#define TOC2 (*(volatile word*)(port+0x18))
#define TOC3 (*(volatile word*)(port+0x1A))
#define TOC4 (*(volatile word*)(port+0x1C))
#define TOC5 (*(volatile word*)(port+0x1E))
#define TCTL1 (*(volatile byte*)(port+0x20))
#define TCTL2 (*(volatile byte*)(port+0x21))
#define TMSK1 (*(volatile byte*)(port+0x22))
#define TFLG1 (*(volatile byte*)(port+0x23))
#define TMSK2 (*(volatile byte*)(port+0x24))
#define TFLG2 (*(volatile byte*)(port+0x25))
#define PACTL (*(volatile byte*)(port+0x26))
#define PACNT (*(volatile byte*)(port+0x27))
#define SPCR (*(volatile byte*)(port+0x28))
#define SPSR (*(volatile byte*)(port+0x29))
#define SPDR (*(volatile byte*)(port+0x2A))
#define BAUD (*(volatile byte*)(port+0x2B))
#define SCCR1 (*(volatile byte*)(port+0x2C))
#define SCCR2 (*(volatile byte*)(port+0x2D))
#define SCSR (*(volatile byte*)(port+0x2E))
#define SCDR (*(volatile byte*)(port+0x2F))
#define ADCTL (*(volatile byte*)(port+0x30))
#define ADR1 (*(volatile byte*)(port+0x31))
#define ADR2 (*(volatile byte*)(port+0x32))
#define ADR3 (*(volatile byte*)(port+0x33))
#define ADR4 (*(volatile byte*)(port+0x34))
#define OPTION (*(volatile byte*)(port+0x39))
#define COPRST (*(volatile byte*)(port+0x3A))
#define PPROG (*(volatile byte*)(port+0x3B))
#define HPRIO (*(volatile byte*)(port+0x3C))
#define INIT (*(volatile byte*)(port+0x3D))
#define CONFIG (*(volatile byte*)(port+0x3F))
typedef unsigned char byte;
typedef unsigned short word;
volatile byte* port=(byte*)0x1000;
int main() {
TCTL1=0x01; /* toggle oc5 = PA3 */
TOC5=TCNT+50;
while (1) {
while ((TFLG1&0x08)==0x00) /* wacht op OC5F */;
TFLG1=0x08; /* reset OC5F */
TOC5+=1000;
}
return 0;
}
inline
functies.
In C++ kunnen we de gebruikte methode om de poorten te benaderen verbergen
achter inline
functies.
enum regByte {PORTA, PIOC=0x02, PORTC, PORTB, PORTCL, DDRC=0x07,
PORTD, DDRD, PORTE, CFORC, OC1M, OC1D, TCTL1=0x20, TCTL2,
TMSK1, TFLG1, TMSK2, TFLG2, PACTL, PACNT, SPCR, SPSR,
SPDR, BAUD, SCCR1, SCCR2, SCSR, SCDR, ADCTL, ADR1, ADR2,
ADR3, ADR4, OPTION=0x39, COPRST, PPROG, HPRIO, INIT,
CONFIG=0x3f};
enum regWord {TCNT=0x0E, TIC1=0x10, TIC2=0x12, TIC3=0x14,
TOC1=0x16, TOC2=0x18, TOC3=0x1A, TOC4=0x1C, TOC5=0x1E};
typedef unsigned char byte;
typedef unsigned short word;
volatile byte* port=(byte*)0x1000;
#pragma interface
inline byte read(regByte r) {
return *(port+r);
}
inline word read(regWord r) {
return *(word*)(port+r);
}
inline void write(regByte r, byte b) {
*(port+r)=b;
}
inline void write(regWord r, word w) {
*(word*)(port+r)=w;
}
int main() {
write(TCTL1, 0x01); /* toggle oc5 = PA3 */
write(TOC5, read(TCNT)+50);
while (1) {
while ((read(TFLG1)&0x08)==0x00) /* wacht op OC5F */;
write(TFLG1, 0x08); /* reset OC5F */
write(TOC5, read(TOC5)+1000);
}
return 0;
}
class
.
In C++ kun je zelf een class maken waarin alle details verborgen zijn:
typedef unsigned char byte;
#pragma interface
class IOBox {
public:
IOBox();
byte getSwitch0() const;
byte getSwitch1() const;
byte getSwitch2() const;
byte getSwitch3() const;
byte getSwitch4() const;
byte getSwitch5() const;
byte getSwitch6() const;
byte getSwitch7() const;
byte getAllSwitches() const;
void setLed0(byte b);
void setLed1(byte b);
void setLed2(byte b);
void setLed3(byte b);
void setLed4(byte b);
void setLed5(byte b);
void setLed6(byte b);
void setLed7(byte b);
void setAllLeds(byte b);
private:
struct ports {
byte c;
byte b;
} __attribute__ ((packed));
volatile ports* port;
};
inline IOBox::IOBox(): port(reinterpret_cast<volatile ports*>(0x1003)) { }
inline byte IOBox::getSwitch0() const { return port->c&0x01; }
inline byte IOBox::getSwitch1() const { return port->c&0x02; }
inline byte IOBox::getSwitch2() const { return port->c&0x04; }
inline byte IOBox::getSwitch3() const { return port->c&0x08; }
inline byte IOBox::getSwitch4() const { return port->c&0x10; }
inline byte IOBox::getSwitch5() const { return port->c&0x20; }
inline byte IOBox::getSwitch6() const { return port->c&0x40; }
inline byte IOBox::getSwitch7() const { return port->c&0x80; }
inline byte IOBox::getAllSwitches() const { return port->c; }
inline void IOBox::setLed0(byte b) { if (b) port->b|=0x01; else port->b&=~0x01; }
inline void IOBox::setLed1(byte b) { if (b) port->b|=0x02; else port->b&=~0x02; }
inline void IOBox::setLed2(byte b) { if (b) port->b|=0x04; else port->b&=~0x04; }
inline void IOBox::setLed3(byte b) { if (b) port->b|=0x08; else port->b&=~0x08; }
inline void IOBox::setLed4(byte b) { if (b) port->b|=0x10; else port->b&=~0x10; }
inline void IOBox::setLed5(byte b) { if (b) port->b|=0x20; else port->b&=~0x20; }
inline void IOBox::setLed6(byte b) { if (b) port->b|=0x40; else port->b&=~0x40; }
inline void IOBox::setLed7(byte b) { if (b) port->b|=0x80; else port->b&=~0x80; }
inline void IOBox::setAllLeds(byte b) { port->b=b; }
int main() {
IOBox iobox;
iobox.setAllLeds(0);
while (1) {
iobox.setLed7(iobox.getSwitch0());
iobox.setLed6(iobox.getSwitch1());
iobox.setLed5(iobox.getSwitch2());
iobox.setLed4(iobox.getSwitch3());
iobox.setLed3(iobox.getSwitch4());
iobox.setLed2(iobox.getSwitch5());
iobox.setLed1(iobox.getSwitch6());
iobox.setLed0(iobox.getSwitch7());
}
return 0;
}
Door alle memberfuncties inline te definiëren levert deze versie
ook een snel (en kort) programma op! Als een inline functie wordt aangeroepen
dan wordt de code van deze functie op de plaats van aanroep ingevuld. Er
wordt niet zoals bij een "normale" functieaanroep met een JSR
instructie naar de functie toegesprongen en aan het einde van de functie
weer teruggesprongen met een RTS
instructie.
De regel:
#pragma
interface
is gcc specifiek en is nodig omdat gcc anders alle inline functies toch in
het programma opneemt (terwijl ze niet aangeroepen worden). Zonder
het gebruik van deze regel is het bovenstaande programma vertaald met de
opties -Os
-fomit-frame-pointer
-mshort
465 bytes groot.
Bij het gebruik van
#pragma
interface
is het programma nog maar 223 bytes groot.
In de onderstaande tabel kun je zien hoeveel machinecode instructies en clockcycles het kost om volgens een van de hierboven beschreven methodes PB5 (led5 van de I/O box) gelijk te maken aan PC2 (switch2 van de I/O box).
C code | met opties -Os
-fomit-frame-pointer
-mshort
1 |
met opties -O3
-fomit-frame-pointer
-mshort
2 |
|||||
---|---|---|---|---|---|---|---|
Assembler code | size 3 | speed 4 | Assembler code | size | speed | ||
1 |
|
ldy #
|
24 |
20 |
idem |
24 |
20 |
1a |
|
.L1: ldx #
|
25 |
25 |
ldy #
|
24 |
20 |
1b |
|
std $0004 ldd |
34 |
31 |
ldy |
24 |
20 |
2 |
|
.L1: ldx #
|
25 |
25 |
ldy #
|
24 |
20 |
3 |
|
ldx #
|
19 5 |
19 6 |
idem |
19 |
19 |
4 |
|
ldx #
|
19 |
19 |
idem |
19 |
19 |
5 |
|
ldx #
|
19 |
19 |
idem |
19 |
19 |
6 |
|
des
ldx #
|
27 |
42 |
idem |
27 |
42 |
7 |
|
des
ldx #
|
27 |
42 |
idem |
27 |
42 |
8 |
|
ldy #
|
24 |
20 |
idem |
24 |
20 |
v4 |
|
ldx #$1003 .L1: ldy #$1003 ldab 0,x bitb #$04 beq .L2 bset 1,x,#$20 bra .L1 .L2: bclr 1,x,#$20 bra .L1 |
24 |
23 |
ldx #$1003 ldy #$1003 .L1: ldab 0,x bitb #$04 beq .L2 bset 1,x,#$20 bra .L1 .L2: bclr 1,x,#$20 bra .L1 |
24 |
19 |
Notes:
main
.
while
lus 1x te doorlopen.
ldy
#1003
is niet meer nodig dit bespaart 4 bytes en ldab 0,x
is 1 byte korter dan ldab 0,y
.
ldab
0,x
is 1 cycle sneller dan ldab 0,y
.
IOBox
inclusief alle inline functies is te groot om in deze tabel weer te geven
en is daarom in de header file iobox.h geplaatst.