ARDUINO WERKPLAATS
◂ gebruikt de lijnvolger · en voor het eerst geluid Project 07 · het behendigheidsspel

Vaste hand.

Ken je dat spel met een ring om een gebogen ijzerdraad, waarbij het zoemt zodra je de draad raakt? Dat bouwen we na — maar dan met licht. Je tekent zelf een kronkelparcours, volgt het met het sensortje, en bij elke misser piept het. Ondertussen loopt de klok.

Sensor: lijnvolger Nieuw: buzzer + millis() Zelf tekenen: het parcours ~45 min
Welk bord gebruik je?
Alle pinnen in dit project zijn op beide borden gelijk.
Welk scherm gebruik je?
Het spel in stap 7 wisselt mee naar het scherm dat jij hebt.
0

Wat je nodig hebt

  • 1.44 inch TFT-scherm Je 1.44" TFT-scherm, aangesloten zoals in project 01.
  • 0.96 inch OLED-schermpje Je 0.96" OLED-schermpje, aangesloten zoals in de OLED-start.
  • Lijnvolger met vier pinnen De lijnvolger: het printje met vier pinnen (VCC · GND · D0 · A0) en één zwart blokje. Niet de IR-obstakelsensor met drie pinnen — zie het verschil.
  • Passieve piezo-buzzer Een buzzer: het ronde zwarte kastje met twee pootjes.
  • Een drukknop — of gewoon twee draadjes die je even tegen elkaar houdt, net als in de OLED-start.
  • Wit papier, een dikke zwarte stift en een stukje karton. Daar maak je in stap 3 het parcours en het sledeje van.
Wat er nieuw is in dit project Twee dingen. Ten eerste de buzzer: voor het eerst laat je de Arduino zélf iets doen, in plaats van alleen meten en op het scherm zetten. Ten tweede millis(), waarmee je een klok laat lopen terwijl je ondertussen gewoon doormeet. Dat lukt niet met delay(), en dat is precies het punt.
1

Hoe het spel werkt

De lijnvolger schijnt onzichtbaar licht naar beneden en meet hoeveel ervan terugkaatst. Wit papier kaatst veel terug, zwarte stift bijna niets. Daar heb je alles mee: zit het sensortje boven de lijn, dan is de meting "donker"; schiet je ernaast, dan wordt hij ineens "licht".

boven de lijn — goed bijna niets terug meting: donker ernaast — piep! veel terug meting: licht
Zwart slikt het licht op, wit kaatst het terug. Dát verschil is het hele spel.

Het spel verloopt in drie standen, en de knop brengt je steeds naar de volgende:

Wachten
Het scherm zegt "druk op de knop". Zet de sensor vast op de startstip.
Spelen
De klok loopt. Volg de lijn. Elke keer dat je eraf raakt: piep, en een fout erbij.
Klaar
Aan de finish druk je weer. Je ziet je tijd én je aantal missers.
Optillen is valsspelen — en daarom maken we een sledeje Til je het sensortje op, dan komt er óók bijna niets terug en denkt hij dat je nog op de zwarte lijn zit. Daarom monteren we hem in stap 3 op een stukje karton, zodat hij altijd op dezelfde hoogte over het papier glijdt. Zonder dat sledeje klopt het spel gewoon niet.
2

Alles aansluiten

Drie dingen erbij: de sensor, de buzzer en de knop. We gebruiken van de lijnvolger alleen de A0-pin — dat is de analoge uitgang, en juist die maakt dit spel mogelijk.

