Embedded HandbookПриложения
Все разделы › III. Периферия
STM32F103 · уровень средний · 30 мин чтения
Открыть в интерактивном справочнике

22д. Символьный ЖК-дисплей LCD1602 на HD44780: 4 бита, инициализация, время команд, CGRAM, I²C-переходник

Две строки по 16 знаков, зелёная или синяя подсветка, шестнадцать выводов в ряд — самый распространённый текстовый дисплей в любительской и учебной электронике. Внутри — контроллер HD44780 или совместимый с ним. В разделе 22в стекло ЖКИ раскачивал сам микроконтроллер; здесь всё это делает контроллер на плате модуля, а программа лишь посылает ему команды и коды знаков. Протокол простой, но в нём есть три места, где спотыкаются все: инициализация, которая «то работает, то нет», время выполнения команд и кириллица, которой в модуле нет. Ниже — как устроен модуль, как его подключить на шесть выводов или через переходник I²C, почему инициализация именно такая и сколько стоит вывод одной строки.

Опоры: распространённая редакция даташита HD44780U (Hitachi): система команд, время их выполнения при частоте генератора 270 кГц, времянка шины, порядок инициализации в 4 битах, таблица знаков ПЗУ A00; даташиты PCF8574 и PCF8574A (NXP, Texas Instruments): адреса и скорость шины. В проекте этих документов нет, поэтому их цифры оговорены в подписи в конце. DocID13587 Rev 17 (STM32F103x8/xB): выводы PA4–PA7 без допуска 5 В, с. 29; PB6 и PB7 — FT, I2C1_SCL и I2C1_SDA, с. 32. Все числа ниже — расчёт _verify/lcd22f/ref22f.py (Python 3.11, образ stm-lab:1) и прогон стенда sim22f.c с моделью контроллера на компьютере (gcc 12.2.0) и в QEMU 7.2.22 на Cortex-M3 (arm-none-eabi-gcc 12.2.1).

Как это называется в документации

Оригинал Перевод
character LCD module символьный ЖК-модуль
DDRAM (display data RAM) память кодов знаков на экране
CGROM (character generator ROM) ПЗУ знакогенератора, встроенный шрифт
CGRAM (character generator RAM) ОЗУ своих знаков
address counter (AC) счётчик адреса, позиция курсора
busy flag (BF) флаг занятости
enable (E) строб записи
register select (RS) выбор: команда или данные
function set, entry mode set выбор режима шины, режим ввода
display shift, cursor shift сдвиг окна экрана, сдвиг курсора
initializing by instruction инициализация командами
contrast (VO, VEE) вход контраста
backlight подсветка
I/O expander расширитель портов

Вверху — запись байта данных в 4 битах: RS = 1, два импульса E, старший и младший полубайты на D4–D7, защёлка по спаду E; RS не меньше 60 нс до фронта E, E не короче 450 нс, данные не меньше 195 нс до спада, цикл E не меньше 1000 нс, после байта контроллер занят: запись 41 мкс, команда 37 мкс, clear и home 1,52 мс. Под ней — инициализация: питание 40 мс, 0x3 и 4,1 мс, 0x3 и 100 мкс, 0x3 и 100 мкс, 0x2, затем 0x28, 0x08, 0x01, 0x06, 0x0C. Ниже — подключение LCD1602 к STM32F103: RS к PA0, E к PA1, D4–D7 к PA4–PA7, D0–D3 не подключены, RW, VSS и K на 0 В, VDD на +5 В, VO на движок потенциометра 10 кОм, вывод A подсветки через резистор; строка 2 начинается с адреса 0x40. Внизу — биты переходника PCF8574: P0 RS, P1 RW, P2 E, P3 подсветка, P4–P7 — D4–D7, адрес 0x27 или 0x3F


22д.1 Как устроен модуль: DDRAM, знакогенератор и CGRAM

В модуле три памяти. DDRAM — 80 байт кодов знаков. Экран — окно в неё: у LCD1602 видно 16 знакомест в двух строках, но строка в памяти длиннее — 40 ячеек. Первая строка начинается с адреса 0x00, вторая — с 0x40, а не с 0x10: между ними в адресах дыра. У дисплеев 20×4 четыре строки лежат по адресам 0x00, 0x40, 0x14, 0x54 — третья строка продолжает первую, четвёртая — вторую; команды перехода на начало строк — 80 C0 94 D4.

Знакогенератор (CGROM) — ПЗУ со шрифтом: по коду из DDRAM он выдаёт рисунок 5×8 точек. Коды 0x20…0x7D совпадают с ASCII, кроме 0x5C — там знак иены. Верхняя половина таблицы зависит от варианта ПЗУ, и он прошит в кристалле при изготовлении. Самый частый, A00, — японский: там катакана и несколько математических знаков, кириллицы нет. Вариант A02 — европейский набор с частью кириллицы; есть модули совместимых контроллеров с полной кириллицей. Какое ПЗУ у вашего модуля, по внешнему виду не понять: выведите коды 0xA0…0xFF и посмотрите.

CGRAM — 64 байта под восемь своих знаков: по восемь строк на знак, в каждой строке пять младших бит — пять точек. Свои знаки получают коды 0…7 (и повторно 8…15) — 22д.8.

Отдельно о стекле. ЖКИ нельзя держать под постоянным напряжением (раздел 22в), и HD44780 сам выдаёт на сегменты переменные сигналы с мультиплексом. Программе об этом думать не нужно — зато нужно выставить контраст (22д.2).


22д.2 Выводы и питание: контраст, подсветка, RW

Шестнадцать выводов модуля:

