Wat je nodig hebt
-
Je 1.44" TFT-scherm, aangesloten zoals in project 01.
-
Je 0.96" OLED-schermpje, aangesloten zoals in de OLED-start.
-
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. -
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.
millis(),
waarmee je een klok laat lopen terwijl je ondertussen gewoon doormeet. Dat lukt niet
met delay(), en dat is precies het punt.
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".
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.
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.
| Onderdeel | Pin | Naar je Arduino | Draad |
|---|---|---|---|
| Lijnvolger | VCC | 5V | rood |
| Lijnvolger | GND | GND | zwart |
| Lijnvolger | A0 | A2 | teal |
| Buzzer | pootje 1 | D3 | amber |
| Buzzer | pootje 2 | GND | zwart |
| Knop | één kant | D2 | groen |
| Knop | andere kant | GND | zwart |
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.
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.
Het sledeje
- Knip een stukje karton van ongeveer 5 bij 8 cm.
- Knip er een gaatje in waar het zwarte blokje van de sensor doorheen kan kijken.
- Plak of tape de sensor erop, zodat het blokje 3 tot 5 millimeter boven het papier hangt.
- Zorg dat de draadjes losjes zitten en niet aan het karton trekken.
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.
#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:
- Zet hem op een wit stuk van het vel. Noteer het getal. Dat is jouw
WIT. - Zet hem midden op de zwarte lijn. Noteer het getal. Dat is jouw
ZWART.
OBJECT zelf op LOW of HIGH kon zetten.
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.
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.
#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.
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.
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.
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.
delay() zet alles stil; millis() laat je gewoon doorwerken.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
}
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.
Het complete spel
Alles bij elkaar. Vul bovenaan jouw eigen getallen uit stap 4 in bij
WIT en ZWART, anders werkt het niet.
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.
#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();
}
}
clearDisplay(), alles opnieuw tekenen,
display(). Dat scheelt een hoop code.
#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;
}
}
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.
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
ZWARTaf ligt en gebruikmap()om daar een toonhoogte van te maken.
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.