Контроллер долива аквариума я отдал отцу и через неделю получил первый запрос на доработку: проценты на экране — это хорошо, но «глаза не цепляются», хочется полоску, как на телефоне. Первая версия бара была честной, но уродливой: я печатал строку из решёток — сколько процентов, столько и символов. Выглядело это рвано: буква решётки уже знакоместа, между клетками просвечивали щели, а на краях бар «дышал», потому что целые клетки прибавлялись в неудачных местах. Хотелось настоящих сплошных клеток — а они есть: у HD44780 каждая клетка экрана — это глиф, который можно перерисовать под себя.
В этом уроке соберём классический горизонтальный прогресс-бар на LCD 16x2: шестнадцать клеток, каждая — свой символ с частичной заливкой, шаг точности — один столбец пикселей, то есть 1,25 % на весь экран. Материал опирается на предыдущий урок про createChar: там мы разобрали, как устроены слоты CGRAM и почему нулевой слот печатают только через write. Здесь пойдём дальше — от отдельных значков к целой линейке глифов, которые работают согласованно, как один прибор.
Железо подойдёт любое: экран по I2C или по параллельной шине из урока про шесть проводов — код бара отличается только конструктором дисплея. В симуляторе соберём I2C-вариант: движок рисует дисплей сам из кода, а старт процесса назначим кнопке. Сразу оговорюсь про тренажёр: пиксельные глифы он не рисует, поэтому бар там собран из символов-заместителей, но вся логика подсчёта клеток в нём настоящая.
Пять колонок и математика бара
Начнём с арифметики, потому что она определяет, насколько гладким получится бар. Каждое знакоместо — сетка 5 на 8 пикселей, значит строка 16x2 — это 16 умножить на 5, то есть 80 столбцов. Растягиваем 100 % на всю строку — и один столбец пикселей стоит 1,25 %, вдвое точнее, чем целая клетка. На двадцатиколоночных панелях математика сходится идеально: 20 на 5 — ровно 100 столбцов, по одному на процент, никакой дроби.
Слотовая бухгалтерия такая: клетка бара бывает пустой, частичной и полной. Пустая и четыре степени частичной заливки — это пять глифов, которые мы зальём в слоты 0–4 командой createChar — те же массивы байтов, что складывались в уроке про массивы и журналы. Полная клетка слота не тратит: сплошной блок уже лежит в ROM знакогенератора под кодом 255, и lcd.write(255) печатает его без всякой подготовки. Итого бар съедает пять слотов из восьми — если рядом живут градус и стрелки из меню с курсором, раскладывайте слоты заранее, иначе на пятом значке упрётесь в потолок.
| Код или слот | Что печатает | Откуда берётся |
|---|---|---|
| Слот 0 | Пустая клетка — рамка | CGRAM, createChar(0, bar0) |
| Слоты 1–4 | Клетка с заливкой на 1–4 колонки слева | CGRAM, createChar с массивом байтов |
| Код 255 | Сплошной блок | ROM знакогенератора, lcd.write(255) без createChar |
| Точность | Клетка — 6,25 %, столбец — 1,25 %; на 20x4 столбец ровно 1 % | Считается от 5 колонок в знакоместе |
#include <Wire.h>#include <LiquidCrystal_I2C.h> LiquidCrystal_I2C lcd(0x27, 16, 2); // пять клеток бара: рамка и четыре степени заливки слева направоbyte bar0[8] = {0b11111,0b10001,0b10001,0b10001,0b10001,0b10001,0b10001,0b11111};byte bar1[8] = {0b11111,0b10000,0b10000,0b10000,0b10000,0b10000,0b10000,0b11111};byte bar2[8] = {0b11111,0b11000,0b11000,0b11000,0b11000,0b11000,0b11000,0b11111};byte bar3[8] = {0b11111,0b11100,0b11100,0b11100,0b11100,0b11100,0b11100,0b11111};byte bar4[8] = {0b11111,0b11110,0b11110,0b11110,0b11110,0b11110,0b11110,0b11111}; int percent = 0; // ваш источник прогрессаunsigned long last = 0; void setup() { lcd.init(); lcd.backlight(); lcd.createChar(0, bar0); // слоты 0-4 - под клетки бара lcd.createChar(1, bar1); lcd.createChar(2, bar2); lcd.createChar(3, bar3); lcd.createChar(4, bar4);} // p: 0..100; полных клеток столько, на сколько столбцов хватает процентовvoid drawBar(int p) { int cells = p * 80 / 100; // столбцы пикселей 0..80 for (int i = 0; i < 16; i++) { int v = cells - i * 5; // сколько столбцов в этой клетке lcd.setCursor(i, 0); if (v >= 5) lcd.write(255); // полный блок из ROM else if (v <= 0) lcd.write(byte(0)); // рамка - слот 0 else lcd.write(byte(v)); // частичная заливка } lcd.setCursor(0, 1); lcd.print(p); lcd.print("% "); // хвост затирает цифры сотен} void loop() { if (millis() - last > 150) { // обновление раз в 150 мс last = millis(); percent += 2; if (percent > 100) percent = 0; // демо-цикл по кругу drawBar(percent); }}Разберём сердце скетча — переменную v. Мы идём по клеткам слева направо и в каждой спрашиваем: сколько её столбцов уже «оплачено» процентами? Из общего числа столбцов cells вычитаем пять столбцов на каждую пройденную клетку — и получаем заливку текущей. Всё, что больше четырёх, — полная клетка, ноль и меньше — пустая, между ними — частичная, и она может быть только одна, на фронте бара. Частичные слоты печатаются строго через lcd.write(byte(v)): нулевой байт внутри print обрывается как конец строки, эта грабля уже ждала нас в уроке про знакогенератор. Если экран — 20x4, формула не меняется: в двух местах скетча 16 и 80 заменяются на 20 и 100, и бар получает шаг ровно в один процент.
Второй вопрос — темп обновлений. Перерисовка одной строки по шине I2C на 100 кГц занимает порядка 10–15 мс: на каждый символ в четырёхбитном режиме уходит несколько байтов шины. Поэтому обновлять бар чаще, чем раз в 50 мс, бессмысленно — глаз не увидит, а шина будет занята. Правило то же, что и в уроке про millis вместо delay: дисплей обновляют, когда изменились данные, а не каждый проход loop. Полная перерисовка строки поверх старой картинки при этом безопасна — стирать экран отдельно не нужно.
Симулятор: репетиция на символах-заместителях
Интерпретатор тренажёра команды createChar не поддерживает: слоты и пиксельные глифы он не воспроизводит — эта оговорка была ещё в уроке про свои символы. Поэтому бар в симуляторе собран из символов-заместителей: заполненные клетки печатаются решётками, пустые — прочерками. Логика при этом один в один с настоящим скетчем: считаем заполненные клетки от процентов и печатаем их по одной, остаток затираем. Когда перенесёте код на реальную плату, замените две строки печати на write(byte(n)) из блока кода выше — и вместо решёток появятся честные клетки с частичной заливкой.
Кнопка на D2 запускает процесс: нажмите её в интерфейсе тренажёра и следите за второй строкой — шаг прогресса идёт каждые 120 мс по millis, весь цикл занимает около шести секунд. После финиша нажмите кнопку снова — процесс перезапустится, и вы увидите, как частичная клетка аккуратно перестраивается с хвоста на хвост. Проценты печатаются с запасом пробелов: без хвостов двузначное число после трёхзначного оставило бы прилипший ноль.
Где такой бар пригодится первым делом: заряд и разряд аккумулятора, налив бака, копирование в EEPROM постранично, проход по шагам длинной процедуры. Тот же бар живёт и на параллельной схеме: поменяйте шапку — конструктор LiquidCrystal вместо I2C-версии и begin вместо init — и логика останется один в один. В I2C-варианте весь вывод уходит через переходник с чипом-расширителем — как он устроен внутри и почему от него зависят адреса 0x27 и 0x3F, разбираем в следующем уроке про PCF8574. Бар при этом не меняет ни строчки: библиотека прячет шину за тем же print и write.
Моё правило по интерфейсу: на 16x2 я всегда печатаю процент цифрой под баром — полоска даёт динамику, цифра точность, вместе они читаются мгновенно. На 20x4, где столбец стоит ровно один процент, цифра уже лишняя: бар самодостаточен. Вертикальные бары для уровней громкости или сигнала собираются из тех же байтов, повёрнутых на девяносто градусов — заливка снизу вверх, и это отличное упражнение на понимание порядка бит в байте.
- Поменяйте шаг демо со 2 % на 1 % и посмотрите, как оживает частичная клетка: на 16x2 у неё пять состояний, и переходы видны каждые 1,25 %.
- Свяжите бар с реальным процессом: копирование массива в EEPROM постранично или серия шагов серво — прогресс считайте от счётчика шагов, а не от времени.
- Соберите вертикальный бар: пять глифов с заливкой снизу вверх (байты 0b00001, 0b00011 и дальше), выведите уровень во второй строке.
- Добавьте на экран слоты-иконки из урока про createChar и распланируйте все восемь слотов на бумажке: бар, градус, стрелка — и посмотрите, хватает ли их одновременно.
- Строка 16x2 — это 80 столбцов пикселей: клетка стоит 6,25 %, столбец 1,25 %; на 20x4 столбец ровно 1 %
- Бар занимает пять слотов CGRAM: 0 — пустая рамка, 1–4 — заливка на 1–4 колонки слева
- Полная клетка — код 255 из ROM: lcd.write(255) без createChar, слот экономится
- Частичные слоты печатаются только через lcd.write(byte(n)): ноль внутри print обрывается как конец строки
- clear() в каждом кадре даёт строб; обновляйте печать поверх с затиранием хвостов и только при изменении данных
- Перерисовка строки по I2C — порядка 10–15 мс на 100 кГц: обновления чаще 50 мс глазу не видны
- 1. Сколько столбцов пикселей укладывается в строку LCD 16x2 и какой шаг это даёт?
- 2. Почему для полностью закрашенной клетки бара не создают свой глиф?
- 3. Сколько слотов CGRAM расходует горизонтальный бар из этого урока?
- 4. Бар на экране мигает при каждом обновлении. Какая строка скетча вероятнее всего виновата?
- 5. Что чаще всего печатает lcd.print((char)0) вместо глифа из слота 0?
Частые вопросы по теме урока
- Сколько столбцов пикселей укладывается в строку LCD 16x2 и какой шаг это даёт?
- Знакоместо — 5 пикселей в ширину, 16 знакомест дают 80 столбцов. Сто процентов на 80 столбцов — по 1,25 % на каждый. 80 столбцов — 1,25 % на столбец
- Почему для полностью закрашенной клетки бара не создают свой глиф?
- Сплошной блок — готовый символ ROM под кодом 255: lcd.write(255) печатает его без createChar, слот CGRAM остаётся свободным. Он уже лежит в ROM знакогенератора под кодом 255
- Сколько слотов CGRAM расходует горизонтальный бар из этого урока?
- Пустая рамка и четыре степени частичной заливки — пять глифов в слотах 0–4. Полный блок берётся из ROM и слота не занимает. Пять — слоты 0–4
- Бар на экране мигает при каждом обновлении. Какая строка скетча вероятнее всего виновата?
- clear() перед каждым кадром оставляет на экране пустую память между кадрами — отсюда строб. Стирать экран нужно при смене экрана, а не кадра. lcd.clear() перед каждой перерисовкой
- Что чаще всего печатает lcd.print((char)0) вместо глифа из слота 0?
- Нулевой байт внутри строковых функций означает конец строки, поэтому печать пустая. Глиф из слота 0 выводят через lcd.write(byte(0)). Пустое место — ноль обрывается как конец строки