// Copyright (c) 2026 ЦПМК по информатике, Юрий Дементьев
// Licensed under the MIT License.
// https://robot.mipt.ru/
// Пример работы с I2C OLED дисплеем 128*64 пикселя на базе контроллера SSD1306
// подключение:
// VCC - 3.3В или 5В
// GND - GND (0 В)
// SCL - SCL (A5 Arduino UNO)
// SDA - SDA (A4 Arduino UNO)
// в дисплее есть встроенная подтяжка для I2C
#include <Wire.h> // Стандартная библиотека работы с I2C
#define OLED_ADDR 0x3C // Стандартный адрес I2C для SSD1306
// Упрощенный шрифт ASCII (символы 32-126)
// PROGMEM заставляет компилятор оставить данные во Flash-памяти и не копировать их в SRAM при старте программы.
// байты тут - столбцы при выводе символа на экран, символ можно увидеть повернув голову, например 0b01011111 превратится в !
const uint8_t font5x7[][5] PROGMEM = {
{ 0b00000000,
0b00000000,
0b00000000,
0b00000000,
0b00000000 }, // space
{ 0b00000000,
0b00000000,
0b01011111,
0b00000000,
0b00000000 }, // !
{ 0b00000000,
0b00000111,
0b00000000,
0b00000111,
0b00000000 }, // "
{ 0b00010100,
0b01111111,
0b00010100,
0b01111111,
0b00010100 }, // #
{ 0b00100100,
0b00101010,
0b01111111,
0b00101010,
0b00010010 }, // $
{ 0b00100011,
0b00010011,
0b00001000,
0b01100100,
0b01100010 }, // %
{ 0b00110110,
0b01001001,
0b01010101,
0b00100010,
0b01010000 }, // &
{ 0b00000000,
0b00000101,
0b00000011,
0b00000000,
0b00000000 }, // '
{ 0b00000000,
0b00011100,
0b00100010,
0b01000001,
0b00000000 }, // (
{ 0b00000000,
0b01000001,
0b00100010,
0b00011100,
0b00000000 }, // )
{ 0b00010100,
0b00001000,
0b00111110,
0b00001000,
0b00010100 }, // *
{ 0b00001000,
0b00001000,
0b00111110,
0b00001000,
0b00001000 }, // +
{ 0b00000000,
0b01010000,
0b00110000,
0b00000000,
0b00000000 }, // ,
{ 0b00001000,
0b00001000,
0b00001000,
0b00001000,
0b00001000 }, // -
{ 0b00000000,
0b01100000,
0b01100000,
0b00000000,
0b00000000 }, // .
{ 0b00100000,
0b00010000,
0b00001000,
0b00000100,
0b00000010 }, // /
{ 0b00111110,
0b01010001,
0b01001001,
0b01000101,
0b00111110 }, // 0
{ 0b00000000,
0b01000010,
0b01111111,
0b01000000,
0b00000000 }, // 1
{ 0b01000010,
0b01100001,
0b01010001,
0b01001001,
0b01000110 }, // 2
{ 0b00100001,
0b01000001,
0b01000101,
0b01001011,
0b00110001 }, // 3
{ 0b00011000,
0b00010100,
0b00010010,
0b01111111,
0b00010000 }, // 4
{ 0b00100111,
0b01000101,
0b01000101,
0b01000101,
0b00111001 }, // 5
{ 0b00111100,
0b01001010,
0b01001001,
0b01001001,
0b00110000 }, // 6
{ 0b00000001,
0b01110001,
0b00001001,
0b00000101,
0b00000011 }, // 7
{ 0b00110110,
0b01001001,
0b01001001,
0b01001001,
0b00110110 }, // 8
{ 0b00000110,
0b01001001,
0b01001001,
0b00101001,
0b00011110 }, // 9
{ 0b00000000,
0b00110110,
0b00110110,
0b00000000,
0b00000000 }, // :
{ 0b00000000,
0b01010110,
0b00110110,
0b00000000,
0b00000000 }, // ;
{ 0b00001000,
0b00010100,
0b00100010,
0b01000001,
0b00000000 }, // <
{ 0b00010100,
0b00010100,
0b00010100,
0b00010100,
0b00010100 }, // =
{ 0b00000000,
0b01000001,
0b00100010,
0b00010100,
0b00001000 }, // >
{ 0b00000010,
0b00000001,
0b01010001,
0b00001001,
0b00000110 }, // ?
{ 0b00110010,
0b01001001,
0b01111001,
0b01000001,
0b00111110 }, // @
{ 0b01111110,
0b00010001,
0b00010001,
0b00010001,
0b01111110 }, // A
{ 0b01111111,
0b01001001,
0b01001001,
0b01001001,
0b00110110 }, // B
{ 0b00111110,
0b01000001,
0b01000001,
0b01000001,
0b00100010 }, // C
{ 0b01111111,
0b01000001,
0b01000001,
0b00100010,
0b00011100 }, // D
{ 0b01111111,
0b01001001,
0b01001001,
0b01001001,
0b01000001 }, // E
{ 0b01111111,
0b00001001,
0b00001001,
0b00001001,
0b00000001 }, // F
{ 0b00111110,
0b01000001,
0b01001001,
0b01001001,
0b01111010 }, // G
{ 0b01111111,
0b00001000,
0b00001000,
0b00001000,
0b01111111 }, // H
{ 0b00000000,
0b01000001,
0b01111111,
0b01000001,
0b00000000 }, // I
{ 0b00100000,
0b01000000,
0b01000001,
0b00111111,
0b00000001 }, // J
{ 0b01111111,
0b00001000,
0b00010100,
0b00100010,
0b01000001 }, // K
{ 0b01111111,
0b01000000,
0b01000000,
0b01000000,
0b01000000 }, // L
{ 0b01111111,
0b00000010,
0b00001100,
0b00000010,
0b01111111 }, // M
{ 0b01111111,
0b00000100,
0b00001000,
0b00010000,
0b01111111 }, // N
{ 0b00111110,
0b01000001,
0b01000001,
0b01000001,
0b00111110 }, // O
{ 0b01111111,
0b00001001,
0b00001001,
0b00001001,
0b00000110 }, // P
{ 0b00111110,
0b01000001,
0b01010001,
0b00100001,
0b01011110 }, // Q
{ 0b01111111,
0b00001001,
0b00011001,
0b00101001,
0b01000110 }, // R
{ 0b01000110,
0b01001001,
0b01001001,
0b01001001,
0b00110001 }, // S
{ 0b00000001,
0b00000001,
0b01111111,
0b00000001,
0b00000001 }, // T
{ 0b00111111,
0b01000000,
0b01000000,
0b01000000,
0b00111111 }, // U
{ 0b00011111,
0b00100000,
0b01000000,
0b00100000,
0b00011111 }, // V
{ 0b00111111,
0b01000000,
0b00111000,
0b01000000,
0b00111111 }, // W
{ 0b01100011,
0b00010100,
0b00001000,
0b00010100,
0b01100011 }, // X
{ 0b00000111,
0b00001000,
0b01110000,
0b00001000,
0b00000111 }, // Y
{ 0b01100001,
0b01010001,
0b01001001,
0b01000101,
0b01000011 } // Z
};
void sendCmd(uint8_t command) { // отправка команд на дисплей
Wire.beginTransmission(OLED_ADDR);
Wire.write(0x80); // Байт управления: следующая посылка — команда
Wire.write(command);
Wire.endTransmission();
}
void setCursor(uint8_t page, uint8_t col) { // задаем место печати символа
// в контроллере SSD1306 память 128x64 пикселя разделена на 8 страниц (строк) по 128 столбцов
sendCmd(0xB0 + page); // Установка страницы (строки)
sendCmd(col & 0x0F); // Столбец (младшие 4 бита)
sendCmd(0x10 | (col >> 4)); // Столбец (старшие 4 бита)
}
void clear(uint8_t page, uint8_t col) { // функция очистки с заданного места и до конца строки
setCursor(page, col);
Wire.beginTransmission(OLED_ADDR);
Wire.write(0x40); // следующий байт (или поток байтов) будет данными пикселей, которые отобразятся на экране
for (int i = col; i < 128; i++) {
Wire.write(0);
if (i > 0 && i % 16 == 0) { // Каждые 16 байт перезапускаем передачу (защита буфера Wire)
Wire.endTransmission();
Wire.beginTransmission(OLED_ADDR);
Wire.write(0x40);
}
}
Wire.endTransmission();
}
void clear(uint8_t page) { // очистка строки
clear(page, 0);
}
void clear() { // очистка всего экрана
for (int p = 0; p < 8; p++) {
clear(p);
}
}
void printStr(uint8_t page, uint8_t col, const char* s) { // печать на экран
setCursor(page, col);
while (*s && col < 122) { // Условие: пока есть символы И есть место на экране
char c = *s;
if (c >= 'a' && c <= 'z') c -= 32; // Если строчная буква (a-z), превращаем в заглавную (A-Z)
int fontIndex = c - 32;
if (fontIndex < 0 || fontIndex > 58) fontIndex = '?' - 32; // Если символ вне диапазона шрифта заменяем на знак вопроса
Wire.beginTransmission(OLED_ADDR); // Печать символа
Wire.write(0x40); // следующий байт (или поток байтов) будет данными пикселей, которые отобразятся на экране
for (int i = 0; i < 5; i++) {
Wire.write(pgm_read_byte(&(font5x7[fontIndex][i]))); // побайтно считываем символы из таблицы
}
Wire.write(0x00); // Межсимвольный интервал 1 столбец
Wire.endTransmission();
s++;
col += 6; // Сдвигаемся на 5 столюцов буквы + 1 пробел
}
clear(page, col); // стираем остаток строки
}
void printStr(uint8_t page, const char* s) { // печать с начала строки
printStr(page, 0, s);
}
void drawProgressBar(uint8_t page, int percent) { // рисование ползунка
setCursor(page, 13); // начало X=13 окончание X=115 ширина 100+2
Wire.beginTransmission(OLED_ADDR);
Wire.write(0x40);
Wire.write(0b01111110); // Левый край
for (int i = 0; i < 100; i++) {
// Каждые 16 байт перезапускаем передачу (защита буфера Wire)
if (i > 0 && i % 16 == 0) {
Wire.endTransmission();
Wire.beginTransmission(OLED_ADDR);
Wire.write(0x40);
}
if (i < percent) Wire.write(0b01111110); // рисуем заполненную область
else Wire.write(0b01000010); // или только границу
}
Wire.write(0b01111110); // Правый край
Wire.endTransmission();
}
void setup() {
Wire.begin();
Wire.setClock(400000);
// Инициализация SSD1306
uint8_t init[] = { 0xAE, 0xD5, 0x80, 0xA8, 0x3F, 0xD3, 0x00, 0x40, 0x8D, 0x14, 0xA1, 0xC8, 0xDA, 0x12, 0x81, 0xCF, 0xD9, 0xF1, 0xDB, 0x40, 0xA4, 0xA6, 0xAF };
for (uint8_t i = 0; i < sizeof(init); i++) sendCmd(init[i]);
clear();
printStr(0, "https://robot.mipt.ru/");
}
void loop() {
// выведем на экран напряжение на A0
int adcValue = analogRead(A0); // Читаем значение 10 бит (0 - 1023)
float voltage = adcValue * (5.0 / 1023.0); // Переводим в вольты
char buf[22]; // Тут формируем строку. На экране поместится максимум 22 символа в строке
char fBuf[10]; // Временный буфер для дробного числа (snprintf в ардуино их не выводит)
// Конвертируем float (voltage) в строку vBuf
dtostrf(voltage, 4, 2, fBuf); // (число, общая ширина 4, знаков после запятой 2, куда писать)
// Формируем итоговую строку в buf
// %d - для целого числа АЦП, %s - для строки с вольтами
snprintf(buf, sizeof(buf), "A0: %d, %s V", adcValue, fBuf);
printStr(3, buf); // Выводим строку на экран
drawProgressBar(5, voltage * 20); // рисуем ползунок
// напечатаем время с момента запуска микроконтроллера
unsigned long currentMillis = millis();
int h = (currentMillis / 3600000) % 24; // часы
int m = (currentMillis / 60000) % 60; // минуты
int s = (currentMillis / 1000) % 60; // секунды
int ms_hundreds = (currentMillis % 1000) / 10; // Сотые доли (альтернативный способ вывода дробных чисел)
// Формируем строку времени: ЧЧ:ММ:СС.сс
snprintf(buf, sizeof(buf), "UP: %02dH %02dM %02d.%02dS", h, m, s, ms_hundreds);
printStr(7, buf);
delay(100);
}