Промышленное производство
Промышленный Интернет вещей | Промышленные материалы | Техническое обслуживание и ремонт оборудования | Промышленное программирование |
home  MfgRobots >> Промышленное производство >  >> Manufacturing Technology >> Производственный процесс

Сторонние точные часы на Arduino Nano и 16×2 LCD без RTC-модуля

Компоненты и материалы

О проекте

Начал как учебное упражнение, но получились очень точные часы. После 5 дней работы не потеряли и не набрали ни секунды.

Проблема использования только Arduino — внутренний генератор не идеально точен. Функция millis() накапливает ошибку, и часы либо отстают, либо спешат. Мой подход: измерить неточность конкретного Arduino (сколько миллисекунд теряет/набирает в час) и программно компенсировать её каждый час.

Также внутренний счетчик millis() переполняется каждые ~50 дней и его нельзя изменить. Решение — заменить прерывание millis() собственным счетчиком миллисекунд от полуночи, сбрасываемым ежедневно.

Оценка неточности

Предположил, что системные часы компьютера (и millis() в Processing) точны. Написал программу для Arduino, отправляющую прошедшие миллисекунды в Processing каждые 2 секунды. Скрипт на Processing сравнивал их со своими миллисекундами, показывая разницу в реальном времени и итоговую за час. Это дало значение коррекции для программы часов.

Код Arduino и скрипт Processing приведены ниже. Если Processing не установлен: https://processing.org

Код часов

Ключевые моменты: настройка прерывания, работа с временем, хранение даты.

Прерывание таймера

Настраиваем прерывание каждую миллисекунду. Это отключает стандартный millis() и delay().

// Настройка прерывания по таймеру 0
TCCR0A = (1 << WGM01);              // CTC mode
OCR0A = 0xF9;                       // 249 = 1 мс при 16 МГц / 64
TIMSK0 |= (1 << OCIE0A);            // Enable compare match interrupt
TCCR0B |= (1 << CS01) | (1 << CS00); // Prescaler 64
TCNT0 = 0;                          // Сброс счетчика
sei();                              // Глобальное разрешение прерываний

Обработчик прерывания (вызывается каждую мс):

ISR(TIMER0_COMPA_vect) {
  if (currentMode != SET_TIME) currentTime++;
  elapsed++;
}

Переменные currentTime и elapsed объявлены как volatile.

currentTime — миллисекунды от полуночи. Есть функции преобразования в ЧЧ:ММ:СС и сброса при настройке.

Каждые 24 часа вычитаем суточные миллисекунды и увеличиваем дату на 1 день. Переполнение переменной не страшно.

if ((currentMode == SHOW_TIME) && (currentTime > millisecondsInADay)) {
  noInterrupts();
  currentTime -= millisecondsInADay;
  interrupts();
  currentDate++;
}

Каждый час корректируем время на измеренную погрешность:

if (elapsed >= millisecondsInHour) {
  noInterrupts();
  currentTime += speedCorrection; // поправка на неточность генератора
  elapsed = 0;
  interrupts();
}

Хранение и расчет даты

Дата хранится как Юлианская дата (дни с 1 января 4713 г. до н.э.). Есть функции перевода в григорианский календарь и обратно.

float JulianDate(int iday, int imonth, int iyear) {
  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) {
  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;
}

Кнопки настройки

Кнопка Mode переключает режимы: Показ времени → Установка времени → Установка года → Установка даты → Коррекция скорости → Показ времени. Две другие кнопки меняют значения в текущем режиме.

Если часы идут неточно, в режиме «Коррекция скорости» можно изменить поправку шагами по 5 мс.

Исходный код

Программа часов (Arduino)

// Paul Brace - Feb 2021
// Simple Clock with Date using only Arduino - no RTC module
// Incorporates time correction adjustment for internal clock inaccuracy.
// Once correct speed adjustment set the clock is surprisingly accurate.
// In my test it did not lose or gain any time over a 5 day period.
// Displays time on a 16x2 LCD display
// Buttons to set time
// Mode button (pin 2) toggles set time, set date and run
// Button 1 (pin 3) Increments Minutes and Month and decreases Year/speed adj
// Button 2 (pin 4) Increments Hour and Day and increases Year/speed adj
// 24 Hour display

#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; // мс/час, которые мой Nano отстает (отрицательное если спешит)

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) | (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++;
}

float JulianDate(int iday, int imonth, int iyear) { /* ... */ }
void GregorianDate(float jd, int &iday, int &imonth, int &iyear) { /* ... */ }
void SplitTime(unsigned long curr, unsigned long &ulHour, unsigned long &ulMin, unsigned long &ulSec) { /* ... */ }
unsigned long SetTime(unsigned long ulHour, unsigned long ulMin, unsigned long ulSec) { /* ... */ }

void Wait(unsigned long value) {
  unsigned long startTime = elapsed;
  while ((elapsed - startTime) < value) {}
}

void CheckButtons() { /* ... */ }
String FormatNumber(int value) { /* ... */ }
void ShowTime(unsigned long value) { /* ... */ }
void ShowDate(float value) { /* ... */ }
void ShowDDMMSet(float value) { /* ... */ }
void ShowYearSet(float jd) { /* ... */ }
void ShowTimeSet(unsigned long value) { /* ... */ }
void ShowSpeedSet() { /* ... */ }

Arduino-скетч для измерения погрешности

// Paul Brace Feb 2021
// For use with corresponding Processing script
// to compare millis() from here to millis() in Processing
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 для тестирования

// Paul Brace Feb 2021
// Script to accept millis() from Arduino and compare to internal millis()
// Assumes computer clock is accurate
// -ve = Arduino slow -> enter as +ve adjustment in clock
// +ve = Arduino fast -> enter as -ve adjustment
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) {
      if (bytesCount < 14) {
        serialBytesArray[bytesCount] = inByte;
        bytesCount++;
      }
    }
    if (inByte == 69) {
      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;
    }
  }
}

Схемы

Сторонние точные часы на Arduino Nano и 16×2 LCD без RTC-модуля
Сторонние точные часы на Arduino Nano и 16×2 LCD без RTC-модуля

Производственный процесс

  1. Arduino POV-часы: сборка дисплея на эффекте сохранения образа
  2. Создание DTMF-декодера на Arduino без внешних микросхем
  3. Создание потрясающей системы Ambilight для монитора на Arduino
  4. Стильные настенные часы на четырех кольцах Adafruit Neopixel 60 LED и Arduino
  5. Слово-часы (Word Clock) на Arduino — DIY-часы с текстовой индикацией времени
  6. Часы реального времени на Arduino с расчётом времени намаза
  7. Создание пользовательских LED-часов с будильником — видно с 12 метров
  8. Умный вольтметр на Arduino и смартфоне — простое DIY-проект
  9. IoT-монитор пульса в реальном времени на Arduino и MAX30100
  10. Веб-сервер на Arduino Uno WiFi: управление LED через браузер