In het leeslab ‘Binaire getallen, hoe werken ze?’ hebben we geleerd hoe binaire getallen werken. Maar hoe passen we deze eigenlijk toe in Arduino code? In dit lab leren we hoe we bits en bytes in Arduino code gebruiken! En hoe we de bitwise operators toe kunnen passen! Dit zullen we doen door een ledmatrix te programmeren met scrollende tekst.

In dit lab focusen we ons vooral op bits en bytes, wil je meer leren over de ledmatrix? Volg dan het volgende lab: ‘Arduino met LedMatrix en joystick’.

En nog een aantal tips: Vergeet niet ondertussen je code te testen, als het niet werkt wil je dat namelijk zo snel mogelijk weten! En wees ook niet bang om ondertussen zelf aanpassingen aan de code te maken. Van afdwalen leer je juist ontzettend veel! Als het niet werkt, kan je de code altijd terugzetten. Om de code terug te zetten kan je er voor kiezen om de code opnieuw vanuit deze pagina te kopiëren. Maar je kan ook een paar keer de toetsencombinatie Ctrl+Z in de Arduino IDE te gebruiken om de code telkens een stapje terug te zetten.

Nog een tip: De ledmatrix heeft 5 volt nodig, gebruik daarom de +5V aansluiting op de Arduino, niet de 3V3.

Wat heb je nodig

Om dit lab uit te voeren hebben we een aantal dingen nodig:

  • Een Arduino Nano
  • Een ledmatrix MAX7219
  • Een aantal jumpers
  • Een breadboard (uitleg)

De code voorbereiden

Laten we beginnen door een lege Arduino sketch te laden, deze zal er ongeveer als volgt uitzien:

void setup() {
  // De code hier zal één keer draaien wanneer het programma start
}

void loop() {
  // De code hier zal telkens herhaald worden
}

Om met de ledmatrix te communiceren zullen we de LedControl library gebruiken. Deze library kunnen we aan onze sketch toevoegen door het volgende helemaal bovenin aan onze code toe te voegen:

#include "LedControl.h"

Ook is het belangrijk dat de ‘LedControl’ library, geschreven door ‘Eberhard Fahle’, geïnstalleerd is. Als dit niet het geval is, kun je deze met library manager volgens de volgende instructies installeren: ‘Een Arduino library downloaden’.

Nu we de library geïmporteerd hebben kunnen we de pins die we willen gebruiken instellen. Om dit voor elkaar te krijgen zetten we de volgende code direct onder de #include “LedControl.h”:

/*
D12: DataIn
D11: CLK
D10: CS
Aantal displays: 1
*/
LedControl lc = LedControl(12, 11, 10, 1);

Vervolgens kunnen we het display aanzetten door de volgende code toe te voegen aan de void setup():

// Zet het display uit voor het geval dat hij aan stond:
lc.shutdown(0, false);
// Zet de helderheid op gemiddeld:
lc.setIntensity(0, 8);
// Maak het scherm leeg:
lc.clearDisplay(0);

Als het goed is ziet de code er nu ongeveer als volgt uit:

#include "LedControl.h"

/*
D12: DataIn
D11: CLK
D10: CS
Aantal displays: 1
*/
LedControl lc = LedControl(12, 11, 10, 1);

void setup() {
  // De code hier zal één keer draaien wanneer het programma start
  
  // Zet het display uit voor het geval dat hij aan stond:
  lc.shutdown(0, false);
  // Zet de helderheid op gemiddeld:
  lc.setIntensity(0, 8);
  // Maak het scherm leeg:
  lc.clearDisplay(0);
}

void loop() {
  // De code hier zal telkens herhaald worden
}

Iets op het scherm zetten

Nu is het tijd om iets op het scherm kunnen zetten! De library die we gebruiken wilt graag dat we voor elke regel los met een byte aangeven wat we op het scherm willen zetten. Dit doen we bijvoorbeeld met de  code hieronder, deze code vult de helft van de bovenste regel van onze ledmatrix. Deze code kan in de void loop() gezet worden:


// Werking functie: lc.setRow(displaynummer, regelnummer, byte met onze data);
lc.setRow(0, 0, 0b11110000);

Om het hele scherm te vullen kunnen we deze code dupliceren voor elke regel. Probeer dit eens zelf! Vergeet niet om het regelnummer aan te passen. Als je dat voor elkaar hebt gekregen kun je ook proberen om een ander vormpje op het scherm te zetten!

Oplossing:
De volledige code zal er nu ongeveer zo uitzien:

#include "LedControl.h"

/*
D12: DataIn
D11: CLK
D10: CS
Aantal displays: 1
*/
LedControl lc = LedControl(12, 11, 10, 1);

void setup() {
  // De code hier zal één keer draaien wanneer het programma start
  
  // Zet het display uit voor het geval dat hij aan stond:
  lc.shutdown(0, false);
  // Zet de helderheid op gemiddeld:
  lc.setIntensity(0, 8);
  // Maak het scherm leeg:
  lc.clearDisplay(0);
}

void loop() {
  // De code hier zal telkens herhaald worden

  lc.setRow(0, 0, 0b00001111);
  lc.setRow(0, 1, 0b11110000);
  lc.setRow(0, 2, 0b00001111);
  lc.setRow(0, 3, 0b11110000);
  lc.setRow(0, 4, 0b00001111);
  lc.setRow(0, 5, 0b11110000);
  lc.setRow(0, 6, 0b00001111);
  lc.setRow(0, 7, 0b11110000);
}

Werken met variabelen

Om bitwise operators te gebruiken is het handig om met variabelen te werken. Variabele zijn namelijk getallen waarvan we de waarde kunnen veranderen. Hierdoor kunnen we wat op het scherm staat veranderen. Het handigst is om deze variabelen toe te voegen boven de void setup(). We maken hiervoor gebruik van de data type ‘byte’:

byte regel0 = 0b11110000;
byte regel1 = 0b11110000;
byte regel2 = 0b11110000;
byte regel3 = 0b11110000;
byte regel4 = 0b11110000;
byte regel5 = 0b11110000;
byte regel6 = 0b11110000;
byte regel7 = 0b11110000;

Vervolgens kunnen we code vertellen om deze variabeles te gebruiken door deze in te vullen in de functie lc.setRow(). Elke setRow functie zal er dus ongeveer als volgt uitzien:

lc.setRow(0, 5, regel5);

Probeer nu eens om opnieuw aan te passen wat er op het scherm te zien is! Probeer dit te doen door de variabelen aan te passen!

Ons programma ziet er nu als volgt uit:

#include "LedControl.h"

/*
D12: DataIn
D11: CLK
D10: CS
Aantal displays: 1
*/
LedControl lc = LedControl(12, 11, 10, 1);

byte regel0 = 0b11110000;
byte regel1 = 0b11110000;
byte regel2 = 0b11110000;
byte regel3 = 0b11110000;
byte regel4 = 0b11110000;
byte regel5 = 0b11110000;
byte regel6 = 0b11110000;
byte regel7 = 0b11110000;

void setup() {
  // De code hier zal één keer draaien wanneer het programma start
  
  // Zet het display uit voor het geval dat hij aan stond:
  lc.shutdown(0, false);
  // Zet de helderheid op gemiddeld:
  lc.setIntensity(0, 8);
  // Maak het scherm leeg:
  lc.clearDisplay(0);
}

void loop() {
  // De code hier zal telkens herhaald worden

  lc.setRow(0, 0, regel0);
  lc.setRow(0, 1, regel1);
  lc.setRow(0, 2, regel2);
  lc.setRow(0, 3, regel3);
  lc.setRow(0, 4, regel4);
  lc.setRow(0, 5, regel5);
  lc.setRow(0, 6, regel6);
  lc.setRow(0, 7, regel7);
}

Scrollen

Nu is het eindelijk tijd om de tekst te laten bewegen. Dit kunnen we doen door een bitwise operator te gebruiken. Welke denk je dat we hiervoor zullen gebruiken? Misschien wil je even teruglezen in het leeslab ‘Binaire getallen, hoe werken ze?’.

Dat is correct, we gaan bitshiften. Als je een vermoeden hebt dat je al weet hoe we dit gaan doen, probeer het dan eens zelf (vergeet hierbij niet om een delay in de loop toe te voegen). Zo niet, lees verder!

De reden dat we in de vorige stap variabelen hebben toe gevoegd, is omdat we de waarde ervan kunnen veranderen. We zullen dus de bitwise operators hierop dus toe kunnen passen.  Om bijvoorbeeld alle bits in regel0 naar rechts te verplaatsen kunnen we de volgende code aan de void setup() toevoegen:

regel0 = regel0 >> 1;

Met deze code geven we aan dat regel0 gelijk moet zijn aan: regel0 maar dan één stapje naar rechts.

Als we deze code verplaatsen naar de void loop() zal het elke keer dat we het scherm herladen opnieuw worden uitgevoerd. Probeer het eens uit!

