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

22з. Цветной TFT-дисплей ST7735 по SPI: инициализация, окно адресов, RGB565, шрифт, DMA и частичное обновление

В разделе 22д контроллер HD44780 сам рисовал знаки, а программа слала ему коды. Цветной TFT 128×160 на контроллере ST7735 устроен так же — память кадра стоит в самом модуле, — только знаков он не знает: каждую точку рисует программа, и точек 20 480 по два байта. Общий взгляд на такие дисплеи — в разделе 22а (два пути к стеклу, время кадра по SPI), грабли прототипа с порядком байтов и памятью — в 83.3. Здесь — конкретный модуль на STM32F103 от первой команды до перерисовки одной цифры: выводы и SPI1, инициализация, окно адресов, RGB565, прямоугольник, линия и шрифт, DMA, кадры в секунду и частичное обновление, когда буфер кадра в ОЗУ не помещается.

Опоры: RM0008 (DocID13902 Rev 17): запрос SPI1_TX — канал 3 DMA1, с. 281; биты MSIZE и MINC, с. 285; CNDTR — до 65 535, с. 286; передача SPI через DMA и ожидание TXE, потом BSY, с. 719; бит DFF — только при SPE = 0, с. 742; делитель BR, с. 743. DocID13587 Rev 17 (STM32F103x8/xB): 20 Кбайт ОЗУ, с. 1; PA1–PA7 без допуска 5 В, с. 29; такт SPI ведущего — до 18 МГц, с. 71. Документа на ST7735 в проекте нет. Коды команд, размер 128×160, значение COLMOD для RGB565, четыре значения MADCTL для поворотов и порядок «старший байт первым» — из драйвера ST в паках Keil (STM32G4xx DFP 1.6.0, Drivers/BSP/Components/st7735, версия 2.0.0); режим SPI и предел записи 15 МГц — из файла платы stm32f4xx_nucleo.c (STM32F4xx DFP 2.17.1), где этот дисплей подключён к Nucleo. Назначение битов MADCTL, паузы после сброса и сна, размер памяти контроллера и сдвиг стекла — по документации производителя, не проверено: в модели это допущения. Все числа ниже — прогон драйвера _verify/tft22i/tft22i.c на компьютере (gcc 12.2.0) со своей моделью контроллера и стекла на Python 3.11; прошивка собрана armclang 6.24 под Cortex-M3.

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

Оригинал Перевод
D/C (data/command), A0, RS выбор: команда или данные
frame memory, GRAM память кадра в контроллере
CASET / RASET окно: столбцы / строки
RAMWR (memory write) запись в память кадра
MADCTL (memory data access control) порядок обхода памяти: зеркала, поворот, RGB или BGR
COLMOD (interface pixel format) формат пикселя на входе
SWRESET, SLPOUT, DISPON программный сброс, выход из сна, включение показа
backlight подсветка
transmit-only, TXE, BSY только передача; буфер передачи пуст; блок занят
dirty rectangle «грязный» прямоугольник — что изменилось с прошлого кадра

Вверху — подключение модуля ST7735 к STM32F103: SPI1 только на передачу, 9 МГц, режим 3; PA5 — SCK, PA7 — MOSI, PA4 — CS, который держат в нуле, PA3 — D/C, 0 — команда, 1 — данные, PA2 — RST, PA1 — подсветка; питание модуля 3,3 В, выводы PA4–PA7 без допуска 5 В, пиксели идут через DMA1 канал 3 словами по 16 бит, старший байт первым. Посередине — запись окна: при D/C = 0 команда 2A CASET, при D/C = 1 её параметры 00 02 00 81 — столбцы 2…129, команда 2B RASET и параметры 00 01 00 A0 — строки 1…160, команда 2C RAMWR и дальше 20 480 пикселей RGB565; окно — 11 байт, весь экран 40 960 байт плюс 11, 18,21 мс на 18 МГц. Внизу — память кадра контроллера 132 × 162 и стекло 128 × 160 со сдвигом 2 и 1, при повороте сдвиги меняются местами; ОЗУ F103C8 20 480 байт и один кадр 40 960 байт в одном масштабе: кадр и тем более двойной буфер не помещаются, две строки по 640 байт — помещаются


22з.1 Модуль и подключение: выводы, D/C и SPI1

Модуль — стекло 128×160 и контроллер ST7735 с памятью кадра: что записано, то он и показывает сам, без участия микроконтроллера (путь «дисплей со своим контроллером» из 22а.1). Формат пикселя задаёт COLMOD: 0x03 — 12 бит, 0x05 — RGB565, 0x06 — 18 бит; здесь RGB565, два байта на точку. Выводы модуля: питание, земля, такт и данные SPI, выбор CS, линия D/C, сброс RST и подсветка; подписи на платах разные — данные бывают подписаны SDA, D/C — A0 или RS. Чтения нет: MISO не нужен, и SPI1 работает только на передачу.

Модуль F103 Что
SCK PA5 такт SPI1
MOSI (SDA) PA7 данные SPI1
CS PA4 выбор: на шине один дисплей — держим в нуле
D/C PA3 0 — байт команды, 1 — параметры и пиксели
RST PA2 сброс
подсветка PA1 светодиоды подсветки

D/C — то же, что RS у HD44780 (22д): тот же байт значит команду или данные. PA1–PA7 у F103 5 В не допускают (с. 29), так что модуль питают от 3,3 В; есть ли на модуле свой стабилизатор и каков ток подсветки — по описанию модуля, здесь не проверялось.

SPI1. Он на APB2, 72 МГц; делитель BR (с. 743) даёт 36, 18 или 9 МГц при /2, /4, /8. Предел такта ведущего у F103 — 18 МГц (с. 71), у ST7735 на запись — 15 МГц по комментарию в файле платы ST. Прошивка берёт /8 — 9 МГц, внутри обоих пределов. Режим — 3 (CPOL = 1, CPHA = 1), как в том же файле ST. Команды — по 8 бит, пиксели — словами по 16 бит (DFF = 1): 16-битное слово SPI уходит старшим байтом вперёд, и переставлять байты цвета, как в 83.3, не нужно.

Главное правило шины: D/C и DFF трогают, только когда прошлый байт ушёл целиком. По флагу DMA последнее слово только легло в буфер передачи; конец — TXE = 1, а потом BSY = 0 (с. 719). DFF меняют только при выключенном SPI (с. 742):

