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

22о. Дисплей 320×240 на ILI9341: SPI с DMA против параллельной шины через FSMC на F407 — скорость заливки, окна, шрифты и разрыв кадра

В разделе 22з маленький ST7735 (128×160) жил на SPI: окно CASET/RASET, команда RAMWR, RGB565, линия D/C — всё это здесь не повторяется. ILI9341 говорит теми же командами, но точек у него 76 800, в 3,75 раза больше, и здесь впервые встаёт вопрос не «как нарисовать», а «успеет ли шина». Раздел 22а дал общий взгляд (кадр 240×320 — 153 600 байт), 41б — устройство FSMC, 22к — сенсор, который обычно стоит на том же модуле, 198с — готовый прибор с этим экраном на SPI. Здесь — сам ILI9341 и две шины к нему на F407: SPI с DMA и параллельная шина 8080 через FSMC; сколько длится кадр на каждой, что делать, когда кадр не помещается в ОЗУ, как рисовать текст с кириллицей и сглаживанием и почему картинка рвётся, даже когда всё посчитано верно.

Опоры: RM0090 Rev 19: SRAM1 112 Кбайт и SRAM2 16 Кбайт, CCM 64 Кбайт, с. 60; CCM доступна только ядру, с. 68; SPI1_TX — DMA2, поток 3 или 5, канал 3, с. 308; «память → память» умеет только DMA2, источник — PAR, с. 311; NDTR — до 65 535, с. 331; передача SPI через DMA — сначала TXE, потом BSY, с. 895; FIFO записи FSMC на 2 слова и ожидание текущего цикла, с. 1544; на 16-битной шине FSMC_A[24:0] = HADDR[25:1], с. 1548; запись в режиме 1 — ADDSET + (DATAST + 1) тактов HCLK, с. 1555. DS8626 Rev 9 (F405/F407): 192 + 4 Кбайт ОЗУ, с. 1; SPI1 до 42 Мбит/с, с. 35; выводы FSMC в таблице 9, с. 65–66; ток вывода до 25 мА, с. 79. DocID13587 Rev 17 (F103x8/xB): 20 Кбайт ОЗУ, с. 1; SPI ведущего до 18 МГц, с. 71. Документа на ILI9341 в проекте нет. Коды команд, код микросхемы 0x9341 и длина ответа на 0xD3 — из драйвера ST ili9341.h/ili9341.c (STM32F4xx DFP 2.17.1, BSP Components); там же комментарий «из даташита» о 328 строках кадра. Пределы SPI 10 МГц на запись и 6,66 МГц на чтение, режим SPI 0 и невозможность прочитать код без вывода EXTC — из файлов платы STM32F429I-Discovery; тайминги FSMC ADDSET 5, DATAST 9 — из BSP STM324xG-EVAL (его дисплей стоит на FSMC); светодиоды на PD12–PD15 и акселерометр на SPI1 — из BSP STM32F4-Discovery. Биты MADCTL и значения 0x55/0x66 — из MIPI-драйвера LVGL 9.3.0 (lv_lcd_generic_mipi.h), таблица поворотов и паузы — из драйвера ILI9341 проекта QMK. Что эти биты делают в самом кристалле, раскладка RDDST, холостой байт перед ответом и частота развёртки 70 Гц — по документации производителя, не проверено: в модели это допущения. Все числа ниже — прогон драйвера _verify/tft22o/ili22o.c на компьютере (gcc 12.2.0) со своей моделью контроллера и стекла на Python 3.11; прошивки для двух шин собраны armclang 6.24 под Cortex-M4.

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

Оригинал Перевод
PIXSET (pixel format set), 0x55 / 0x66 формат точки на входе: 16 или 18 бит
IM[3:0] (interface mode) выводы выбора шины: SPI или 8080 и её ширина
RDID4 (0xD3), RDDST (0x09) чтение кода микросхемы и слова состояния
dummy read холостое чтение перед ответом
WRX, RDX стробы записи и чтения шины 8080
TE (tearing effect output), TEON вывод «идёт гашение» и команда, которая его включает
vertical blanking, VFP кадровое гашение: строки, которых нет на стекле
ML (vertical refresh order) направление развёртки стекла: сверху или снизу
memory-to-memory transfer DMA без запроса периферии: из памяти в память
coverage, antialiased glyph доля точки под штрихом; сглаженный знак
code point номер знака в Юникоде

Вверху — две шины от STM32F407 к ILI9341. Слева SPI1 с DMA2 поток 3 на 42 МГц: PA5 — такт, PA7 — данные, PA6 — чтение кода, PB7 — D/C, PB0 — вход TE; точка — 2 байта по проводу, DMA шлёт строку, процессор считает следующую. Справа FSMC, банк 1, NE1, шина 8080 на 16 бит: D0–D15 с PD14, запись NWE на PD5, выбор NE1 на PD7, D/C на линии A18 — PD13, TE на PB0; команда — запись по адресу 0x6000 0000, данные — по 0x6008 0000, точка — одна запись STRH: 15 тактов HCLK, 89,3 нс. Посередине — время кадра 240 на 320 RGB565, 153 611 байт или 76 811 записей: SPI 18 МГц у F103 — 68,27 мс, SPI 21 МГц — 58,52 мс, SPI 42 МГц — 29,26 мс, FSMC — 6,86 мс; пунктиром период развёртки 14,29 мс и два периода 28,57 мс. Внизу — строки стекла во времени: от каждого фронта TE развёртка идёт с 8 строк гашения до строки 319; запись кадра через FSMC начинается на фронте TE и всё время впереди луча — разрыва нет; запись по SPI на 42 МГц длится дольше двух периодов, луч её обгоняет — сверху новый кадр, снизу старый


22о.1 ILI9341 против ST7735: размер, MADCTL, формат точки, шина и код микросхемы

Память кадра — 240 × 320 слов, ровно по стеклу, без сдвига, который мешал в 22з. Команды окна и записи те же, вторая команда окна у ILI9341 в документах ST зовётся PASET — «страницы», то есть строки. Что нового:

int ili_read_info(const ili_t *t, ili_info_t *inf)
{
    uint8_t b[5];
    t->bus->read(ILI_RDID4, b, 4);                      /* b[0] — холостой */
    inf->raw[0] = b[1];
    inf->raw[1] = b[2];
    inf->raw[2] = b[3];
    inf->id = (uint16_t)(b[2] << 8 | b[3]);
    t->bus->read(ILI_RDDST, b, 5);
    for (int i = 0; i < 4; i++)
        inf->st[i] = b[i + 1];
    return inf->id == ILI_ID;
}

В прогоне по SPI и по FSMC код прочитан: 00 93 41, состояние 84 53 06 00 — в раскладке модели это бит BGR, формат 101 (16 бит), сна нет, показ и TE включены. Запуск короче, чем у ST: сброс, SWRESET, 120 мс, SLPOUT, 120 мс, PIXSET 0x55, MADCTL, DISPON и TEON — 6 команд и 9 байт, всего 240 мс пауз. QMK ждёт 120 мс после сброса и 5 мс после выхода из сна, ST — 200 мс после выхода; сколько нужно на самом деле, знает только документ производителя. Регистры питания, VCOM и гаммы драйвер ST задаёт сам; что модуль работает и без них, на железе не проверялось.

Чтение по SPI медленнее записи: ST пишет для ILI9341 о 6,66 МГц на чтение против 10 МГц на запись. Прошивка fw22o_spi.c на время чтения ставит делитель 16 — 5,25 МГц — и возвращает 42 МГц для записи.


22о.2 SPI с DMA на F103 и F407: 153 611 байт на кадр

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

Шина Где Кадр Кадров в секунду
SPI 10 МГц предел записи по ST 122,89 мс 8,1
SPI 18 МГц F103: 72 / 4, предел F103 (с. 71) 68,27 мс 14,6
SPI 21 МГц F407: 84 / 4 58,52 мс 17,1
SPI 42 МГц F407: 84 / 2, предел SPI1 (с. 35) 29,26 мс 34,2