Arduino Uno R4 Lijnvolger VCC5V GNDGND A0A2 D0 niet gebruiken Buzzer pootje 1D3 pootje 2GND Knop de andere kant naar GND D2
De A0 van de lijnvolger gaat naar A2 van je Arduino — let op, dat zijn twee verschillende namen voor twee verschillende dingen.
OnderdeelPinNaar je ArduinoDraad
LijnvolgerVCC5Vrood
LijnvolgerGNDGNDzwart
LijnvolgerA0A2teal
Buzzerpootje 1D3amber
Buzzerpootje 2GNDzwart
Knopéén kantD2groen
Knopandere kantGNDzwart
Twee keer "A0" — verwarrend, maar logisch Op de lijnvolger betekent A0 "analoge uitgang". Op je Arduino is A0 de naam van een pin. Ze horen niet bij elkaar. Wij prikken de A0 van de sensor in A2 van de Arduino, want A0 en A1 gebruikt de joystick uit project 01.
Waarom die knop op D2 mag In project 01 zit daar de knop van de joystick. Dat botst niet: je speelt dit spel niet tegelijk met dat spel. Heb je de joystick nog aangesloten, dan kun je zelfs zijn knop gebruiken — die zit al op D2 en werkt precies hetzelfde.
3

Het parcours tekenen en het sledeje maken

Geen code in deze stap — knutselen. En dit is meteen het leukste stuk, want jij bepaalt hoe moeilijk het spel wordt.

Het parcours

Teken op een wit vel een kronkelende lijn met een dikke zwarte stift. Zet aan het begin een dikke stip (de start) en aan het eind ook een (de finish). Hoe dikker je lijn, hoe makkelijker; hoe meer bochten, hoe gemener. Begin met een lijn van ongeveer een centimeter breed — dat speelt lekker.

start finish het sledeje van opzij karton sensor 3–5 mm papier met de lijn het karton glijdt, de hoogte blijft gelijk
Links je eigen parcours, rechts waarom het sledeje nodig is.

Het sledeje

  1. Knip een stukje karton van ongeveer 5 bij 8 cm.
  2. Knip er een gaatje in waar het zwarte blokje van de sensor doorheen kan kijken.
  3. Plak of tape de sensor erop, zodat het blokje 3 tot 5 millimeter boven het papier hangt.
  4. Zorg dat de draadjes losjes zitten en niet aan het karton trekken.
Test het meteen even Schuif het sledeje over je parcours heen en weer. Het moet soepel glijden zonder te kantelen. Kantelt hij, maak het karton dan wat groter — meer oppervlak is stabieler.
4

Kalibreren: welke getallen zie jij?

Nu meten we wat de sensor eigenlijk teruggeeft. Dat verschilt per printje, per stift en per papiersoort, dus iedereen krijgt andere getallen. Daarom meet je ze zelf.

01_kalibreren.ino
#define SENSOR A2      // de A0-pin van de lijnvolger

void setup() {
  Serial.begin(9600);
}

void loop() {
  Serial.println(analogRead(SENSOR));   // een getal van 0 t/m 1023
  delay(300);
}

Upload dit, open Tools → Serial Monitor en doe twee metingen met het sledeje op het papier:

  1. Zet hem op een wit stuk van het vel. Noteer het getal. Dat is jouw WIT.
  2. Zet hem midden op de zwarte lijn. Noteer het getal. Dat is jouw ZWART.
Welke van de twee is het hoogst? Dat maakt niet uit! Bij sommige printjes gaat het getal omhoog boven wit, bij andere omlaag. Onze code kijkt straks alleen welke van jouw twee getallen het dichtst bij de meting ligt. Zo werkt het spel bij allebei de soorten — precies zoals in project 05, waar je OBJECT zelf op LOW of HIGH kon zetten.
Liggen de twee getallen dicht bij elkaar? Als WIT en ZWART maar een paar tientallen verschillen, gaat het spel niet lekker werken. Probeer dan: het sledeje wat lager, een dikkere of zwartere stift, of feller wit papier. Een verschil van minstens 150 speelt prettig.
5

De buzzer: de Arduino maakt geluid

Tot nu toe las je alleen dingen uit en zette je ze op het scherm. Nu draaien we het om: jij stuurt een onderdeel aan. Met tone() geef je een pin een snel trillend signaal, en dat hoor je als een toon.

02_buzzer_test.ino
#define BUZZER 3