static void spi_idle(void)
{
    if (DMA1_Channel3->CCR & DMA_CCR1_EN) {
        while (!(DMA1->ISR & DMA_ISR_TCIF3)) {
        }
        DMA1->IFCR = DMA_IFCR_CTCIF3;
        DMA1_Channel3->CCR = 0;
    }
    while (!(SPI1->SR & SPI_SR_TXE)) {
    }
    while (SPI1->SR & SPI_SR_BSY) {
    }
    (void)SPI1->DR;                     /* приёма нет: снять RXNE и OVR (раздел 18.6) */
    (void)SPI1->SR;
}

static void spi_width(uint8_t w16)      /* DFF — только при выключенном SPI (RM0008, с. 742) */
{
    if (wide == w16)
        return;
    SPI1->CR1 &= ~SPI_CR1_SPE;
    if (w16)
        SPI1->CR1 |= SPI_CR1_DFF;
    else
        SPI1->CR1 &= ~SPI_CR1_DFF;
    SPI1->CR1 |= SPI_CR1_SPE;
    wide = w16;
}

С этого начинается каждая функция шины: байт команды, параметры, строка пикселей через DMA. Драйвер tft22i.c регистров не знает — он зовёт шину через таблицу функций, и тот же код в стенде печатает байты для модели. Для сравнения: монохромный SSD1306 128 × 64 в сканере 198р — весь экран 1024 байта, а здесь 40 960: в 40 раз больше.


22з.2 Инициализация: SWRESET, SLPOUT, COLMOD, MADCTL

void tft_init(tft_t *t, const tft_bus_t *bus, uint8_t rot, uint8_t bgr, uint8_t col0, uint8_t row0)
{
    static const uint8_t rgb565 = 0x05u;
    t->bus = bus;
    t->col0 = col0;
    t->row0 = row0;
    t->madctl = bgr ? MAD_BGR : 0u;
    bus->reset();
    bus->cmd(ST_SWRESET);
    bus->delay_ms(150);                                 /* с запасом: документа производителя в проекте нет */
    bus->cmd(ST_SLPOUT);
    bus->delay_ms(120);
    bus->cmd(ST_COLMOD);
    bus->data(&rgb565, 1);                              /* 16 бит на пиксель */
    tft_rotate(t, rot);
    bus->cmd(ST_NORON);
    bus->cmd(ST_DISPON);
}

Импульс RST, программный сброс, выход из сна, формат пикселя, порядок обхода памяти, обычный режим и включение показа: в прогоне 6 команд и 8 байт, на проводе 7,1 мкс при 9 МГц. Время запуска — паузы: 150 мс после SWRESET и 120 мс после SLPOUT, всего 270 мс. Сколько нужно на самом деле, написано в документе производителя; паузы здесь с запасом и не проверены. Драйвер ST из пака обходится без SWRESET и без пауз, зато задаёт ещё частоту кадров (B1–B3), режим инверсии (B4), питание (C0–C4), VCOM (C5) и гамму (E0, E1) — значения этих регистров зависят от стекла. Короткая последовательность полагается на их значения после сброса — на настоящем модуле это не проверялось.

MADCTL. Биты: MY (0x80) — зеркало строк, MX (0x40) — зеркало столбцов, MV (0x20) — обмен строк и столбцов, то есть поворот на 90°, BGR (0x08) — стекло с порядком синий-зелёный-красный. tft_rotate берёт для четырёх поворотов 0x00, 0x60, 0xC0 и 0xA0 — те же четыре значения, что в таблице поворотов драйвера ST; какое из них «прямо», зависит от того, как стекло стоит на модуле. С битом BGR модель получила 0x08, 0x68, 0xC8 и 0xA8, и во всех четырёх поворотах картинка совпала с эталоном точка в точку. При MV ширина и высота меняются местами (160×128), а с ними и сдвиг стекла — о нём 22з.7.


22з.3 Окно CASET/RASET, RAMWR и RGB565

void tft_window(const tft_t *t, int16_t x, int16_t y, int16_t w, int16_t h)
{
    uint16_t x0 = (uint16_t)(x + t->xoff), x1 = (uint16_t)(x0 + w - 1);
    uint16_t y0 = (uint16_t)(y + t->yoff), y1 = (uint16_t)(y0 + h - 1);
    uint8_t ca[4] = {(uint8_t)(x0 >> 8), (uint8_t)x0, (uint8_t)(x1 >> 8), (uint8_t)x1};
    uint8_t ra[4] = {(uint8_t)(y0 >> 8), (uint8_t)y0, (uint8_t)(y1 >> 8), (uint8_t)y1};
    t->bus->cmd(ST_CASET);
    t->bus->data(ca, 4);
    t->bus->cmd(ST_RASET);
    t->bus->data(ra, 4);
    t->bus->cmd(ST_RAMWR);                              /* дальше — пиксели слева направо, сверху вниз */
}

Окно — 11 байт: команда и два 16-битных края для столбцов, то же для строк, и RAMWR. Дальше пиксели идут подряд, сколько угодно, пока не придёт новая команда; дойдя до правого края окна, запись переходит на следующую строку. Весь экран модуля со сдвигом стекла 2 и 1 — CASET 00 02 00 81 (столбцы 2…129) и RASET 00 01 00 A0 (строки 1…160).

RGB565. Перевод из 24-битного цвета — функция из 83.3: старшие 5, 6 и 5 бит. Оранжевый (255, 128, 0) — FC00. Отбрасывание младших битов — не то же, что округление: серый 200 даёт CE59, а с округлением (v · 31 + 127) / 255 — C638. Обратно в 8 бит (старшие биты повторяются в младших) это 206, 203, 206 против 198, 199, 198. По всем 256 уровням наибольшая ошибка отбрасывания — 7, 3 и 7 единиц по красному, зелёному и синему, округления — 4, 2 и 4. Для заливок разница не видна, для плавных переходов цвета округление честнее.


22з.4 Прямоугольник, линия и шрифт 5×7

Прямоугольник — окно и w · h слов одного цвета. DMA шлёт одно и то же слово без шага по памяти (MINC = 0, с. 285), так что памяти под заливку не нужно вовсе. Точка — прямоугольник 1×1: 11 байт окна ради 2 байт цвета.

Линия — Брезенхем. Точки те же, что у классического алгоритма, но идущие подряд по главной оси уходят одним прямоугольником:

void tft_line(const tft_t *t, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t c)
{
    int dx = x1 > x0 ? x1 - x0 : x0 - x1, sx = x0 < x1 ? 1 : -1;
    int dy = y1 > y0 ? y0 - y1 : y1 - y0, sy = y0 < y1 ? 1 : -1;
    int err = dx + dy, flat = dx >= -dy;
    int x = x0, y = y0, rx = x0, ry = y0;               /* rx, ry — начало текущего отрезка */
    for (;;) {
        int px = x, py = y, e2 = 2 * err;
        if (x == x1 && y == y1) {
            tft_fill_rect(t, (int16_t)(rx < px ? rx : px), (int16_t)(ry < py ? ry : py),
                          (int16_t)((rx > px ? rx - px : px - rx) + 1), (int16_t)((ry > py ? ry - py : py - ry) + 1), c);
            return;
        }
        if (e2 >= dy) { err += dy; x += sx; }
        if (e2 <= dx) { err += dx; y += sy; }
        if (flat ? y != py : x != px) {                 /* шаг по второй оси — отрезок кончился */
            tft_fill_rect(t, (int16_t)(rx < px ? rx : px), (int16_t)(ry < py ? ry : py),
                          (int16_t)((rx > px ? rx - px : px - rx) + 1), (int16_t)((ry > py ? ry - py : py - ry) + 1), c);
            rx = x;
            ry = y;
        }
    }
}

18 отрезков из центра экрана во все стороны — 1200 точек. Модель сверила их с классическим Брезенхемом точка в точку — расхождений 0. Ушло 8725 байт; по точке было бы 13 байт на точку — 15 600.

Шрифт — свой массив 5×7: знак — 5 байт-столбцов, бит 0 — верхняя строка, подряд от пробела до «Z» — 59 знаков, 295 байт (та же раскладка, что у шрифта в 198р). Клетка знака — 6×8 точек с промежутком, при масштабе 3 — 18×24. Знак рисуется окном на клетку и строками вместе с фоном, поэтому старый знак стирать не нужно.

Картинка проверки tft_demo — заливка фона, надпись, три прямоугольника красный, зелёный, синий, три линии и подпись — совпала с эталоном модели во всех 20 480 точках; на неё ушло 51 926 байт и 1259 переключений D/C.


22з.5 Скорость: байты, DMA и кадры в секунду

Время на проводе — байты × 8 / частота SPI, без пауз между словами; так шлёт DMA. Весь экран тремя способами:

Способ Байт 9 МГц 18 МГц 36 МГц
по точке (tft_pixel) 266 240 236,66 мс 118,33 мс 59,16 мс
окно и заливка через DMA 40 971 36,42 мс 18,21 мс 9,10 мс
кадров в секунду, по точке — 4,2 8,5 16,9
кадров в секунду, окно и DMA — 27,5 54,9 109,8

По точке — 6,5 раза больше байт и 122 879 переключений D/C против 6. Вывод через строки-буферы (22з.6) даёт те же 40 971 байт, что и заливка. 36 МГц (делитель 2) — за пределом и F103 (18 МГц), и ST7735 (15 МГц по ST): эта колонка лишь показывает потолок. Заливка всего экрана — одна передача DMA на 20 480 слов: CNDTR вмещает до 65 535 (с. 286).

Цикл с опросом TXE шлёт те же байты, но процессор занят всё время, а между словами остаются паузы (раздел 18.8) — их модель не считает. С DMA процессор свободен: пока уходит строка 128 точек на 18 МГц — 113,8 мкс, — у ядра на 72 МГц есть 8192 такта, 64 на точку, чтобы посчитать следующую строку. Поэтому строк-буферов два: DMA шлёт одну, процессор заполняет другую, а spi_idle в начале следующей передачи дожидается конца предыдущей. Для сравнения, кадр 240×320 на тех же 18 МГц — 68,27 мс (18.8): точек там в 3,75 раза больше.


22з.6 Двойной буфер не помещается: «грязные» прямоугольники

Кадр 128×160 по два байта — 40 960 байт, двойной буфер — 81 920, а ОЗУ у F103C8 — 20 Кбайт, 20 480 байт (с. 1). Не помещается и один кадр (83.3). Значит, кадр живёт только в памяти дисплея, а микроконтроллер присылает изменившиеся куски. Сцена описана функцией «цвет точки (x, y)» — meter_shade прибора из примера: заголовок, число крупным шрифтом и полоса. Перерисовка берёт список прямоугольников, считает каждый строку за строкой в два буфера по строке и шлёт через DMA:

void tft_flush(const tft_t *t, tft_dirty_t *d, tft_shader_t shade, const void *ctx)
{
    for (uint8_t k = 0; k < d->n; k++) {
        tft_rect_t r = d->r[k];
        if (!clip(t, &r.x, &r.y, &r.w, &r.h))
            continue;
        tft_window(t, r.x, r.y, r.w, r.h);
        for (int16_t y = r.y; y < r.y + r.h; y++) {
            uint16_t *p = line[y & 1];                  /* пока DMA шлёт прошлую строку */
            for (int16_t i = 0; i < r.w; i++)
                p[i] = shade(ctx, (int16_t)(r.x + i), y);
            t->bus->pixels(p, (uint32_t)r.w);
        }
    }
    d->n = 0;
}

Две строки по 160 точек — 640 байт: столько статической памяти и показал armclang у tft22i.o. Что грязно, решает сам прибор: meter_set сравнивает новый текст со старым по знакам и отмечает клетки, которые изменились, а у полосы — только полоску между старым и новым концом. tft_dirty_add сливает касающиеся прямоугольники в один.

Шаг Прямоугольники Точек Байт 18 МГц
весь экран, 23,4 1 20 480 40 971 18,21 мс
23,4 → 23,5 клетка «4» и полоска в точку 444 910 0,40 мс
23,5 → 24,9 две клетки и полоска 4 точки 912 1857 0,83 мс
24,9 → 25,0 две клетки 864 1750 0,78 мс
25,0 → 19,9 клетки «2» и «5» слиты в одну, клетка «0» и полоска 15 точек 1476 2985 1,33 мс

Сто шагов по 0,1 (от 20,0 до 29,9) — 98 129 байт, в среднем 981 байт и 0,44 мс на шаг вместо 40 971 байта и 18,21 мс на полную перерисовку. После каждого шага стекло модели совпало с эталоном прибора с тем же числом — 0 расхождений: частичное обновление ничего не теряет.

Дисплей показывает свою память сам и о наших записях не знает: клетка, которую перезаписывают в момент, когда стекло её обновляет, на один кадр может показаться наполовину старой. У ST7735 для этого есть команда TEON (0x35) — включить линию TE; модель её не считает, и если линии на модуле нет, от разрыва спасает только малый размер перерисовки.


22з.7 Грабли

Картинка сдвинута на 1–2 точки, у края полоска мусора. У модели модуля Б память 132 × 162, а стекло стоит со сдвигом 2 столбца и 1 строку. С нулевым сдвигом в драйвере картинка уезжает на 2 влево и 1 вверх, а справа и снизу видны 446 точек мусора памяти (2 × 160 + 128 − 2). Сдвиг задаётся в tft_init (col0, row0); у настоящего модуля его берут из описания или подбирают по краю картинки.