18 МГц — те же 68,27 мс, что в 18.8; 21 МГц — 58,52 мс против 58,51 в 198с (там нет 11 байт окна). 42 МГц у SPI1 F407 разрешены даташитом, но вчетверо выше 10 МГц из комментария ST: работает ли модуль на такой частоте и на каких проводах — не проверено.

На F407 пиксели идут через DMA2 поток 3, канал 3 (с. 308), словами по 16 бит при DFF = 1 — старший байт уходит первым сам. Заливка всего экрана — 76 800 одинаковых слов, а NDTR вмещает 65 535 (с. 331), поэтому передачи две:

static void bus_fill(uint16_t c, uint32_t n)
{
    spi_idle();
    spi_setup(1, BR_WRITE);
    DC_HIGH();
    fill_word = c;
    while (n) {                                         /* весь экран — 76 800 слов: две передачи */
        uint32_t k = n > 65535u ? 65535u : n;
        spi_idle();
        dma_go(&fill_word, k, 0);
        n -= k;
    }
}

spi_idle — то же правило, что в 22з: после флага DMA дождаться TXE = 1 и BSY = 0 (с. 895) и только потом трогать D/C. Вывод через строки занимает те же 153 611 байт, но передач DMA уже 320 — по одной на строку. Строка 240 точек на 42 МГц уходит за 91,43 мкс; у ядра на 168 МГц это 15 360 тактов, 64 на точку, — посчитать следующую строку хватает.


22о.3 Кадр в ОЗУ не помещается: окна и «грязные» прямоугольники

Кадр — 153 600 байт. У F103C8 ОЗУ 20 480 байт (с. 1). У F407 непрерывный кусок для DMA — SRAM1 и SRAM2, 131 072 байта (с. 60): тоже мало. Ещё 65 536 байт CCM DMA не видит вовсе — она доступна только ядру (с. 68). Так что двойной буфер исключён, одинарный тоже: картинку хранит дисплей, а программа присылает только изменившееся. Буфер — две строки по 320 точек, 1280 байт: столько статической памяти и показал armclang у ili22o.o.

Пример — круг радиусом 16 точек, который двигается по градиенту. Сцена задана функцией «цвет точки (x, y)», перерисовка берёт старое и новое место круга, сливает касающиеся прямоугольники (функция та же, что в 22з) и шлёт их строками:

Шаг Прямоугольники Точек SPI 42 МГц, байт SPI 42 МГц FSMC
весь экран 1 76 800 153 611 29,26 мс 6,858 мс
сдвиг на (4, 3) старое и новое слиты: 37 × 36 1332 2675 0,51 мс 0,12 мс
сдвиг на (16, 12) слиты: 49 × 45 2205 4421 0,84 мс 0,198 мс
прыжок два по 33 × 33 2178 4378 0,83 мс 0,196 мс

Сто шагов (первый — прыжок через экран, дальше по 2 точки) — 243 537 байт и 46,39 мс на SPI, то есть в среднем 2435 байт и 0,46 мс на шаг вместо 29,26 мс на весь экран. После всех шагов стекло модели совпало с эталоном — 0 расхождений по обеим шинам.


22о.4 Параллельная шина 8080 через FSMC на F407

Как FSMC превращает запись по адресу в цикл на выводах и почему линия D/C стоит на адресной линии, — в 41б; здесь конкретная разводка. Дисплей — на NE1 (PD7), строб записи WRX — NWE (PD5), чтения RDX — NOE (PD4), данные D0–D15 — PD14, PD15, PD0, PD1, PE7–PE15, PD8, PD9, PD10, всё AF12 (с. 65–66). D/C — на линии A18 (PD13). На 16-битной шине FSMC_A[n] — это бит n + 1 адреса (с. 1548), отсюда адреса:

#define LCD_RS_LINE 18u                                 /* FSMC_A18 */
#define LCD_CMD_ADDR 0x60000000u                        /* NE1, A18 = 0: команда */
#define LCD_DATA_ADDR (0x60000000u | 1u << (LCD_RS_LINE + 1u))   /* A18 = 1: данные, 0x60080000 */

Команда — запись байта по 0x6000 0000, параметр и точка — запись слова по 0x6008 0000: одна инструкция STRH, ни D/C, ни ожидания BSY. Модель берёт D/C из бита 19 адреса и на обеих шинах собирает одну и ту же картинку: 0 расхождений. По SPI картинка проверки — 198 888 байт, по FSMC — 99 488 записей: 99 400 точек плюс команды.

Тайминги. BSP STM324xG-EVAL для своего дисплея на FSMC при 168 МГц ставит ADDSET 5 и DATAST 9 в режиме 1. Запись — ADDSET + (DATAST + 1) тактов (с. 1555): 15 тактов по 5,952 нс, 89,29 нс на точку. Кадр — 76 811 записей и 6,858 мс, до 145,8 кадра в секунду. Каждый такт, снятый с цикла, экономит 0,457 мс на кадр; насколько можно ужать цикл для ILI9341, скажет только его документ (как считать — 41б.4), а начинают с запасом.

Процессор или DMA. Цикл STRH в прошивке быстрее шины не станет: у FSMC FIFO записи на 2 слова, и пока идёт текущий цикл, следующую запись контроллер задерживает тактами ожидания (с. 1544) — процессор стоит всё время кадра. DMA2 в режиме «память → память» (только DMA2, с. 311) берёт это на себя:

/* «Память → память»: источник — DMA_SxPAR, приёмник — DMA_SxM0AR (RM0090, рис. 38). */
void lcd_dma_start(uint32_t dst, const uint16_t *src, uint32_t n, int src_inc)
{
    DMA2_Stream0->PAR = (uint32_t)src;
    DMA2_Stream0->M0AR = dst;                           /* адрес данных дисплея, без приращения */
    DMA2_Stream0->NDTR = n;
    DMA2_Stream0->FCR = DMA_SxFCR_DMDIS | DMA_SxFCR_FTH_0;          /* FIFO обязателен в этом режиме */
    DMA2_Stream0->CR = DMA_SxCR_DIR_1 | (src_inc ? DMA_SxCR_PINC : 0u)
                     | DMA_SxCR_PSIZE_0 | DMA_SxCR_MSIZE_0 | DMA_SxCR_PL_1 | DMA_SxCR_EN;
    dma_on = 1;
}

Здесь PAR — источник: строка в ОЗУ с приращением или одно слово цвета без него; M0AR — адрес данных дисплея, без приращения. Заливка экрана — те же две передачи по 65 535 и 11 265. Строка 240 точек по FSMC — 21,43 мкс, всего 3600 тактов ядра, 15 на точку: сложную сцену процессор посчитать уже не успевает, и в паре с FSMC выгоднее рисовать прямоугольниками и заливками, где считать нечего.


22о.5 Сравнение и разрыв кадра: TE и 0x35

Шина Кадр Кадров в секунду Процессор во время кадра
SPI 21 МГц, опрос TXE 58,52 мс и паузы между словами меньше 17,1 занят весь кадр
SPI 21 МГц + DMA 58,52 мс 17,1 свободен: 2 передачи на заливку
SPI 42 МГц + DMA 29,26 мс 34,2 свободен
FSMC, запись STRH 6,858 мс 145,8 стоит на каждой записи
FSMC + DMA2 «память → память» 6,858 мс 145,8 свободен: 2 передачи на заливку

Паузы между словами при опросе модель не считает; что добавляет к циклу сам DMA при доступе к FSMC — тоже. FSMC быстрее SPI на 42 МГц в 4,3 раза.

Разрыв. ILI9341 показывает свою память сам: строка за строкой, сверху вниз, и запись программы о развёртке ничего не знает. Если луч дошёл до строки раньше, чем в неё легла новая картинка, на стекле верх нового кадра и низ старого. Против этого у контроллера есть вывод TE, его включает команда TEON (0x35); по описанию в LVGL сигнал приходит в начале VFP — кадрового гашения. Драйвер шлёт параметр 0 — по документации производителя это «только гашение», не проверено. В модели развёртка — 70 кадров в секунду, 328 строк: 320 видимых и 8 строк гашения (числа строк — из комментария в ili9341.h ST), период 14,286 мс, строка 43,55 мкс; от фронта TE до строки 0 — 8 строк. Анимация — полоса, которая за 6 кадров пробегает экран, — и подсчёт показов, на которых видно больше одного кадра:

Случай Запись области Показов С разрывом
FSMC, без синхронизации 6,857 мс 5 3
FSMC, по фронту TE 6,857 мс 7 0
FSMC, по TE, поворот 180° (C8) 6,857 мс 7 6
FSMC, по TE, 180° и ML (D8) 6,857 мс 7 0
FSMC, по TE, альбомная (68) 6,857 мс 7 6
SPI 42 МГц, весь экран по TE 29,257 мс 19 12
SPI 42 МГц, окно 240 × 100 по TE 9,142 мс 7 0
SPI 42 МГц, то же окно без синхронизации 9,142 мс 6 1

Правило, которое видно из таблицы: по фронту TE начинают запись, и она должна всё время идти впереди луча. FSMC пишет кадр за 6,857 мс при периоде 14,286 — успевает. SPI на 42 МГц пишет 29,257 мс — дольше двух периодов (28,571 мс), и никакой момент старта не спасает: только окно поменьше. Окно на строках 110–209 — 24 000 точек за 9,142 мс — успевает, потому что луч приходит к строке 110 позже.

Два случая, где TE не помогает и на FSMC. При повороте на 180° запись идёт снизу вверх, навстречу лучу; бит ML пускает развёртку снизу вверх, и разрыв пропадает — это делает ili_rotate:

void ili_rotate(ili_t *t, uint8_t rot, uint8_t ml)
{
    static const uint8_t mad[4] = {0x00u, MAD_MX | MAD_MV, MAD_MX | MAD_MY, MAD_MV | MAD_MY};
    uint8_t m = (uint8_t)((t->madctl & MAD_BGR) | mad[rot & 3u]);
    if (ml && (m & MAD_MY) && !(m & MAD_MV))
        m |= MAD_ML;                                    /* запись снизу вверх — и развёртка снизу вверх */
    t->madctl = m;
    t->w = (uint16_t)((m & MAD_MV) ? ILI_H : ILI_W);
    t->h = (uint16_t)((m & MAD_MV) ? ILI_W : ILI_H);
    t->bus->cmd(ILI_MADCTL);
    t->bus->data(&t->madctl, 1);
}

В альбомной ориентации (MV) запись идёт столбцами стекла поперёк развёртки, и граница кадров на стекле — наклонная линия: 6 показов из 7 с разрывом и по TE. Лечится картинкой, повёрнутой в программе при книжной ориентации памяти, или маленькими окнами. Фронт TE прошивки ждут опросом флага EXTI0 (вывод PB0).


22о.6 Шрифты: моно и сглаженный, строка одним окном, UTF-8 и кириллица

Шрифт свой: знак задан ломаными на сетке 4 × 7, клетка — 12 × 20 точек. Генератор gen_font22o.py делит каждую точку клетки на 8 × 8 проб и считает, сколько проб под штрихом толщиной 1,6 точки. Моно — точка горит, если покрыто не меньше половины; сглаженный — покрытие переводится в 16 уровней, 4 бита на точку. Знак — 40 байт в моно и 120 в сглаженном; 51 знак (цифры, знаки препинания, «°», «C», «?» и 33 заглавные буквы кириллицы) — 2040 и 6120 байт плюс таблица кодов 102 байта. armclang показал у font22o.o 8262 байта констант — ровно эта сумма.

Сглаживание — смешение цвета текста и фона по уровню: для каждой составляющей RGB565 (fg · a + bg · (15 − a) + 7) / 15. Шестнадцать оттенков считаются один раз на строку и дальше берутся из таблицы. В строке проверки «ТЕМПЕРАТУРА 23,5 °C» встречаются все 16 уровней.

Кириллица. Строка в программе — UTF-8: буква кириллицы и «°» — два байта, так что 19 знаков проверочной строки — 31 байт. Декодер достаёт кодовую точку, а таблица кодов, отсортированная по возрастанию, даёт номер знака двоичным поиском — до 6 сравнений на 51 знак:

uint32_t ili_utf8_next(const char **s)
{
    const uint8_t *p = (const uint8_t *)*s;
    uint32_t c = p[0], need, min;
    if (c == 0u)
        return 0u;
    if (c < 0x80u) {                                    /* 0xxxxxxx — ASCII */
        *s += 1;
        return c;
    }
    if ((c & 0xE0u) == 0xC0u) {                         /* 110xxxxx 10xxxxxx — кириллица, «°» */
        need = 1u, min = 0x80u, c &= 0x1Fu;
    } else if ((c & 0xF0u) == 0xE0u) {
        need = 2u, min = 0x800u, c &= 0x0Fu;
    } else if ((c & 0xF8u) == 0xF0u) {
        need = 3u, min = 0x10000u, c &= 0x07u;
    } else {                                            /* продолжение без начала или 0xF8…0xFF */
        *s += 1;
        return 0xFFFDu;
    }
    for (uint32_t i = 1; i <= need; i++) {
        if ((p[i] & 0xC0u) != 0x80u) {                 /* оборвалась: съесть только начало */
            *s += i;
            return 0xFFFDu;
        }
        c = c << 6 | (p[i] & 0x3Fu);
    }
    *s += need + 1u;
    if (c < min || c > 0x10FFFFu || (c >= 0xD800u && c <= 0xDFFFu))
        return 0xFFFDu;                                 /* лишне длинная запись, вне Юникода, суррогат */
    return c;
}

В прогоне «Ёж» дал U+0401 и U+0436: «Ё» есть в таблице, строчной «ж» нет — рисуется «?». Оборванный байт D0, байт FF, лишне длинная запись C0 AF и суррогат ED A0 80 дали по одному U+FFFD, а «A» после FF не потерялась. Знак «€» (три байта) декодирован верно, но в таблице его нет.

Строка — одним окном. ili_text открывает окно на всю строку и шлёт 20 строк по n × 12 точек, ili_text_per_glyph — окно на каждый знак:

Способ SPI 42 МГц, байт Переключений D/C Передач DMA SPI 42 МГц FSMC
окно на строку 9131 6 20 1739,2 мкс 408,1 мкс
окно на знак 9329 114 380 1777,0 мкс 425,8 мкс

Байт разница маленькая — 18 лишних окон по 11 байт. Главное — передач DMA в 19 раз больше, и каждая — запуск потока и ожидание конца, которых модель не считает. Моно и сглаженный шрифт по байтам одинаковы: на шину идут готовые точки RGB565, разница только в ПЗУ. Все четыре строки совпали с эталоном модели на обеих шинах.


22о.7 Грабли

Код микросхемы читается как FF FF FF. На многих модулях вывод SDO (MISO) не выведен или не подключён. С подтяжкой на PA6 в модели приходит FF FF FF и слово состояния FF FF FF FF, ili_read_info возвращает 0. У STM32F429I-Discovery код не читается по другой причине: по комментарию ST, вывод EXTC не подключён к питанию, и команда 0xD3 недоступна. Не сверять код — не значит, что дисплей неисправен.

Цвета перепутаны при DMA байтами. SPI оставили 8-битным, а массив uint16_t отдали DMA как байты: в памяти он лежит младшим вперёд. В модели красный стал (0, 28, 198), тёмно-синий фон — почти чёрным, не совпали 75 671 точка. Пиксели — словами при DFF = 1 (22з), на FSMC шина и так 16-битная.

Красный стал синим. Стекло с порядком B-G-R, а бит BGR не поставлен: в модели не совпали 73 110 точек. Оба открытых драйвера, ST и QMK, этот бит ставят. Переставить бит, а не цвета в программе.

TE включили, а он выключен. В заголовке опкодов QMK (qp_ili9xxx_opcodes.h) 0x34 назван «TEARING_ON», а 0x35 — «TEARING_OFF», в драйвере ST и в LVGL — наоборот: 0x35 — TEON. Писать коды, а не чужие имена, и проверять RDDST.

Подсветка. Ток вывода F407 — до 25 мА (с. 79); подсветку модуля от вывода GPIO напрямую не питают — через транзистор, если его нет на самом модуле, и яркость задают ШИМ таймера. Ток подсветки и есть ли на модуле транзистор — по описанию модуля, здесь не проверялось.