Вывод Имя Куда
1 VSS 0 В
2 VDD +5 В (у модулей на 3,3 В — 3,3 В)
3 VO контраст: движок потенциометра между VDD и 0 В
4 RS 0 — команда, 1 — данные
5 RW 0 — запись, 1 — чтение
6 E строб
7–14 D0–D7 данные; в 4 битах — только D4–D7
15, 16 A, K анод и катод подсветки

Контраст. Видимость точек задаёт напряжение между VDD и VO: чем ниже VO, тем темнее точки. Потенциометр 10 кОм между +5 В и землёй берёт 0,5 мА и даёт плавную подстройку. Самая частая беда нового модуля — «не работает», когда работает: при неудачном положении движка экран пустой или первая строка залита чёрными прямоугольниками. Чёрные прямоугольники в первой строке и пустая вторая — это ещё и признак, что контроллер жив, но не инициализирован.

Подсветка — светодиод (или цепочка) между выводами 15 и 16. На многих модулях токоограничивающий резистор уже стоит на плате; если его нет, резистор ставят снаружи по току и прямому напряжению из даташита своего модуля. Для примера: при прямом напряжении 3,2 В и токе 15 мА от 5 В — 120 Ом и 27 мВт на резисторе.

RW на земле. Модуль умеет отдавать данные (флаг занятости, содержимое памяти), но для этого шина данных должна стать входом микроконтроллера, а выход модуля на 5 В выдаёт пятивольтовые уровни. Выводы PA4–PA7 у F103 не допускают 5 В (в таблице выводов DocID13587 у них нет пометки FT, с. 29). Поэтому в этом разделе модуль только принимает: RW на земле, а время выполнения команды выжидается паузой (22д.6).

Уровни. Модуль на 5 В принимает единицу от выхода на 3,3 В: порог входа HD44780 при питании 5 В — около 2,2 В по распространённой редакции даташита. А вот модуль на 5 В, запитанный от 3,3 В, обычно не даёт контраста: точкам не хватает напряжения. Для 3,3 В берут модуль, рассчитанный на 3,3 В.


22д.3 Восемь бит и четыре: строб E и защёлка по спаду

У контроллера два режима шины. В 8-битном байт идёт по D0–D7 за один строб: нужно 10 выводов (RS, E и восемь линий данных). В 4-битном — по D4–D7 двумя стробами, сначала старший полубайт, потом младший: 6 выводов. Через переходник I²C — 2 провода (22д.9). Скорость почти не страдает: время определяет не шина, а выполнение команды контроллером (22д.6).

Порядок записи: выставить RS и данные, поднять E, опустить E. Данные защёлкиваются по спаду E — к этому моменту они должны стоять спокойно. Подключение — как в стенде Лаборатории: RS — PA0, E — PA1, D4–D7 — PA4–PA7.

Драйвер раздела не знает о регистрах: функция порта out получает байт линий (RS, RW, E, подсветка и D4–D7 в старших битах), а порт решает, как его выставить — выводами GPIO или байтом по I²C:

void lcd_nibble(lcd_port_t *p, uint8_t rs, uint8_t n)
{
    uint8_t bus = (uint8_t)((n & 0x0Fu) << 4) | (rs ? LCD_RS : 0u) | p->bl;
    p->out(p, bus);                         /* RS и данные — раньше строба */
    if (p->e_us)
        p->delay_us(p, p->e_us);
    p->out(p, bus | LCD_E);
    if (p->e_us)
        p->delay_us(p, p->e_us);            /* E в единице: не меньше 450 нс при 3 В */
    p->out(p, bus);                         /* спад E: контроллер защёлкивает полубайт */
}

void lcd_byte(lcd_port_t *p, uint8_t rs, uint8_t v)
{
    lcd_nibble(p, rs, v >> 4);              /* старший полубайт первым */
    lcd_nibble(p, rs, v & 0x0Fu);
    if (p->flush)
        p->flush(p);                        /* I2C: байт для дисплея — одна посылка */
    if (p->byte_us)
        p->delay_us(p, p->byte_us);         /* занят: 37 мкс после команды, 41 после записи */
}

Для GPIO функция порта — одна запись в BSRR (раздел 8.2): старшая половина сбрасывает все шесть линий, младшая устанавливает нужные, и если бит есть в обеих, побеждает установка. Так RS, E и D4–D7 меняются разом, без промежуточных состояний:

static void gpio_out(lcd_port_t *p, uint8_t bus)
{
    (void)p;
    uint32_t set = ((bus & LCD_RS) ? 1u : 0u)           /* RS → PA0 */
                 | ((bus & LCD_E) ? 2u : 0u)            /* E  → PA1 */
                 | (bus & 0xF0u);                       /* D4–D7 → PA4–PA7 */
    GPIOA->BSRR = (0xF3u << 16) | set;                  /* сброс и установка одной записью */
}

22д.4 Тайминги шины: длительность E и установка данных

Шина HD44780 медленная по меркам микроконтроллера. По распространённой редакции даташита HD44780U:

Питание 5 В Питание 3 В
RS и RW до фронта E ≥ 40 нс ≥ 60 нс
длительность E в единице ≥ 230 нс ≥ 450 нс
данные до спада E ≥ 80 нс ≥ 195 нс
период E (от фронта до фронта) ≥ 500 нс ≥ 1000 нс

На 72 МГц такт процессора — 13,9 нс, и импульс из двух записей в порт подряд длится считанные такты — короче, чем требует таблица. Отсюда частая загадка: на 8 МГц дисплей работал, после перехода на 72 МГц (раздел 6) показывает мусор. Драйвер раздела берёт значения для 3 В с запасом и не считает такты: пауза 1 мкс перед фронтом E и 1 мкс строб. Значит, RS и данные стоят 1000 нс до фронта, E в единице 1000 нс, период E — 2000 нс. Модель стенда сверяет все четыре условия на каждом стробе: нарушений — 0. Через переходник I²C эти условия выполняются сами собой: каждое изменение линии — целый байт по шине, 90 мкс при 100 кГц и 22,5 мкс при 400 кГц.


