Мой первый работающий вывод на LCD выглядел как авария: подпись «Temp:» была напечатана в setup(), а значение — в loop() с начала строки, и цифры наезжали на текст. Я лечил это вызовом lcd.clear() перед каждым обновлением и получал мерцающий экран: гаснет, рисуется, гаснет. Тогда же я узнал, что clear — самая дорогая команда дисплея, около полутора миллисекунд против 37 микросекунд у обычной, и что весь этот танец лечится тремя функциями, которые надо просто понять один раз: begin, setCursor и print. Ни одна из них не двигает моторы и не читает датчики — но без них даже идеальная схема выглядит слепой.
Это третий урок раздела: схему по 4 битам мы уже собрали в уроке про подключение LCD 16x2, внутренности HD44780 вспомнили по дороге. Остался язык, на котором с дисплеем разговаривают скетчи: словарь меньше двадцати слов, но правила употребления строже, чем у любого европейского. Разберём каждую команду с её нюансами и соберём секундомер на две строки, который обновляется без единого clear.
begin: договор о размере экрана
lcd.begin(16, 2) — первая строка любого дисплейного скетча, и это не формальность. Команда проводит полную инициализацию контроллера: переводит его в 4-битный режим, объявляет две строки и шрифт 5x8, включает дисплей, гасит видимую мигалку курсора и возвращает курсор в левый верхний угол. Аргументы тут — не пожелания, а параметры железа: по ним библиотека раскладывает адреса DDRAM под объявленную геометрию. Забытый begin даёт симптом из прошлого урока: залитая первая строка на пустом фоне — контроллер включён, но не проинициализирован. Для экрана 20x4 меняется только число: begin(20, 4), вся остальная логика скетча остаётся той же — в этом прелесть единого API.
В параллельной библиотеке LiquidCrystal begin обязателен в setup(). У I2C-варианта из урока LCD 16x2 по I2C ту же работу делает lcd.init() из библиотеки LiquidCrystal_I2C — разные обёртки над одной и той же стартовой последовательностью HD44780. Если будете читать чужой код и встретите и то и другое — это не два разных дисплея, это два обычая. Кстати, begin возвращает управление сразу: вся инициализация занимает доли миллисекунды, ждать после неё не нужно — ни delay(100), ни задержек из старых скетчей.
setCursor и print: координаты и печать
setCursor(столбец, строка) перемещает невидимый курсор. Нумерация с нуля: левый верхний угол — setCursor(0, 0), последний столбец второй строки — setCursor(15, 1). Каждый следующий print() пишет с текущей позиции и сам сдвигает курсор вправо после каждого символа. Поэтому порядок действий в скетче строгий: сначала приехать, потом печатать — обратный порядок и рождает те самые наезды цифр на подписи. Координаты за границей экрана не роняют программу: курсор уходит в скрытую зону DDRAM, о которой мы говорили в уроке что внутри символьного дисплея, и текст просто не виден. В симуляторе координаты за краем просто обрезаются — поведение чуть мягче реального железа, но вывод один: писать за границей можно, показывать будет нечего.
print() печатает не только строки в кавычках. lcd.print(25) выведет десятичное число, lcd.print(3.14) — с двумя знаками по умолчанию, а второй аргумент задаёт точность: lcd.print(voltage, 1) даст одну цифру после запятой. Это делает библиотеку самодостаточной для приборов: температуру, напряжение, счётчик — всё печатает одна команда без ручных пересчётов в строку. Тот же принцип действует и в Serial.print — API Arduino стараются держать единым. Один нюанс: печать float без второго аргумента даёт два знака по умолчанию — не два «красивых», а ровно два.
Теперь приём, который убрал моё мерцание. Метки, заголовки, рамки печатаются один раз в setup() — DDRAM их помнит сама. В loop() обновляются только изменяющиеся значения: setCursor на точное место, печать числа, хвост из пары пробелов, чтобы затереть старые цифры покороче. Правило, которое я вывел для себя: сколько на экране точек обновления — столько и setCursor, и ни одного clear в горячем цикле. Метки при этом живут в DDRAM сами по себе — мы выяснили это ещё на счётчике из первого урока раздела.
#include <LiquidCrystal.h> LiquidCrystal lcd(12, 11, 5, 4, 3, 2);float u = 4.87; // условные вольты с делителя void setup() { lcd.begin(16, 2); lcd.setCursor(0, 0); lcd.print("Hello!"); // с начала первой строки lcd.setCursor(11, 0); lcd.print(25); // числа печатаются как текст lcd.setCursor(0, 1); lcd.print("U="); lcd.print(u, 1); // одна цифра после запятой: U=4.9} void loop() {}Про видимую часть курсора. По умолчанию он невидим, но у контроллера есть два режима подсветки позиции: lcd.cursor() рисует подчёркивание в текущей ячейке, lcd.blink() — мигающий блок. Для обычных приборов они не нужны, но в меню и настройках мигающий блок — честный ответ на вопрос «что именно я сейчас кручу»; полноценную навигацию по меню мы строим в уроке LCD-меню и курсор. Парная им пара lcd.display() и lcd.noDisplay() гасит не подсветку, а сами символы: текст исчезает, DDRAM и подсветка остаются жить. Это готовый будильник «тапнуть экран, чтобы посмотреть время». И запомните обратную сторону: cursor() и blink() рисуют по той же сетке 5x8 — мигающий блок закрывает ровно одну ячейку, не больше.
И последнее: если экран выглядит зависшим, проверьте сначала, доходит ли до loop() хоть что-то. Маркер жизни в углу — один символ, мигающий раз в секунду, — решает спор между железом и кодом за мгновение. Ставить его — та же привычка, что и логировать значения в Serial: millis вместо delay потом позволит сделать это без блокировки всего скетча. А ещё маркер экономит нервы при отладке: мигает — плата жива, и искать надо в обвязке, а не в прошивке.
| Команда | Что делает | Когда звать |
|---|---|---|
| lcd.begin(16, 2) | инициализация контроллера | один раз в setup() |
| lcd.setCursor(col, row) | курсор в ячейку, нумерация с нуля | перед каждой печатью |
| lcd.print(x) | печать строки или числа с текущей позиции | по мере изменения значений |
| lcd.print(x, 1) | float с одним знаком после запятой | показания с точностью |
| lcd.clear() | полное стирание, курсор в (0, 0) | редко: смена режима или меню |
| lcd.cursor() / lcd.blink() | подчёркивание или мигающий блок | меню и настройки |
Стоп-часы: две строки и прыгающий курсор
Соберём секундомер: на верхней строке — подпись и живой маркер в правом углу, на нижней — время в формате MM:SS. Формат с ведущим нулём — не украшение: без него позиция цифр ползла бы по строке, и затирание пробелами пришлось бы делать хитрее. Serial-дублирование оставляю по привычке из первого урока раздела: экран показывает настоящее, лог хранит прошлое.
Обратите внимание на скелет: в setup() печатается только подпись Timer:, всё остальное живёт в loop() на фиксированных позициях. Позиция значения — setCursor(0, 1), позиция маркера — setCursor(15, 0): ровно две точки обновления, ни одного clear, и картинка не дёргается. Порядок обновления не важен — курсор прыгает между ячейками свободно, как треклятый заяц, только быстрее. Ведущие нули добиваются простым if: если секунд меньше десяти, сначала печатаем символ 0, потом само число — старый добрый ручной формат, который работает и на 16-сегментных табло, и в Serial.
Минуты считаются целочисленным делением, секунды — вычитанием: обе величины происходят из одного sec и никогда, ни при каких задержках, не разъедутся. Звёздочка мигает по чётности секунд — s % 2 == 0, дешёвый генератор мигания без единого аппаратного таймера. Когда секундомер покажет 15:59, следующая секунда перевернёт минуты сама. Проверьте на симуляторе и второй мой тезис: закомментируйте setCursor(15, 0) — звёздочка замрёт, хотя таймер продолжит идти, и это первый симптом забытой точки обновления.
- 1. Что делает lcd.begin(16, 2)?
- 2. Куда встанет курсор после setCursor(15, 1) на экране 16x2?
- 3. Что выведет lcd.print(3.14159, 2)?
- 4. Почему экран мерцает, если вызывать lcd.clear() в каждом проходе loop?
- 5. Как убрать устаревшие цифры после print(счётчик) без clear?
- lcd.begin(16, 2) в setup() обязателен для LiquidCrystal: 4-битный режим, две строки, шрифт 5x8; для 20x4 меняется только аргумент
- setCursor(столбец, строка) — нумерация с нуля; сначала приехать, потом печатать; каждый print сам сдвигает курсор вправо
- print понимает строки, целые и float: второй аргумент — число знаков после запятой, lcd.print(v, 1) даёт одну цифру
- Метки печатаются один раз в setup(), в loop() обновляются только значения на фиксированных позициях — без clear, с хвостом пробелов
- lcd.cursor() и lcd.blink() показывают позицию курсора; display/noDisplay гасят символы, но не подсветку — готовый «режим сна» прибора
- Маркер жизни в углу экрана отличает зависший скетч от просто молчащего датчика
Частые вопросы по теме урока
- Что делает lcd.begin(16, 2)?
- begin проводит полную инициализацию HD44780: 4-битный режим, размер экрана, включение дисплея, курсор в (0, 0). Без него контроллер команд не понимает. Настраивает контроллер: 4 бита, две строки, шрифт 5x8
- Куда встанет курсор после setCursor(15, 1) на экране 16x2?
- Первый аргумент — столбец 0–15, второй — строка 0–1. Столбец 15 строки 1 — самая последняя видимая ячейка экрана. В последний столбец второй строки
- Что выведет lcd.print(3.14159, 2)?
- Второй аргумент print задаёт число знаков после запятой для float. Два знака — получите 3.14; без аргумента было бы 3.14 по умолчанию. 3.14
- Почему экран мерцает, если вызывать lcd.clear() в каждом проходе loop?
- clear — самая долгая команда: около 1,5 мс полного стирания DDRAM. При частых вызовах глаз различает фазу пустого экрана — появляется мерцание. clear стирает весь экран, и момент перерисовки виден глазом
- Как убрать устаревшие цифры после print(счётчик) без clear?
- Пробел — такой же символ, он перезаписывает старые ячейки DDRAM. Хвост из пробелов после переменной части — стандартный приём обновления без стирания всего экрана. Напечатать несколько пробелов после числа