После поворота сдвиг вернулся. При MV столбцы становятся строками — и сдвиги тоже. Не поменять их местами — в альбомной ориентации картинка съезжает на точку по обеим осям, мусор — 287 точек. tft_rotate меняет их сама.

Красный стал синим. Стекло с порядком B-G-R, а бит BGR в MADCTL не поставлен: в модели модуля Б красный прямоугольник синий, синий красный, 19 202 точки не совпали. Тот же код на модуле А (стекло R-G-B, без бита) — 0. Если красный синий — переставить бит, а не цвета в программе.

Пропадают точки в конце строк, текст исчезает. D/C опускают по флагу окончания DMA: в этот момент последние слова ещё в SPI и уходят как команды. В модели при двух словах на хвосте из 24 808 точек картинки записано 22 816, контроллер получил 1008 «команд» из байтов цвета (07, 08, 1F, E0, F8, FF), а вся строка после такой «команды» пропадает целиком — от текста осталась верхняя строка точек. Лечится spi_idle: TXE = 1, потом BSY = 0 (с. 719), и только потом D/C.

Цвета «поплыли» при выводе через DMA. Массив uint16_t ушёл байтами, младшим вперёд (83.3). Пиксели — словами при DFF = 1.

DFF не переключается. Бит меняют только при SPE = 0 (с. 742) — spi_width выключает и включает SPI.

Вывод медленный. Рисуют по точке: 13 байт и шесть переключений D/C на точку; экран по точке — 118,33 мс на 18 МГц против 18,21 мс окном. Каждое переключение D/C — запись в GPIO и ожидание BSY; с медленной функцией вывода (HAL вместо BSRR) по точке выходит ещё дольше. Рисовать окнами и отрезками.


22з.8 Итоги

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

tft22i.h — команды ST7735, биты MADCTL, шина, драйвер, прибор: типы и прототипы.

/* tft22i.h — раздел 22з: цветной TFT 128×160 на контроллере ST7735 без регистров микроконтроллера.
 * Драйвер говорит с дисплеем через tft_bus_t: в прошивке fw22i.c это SPI1 + DMA1 канал 3 на F103,
 * в стенде host22i.c — печать байтов, которые читает модель контроллера model22i.py.
 * Коды команд — как в st7735_reg.h драйвера ST из паков Keil (BSP Components/st7735). */
#ifndef TFT22I_H
#define TFT22I_H
#include <stdint.h>

/* ── команды ST7735 ───────────────────────────────────────────────────── */
#define ST_SWRESET 0x01u                /* программный сброс */
#define ST_SLPOUT  0x11u                /* выход из сна */
#define ST_NORON   0x13u                /* обычный режим (не частичный) */
#define ST_DISPON  0x29u                /* включить показ */
#define ST_CASET   0x2Au                /* окно: столбцы от и до */
#define ST_RASET   0x2Bu                /* окно: строки от и до */
#define ST_RAMWR   0x2Cu                /* дальше — пиксели в окно */
#define ST_MADCTL  0x36u                /* порядок обхода памяти и цветов */
#define ST_COLMOD  0x3Au                /* формат пикселя: 0x05 — RGB565 */

/* ── MADCTL: биты зеркал, обмена осей и порядка цветов ────────────────── */
#define MAD_MY  0x80u                   /* зеркало строк */
#define MAD_MX  0x40u                   /* зеркало столбцов */
#define MAD_MV  0x20u                   /* обмен строк и столбцов: поворот на 90° */
#define MAD_BGR 0x08u                   /* стекло с порядком B-G-R */

#define TFT_W 128u                      /* стекло в книжной ориентации */
#define TFT_H 160u
#define TFT_LINE_MAX 160u               /* самая длинная строка — в альбомной */
#define TFT_DIRTY_MAX 4u                /* «грязных» прямоугольников до слияния в один */

/* Шина: каждая функция сама дожидается конца предыдущей передачи (DMA, TXE, BSY) и только потом
 * трогает D/C. pixels и fill могут вернуться раньше, чем байты ушли: передачу ведёт DMA. */
typedef struct {
    void (*reset)(void);                                /* RST в 0 и обратно */
    void (*delay_ms)(uint32_t ms);
    void (*cmd)(uint8_t c);                             /* D/C = 0: байт команды */
    void (*data)(const uint8_t *p, uint32_t n);         /* D/C = 1: байты параметров */
    void (*pixels)(const uint16_t *px, uint32_t n);     /* D/C = 1: n пикселей, старший байт первым */
    void (*fill)(uint16_t color, uint32_t n);           /* D/C = 1: n пикселей одного цвета */
} tft_bus_t;

typedef struct {
    const tft_bus_t *bus;
    uint16_t w, h;                      /* размер после поворота */
    uint8_t madctl;
    uint8_t col0, row0;                 /* сдвиг стекла в памяти контроллера (книжная ориентация) */
    uint8_t xoff, yoff;                 /* тот же сдвиг для текущего поворота */
} tft_t;

typedef struct { int16_t x, y, w, h; } tft_rect_t;
typedef struct { tft_rect_t r[TFT_DIRTY_MAX]; uint8_t n; } tft_dirty_t;
typedef uint16_t (*tft_shader_t)(const void *ctx, int16_t x, int16_t y);  /* цвет точки сцены */

uint16_t tft_rgb(uint8_t r, uint8_t g, uint8_t b);        /* RGB888 → RGB565 отбрасыванием младших битов */
uint16_t tft_rgb_round(uint8_t r, uint8_t g, uint8_t b);  /* … с округлением */

void tft_init(tft_t *t, const tft_bus_t *bus, uint8_t rot, uint8_t bgr, uint8_t col0, uint8_t row0);
void tft_rotate(tft_t *t, uint8_t rot);                   /* 0…3: книжная, альбомная, 180°, 270° */
void tft_window(const tft_t *t, int16_t x, int16_t y, int16_t w, int16_t h);
void tft_fill_rect(const tft_t *t, int16_t x, int16_t y, int16_t w, int16_t h, uint16_t c);
void tft_pixel(const tft_t *t, int16_t x, int16_t y, uint16_t c);
void tft_line(const tft_t *t, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t c);
int tft_glyph(char ch, int col, int row);                 /* точка знака 5×7: 1 — горит */
void tft_text(const tft_t *t, int16_t x, int16_t y, const char *s, uint8_t scale, uint16_t fg, uint16_t bg);

void tft_dirty_add(tft_dirty_t *d, tft_rect_t r);
void tft_flush(const tft_t *t, tft_dirty_t *d, tft_shader_t shade, const void *ctx);

