Lezen en schrijven van de 68HC11 ports.

© 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.

  1. Via volatile byte* variabele.
  2. Via volatile byte* cast.
  3. Via volatile struct ports* variabele.
  4. Via volatile byte array.
  5. Via volatile byte* pointer arithmetic. (Met dank aan Chris Werkmeester.)
  6. Via volatile struct ports* variabele met union en bitfields om afzonderlijke bits te benaderen.
  7. Via volatile union ports array met bitfields om afzonderlijke bits te benaderen.
  8. Via volatile byte& variabele. Alleen toepasbaar in C++.

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:

  1. Via functies.
  2. Via macro's.
  3. Via inline functies. Alleen toepasbaar in C++.
  4. Via een class. Alleen toepasbaar in C++.

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.

Methode 1: Via 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.

Gebruik van 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 keyword const door de C standaardisatie commissie is bedacht. Stroustrup gebruikte in eerste instantie het keyword readonly. Stroustrup heeft ook het keyword writeonly 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 keyword const te gebruiken. Vreemd genoeg is er wel een verschil tussen const in C en const in C++. In C89 mag een als const 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:

Gebruik van extern const.

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.

Methode 2: Via 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.

Wat willen we bereiken?

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.

Methode 3: Via 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:

Allignment.

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;
}

Gebruik van 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:

16 bits registers.

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:

Methode 4: Via 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 gebruik van enum.

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.

16 bits registers.

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:

Methode 5: Via 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 gebruik van enum.

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.

16 bits registers.

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:

Methode 6: Via 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;
}

Methode 7: Via 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;
}

Methode 8: Via 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:

Verbergen 1: Verbergen via functies.

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;
}

Verbergen 2: Verbergen via #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;
}

Verbergen 3: Verbergen via 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;
}

Verbergen 4: Verbergen via een 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.

De verschillende methoden vergeleken:

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
volatile byte* portc=(byte*)0x1003;
volatile byte* portb=(byte*)0x1004;
while (1) {
   if (*portc&0x04)
      *portb|=0x20;
   else
      *portb&=~0x20;
}
     ldy  #$1003
     ldx  #$1004
.L1: ldab 0,y
     bitb #$04
     beq  .L2
     bset 0,x,#$20
     bra  .L1
.L2: bclr 0,x,#$20
     bra  .L1

24

20

idem

24

20

1a

volatile const byte* const portc=(byte*)0x1003;
volatile byte* const portb=(byte*)0x1004;
while (1) {
   if (*portc&0x04)
      *portb|=0x20;
   else
      *portb&=~0x20;
}
.L1: ldx  #$1003
     ldab 0,x
     bitb #$04
     beq  .L2
     ldx  #$1004
     bset 0,x,#$20
     bra  .L1
     ldx  #$1004
.L2: bclr 0,x,#$20
     bra  .L1

25

25

     ldy  #$1003
     ldx  #$1004
.L1: ldab 0,y
     bitb #$04
     beq  .L2
     bset 0,x,#$20
     bra  .L1
.L2: bclr 0,x,#$20
     bra  .L1

24

20

1b

extern volatile const byte* const portc;
extern volatile byte* const portb;
while (1) {
   if (*portc&0x04)
      *portb|=0x20;
   else
      *portb&=~0x20;
}
     std  $0004
     ldd  $c05e
     ldx  $c060
     std  $0002
     ldd  $0004
.L1: stx  $0004
     ldx  $0002
     ldab 0,x
     ldx  $0004
     bitb #$04
     beq  .L2
     bset 0,x,#$20
     bra  .L1
     ldx  #$1004
.L2: bclr 0,x,#$20
     bra  .L1

34
+4

31

     ldy  $c054
     ldx  $c056
.L1: ldab 0,y
     bitb #$04
     beq  .L2
     bset 0,x,#$20
     bra  .L1
.L2: bclr 0,x,#$20
     bra  .L1

24
+4

20

2
while (1) {
   if (*(volatile byte*)0x1003&0x04)
      *(volatile byte*)0x1004|=0x20;
   else
      *(volatile byte*)0x1004&=~0x20;
}