22д.5 Инициализация «по инструкции» и почему именно так

После подачи питания контроллер выполняет внутренний сброс и оказывается в 8-битном режиме. Пока он не готов, стробы теряются: даташит велит ждать больше 15 мс после того, как питание достигло 4,5 В, и больше 40 мс после 2,7 В. Драйвер ждёт 40 мс — годится и для 5 В, и для 3,3 В.

Дальше порядок из даташита:

void lcd_init(lcd_port_t *p)
{
    p->delay_us(p, 40000u);                 /* 40 мс после подачи питания (при 5 В хватит 15) */
    lcd_nibble_wait(p, 0x3u, 4100u);        /* 0x30 «8 бит» трижды: из любого состояния */
    lcd_nibble_wait(p, 0x3u, 100u);         /* контроллер приходит в 8-битный режим */
    lcd_nibble_wait(p, 0x3u, 100u);
    lcd_nibble_wait(p, 0x2u, 100u);         /* 0x20 в 8 битах: переход на 4 бита */
    lcd_cmd(p, LCD_FUNC_4B_2L);             /* теперь — байтами из двух полубайтов */
    lcd_cmd(p, LCD_DISP_OFF);
    lcd_cmd(p, LCD_CLEAR);
    lcd_cmd(p, LCD_ENTRY_INC);
    lcd_cmd(p, LCD_DISP_ON);
}

В 8-битном режиме D0–D3 модуля висят на земле, поэтому одиночный полубайт 0x3 на D4–D7 контроллер читает как полный байт 0x30 — команду «шина 8 бит». Полубайт 0x2 даёт 0x20 — «шина 4 бита», и с этого момента каждый байт идёт двумя полубайтами. Вся последовательность занимает 46 678 мкс, из них 40 мс — ожидание питания.

Зачем тогда три раза 0x3, если после включения хватило бы одного 0x2? Потому что включение питания — не единственный способ попасть в lcd_init. Кнопка RESET, прошивка по SWD, сторожевой таймер сбрасывают микроконтроллер, а дисплей остаётся под питанием — в 4-битном режиме. Хуже того, сброс мог прийти между двумя полубайтами, и контроллер уже держит старший полубайт и ждёт младший. Из такого состояния одиночный 0x2 становится чужой половиной байта.

Стенд перебирает все состояния контроллера: 8 бит после включения, 4 бита в ожидании старшего полубайта и 4 бита с уже принятым старшим (16 вариантов его значения). После инициализации выводятся две строки, и экран сверяется с ожидаемым:

Состояние при старте 0x3, 0x3, 0x3, 0x2, первая пауза 4,1 мс то же, первая пауза 100 мкс только 0x2
8 бит (питание включено только что) да да да
4 бита, ждёт старший полубайт да да нет
4 бита, старший уже принят 16 из 16 15 из 16 14 из 16

Как работают три 0x3. Если контроллер в 8 битах, каждый 0x3 — команда 0x30, безвредная. Если в 4 битах и ждёт старший полубайт, первые два 0x3 складываются в байт 0x33 — «8 бит», и третий уже читается как 0x30. Если старший полубайт уже принят, первый 0x3 дописывает неизвестный байт, а следующие два снова складываются в 0x33 (если же дописанный байт сам оказался командой «8 бит», они читаются как 0x30). Из любого состояния после трёх стробов контроллер в 8 битах, и 0x2 переводит его в 4 бита предсказуемо.

Пауза 4,1 мс после первого 0x3 тоже не случайна. Если принятый старший полубайт был 0, первый 0x3 собирает байт 0x03 — команду «домой», которая выполняется 1,52 мс. С паузой 100 мкс следующий 0x3 приходит, пока контроллер занят, теряется, и дальше всё съезжает на полубайт: модель показывает 9 потерянных полубайтов и на экране остатки старого текста. Отсюда 15 из 16 в средней колонке.

Одиночный 0x2 на контроллере, ждущем старший полубайт, работает так: 0x2 запоминается, следующий 0x2 из команды 0x28 дописывает байт 0x22 — «4 бита, одна строка», а полубайт 0x8 приходит, пока команда выполняется, и теряется. В модели первая строка выводится, вторая — нет. Эта ошибка коварна: после включения питания всё работает, и проявляется она только при сбросе одного микроконтроллера.

Попробуйте в приложении: Лаборатория → стенд «LCD1602».


22д.6 Команды, время их выполнения и флаг занятости

Команда Код Время при 270 кГц
очистка (clear) 0x01 1,52 мс
курсор в начало (home) 0x02 1,52 мс
режим ввода: адрес растёт, без сдвига 0x06 37 мкс
экран вкл./выкл., курсор, мигание 0x08…0x0F 37 мкс
сдвиг курсора или экрана 0x10…0x1F 37 мкс
режим шины: 4 бита, 2 строки, 5×8 0x28 37 мкс
адрес CGRAM 0x40 + адрес 37 мкс
адрес DDRAM 0x80 + адрес 37 мкс
запись данных (RS = 1) код знака 41 мкс

Пока команда выполняется, контроллер занят и новые стробы не принимает: полубайт пропадает, и все байты дальше собираются из чужих половинок. Время дано для частоты внутреннего генератора 270 кГц; у реального модуля генератор отличается, поэтому драйвер ждёт с запасом: 50 мкс после каждого байта и 2 мс после clear и home. Между спадом E последнего полубайта и спадом E следующего проходит 52 мкс — запас 27 % над 41 мкс; 2 мс — запас 32 % над 1,52 мс.