Als het plaatje te snel verplaatst kan je altijd een delay() toevoegen aan de loop, gebruik bijvoorbeeld de volgende regel code:

delay(250);

Deze code verteld de Arduino om 250 miliseconden (0,25 seconden) te wachten. We hebben nu de eerste regel laten scrollen! Krijg jij het voor elkaar om de rest van de rest van de regels mee te laten scrollen?

Oplossing:
De code zal er nu ongeveer zo uit moeten zien:

#include "LedControl.h"

/*
D12: DataIn
D11: CLK
D10: CS
Aantal displays: 1
*/
LedControl lc = LedControl(12, 11, 10, 1);

byte regel0 = 0b11110000;
byte regel1 = 0b11110000;
byte regel2 = 0b11110000;
byte regel3 = 0b11110000;
byte regel4 = 0b11110000;
byte regel5 = 0b11110000;
byte regel6 = 0b11110000;
byte regel7 = 0b11110000;

void setup() {
  // De code hier zal één keer draaien wanneer het programma start
  
  // Zet het display uit voor het geval dat hij aan stond:
  lc.shutdown(0, false);
  // Zet de helderheid op gemiddeld:
  lc.setIntensity(0, 8);
  // Maak het scherm leeg:
  lc.clearDisplay(0);
}

void loop() {
  // De code hier zal telkens herhaald worden

  lc.setRow(0, 0, regel0);
  lc.setRow(0, 1, regel1);
  lc.setRow(0, 2, regel2);
  lc.setRow(0, 3, regel3);
  lc.setRow(0, 4, regel4);
  lc.setRow(0, 5, regel5);
  lc.setRow(0, 6, regel6);
  lc.setRow(0, 7, regel7);

  regel0 = regel0 >> 1;
  regel1 = regel1 >> 1;
  regel2 = regel2 >> 1;
  regel3 = regel3 >> 1;
  regel4 = regel4 >> 1;
  regel5 = regel5 >> 1;
  regel6 = regel6 >> 1;
  regel7 = regel7 >> 1;

  delay(250);
}

Een breder plaatje

We hebben nu een plaatje gemaakt wat wegscrolt! Maar wat nog wel wat jammer is, is dat er niet meer op het scherm komt. Maar hoe lossen we dit op?

Zoals in het leeslab ‘Binaire getallen, hoe werken ze?’ beschreven staat bevat een byte, 8 bits. Maar we kunnen ook een data type gebruiken die meer bits bevat. Laten we eens zien wat er gebeurd als we een uint64_t gebruiken in plaats van een byte! Laten we de regel byte regel0 = 0b11110000; vervangen met:

uint64_t regel0 = 0b1111000011110000111100001111000011110000111100001111000011110000;

Kijk eens of het werkt! Zo ja, lukt het je ook om de andere regels op dezelfde manier aan te passen? En, probeer er nu eens een plaatje van te maken!

Onze code ziet er nu als volgt uit:

#include "LedControl.h"

/*
D12: DataIn
D11: CLK
D10: CS
Aantal displays: 1
*/
LedControl lc = LedControl(12, 11, 10, 1);

uint64_t regel0 = 0b1111000011110000111100001111000011110000111100001111000011110000;
uint64_t regel1 = 0b1111000011110000111100001111000011110000111100001111000011110000;
uint64_t regel2 = 0b1111000011110000111100001111000011110000111100001111000011110000;
uint64_t regel3 = 0b1111000011110000111100001111000011110000111100001111000011110000;
uint64_t regel4 = 0b1111000011110000111100001111000011110000111100001111000011110000;
uint64_t regel5 = 0b1111000011110000111100001111000011110000111100001111000011110000;
uint64_t regel6 = 0b1111000011110000111100001111000011110000111100001111000011110000;
uint64_t regel7 = 0b1111000011110000111100001111000011110000111100001111000011110000;

void setup() {
  // De code hier zal één keer draaien wanneer het programma start
  
  // Zet het display uit voor het geval dat hij aan stond:
  lc.shutdown(0, false);
  // Zet de helderheid op gemiddeld:
  lc.setIntensity(0, 8);
  // Maak het scherm leeg:
  lc.clearDisplay(0);
}

