Вывеску из LCD 16x2 я повесил в окно мастерской и неделю считал дело законченным: температура, время, приглашение заходить. В субботу мимо прошла соседка, которой я месяц назад перепаивал паяльник, — и прошла мимо. Потом призналась: экран она не заметила вовсе. Текст, который не двигается, глаз не цепляет: статичный экран — это плакат, а плакаты смотрят один раз. Хотелось того самого бега строки из витрин и касс торгового центра — и у HD44780 он собирается за вечер, если не наступать на грабли, на которые я потратил первую ночь.
В этом уроке соберём рекламную бегущую строку на I2C-дисплее: сообщение любой длины едет по верхней строке, кнопка ставит его на паузу, светодиод дублирует состояние для тех, кто смотрит издалека. Разберём, почему встроенный scrollDisplayLeft() для вывески не годится, как нарезать длинный текст на шестнадцатисимвольные окна и с каким темпом их выводить, чтобы экран не мигал и не забивал шину. Схема — четыре провода из первого урока про I2C, ничего допаивать не нужно.
Три способа заставить текст двигаться
У библиотеки LiquidCrystal есть два готовых механизма, и оба на вывеске играют против вас. Первый — scrollDisplayLeft() и scrollDisplayRight(): сдвигают картинку на одно знакоместо за вызов. Ловушка в слове «всю»: едет и верхняя, и нижняя строка одновременно, разъединить их нельзя. Вторая ловушка глубже: у каждой строки экрана за видимой частью спрятано ещё 24 знакоместа видеопамяти DDRAM — всего их 40. Прокрутка проезжает и невидимые клетки, и на границе на экране мелькает хвост чужой памяти, потом дыра, и только потом текст возвращается. Выглядит это как поломка, хотя библиотека честно исполнила команду.
Второй механизм — autoscroll(): режим печатной машинки, где каждый новый print не пишется поверх, а проталкивает весь текст дальше. Для ленты входящих символов — журнал событий, построчный лог — он хорош. Но вывеске нужен не сдвиг всего, а окно: шестнадцать видимых клеток, через которые едет длинная строка. Это третий способ — свой, и он единственный даёт полный контроль: скорость, пауза, граница, вторая строка под свои нужды.
| Способ | Что делает | Когда годится |
|---|---|---|
| scrollDisplayLeft() / Right() | Сдвигает всю картинку на знакоместо, обе строки сразу, с проездом через невидимые 24 клетки DDRAM | Короткий эффект, когда обе строки и есть контент; для вывески не годится |
| autoscroll() | Каждый print проталкивает текст дальше, как на печатной машинке | Лента входящих символов: журнал, лог подключений |
| Окно по substring() | Печатаешь 16 символов из длинной строки на фиксированной позиции | Вывеска и реклама: контроль скорости, паузы и каждой клетки |
#include <Wire.h>#include <LiquidCrystal_I2C.h> LiquidCrystal_I2C lcd(0x27, 16, 2); String msg = "Skidka 20% na nabor! Do kontsa nedeli. ";int len = 0;int pos = 0;bool running = true;bool lastBtn = HIGH;unsigned long last = 0; void setup() { pinMode(2, INPUT_PULLUP); // кнопка на землю: нажата = LOW pinMode(12, OUTPUT); // LED горит, пока строка бежит lcd.init(); lcd.backlight(); len = msg.length(); digitalWrite(12, HIGH); lcd.setCursor(0, 1); lcd.print("Rabotaet ");} void loop() { bool btn = digitalRead(2); if (btn == LOW && lastBtn == HIGH) { running = !running; lcd.setCursor(0, 1); if (running) { lcd.print("Rabotaet "); digitalWrite(12, HIGH); } else { lcd.print("Pauza "); digitalWrite(12, LOW); } delay(200); // антидребезг на грубую } lastBtn = btn; if (running && millis() - last > 250) { last = millis(); pos = pos + 1; if (pos > len - 16) pos = 0; lcd.setCursor(0, 0); lcd.print(msg.substring(pos, pos + 16)); }}Сердце скетча — окно. В msg лежит вся реклама целиком, вместе с хвостом из двенадцати пробелов в конце. substring(pos, pos + 16) вырезает из неё ровно шестнадцать символов, и печать идёт на одну и ту же позицию (0, 0) — поверх прошлого кадра, без clear(). Позиция ползёт от нуля до len - 16: за этой границей substring отдал бы обрубок короче шестнадцати клеток, а хвост экрана остался бы от предыдущего прохода — вот зачем пробелы в конце сообщения: на стыке цикла экран чистится сам, без отдельной команды.
Темп задаёт millis(), а не delay(): шаг в одно знакоместо раз в 250 мс — комфортная скорость для чтения через витринное стекло. Быстрее 100 мс на шаг гнаться смысла нет: глаз не успевает, а полная перерисовка шестнадцати символов по I2C занимает порядка 10–15 мс — при попытке обновляться каждые 20 мс шина занята на половину, и вряд ли у неё нет других дел. Ритм «дисплей обновляется, когда пришло его время» — тот же, что в уроке про millis вместо delay: delay(200) остался только в обработчике кнопки как грубый антидребезг, тонкая работа с ним — отдельная тема. Кнопка висит на D2 с INPUT_PULLUP: нажатие замыкает пин на землю и видно как LOW — подробности в уроке про digitalRead. LED на D12 горит, пока строка бежит: издалека, без чтения мелкого шрифта, видно, жива ли вывеска.
Симулятор: вывеска с паузой
В тренажёре тот же скетч из блока кода, и исполняется он целиком: substring, millis-ритм, обработка кнопки. Кнопка уже подключена к D2, светодиод — к D12, и гаснет он вместе со строкой, как и на живой плате. Нажмите кнопку — строка замрёт, на нижней строке появится Pauza, светодиод погаснет; ещё нажатие — и реклама поедет дальше. Ритм в тренажёре ускоренный, чтобы не ждать: шаг окна идёт раз в 250 мс виртуального времени.
Одна честная оговорка: реклама здесь набрана транслитом, потому что русский текст на символьном LCD — тема, которая заслуживает двух отдельных уроков, и они идут следом. Логика окна от языка не зависит: когда разберётесь с кириллицей, поменяете строку msg — а остальной код не тронете.
Отдельная бухгалтерия — память. Строка сообщения лежит в SRAM целиком, а у Uno её всего два килобайта: полсотни байтов вывески — терпимо, но если соберётесь бегать текстом на несколько экранов, либо дробите объявление на куски, либо держите в голове, что каждый символ — это байт оперативки плюс байт позиции. Вторая строка при этом остаётся под ваши нужды: у меня там живёт статус и аптайм — глаз цепляется за движение вверху, а факты читает внизу, и оба хвоста у обеих строк затираются тем же приёмом с пробелами, что и окно.
Куда расти дальше. Первое — скорость с ручки: потенциометр на A0 через analogRead задаёт интервал между шагами, и вывеска подстраивается под зрителя. Мой практический потолок получился такой: шаг 120 мс на экране по параллельным проводам и 180 мс по I2C — быстрее глаз перестаёт складывать буквы в слова. Второе — две строки в противофазе: на 20x4-экране из урока про адресацию строк окно шире — substring(pos, pos + 20), а строк хватит на два сообщения, бегущих навстречу. Третье — стыковка с меню и курсором: заголовок меню едет, пункты стоят на месте. И четвёртое, о котором весь остаток раздела: что делать, когда на вывеске нужен русский язык.
- 1. Что делает lcd.scrollDisplayLeft() на дисплее 16x2?
- 2. Зачем в конце строки-сообщения стоит хвост из пробелов?
- 3. С каким шагом разумно двигать окно бегущей строки на I2C-дисплее?
- 4. Кнопка подключена с pinMode(2, INPUT_PULLUP). Что digitalRead(2) вернёт при нажатии?
- 5. Что изменится в скетче при переносе вывески на экран 20x4?
- Вывеску делай окном: substring(pos, pos + 16) раз в 250 мс по millis — полный контроль скорости, паузы и каждой клетки.
- scrollDisplayLeft() гонит обе строки и показывает невидимые 24 знакоместа DDRAM — для рекламы не годится.
- Хвост из пробелов в конце сообщения чистит стык цикла без clear() — экран не мигает.
- Кнопка на INPUT_PULLUP: нажатие — это LOW; LED дублирует состояние для взгляда издалека.
- На 20x4 окно шире: substring(pos, pos + 20) и граница len - 20, остальное без изменений.
Частые вопросы по теме урока
- Что делает lcd.scrollDisplayLeft() на дисплее 16x2?
- Прокрутка двигает всё содержимое сразу и проходит 24 невидимых знакоместа за краем каждой строки — отсюда мусор и дыры на границе. Для вывески это мешает. Сдвигает всю картинку: обе строки сразу, с проездом через невидимые клетки DDRAM
- Зачем в конце строки-сообщения стоит хвост из пробелов?
- Последнее окно цикла короче шестнадцати символов, и без пробелов на экране оставался бы хвост прошлого прохода. Пробелы чистят его автоматически. Чтобы на стыке цикла хвост экрана затирался без clear()
- С каким шагом разумно двигать окно бегущей строки на I2C-дисплее?
- Перерисовка 16 символов по I2C занимает 10–15 мс; шаг в одно знакоместо раз в 150–300 мс читается комфортно и не забивает шину. Раз в 150–300 мс: глаз успевает, шина не перегружена
- Кнопка подключена с pinMode(2, INPUT_PULLUP). Что digitalRead(2) вернёт при нажатии?
- При подтяжке к питанию покой — HIGH, а нажатая кнопка замыкает пин на землю: нажатие видно как LOW. LOW: кнопка замыкает пин на землю
- Что изменится в скетче при переносе вывески на экран 20x4?
- Меняется только ширина окна: двадцать символов вместо шестнадцати и соответствующая граница позиции. Ритм и логика паузы остаются. Окно станет substring(pos, pos + 20), граница — len - 20