Стенд уменьшает паузу после байта: при 39 мкс полубайты ещё не теряются (39 + 2 = 41 мкс), при 38 мкс теряются 18 полубайтов и текст на экране не тот. А очистка без паузы 2 мс губит весь следующий текст: 26 полубайтов приходят, пока контроллер стирает память, и экран остаётся пустым.

void lcd_cmd(lcd_port_t *p, uint8_t c)
{
    lcd_byte(p, 0, c);
    if (c == LCD_CLEAR || (c & 0xFEu) == LCD_HOME)
        p->delay_us(p, 2000u);              /* clear и home — 1,52 мс */
}

Флаг занятости. Вместо паузы можно спрашивать контроллер: при RS = 0 и RW = 1 на D7 выходит флаг BF, и пока он равен 1, контроллер занят. Это быстрее (не ждать худший случай), но требует вывода RW и переключения D4–D7 на вход, а модуль на 5 В отдаёт пятивольтовую единицу (22д.2). С RW на земле флаг не прочитать — остаётся ждать.


22д.7 Адрес курсора, строки и сдвиг экрана

Запись знака кладёт код по адресу из счётчика AC и увеличивает его (режим 0x06). Переход на позицию — команда 0x80 | адрес:

void lcd_goto(lcd_port_t *p, uint8_t row, uint8_t col)
{
    static const uint8_t row_addr[4] = {0x00u, 0x40u, 0x14u, 0x54u};   /* 1602, 1604 и 2004 */
    lcd_cmd(p, (uint8_t)(LCD_DDRAM | (row_addr[row & 3u] + col)));
}

Строка не переносится на следующую: после 16-го знака запись идёт в невидимые ячейки той же строки. Стенд пишет 20 знаков ABCDEFGHIJKLMNOPQRST в первую строку: на экране первые 16, вторая строка пуста, счётчик адреса — 0x14. Только на 41-м знаке адрес из 0x27 перескакивает на 0x40, и знак появляется в начале второй строки.

Невидимая часть строки полезна: команда 0x18 сдвигает окно экрана на знакоместо влево, сами коды в памяти не трогаются. После четырёх сдвигов видны знаки с 4-го по 19-й — EFGHIJKLMNOPQRST. Так делают бегущую строку длиной до 40 знаков без перезаписи. Сдвиг действует на обе строки сразу.

Обновлять экран лучше без очистки: clear стоит 1,52 мс и даёт заметное мигание. Перешли на позицию и переписали только изменившиеся знаки, дополнив число пробелами до прежней ширины.


22д.8 Свои символы: CGRAM и кириллица

Свой знак — восемь байт в CGRAM: команда 0x40 + 8·код, затем восемь записей данных, по строке на запись (адреса знаков 0…7 — 40 48 50 58 60 68 70 78). Бит 4 строки — левая точка, бит 0 — правая; восьмая строка — место курсора, её оставляют пустой.

void lcd_glyph(lcd_port_t *p, uint8_t code, const uint8_t rows[8])
{
    lcd_cmd(p, (uint8_t)(LCD_CGRAM | ((code & 7u) << 3)));   /* 8 строк на знак */
    for (uint32_t i = 0; i < 8u; i++)
        lcd_byte(p, 1, rows[i]);
    /* адрес остался в CGRAM: следующая запись испортит знак, пока не будет lcd_goto */
}

После загрузки знака счётчик адреса указывает в CGRAM. Если сразу вывести текст без lcd_goto, он уйдёт не на экран, а в рисунок следующего знака: стенд записал AB — экран пуст, а в строках знака с кодом 1 появились 01 и 02.

Кириллица. В ПЗУ A00 русских букв нет, и строка в UTF-8 выводится мусором: каждая русская буква — два байта D0 xx или D1 xx, и обоим соответствуют японские знаки. Восьми своих знаков на весь алфавит не хватит, но хватает на типичную надпись. Приём: буквы, похожие на латинские (А В Е К М Н О Р С Т У Х), выводят латинскими, а для остальных рисуют свои. Драйвер раздела рисует Б Г Д Ж И Л П Я, а lcd_utf8 переводит строку UTF-8 в коды модуля:

static const char lcd_cyr[32] = {
    'A', 8, 'B', 9, 10, 'E', 11, '3', 12, '?', 'K', 13, 'M', 'H', 'O', 14,
    'P', 'C', 'T', 'Y', '?', 'X', '?', '?', '?', '?', '?', '?', 'b', '?', '?', 15,
};

Свои знаки берутся с кодами 8…15, а не 0…7: это те же знаки CGRAM, но код 0 внутри строки C — её конец, и lcd_puts на нём остановится. Строка ПРИВЕТ, ДРУГ! — 23 байта UTF-8 — превращается в 13 кодов 0E 50 0C 42 45 54 2C 20 0A 50 59 09 21, и на экране модели — ПPИBET, ДPYГ! (П, И, Д и Г — свои знаки, остальное — латиница). Знак градуса в A00 есть готовый — код 0xDF; ТЕМП. 23,5°C выводится так же. Буквы без замены (Ф, Ы, строчные) становятся ? — честнее, чем мусор. Полный алфавит — только модуль с другим ПЗУ.


22д.9 Переходник I²C на PCF8574

Плата-переходник, напаянная на выводы модуля, — это расширитель портов PCF8574: восемь выводов P0–P7, которые выставляются байтом по I²C. На распространённой плате P0 — RS, P1 — RW, P2 — E, P3 — подсветка (через транзистор), P4–P7 — D4–D7. Это не стандарт: встречаются платы с другой разводкой, и тогда драйвер с «обычной» таблицей не оживит экран. Стенд подставил драйверу условную таблицу с перепутанными половинами байта — контроллер так и не вышел из 8 бит, экран пуст. Разводку своей платы проверяют прозвонкой от выводов PCF8574 к выводам модуля.