void loop() {
  // De code hier zal telkens herhaald worden

  lc.setRow(0, 0, regel0);
  lc.setRow(0, 1, regel1);
  lc.setRow(0, 2, regel2);
  lc.setRow(0, 3, regel3);
  lc.setRow(0, 4, regel4);
  lc.setRow(0, 5, regel5);
  lc.setRow(0, 6, regel6);
  lc.setRow(0, 7, regel7);

  regel0 = regel0 >> 1;
  regel1 = regel1 >> 1;
  regel2 = regel2 >> 1;
  regel3 = regel3 >> 1;
  regel4 = regel4 >> 1;
  regel5 = regel5 >> 1;
  regel6 = regel6 >> 1;
  regel7 = regel7 >> 1;

  delay(250);
}

De laatste stap

Zoals we in het leeslab ‘Binaire getallen, hoe werken ze?’ hebben gelezen, zal de >>-bewerking alle bits naar rechts verplaatsen, aan de linkerkant worden dan nullen toegevoegd. Dit zorgt er dus voor dat ons plaatje na één keer verdwijnt. Maar hoe zorgen we ervoor dat ons plaatje telkens terugkomt?

Laten we eerst bedenken hoe we dit voor elkaar kunnen krijgen. Welke bitwise operators willen we hiervoor gebruiken? Probeer het probleem eens op te delen in stukken, welke stappen zou de computer uit kunnen voeren om dit voor elkaar te krijgen? En hoe kunnen we dit vervolgens in code implementeren?

Oplossing: (er zijn ook andere oplossingen mogelijk)

We willen alle bits naar rechts verplaatsen. Maar de meest rechter bit willen we helemaal naar links verplaatsen en aan het begin van de reeks invoegen. Dit is in het plaatje hieronder (verkort) te zien.

Het rode deel hebben we al toegevoegd, dit wordt namelijk al gedaan door de volgende code:

regel0 = regel0 >> 1;

We zullen dus alleen het deel wat met de groene pijl aangewezen wordt toe moeten voegen. Daarmee brengen we dan de laatste bit helemaal aan de linker kant, zodat deze helemaal aan het begin staat. Maar hoe kunnen we dit combineren met de rode pijlen?

We kunnen de variabele regel0 klonen! Dan kunnen we de stap die met de rode pijl beschreven staat op het origineel uitvoeren. Vervolgens kunnen we de stap die met de groene pijl beschreven wordt uitvoeren op de kloon. Dan hebben we nog maar één ding te doen: het samenvoegen van de twee variabelen.

Maar hoe implementeren we dit in code? Dit doen we als volgt:

  • We klonen onze variabele, zodat we er in plaats van één, twee hebben(dit doen gelijk voordat we de bits naar rechts verplaatsen):
uint64_t kloonRegel0 = regel0;
  • Dan verplaatsen we de bits in de orginele variabele één stap naar rechts:
regel0 = regel0 >> 1;
  • Vervolgens verplaatsen we de bits in onze gekloonde variabele helemaal naar links. Deze moet in een uint64_t dus 63 stappen naar links verplaatsen (een uint64_t is 64 bits lang).
kloonRegel0 = kloonRegel0 << 63;
  • Nu kunnen we de twee variabelen weer samenvoegen. Omdat de bitshift bewerking alle nieuwe bits gelijk maakt aan nul kunnen we de twee variabelen weer samenvoegen met zowel een OR- als een XOR-bewerking. In het voorbeeld is gekozen voor de OR-bewerking:
regel0 = regel0 | kloonRegel0;

De code om het hele proces uit te voeren zal er dus als volgt uitzien:


uint64_t kloonRegel0 = regel0;
regel0 = regel0 >> 1;
kloonRegel0 = kloonRegel0 << 63;
regel0 = regel0 | kloonRegel0;

Laten we de code testen!

Ons programma ziet er nu als volgt uit:

#include "LedControl.h"

/*
D12: DataIn
D11: CLK
D10: CS
Aantal displays: 1
*/
LedControl lc = LedControl(12, 11, 10, 1);

uint64_t regel0 = 0b1111000011110000111100001111000011110000111100001111000011110000;
uint64_t regel1 = 0b1111000011110000111100001111000011110000111100001111000011110000;
uint64_t regel2 = 0b1111000011110000111100001111000011110000111100001111000011110000;
uint64_t regel3 = 0b1111000011110000111100001111000011110000111100001111000011110000;
uint64_t regel4 = 0b1111000011110000111100001111000011110000111100001111000011110000;
uint64_t regel5 = 0b1111000011110000111100001111000011110000111100001111000011110000;
uint64_t regel6 = 0b1111000011110000111100001111000011110000111100001111000011110000;
uint64_t regel7 = 0b1111000011110000111100001111000011110000111100001111000011110000;