FSMC и светодиоды платы. У STM32F4-Discovery на PD12–PD15 стоят светодиоды (BSP), а PD13 — это A18, то есть D/C в нашей разводке, PD14 и PD15 — D0 и D1. Ремапа здесь нет: FSMC_A18 в таблицах выводов DS8626 есть только у PD13. Выход — другая адресная линия для D/C (и пересчитать адрес, 41б.2) или плата без нагрузки на шине; D0 и D1 не перенести. На той же плате к SPI1 (PA5–PA7) подключён акселерометр с выбором на PE3 — прошивка держит PE3 в единице.

Рвётся, хотя TE есть. Запись дольше двух периодов развёртки (SPI на 42 МГц, весь экран), поворот на 180° без ML или альбомная ориентация — 22о.5.


22о.8 Итоги

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

Таблицы шрифта font22o.c (683 строки) сделаны генератором gen_font22o.py и лежат рядом с проверкой в _verify/tft22o/.

ili22o.h — команды ILI9341, биты MADCTL, шина, драйвер, шрифт, сцена: типы и прототипы.

/* ili22o.h — раздел 22о: дисплей 240×320 на ILI9341 без регистров микроконтроллера.
 * Драйвер говорит с дисплеем через ili_bus_t: в прошивке это SPI1 + DMA2 (fw22o_spi.c) или
 * FSMC + DMA2 «память → память» (fw22o_fsmc.c + bus22o_fsmc.c), в стенде host22o.c — печать
 * для модели контроллера model22o.py.
 * Коды команд — как в ili9341.h драйвера ST из паков Keil (BSP Components/ili9341) и в LVGL;
 * биты MADCTL — как маски MIPI-драйвера LVGL (lv_lcd_generic_mipi.h). */
#ifndef ILI22O_H
#define ILI22O_H
#include <stdint.h>

/* ── команды ILI9341 ──────────────────────────────────────────────────── */
#define ILI_SWRESET 0x01u               /* программный сброс */
#define ILI_RDDST   0x09u               /* чтение состояния: 4 байта после холостого */
#define ILI_SLPOUT  0x11u               /* выход из сна */
#define ILI_DISPON  0x29u               /* включить показ */
#define ILI_CASET   0x2Au               /* окно: столбцы от и до */
#define ILI_PASET   0x2Bu               /* окно: строки («страницы») от и до */
#define ILI_RAMWR   0x2Cu               /* дальше — пиксели в окно */
#define ILI_TEOFF   0x34u               /* выключить вывод TE */
#define ILI_TEON    0x35u               /* включить вывод TE; параметр 0 — импульс только в гашении */
#define ILI_MADCTL  0x36u               /* порядок обхода памяти, порядок цветов, направление развёртки */
#define ILI_COLMOD  0x3Au               /* формат пикселя (PIXSET) */
#define ILI_RDID4   0xD3u               /* чтение кода микросхемы: холостой байт, 00, 93, 41 */

#define ILI_ID      0x9341u             /* как ILI9341_ID в драйвере ST */
#define COLMOD_16   0x55u               /* 16 бит на точку: RGB565, 2 байта */
#define COLMOD_18   0x66u               /* 18 бит на точку: 3 байта по SPI */

/* ── MADCTL ──────────────────────────────────────────────────────────── */
#define MAD_MY  0x80u                   /* зеркало строк */
#define MAD_MX  0x40u                   /* зеркало столбцов */
#define MAD_MV  0x20u                   /* обмен строк и столбцов: поворот на 90° */
#define MAD_ML  0x10u                   /* развёртка стекла снизу вверх (на память не влияет) */
#define MAD_BGR 0x08u                   /* стекло с порядком B-G-R */
#define MAD_MH  0x04u                   /* развёртка строки справа налево */

#define ILI_W 240u                      /* память и стекло в книжной ориентации */
#define ILI_H 320u
#define ILI_LINE_MAX 320u               /* самая длинная строка — в альбомной */
#define ILI_DIRTY_MAX 4u

/* Шина. Каждая функция сама дожидается конца предыдущей передачи и только потом трогает D/C;
 * pixels и fill могут вернуться раньше, чем всё ушло: передачу ведёт DMA. */
typedef struct {
    void (*reset)(void);
    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 точек RGB565 */
    void (*fill)(uint16_t color, uint32_t n);           /* D/C = 1: n точек одного цвета */
    void (*read)(uint8_t c, uint8_t *out, uint32_t n);  /* команда, потом n байт ответа */
    void (*wait_te)(void);                              /* дождаться фронта TE */
} ili_bus_t;

typedef struct {
    const ili_bus_t *bus;
    uint16_t w, h;                      /* после поворота */
    uint8_t madctl;
} ili_t;

typedef struct {
    uint16_t id;                        /* 0x9341, если прочитан */
    uint8_t raw[4];                     /* ответ на D3 без холостого байта… */
    uint8_t st[4];                      /* …и на RDDST: D31…D0 */
} ili_info_t;

typedef struct { int16_t x, y, w, h; } ili_rect_t;
typedef struct { ili_rect_t r[ILI_DIRTY_MAX]; uint8_t n; } ili_dirty_t;
typedef uint16_t (*ili_shader_t)(const void *ctx, int16_t x, int16_t y);

uint16_t ili_rgb(uint8_t r, uint8_t g, uint8_t b);       /* RGB888 → RGB565, как в 22з */
void ili_init(ili_t *t, const ili_bus_t *bus, uint8_t rot, uint8_t bgr);
void ili_rotate(ili_t *t, uint8_t rot, uint8_t ml);      /* 0…3; ml — развёртка по ходу записи */
int ili_read_info(const ili_t *t, ili_info_t *inf);       /* 1 — код 0x9341 прочитан */
void ili_te_on(const ili_t *t);
void ili_window(const ili_t *t, int16_t x, int16_t y, int16_t w, int16_t h);
void ili_fill_rect(const ili_t *t, int16_t x, int16_t y, int16_t w, int16_t h, uint16_t c);
void ili_shade_rect(const ili_t *t, ili_rect_t r, ili_shader_t shade, const void *ctx);

/* ── шрифт 12×20: UTF-8 → кодовая точка → номер знака ──────────────────── */
#define FONT22O_N 51
#define FONT22O_W 12
#define FONT22O_H 20
#define FONT22O_MONO_BYTES (FONT22O_H * 2)
#define FONT22O_AA_BYTES (FONT22O_W * FONT22O_H / 2)
extern const uint16_t font22o_codes[FONT22O_N];
extern const uint8_t font22o_mono[FONT22O_N][FONT22O_MONO_BYTES];
extern const uint8_t font22o_aa[FONT22O_N][FONT22O_AA_BYTES];

uint32_t ili_utf8_next(const char **s);                  /* 0 — конец строки; ошибка — U+FFFD */
int ili_glyph_index(uint32_t cp);                         /* −1 — знака нет */
uint16_t ili_blend(uint16_t fg, uint16_t bg, uint8_t a);  /* a = 0…15 */
void ili_text(const ili_t *t, int16_t x, int16_t y, const char *utf8, uint8_t aa, uint16_t fg, uint16_t bg);
void ili_text_per_glyph(const ili_t *t, int16_t x, int16_t y, const char *utf8, uint8_t aa, uint16_t fg, uint16_t bg);

/* ── «грязные» прямоугольники ─────────────────────────────────────────── */
void ili_dirty_add(ili_dirty_t *d, ili_rect_t r);
void ili_flush(const ili_t *t, ili_dirty_t *d, ili_shader_t shade, const void *ctx);

/* ── сцена примера: градиент и круг, который двигается ──────────────────── */
#define BALL_R 16
typedef struct { int16_t cx, cy; } scene_t;
uint16_t scene_shade(const void *ctx, int16_t x, int16_t y);
void scene_move(scene_t *s, ili_dirty_t *d, int16_t cx, int16_t cy);

#endif

ili22o.c — драйвер: запуск, код микросхемы, окно, заливка, строки, UTF-8, шрифт моно и сглаженный, «грязные» прямоугольники — общий для прошивок и стенда.