Адрес. У PCF8574 адреса 0x20…0x27, у PCF8574A — 0x38…0x3F; три младших бита задают перемычки A0–A2. На большинстве плат перемычки разомкнуты, и адрес — 0x27 или 0x3F, в линии с битом записи — 0x4E или 0x7E (о сдвиге адреса — раздел 17.2). Не знаете, какой кристалл, — переберите адреса и посмотрите, кто ответит.

Каждый байт по I²C — новое состояние всех восьми выводов. Полубайт — три байта: данные, данные с E = 1, данные с E = 0. Знак H (0x48) с включённой подсветкой уходит шестью байтами 49 4D 49 89 8D 89. Посылка на I2C1 F103 опросом флагов (настройка на 100 кГц, CCR = 180 — раздел 17.3):

static void i2c_flush(lcd_port_t *p)
{
    (void)p;
    if (!i2c_n)
        return;
    I2C1->CR1 |= I2C_CR1_START;
    while (!(I2C1->SR1 & I2C_SR1_SB))
        ;
    I2C1->DR = PCF_ADDR << 1;                           /* 0x4E: адрес и бит записи */
    while (!(I2C1->SR1 & (I2C_SR1_ADDR | I2C_SR1_AF)))
        ;
    if (I2C1->SR1 & I2C_SR1_AF) {                       /* никто не ответил: не тот адрес */
        I2C1->SR1 &= ~I2C_SR1_AF;
        I2C1->CR1 |= I2C_CR1_STOP;
        i2c_nack++;
        i2c_n = 0;
        return;
    }
    (void)I2C1->SR2;                                    /* чтение SR2 снимает ADDR */
    for (uint32_t i = 0; i < i2c_n; i++) {
        while (!(I2C1->SR1 & I2C_SR1_TXE))
            ;
        I2C1->DR = i2c_buf[i];                          /* каждый байт — новое состояние P0…P7 */
    }
    while (!(I2C1->SR1 & I2C_SR1_BTF))
        ;
    I2C1->CR1 |= I2C_CR1_STOP;
    i2c_n = 0;
}

Подсветка — тоже бит в каждом байте. Драйвер держит его в p->bl и добавляет к каждому байту: забытый бит гасит подсветку при первой же записи. PCF8574 по даташитам рассчитан на шину 100 кГц; на 400 кГц многие платы работают, но это за пределами документа. Выводы PB6 и PB7 у F103 допускают 5 В (FT, с. 32), так что переходник с подтяжками к 5 В их не повредит; подтяжки на шине обязательны (раздел 17.1).


22д.10 Сколько стоит одна строка: GPIO против I²C

Строка — команда адреса и 16 знаков, 17 байт. Через GPIO драйвер тратит на байт 4 мкс на два полубайта и 50 мкс ожидания — 54 мкс, на строку 918 мкс. Контроллеру по даташиту нужно 693 мкс (37 + 16·41); разница — запас на разброс генератора.

Через I²C каждый байт для дисплея — посылка из адреса и шести байт: 65 периодов такта (старт и стоп в модели — по периоду). Ждать после байта не нужно — посылка сама длиннее 41 мкс:

Шина Знак Строка (17 байт) Инициализация От спада E до следующего спада Потеряно полубайтов
GPIO 54 мкс 918 мкс 46 678 мкс 52 мкс 0
I²C 100 кГц 650 мкс 11 050 мкс 51 170 мкс 380 мкс 0
I²C 400 кГц 162,5 мкс 2762,5 мкс 47 592,5 мкс 95 мкс 0
I²C 1000 кГц (вне PCF8574) 65 мкс 1105 мкс 46 877 мкс 38 мкс 10

На 1 МГц посылка короче времени записи, и модель теряет 10 полубайтов — шина задаёт паузу, пока она медленнее контроллера. Байты можно экономить: выставлять данные вместе с фронтом E (два байта на полубайт) и слать всю строку одной посылкой — 6230 мкс на 100 кГц и 1557,5 мкс на 400 кГц. Но тогда между спадами E на 400 кГц остаётся 45 мкс — всего на 4 мкс больше времени записи, а RS меняется одновременно с фронтом E, без предписанной установки. Три байта на полубайт надёжнее.

Процессор. Опрос флагов I²C держит процессор всю посылку. Если перерисовывать обе строки 10 раз в секунду, это 1,8 % времени процессора через GPIO, 22,1 % через I²C на 100 кГц и 5,5 % на 400 кГц. Поэтому перерисовывают только изменившееся, не чаще нескольких раз в секунду (глаз всё равно не успевает читать), а для фоновой работы отдают I²C прерываниям или DMA.

Прошивка раздела fw22f.c — инициализация, восемь своих знаков и две строки по-русски: через GPIO — 1024 байта кода и 16 байт данных, через I²C — 1260 байт кода, 16 байт данных и 16 байт .bss.


22д.11 Грабли

Экран пустой или в первой строке чёрные прямоугольники. Контраст: покрутите потенциометр на VO. Прямоугольники при пустой второй строке — контроллер не инициализирован: проверьте RS, E и D4–D7.

После перепрошивки мусор, после выключения питания — всё в порядке. Инициализация одним 0x2: дисплей остался в 4 битах. Три 0x3 с паузами 4,1 мс и 100 мкс перед 0x2.

Видна только первая строка. Режим шины 0x20 вместо 0x28 или полубайт потерян при инициализации — контроллер собрал 0x22.

Текст пропадает или съезжает после очистки. После clear и home нет паузы 2 мс.

На 72 МГц мусор, на 8 МГц работало. Строб E короче 450 нс или данные меняются у самого спада. Паузы в микросекундах, а не пустые команды.