void setup() {
  pinMode(BUZZER, OUTPUT);
}

void loop() {
  tone(BUZZER, 880, 200);   // 880 trillingen per seconde, 200 ms lang
  delay(1000);
  tone(BUZZER, 440, 200);   // 440 = een octaaf lager
  delay(1000);
}

Je hoort om beurten een hoge en een lage piep. Het getal in het midden is de toonhoogte: hoger getal, hoger piepje. Speel er gerust mee.

De handigste eigenschap van tone() Het derde getal is hoe lang de toon duurt — maar je code wacht daar niet op. tone() zet de piep aan en gaat meteen door met de volgende regel. Dat is precies wat we nodig hebben: tijdens het spel mag een piepje het spel niet ophouden.
Piept jouw buzzer meteen, ook zonder code? Dan heb je een actieve buzzer in plaats van een passieve. Die maakt zijn eigen toon zodra er spanning op staat. Vervang dan in alle sketches hieronder tone(BUZZER, 880, 150); door twee regels: digitalWrite(BUZZER, HIGH); delay(80); digitalWrite(BUZZER, LOW); Je kunt de toonhoogte dan niet kiezen, maar piepen doet hij.
6

De stopwatch: tellen zonder stil te staan

In project 03 gebruikten we delay(2000) om te wachten. Het nadeel: tijdens een delay staat je hele Arduino stil. Hij meet niets, hij kijkt niet naar de knop, hij doet niks. Voor een spel is dat rampzalig — je zou de helft van je missers niet zien.

De oplossing is millis(). Dat is een teller die vanaf het aanzetten vanzelf doorloopt en je vertelt hoeveel duizendste seconden er voorbij zijn. Je wacht niet, je kijkt alleen op de klok.

met delay(1000): niets doen · niets doen · niets doen · niets doen de knop indrukken? wordt gemist. met millis(): elke ronde: meten, knop checken, en even op de klok kijken
delay() zet alles stil; millis() laat je gewoon doorwerken.
03_stopwatch.ino
unsigned long startTijd;      // "unsigned long" = een heel groot geheel getal
unsigned long vorigePrint = 0;

void setup() {
  Serial.begin(9600);
  startTijd = millis();       // onthoud wanneer we begonnen
}

void loop() {
  unsigned long nu = millis();          // hoe laat is het nu?

  // elke halve seconde de tijd tonen -- zonder delay!
  if (nu - vorigePrint >= 500) {
    vorigePrint = nu;
    Serial.println((nu - startTijd) / 1000.0, 1);   // seconden, 1 cijfer achter de komma
  }

  // hier zou je ondertussen gewoon kunnen blijven meten en knoppen checken
}
Waarom unsigned long? millis() telt duizendsten van seconden. Na één minuut zit je al op 60.000, en een gewone int gaat maar tot 32.767. Een unsigned long kan tot ruim vier miljard — genoeg voor 49 dagen.
7

Het complete spel

Alles bij elkaar. Vul bovenaan jouw eigen getallen uit stap 4 in bij WIT en ZWART, anders werkt het niet.

De twee slimme regeltjes in deze sketch

1. Welke kant op je printje ook werkt. opDeLijn() kijkt of de meting meer op jouw ZWART of meer op jouw WIT lijkt. Daarom hoef je niet te weten of het getal omhoog of omlaag gaat.

2. Alleen de overgang telt. Blijf je een seconde naast de lijn hangen, dan is dat één fout — geen honderd. Dat is dezelfde flankdetectie als in project 05: we onthouden wat het vorige rondje was.

VASTE HAND Druk op de knop Zet hem op de lijn wachten VASTE HAND tijd 12.4 fouten 2 spelen KLAAR! tijd 31.8 fouten 3 Druk = opnieuw klaar
De drie standen op je 128 × 128 scherm. De knop brengt je van links naar rechts.
04_vaste_hand.ino
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <SPI.h>