/* ili22o.c — раздел 22о: драйвер ILI9341 (запуск, код микросхемы, окно, заливка, строки через
 * буфер, шрифт 12×20 моно и сглаженный, UTF-8, «грязные» прямоугольники) без регистров
 * микроконтроллера: общий для прошивок fw22o_spi.c, fw22o_fsmc.c и стенда host22o.c. */
#include "ili22o.h"

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

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

/* Повороты — как таблица QMK для ILI9341: 0, MX|MV, MX|MY, MV|MY. ML переворачивает развёртку
 * стекла, а не память: при 180° запись идёт снизу вверх, и развёртку пускают в ту же сторону. */
void ili_rotate(ili_t *t, uint8_t rot, uint8_t ml)
{
    static const uint8_t mad[4] = {0x00u, MAD_MX | MAD_MV, MAD_MX | MAD_MY, MAD_MV | MAD_MY};
    uint8_t m = (uint8_t)((t->madctl & MAD_BGR) | mad[rot & 3u]);
    if (ml && (m & MAD_MY) && !(m & MAD_MV))
        m |= MAD_ML;                                    /* запись снизу вверх — и развёртка снизу вверх */
    t->madctl = m;
    t->w = (uint16_t)((m & MAD_MV) ? ILI_H : ILI_W);
    t->h = (uint16_t)((m & MAD_MV) ? ILI_W : ILI_H);
    t->bus->cmd(ILI_MADCTL);
    t->bus->data(&t->madctl, 1);
}

void ili_init(ili_t *t, const ili_bus_t *bus, uint8_t rot, uint8_t bgr)
{
    static const uint8_t pixfmt = COLMOD_16;
    t->bus = bus;
    t->madctl = bgr ? MAD_BGR : 0u;
    bus->reset();
    bus->cmd(ILI_SWRESET);
    bus->delay_ms(120);                                 /* паузы с запасом: документа производителя в проекте нет */
    bus->cmd(ILI_SLPOUT);
    bus->delay_ms(120);
    bus->cmd(ILI_COLMOD);
    bus->data(&pixfmt, 1);                              /* 0x55: 16 бит и на шине MCU, и на RGB */
    ili_rotate(t, rot, 0);
    bus->cmd(ILI_DISPON);
}

int ili_read_info(const ili_t *t, ili_info_t *inf)
{
    uint8_t b[5];
    t->bus->read(ILI_RDID4, b, 4);                      /* b[0] — холостой */
    inf->raw[0] = b[1];
    inf->raw[1] = b[2];
    inf->raw[2] = b[3];
    inf->id = (uint16_t)(b[2] << 8 | b[3]);
    t->bus->read(ILI_RDDST, b, 5);
    for (int i = 0; i < 4; i++)
        inf->st[i] = b[i + 1];
    return inf->id == ILI_ID;
}

void ili_te_on(const ili_t *t)
{
    static const uint8_t vblank_only = 0x00u;           /* импульс TE только во время гашения */
    t->bus->cmd(ILI_TEON);
    t->bus->data(&vblank_only, 1);
}

void ili_window(const ili_t *t, int16_t x, int16_t y, int16_t w, int16_t h)
{
    uint16_t x1 = (uint16_t)(x + w - 1), y1 = (uint16_t)(y + h - 1);
    uint8_t ca[4] = {(uint8_t)((uint16_t)x >> 8), (uint8_t)x, (uint8_t)(x1 >> 8), (uint8_t)x1};
    uint8_t pa[4] = {(uint8_t)((uint16_t)y >> 8), (uint8_t)y, (uint8_t)(y1 >> 8), (uint8_t)y1};
    t->bus->cmd(ILI_CASET);
    t->bus->data(ca, 4);
    t->bus->cmd(ILI_PASET);
    t->bus->data(pa, 4);
    t->bus->cmd(ILI_RAMWR);
}

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

void ili_fill_rect(const ili_t *t, int16_t x, int16_t y, int16_t w, int16_t h, uint16_t c)
{
    ili_rect_t r = {x, y, w, h};
    if (!clip(t, &r))
        return;
    ili_window(t, r.x, r.y, r.w, r.h);
    t->bus->fill(c, (uint32_t)r.w * (uint32_t)r.h);     /* весь экран — 76 800 слов: больше, чем NDTR */
}

void ili_shade_rect(const ili_t *t, ili_rect_t r, ili_shader_t shade, const void *ctx)
{
    if (!clip(t, &r))
        return;
    ili_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);
    }
}

/* ── UTF-8 и таблица знаков ───────────────────────────────────────────── */
uint32_t ili_utf8_next(const char **s)
{
    const uint8_t *p = (const uint8_t *)*s;
    uint32_t c = p[0], need, min;
    if (c == 0u)
        return 0u;
    if (c < 0x80u) {                                    /* 0xxxxxxx — ASCII */
        *s += 1;
        return c;
    }
    if ((c & 0xE0u) == 0xC0u) {                         /* 110xxxxx 10xxxxxx — кириллица, «°» */
        need = 1u, min = 0x80u, c &= 0x1Fu;
    } else if ((c & 0xF0u) == 0xE0u) {
        need = 2u, min = 0x800u, c &= 0x0Fu;
    } else if ((c & 0xF8u) == 0xF0u) {
        need = 3u, min = 0x10000u, c &= 0x07u;
    } else {                                            /* продолжение без начала или 0xF8…0xFF */
        *s += 1;
        return 0xFFFDu;
    }
    for (uint32_t i = 1; i <= need; i++) {
        if ((p[i] & 0xC0u) != 0x80u) {                 /* оборвалась: съесть только начало */
            *s += i;
            return 0xFFFDu;
        }
        c = c << 6 | (p[i] & 0x3Fu);
    }
    *s += need + 1u;
    if (c < min || c > 0x10FFFFu || (c >= 0xD800u && c <= 0xDFFFu))
        return 0xFFFDu;                                 /* лишне длинная запись, вне Юникода, суррогат */
    return c;
}

int ili_glyph_index(uint32_t cp)
{
    int lo = 0, hi = FONT22O_N - 1;
    while (lo <= hi) {                                  /* двоичный поиск: 51 знак — до 6 сравнений */
        int mid = (lo + hi) / 2;
        if (font22o_codes[mid] == cp)
            return mid;
        if (font22o_codes[mid] < cp)
            lo = mid + 1;
        else
            hi = mid - 1;
    }
    return -1;
}

uint16_t ili_blend(uint16_t fg, uint16_t bg, uint8_t a)
{
    uint32_t r = ((fg >> 11) * a + (bg >> 11) * (15u - a) + 7u) / 15u;
    uint32_t g = (((fg >> 5) & 63u) * a + ((bg >> 5) & 63u) * (15u - a) + 7u) / 15u;
    uint32_t b = ((fg & 31u) * a + (bg & 31u) * (15u - a) + 7u) / 15u;
    return (uint16_t)(r << 11 | g << 5 | b);
}

static int glyphs(const char *s, int16_t *idx, int max)
{
    int n = 0;
    uint32_t cp;
    while (n < max && (cp = ili_utf8_next(&s)) != 0u) {
        int g = ili_glyph_index(cp);
        idx[n++] = (int16_t)(g >= 0 ? g : ili_glyph_index('?'));
    }
    return n;
}

static void glyph_row(uint16_t *p, int g, int row, uint8_t aa, const uint16_t *pal)
{
    for (int c = 0; c < FONT22O_W; c++) {
        uint8_t a;
        if (aa) {
            uint8_t b = font22o_aa[g][row * (FONT22O_W / 2) + c / 2];
            a = (uint8_t)((c & 1) ? (b & 15u) : (b >> 4));
        } else {
            a = (font22o_mono[g][row * 2 + c / 8] >> (7 - c % 8)) & 1u ? 15u : 0u;
        }
        p[c] = pal[a];
    }
}