Русский текст — японские знаки. ПЗУ A00 без кириллицы, строка в UTF-8. Свои знаки в CGRAM и замена похожими латинскими.

Свои знаки «поплыли» после вывода текста. После загрузки CGRAM не вызван lcd_goto — текст ушёл в CGRAM.

Свой знак с кодом 0 обрывает строку. Используйте коды 8…15.

Текст длиннее 16 знаков не переносится на вторую строку. Строка в памяти — 40 ячеек; на вторую — lcd_goto(1, 0).

Переходник I²C молчит. Адрес 0x3F вместо 0x27 (PCF8574A) или адрес передан без сдвига: в линии 0x4E. Другая разводка платы — таблица pcf_map_t.

Подсветка гаснет при первой записи. Бит подсветки не добавлен к каждому байту PCF8574.

Чтение флага занятости сожгло вывод. Модуль на 5 В выдал единицу на PA4–PA7 без допуска 5 В. RW — на землю, ожидание — паузой.


22д.12 Итоги

Полные исходники

lcd22f.h — линии, команды, порт, кириллица, PCF8574.

/* lcd22f.h — раздел 22д: символьный ЖК-дисплей на HD44780 в 4 битах, без регистров.
 * Одна логика для двух подключений: выводы GPIO и переходник I2C на PCF8574.
 * Тот же код — в прошивке fw22f.c и в стенде sim22f.c. */
#ifndef LCD22F_H
#define LCD22F_H
#include <stdint.h>

/* ── логические линии: биты байта, который драйвер отдаёт порту ───────── */
#define LCD_RS 0x01u            /* 0 — команда, 1 — данные */
#define LCD_RW 0x02u            /* всегда 0: только запись, RW на земле */
#define LCD_E  0x04u            /* строб: полубайт защёлкивается по спаду */
#define LCD_BL 0x08u            /* подсветка (есть только у переходника I2C) */
/* D4…D7 — биты 4…7 */

/* ── команды ──────────────────────────────────────────────────────────── */
#define LCD_CLEAR      0x01u    /* очистка: 1,52 мс */
#define LCD_HOME       0x02u    /* курсор и сдвиг — в начало: 1,52 мс */
#define LCD_ENTRY_INC  0x06u    /* адрес растёт, экран не сдвигается */
#define LCD_DISP_OFF   0x08u
#define LCD_DISP_ON    0x0Cu    /* экран включён, курсора нет */
#define LCD_SHIFT_L    0x18u    /* окно экрана — на знакоместо влево */
#define LCD_FUNC_4B_2L 0x28u    /* 4 бита, 2 строки, знак 5×8 */
#define LCD_CGRAM      0x40u    /* | адрес CGRAM 0…63 */
#define LCD_DDRAM      0x80u    /* | адрес DDRAM */

typedef struct lcd_port {
    void (*out)(struct lcd_port *p, uint8_t bus);    /* выставить RS, RW, E, BL, D4–D7 */
    void (*flush)(struct lcd_port *p);               /* I2C: отправить накопленное; у GPIO — 0 */
    void (*delay_us)(struct lcd_port *p, uint32_t us);
    uint8_t e_us;       /* пауза до строба и длина строба: GPIO — 1, I2C — 0 (байт по шине дольше) */
    uint8_t byte_us;    /* пауза после байта: GPIO — 50 (команда 37 мкс, запись 41), I2C — 0 */
    uint8_t bl;         /* LCD_BL — подсветка включена */
} lcd_port_t;

void lcd_nibble(lcd_port_t *p, uint8_t rs, uint8_t n);
void lcd_byte(lcd_port_t *p, uint8_t rs, uint8_t v);
void lcd_cmd(lcd_port_t *p, uint8_t c);
void lcd_init(lcd_port_t *p);
void lcd_goto(lcd_port_t *p, uint8_t row, uint8_t col);
void lcd_puts(lcd_port_t *p, const char *s);
void lcd_glyph(lcd_port_t *p, uint8_t code, const uint8_t rows[8]);   /* после неё — lcd_goto */

/* ── кириллица: UTF-8 → коды знакогенератора A00 ──────────────────────── */
extern const uint8_t lcd_cyr_glyph[8][8];           /* Б Г Д Ж И Л П Я — коды 8…15 */
uint32_t lcd_utf8(char *dst, uint32_t max, const char *src);

/* ── PCF8574: какой вывод P0…P7 у каждой логической линии ─────────────── */
typedef struct {
    uint8_t pin[8];     /* для RS, RW, E, BL, D4, D5, D6, D7 */
} pcf_map_t;
extern const pcf_map_t PCF_MAP_COMMON;
uint8_t pcf_byte(const pcf_map_t *m, uint8_t bus);

#endif

lcd22f.c — драйвер без регистров: общая часть прошивки и стенда.

/* lcd22f.c — раздел 22д: HD44780 в 4 битах — инициализация, команды, CGRAM, кириллица, PCF8574 (без регистров). */
#include "lcd22f.h"

void lcd_nibble(lcd_port_t *p, uint8_t rs, uint8_t n)
{
    uint8_t bus = (uint8_t)((n & 0x0Fu) << 4) | (rs ? LCD_RS : 0u) | p->bl;
    p->out(p, bus);                         /* RS и данные — раньше строба */
    if (p->e_us)
        p->delay_us(p, p->e_us);
    p->out(p, bus | LCD_E);
    if (p->e_us)
        p->delay_us(p, p->e_us);            /* E в единице: не меньше 450 нс при 3 В */
    p->out(p, bus);                         /* спад E: контроллер защёлкивает полубайт */
}