/* ── пример: прибор с числом и полосой, перерисовка только изменившегося ── */
#define METER_X 4                       /* число: масштаб 3, знак 18×24 */
#define METER_Y 40
#define METER_CELL_W 18
#define METER_CELL_H 24
#define METER_CHARS 6
#define BAR_X 4                         /* полоса: рамка 120×16, внутри до 116 точек */
#define BAR_Y 100
#define BAR_W 120
#define BAR_H 16
typedef struct {
    char text[METER_CHARS + 1];
    uint16_t bar;                       /* длина полосы, 0…BAR_W − 4 */
    uint16_t fg, bg, accent;
} meter_t;
uint16_t meter_shade(const void *ctx, int16_t x, int16_t y);
void meter_set(meter_t *m, tft_dirty_t *d, const char *text, uint16_t bar);
void meter_value(meter_t *m, tft_dirty_t *d, int tenths);    /* «dd.d C» и полоса: 40,0 — вся */
void tft_demo(const tft_t *t);                                  /* картинка для проверки: прямоугольники, линии, текст */

#endif

tft22i.c — драйвер: запуск, окно, прямоугольник, линия, шрифт 5×7, «грязные» прямоугольники и прибор — общий для прошивки и стенда.

/* tft22i.c — раздел 22з: драйвер ST7735 (инициализация, окно, рисование, шрифт 5×7, «грязные»
 * прямоугольники) без регистров микроконтроллера: общий для прошивки fw22i.c и стенда host22i.c. */
#include "tft22i.h"

static uint16_t line[2][TFT_LINE_MAX];  /* две строки: DMA шлёт одну, процессор готовит другую */

/* Шрифт 5×7: знак — 5 столбцов, бит 0 — верхняя строка; подряд от пробела до «Z». */
static const uint8_t font5x7['Z' - ' ' + 1][5] = {
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x43, 0x33, 0x08, 0x66, 0x61},   /* % */
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x08, 0x08, 0x08, 0x00},   /* - */
    {0x00, 0x60, 0x60, 0x00, 0x00},   /* . */
    {0x40, 0x30, 0x08, 0x06, 0x01},   /* / */
    {0x3E, 0x51, 0x49, 0x45, 0x3E},   /* 0 */
    {0x00, 0x42, 0x7F, 0x40, 0x00},   /* 1 */
    {0x42, 0x61, 0x51, 0x49, 0x46},   /* 2 */
    {0x21, 0x41, 0x45, 0x4B, 0x31},   /* 3 */
    {0x18, 0x14, 0x12, 0x7F, 0x10},   /* 4 */
    {0x27, 0x45, 0x45, 0x45, 0x39},   /* 5 */
    {0x3C, 0x4A, 0x49, 0x49, 0x30},   /* 6 */
    {0x01, 0x71, 0x09, 0x05, 0x03},   /* 7 */
    {0x36, 0x49, 0x49, 0x49, 0x36},   /* 8 */
    {0x06, 0x49, 0x49, 0x29, 0x1E},   /* 9 */
    {0x00, 0x36, 0x36, 0x00, 0x00},   /* : */
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x7E, 0x09, 0x09, 0x09, 0x7E},   /* A */
    {0x7F, 0x49, 0x49, 0x49, 0x36},   /* B */
    {0x3E, 0x41, 0x41, 0x41, 0x22},   /* C */
    {0x7F, 0x41, 0x41, 0x22, 0x1C},   /* D */
    {0x7F, 0x49, 0x49, 0x49, 0x41},   /* E */
    {0x7F, 0x09, 0x09, 0x09, 0x01},   /* F */
    {0x3E, 0x41, 0x49, 0x49, 0x7A},   /* G */
    {0x7F, 0x08, 0x08, 0x08, 0x7F},   /* H */
    {0x00, 0x41, 0x7F, 0x41, 0x00},   /* I */
    {0x20, 0x40, 0x41, 0x3F, 0x01},   /* J */
    {0x7F, 0x08, 0x14, 0x22, 0x41},   /* K */
    {0x7F, 0x40, 0x40, 0x40, 0x40},   /* L */
    {0x7F, 0x02, 0x0C, 0x02, 0x7F},   /* M */
    {0x7F, 0x04, 0x08, 0x10, 0x7F},   /* N */
    {0x3E, 0x41, 0x41, 0x41, 0x3E},   /* O */
    {0x7F, 0x09, 0x09, 0x09, 0x06},   /* P */
    {0x3E, 0x41, 0x51, 0x21, 0x5E},   /* Q */
    {0x7F, 0x09, 0x19, 0x29, 0x46},   /* R */
    {0x46, 0x49, 0x49, 0x49, 0x31},   /* S */
    {0x01, 0x01, 0x7F, 0x01, 0x01},   /* T */
    {0x3F, 0x40, 0x40, 0x40, 0x3F},   /* U */
    {0x1F, 0x20, 0x40, 0x20, 0x1F},   /* V */
    {0x3F, 0x40, 0x38, 0x40, 0x3F},   /* W */
    {0x63, 0x14, 0x08, 0x14, 0x63},   /* X */
    {0x03, 0x04, 0x78, 0x04, 0x03},   /* Y */
    {0x61, 0x51, 0x49, 0x45, 0x43},   /* Z */
};

uint16_t tft_rgb(uint8_t r, uint8_t g, uint8_t b)
{
    return (uint16_t)(((r & 0xF8u) << 8) | ((g & 0xFCu) << 3) | (b >> 3));
}

uint16_t tft_rgb_round(uint8_t r, uint8_t g, uint8_t b)
{
    uint32_t r5 = (r * 31u + 127u) / 255u, g6 = (g * 63u + 127u) / 255u, b5 = (b * 31u + 127u) / 255u;
    return (uint16_t)(r5 << 11 | g6 << 5 | b5);
}

void tft_rotate(tft_t *t, uint8_t rot)
{
    static const uint8_t mad[4] = {0x00u, MAD_MV | MAD_MX, MAD_MX | MAD_MY, MAD_MV | MAD_MY};
    uint8_t m = (uint8_t)((t->madctl & MAD_BGR) | mad[rot & 3u]);
    int swap = (m & MAD_MV) != 0;                       /* MV: строки и столбцы меняются местами */
    t->madctl = m;
    t->w = (uint16_t)(swap ? TFT_H : TFT_W);
    t->h = (uint16_t)(swap ? TFT_W : TFT_H);
    t->xoff = swap ? t->row0 : t->col0;                 /* и сдвиг стекла тоже */
    t->yoff = swap ? t->col0 : t->row0;
    t->bus->cmd(ST_MADCTL);
    t->bus->data(&t->madctl, 1);
}