/* Строка — одно окно на всю строку текста: 20 передач по n × 12 точек. */
void ili_text(const ili_t *t, int16_t x, int16_t y, const char *utf8, uint8_t aa, uint16_t fg, uint16_t bg)
{
    int16_t idx[ILI_LINE_MAX / FONT22O_W];
    uint16_t pal[16];
    int n = glyphs(utf8, idx, (int)(sizeof idx / sizeof idx[0]));
    if (n == 0)
        return;
    for (uint8_t a = 0; a < 16u; a++)
        pal[a] = ili_blend(fg, bg, a);                  /* 16 оттенков — один раз на строку */
    ili_window(t, x, y, (int16_t)(n * FONT22O_W), FONT22O_H);
    for (int row = 0; row < FONT22O_H; row++) {
        uint16_t *p = line[row & 1];
        for (int k = 0; k < n; k++)
            glyph_row(p + k * FONT22O_W, idx[k], row, aa, pal);
        t->bus->pixels(p, (uint32_t)(n * FONT22O_W));
    }
}

/* Для сравнения: окно на каждый знак — 20 передач по 12 точек на знак. */
void ili_text_per_glyph(const ili_t *t, int16_t x, int16_t y, const char *utf8, uint8_t aa, uint16_t fg, uint16_t bg)
{
    int16_t idx[ILI_LINE_MAX / FONT22O_W];
    uint16_t pal[16];
    int n = glyphs(utf8, idx, (int)(sizeof idx / sizeof idx[0]));
    for (uint8_t a = 0; a < 16u; a++)
        pal[a] = ili_blend(fg, bg, a);
    for (int k = 0; k < n; k++) {
        ili_window(t, (int16_t)(x + k * FONT22O_W), y, FONT22O_W, FONT22O_H);
        for (int row = 0; row < FONT22O_H; row++) {
            uint16_t *p = line[row & 1];
            glyph_row(p, idx[k], row, aa, pal);
            t->bus->pixels(p, FONT22O_W);
        }
    }
}

/* ── «грязные» прямоугольники ─────────────────────────────────────────── */
static int touch(ili_rect_t a, ili_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 ili_rect_t bbox(ili_rect_t a, ili_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 (ili_rect_t){x0, y0, (int16_t)(x1 - x0), (int16_t)(y1 - y0)};
}

void ili_dirty_add(ili_dirty_t *d, ili_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];
            ili_dirty_add(d, r);
            return;
        }
    if (d->n < ILI_DIRTY_MAX)
        d->r[d->n++] = r;
    else
        ili_dirty_add(d, bbox(d->r[--d->n], r));
}

void ili_flush(const ili_t *t, ili_dirty_t *d, ili_shader_t shade, const void *ctx)
{
    for (uint8_t k = 0; k < d->n; k++)
        ili_shade_rect(t, d->r[k], shade, ctx);
    d->n = 0;
}

/* ── сцена: вертикальный градиент и круг ──────────────────────────────── */
uint16_t scene_shade(const void *ctx, int16_t x, int16_t y)
{
    const scene_t *s = ctx;
    int dx = x - s->cx, dy = y - s->cy;
    if (dx * dx + dy * dy <= BALL_R * BALL_R)
        return ili_rgb(255, 128, 0);
    return ili_rgb(0, (uint8_t)(y * 255 / 319), (uint8_t)(255 - y * 255 / 319));
}

void scene_move(scene_t *s, ili_dirty_t *d, int16_t cx, int16_t cy)
{
    ili_dirty_add(d, (ili_rect_t){(int16_t)(s->cx - BALL_R), (int16_t)(s->cy - BALL_R), 2 * BALL_R + 1, 2 * BALL_R + 1});
    s->cx = cx;
    s->cy = cy;
    ili_dirty_add(d, (ili_rect_t){(int16_t)(cx - BALL_R), (int16_t)(cy - BALL_R), 2 * BALL_R + 1, 2 * BALL_R + 1});
}

bus22o_fsmc.c — шина ili_bus_t поверх FSMC: адреса команды и данных, запись STRH, DMA «память → память».

/* bus22o_fsmc.c — раздел 22о: шина ili_bus_t поверх FSMC (8080, 16 бит) на STM32F407.
 * Дисплей на NE1, вход D/C (RS) — на линии FSMC_A18 (PD13). На 16-битной шине FSMC_A[n] = HADDR[n+1]
 * (RM0090, таблица 217; раздел 41б.2), поэтому данные — по 0x6008 0000, а не по 0x6004 0000.
 * Запись — одна инструкция STRH; пиксели — DMA2 «память → память»: источник — строка в ОЗУ
 * или одно слово цвета, приёмник — адрес данных без приращения.
 * Железо (сброс, паузы, TE, DMA) — в fw22o_fsmc.c; в стенде host22o.c — печать для модели,
 * а запись и чтение по адресу подменяются (-DFSMC_HOST). */
#include "ili22o.h"

#ifdef FSMC_HOST
void host_wr(uint32_t a, uint16_t v);
uint16_t host_rd(uint32_t a);
#define FSMC_WR(a, v) host_wr((a), (uint16_t)(v))
#define FSMC_RD(a) host_rd(a)
#else
#define FSMC_WR(a, v) (*(volatile uint16_t *)(a) = (uint16_t)(v))   /* одна инструкция STRH */
#define FSMC_RD(a) (*(volatile uint16_t *)(a))
#endif

#define LCD_RS_LINE 18u                                 /* FSMC_A18 */
#define LCD_CMD_ADDR 0x60000000u                        /* NE1, A18 = 0: команда */
#define LCD_DATA_ADDR (0x60000000u | 1u << (LCD_RS_LINE + 1u))   /* A18 = 1: данные, 0x60080000 */

void lcd_hw_reset(void);
void lcd_hw_delay_ms(uint32_t ms);
void lcd_hw_wait_te(void);
void lcd_dma_start(uint32_t dst, const uint16_t *src, uint32_t n, int src_inc);
void lcd_dma_wait(void);

static uint16_t fill_word;                              /* цвет заливки: DMA читает его без приращения */

static void f_cmd(uint8_t c)
{
    lcd_dma_wait();                                     /* пиксели прошлой передачи ушли */
    FSMC_WR(LCD_CMD_ADDR, c);
}

static void f_data(const uint8_t *p, uint32_t n)
{
    lcd_dma_wait();
    while (n--)
        FSMC_WR(LCD_DATA_ADDR, *p++);                   /* параметр — младшие 8 бит слова */
}

static void f_pixels_dma(const uint16_t *px, uint32_t n)
{
    lcd_dma_wait();
    lcd_dma_start(LCD_DATA_ADDR, px, n, 1);             /* и сразу назад: строку пишет DMA */
}

static void f_fill_dma(uint16_t c, uint32_t n)
{
    lcd_dma_wait();
    fill_word = c;
    while (n) {
        uint32_t k = n > 65535u ? 65535u : n;           /* NDTR — до 65 535 (RM0090, с. 331) */
        lcd_dma_wait();
        lcd_dma_start(LCD_DATA_ADDR, &fill_word, k, 0);
        n -= k;
    }
}

static void f_pixels_cpu(const uint16_t *px, uint32_t n)
{
    while (n--)
        FSMC_WR(LCD_DATA_ADDR, *px++);                  /* процессор стоит, пока идёт цикл шины */
}

static void f_fill_cpu(uint16_t c, uint32_t n)
{
    while (n--)
        FSMC_WR(LCD_DATA_ADDR, c);
}

static void f_read(uint8_t c, uint8_t *out, uint32_t n)
{
    lcd_dma_wait();
    FSMC_WR(LCD_CMD_ADDR, c);
    for (uint32_t i = 0; i < n; i++)
        out[i] = (uint8_t)FSMC_RD(LCD_DATA_ADDR);       /* первое чтение — холостое */
}

const ili_bus_t fsmc_bus_dma = {lcd_hw_reset, lcd_hw_delay_ms, f_cmd, f_data, f_pixels_dma, f_fill_dma, f_read, lcd_hw_wait_te};
const ili_bus_t fsmc_bus_cpu = {lcd_hw_reset, lcd_hw_delay_ms, f_cmd, f_data, f_pixels_cpu, f_fill_cpu, f_read, lcd_hw_wait_te};

fw22o_spi.c — прошивка STM32F407: SPI1 и DMA2 поток 3, чтение кода, TE на EXTI0, круг по TE.

