Экран в моей теплице показывал журнал событий, и упёрся я в шестнадцать символов в первый же день. Хотелось видеть последнее событие целиком: «Полив 45 мин, свет 12 ч, окна закрыты» — а на месте оставалось «Полив 45 мин, све». Я честно ужал формулировку, потом ещё раз, потом третий: «Пол. 45м, св 12ч» — журнал начал читаться как шифровка военного времени, а каждый новый пункт всё равно не влезал. Вторая строка была занята показаниями датчиков, и единственным местом для длинного текста оставалась одна строка шириной с ладонь. Ушло полвечера, чтобы перестать воевать с формулировками и понять, как устроен сам экран.
В этом уроке разберём оба способа вытянуть длинный текст на 16x2: встроенную прокрутку scrollDisplayLeft() и ручное окно — когда скетч сам решает, какие шестнадцать символов строки показывать прямо сейчас. У встроенной прокрутки есть подлая особенность, о которой молчит половина статей и из-за которой я однажды потерял заголовок: она едет по всему экрану сразу. Ручное окно же собрано из того, что мы уже знаем: setCursor() и print() из урока про первый текст на I2C-дисплее плюс цикл for из урока про циклы. Скорость прокрутки подберём не вслепую, а по числам: у бегущей строки есть разумные пределы, и мы их обозначим. В конце соберём строку с паузой по кнопке в симуляторе.
Почему текст обрезается: экран — это окно в память
У контроллера HD44780, который сидит за стеклом, есть собственная память под текст — DDRAM на 80 байт: по сорок байт на каждую из двух строк. Видимая зона при этом — 16 знакомест. Экран — не вся память, а смотровое окошко в неё: команда scrollDisplayLeft() сдвигает это окошко на один знакоместо влево, scrollDisplayRight() — вправо. Я печатаю длинный текст в строку, он честно записывается в DDRAM дальше шестнадцатого символа, и прокрутка выносит на стекло то, что за краем. Вся адресация строк и память знакомест разобраны у нас в разборе begin, setCursor и print — здесь беру готовое.
Одна подробность из той же главы, которая однажды сэкономит вам вечер: в памяти вторая строка лежит не сразу за первой. У классического HD44780 первая строка занимает адреса с нулевого по 0x27, а вторая начинается с 0x40 — со сдвигом. Пока вы работаете через setCursor(столбец, строка), сдвиг вас не касается, но как только захочется писать в DDRAM напрямую или поймать «текст поплыл по строкам», вспомните про эту дырку в сорок байт: у меня один раз именно она объясняла, почему запись двадцати символов в строку «не перетекла» на соседнюю, а исчезла.
Выглядит всё как подарок, но ловушка уже упомянута: окно общее для обеих строк. Заголовок на верхней строке уедет вместе с бегущим текстом — я проверял на живом дисплее: печатаю «Zhurnal» сверху, запускаю прокрутку, и через несколько секунд от заголовка остаётся одно воспоминание. Плюс темп: типовые примеры крутят scrollDisplayLeft() в цикле с delay(400), и это ещё прилично. Быстрее 150 мс на символ глаз перестаёт цеплять слова, медленнее 700 мс — табло выглядит зависшим. Мой рабочий диапазон — 300–450 мс на шаг: при нём строка читается спокойно даже с расстояния в пару метров.
#include <Wire.h>#include <LiquidCrystal_I2C.h> LiquidCrystal_I2C lcd(0x27, 16, 2); void setup() { lcd.init(); lcd.backlight(); lcd.setCursor(0, 0); lcd.print("Zagolovok"); // уедет вместе с текстом! lcd.setCursor(0, 1); lcd.print("Poliv 45 min, svet 12h, okna zakryty");} void loop() { lcd.scrollDisplayLeft(); // сдвиг окна на 1 знакоместо delay(400); // 400 мс на символ — ещё читаемо}Теперь то, что использую я. Ручное окно устроено скучнее и потому надёжнее: длинный текст лежит в переменной, а на дисплей выводится только его кусок с позиции shift длиной ровно шестнадцать символов — циклом for, по одному символу. Захотели сдвинуть — увеличили shift на единицу и перерисовали окно. Заголовок при этом не трогается вообще: он напечатан один раз и стоит на верхней строке, потому что перерисовываем мы только вторую. Затирать старые символы не нужно: окно всегда ровно шестнадцать знакомест, каждый шаг перезаписывает всю строку целиком — и никаких хвостов от прежних букв. Именно поэтому окно обязано быть ровно шестнадцать, а не «сколько получится»: напечатаете окно на десять символов — шесть правых знакомест останутся жить со вчерашним содержимым.
Посмотрим на цену вопроса, потому что я поначалу боялся «спамить» дисплей. Один символ по I2C — это отдельный пакет на шине; при стандартных ста килогерцах полная перерисовка окна из шестнадцати символов занимает около полутора-двух миллисекунд. Даже если дёргать её двадцать раз в секунду, процессор заметит доли процента. Но визуально чаще 100–150 мс смысла нет: бегущая строка читается именно потому, что буквы стоят на месте достаточно долго. Пара слов про память: строка на 62 символа в String — это примерно 62 байта кучи, а у Uno всей SRAM 2048 байт; для одной строки заголовка цена ниже любой экономии на буквах. Владельцы больших проектов с десятком длинных строк берут массив char и хранят текст во flash через PROGMEM — но это уже другая весовая категория, о которой у нас отдельный разговор.
Тренажёр: бегущая строка с паузой
Скетч в тренажёре — рабочий инструмент, а не витрина: журнал теплицы бежит по нижней строке, заголовок стоит на верхней. Кнопка на D2 ставит прокрутку на паузу и возвращает ход — на настоящей сборке так удобно вглядеться в конкретное событие. Скорость задана константой speedMs на 400 мс: поставьте 150 — и почувствуете, как текст перестаёт читаться. Длина строки на настоящей плате берётся через msg.length(), но движок симулятора методы String не понимает, поэтому длина задана числом 62 — считаем честно, вместе с хвостовыми пробелами, которые дают строке докрутиться до конца перед повтором.
Обратите внимание на приём, который экономит нервы. Прокрутка живёт не на delay, а на millis(): цикл крутится свободно, кнопка опрашивается каждые десять миллисекунд, и пауза срабатывает мгновенно, посреди бегущей строки. Соберёте то же самое через delay(400) — и нажатие станет лотереей; каркас «цикл без блокировок» разобран в уроке про millis вместо delay. Когда в loop появляется несколько независимых дел — строка бежит, датчик опрашивается, реле щёлкает — раскладывайте их по расписанию через планировщик задач на millis, он ровно про это.
Ещё одно место, где проверяется ваша аккуратность: константа 62. Длина строки должна совпадать с реальной длиной текста до символа: поставите больше — окно заедет в пустоту и на экране появится мусор, поставите меньше — часть фразы никогда не покажется. На настоящей плате длину берут через msg.length(), и константа не нужна. А когда хотите двигать не буквы, а нарисованные стрелки и градусы, получится анимация статуса — я собирал так индикатор с собственными символами createChar. А если длинный текст нужен целиком и без движения, иногда честнее разбить его на два статичных экрана и переключать по кнопке: для текста до 32 символов это проще любой прокрутки, и глаз не устаёт.
| Способ | Что едет | Когда выбираю его |
|---|---|---|
| scrollDisplayLeft() | Весь экран: обе строки разом | Чистая бегущая строка без статики: заголовков, шапок и постоянных цифр на экране нет |
| Ручное окно на 16 символов | Только нужная строка | Почти всегда: заголовок и цифры остаются на местах, а по кругу едет одна строка |
| Два экрана по очереди | Ничего, экраны сменяются | Текст до 32 символов, который читают целиком, а не подряд: два экрана по кнопке |
- Экран 16x2 — окно в DDRAM на 80 байт: scrollDisplayLeft() двигает обе строки разом, статичный заголовок так не удержать
- Ручное окно: длинный текст лежит в переменной, цикл for печатает 16 символов с позиции shift — остальной экран не трогается
- Темп прокрутки 300–450 мс на символ читаем; быстрее 150 мс глаз не успевает, медленнее 700 — табло выглядит зависшим
- Перерисовка окна из 16 символов по I2C на 100 кГц стоит около 2 мс — двигать её чаще 100 мс незачем
- Прокрутку держите на millis(), а не на delay: кнопка паузы должна опрашиваться между шагами, а не после них
- 1. Что именно сдвигает scrollDisplayLeft()?
- 2. Какой темп бегущей строки я считаю читаемым?
- 3. Сколько байт DDRAM приходится на одну строку в адресуемом диапазоне HD44780?
- 4. Почему пауза по кнопке в тренажёре срабатывает мгновенно?
- 5. Сколько примерно длится перерисовка окна из 16 символов по I2C на 100 кГц?
Частые вопросы по теме урока
- Что именно сдвигает scrollDisplayLeft()?
- Сдвигается смотровое окно, общее для обеих строк: поэтому статичный заголовок уезжает вместе с бегущим текстом. Хотите неподвижную шапку — собирайте ручное окно. Окно просмотра сразу по обеим строкам экрана
- Какой темп бегущей строки я считаю читаемым?
- При 300–450 мс на шаг строка читается с пары метров. Быстрее 150 мс слова сливаются, медленнее 700 — выглядит зависанием. 300–450 мс на символ
- Сколько байт DDRAM приходится на одну строку в адресуемом диапазоне HD44780?
- На строку отведено 40 байт, всего 80: именно поэтому заголовок в начале строки «кружит» по экрану каждые сорок шагов прокрутки и так же внезапно возвращается. 40
- Почему пауза по кнопке в тренажёре срабатывает мгновенно?
- Таймер прокрутки построен на millis(), loop не блокируется: между шагами строки скетч успевает опросить кнопку и обработать нажатие. Цикл крутится на millis() и опрашивает кнопку каждые 10 мс
- Сколько примерно длится перерисовка окна из 16 символов по I2C на 100 кГц?
- Каждый символ — отдельный пакет на шине; шестнадцать пакетов при 100 кГц занимают около полутора-двух миллисекунд, так что частая перерисовка окну не страшна. 1,5–2 мс