void tft_init(tft_t *t, const tft_bus_t *bus, uint8_t rot, uint8_t bgr, uint8_t col0, uint8_t row0)
{
    static const uint8_t rgb565 = 0x05u;
    t->bus = bus;
    t->col0 = col0;
    t->row0 = row0;
    t->madctl = bgr ? MAD_BGR : 0u;
    bus->reset();
    bus->cmd(ST_SWRESET);
    bus->delay_ms(150);                                 /* с запасом: документа производителя в проекте нет */
    bus->cmd(ST_SLPOUT);
    bus->delay_ms(120);
    bus->cmd(ST_COLMOD);
    bus->data(&rgb565, 1);                              /* 16 бит на пиксель */
    tft_rotate(t, rot);
    bus->cmd(ST_NORON);
    bus->cmd(ST_DISPON);
}

void tft_window(const tft_t *t, int16_t x, int16_t y, int16_t w, int16_t h)
{
    uint16_t x0 = (uint16_t)(x + t->xoff), x1 = (uint16_t)(x0 + w - 1);
    uint16_t y0 = (uint16_t)(y + t->yoff), y1 = (uint16_t)(y0 + h - 1);
    uint8_t ca[4] = {(uint8_t)(x0 >> 8), (uint8_t)x0, (uint8_t)(x1 >> 8), (uint8_t)x1};
    uint8_t ra[4] = {(uint8_t)(y0 >> 8), (uint8_t)y0, (uint8_t)(y1 >> 8), (uint8_t)y1};
    t->bus->cmd(ST_CASET);
    t->bus->data(ca, 4);
    t->bus->cmd(ST_RASET);
    t->bus->data(ra, 4);
    t->bus->cmd(ST_RAMWR);                              /* дальше — пиксели слева направо, сверху вниз */
}

static int clip(const tft_t *t, int16_t *x, int16_t *y, int16_t *w, int16_t *h)
{
    if (*x < 0) { *w = (int16_t)(*w + *x); *x = 0; }
    if (*y < 0) { *h = (int16_t)(*h + *y); *y = 0; }
    if (*x + *w > (int16_t)t->w) *w = (int16_t)(t->w - *x);
    if (*y + *h > (int16_t)t->h) *h = (int16_t)(t->h - *y);
    return *w > 0 && *h > 0;
}

void tft_fill_rect(const tft_t *t, int16_t x, int16_t y, int16_t w, int16_t h, uint16_t c)
{
    if (!clip(t, &x, &y, &w, &h))
        return;
    tft_window(t, x, y, w, h);
    t->bus->fill(c, (uint32_t)w * (uint32_t)h);         /* один цвет: DMA без шага по памяти */
}

void tft_pixel(const tft_t *t, int16_t x, int16_t y, uint16_t c)
{
    tft_fill_rect(t, x, y, 1, 1, c);                    /* 11 байт окна ради 2 байт цвета */
}

/* Брезенхем: точки те же, что у классического, но подряд идущие по главной оси уходят одним
 * прямоугольником — окно на отрезок, а не на точку. */
void tft_line(const tft_t *t, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t c)
{
    int dx = x1 > x0 ? x1 - x0 : x0 - x1, sx = x0 < x1 ? 1 : -1;
    int dy = y1 > y0 ? y0 - y1 : y1 - y0, sy = y0 < y1 ? 1 : -1;
    int err = dx + dy, flat = dx >= -dy;
    int x = x0, y = y0, rx = x0, ry = y0;               /* rx, ry — начало текущего отрезка */
    for (;;) {
        int px = x, py = y, e2 = 2 * err;
        if (x == x1 && y == y1) {
            tft_fill_rect(t, (int16_t)(rx < px ? rx : px), (int16_t)(ry < py ? ry : py),
                          (int16_t)((rx > px ? rx - px : px - rx) + 1), (int16_t)((ry > py ? ry - py : py - ry) + 1), c);
            return;
        }
        if (e2 >= dy) { err += dy; x += sx; }
        if (e2 <= dx) { err += dx; y += sy; }
        if (flat ? y != py : x != px) {                 /* шаг по второй оси — отрезок кончился */
            tft_fill_rect(t, (int16_t)(rx < px ? rx : px), (int16_t)(ry < py ? ry : py),
                          (int16_t)((rx > px ? rx - px : px - rx) + 1), (int16_t)((ry > py ? ry - py : py - ry) + 1), c);
            rx = x;
            ry = y;
        }
    }
}

int tft_glyph(char ch, int col, int row)
{
    if (ch >= 'a' && ch <= 'z')
        ch = (char)(ch - 'a' + 'A');
    if (ch < ' ' || ch > 'Z' || col < 0 || col > 4 || row < 0 || row > 6)
        return 0;                                       /* промежуток между знаками и неизвестные знаки */
    return (font5x7[ch - ' '][col] >> row) & 1;
}

void tft_text(const tft_t *t, int16_t x, int16_t y, const char *s, uint8_t scale, uint16_t fg, uint16_t bg)
{
    int16_t cw = (int16_t)(6 * scale), ch = (int16_t)(8 * scale);   /* знак 5×7 и промежуток в точку */
    for (; *s; s++, x = (int16_t)(x + cw)) {
        if (x < 0 || y < 0 || x + cw > (int16_t)t->w || y + ch > (int16_t)t->h)
            return;                                     /* знак целиком или никак */
        tft_window(t, x, y, cw, ch);
        for (int r = 0; r < ch; r++) {
            uint16_t *p = line[r & 1];
            for (int c = 0; c < cw; c++)
                p[c] = tft_glyph(*s, c / scale, r / scale) ? fg : bg;
            t->bus->pixels(p, (uint32_t)cw);            /* фон тоже: старый знак стирать не нужно */
        }
    }
}

/* ── «грязные» прямоугольники ─────────────────────────────────────────── */
static int touch(tft_rect_t a, tft_rect_t b)
{
    return a.x <= b.x + b.w && b.x <= a.x + a.w && a.y <= b.y + b.h && b.y <= a.y + a.h;
}

static tft_rect_t bbox(tft_rect_t a, tft_rect_t b)
{
    int16_t x0 = a.x < b.x ? a.x : b.x, y0 = a.y < b.y ? a.y : b.y;
    int16_t x1 = a.x + a.w > b.x + b.w ? (int16_t)(a.x + a.w) : (int16_t)(b.x + b.w);
    int16_t y1 = a.y + a.h > b.y + b.h ? (int16_t)(a.y + a.h) : (int16_t)(b.y + b.h);
    return (tft_rect_t){x0, y0, (int16_t)(x1 - x0), (int16_t)(y1 - y0)};
}