/* fw22o_spi.c — раздел 22о: ILI9341 на STM32F407 по SPI1 с DMA, 168 МГц, APB2 84 МГц.
 * SCK — PA5, MISO — PA6 (с подтяжкой вверх), MOSI — PA7 (AF5); CS — PB6, D/C — PB7, RST — PB8,
 * TE — PB0 (EXTI0), подсветка — PB1. На STM32F4DISCOVERY к тем же PA5–PA7 подключён акселерометр
 * с выбором на PE3 — его держим в единице.
 * Запись — 84 / 2 = 42 МГц, чтение — 84 / 16 = 5,25 МГц (ST в файле платы 429I-Discovery пишет для
 * ILI9341 о 10 МГц на запись и 6,66 МГц на чтение). Пиксели — DMA2 поток 3 канал 3 (SPI1_TX,
 * RM0090, с. 308) словами по 16 бит: старший байт первым сам. Тактирование — как в разделе 6. */
#include "stm32f4xx.h"
#include "ili22o.h"

#define BR_WRITE 0u                                     /* /2: 42 МГц; SPI_CR1_BR_0 — /4: 21 МГц */
#define BR_READ (SPI_CR1_BR_1 | SPI_CR1_BR_0)           /* /16: 5,25 МГц */
#define CS_LOW()  (GPIOB->BSRR = 1u << (6 + 16))
#define DC_LOW()  (GPIOB->BSRR = 1u << (7 + 16))
#define DC_HIGH() (GPIOB->BSRR = 1u << 7)

static volatile uint32_t ticks;
static uint8_t dma_on;
static uint16_t fill_word;

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 (RM0090, с. 895). */
static void spi_idle(void)
{
    if (dma_on) {
        while (!(DMA2->LISR & DMA_LISR_TCIF3)) {
        }
        DMA2->LIFCR = DMA_LIFCR_CTCIF3 | DMA_LIFCR_CHTIF3 | DMA_LIFCR_CTEIF3 | DMA_LIFCR_CDMEIF3 | DMA_LIFCR_CFEIF3;
        dma_on = 0;
    }
    while (!(SPI1->SR & SPI_SR_TXE)) {
    }
    while (SPI1->SR & SPI_SR_BSY) {
    }
    (void)SPI1->DR;                                     /* приём не читали: снять RXNE и OVR */
    (void)SPI1->SR;
}

static void spi_setup(uint8_t w16, uint32_t br)        /* DFF и BR — только при выключенном SPI */
{
    uint32_t cr = (SPI1->CR1 & ~(SPI_CR1_DFF | SPI_CR1_BR)) | (w16 ? SPI_CR1_DFF : 0u) | br;
    if (cr == SPI1->CR1)
        return;
    SPI1->CR1 &= ~SPI_CR1_SPE;
    SPI1->CR1 = cr & ~SPI_CR1_SPE;
    SPI1->CR1 = cr | SPI_CR1_SPE;
}

static void dma_go(const void *src, uint32_t n, uint32_t minc)
{
    DMA2_Stream3->M0AR = (uint32_t)src;
    DMA2_Stream3->NDTR = n;                             /* до 65 535 (RM0090, с. 331) */
    DMA2_Stream3->CR = 3u << DMA_SxCR_CHSEL_Pos | DMA_SxCR_DIR_0 | minc
                     | DMA_SxCR_PSIZE_0 | DMA_SxCR_MSIZE_0 | DMA_SxCR_EN;
    dma_on = 1;
}

static void bus_reset(void)
{
    GPIOB->BSRR = 1u << (8 + 16);
    delay_ms(10);
    GPIOB->BSRR = 1u << 8;
    delay_ms(10);
}

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

static void bus_data(const uint8_t *p, uint32_t n)
{
    spi_idle();
    spi_setup(0, BR_WRITE);
    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_setup(1, BR_WRITE);
    DC_HIGH();
    dma_go(px, n, DMA_SxCR_MINC);
}

static void bus_fill(uint16_t c, uint32_t n)
{
    spi_idle();
    spi_setup(1, BR_WRITE);
    DC_HIGH();
    fill_word = c;
    while (n) {                                         /* весь экран — 76 800 слов: две передачи */
        uint32_t k = n > 65535u ? 65535u : n;
        spi_idle();
        dma_go(&fill_word, k, 0);
        n -= k;
    }
}

static void bus_read(uint8_t c, uint8_t *out, uint32_t n)
{
    bus_cmd(c);
    spi_idle();
    spi_setup(0, BR_READ);                              /* чтение медленнее записи */
    DC_HIGH();
    for (uint32_t i = 0; i < n; i++) {
        SPI1->DR = 0x00u;                               /* такт для ответа */
        while (!(SPI1->SR & SPI_SR_RXNE)) {
        }
        out[i] = (uint8_t)SPI1->DR;
    }
    spi_idle();
    spi_setup(0, BR_WRITE);
}

static void bus_wait_te(void)
{
    EXTI->PR = EXTI_PR_PR0;
    while (!(EXTI->PR & EXTI_PR_PR0)) {
    }
}

static const ili_bus_t bus = {bus_reset, delay_ms, bus_cmd, bus_data, bus_pixels, bus_fill, bus_read, bus_wait_te};
static ili_t lcd;
static ili_info_t info;
static ili_dirty_t dirty;
static scene_t ball = {120, 160};

int main(void)
{
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_GPIOBEN | RCC_AHB1ENR_GPIOEEN | RCC_AHB1ENR_DMA2EN;
    RCC->APB2ENR |= RCC_APB2ENR_SPI1EN | RCC_APB2ENR_SYSCFGEN;
    GPIOE->BSRR = 1u << 3;                              /* акселерометр Discovery: CS в 1 */
    GPIOE->MODER = (GPIOE->MODER & ~(3u << 6)) | 1u << 6;
    GPIOA->MODER = (GPIOA->MODER & ~(0x3Fu << 10)) | 0x2Au << 10;  /* PA5–PA7 — AF */
    GPIOA->AFR[0] = (GPIOA->AFR[0] & ~(0xFFFu << 20)) | 0x555u << 20;
    GPIOA->OSPEEDR |= 0x3Fu << 10;
    GPIOA->PUPDR = (GPIOA->PUPDR & ~(3u << 12)) | 1u << 12;        /* MISO с подтяжкой: без провода — 0xFF */
    GPIOB->BSRR = 1u << 7 | 1u << 8 | 1u << (6 + 16);
    GPIOB->MODER = (GPIOB->MODER & ~(3u << 0 | 3u << 2 | 0x3Fu << 12)) | 1u << 2 | 0x15u << 12;
    SYSCFG->EXTICR[0] = (SYSCFG->EXTICR[0] & ~SYSCFG_EXTICR1_EXTI0) | SYSCFG_EXTICR1_EXTI0_PB;
    EXTI->RTSR |= EXTI_RTSR_TR0;
    EXTI->IMR |= EXTI_IMR_MR0;
    SPI1->CR1 = SPI_CR1_MSTR | SPI_CR1_SSM | SPI_CR1_SSI | BR_WRITE;   /* режим 0, как у ST для ILI9341 */
    SPI1->CR2 = SPI_CR2_TXDMAEN;
    SPI1->CR1 |= SPI_CR1_SPE;
    DMA2_Stream3->PAR = (uint32_t)&SPI1->DR;
    SysTick_Config(168000u);

    ili_init(&lcd, &bus, 0, 1);
    if (!ili_read_info(&lcd, &info)) {                  /* без MISO или без EXTC — 0xFF… */
        GPIOB->BSRR = 1u << 1;
        ili_fill_rect(&lcd, 0, 0, (int16_t)lcd.w, (int16_t)lcd.h, ili_rgb(255, 0, 0));
        for (;;) {
        }
    }
    ili_te_on(&lcd);
    GPIOB->BSRR = 1u << 1;
    ili_dirty_add(&dirty, (ili_rect_t){0, 0, (int16_t)lcd.w, (int16_t)lcd.h});
    ili_flush(&lcd, &dirty, scene_shade, &ball);
    int16_t dx = 2, dy = 3;
    for (;;) {                                          /* маленькое окно успевает до развёртки */
        if (ball.cx + dx < BALL_R || ball.cx + dx > (int16_t)lcd.w - 1 - BALL_R)
            dx = (int16_t)-dx;
        if (ball.cy + dy < BALL_R || ball.cy + dy > (int16_t)lcd.h - 1 - BALL_R)
            dy = (int16_t)-dy;
        scene_move(&ball, &dirty, (int16_t)(ball.cx + dx), (int16_t)(ball.cy + dy));
        bus.wait_te();
        ili_flush(&lcd, &dirty, scene_shade, &ball);
    }
}

