Bouw een nauwkeurige klok met Arduino Nano en 16x2 LCD
Onderdelen en benodigdheden
| Onderdeel | Aantal |
|---|---|
| Arduino Nano R3 (werkt met elke Arduino) | 1 |
| Alfanumeriek LCD, 16x2 | 1 |
| Tactiele schakelaars | 3 |
| Trim-potentiometer, 10 kohm | 1 |
| Jumper wires | 1 set |
Over dit project
Ik startte dit als een academische oefening, maar eindigde met een zeer nauwkeurige klok. Na 5 dagen draaien had hij geen tijd verloren of gewonnen.
Het hoofdprobleem met alleen een Arduino is dat de interne kloksnelheid niet 100% nauwkeurig is. Als je je alleen hierop baseert, zal de telling van verstreken milliseconden met een klein percentage afwijken en zal de klok ofwel achterlopen of voorlopen. Mijn aanpak was om de nauwkeurigheid van de gebruikte Arduino te testen en te bepalen hoeveel milliseconden hij per uur verloor of won. Alles wat nodig was, was een snelheidscorrectie te programmeren die dit verschil elk uur optelt of aftrekt van de intern bijgehouden milliseconden.
Mijn andere zorg was of de Arduino-klok consistent onnauwkeurig was, maar zoals aangegeven, heeft de geprogrammeerde klok zeer nauwkeurige tijd gehouden over 5 dagen, dus de onnauwkeurigheid lijkt consistent.
Het tweede probleem is dat de interne millis()-functie zichzelf elke 50 dagen reset en je de milliseconde-telling niet kunt manipuleren. De oplossing was om de millis()-interrupt te vervangen door een teller die ik kon manipuleren en die de milliseconden vanaf middernacht telt, elke dag resettend, waardoor elke runtime-beperking verdwijnt.
De onnauwkeurigheid bepalen
Om de onnauwkeurigheid te bepalen, ging ik ervan uit dat mijn computertijd, en dus millis() in Processing, nauwkeurig was. Ik maakte dus een programma voor de Arduino om het aantal verstreken milliseconden sinds de handshake elke 2 seconden naar Processing te sturen, en een script voor Processing om dit te lezen en te vergelijken met zijn eigen verstreken milliseconden, met een realtime resultaat en het verschil na een uur. Dit gaf het aantal milliseconden dat in een uur verloren of gewonnen was, en dus de waarde voor de snelheidscorrectie in het klokprogramma.
De code voor het Arduino-programma en het Processing-script zijn hieronder te vinden.
Als je Processing niet geïnstalleerd hebt, bezoek https://processing.org om het te downloaden en erover te leren.
De klokcode
De belangrijkste aandachtsgebieden in de klokcode zijn het instellen van de interrupt, hoe deze wordt gebruikt, en de manier waarop de datum wordt opgeslagen en gemanipuleerd.
De interrupt
De volgende code stelt een interrupt in die elke milliseconde triggert. Dit omleidt de interrupt die millis() onderhoudt, zodat millis() en delay() niet meer werken.
// Stel tijd-interrupt in - millis() loopt over na 50 dagen dus
// we gebruiken onze eigen milliseconde-teller die we aan het einde van
// elke dag kunnen resetten
// Zet CTC-modus: Compare time en trigger interrupt
TCCR0A = (1 << WGM01);
// Stel waarde voor vergelijkingstijd: OCR0A voor 1ms = 249 (8 bits, max 256)
// [(Kloksnelheid/Prescaler)*Tijd in seconden] - 1
// [(16.000.000/64) * 0.001] - 1 = 249 = 1 milliseconde
OCR0A = 0xF9;
// Zet timer compare interrupt
TIMSK0 |= (1 << OCIE0A);
// Zet prescaler 1/64 klok
// d.w.z. 110 voor laatste 3 bits
TCCR0B |= (1 << CS01);
TCCR0B |= (1 << CS00);
// Initialiseer tellerwaarde op 0
TCNT0 = 0;
// Schakel interrupt in
sei();
Dit is de code die elke seconde wordt aangeroepen:
// Deze interrupt wordt aangeroepen wanneer de vergelijkingstijd bereikt is
// dus een keer per milliseconde gebaseerd op OCR0A registerinstelling.
ISR(TIMER0_COMPA_vect) {
if (currentMode != SET_TIME) currentTime++;
elapsed++;
}
currentTime en elapsed zijn unsigned long variabelen. Let op dat deze gekwalificeerd zijn als volatile bij de definitie, omdat we de variabelen ook in de hoofdcode manipuleren. Dit dwingt het systeem om de variabele elke keer te lezen wanneer deze gebruikt wordt en geen gecachte waarde te gebruiken.
currentTime slaat het aantal milliseconden sinds middernacht op en er zijn routines om dit om te zetten naar HH:MM:SS en het te resetten wanneer je de tijd instelt.
Wanneer 24 uur verstreken zijn, trekt het systeem het aantal milliseconden in een dag van de tijd af en verhoogt de datum met 1 dag. De klok wordt dus niet beïnvloed door de maximumwaarde die de variabele kan opslaan, in tegenstelling tot millis().
// Als aan het einde van de dag, reset tijd en verhoog datum
if ((currentMode == SHOW_TIME) && (currentTime > millisecondsInADay)) {
// Volgende dag
// Stop interrupts tijdens reset tijd
noInterrupts();
currentTime -= millisecondsInADay;
interrupts();
currentDate++;
}
Let op dat we interrupts uitschakelen tijdens het manipuleren van currentTime, anders zou de interrupt-call in het midden van de berekening getriggerd kunnen worden en de berekening corrupt maken.
Na elk verstreken uur past het systeem het aantal verstreken milliseconden aan met de eerder berekende snelheidscorrectie, zodat de huidige tijd gecorrigeerd wordt voor de te snelle of te langzame interne klok.
// Aan het einde van elk uur pas de verstreken tijd aan voor
// de onnauwkeurigheid in de Arduino-klok
if (elapsed >= millisecondsInHour) {
noInterrupts();
// Tijd aanpassen voor trage/snelle Arduino-klok
currentTime += speedCorrection;
// Reset voor het tellen van het volgende uur
elapsed = 0;
interrupts();
}
Datumopslag en berekening
De datum wordt opgeslagen als een Juliaanse datum, het aantal dagen dat verstreken is sinds maandag 1 januari 4713 v.Chr. Routines zijn opgenomen om de Juliaanse datum te berekenen en terug te converteren naar de Gregoriaanse kalender.
float JulianDate(int iday, int imonth, int iyear) {
// Bereken Juliaanse datum (getest tot jaar 20.000)
unsigned long d = iday, m = imonth, y = iyear;
if (m < 3) { m += 12; y -= 1; }
unsigned long t1 = (153 * m - 457) / 5;
unsigned long t2 = 365 * y + (y / 4) - (y / 100) + (y / 400);
return 1721118.5 + d + t1 + t2;
}
void GregorianDate(float jd, int &iday, int &imonth, int &iyear) {
// Let op: 2100 is het volgende overgeslagen schrikkeljaar - compenseert voor overgeslagen schrikkeljaren
unsigned long f = jd + 68569.5;
unsigned long e = (4.0 * f) / 146097;
unsigned long g = f - (146097 * e + 3) / 4;
unsigned long h = 4000ul * (g + 1) / 1461001;
unsigned long t = g - (1461 * h / 4) + 31;
unsigned long u = (80ul * t) / 2447;
unsigned long v = u / 11;
iyear = 100 * (e - 49) + h + v;
imonth = u + 2 - 12 * v;
iday = t - 2447 * u / 80;
}
De instelknoppen
De Mode-knop schakelt de huidige modus door van Tijd Tonen, naar Tijd Instellen, Jaar Instellen, Datum Instellen, Snelheidscorrectie Instellen en terug naar Tijd Tonen. Elke modus is zelfverklarend en gebruikt de andere 2 knoppen om de huidige instelling aan te passen.
Eenmaal de klok draait, als hij aan of achterloopt, kun je de snelheidscorrectie wijzigen door de Snelheidscorrectie Instellen-modus te openen en de omhoog/omlaag knoppen te gebruiken om deze met 5 seconden per keer te verhogen of verlagen.
Code
Klokprogramma
// Paul Brace - Feb 2021
// Simpele klok met datum met alleen een Arduino - geen RTC-module
// Programma bevat een tijdcorrectie om de interne kloksnelheid te compenseren.
// Eenmaal de juiste snelheidscorrectie ingesteld, is de klok verrassend nauwkeurig.
// In mijn test verloor of won hij geen tijd over een periode van 5 dagen.
// Toont tijd op een 16x2 LCD-display
// Knoppen om tijd in te stellen
// Mode-knop (pin 2) schakelt tussen tijd instellen, datum instellen en draaien
// Knop 1 (pin 3) verhoogt minuten en maand en verlaagt jaar/snelheidscorrectie
// Knop 2 (pin 4) verhoogt uren en dag en verhoogt jaar/snelheidscorrectie
// 24-uurs weergave
#include
LiquidCrystal lcd(12, 13, 6, 7, 8, 9);
#define MODE_BUTTON 2
#define HOUR_BUTTON 3
#define UP_BUTTON 3
#define DAY_BUTTON 3
#define MINUTE_BUTTON 4
#define DOWN_BUTTON 4
#define MONTH_BUTTON 4
#define SHOW_TIME 1
#define SET_TIME 2
#define SET_YEAR 3
#define SET_DATE 4
#define SET_SPEED_ADJ 5
int speedCorrection = 3545; // Milliseconden dat mijn Nano per uur te langzaam loopt
// Negatief getal als het te snel loopt
// Aanpassen aan jouw Arduino
volatile unsigned long currentTime;
unsigned long lastTime = -1000;
volatile unsigned long elapsed;
unsigned long millisecondsInADay;
unsigned long millisecondsInHour;
int currentMode;
float currentDate;
float lastDate = 0.0;
int currentDay, currentMonth, currentYear;
char *dayArray[] = { "Tue. ", "Wed. ", "Thur. ", "Fri. ", "Sat. ", "Sun. ", "Mon. " };
void setup() {
TCCR0A = (1 << WGM01);
OCR0A = 0xF9;
TIMSK0 |= (1 << OCIE0A);
TCCR0B |= (1 << CS01);
TCCR0B |= (1 << CS00);
TCNT0 = 0;
sei();
pinMode(MINUTE_BUTTON, INPUT_PULLUP);
pinMode(HOUR_BUTTON, INPUT_PULLUP);
pinMode(MODE_BUTTON, INPUT_PULLUP);
currentTime = 0;
currentDate = JulianDate(1, 1, 2021);
elapsed = 0;
millisecondsInADay = 24ul * 60 * 60 * 1000;
millisecondsInHour = 60ul * 60 * 1000;
currentMode = SHOW_TIME;
lcd.begin(16, 2);
lcd.noAutoscroll();
lcd.display();
lcd.clear();
ShowTime(currentTime);
}
void loop() {
if ((currentMode == SHOW_TIME) && (currentTime > millisecondsInADay)) {
noInterrupts();
currentTime -= millisecondsInADay;
interrupts();
currentDate++;
}
if (elapsed >= millisecondsInHour) {
noInterrupts();
currentTime += speedCorrection;
elapsed = 0;
interrupts();
}
CheckButtons();
switch (currentMode) {
case SHOW_TIME: ShowTime(currentTime); ShowDate(currentDate); break;
case SET_TIME: ShowTimeSet(currentTime); break;
case SET_YEAR: ShowYearSet(currentDate); break;
case SET_DATE: ShowDDMMSet(currentDate); break;
case SET_SPEED_ADJ: ShowSpeedSet(); break;
}
Wait(150);
}
ISR(TIMER0_COMPA_vect) {
if (currentMode != SET_TIME) currentTime++;
elapsed++;
}
// JulianDate, GregorianDate, SplitTime, SetTime, Wait, CheckButtons,
// FormatNumber, ShowTime, ShowDate, ShowDDMMSet, ShowYearSet, ShowTimeSet, ShowSpeedSet
// ... (zie volledige code in origineel)
Arduino timerprogramma
// Paul Brace Feb 2021
// Voor gebruik met bijbehorend Processing-script
// Om millis() hier te vergelijken met millis() in Processing met de computertijd
int inByte = 0;
unsigned long firstReading = 100000;
void setup() {
Serial.begin(9600);
sayHello();
}
void loop() {
if (Serial.available() > 0) {
inByte = Serial.read();
Serial.print(millis() - firstReading);
Serial.print('E');
delay(2000);
}
}
void sayHello() {
while (Serial.available() <= 0) {
Serial.print('Z');
delay(200);
}
firstReading = millis();
}
Processing timer testscript
// Paul Brace Feb 2021
// Script om millis() van Arduino te accepteren en te vergelijken met interne millis()
// om de onnauwkeurigheid van de Arduino-klok te beoordelen.
// Veronderstelt dat de computertijd nauwkeurig is.
// -ve = Arduino loopt traag -> invoeren als +ve correctie in klokprogramma
// +ve = Arduino loopt snel -> invoeren als -ve correctie om klok te vertragen
import processing.serial.*;
Serial theSerialPort;
int[] serialBytesArray = new int[15];
int bytesCount = 0;
boolean init = false;
int fillColor = 255;
long mills = 0, first = 0, now, firstReading = 100000, DiffPerHour = 0;
int inByte;
void setup() {
size(500, 500);
background(70);
noStroke();
printArray(Serial.list());
String thePortName = Serial.list()[1];
theSerialPort = new Serial(this, thePortName, 9600);
}
void draw() {
background(70);
fill(fillColor);
textSize(25);
text(hour() + ":" + minute() + ":" + second(), 50, 50);
text("Incoming elapsed: " + mills, 50, 100);
text("Local elapsed: " + (now - firstReading), 50, 150);
text("Diff: " + (mills - (now - firstReading)), 50, 200);
if (((now - firstReading) >= 3600000) && (DiffPerHour == 0)) {
DiffPerHour = mills - (now - firstReading);
}
text("Diff after 1 hour: " + DiffPerHour, 50, 300);
}
void serialEvent(Serial myPort) {
inByte = myPort.read();
if (!init) {
if (inByte == 'Z') {
myPort.clear();
init = true;
myPort.write('Z');
if (first == 0) first = millis();
}
} else {
if (inByte != 69) { // niet 'E'
if (bytesCount < 14) {
serialBytesArray[bytesCount] = inByte;
bytesCount++;
}
}
if (inByte == 69) { // 'E' = einde bericht
now = millis();
mills = 0;
for (int i = 1; i <= bytesCount; i++) {
mills += (serialBytesArray[i-1] - 48) * pow(10, (bytesCount - i));
}
if (firstReading == 100000) firstReading = now;
myPort.write('Z');
bytesCount = 0;
}
}
}
Schema's
Productieproces
- Arduino POV-klok: Bouw een Persistence-of-Vision LED-display
- Een DTMF-decoder bouwen met alleen een Arduino – Geen extra IC's nodig
- Bouw een indrukwekkend monitor Ambilight-systeem met Arduino
- Een stijlvolle wandklok bouwen met Adafruit 60-LED Neopixel Ring & Arduino
- DIY Arduino Woordklok – Bouw een Elegant Real-Time Display
- Bouw een Arduino Real-Time Clock voor Islamitische Gebedstijden
- Bouw een Aangepaste LED Hoofdklok met Alarm – Zichtbaar vanaf 12 Meter
- Een slimme voltmeter bouwen met Arduino en smartphone – Eenvoudig DIY-project
- Real-time IoT hartslagmonitor met Arduino en MAX30100
- Arduino Uno WiFi Webserver: LED aan/uit schakelen via browser
- Bouw een Precieze 7-Segment Klok met Arduino Pro Mini & DS1302 RTC