void tft_dirty_add(tft_dirty_t *d, tft_rect_t r)
{
    if (r.w <= 0 || r.h <= 0)
        return;
    for (uint8_t i = 0; i < d->n; i++)
        if (touch(d->r[i], r)) {                        /* касается или пересекает — объединить */
            r = bbox(d->r[i], r);
            d->r[i] = d->r[--d->n];
            tft_dirty_add(d, r);                        /* объединение может задеть соседей */
            return;
        }
    if (d->n < TFT_DIRTY_MAX) {
        d->r[d->n++] = r;
    } else {                                            /* мест нет: слить с последним */
        r = bbox(d->r[--d->n], r);
        tft_dirty_add(d, r);
    }
}

void tft_flush(const tft_t *t, tft_dirty_t *d, tft_shader_t shade, const void *ctx)
{
    for (uint8_t k = 0; k < d->n; k++) {
        tft_rect_t r = d->r[k];
        if (!clip(t, &r.x, &r.y, &r.w, &r.h))
            continue;
        tft_window(t, r.x, r.y, r.w, r.h);
        for (int16_t y = r.y; y < r.y + r.h; y++) {
            uint16_t *p = line[y & 1];                  /* пока DMA шлёт прошлую строку */
            for (int16_t i = 0; i < r.w; i++)
                p[i] = shade(ctx, (int16_t)(r.x + i), y);
            t->bus->pixels(p, (uint32_t)r.w);
        }
    }
    d->n = 0;
}

/* ── пример: прибор с заголовком, числом и полосой ────────────────────── */
uint16_t meter_shade(const void *ctx, int16_t x, int16_t y)
{
    const meter_t *m = ctx;
    if (y >= 6 && y < 22 && x >= 4 && x < 52)                       /* «TEMP», масштаб 2 */
        return tft_glyph("TEMP"[(x - 4) / 12], (x - 4) % 12 / 2, (y - 6) / 2) ? m->fg : m->bg;
    if (y >= METER_Y && y < METER_Y + METER_CELL_H && x >= METER_X && x < METER_X + METER_CHARS * METER_CELL_W)
        return tft_glyph(m->text[(x - METER_X) / METER_CELL_W], (x - METER_X) % METER_CELL_W / 3, (y - METER_Y) / 3)
                   ? m->fg : m->bg;
    if (y >= BAR_Y && y < BAR_Y + BAR_H && x >= BAR_X && x < BAR_X + BAR_W) {
        if (y == BAR_Y || y == BAR_Y + BAR_H - 1 || x == BAR_X || x == BAR_X + BAR_W - 1)
            return m->fg;                                           /* рамка */
        if (x >= BAR_X + 2 && x < BAR_X + 2 + m->bar && y >= BAR_Y + 2 && y < BAR_Y + BAR_H - 2)
            return m->accent;
    }
    return m->bg;
}

void meter_set(meter_t *m, tft_dirty_t *d, const char *text, uint16_t bar)
{
    int end = 0;
    for (int i = 0; i < METER_CHARS; i++) {
        char c = end ? ' ' : text[i];
        if (!c) {
            end = 1;
            c = ' ';
        }
        if (c != m->text[i]) {                          /* изменился знак — грязна только его клетка */
            m->text[i] = c;
            tft_dirty_add(d, (tft_rect_t){(int16_t)(METER_X + i * METER_CELL_W), METER_Y, METER_CELL_W, METER_CELL_H});
        }
    }
    if (bar > BAR_W - 4)
        bar = BAR_W - 4;
    if (bar != m->bar) {                                /* полоса — только полоска между старым и новым концом */
        uint16_t a = bar < m->bar ? bar : m->bar, b = bar < m->bar ? m->bar : bar;
        tft_dirty_add(d, (tft_rect_t){(int16_t)(BAR_X + 2 + a), BAR_Y + 2, (int16_t)(b - a), BAR_H - 4});
        m->bar = bar;
    }
}

void meter_value(meter_t *m, tft_dirty_t *d, int tenths)
{
    char s[METER_CHARS + 1] = "00.0 C";
    if (tenths < 0)
        tenths = 0;
    if (tenths > 999)
        tenths = 999;
    s[0] = (char)('0' + tenths / 100);
    s[1] = (char)('0' + tenths / 10 % 10);
    s[3] = (char)('0' + tenths % 10);
    meter_set(m, d, s, (uint16_t)(tenths * (BAR_W - 4) / 400));    /* полоса: 40,0 — вся */
}

/* ── пример: картинка из прямоугольников, линий и текста ──────────────── */
void tft_demo(const tft_t *t)
{
    uint16_t bg = tft_rgb(0, 0, 64), white = tft_rgb(255, 255, 255), yellow = tft_rgb(255, 255, 0);
    int16_t w = (int16_t)t->w, h = (int16_t)t->h;
    tft_fill_rect(t, 0, 0, w, h, bg);
    tft_text(t, 4, 4, "ST7735", 2, white, bg);
    tft_fill_rect(t, 4, 24, 36, 20, tft_rgb(255, 0, 0));
    tft_fill_rect(t, 46, 24, 36, 20, tft_rgb(0, 255, 0));
    tft_fill_rect(t, 88, 24, 36, 20, tft_rgb(0, 0, 255));
    tft_text(t, 4, 48, "R  G  B", 1, white, bg);
    tft_line(t, 0, 60, (int16_t)(w - 1), (int16_t)(h - 13), yellow);
    tft_line(t, (int16_t)(w - 1), 60, 0, (int16_t)(h - 13), yellow);
    tft_line(t, (int16_t)(w / 2), 60, (int16_t)(w / 2 + 9), (int16_t)(h - 13), white);
    tft_text(t, 4, (int16_t)(h - 10), w > h ? "160X128" : "128X160", 1, white, bg);
}

fw22i.c — прошивка STM32F103: SPI1 и DMA1 канал 3, D/C, RST, подсветка; прибор обновляется раз в 0,5 с.

/* fw22i.c — раздел 22з: TFT 128×160 на ST7735 у STM32F103 (BluePill), 72 МГц.
 * SPI1 ведущим: SCK — PA5, MOSI — PA7 (MISO не нужен), режим 3, как у платы ST с этим дисплеем;
 * CS — PA4 (на шине один дисплей: держим выбранным), D/C — PA3, RST — PA2, подсветка — PA1.
 * Пиксели и заливки идут через DMA1 канал 3 (SPI1_TX) 16-битными словами: старший байт первым сам.
 * На экране — прибор из tft22i.c: число и полоса, раз в 0,5 с перерисовывается только изменившееся.
 * Тактирование до 72 МГц — как в разделе 6 (здесь не повторяется); SPI1 на APB2 — 72 МГц. */
