Содержание

Эксперимент 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. };

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

Код класса снабжен комментариями, подробно описывающими почти каждую строку. Кроме этого дадим несколько пояснений. Когда мы говорим «Сохраняем время запуска секундомера», то имеется в виду, что мы сохраняем значение количества миллисекунд с момента запуска микроконтроллера. И все упоминания времени следует читать в этом ключе.

Заострим внимание на вычислении количества часов:

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

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