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 номер знака в Юникоде
22о.1 ILI9341 против ST7735: размер, MADCTL, формат точки, шина и код микросхемы
Память кадра — 240 × 320 слов, ровно по стеклу, без сдвига, который
мешал в 22з. Команды окна и записи те же, вторая команда окна у ILI9341
в документах ST зовётся PASET — «страницы», то есть строки. Что нового:
MADCTL(0x36) — кMY,MX,MVиBGRиз 22з добавленыML(0x10) иMH(0x04). Они меняют не порядок записи в память, а направление развёртки стекла (так их описывают маски LVGL) — это понадобится в 22о.5. Повороты драйвер берёт из таблицы QMK: 0x00,MX|MV,MX|MY,MV|MY; с битомBGRв модели это 08, 68, C8 и A8, и во всех четырёх картинка совпала с эталоном точка в точку. Драйвер ST ставит при запуске 0xC8 — тот же 180°.PIXSET(0x3A) — 0x55 даёт 16 бит на точку (RGB565, 2 байта), 0x66 — 18 бит, по SPI это 3 байта на точку. LVGL пишет 0x55, QMK — 0x05: по документации производителя младшая тройка битов — формат для шины MCU, старшая — для RGB-интерфейса, так что для SPI и 8080 оба значения одинаковы (не проверено). 18 бит — это 230 411 байт на кадр вместо 153 611: на 42 МГц 43,89 мс вместо 29,26.- Шина выбирается выводами
IM0–IM3прямо на шлейфе стекла: SPI или параллельная 8080 разной ширины. Какое сочетание что значит — в документации производителя; на готовом модуле выводы уже запаяны, и модуль «на SPI» по 8080 не заработает. - Чтение. По
0xD3контроллер отдаёт холостой байт и три байта кода: 00, 93, 41 (у ST —ILI9341_ID= 0x9341, длина ответа 3). По0x09(RDDST) — холостой байт и 32 бита состояния: сон, показ, формат,TE.
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 Итоги
- ILI9341: память 240 × 320 без сдвига;
MADCTL— повороты 08, 68, C8, A8 и развёрткаML;PIXSET0x55 — 16 бит, 0x66 — 18 бит (230 411 байт на кадр); код по0xD3— 00 93 41 после холостого байта. - Запуск: 6 команд, 9 байт, 240 мс пауз.
- Кадр по SPI — 153 611 байт: 68,27 мс на 18 МГц (F103), 58,52 мс на
21 МГц, 29,26 мс на 42 МГц (F407); заливка — 2 передачи DMA,
NDTRдо 65 535. - Кадр 153 600 байт не помещается ни в 20 480 байт F103C8, ни в 131 072 байта SRAM1 + SRAM2 F407; CCM DMA не видит. Буфер — две строки, 1280 байт; шаг круга — в среднем 2435 байт и 0,46 мс вместо 29,26 мс.
- FSMC: команда по 0x6000 0000, данные по 0x6008 0000 (A18); 15 тактов — 89,29 нс на точку, кадр 6,858 мс, 145,8 кадра в секунду; DMA — только DMA2 «память → память».
- Разрыв: запись от фронта TE должна идти впереди луча. FSMC — 0 из 7
показов с разрывом, SPI 42 МГц на весь экран — 12 из 19, окно 240 × 100 —
0; 180° без
MLи альбомная — 6 из 7. - Шрифт 12 × 20: моно 40 байт на знак, сглаженный 120; строка одним окном — 20 передач DMA против 380; UTF-8 → кодовая точка → двоичный поиск.
- Грабли: MISO и EXTC, байты при 8-битном DMA,
BGR, имена 0x34/0x35 в QMK, подсветка, PD12–PD15 на Discovery.
Полные исходники
Таблицы шрифта 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».