fw22o_fsmc.c — прошивка STM32F407: выводы FSMC, тайминги, DMA2 поток 0, круг по TE.

/* fw22o_fsmc.c — раздел 22о: ILI9341 на STM32F407 по шине 8080 через FSMC, 168 МГц.
 * NE1 — PD7, NOE — PD4, NWE — PD5, D/C (RS) — FSMC_A18 = PD13; D0–D15: PD14, PD15, PD0, PD1, PE7–PE15,
 * PD8, PD9, PD10 — всё AF12 (DS8626, таблица 9). RST — PB8, TE — PB0 (EXTI0), подсветка — PB1.
 * Тайминги — как у BSP STM324xG-EVAL для его дисплея на FSMC: ADDSET 5, DATAST 9, режим 1:
 * запись 5 + 9 + 1 = 15 тактов HCLK = 89,3 нс. Пиксели — DMA2 поток 0 «память → память»
 * (только DMA2 так умеет, RM0090, с. 311). Шина ili_bus_t — bus22o_fsmc.c.
 * Тактирование до 168 МГц — как в разделе 6 (здесь не повторяется). */
#include "stm32f4xx.h"
#include "ili22o.h"

extern const ili_bus_t fsmc_bus_dma;

#define BTR(addset, datast, busturn) \
    (((uint32_t)(addset) << FSMC_BTR1_ADDSET_Pos) | ((uint32_t)(datast) << FSMC_BTR1_DATAST_Pos) | \
     ((uint32_t)(busturn) << FSMC_BTR1_BUSTURN_Pos))

static volatile uint32_t ticks;
static uint8_t dma_on;

void SysTick_Handler(void)
{
    ticks++;
}

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

void lcd_hw_reset(void)
{
    GPIOB->BSRR = 1u << (8 + 16);
    lcd_hw_delay_ms(10);
    GPIOB->BSRR = 1u << 8;
    lcd_hw_delay_ms(10);
}

void lcd_hw_wait_te(void)               /* фронт TE ставит флаг EXTI0; прерывание в NVIC не включено */
{
    EXTI->PR = EXTI_PR_PR0;
    while (!(EXTI->PR & EXTI_PR_PR0)) {
    }
}

void lcd_dma_wait(void)
{
    if (!dma_on)
        return;
    while (!(DMA2->LISR & DMA_LISR_TCIF0)) {
    }
    DMA2->LIFCR = DMA_LIFCR_CTCIF0 | DMA_LIFCR_CHTIF0 | DMA_LIFCR_CTEIF0 | DMA_LIFCR_CDMEIF0 | DMA_LIFCR_CFEIF0;
    dma_on = 0;
}

/* «Память → память»: источник — DMA_SxPAR, приёмник — DMA_SxM0AR (RM0090, рис. 38). */
void lcd_dma_start(uint32_t dst, const uint16_t *src, uint32_t n, int src_inc)
{
    DMA2_Stream0->PAR = (uint32_t)src;
    DMA2_Stream0->M0AR = dst;                           /* адрес данных дисплея, без приращения */
    DMA2_Stream0->NDTR = n;
    DMA2_Stream0->FCR = DMA_SxFCR_DMDIS | DMA_SxFCR_FTH_0;          /* FIFO обязателен в этом режиме */
    DMA2_Stream0->CR = DMA_SxCR_DIR_1 | (src_inc ? DMA_SxCR_PINC : 0u)
                     | DMA_SxCR_PSIZE_0 | DMA_SxCR_MSIZE_0 | DMA_SxCR_PL_1 | DMA_SxCR_EN;
    dma_on = 1;
}

static void gpio_af12(GPIO_TypeDef *g, uint32_t pins)
{
    for (uint32_t p = 0; p < 16u; p++) {
        if (!(pins >> p & 1u))
            continue;
        g->MODER = (g->MODER & ~(3u << 2 * p)) | 2u << 2 * p;
        g->OSPEEDR |= 3u << 2 * p;
        g->AFR[p >> 3] = (g->AFR[p >> 3] & ~(15u << 4 * (p & 7u))) | 12u << 4 * (p & 7u);
    }
}

static void fsmc_lcd_init(void)
{
    RCC->AHB3ENR |= RCC_AHB3ENR_FSMCEN;
    FSMC_Bank1->BTCR[1] = BTR(5, 9, 0);                 /* BTR1: как BSP STM324xG-EVAL */
    FSMC_Bank1->BTCR[0] = (FSMC_Bank1->BTCR[0]          /* после сброса — NOR с MUXEN, его снять */
                           & ~(FSMC_BCR1_MUXEN | FSMC_BCR1_MTYP | FSMC_BCR1_MWID | FSMC_BCR1_WAITEN | FSMC_BCR1_EXTMOD))
                        | FSMC_BCR1_MWID_0 | FSMC_BCR1_WREN | FSMC_BCR1_MBKEN;
}

static ili_t lcd;
static ili_dirty_t dirty;
static scene_t ball = {120, 160};

int main(void)
{
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN | RCC_AHB1ENR_GPIODEN | RCC_AHB1ENR_GPIOEEN | RCC_AHB1ENR_DMA2EN;
    RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;
    gpio_af12(GPIOD, 1u << 0 | 1u << 1 | 1u << 4 | 1u << 5 | 1u << 7 | 1u << 8 | 1u << 9 | 1u << 10
                         | 1u << 13 | 1u << 14 | 1u << 15);
    gpio_af12(GPIOE, 0xFF80u);                          /* PE7–PE15 */
    GPIOB->BSRR = 1u << 8;                              /* RST в 1 */
    GPIOB->MODER = (GPIOB->MODER & ~(3u << 0 | 3u << 2 | 3u << 16)) | 1u << 2 | 1u << 16;   /* PB1, PB8 — выходы, PB0 — вход */
    SYSCFG->EXTICR[0] = (SYSCFG->EXTICR[0] & ~SYSCFG_EXTICR1_EXTI0) | SYSCFG_EXTICR1_EXTI0_PB;
    EXTI->RTSR |= EXTI_RTSR_TR0;
    EXTI->IMR |= EXTI_IMR_MR0;
    fsmc_lcd_init();
    SysTick_Config(168000u);                            /* 1 мс */

    ili_init(&lcd, &fsmc_bus_dma, 0, 1);
    ili_te_on(&lcd);
    GPIOB->BSRR = 1u << 1;                              /* подсветка */
    ili_dirty_add(&dirty, (ili_rect_t){0, 0, (int16_t)lcd.w, (int16_t)lcd.h});
    ili_flush(&lcd, &dirty, scene_shade, &ball);
    ili_text(&lcd, 4, 8, "ТЕМПЕРАТУРА 23,5 °C", 1, ili_rgb(255, 255, 255), ili_rgb(0, 0, 64));
    int16_t dx = 2, dy = 3;
    for (;;) {                                          /* круг отскакивает от краёв, кадр — по TE */
        int16_t x = (int16_t)(ball.cx + dx), y = (int16_t)(ball.cy + dy);
        if (x < BALL_R || x > (int16_t)lcd.w - 1 - BALL_R)
            dx = (int16_t)-dx;
        if (y < 40 + BALL_R || y > (int16_t)lcd.h - 1 - BALL_R)
            dy = (int16_t)-dy;
        scene_move(&ball, &dirty, (int16_t)(ball.cx + dx), (int16_t)(ball.cy + dy));
        fsmc_bus_dma.wait_te();
        ili_flush(&lcd, &dirty, scene_shade, &ball);
    }
}

Дальше: раздел 43 «Аппаратная криптография: CRC, RNG, HASH, CRYP».

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