Эксперимент 50. Класс секундомера, логика работы

В прошлом эксперименте мы нарисовали интерфейс секундомера. Вывели симпатичную картинку и нули вместо часов, минут и секунд. Теперь нужно «оживить» этот прототип, заставить его считать время.

Первый вопрос, который возникает — как отсчитывать время? Как вариант, знакомая нам функция time.sleep(1), которая дает задержку на 1 секунду. Алгоритм работы получается простой. Завели переменную с секундами, которая в начале равна нулю. Отображаем ее значение на дисплее и делаем паузу на секунду. Потом увеличиваем значение переменной на 1 и отображаем новое значение. И так дальше.

Приведенный выше алгоритм простой, но не правильный. Дело в том, что на отображение данных на дисплее уходит время помимо паузы на 1 секунду и такой секундомер не будет считать правильно. Для данной задачи есть более верное решение.

Системная библиотека предлагает функцию millis(), которая возвращает количество миллисекунд с начала запуска микроконтроллера. Это значение обновляется аппаратно по прерыванию от таймера, который настроен точно и не зависит от действий программы. Поэтому лучше ориентироваться именно на это значение.

При запуске таймера мы будем запоминать текущее значение millis() и следить за его изменением.

Схема эксперимента

Рисунок 1. Монтажная схема эксперимента для дисплея с 8 выводами

Рисунок 2. Монтажная схема эксперимента для дисплея с 11 выводами

Класс Stopwatch

  1. class Stopwatch {
  2. private:
  3. unsigned int hours = 0; // Значение часов
  4. unsigned int mins = 0; // Значение минут
  5. unsigned int secs = 0; // Значение секунд
  6. unsigned long start_time = 0; // Время запуска
  7. bool state = 0; // Состояние: 0 - секундомер не запущен, 1 - запущен
  8. unsigned int backcolor = tft.color565(255, 251, 240); // Цвет фона
  9. unsigned int fontcolor = tft.color565(39, 40, 51); // Цвет шрифта
  10.  
  11. public:
  12. // Метод инициализации вместо конструктора, которым мы не можем воспользоваться,
  13. // т.к. нам нужен уже инициализированный объект дисплея
  14. void init() {
  15. tft.fillScreen(backcolor); // Закрашиваем дисплей цветом фона
  16. reader.drawBMP("/time.bmp", tft, 0, 0); // Выводим изображение секундомера
  17. drawInit(); // Вызов функции отображения начального значения- нулей
  18. }
  19.  
  20. // Метод вывода начального значения секундомера — нулей
  21. void drawInit() {
  22. drawValue(8, 125, 3, 0); // Выводим на дисплей 0 часов
  23. drawValue(55, 125, 3, 0); // Выводим на дисплей 0 минут
  24. drawValue(98, 132, 2, 0); // Выводим на дисплей 0 секунд
  25.  
  26. tft.setTextColor(fontcolor); // Задаем цвет текста
  27.  
  28. tft.setCursor(41, 125); // Задаем координаты вывода
  29. tft.setTextSize(3); // Задаем размер текста
  30. tft.print(":"); // Выводим разделитель часов и минут
  31.  
  32. tft.setCursor(88, 132); // Задаем координаты вывода
  33. tft.setTextSize(2); // Задаем размер текста
  34. tft.print(":"); // Выводим разделитель минут и секунд
  35. }
  36.  
  37. // Метод отображения на дисплее значений
  38. void drawValue(int x, int y, int size, int value) {
  39. // Перед выводом на дисплей новых данных закрашиваем старые прямоугольником с цветом фона
  40. tft.fillRect(x, y, 6 * size * 2, 8 * size -2, backcolor);
  41.  
  42. tft.setCursor(x, y); // Задаем координаты вывода
  43. tft.setTextColor(fontcolor); // Задаем цвет текста
  44. tft.setTextSize(size); // Задаем размер текста
  45.  
  46. // Если значение для вывода менее 10, то добавляем ведущий ноль, чтобы всегда было 2 знака
  47. if (value < 10) tft.print(0);
  48. tft.print(value); // Выводим значение
  49. }
  50.  
  51. // Основной метод секундомера, отсчет времени
  52. void tick() {
  53. if (state) { // отсчет производим только, если секундомер запущен
  54. // Находим разницу в миллисекундах между текущим временем и временем запуска
  55. unsigned long diff = millis() - start_time;
  56. int hours_cur = diff / (1000 * 60 * 60); // Деление без остатка. Находим сколько целых часов прошло
  57. diff = diff % (1000 * 60 * 60); // Находим остаток после деления на часы и сохраняем в diff
  58. int mins_cur = diff / (1000 * 60); // Деление без остатка. Находим сколько целых минут прошло
  59. diff = diff % (1000 * 60); // Находим остаток после деления на минуты и сохраняем в diff
  60. int secs_cur = diff / (1000); // Деление без остатка. Находим сколько целых секунд прошло
  61.  
  62. // Если сохраненное ранее значение часов отличается от вычисленных — обновляем и выводим на дисплей
  63. if (hours_cur != hours) {
  64. hours = hours_cur;
  65. drawValue(8, 125, 3, hours);
  66. }
  67.  
  68. // Если сохраненное ранее значение минут отличается от вычисленных — обновляем и выводим на дисплей
  69. if (mins_cur != mins) {
  70. mins = mins_cur;
  71. drawValue(55, 125, 3, mins);
  72. }
  73.  
  74. // Если сохраненное ранее значение секунд отличается от вычисленных — обновляем и выводим на дисплей
  75. if (secs_cur != secs) {
  76. secs = secs_cur;
  77. drawValue(98, 132, 2, secs);
  78. }
  79. }
  80. }
  81.  
  82. // Метод запуска и остановки секундомера
  83. void startStop() {
  84. state = !state; // Инвертируем состояние секундомера
  85. if (state) start_time = millis(); // Если запущен сохраняем время запуска секундомера
  86. }
  87. };