void lcd_byte(lcd_port_t *p, uint8_t rs, uint8_t v)
{
    lcd_nibble(p, rs, v >> 4);              /* старший полубайт первым */
    lcd_nibble(p, rs, v & 0x0Fu);
    if (p->flush)
        p->flush(p);                        /* I2C: байт для дисплея — одна посылка */
    if (p->byte_us)
        p->delay_us(p, p->byte_us);         /* занят: 37 мкс после команды, 41 после записи */
}

void lcd_cmd(lcd_port_t *p, uint8_t c)
{
    lcd_byte(p, 0, c);
    if (c == LCD_CLEAR || (c & 0xFEu) == LCD_HOME)
        p->delay_us(p, 2000u);              /* clear и home — 1,52 мс */
}

static void lcd_nibble_wait(lcd_port_t *p, uint8_t n, uint32_t us)
{
    lcd_nibble(p, 0, n);
    if (p->flush)
        p->flush(p);
    p->delay_us(p, us);
}

void lcd_init(lcd_port_t *p)
{
    p->delay_us(p, 40000u);                 /* 40 мс после подачи питания (при 5 В хватит 15) */
    lcd_nibble_wait(p, 0x3u, 4100u);        /* 0x30 «8 бит» трижды: из любого состояния */
    lcd_nibble_wait(p, 0x3u, 100u);         /* контроллер приходит в 8-битный режим */
    lcd_nibble_wait(p, 0x3u, 100u);
    lcd_nibble_wait(p, 0x2u, 100u);         /* 0x20 в 8 битах: переход на 4 бита */
    lcd_cmd(p, LCD_FUNC_4B_2L);             /* теперь — байтами из двух полубайтов */
    lcd_cmd(p, LCD_DISP_OFF);
    lcd_cmd(p, LCD_CLEAR);
    lcd_cmd(p, LCD_ENTRY_INC);
    lcd_cmd(p, LCD_DISP_ON);
}

void lcd_goto(lcd_port_t *p, uint8_t row, uint8_t col)
{
    static const uint8_t row_addr[4] = {0x00u, 0x40u, 0x14u, 0x54u};   /* 1602, 1604 и 2004 */
    lcd_cmd(p, (uint8_t)(LCD_DDRAM | (row_addr[row & 3u] + col)));
}

void lcd_puts(lcd_port_t *p, const char *s)
{
    while (*s)
        lcd_byte(p, 1, (uint8_t)*s++);
}

void lcd_glyph(lcd_port_t *p, uint8_t code, const uint8_t rows[8])
{
    lcd_cmd(p, (uint8_t)(LCD_CGRAM | ((code & 7u) << 3)));   /* 8 строк на знак */
    for (uint32_t i = 0; i < 8u; i++)
        lcd_byte(p, 1, rows[i]);
    /* адрес остался в CGRAM: следующая запись испортит знак, пока не будет lcd_goto */
}

/* Б Г Д Ж И Л П Я — строки 5×8, бит 4 — левая точка. В ПЗУ A00 этих букв нет. */
const uint8_t lcd_cyr_glyph[8][8] = {
    {0x1F, 0x10, 0x10, 0x1E, 0x11, 0x11, 0x1E, 0x00},   /* Б */
    {0x1F, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x00},   /* Г */
    {0x06, 0x0A, 0x0A, 0x0A, 0x0A, 0x1F, 0x11, 0x00},   /* Д */
    {0x15, 0x15, 0x0E, 0x04, 0x0E, 0x15, 0x15, 0x00},   /* Ж */
    {0x11, 0x11, 0x13, 0x15, 0x19, 0x11, 0x11, 0x00},   /* И */
    {0x07, 0x09, 0x09, 0x09, 0x09, 0x09, 0x11, 0x00},   /* Л */
    {0x1F, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x00},   /* П */
    {0x0F, 0x11, 0x11, 0x0F, 0x05, 0x09, 0x11, 0x00},   /* Я */
};

/* А…Я (U+0410…U+042F): похожая латинская буква, свой знак 8…15 или «?».
 * Коды 8…15 — те же знаки CGRAM, что 0…7, но ноль в строке C — её конец. */
static const char lcd_cyr[32] = {
    'A', 8, 'B', 9, 10, 'E', 11, '3', 12, '?', 'K', 13, 'M', 'H', 'O', 14,
    'P', 'C', 'T', 'Y', '?', 'X', '?', '?', '?', '?', '?', '?', 'b', '?', '?', 15,
};

uint32_t lcd_utf8(char *dst, uint32_t max, const char *src)
{
    const uint8_t *s = (const uint8_t *)src;
    uint32_t n = 0;
    while (*s && n + 1u < max) {
        uint8_t c = *s++;
        if (c < 0x80u) {
            dst[n++] = (char)c;
            continue;
        }
        uint32_t u = 0;
        if ((c & 0xE0u) == 0xC0u && (*s & 0xC0u) == 0x80u)
            u = ((uint32_t)(c & 0x1Fu) << 6) | (*s++ & 0x3Fu);
        while ((*s & 0xC0u) == 0x80u)       /* хвост длинных последовательностей — мимо */
            s++;
        if (u >= 0x410u && u <= 0x42Fu)
            dst[n++] = lcd_cyr[u - 0x410u];
        else if (u == 0x401u)               /* Ё */
            dst[n++] = 'E';
        else if (u == 0xB0u)                /* знак градуса — в ПЗУ A00 код 0xDF */
            dst[n++] = (char)0xDF;
        else
            dst[n++] = '?';
    }
    dst[n] = 0;
    return n;
}

/* Распространённая плата: P0 RS, P1 RW, P2 E, P3 подсветка, P4…P7 — D4…D7. Бывают и другие. */
const pcf_map_t PCF_MAP_COMMON = {{0, 1, 2, 3, 4, 5, 6, 7}};