void setup() {
  // De code hier zal één keer draaien wanneer het programma start
  
  // Zet het display uit voor het geval dat hij aan stond:
  lc.shutdown(0, false);
  // Zet de helderheid op gemiddeld:
  lc.setIntensity(0, 8);
  // Maak het scherm leeg:
  lc.clearDisplay(0);
}

void loop() {
  // De code hier zal telkens herhaald worden

  lc.setRow(0, 0, regel0);
  lc.setRow(0, 1, regel1);
  lc.setRow(0, 2, regel2);
  lc.setRow(0, 3, regel3);
  lc.setRow(0, 4, regel4);
  lc.setRow(0, 5, regel5);
  lc.setRow(0, 6, regel6);
  lc.setRow(0, 7, regel7);

  
  uint64_t kloonRegel0 = regel0;
  regel0 = regel0 >> 1;
  kloonRegel0 = kloonRegel0 << 63; 
  regel0 = regel0 | kloonRegel0; 
  
  uint64_t kloonRegel1 = regel1; 
  regel1 = regel1 >> 1;  
  kloonRegel1 = kloonRegel1 << 63; 
  regel1 = regel1 | kloonRegel1; 
  
  uint64_t kloonRegel2 = regel2; 
  regel2 = regel2 >> 1;
  kloonRegel2 = kloonRegel2 << 63; 
  regel2 = regel2 | kloonRegel2; 
  
  uint64_t kloonRegel3 = regel3; 
  regel3 = regel3 >> 1;
  kloonRegel3 = kloonRegel3 << 63; 
  regel3 = regel3 | kloonRegel3; 
  
  uint64_t kloonRegel4 = regel4; 
  regel4 = regel4 >> 1;
  kloonRegel4 = kloonRegel4 << 63; 
  regel4 = regel4 | kloonRegel4; 
  
  uint64_t kloonRegel5 = regel5; 
  regel5 = regel5 >> 1;
  kloonRegel5 = kloonRegel5 << 63; 
  regel5 = regel5 | kloonRegel5; 
  
  uint64_t kloonRegel6 = regel6; 
  regel6 = regel6 >> 1;
  kloonRegel6 = kloonRegel6 << 63; 
  regel6 = regel6 | kloonRegel6; 
  
  uint64_t kloonRegel7 = regel7; 
  regel7 = regel7 >> 1;
  kloonRegel7 = kloonRegel7 << 63;
  regel7 = regel7 | kloonRegel7;


  delay(250);
}

Onze code opschonen

Ons programma is af! We kunnen er nu voor kiezen om de code een beetje op te schonen. Maar hoe doen we dit?

We kunnen code als regel2 = regel2 >> 1; verkorten naar regel2 >>= 1;, dit doet namelijk precies hetzelfde. Hierdoor zal dus het volgende veranderen:


// origineel:
uint64_t kloonRegel2 = regel2;
regel2 = regel2 >> 1;
kloonRegel2 = kloonRegel2 << 63; 
regel2 = regel2 | kloonRegel2; 
  
// nieuw: 
uint64_t kloonRegel2 = regel2; 
regel2 >>= 1;
kloonRegel2 <<= 63;
regel2 |= kloonRegel2;

Als je al weet hoe functies werken zou je ook kunnen proberen om de herhaalde blokken code in functies te zetten.

Klik hier voor meer details

Een functie die de bewerking uitvoert kan er zo uitzien:


// als functie:
uint64_t stapje( uint64_t i ) {
uint64_t kloonRegel2 = i; 
i >>= 1;
kloonRegel2 <<= 63;
i |= kloonRegel2;
return i;
}

De funxtie definitie plaats je buiten de loop() functie, in dit geval het liefst voor de lopp() en de setup().
De aanroep van de functie:


// origineel: 
uint64_t kloonRegel2 = regel2; 
regel2 >>= 1; 
kloonRegel2 <<= 63; 
regel2 |= kloonRegel2; 
  
// nieuw: 
regel2 = stapje(regel2);

Wat nu?

Probeer eens wat met de code te spelen! Wat kan je allemaal veranderen? Misschien lukt het je wel om nieuwe features toe te voegen! En als je zelf geen leuke ideeën hebt, kan je altijd de volgende dingen proberen:

  • Kan het plaatje ook de andere kant op scrollen?
  • Kunnen we het scherm aan en uit laten knipperen?
  • Zouden we een soort fotolijst die niet meescrolt toe kunnen voegen? (Bekijk hiervoor hoe bitmasks werken in het leeslab ‘Binaire getallen, hoe werken ze?’ onder het kopje bitwise operators).