#define TFT_CS 10
#define TFT_RST 9
#define TFT_DC 8
#define SENSOR A2
#define BUZZER 3
#define KNOP   2

// ===== VUL HIER JOUW METINGEN UIT STAP 4 IN =====
const int WIT   = 620;
const int ZWART = 180;
// ================================================

Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);

const byte WACHT = 0, SPEEL = 1, KLAAR = 2;
byte stand = WACHT;

unsigned long startTijd = 0;
int  fouten = 0;
bool vorigeOpLijn = true;
int  vorigeTiende = -1;
bool vorigeKnop = HIGH;

// Lijkt de meting meer op ZWART dan op WIT? Dan zit je op de lijn.
// Zo werkt het bij elk printje, welke kant het getal ook op gaat.
bool opDeLijn(int waarde) {
  return abs(waarde - ZWART) < abs(waarde - WIT);
}

// Geeft true op het moment dat de knop wordt INGEDRUKT (niet zolang hij vastzit)
bool knopGedrukt() {
  bool nu = digitalRead(KNOP);
  bool gedrukt = (vorigeKnop == HIGH && nu == LOW);
  vorigeKnop = nu;
  if (gedrukt) delay(200);      // ontdubbelen: knoppen "stuiteren"
  return gedrukt;
}

void schermWacht() {
  tft.fillScreen(ST77XX_BLACK);
  tft.setTextSize(1);
  tft.setTextColor(ST77XX_WHITE);
  tft.setCursor(6, 4);    tft.print("VASTE HAND");
  tft.setTextSize(2);
  tft.setTextColor(ST77XX_GREEN);
  tft.setCursor(6, 46);   tft.print("Druk op");
  tft.setCursor(6, 66);   tft.print("de knop");
  tft.setTextSize(1);
  tft.setTextColor(ST77XX_WHITE);
  tft.setCursor(6, 114);  tft.print("Zet hem op de lijn");
}

void schermSpelVast() {          // de tekst die tijdens het spelen blijft staan
  tft.fillScreen(ST77XX_BLACK);
  tft.setTextSize(1);
  tft.setTextColor(ST77XX_WHITE);
  tft.setCursor(6, 4);    tft.print("VASTE HAND");
  tft.setCursor(6, 22);   tft.print("tijd");
  tft.setCursor(6, 64);   tft.print("fouten");
}

void toonTijd(unsigned long ms) {
  tft.fillRect(0, 34, 128, 24, ST77XX_BLACK);   // alleen dit vakje wissen
  tft.setTextSize(3);
  tft.setTextColor(ST77XX_CYAN);
  tft.setCursor(6, 34);
  tft.print(ms / 1000.0, 1);
}

void toonFouten() {
  tft.fillRect(0, 76, 128, 32, ST77XX_BLACK);
  tft.setTextSize(4);
  tft.setTextColor(fouten == 0 ? ST77XX_GREEN : ST77XX_RED);
  tft.setCursor(6, 76);
  tft.print(fouten);
}

void setup() {
  pinMode(KNOP, INPUT_PULLUP);
  pinMode(BUZZER, OUTPUT);
  tft.initR(INITR_144GREENTAB);
  schermWacht();
}

