В прошлом эксперименте мы нарисовали интерфейс секундомера. Вывели симпатичную картинку и нули вместо часов, минут и секунд. Теперь нужно «оживить» этот прототип, заставить его считать время.
Первый вопрос, который возникает — как отсчитывать время? Как вариант, знакомая нам функция time.sleep(1), которая дает задержку на 1 секунду. Алгоритм работы получается простой. Завели переменную с секундами, которая в начале равна нулю. Отображаем ее значение на дисплее и делаем паузу на секунду. Потом увеличиваем значение переменной на 1 и отображаем новое значение. И так дальше.
Приведенный выше алгоритм простой, но не правильный. Дело в том, что на отображение данных на дисплее уходит время помимо паузы на 1 секунду и такой секундомер не будет считать правильно. Для данной задачи есть более верное решение.
Системная библиотека предлагает функцию millis()
, которая возвращает количество миллисекунд с начала запуска микроконтроллера. Это значение обновляется аппаратно по прерыванию от таймера, который настроен точно и не зависит от действий программы. Поэтому лучше ориентироваться именно на это значение.
При запуске таймера мы будем запоминать текущее значение millis()
и следить за его изменением.
Рисунок 1. Монтажная схема эксперимента для дисплея с 8 выводами
Рисунок 2. Монтажная схема эксперимента для дисплея с 11 выводами
class Stopwatch { private: unsigned int hours = 0; // Значение часов unsigned int mins = 0; // Значение минут unsigned int secs = 0; // Значение секунд unsigned long start_time = 0; // Время запуска bool state = 0; // Состояние: 0 - секундомер не запущен, 1 - запущен unsigned int backcolor = tft.color565(255, 251, 240); // Цвет фона unsigned int fontcolor = tft.color565(39, 40, 51); // Цвет шрифта public: // Метод инициализации вместо конструктора, которым мы не можем воспользоваться, // т.к. нам нужен уже инициализированный объект дисплея void init() { tft.fillScreen(backcolor); // Закрашиваем дисплей цветом фона reader.drawBMP("/time.bmp", tft, 0, 0); // Выводим изображение секундомера drawInit(); // Вызов функции отображения начального значения- нулей } // Метод вывода начального значения секундомера — нулей void drawInit() { drawValue(8, 125, 3, 0); // Выводим на дисплей 0 часов drawValue(55, 125, 3, 0); // Выводим на дисплей 0 минут drawValue(98, 132, 2, 0); // Выводим на дисплей 0 секунд tft.setTextColor(fontcolor); // Задаем цвет текста tft.setCursor(41, 125); // Задаем координаты вывода tft.setTextSize(3); // Задаем размер текста tft.print(":"); // Выводим разделитель часов и минут tft.setCursor(88, 132); // Задаем координаты вывода tft.setTextSize(2); // Задаем размер текста tft.print(":"); // Выводим разделитель минут и секунд } // Метод отображения на дисплее значений void drawValue(int x, int y, int size, int value) { // Перед выводом на дисплей новых данных закрашиваем старые прямоугольником с цветом фона tft.fillRect(x, y, 6 * size * 2, 8 * size -2, backcolor); tft.setCursor(x, y); // Задаем координаты вывода tft.setTextColor(fontcolor); // Задаем цвет текста tft.setTextSize(size); // Задаем размер текста // Если значение для вывода менее 10, то добавляем ведущий ноль, чтобы всегда было 2 знака if (value < 10) tft.print(0); tft.print(value); // Выводим значение } // Основной метод секундомера, отсчет времени void tick() { if (state) { // отсчет производим только, если секундомер запущен // Находим разницу в миллисекундах между текущим временем и временем запуска int hours_cur = diff / (1000 * 60 * 60); // Деление без остатка. Находим сколько целых часов прошло diff = diff % (1000 * 60 * 60); // Находим остаток после деления на часы и сохраняем в diff int mins_cur = diff / (1000 * 60); // Деление без остатка. Находим сколько целых минут прошло diff = diff % (1000 * 60); // Находим остаток после деления на минуты и сохраняем в diff int secs_cur = diff / (1000); // Деление без остатка. Находим сколько целых секунд прошло // Если сохраненное ранее значение часов отличается от вычисленных — обновляем и выводим на дисплей if (hours_cur != hours) { hours = hours_cur; drawValue(8, 125, 3, hours); } // Если сохраненное ранее значение минут отличается от вычисленных — обновляем и выводим на дисплей if (mins_cur != mins) { mins = mins_cur; drawValue(55, 125, 3, mins); } // Если сохраненное ранее значение секунд отличается от вычисленных — обновляем и выводим на дисплей if (secs_cur != secs) { secs = secs_cur; drawValue(98, 132, 2, secs); } } } // Метод запуска и остановки секундомера void startStop() { state = !state; // Инвертируем состояние секундомера } };
Класс содержит методы:
init
— используется вместо конструктора и вызывается в функции setup()
.drawInit
— отображает начальное значение таймера, нули, как в прошлом эксперименте.startStop
— запускает и останавливает секундомер. drawValue
— выводит значение часов минут или секунд. В качестве параметров принимает координаты, размер шрифта и значение.tick
— системная функция секундомера. Она определяет сколько часов, минут и секунд прошло с момента его запуска. Эту функцию необходимо вызывать постоянно.Код класса снабжен комментариями, подробно описывающими почти каждую строку. Кроме этого дадим несколько пояснений. Когда мы говорим «Сохраняем время запуска секундомера», то имеется в виду, что мы сохраняем значение количества миллисекунд с момента запуска микроконтроллера. И все упоминания времени следует читать в этом ключе.
Заострим внимание на вычислении количества часов:
int hours_cur = diff / (1000 * 60 * 60); diff = diff % (1000 * 60 * 60);
Оператор /
— это оператор деления без остатка. Например 5 / 2 = 2. А остаток 0.5 был отброшен.
А оператор %
— напротив возвращает только остаток от деления. 5 % 2 = 0.5
Сначала мы вычисляем целое количество часов, а потом мы вычисляем по сути сколько осталось после выделения целого количества часов. После этого там останется значение меньше одного часа. Его мы уже будем делить на минуты. А после вычитания минут будем определять сколько осталось секунд.
Теперь сделаем программу с использованием данного класса.
#include <SPI.h> #include <Adafruit_ST7735.h> #include "LittleFS_ImageReader.h" #define PIN_CS 2 #define PIN_DC 4 #define PIN_RST 5 Adafruit_ST7735 tft = Adafruit_ST7735(PIN_CS, PIN_DC, PIN_RST); LittleFS_ImageReader reader; class Stopwatch { private: unsigned int hours = 0; unsigned int mins = 0; unsigned int secs = 0; unsigned long start_time = 0; bool state = 0; unsigned int backcolor = tft.color565(255, 251, 240); unsigned int fontcolor = tft.color565(39, 40, 51); public: void init() { tft.fillScreen(backcolor); reader.drawBMP("/time.bmp", tft, 0, 0); drawInit(); } void drawInit() { drawValue(8, 125, 3, 0); drawValue(55, 125, 3, 0); drawValue(98, 132, 2, 0); tft.setTextColor(fontcolor); tft.setCursor(41, 125); tft.setTextSize(3); tft.print(":"); tft.setCursor(88, 132); tft.setTextSize(2); tft.print(":"); } void drawValue(int x, int y, int size, int value) { tft.fillRect(x, y, 6 * size * 2, 8 * size -2, backcolor); tft.setCursor(x, y); tft.setTextColor(fontcolor); tft.setTextSize(size); if (value < 10) tft.print(0); tft.print(value); } void tick() { if (state) { int hours_cur = diff / (1000 * 60 * 60); diff = diff % (1000 * 60 * 60); int mins_cur = diff / (1000 * 60); diff = diff % (1000 * 60); int secs_cur = diff / (1000); if (hours_cur != hours) { hours = hours_cur; drawValue(8, 125, 3, hours); } if (mins_cur != mins) { mins = mins_cur; drawValue(55, 125, 3, mins); } if (secs_cur != secs) { secs = secs_cur; drawValue(98, 132, 2, secs); } } } void startStop() { state = !state; } }; Stopwatch stopwatch; void setup() { os_update_cpu_frequency(160); LittleFS.begin(); tft.initR(INITR_BLACKTAB); tft.setRotation(2); stopwatch.init(); stopwatch.startStop(); } void loop() { stopwatch.tick(); }
Код эксперимента должен быть понятен. Сначала как обычно подключаем библиотеки и дисплей. Потом код класса секундомера и в конце мы создаем объект секундомера.
Stopwatch stopwatch;
Инициализируем и запускаем его.
stopwatch.init(); stopwatch.startStop();
И в бесконечном цикле вызываем его метод tick()
. В нем он занимается отслеживаем времени и отображением изменений на дисплее.
stopwatch.tick();
Теперь наш секундомер умеет считать время. Но еще не слушается управления. В следующем эксперименте мы добавим управление с помощью кнопки и получим полноценный секундомер.