.L1: ldx  #$1003
     ldab 0,x
     bitb #$04
     beq  .L2
     ldx  #$1004
     bset 0,x,#$20
     bra  .L1
.L2: ldx  #$1004
     bclr 0,x,#$20
     bra  .L1

25

25

     ldy  #$1003
     ldx  #$1004
.L1: ldab 0,y
     bitb #$04
     beq  .L2
     bset 0,x,#$20
     bra  .L1
.L2: bclr 0,x,#$20
     bra  .L1

24

20

3
struct ports {
   byte c;
   byte b;
};
volatile struct ports* port=(struct ports*)0x1003;
while (1) {
   if (port->c&0x04)
      port->b|=0x20;
   else
      port->b&=~0x20;
}
     ldx  #$1003
.L1: ldab 0,x
     bitb #$04
     beq  .L2
     bset 1,x,#$20
     bra  .L1
.L2: bclr 1,x,#$20
     bra  .L1

19  5

19  6

idem

19

19

4
volatile byte* port=(byte*)0x1003;
enum {C, B};
while (1) {
   if (port[C]&0x04)
      port[B]|=0x20;
   else
      port[B]&=~0x20;
}
     ldx  #$1003
.L1: ldab 0,x
     bitb #$04
     beq  .L2
     bset 1,x,#$20
     bra  .L1
.L2: bclr 1,x,#$20
     bra  .L1

19

19

idem

19

19

5
volatile byte* port=(byte*)0x1003;
enum {C, B};
while (1) {
   if (*(port+C)&0x04)
      *(port+B)|=0x20;
   else
      *(port+B)&=~0x20;
}
     ldx  #$1003
.L1: ldab 0,x
     bitb #$04
     beq  .L2
     bset 1,x,#$20
     bra  .L1
.L2: bclr 1,x,#$20
     bra  .L1

19

19

idem

19

19

6
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;
while (1) {
   port->b5=port->c2;
}
     des
     ldx  #$1003
.L1: ldab 0,x
     aslb
     aslb
     aslb
     andb #$20
     tsy
     stab 0,y
     ldab 1,x
     andb #$DF
     orab 0,y
     stab 1,x
     bra  .L1

27

42

idem

27

42

7
union ports {
   byte b;
   struct {
      int b7:1,b6:1,b5:1,b4:1,b3:1,b2:1,b1:1,b0:1;
   };
};
volatile union ports* port=(union ports*)0x1003;
enum {C, B};
while (1) {
   port[B].b5=port[C].b2;
}
     des
     ldx  #$1003
.L1: ldab 0,x
     aslb
     aslb
     aslb
     andb #$20
     tsy
     stab 0,y
     ldab 1,x
     andb #$DF
     orab 0,y
     stab 1,x
     bra  .L1

27

42

idem

27

42

8
volatile byte& portc=*reinterpret_cast<byte*>(0x1003);
volatile byte& portb=*reinterpret_cast<byte*>(0x1004);
while (1) {
   if (portc&0x04)
      portb|=0x20;
   else
      portb&=~0x20;
}
     ldy  #$1003
     ldx  #$1004
.L1: ldab 0,y
     bitb #$04
     beq  .L2
     bset 0,x,#$20
     bra  .L1
.L2: bclr 0,x,#$20
     bra  .L1

24

20

idem

24

20

v4
#include "iobox.h" 7

IOBox iobox;
while (1) {
   iobox.setLed5(iobox.getSwitch2());
}
     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:

  1. Optimaliseer zodat de size minimaal is.
  2. Optimaliseer zodat de speed maximaal is.
  3. Aantal bytes in main.
  4. Aantal clockcycles nodig om de while lus 1x te doorlopen.
  5. Verschil tussen methode 1 en 3: ldy #1003 is niet meer nodig dit bespaart 4 bytes en ldab 0,x is 1 byte korter dan ldab 0,y.
  6. Verschil tussen methode 1 en 3: ldab 0,x is 1 cycle sneller dan ldab 0,y.
  7. De definitie van de class 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.