void loop() {
  // ---------- WACHTEN ----------
  if (stand == WACHT) {
    if (knopGedrukt()) {
      fouten = 0;
      vorigeTiende = -1;
      vorigeOpLijn = opDeLijn(analogRead(SENSOR));  // begin zoals je nu staat
      startTijd = millis();
      stand = SPEEL;
      schermSpelVast();
      toonFouten();
    }
    return;
  }

  // ---------- SPELEN ----------
  if (stand == SPEEL) {
    // 1. van de lijn af geraakt?
    bool nuOpLijn = opDeLijn(analogRead(SENSOR));
    if (vorigeOpLijn && !nuOpLijn) {        // alleen de OVERGANG telt
      fouten++;
      tone(BUZZER, 880, 150);               // piep -- en meteen door
      toonFouten();
    }
    vorigeOpLijn = nuOpLijn;

    // 2. de klok bijwerken, maar alleen als er iets verandert
    unsigned long tijd = millis() - startTijd;
    int tiende = tijd / 100;
    if (tiende != vorigeTiende) {
      vorigeTiende = tiende;
      toonTijd(tijd);
    }

    // 3. finish!
    if (knopGedrukt()) {
      stand = KLAAR;
      tft.fillScreen(ST77XX_BLACK);
      tft.setTextSize(2);
      tft.setTextColor(ST77XX_YELLOW);
      tft.setCursor(6, 8);    tft.print("KLAAR!");
      tft.setTextSize(1);
      tft.setTextColor(ST77XX_WHITE);
      tft.setCursor(6, 40);   tft.print("tijd");
      tft.setTextSize(3);
      tft.setTextColor(ST77XX_CYAN);
      tft.setCursor(6, 52);   tft.print(tijd / 1000.0, 1);
      tft.setTextSize(1);
      tft.setTextColor(ST77XX_WHITE);
      tft.setCursor(6, 84);   tft.print("fouten");
      tft.setTextSize(3);
      tft.setTextColor(fouten == 0 ? ST77XX_GREEN : ST77XX_RED);
      tft.setCursor(6, 96);   tft.print(fouten);
      tft.setTextSize(1);
      tft.setTextColor(ST77XX_WHITE);
      tft.setCursor(6, 120);  tft.print("Druk = opnieuw");
    }
    return;
  }

  // ---------- KLAAR ----------
  if (knopGedrukt()) {
    stand = WACHT;
    schermWacht();
  }
}
VASTE HAND Druk op de knop wachten VASTE HAND tijd fout 12.4 2 Druk = finish spelen KLAAR! tijd fouten 31.8 3 Druk = opnieuw klaar
Dezelfde drie standen, maar op 128 × 64 — dus tijd en fouten naast elkaar.
Op de OLED is dit makkelijker Op het kleurenscherm moeten we bijhouden wát er verandert, anders flikkert het. Jij hebt een kladblok: elke ronde clearDisplay(), alles opnieuw tekenen, display(). Dat scheelt een hoop code.
04_vaste_hand_oled.ino
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SENSOR A2
#define BUZZER 3
#define KNOP   2

// ===== VUL HIER JOUW METINGEN UIT STAP 4 IN =====
const int WIT   = 620;
const int ZWART = 180;
// ================================================

Adafruit_SSD1306 oled(128, 64, &Wire, -1);

const byte WACHT = 0, SPEEL = 1, KLAAR = 2;
byte stand = WACHT;

unsigned long startTijd = 0;
unsigned long eindTijd = 0;
int  fouten = 0;
bool vorigeOpLijn = true;
bool vorigeKnop = HIGH;

// Lijkt de meting meer op ZWART dan op WIT? Dan zit je op de lijn.
bool opDeLijn(int waarde) {
  return abs(waarde - ZWART) < abs(waarde - WIT);
}

// Geeft true op het moment dat de knop wordt INGEDRUKT
bool knopGedrukt() {
  bool nu = digitalRead(KNOP);
  bool gedrukt = (vorigeKnop == HIGH && nu == LOW);
  vorigeKnop = nu;
  if (gedrukt) delay(200);      // ontdubbelen
  return gedrukt;
}

void setup() {
  pinMode(KNOP, INPUT_PULLUP);
  pinMode(BUZZER, OUTPUT);
  oled.begin(SSD1306_SWITCHCAPVCC, 0x3C);   // adres 0x3C (soms 0x3D)
  oled.setTextColor(SSD1306_WHITE);
}