Класс содержит методы:

  • 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

Сначала мы вычисляем целое количество часов, а потом мы вычисляем по сути сколько осталось после выделения целого количества часов. После этого там останется значение меньше одного часа. Его мы уже будем делить на минуты. А после вычитания минут будем определять сколько осталось секунд.

Теперь сделаем программу с использованием данного класса.

Программный код эксперимента

Exp50.ino
  1. #include <SPI.h>
  2. #include <Adafruit_ST7735.h>
  3. #include "LittleFS_ImageReader.h"
  4.  
  5. #define PIN_CS 2
  6. #define PIN_DC 4
  7. #define PIN_RST 5
  8.  
  9. Adafruit_ST7735 tft = Adafruit_ST7735(PIN_CS, PIN_DC, PIN_RST);
  10. LittleFS_ImageReader reader;
  11.  
  12. class Stopwatch {
  13. private:
  14. unsigned int hours = 0;
  15. unsigned int mins = 0;
  16. unsigned int secs = 0;
  17. unsigned long start_time = 0;
  18. bool state = 0;
  19. unsigned int backcolor = tft.color565(255, 251, 240);
  20. unsigned int fontcolor = tft.color565(39, 40, 51);
  21.  
  22. public:
  23. void init() {
  24. tft.fillScreen(backcolor);
  25. reader.drawBMP("/time.bmp", tft, 0, 0);
  26. drawInit();
  27. }
  28.  
  29. void drawInit() {
  30. drawValue(8, 125, 3, 0);
  31. drawValue(55, 125, 3, 0);
  32. drawValue(98, 132, 2, 0);
  33.  
  34. tft.setTextColor(fontcolor);
  35.  
  36. tft.setCursor(41, 125);
  37. tft.setTextSize(3);
  38. tft.print(":");
  39.  
  40. tft.setCursor(88, 132);
  41. tft.setTextSize(2);
  42. tft.print(":");
  43. }
  44.  
  45. void drawValue(int x, int y, int size, int value) {
  46. tft.fillRect(x, y, 6 * size * 2, 8 * size -2, backcolor);
  47.  
  48. tft.setCursor(x, y);
  49. tft.setTextColor(fontcolor);
  50. tft.setTextSize(size);
  51.  
  52. if (value < 10) tft.print(0);
  53. tft.print(value);
  54. }
  55.  
  56. void tick() {
  57. if (state) {
  58. unsigned long diff = millis() - start_time;
  59. int hours_cur = diff / (1000 * 60 * 60);
  60. diff = diff % (1000 * 60 * 60);
  61. int mins_cur = diff / (1000 * 60);
  62. diff = diff % (1000 * 60);
  63. int secs_cur = diff / (1000);
  64.  
  65. if (hours_cur != hours) {
  66. hours = hours_cur;
  67. drawValue(8, 125, 3, hours);
  68. }
  69.  
  70. if (mins_cur != mins) {
  71. mins = mins_cur;
  72. drawValue(55, 125, 3, mins);
  73. }
  74.  
  75. if (secs_cur != secs) {
  76. secs = secs_cur;
  77. drawValue(98, 132, 2, secs);
  78. }
  79. }
  80. }
  81.  
  82. void startStop() {
  83. state = !state;
  84. if (state) start_time = millis();
  85. }
  86. };
  87.  
  88. Stopwatch stopwatch;
  89.  
  90. void setup() {
  91. os_update_cpu_frequency(160);
  92. LittleFS.begin();
  93. tft.initR(INITR_BLACKTAB);
  94. tft.setRotation(2);
  95.  
  96. stopwatch.init();
  97. stopwatch.startStop();
  98. }
  99.  
  100. void loop() {
  101. stopwatch.tick();
  102. }

Код эксперимента должен быть понятен. Сначала как обычно подключаем библиотеки и дисплей. Потом код класса секундомера и в конце мы создаем объект секундомера.

  1. Stopwatch stopwatch;

Инициализируем и запускаем его.

  1. stopwatch.init();
  2. stopwatch.startStop();

И в бесконечном цикле вызываем его метод tick(). В нем он занимается отслеживаем времени и отображением изменений на дисплее.

  1. stopwatch.tick();

Теперь наш секундомер умеет считать время. Но еще не слушается управления. В следующем эксперименте мы добавим управление с помощью кнопки и получим полноценный секундомер.