#include "stm32f10x.h"
#include "tft22i.h"

#define SPI_BR SPI_CR1_BR_1             /* 72 / 8 = 9 МГц; SPI_CR1_BR_0 — 72 / 4 = 18 МГц */
#define DC_LOW()   (GPIOA->BSRR = 1u << (3 + 16))
#define DC_HIGH()  (GPIOA->BSRR = 1u << 3)
#define RST_LOW()  (GPIOA->BSRR = 1u << (2 + 16))
#define RST_HIGH() (GPIOA->BSRR = 1u << 2)

static volatile uint32_t ticks;
static uint8_t wide;                    /* SPI сейчас 16-битный */
static uint16_t fill_word;              /* цвет заливки: DMA читает его без шага по памяти */

void SysTick_Handler(void)
{
    ticks++;
}

static void delay_ms(uint32_t ms)
{
    uint32_t t0 = ticks;
    while (ticks - t0 < ms) {
    }
}

/* Конец передачи: DMA отдал последнее слово, потом TXE = 1 и BSY = 0 (RM0008, с. 719). Только после
 * этого можно трогать D/C и DFF: по флагу DMA в SPI ещё лежат два слова. */
static void spi_idle(void)
{
    if (DMA1_Channel3->CCR & DMA_CCR1_EN) {
        while (!(DMA1->ISR & DMA_ISR_TCIF3)) {
        }
        DMA1->IFCR = DMA_IFCR_CTCIF3;
        DMA1_Channel3->CCR = 0;
    }
    while (!(SPI1->SR & SPI_SR_TXE)) {
    }
    while (SPI1->SR & SPI_SR_BSY) {
    }
    (void)SPI1->DR;                     /* приёма нет: снять RXNE и OVR (раздел 18.6) */
    (void)SPI1->SR;
}

static void spi_width(uint8_t w16)      /* DFF — только при выключенном SPI (RM0008, с. 742) */
{
    if (wide == w16)
        return;
    SPI1->CR1 &= ~SPI_CR1_SPE;
    if (w16)
        SPI1->CR1 |= SPI_CR1_DFF;
    else
        SPI1->CR1 &= ~SPI_CR1_DFF;
    SPI1->CR1 |= SPI_CR1_SPE;
    wide = w16;
}

static void dma_go(const void *src, uint32_t n, uint32_t minc)
{
    DMA1_Channel3->CMAR = (uint32_t)src;
    DMA1_Channel3->CNDTR = n;                           /* не больше 65 535 (RM0008, с. 286) */
    DMA1_Channel3->CCR = DMA_CCR1_DIR | minc | DMA_CCR1_PSIZE_0 | DMA_CCR1_MSIZE_0 | DMA_CCR1_EN;
}

static void bus_reset(void)
{
    RST_LOW();
    delay_ms(10);
    RST_HIGH();
    delay_ms(10);
}

static void bus_cmd(uint8_t c)
{
    spi_idle();
    spi_width(0);
    DC_LOW();
    SPI1->DR = c;
}

static void bus_data(const uint8_t *p, uint32_t n)
{
    spi_idle();
    spi_width(0);
    DC_HIGH();
    while (n--) {
        while (!(SPI1->SR & SPI_SR_TXE)) {
        }
        SPI1->DR = *p++;
    }
}

static void bus_pixels(const uint16_t *px, uint32_t n)
{
    spi_idle();                                         /* прошлая строка ушла целиком */
    spi_width(1);
    DC_HIGH();
    dma_go(px, n, DMA_CCR1_MINC);                       /* и сразу назад: строку шлёт DMA */
}

static void bus_fill(uint16_t c, uint32_t n)
{
    spi_idle();
    spi_width(1);
    DC_HIGH();
    fill_word = c;
    while (n) {
        uint32_t k = n > 65535u ? 65535u : n;
        spi_idle();
        dma_go(&fill_word, k, 0);                       /* MINC = 0: одно слово k раз */
        n -= k;
    }
}

static const tft_bus_t bus = {bus_reset, delay_ms, bus_cmd, bus_data, bus_pixels, bus_fill};
static tft_t tft;
static meter_t meter;
static tft_dirty_t dirty;

int main(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_SPI1EN;
    RCC->AHBENR |= RCC_AHBENR_DMA1EN;
    GPIOA->BSRR = 1u << 2 | 1u << 3 | 1u << (4 + 16);  /* RST и D/C в 1, CS в 0 */
    GPIOA->CRL = (GPIOA->CRL & 0x0F00000Fu)             /* PA0 и PA6 не трогаем */
               | 0x2u << 4 | 0x3u << 8 | 0x3u << 12 | 0x3u << 16    /* PA1–PA4 — выходы */
               | 0xBu << 20 | 0xBu << 28;                           /* PA5, PA7 — SPI1, 50 МГц */
    SPI1->CR1 = SPI_CR1_MSTR | SPI_CR1_SSM | SPI_CR1_SSI | SPI_BR | SPI_CR1_CPOL | SPI_CR1_CPHA;
    SPI1->CR2 = SPI_CR2_TXDMAEN;
    SPI1->CR1 |= SPI_CR1_SPE;
    DMA1_Channel3->CPAR = (uint32_t)&SPI1->DR;
    SysTick_Config(72000u);                             /* 1 мс */
    GPIOA->BSRR = 1u << 1;                              /* подсветка */

    tft_init(&tft, &bus, 0, 1, 2, 1);                   /* как модуль Б модели: сдвиг 2 и 1, стекло BGR */
    tft_demo(&tft);
    delay_ms(2000);
    meter.fg = tft_rgb(255, 255, 255);
    meter.bg = tft_rgb(0, 0, 64);
    meter.accent = tft_rgb(255, 128, 0);
    for (int i = 0; i < METER_CHARS; i++)
        meter.text[i] = ' ';
    tft_dirty_add(&dirty, (tft_rect_t){0, 0, (int16_t)tft.w, (int16_t)tft.h});
    int v = 200;
    for (;;) {
        meter_value(&meter, &dirty, v);                 /* 20,0 … 29,9 по кругу */
        tft_flush(&tft, &dirty, meter_shade, &meter);
        v = v < 299 ? v + 1 : 200;
        delay_ms(500);
    }
}

Дальше: раздел 22и «Расширитель портов MCP23017 по I2C: 16 линий, прерывания по изменению, клавиатура и дребезг» — от экрана к вводу: 16 линий по двум проводам I2C, клавиатура и прерывание по изменению.

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