uint8_t pcf_byte(const pcf_map_t *m, uint8_t bus)
{
    uint8_t v = 0;
    for (uint32_t i = 0; i < 8u; i++)
        if (bus & (1u << i))
            v |= (uint8_t)(1u << m->pin[i]);
    return v;
}

fw22f.c — прошивка STM32F103: GPIO или PCF8574 на I2C1.

/* fw22f.c — раздел 22д: LCD1602 (HD44780) от STM32F103 (BluePill), 72 МГц.
 * Способ 1 (основной): 4 бита на GPIO — RS PA0, E PA1, D4–D7 PA4–PA7 (как в стенде «LCD1602»), RW на земле.
 * Способ 2 (LCD22F_I2C): переходник на PCF8574 через I2C1 (PB6 — SCL, PB7 — SDA), 100 кГц, адрес 0x27.
 * Тактирование до 72 МГц — как в разделе 6 (здесь не повторяется); задержки — по счётчику тактов DWT. */
#include "stm32f10x.h"
#include "lcd22f.h"

#define CPU_MHZ 72u

static void delay_us(lcd_port_t *p, uint32_t us)
{
    (void)p;
    uint32_t t0 = DWT->CYCCNT;
    while (DWT->CYCCNT - t0 < us * CPU_MHZ)
        ;
}

#ifndef LCD22F_I2C
/* ── способ 1: шесть выводов GPIO ─────────────────────────────────────── */
static void gpio_out(lcd_port_t *p, uint8_t bus)
{
    (void)p;
    uint32_t set = ((bus & LCD_RS) ? 1u : 0u)           /* RS → PA0 */
                 | ((bus & LCD_E) ? 2u : 0u)            /* E  → PA1 */
                 | (bus & 0xF0u);                       /* D4–D7 → PA4–PA7 */
    GPIOA->BSRR = (0xF3u << 16) | set;                  /* сброс и установка одной записью */
}

static lcd_port_t lcd = {gpio_out, 0, delay_us, 1u, 50u, 0u};

static void port_init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
    /* PA0, PA1, PA4–PA7: выход push-pull 2 МГц (MODE = 10, CNF = 00); PA2, PA3 — как после сброса */
    GPIOA->CRL = 0x22224422u;
}
#else
/* ── способ 2: PCF8574 на I2C1 ────────────────────────────────────────── */
#define PCF_ADDR 0x27u                                  /* PCF8574, A2–A0 = 111; у PCF8574A — 0x3F */
static uint8_t i2c_buf[8];
static uint32_t i2c_n;
static volatile uint32_t i2c_nack;                      /* сколько посылок осталось без ответа */

static void i2c_out(lcd_port_t *p, uint8_t bus)
{
    (void)p;
    if (i2c_n < sizeof i2c_buf)
        i2c_buf[i2c_n++] = pcf_byte(&PCF_MAP_COMMON, bus);
}

static void i2c_flush(lcd_port_t *p)
{
    (void)p;
    if (!i2c_n)
        return;
    I2C1->CR1 |= I2C_CR1_START;
    while (!(I2C1->SR1 & I2C_SR1_SB))
        ;
    I2C1->DR = PCF_ADDR << 1;                           /* 0x4E: адрес и бит записи */
    while (!(I2C1->SR1 & (I2C_SR1_ADDR | I2C_SR1_AF)))
        ;
    if (I2C1->SR1 & I2C_SR1_AF) {                       /* никто не ответил: не тот адрес */
        I2C1->SR1 &= ~I2C_SR1_AF;
        I2C1->CR1 |= I2C_CR1_STOP;
        i2c_nack++;
        i2c_n = 0;
        return;
    }
    (void)I2C1->SR2;                                    /* чтение SR2 снимает ADDR */
    for (uint32_t i = 0; i < i2c_n; i++) {
        while (!(I2C1->SR1 & I2C_SR1_TXE))
            ;
        I2C1->DR = i2c_buf[i];                          /* каждый байт — новое состояние P0…P7 */
    }
    while (!(I2C1->SR1 & I2C_SR1_BTF))
        ;
    I2C1->CR1 |= I2C_CR1_STOP;
    i2c_n = 0;
}

static lcd_port_t lcd = {i2c_out, i2c_flush, delay_us, 0u, 0u, LCD_BL};

static void port_init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
    RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
    /* PB6, PB7: альтернативная функция, открытый сток, 50 МГц (CNF = 11, MODE = 11) */
    GPIOB->CRL = (GPIOB->CRL & ~(0xFFu << 24)) | (0xFFu << 24);
    I2C1->CR2 = 36u;                                    /* APB1 36 МГц */
    I2C1->CCR = 180u;                                   /* 36 МГц / (2 × 100 кГц) */
    I2C1->TRISE = 37u;
    I2C1->CR1 = I2C_CR1_PE;
}
#endif

int main(void)
{
    char s[17];
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;     /* счётчик тактов DWT для задержек */
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
    port_init();
    lcd_init(&lcd);
    for (uint8_t k = 0; k < 8u; k++)
        lcd_glyph(&lcd, k, lcd_cyr_glyph[k]);           /* Б Г Д Ж И Л П Я → коды 8…15 */
    lcd_goto(&lcd, 0, 0);
    lcd_utf8(s, sizeof s, "ПРИВЕТ, ДРУГ!");
    lcd_puts(&lcd, s);
    lcd_goto(&lcd, 1, 0);
    lcd_utf8(s, sizeof s, "ТЕМП. 23,5°C");
    lcd_puts(&lcd, s);
    for (;;)
        ;
}

Дальше: раздел 22е «Драйверы индикаторов TM1637 и MAX7219: свой двухпроводный протокол, SPI-слова, яркость, каскад матриц 8×8».

Открыть в интерактивном справочнике