void loop() {
  // ---------- WACHTEN ----------
  if (stand == WACHT) {
    oled.clearDisplay();
    oled.setTextSize(1);
    oled.setCursor(0, 0);   oled.print("VASTE HAND");
    oled.setTextSize(2);
    oled.setCursor(0, 24);  oled.print("Druk op");
    oled.setCursor(0, 42);  oled.print("de knop");
    oled.display();

    if (knopGedrukt()) {
      fouten = 0;
      vorigeOpLijn = opDeLijn(analogRead(SENSOR));  // begin zoals je nu staat
      startTijd = millis();
      stand = SPEEL;
    }
    return;
  }

  // ---------- SPELEN ----------
  if (stand == SPEEL) {
    bool nuOpLijn = opDeLijn(analogRead(SENSOR));
    if (vorigeOpLijn && !nuOpLijn) {        // alleen de OVERGANG telt
      fouten++;
      tone(BUZZER, 880, 150);               // piep -- en meteen door
    }
    vorigeOpLijn = nuOpLijn;

    unsigned long tijd = millis() - startTijd;

    oled.clearDisplay();
    oled.setTextSize(1);
    oled.setCursor(0, 0);    oled.print("VASTE HAND");
    oled.setCursor(0, 16);   oled.print("tijd");
    oled.setCursor(74, 16);  oled.print("fout");
    oled.setTextSize(2);
    oled.setCursor(0, 26);   oled.print(tijd / 1000.0, 1);
    oled.setCursor(74, 26);  oled.print(fouten);
    oled.setTextSize(1);
    oled.setCursor(0, 50);   oled.print("Druk = finish");
    oled.display();

    if (knopGedrukt()) {
      eindTijd = tijd;
      stand = KLAAR;
    }
    return;
  }

  // ---------- KLAAR ----------
  oled.clearDisplay();
  oled.setTextSize(2);
  oled.setCursor(0, 0);    oled.print("KLAAR!");
  oled.setTextSize(1);
  oled.setCursor(0, 22);   oled.print("tijd");
  oled.setCursor(74, 22);  oled.print("fouten");
  oled.setTextSize(2);
  oled.setCursor(0, 32);   oled.print(eindTijd / 1000.0, 1);
  oled.setCursor(74, 32);  oled.print(fouten);
  oled.setTextSize(1);
  oled.setCursor(0, 54);   oled.print("Druk = opnieuw");
  oled.display();

  if (knopGedrukt()) {
    stand = WACHT;
  }
}
Werkt het niet zoals verwacht?

Hij telt fouten terwijl je netjes op de lijn zit — je WIT en ZWART liggen te dicht bij elkaar, of het sledeje kantelt. Meet opnieuw (stap 4).

Hij telt nooit een fout — kijk of de A0 van de sensor echt in A2 zit, en of je de getallen niet hebt omgedraaid.

Eén druk telt als twee — verhoog de delay(200) in knopGedrukt() naar 300.

8

Zelf uitbreiden

Van makkelijk naar echt uitdagend:

  • Straftijd in plaats van een aparte teller: elke fout telt als 3 seconden erbij. Eén score, makkelijker vergelijken.
  • Onthoud de beste tijd zolang de Arduino aanstaat en zet die in een hoekje.
  • Maak parcoursen voor elkaar en houd een scorebord bij op papier.
  • Teken een dunnere lijn voor niveau 2. Vergeet niet opnieuw te kalibreren — bij een dunne lijn ziet de sensor vaker een mengsel van zwart en wit.
  • Gevorderd, en het leukst: laat de toon stijgen naarmate je dichter bij de rand komt. Het analoge getal weet precies hoe ver je van het midden zit — koud en warm, maar dan met geluid. Kijk hoe ver de meting van ZWART af ligt en gebruik map() om daar een toonhoogte van te maken.

Wat nu?

Je hebt er een zintuig én een stem bij

Tot dit project kon je Arduino alleen kijken en tonen. Nu kan hij ook iets terugdoen. En met millis() heb je het gereedschap om dingen te laten gebeuren terwijl er iets anders loopt — dat is de bouwsteen onder elk spel dat je hierna nog verzint.

← Project 05  ·  Alle onderdelen  ·  Alle projecten