В этом проекте мы создадим мини‑часы на OLED SSD1306 (128×64, I²C) без модуля реального времени (RTC).
Научимся:
💡 В реальных часах лучше применять RTC (DS3231/DS1307). Здесь мы осваиваем отрисовку и механику времени, поэтому используем «программные» секунды.
getTextBounds).0x3C)
Установка: Скетч → Подключить библиотеку → Управлять библиотеками… и найдите по названиям выше.
getTextBounds() даёт реальные габариты текста для шрифта — по ним легко центрировать строки.: вовсе, а нарисовать две залитые точки через fillCircle.FreeMono9pt7b), чтобы ширина не прыгала при смене цифр.#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// Доп. шрифты Adafruit GFX (изменяем размер и «характер» надписей)
#include <Fonts/FreeMono9pt7b.h>
#include <Fonts/FreeMonoBold18pt7b.h>
#include <Fonts/FreeSans9pt7b.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// ===== Структура «дата/время» =====
struct DateTime {
uint16_t year;
uint8_t month;
uint8_t day;
uint8_t hour;
uint8_t minute;
uint8_t second;
uint8_t dayOfWeek; // 0..6
};
// ===== Начальные дата и время (можно заменить под себя) =====
DateTime startTime = {
2025, 10, 18, 15, 1, 4, 6 // Сб 18 Окт 2025, 15:01:04; 0=SUN … 6=SAT
};
// Служебные переменные для «тикания» каждую секунду
unsigned long lastMillis = 0;
DateTime currentTime;
// Календарные таблицы и короткие названия
const uint8_t monthDays[] = {31,28,31,30,31,30,31,31,30,31,30,31};
const char* dayNames[] = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"};
const char* monthNames[] = {"JAN", "FEB", "MAR", "APR", "MAY", "JUN",
"JUL", "AUG", "SEP", "OCT", "NOV", "DEC"};
void setup() {
Serial.begin(115200);
// Инициализируем дисплей по I²C, адрес чаще всего 0x3C
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;); // стоп, если дисплей не найден
}
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
display.setTextWrap(false); // отключаем перенос строк
currentTime = startTime; // копируем стартовую дату
lastMillis = millis();
Serial.println(F("OLED Clock - Fixed Version"));
}
void loop() {
unsigned long currentMillis = millis();
// Обновляем время ровно раз в секунду
if (currentMillis - lastMillis >= 1000) {
lastMillis = currentMillis;
incrementTime(); // +1 сек с учётом минут/часов/дней/месяцев/года
updateDisplay(); // перерисовка экрана
}
}
// ===== Календарная математика =====
bool isLeapYear(uint16_t year) { // рассчет високосного года по стандартной формуле григорианской системы
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
uint8_t getDaysInMonth(uint16_t year, uint8_t month) {
if (month == 2 && isLeapYear(year)) return 29;
return monthDays[month - 1];
}
// Прибавляем 1 секунду с корректной «перекаткой» полей
void incrementTime() {
currentTime.second++;
if (currentTime.second >= 60) {
currentTime.second = 0;
currentTime.minute++;
if (currentTime.minute >= 60) {
currentTime.minute = 0;
currentTime.hour++;
if (currentTime.hour >= 24) {
currentTime.hour = 0;
currentTime.day++;
currentTime.dayOfWeek = (currentTime.dayOfWeek + 1) % 7;
uint8_t daysInMonth = getDaysInMonth(currentTime.year, currentTime.month);
if (currentTime.day > daysInMonth) {
currentTime.day = 1;
currentTime.month++;
if (currentTime.month > 12) {
currentTime.month = 1;
currentTime.year++;
}
}
}
}
}
}
// Главная функция отрисовки всех элементов экрана
void updateDisplay() {
display.clearDisplay();
// === 1) Вертикальный день недели слева (переворот только на время печати) ===
{
uint8_t oldRot = display.getRotation(); // сохранить текущую ориентацию
display.setRotation(3); // 90° против часовой для «вертикали»
display.setFont(); // компактный встроенный шрифт
display.setTextSize(1);
display.setTextWrap(false);
const char* week = dayNames[currentTime.dayOfWeek]; // "SUN","MON",...
// Положение (можно подстроить под себя)
const int WEEK_X = 21; // от левого края (в повернутой системе)
const int WEEK_Y_OFFSET = -55; // тонкая подстройка по вертикали
// Считаем габариты, чтобы центрировать по высоте
int16_t wx1, wy1; uint16_t ww, wh;
display.getTextBounds(week, 0, 0, &wx1, &wy1, &ww, &wh);
// При rotation=3 width()=64, height()=128 (оси меняются местами)
int rx = WEEK_X - wx1;
int ry = (display.height() - (int)wh)/2 - wy1 + WEEK_Y_OFFSET;
display.setCursor(rx, ry);
display.print(week);
display.setRotation(oldRot); // вернуть ориентацию
}
// === 2) Верхняя строка: компактная дата без дня недели (крупнее) ===
display.setFont(&FreeSans9pt7b); // строгий, хорошо читается
display.setTextSize(1);
display.setTextWrap(false);
char dateStr[16];
snprintf(dateStr, sizeof(dateStr), "%02d %s %04d",
currentTime.day,
monthNames[currentTime.month - 1],
currentTime.year);
int16_t dx1, dy1; uint16_t dw, dh;
display.getTextBounds(dateStr, 0, 0, &dx1, &dy1, &dw, &dh);
// Центровка по горизонтали
int dateX = (SCREEN_WIDTH - (int)dw) / 2 - dx1;
// Подтянуть чуть ниже верхнего края, чтобы «не липло»
int dateTop = 4; // диапазон 2..6
int dateY = dateTop - dy1;
display.setCursor(dateX, dateY);
display.print(dateStr);
// === 3) Время: HH и MM с узким «:», секунды справа в фикс‑боксе ===
const int timeBaseY = 45; // базовая линия для времени
// Секунды в фиксированной ширине по шаблону «88»
display.setFont(&FreeMono9pt7b);
const int rightPad = 2; // отступ от правого края
int16_t sfx1, sfy1; uint16_t sfw, sfh;
display.getTextBounds("88", 0, 0, &sfx1, &sfy1, &sfw, &sfh); // ширина «бокса»
int secRight = SCREEN_WIDTH - rightPad; // правая граница секунд
int secBoxLeft = secRight - (int)sfw; // левая граница фикс. бокса
// Формируем строку секунд
char secStr[3];
snprintf(secStr, sizeof(secStr), "%02d", currentTime.second);
int16_t sx1, sy1; uint16_t sw, sh;
display.getTextBounds(secStr, 0, 0, &sx1, &sy1, &sw, &sh);
int secX = secRight - (int)sw - sx1;
display.setCursor(secX, timeBaseY);
display.print(secStr);
// HH и MM — крупным моноширинным жирным, а «:» рисуем сами кружками
display.setFont(&FreeMonoBold18pt7b);
char hhStr[3], mmStr[3];
snprintf(hhStr, sizeof(hhStr), "%02d", currentTime.hour);
snprintf(mmStr, sizeof(mmStr), "%02d", currentTime.minute);
int16_t hx1, hy1, mx1, my1; uint16_t hw, hh, mw, mh;
display.getTextBounds(hhStr, 0, 0, &hx1, &hy1, &hw, &hh);
display.getTextBounds(mmStr, 0, 0, &mx1, &my1, &mw, &mh);
const int gap = 6; // зазор между (MM) и боксом секунд
const int colonW = 1; // условная ширина двоеточия
const int colonGap = 6; // отступы вокруг двоеточия
// Ширина, доступная под [HH][gap][:][gap][MM] слева от бокса секунд
int availableWidth = secBoxLeft - gap;
int totalW = (int)hw + (int)mw + colonW + 2 * colonGap;
int baseX = (availableWidth - totalW) / 2 + 10; // базовый X (значение 10 сдвигает вправо блок часы : минуты на 10 px)
int hhX = baseX - hx1;
int colonX = baseX + hw + colonGap;
int mmX = colonX + colonW + colonGap - mx1;
// HH
display.setCursor(hhX, timeBaseY);
display.print(hhStr);
// Узкое «:» — две залитые точки (красиво и стабильно на всех шрифтах)
int topY = timeBaseY + hy1; // верхняя граница бокса HH
// Настройки положения точек двоеточия (можно менять под вкус)
uint8_t colonRadius = 2; // 1..3 обычно
uint8_t colonYOffset = 0; // общий вертикальный сдвиг точек
float colonTopK = 0.40f; // относительная позиция верхней точки
float colonBotK = 0.78f; // относительная позиция нижней точки
int cy1 = topY + (int)(hh * colonTopK) + colonYOffset;
int cy2 = topY + (int)(hh * colonBotK) + colonYOffset;
display.fillCircle(colonX, cy1, colonRadius, SSD1306_WHITE);
display.fillCircle(colonX, cy2, colonRadius, SSD1306_WHITE);
// MM
display.setCursor(mmX, timeBaseY);
display.print(mmStr);
// === 4) Прогресс‑бар секунд ===
drawImprovedProgressBar(currentTime.second);
// Показать всё, что нарисовали
display.display();
}
// Полная версия прогресс‑бара с делениями и цифрами снизу
void drawImprovedProgressBar(uint8_t seconds) {
const uint8_t barHeight = 8;
const uint8_t barY = SCREEN_HEIGHT - barHeight - 2;
const uint8_t barWidth = SCREEN_WIDTH - 4;
// Рамка
display.drawRect(2, barY, barWidth, barHeight, SSD1306_WHITE);
// Заполнение: 0..59 → ширина заливки
uint8_t progressWidth = map(seconds, 0, 59, 0, barWidth - 2);
if (progressWidth > 0) {
display.fillRect(3, barY + 1, progressWidth, barHeight - 2, SSD1306_WHITE);
}
// Вертикальные маркеры каждые 10 с (внутри полосы)
for (uint8_t i = 1; i < 6; i++) {
uint8_t markerX = 2 + (i * 10 * barWidth) / 60;
bool isFilled = (markerX <= 3 + progressWidth);
if (isFilled) {
// В заливке рисуем чёрную линию + белые пиксели‑«колпачки» для читаемости
display.drawFastVLine(markerX, barY + 1, barHeight - 3, SSD1306_BLACK);
display.drawPixel(markerX, barY + 1, SSD1306_WHITE);
display.drawPixel(markerX, barY + barHeight - 2, SSD1306_WHITE);
} else {
display.drawFastVLine(markerX, barY + 1, barHeight - 2, SSD1306_WHITE);
}
}
// Подписи 0,10,20,…,60 под полосой (компактный системный шрифт)
display.setFont();
display.setTextSize(1);
int16_t bx1, by1; uint16_t bw, bh;
for (uint8_t i = 0; i <= 6; i++) {
uint8_t labelX = 2 + (i * 10 * barWidth) / 60;
char label[4];
snprintf(label, sizeof(label), "%d", i * 10);
display.getTextBounds(label, 0, 0, &bx1, &by1, &bw, &bh);
display.setCursor(labelX - bw / 2, barY + barHeight + 2);
display.print(label);
}
}
// Альтернативный «компактный» прогресс‑бар (для экспериментов)
void drawSimpleProgressBar(uint8_t seconds) {
const uint8_t barHeight = 6;
const uint8_t barY = SCREEN_HEIGHT - barHeight - 2;
const uint8_t barWidth = SCREEN_WIDTH - 4;
display.drawRect(2, barY, barWidth, barHeight, SSD1306_WHITE);
uint8_t progressWidth = map(seconds, 0, 59, 0, barWidth - 2);
if (progressWidth > 0) {
display.fillRect(3, barY + 1, progressWidth, barHeight - 2, SSD1306_WHITE);
}
for (uint8_t i = 1; i < 6; i++) {
uint8_t markerX = 2 + (i * 10 * barWidth) / 60;
bool isFilled = (markerX <= 3 + progressWidth);
display.drawFastVLine(markerX, barY + 1, barHeight - 2,
isFilled ? SSD1306_BLACK : SSD1306_WHITE);
}
}
Если экран пустой: проверьте адрес I²C (в коде
0x3C), SDA/SCL, питание и контраст/подсветку (для некоторых модулей).
display.display() «слишком часто».WEEK_X, WEEK_Y_OFFSET, dateTop, timeBaseY, colonTopK/BotK.millis() без delay.FreeSans ↔ FreeMonoBold).