22з. Цветной TFT-дисплей ST7735 по SPI: инициализация, окно адресов, RGB565, шрифт, DMA и частичное обновление
В разделе 22д контроллер HD44780 сам рисовал знаки, а программа слала ему коды. Цветной TFT 128×160 на контроллере ST7735 устроен так же — память кадра стоит в самом модуле, — только знаков он не знает: каждую точку рисует программа, и точек 20 480 по два байта. Общий взгляд на такие дисплеи — в разделе 22а (два пути к стеклу, время кадра по SPI), грабли прототипа с порядком байтов и памятью — в 83.3. Здесь — конкретный модуль на STM32F103 от первой команды до перерисовки одной цифры: выводы и SPI1, инициализация, окно адресов, RGB565, прямоугольник, линия и шрифт, DMA, кадры в секунду и частичное обновление, когда буфер кадра в ОЗУ не помещается.
Опоры: RM0008 (DocID13902 Rev 17): запрос SPI1_TX — канал 3 DMA1, с. 281;
биты MSIZE и MINC, с. 285; CNDTR — до 65 535, с. 286; передача SPI
через DMA и ожидание TXE, потом BSY, с. 719; бит DFF — только при
SPE = 0, с. 742; делитель BR, с. 743. DocID13587 Rev 17
(STM32F103x8/xB): 20 Кбайт ОЗУ, с. 1; PA1–PA7 без допуска 5 В, с. 29;
такт SPI ведущего — до 18 МГц, с. 71. Документа на ST7735 в проекте нет.
Коды команд, размер 128×160, значение COLMOD для RGB565, четыре значения
MADCTL для поворотов и порядок «старший байт первым» — из драйвера ST в
паках Keil (STM32G4xx DFP 1.6.0, Drivers/BSP/Components/st7735, версия
2.0.0); режим SPI и предел записи 15 МГц — из файла платы
stm32f4xx_nucleo.c (STM32F4xx DFP 2.17.1), где этот дисплей подключён к
Nucleo. Назначение битов MADCTL, паузы после сброса и сна, размер памяти
контроллера и сдвиг стекла — по документации производителя, не проверено:
в модели это допущения. Все числа ниже — прогон драйвера
_verify/tft22i/tft22i.c на компьютере (gcc 12.2.0) со своей моделью
контроллера и стекла на Python 3.11; прошивка собрана armclang 6.24 под
Cortex-M3.
Как это называется в документации
Оригинал Перевод D/C (data/command), A0, RS выбор: команда или данные frame memory, GRAM память кадра в контроллере CASET / RASET окно: столбцы / строки RAMWR (memory write) запись в память кадра MADCTL (memory data access control) порядок обхода памяти: зеркала, поворот, RGB или BGR COLMOD (interface pixel format) формат пикселя на входе SWRESET, SLPOUT, DISPON программный сброс, выход из сна, включение показа backlight подсветка transmit-only, TXE, BSY только передача; буфер передачи пуст; блок занят dirty rectangle «грязный» прямоугольник — что изменилось с прошлого кадра
22з.1 Модуль и подключение: выводы, D/C и SPI1
Модуль — стекло 128×160 и контроллер ST7735 с памятью кадра: что записано,
то он и показывает сам, без участия микроконтроллера (путь «дисплей со
своим контроллером» из 22а.1). Формат пикселя задаёт COLMOD: 0x03 —
12 бит, 0x05 — RGB565, 0x06 — 18 бит; здесь RGB565, два байта на точку.
Выводы модуля: питание, земля, такт и данные SPI, выбор CS, линия D/C,
сброс RST и подсветка; подписи на платах разные — данные бывают
подписаны SDA, D/C — A0 или RS. Чтения нет: MISO не нужен, и SPI1
работает только на передачу.
| Модуль | F103 | Что |
|---|---|---|
| SCK | PA5 | такт SPI1 |
| MOSI (SDA) | PA7 | данные SPI1 |
| CS | PA4 | выбор: на шине один дисплей — держим в нуле |
| D/C | PA3 | 0 — байт команды, 1 — параметры и пиксели |
| RST | PA2 | сброс |
| подсветка | PA1 | светодиоды подсветки |
D/C — то же, что RS у HD44780 (22д): тот же байт значит команду или данные. PA1–PA7 у F103 5 В не допускают (с. 29), так что модуль питают от 3,3 В; есть ли на модуле свой стабилизатор и каков ток подсветки — по описанию модуля, здесь не проверялось.
SPI1. Он на APB2, 72 МГц; делитель BR (с. 743) даёт 36, 18 или 9 МГц
при /2, /4, /8. Предел такта ведущего у F103 — 18 МГц (с. 71), у ST7735 на
запись — 15 МГц по комментарию в файле платы ST. Прошивка берёт /8 —
9 МГц, внутри обоих пределов. Режим — 3 (CPOL = 1, CPHA = 1), как в том
же файле ST. Команды — по 8 бит, пиксели — словами по 16 бит (DFF = 1):
16-битное слово SPI уходит старшим байтом вперёд, и переставлять байты
цвета, как в 83.3, не нужно.
Главное правило шины: D/C и DFF трогают, только когда прошлый байт
ушёл целиком. По флагу DMA последнее слово только легло в буфер
передачи; конец — TXE = 1, а потом BSY = 0 (с. 719). DFF меняют
только при выключенном SPI (с. 742):
static void spi_idle(void)
{
if (DMA1_Channel3->CCR & DMA_CCR1_EN) {
while (!(DMA1->ISR & DMA_ISR_TCIF3)) {
}
DMA1->IFCR = DMA_IFCR_CTCIF3;
DMA1_Channel3->CCR = 0;
}
while (!(SPI1->SR & SPI_SR_TXE)) {
}
while (SPI1->SR & SPI_SR_BSY) {
}
(void)SPI1->DR; /* приёма нет: снять RXNE и OVR (раздел 18.6) */
(void)SPI1->SR;
}
static void spi_width(uint8_t w16) /* DFF — только при выключенном SPI (RM0008, с. 742) */
{
if (wide == w16)
return;
SPI1->CR1 &= ~SPI_CR1_SPE;
if (w16)
SPI1->CR1 |= SPI_CR1_DFF;
else
SPI1->CR1 &= ~SPI_CR1_DFF;
SPI1->CR1 |= SPI_CR1_SPE;
wide = w16;
}
С этого начинается каждая функция шины: байт команды, параметры, строка
пикселей через DMA. Драйвер tft22i.c регистров не знает — он зовёт шину
через таблицу функций, и тот же код в стенде печатает байты для модели.
Для сравнения: монохромный SSD1306 128 × 64 в сканере 198р — весь экран
1024 байта, а здесь 40 960: в 40 раз больше.
22з.2 Инициализация: SWRESET, SLPOUT, COLMOD, MADCTL
void tft_init(tft_t *t, const tft_bus_t *bus, uint8_t rot, uint8_t bgr, uint8_t col0, uint8_t row0)
{
static const uint8_t rgb565 = 0x05u;
t->bus = bus;
t->col0 = col0;
t->row0 = row0;
t->madctl = bgr ? MAD_BGR : 0u;
bus->reset();
bus->cmd(ST_SWRESET);
bus->delay_ms(150); /* с запасом: документа производителя в проекте нет */
bus->cmd(ST_SLPOUT);
bus->delay_ms(120);
bus->cmd(ST_COLMOD);
bus->data(&rgb565, 1); /* 16 бит на пиксель */
tft_rotate(t, rot);
bus->cmd(ST_NORON);
bus->cmd(ST_DISPON);
}
Импульс RST, программный сброс, выход из сна, формат пикселя, порядок
обхода памяти, обычный режим и включение показа: в прогоне 6 команд и
8 байт, на проводе 7,1 мкс при 9 МГц. Время запуска — паузы: 150 мс после
SWRESET и 120 мс после SLPOUT, всего 270 мс. Сколько нужно на самом
деле, написано в документе производителя; паузы здесь с запасом и не
проверены. Драйвер ST из пака обходится без SWRESET и без пауз, зато
задаёт ещё частоту кадров (B1–B3), режим инверсии (B4), питание
(C0–C4), VCOM (C5) и гамму (E0, E1) — значения этих регистров зависят от
стекла. Короткая последовательность полагается на их значения после
сброса — на настоящем модуле это не проверялось.
MADCTL. Биты: MY (0x80) — зеркало строк, MX (0x40) — зеркало
столбцов, MV (0x20) — обмен строк и столбцов, то есть поворот на 90°,
BGR (0x08) — стекло с порядком синий-зелёный-красный. tft_rotate
берёт для четырёх поворотов 0x00, 0x60, 0xC0 и 0xA0 — те же четыре
значения, что в таблице поворотов драйвера ST; какое из них «прямо»,
зависит от того, как стекло стоит на модуле. С битом BGR модель
получила 0x08, 0x68, 0xC8 и 0xA8, и во всех четырёх поворотах картинка
совпала с эталоном точка в точку. При MV ширина и высота меняются
местами (160×128), а с ними и сдвиг стекла — о нём 22з.7.
22з.3 Окно CASET/RASET, RAMWR и RGB565
void tft_window(const tft_t *t, int16_t x, int16_t y, int16_t w, int16_t h)
{
uint16_t x0 = (uint16_t)(x + t->xoff), x1 = (uint16_t)(x0 + w - 1);
uint16_t y0 = (uint16_t)(y + t->yoff), y1 = (uint16_t)(y0 + h - 1);
uint8_t ca[4] = {(uint8_t)(x0 >> 8), (uint8_t)x0, (uint8_t)(x1 >> 8), (uint8_t)x1};
uint8_t ra[4] = {(uint8_t)(y0 >> 8), (uint8_t)y0, (uint8_t)(y1 >> 8), (uint8_t)y1};
t->bus->cmd(ST_CASET);
t->bus->data(ca, 4);
t->bus->cmd(ST_RASET);
t->bus->data(ra, 4);
t->bus->cmd(ST_RAMWR); /* дальше — пиксели слева направо, сверху вниз */
}
Окно — 11 байт: команда и два 16-битных края для столбцов, то же для
строк, и RAMWR. Дальше пиксели идут подряд, сколько угодно, пока не
придёт новая команда; дойдя до правого края окна, запись переходит на
следующую строку. Весь экран модуля со сдвигом стекла 2 и 1 — CASET
00 02 00 81 (столбцы 2…129) и RASET 00 01 00 A0 (строки 1…160).
RGB565. Перевод из 24-битного цвета — функция из 83.3: старшие 5, 6 и
5 бит. Оранжевый (255, 128, 0) — FC00. Отбрасывание младших битов —
не то же, что округление: серый 200 даёт CE59, а с округлением
(v · 31 + 127) / 255 — C638. Обратно в 8 бит (старшие биты повторяются
в младших) это 206, 203, 206 против 198, 199, 198. По всем 256 уровням
наибольшая ошибка отбрасывания — 7, 3 и 7 единиц по красному, зелёному и
синему, округления — 4, 2 и 4. Для заливок разница не видна, для плавных
переходов цвета округление честнее.
22з.4 Прямоугольник, линия и шрифт 5×7
Прямоугольник — окно и w · h слов одного цвета. DMA шлёт одно и то
же слово без шага по памяти (MINC = 0, с. 285), так что памяти под
заливку не нужно вовсе. Точка — прямоугольник 1×1: 11 байт окна ради
2 байт цвета.
Линия — Брезенхем. Точки те же, что у классического алгоритма, но идущие подряд по главной оси уходят одним прямоугольником:
void tft_line(const tft_t *t, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t c)
{
int dx = x1 > x0 ? x1 - x0 : x0 - x1, sx = x0 < x1 ? 1 : -1;
int dy = y1 > y0 ? y0 - y1 : y1 - y0, sy = y0 < y1 ? 1 : -1;
int err = dx + dy, flat = dx >= -dy;
int x = x0, y = y0, rx = x0, ry = y0; /* rx, ry — начало текущего отрезка */
for (;;) {
int px = x, py = y, e2 = 2 * err;
if (x == x1 && y == y1) {
tft_fill_rect(t, (int16_t)(rx < px ? rx : px), (int16_t)(ry < py ? ry : py),
(int16_t)((rx > px ? rx - px : px - rx) + 1), (int16_t)((ry > py ? ry - py : py - ry) + 1), c);
return;
}
if (e2 >= dy) { err += dy; x += sx; }
if (e2 <= dx) { err += dx; y += sy; }
if (flat ? y != py : x != px) { /* шаг по второй оси — отрезок кончился */
tft_fill_rect(t, (int16_t)(rx < px ? rx : px), (int16_t)(ry < py ? ry : py),
(int16_t)((rx > px ? rx - px : px - rx) + 1), (int16_t)((ry > py ? ry - py : py - ry) + 1), c);
rx = x;
ry = y;
}
}
}
18 отрезков из центра экрана во все стороны — 1200 точек. Модель сверила их с классическим Брезенхемом точка в точку — расхождений 0. Ушло 8725 байт; по точке было бы 13 байт на точку — 15 600.
Шрифт — свой массив 5×7: знак — 5 байт-столбцов, бит 0 — верхняя строка, подряд от пробела до «Z» — 59 знаков, 295 байт (та же раскладка, что у шрифта в 198р). Клетка знака — 6×8 точек с промежутком, при масштабе 3 — 18×24. Знак рисуется окном на клетку и строками вместе с фоном, поэтому старый знак стирать не нужно.
Картинка проверки tft_demo — заливка фона, надпись, три прямоугольника
красный, зелёный, синий, три линии и подпись — совпала с эталоном модели
во всех 20 480 точках; на неё ушло 51 926 байт и 1259 переключений D/C.
22з.5 Скорость: байты, DMA и кадры в секунду
Время на проводе — байты × 8 / частота SPI, без пауз между словами; так шлёт DMA. Весь экран тремя способами:
| Способ | Байт | 9 МГц | 18 МГц | 36 МГц |
|---|---|---|---|---|
по точке (tft_pixel) |
266 240 | 236,66 мс | 118,33 мс | 59,16 мс |
| окно и заливка через DMA | 40 971 | 36,42 мс | 18,21 мс | 9,10 мс |
| кадров в секунду, по точке | — | 4,2 | 8,5 | 16,9 |
| кадров в секунду, окно и DMA | — | 27,5 | 54,9 | 109,8 |
По точке — 6,5 раза больше байт и 122 879 переключений D/C против 6. Вывод
через строки-буферы (22з.6) даёт те же 40 971 байт, что и заливка. 36 МГц
(делитель 2) — за пределом и F103 (18 МГц), и ST7735 (15 МГц по ST): эта
колонка лишь показывает потолок. Заливка всего экрана — одна передача
DMA на 20 480 слов: CNDTR вмещает до 65 535 (с. 286).
Цикл с опросом TXE шлёт те же байты, но процессор занят всё время, а
между словами остаются паузы (раздел 18.8) — их модель не считает. С DMA
процессор свободен: пока уходит строка 128 точек на 18 МГц — 113,8 мкс,
— у ядра на 72 МГц есть 8192 такта, 64 на точку, чтобы посчитать
следующую строку. Поэтому строк-буферов два: DMA шлёт одну, процессор
заполняет другую, а spi_idle в начале следующей передачи дожидается
конца предыдущей. Для сравнения, кадр 240×320 на тех же 18 МГц —
68,27 мс (18.8): точек там в 3,75 раза больше.
22з.6 Двойной буфер не помещается: «грязные» прямоугольники
Кадр 128×160 по два байта — 40 960 байт, двойной буфер — 81 920, а ОЗУ у
F103C8 — 20 Кбайт, 20 480 байт (с. 1). Не помещается и один кадр (83.3).
Значит, кадр живёт только в памяти дисплея, а микроконтроллер присылает
изменившиеся куски. Сцена описана функцией «цвет точки (x, y)» —
meter_shade прибора из примера: заголовок, число крупным шрифтом и
полоса. Перерисовка берёт список прямоугольников, считает каждый строку за
строкой в два буфера по строке и шлёт через DMA:
void tft_flush(const tft_t *t, tft_dirty_t *d, tft_shader_t shade, const void *ctx)
{
for (uint8_t k = 0; k < d->n; k++) {
tft_rect_t r = d->r[k];
if (!clip(t, &r.x, &r.y, &r.w, &r.h))
continue;
tft_window(t, r.x, r.y, r.w, r.h);
for (int16_t y = r.y; y < r.y + r.h; y++) {
uint16_t *p = line[y & 1]; /* пока DMA шлёт прошлую строку */
for (int16_t i = 0; i < r.w; i++)
p[i] = shade(ctx, (int16_t)(r.x + i), y);
t->bus->pixels(p, (uint32_t)r.w);
}
}
d->n = 0;
}
Две строки по 160 точек — 640 байт: столько статической памяти и показал
armclang у tft22i.o. Что грязно, решает сам прибор: meter_set
сравнивает новый текст со старым по знакам и отмечает клетки, которые
изменились, а у полосы — только полоску между старым и новым концом.
tft_dirty_add сливает касающиеся прямоугольники в один.
| Шаг | Прямоугольники | Точек | Байт | 18 МГц |
|---|---|---|---|---|
| весь экран, 23,4 | 1 | 20 480 | 40 971 | 18,21 мс |
| 23,4 → 23,5 | клетка «4» и полоска в точку | 444 | 910 | 0,40 мс |
| 23,5 → 24,9 | две клетки и полоска 4 точки | 912 | 1857 | 0,83 мс |
| 24,9 → 25,0 | две клетки | 864 | 1750 | 0,78 мс |
| 25,0 → 19,9 | клетки «2» и «5» слиты в одну, клетка «0» и полоска 15 точек | 1476 | 2985 | 1,33 мс |
Сто шагов по 0,1 (от 20,0 до 29,9) — 98 129 байт, в среднем 981 байт и 0,44 мс на шаг вместо 40 971 байта и 18,21 мс на полную перерисовку. После каждого шага стекло модели совпало с эталоном прибора с тем же числом — 0 расхождений: частичное обновление ничего не теряет.
Дисплей показывает свою память сам и о наших записях не знает: клетка,
которую перезаписывают в момент, когда стекло её обновляет, на один кадр
может показаться наполовину старой. У ST7735 для этого есть команда
TEON (0x35) — включить линию TE; модель её не считает, и если линии на
модуле нет, от разрыва спасает только малый размер перерисовки.
22з.7 Грабли
Картинка сдвинута на 1–2 точки, у края полоска мусора. У модели модуля
Б память 132 × 162, а стекло стоит со сдвигом 2 столбца и 1 строку. С
нулевым сдвигом в драйвере картинка уезжает на 2 влево и 1 вверх, а
справа и снизу видны 446 точек мусора памяти (2 × 160 + 128 − 2).
Сдвиг задаётся в tft_init (col0, row0); у настоящего модуля его берут
из описания или подбирают по краю картинки.
После поворота сдвиг вернулся. При MV столбцы становятся строками —
и сдвиги тоже. Не поменять их местами — в альбомной ориентации картинка
съезжает на точку по обеим осям, мусор — 287 точек. tft_rotate меняет
их сама.
Красный стал синим. Стекло с порядком B-G-R, а бит BGR в MADCTL не
поставлен: в модели модуля Б красный прямоугольник синий, синий красный,
19 202 точки не совпали. Тот же код на модуле А (стекло R-G-B, без бита) —
0. Если красный синий — переставить бит, а не цвета в программе.
Пропадают точки в конце строк, текст исчезает. D/C опускают по флагу
окончания DMA: в этот момент последние слова ещё в SPI и уходят как
команды. В модели при двух словах на хвосте из 24 808 точек картинки
записано 22 816, контроллер получил 1008 «команд» из байтов цвета (07, 08,
1F, E0, F8, FF), а вся строка после такой «команды» пропадает целиком —
от текста осталась верхняя строка точек. Лечится spi_idle: TXE = 1,
потом BSY = 0 (с. 719), и только потом D/C.
Цвета «поплыли» при выводе через DMA. Массив uint16_t ушёл байтами,
младшим вперёд (83.3). Пиксели — словами при DFF = 1.
DFF не переключается. Бит меняют только при SPE = 0 (с. 742) —
spi_width выключает и включает SPI.
Вывод медленный. Рисуют по точке: 13 байт и шесть переключений D/C
на точку; экран по точке — 118,33 мс на 18 МГц против 18,21 мс окном. Каждое
переключение D/C — запись в GPIO и ожидание BSY; с медленной функцией
вывода (HAL вместо BSRR) по точке выходит ещё дольше. Рисовать окнами и
отрезками.
22з.8 Итоги
- ST7735: стекло 128×160, своя память кадра, RGB565 (
COLMOD0x05); D/C — команда или данные, чтения нет — SPI1 только на передачу. - F103: SPI1 72 МГц / 8 = 9 МГц, режим 3; предел F103 — 18 МГц, ST7735 на
запись — 15 МГц. D/C и
DFF— только послеTXE= 1 иBSY= 0. - Запуск: 6 команд, 8 байт и 270 мс пауз;
MADCTL— поворот (0x00, 0x60, 0xC0, 0xA0) и порядок цветов (0x08). - Окно — 11 байт, весь экран — 40 971 байт: 36,42 мс на 9 МГц, 18,21 мс (54,9 кадра в секунду) на 18 МГц; по точке — 266 240 байт.
- RGB565: отбрасывание ошибается до 7/3/7 единиц, округление — до 4/2/4.
- Линия — Брезенхем отрезками: 1200 точек за 8725 байт вместо 15 600; шрифт 5×7 — 295 байт.
- Кадр 40 960 байт больше 20 480 байт ОЗУ F103C8: картинку хранит дисплей, программа шлёт «грязные» прямоугольники через две строки по 640 байт — шаг прибора в среднем 981 байт, 0,44 мс на 18 МГц.
- Грабли: сдвиг стекла 2/1 и его обмен при повороте,
BGR, D/C раньше конца передачи.
Полные исходники
tft22i.h — команды ST7735, биты MADCTL, шина, драйвер, прибор: типы и прототипы.
/* tft22i.h — раздел 22з: цветной TFT 128×160 на контроллере ST7735 без регистров микроконтроллера.
* Драйвер говорит с дисплеем через tft_bus_t: в прошивке fw22i.c это SPI1 + DMA1 канал 3 на F103,
* в стенде host22i.c — печать байтов, которые читает модель контроллера model22i.py.
* Коды команд — как в st7735_reg.h драйвера ST из паков Keil (BSP Components/st7735). */
#ifndef TFT22I_H
#define TFT22I_H
#include <stdint.h>
/* ── команды ST7735 ───────────────────────────────────────────────────── */
#define ST_SWRESET 0x01u /* программный сброс */
#define ST_SLPOUT 0x11u /* выход из сна */
#define ST_NORON 0x13u /* обычный режим (не частичный) */
#define ST_DISPON 0x29u /* включить показ */
#define ST_CASET 0x2Au /* окно: столбцы от и до */
#define ST_RASET 0x2Bu /* окно: строки от и до */
#define ST_RAMWR 0x2Cu /* дальше — пиксели в окно */
#define ST_MADCTL 0x36u /* порядок обхода памяти и цветов */
#define ST_COLMOD 0x3Au /* формат пикселя: 0x05 — RGB565 */
/* ── MADCTL: биты зеркал, обмена осей и порядка цветов ────────────────── */
#define MAD_MY 0x80u /* зеркало строк */
#define MAD_MX 0x40u /* зеркало столбцов */
#define MAD_MV 0x20u /* обмен строк и столбцов: поворот на 90° */
#define MAD_BGR 0x08u /* стекло с порядком B-G-R */
#define TFT_W 128u /* стекло в книжной ориентации */
#define TFT_H 160u
#define TFT_LINE_MAX 160u /* самая длинная строка — в альбомной */
#define TFT_DIRTY_MAX 4u /* «грязных» прямоугольников до слияния в один */
/* Шина: каждая функция сама дожидается конца предыдущей передачи (DMA, TXE, BSY) и только потом
* трогает D/C. pixels и fill могут вернуться раньше, чем байты ушли: передачу ведёт DMA. */
typedef struct {
void (*reset)(void); /* RST в 0 и обратно */
void (*delay_ms)(uint32_t ms);
void (*cmd)(uint8_t c); /* D/C = 0: байт команды */
void (*data)(const uint8_t *p, uint32_t n); /* D/C = 1: байты параметров */
void (*pixels)(const uint16_t *px, uint32_t n); /* D/C = 1: n пикселей, старший байт первым */
void (*fill)(uint16_t color, uint32_t n); /* D/C = 1: n пикселей одного цвета */
} tft_bus_t;
typedef struct {
const tft_bus_t *bus;
uint16_t w, h; /* размер после поворота */
uint8_t madctl;
uint8_t col0, row0; /* сдвиг стекла в памяти контроллера (книжная ориентация) */
uint8_t xoff, yoff; /* тот же сдвиг для текущего поворота */
} tft_t;
typedef struct { int16_t x, y, w, h; } tft_rect_t;
typedef struct { tft_rect_t r[TFT_DIRTY_MAX]; uint8_t n; } tft_dirty_t;
typedef uint16_t (*tft_shader_t)(const void *ctx, int16_t x, int16_t y); /* цвет точки сцены */
uint16_t tft_rgb(uint8_t r, uint8_t g, uint8_t b); /* RGB888 → RGB565 отбрасыванием младших битов */
uint16_t tft_rgb_round(uint8_t r, uint8_t g, uint8_t b); /* … с округлением */
void tft_init(tft_t *t, const tft_bus_t *bus, uint8_t rot, uint8_t bgr, uint8_t col0, uint8_t row0);
void tft_rotate(tft_t *t, uint8_t rot); /* 0…3: книжная, альбомная, 180°, 270° */
void tft_window(const tft_t *t, int16_t x, int16_t y, int16_t w, int16_t h);
void tft_fill_rect(const tft_t *t, int16_t x, int16_t y, int16_t w, int16_t h, uint16_t c);
void tft_pixel(const tft_t *t, int16_t x, int16_t y, uint16_t c);
void tft_line(const tft_t *t, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t c);
int tft_glyph(char ch, int col, int row); /* точка знака 5×7: 1 — горит */
void tft_text(const tft_t *t, int16_t x, int16_t y, const char *s, uint8_t scale, uint16_t fg, uint16_t bg);
void tft_dirty_add(tft_dirty_t *d, tft_rect_t r);
void tft_flush(const tft_t *t, tft_dirty_t *d, tft_shader_t shade, const void *ctx);
/* ── пример: прибор с числом и полосой, перерисовка только изменившегося ── */
#define METER_X 4 /* число: масштаб 3, знак 18×24 */
#define METER_Y 40
#define METER_CELL_W 18
#define METER_CELL_H 24
#define METER_CHARS 6
#define BAR_X 4 /* полоса: рамка 120×16, внутри до 116 точек */
#define BAR_Y 100
#define BAR_W 120
#define BAR_H 16
typedef struct {
char text[METER_CHARS + 1];
uint16_t bar; /* длина полосы, 0…BAR_W − 4 */
uint16_t fg, bg, accent;
} meter_t;
uint16_t meter_shade(const void *ctx, int16_t x, int16_t y);
void meter_set(meter_t *m, tft_dirty_t *d, const char *text, uint16_t bar);
void meter_value(meter_t *m, tft_dirty_t *d, int tenths); /* «dd.d C» и полоса: 40,0 — вся */
void tft_demo(const tft_t *t); /* картинка для проверки: прямоугольники, линии, текст */
#endif
tft22i.c — драйвер: запуск, окно, прямоугольник, линия, шрифт 5×7, «грязные» прямоугольники и прибор — общий для прошивки и стенда.
/* tft22i.c — раздел 22з: драйвер ST7735 (инициализация, окно, рисование, шрифт 5×7, «грязные»
* прямоугольники) без регистров микроконтроллера: общий для прошивки fw22i.c и стенда host22i.c. */
#include "tft22i.h"
static uint16_t line[2][TFT_LINE_MAX]; /* две строки: DMA шлёт одну, процессор готовит другую */
/* Шрифт 5×7: знак — 5 столбцов, бит 0 — верхняя строка; подряд от пробела до «Z». */
static const uint8_t font5x7['Z' - ' ' + 1][5] = {
{0x00, 0x00, 0x00, 0x00, 0x00},
{0x00, 0x00, 0x00, 0x00, 0x00},
{0x00, 0x00, 0x00, 0x00, 0x00},
{0x00, 0x00, 0x00, 0x00, 0x00},
{0x00, 0x00, 0x00, 0x00, 0x00},
{0x43, 0x33, 0x08, 0x66, 0x61}, /* % */
{0x00, 0x00, 0x00, 0x00, 0x00},
{0x00, 0x00, 0x00, 0x00, 0x00},
{0x00, 0x00, 0x00, 0x00, 0x00},
{0x00, 0x00, 0x00, 0x00, 0x00},
{0x00, 0x00, 0x00, 0x00, 0x00},
{0x00, 0x00, 0x00, 0x00, 0x00},
{0x00, 0x00, 0x00, 0x00, 0x00},
{0x00, 0x08, 0x08, 0x08, 0x00}, /* - */
{0x00, 0x60, 0x60, 0x00, 0x00}, /* . */
{0x40, 0x30, 0x08, 0x06, 0x01}, /* / */
{0x3E, 0x51, 0x49, 0x45, 0x3E}, /* 0 */
{0x00, 0x42, 0x7F, 0x40, 0x00}, /* 1 */
{0x42, 0x61, 0x51, 0x49, 0x46}, /* 2 */
{0x21, 0x41, 0x45, 0x4B, 0x31}, /* 3 */
{0x18, 0x14, 0x12, 0x7F, 0x10}, /* 4 */
{0x27, 0x45, 0x45, 0x45, 0x39}, /* 5 */
{0x3C, 0x4A, 0x49, 0x49, 0x30}, /* 6 */
{0x01, 0x71, 0x09, 0x05, 0x03}, /* 7 */
{0x36, 0x49, 0x49, 0x49, 0x36}, /* 8 */
{0x06, 0x49, 0x49, 0x29, 0x1E}, /* 9 */
{0x00, 0x36, 0x36, 0x00, 0x00}, /* : */
{0x00, 0x00, 0x00, 0x00, 0x00},
{0x00, 0x00, 0x00, 0x00, 0x00},
{0x00, 0x00, 0x00, 0x00, 0x00},
{0x00, 0x00, 0x00, 0x00, 0x00},
{0x00, 0x00, 0x00, 0x00, 0x00},
{0x00, 0x00, 0x00, 0x00, 0x00},
{0x7E, 0x09, 0x09, 0x09, 0x7E}, /* A */
{0x7F, 0x49, 0x49, 0x49, 0x36}, /* B */
{0x3E, 0x41, 0x41, 0x41, 0x22}, /* C */
{0x7F, 0x41, 0x41, 0x22, 0x1C}, /* D */
{0x7F, 0x49, 0x49, 0x49, 0x41}, /* E */
{0x7F, 0x09, 0x09, 0x09, 0x01}, /* F */
{0x3E, 0x41, 0x49, 0x49, 0x7A}, /* G */
{0x7F, 0x08, 0x08, 0x08, 0x7F}, /* H */
{0x00, 0x41, 0x7F, 0x41, 0x00}, /* I */
{0x20, 0x40, 0x41, 0x3F, 0x01}, /* J */
{0x7F, 0x08, 0x14, 0x22, 0x41}, /* K */
{0x7F, 0x40, 0x40, 0x40, 0x40}, /* L */
{0x7F, 0x02, 0x0C, 0x02, 0x7F}, /* M */
{0x7F, 0x04, 0x08, 0x10, 0x7F}, /* N */
{0x3E, 0x41, 0x41, 0x41, 0x3E}, /* O */
{0x7F, 0x09, 0x09, 0x09, 0x06}, /* P */
{0x3E, 0x41, 0x51, 0x21, 0x5E}, /* Q */
{0x7F, 0x09, 0x19, 0x29, 0x46}, /* R */
{0x46, 0x49, 0x49, 0x49, 0x31}, /* S */
{0x01, 0x01, 0x7F, 0x01, 0x01}, /* T */
{0x3F, 0x40, 0x40, 0x40, 0x3F}, /* U */
{0x1F, 0x20, 0x40, 0x20, 0x1F}, /* V */
{0x3F, 0x40, 0x38, 0x40, 0x3F}, /* W */
{0x63, 0x14, 0x08, 0x14, 0x63}, /* X */
{0x03, 0x04, 0x78, 0x04, 0x03}, /* Y */
{0x61, 0x51, 0x49, 0x45, 0x43}, /* Z */
};
uint16_t tft_rgb(uint8_t r, uint8_t g, uint8_t b)
{
return (uint16_t)(((r & 0xF8u) << 8) | ((g & 0xFCu) << 3) | (b >> 3));
}
uint16_t tft_rgb_round(uint8_t r, uint8_t g, uint8_t b)
{
uint32_t r5 = (r * 31u + 127u) / 255u, g6 = (g * 63u + 127u) / 255u, b5 = (b * 31u + 127u) / 255u;
return (uint16_t)(r5 << 11 | g6 << 5 | b5);
}
void tft_rotate(tft_t *t, uint8_t rot)
{
static const uint8_t mad[4] = {0x00u, MAD_MV | MAD_MX, MAD_MX | MAD_MY, MAD_MV | MAD_MY};
uint8_t m = (uint8_t)((t->madctl & MAD_BGR) | mad[rot & 3u]);
int swap = (m & MAD_MV) != 0; /* MV: строки и столбцы меняются местами */
t->madctl = m;
t->w = (uint16_t)(swap ? TFT_H : TFT_W);
t->h = (uint16_t)(swap ? TFT_W : TFT_H);
t->xoff = swap ? t->row0 : t->col0; /* и сдвиг стекла тоже */
t->yoff = swap ? t->col0 : t->row0;
t->bus->cmd(ST_MADCTL);
t->bus->data(&t->madctl, 1);
}
void tft_init(tft_t *t, const tft_bus_t *bus, uint8_t rot, uint8_t bgr, uint8_t col0, uint8_t row0)
{
static const uint8_t rgb565 = 0x05u;
t->bus = bus;
t->col0 = col0;
t->row0 = row0;
t->madctl = bgr ? MAD_BGR : 0u;
bus->reset();
bus->cmd(ST_SWRESET);
bus->delay_ms(150); /* с запасом: документа производителя в проекте нет */
bus->cmd(ST_SLPOUT);
bus->delay_ms(120);
bus->cmd(ST_COLMOD);
bus->data(&rgb565, 1); /* 16 бит на пиксель */
tft_rotate(t, rot);
bus->cmd(ST_NORON);
bus->cmd(ST_DISPON);
}
void tft_window(const tft_t *t, int16_t x, int16_t y, int16_t w, int16_t h)
{
uint16_t x0 = (uint16_t)(x + t->xoff), x1 = (uint16_t)(x0 + w - 1);
uint16_t y0 = (uint16_t)(y + t->yoff), y1 = (uint16_t)(y0 + h - 1);
uint8_t ca[4] = {(uint8_t)(x0 >> 8), (uint8_t)x0, (uint8_t)(x1 >> 8), (uint8_t)x1};
uint8_t ra[4] = {(uint8_t)(y0 >> 8), (uint8_t)y0, (uint8_t)(y1 >> 8), (uint8_t)y1};
t->bus->cmd(ST_CASET);
t->bus->data(ca, 4);
t->bus->cmd(ST_RASET);
t->bus->data(ra, 4);
t->bus->cmd(ST_RAMWR); /* дальше — пиксели слева направо, сверху вниз */
}
static int clip(const tft_t *t, int16_t *x, int16_t *y, int16_t *w, int16_t *h)
{
if (*x < 0) { *w = (int16_t)(*w + *x); *x = 0; }
if (*y < 0) { *h = (int16_t)(*h + *y); *y = 0; }
if (*x + *w > (int16_t)t->w) *w = (int16_t)(t->w - *x);
if (*y + *h > (int16_t)t->h) *h = (int16_t)(t->h - *y);
return *w > 0 && *h > 0;
}
void tft_fill_rect(const tft_t *t, int16_t x, int16_t y, int16_t w, int16_t h, uint16_t c)
{
if (!clip(t, &x, &y, &w, &h))
return;
tft_window(t, x, y, w, h);
t->bus->fill(c, (uint32_t)w * (uint32_t)h); /* один цвет: DMA без шага по памяти */
}
void tft_pixel(const tft_t *t, int16_t x, int16_t y, uint16_t c)
{
tft_fill_rect(t, x, y, 1, 1, c); /* 11 байт окна ради 2 байт цвета */
}
/* Брезенхем: точки те же, что у классического, но подряд идущие по главной оси уходят одним
* прямоугольником — окно на отрезок, а не на точку. */
void tft_line(const tft_t *t, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint16_t c)
{
int dx = x1 > x0 ? x1 - x0 : x0 - x1, sx = x0 < x1 ? 1 : -1;
int dy = y1 > y0 ? y0 - y1 : y1 - y0, sy = y0 < y1 ? 1 : -1;
int err = dx + dy, flat = dx >= -dy;
int x = x0, y = y0, rx = x0, ry = y0; /* rx, ry — начало текущего отрезка */
for (;;) {
int px = x, py = y, e2 = 2 * err;
if (x == x1 && y == y1) {
tft_fill_rect(t, (int16_t)(rx < px ? rx : px), (int16_t)(ry < py ? ry : py),
(int16_t)((rx > px ? rx - px : px - rx) + 1), (int16_t)((ry > py ? ry - py : py - ry) + 1), c);
return;
}
if (e2 >= dy) { err += dy; x += sx; }
if (e2 <= dx) { err += dx; y += sy; }
if (flat ? y != py : x != px) { /* шаг по второй оси — отрезок кончился */
tft_fill_rect(t, (int16_t)(rx < px ? rx : px), (int16_t)(ry < py ? ry : py),
(int16_t)((rx > px ? rx - px : px - rx) + 1), (int16_t)((ry > py ? ry - py : py - ry) + 1), c);
rx = x;
ry = y;
}
}
}
int tft_glyph(char ch, int col, int row)
{
if (ch >= 'a' && ch <= 'z')
ch = (char)(ch - 'a' + 'A');
if (ch < ' ' || ch > 'Z' || col < 0 || col > 4 || row < 0 || row > 6)
return 0; /* промежуток между знаками и неизвестные знаки */
return (font5x7[ch - ' '][col] >> row) & 1;
}
void tft_text(const tft_t *t, int16_t x, int16_t y, const char *s, uint8_t scale, uint16_t fg, uint16_t bg)
{
int16_t cw = (int16_t)(6 * scale), ch = (int16_t)(8 * scale); /* знак 5×7 и промежуток в точку */
for (; *s; s++, x = (int16_t)(x + cw)) {
if (x < 0 || y < 0 || x + cw > (int16_t)t->w || y + ch > (int16_t)t->h)
return; /* знак целиком или никак */
tft_window(t, x, y, cw, ch);
for (int r = 0; r < ch; r++) {
uint16_t *p = line[r & 1];
for (int c = 0; c < cw; c++)
p[c] = tft_glyph(*s, c / scale, r / scale) ? fg : bg;
t->bus->pixels(p, (uint32_t)cw); /* фон тоже: старый знак стирать не нужно */
}
}
}
/* ── «грязные» прямоугольники ─────────────────────────────────────────── */
static int touch(tft_rect_t a, tft_rect_t b)
{
return a.x <= b.x + b.w && b.x <= a.x + a.w && a.y <= b.y + b.h && b.y <= a.y + a.h;
}
static tft_rect_t bbox(tft_rect_t a, tft_rect_t b)
{
int16_t x0 = a.x < b.x ? a.x : b.x, y0 = a.y < b.y ? a.y : b.y;
int16_t x1 = a.x + a.w > b.x + b.w ? (int16_t)(a.x + a.w) : (int16_t)(b.x + b.w);
int16_t y1 = a.y + a.h > b.y + b.h ? (int16_t)(a.y + a.h) : (int16_t)(b.y + b.h);
return (tft_rect_t){x0, y0, (int16_t)(x1 - x0), (int16_t)(y1 - y0)};
}
void tft_dirty_add(tft_dirty_t *d, tft_rect_t r)
{
if (r.w <= 0 || r.h <= 0)
return;
for (uint8_t i = 0; i < d->n; i++)
if (touch(d->r[i], r)) { /* касается или пересекает — объединить */
r = bbox(d->r[i], r);
d->r[i] = d->r[--d->n];
tft_dirty_add(d, r); /* объединение может задеть соседей */
return;
}
if (d->n < TFT_DIRTY_MAX) {
d->r[d->n++] = r;
} else { /* мест нет: слить с последним */
r = bbox(d->r[--d->n], r);
tft_dirty_add(d, r);
}
}
void tft_flush(const tft_t *t, tft_dirty_t *d, tft_shader_t shade, const void *ctx)
{
for (uint8_t k = 0; k < d->n; k++) {
tft_rect_t r = d->r[k];
if (!clip(t, &r.x, &r.y, &r.w, &r.h))
continue;
tft_window(t, r.x, r.y, r.w, r.h);
for (int16_t y = r.y; y < r.y + r.h; y++) {
uint16_t *p = line[y & 1]; /* пока DMA шлёт прошлую строку */
for (int16_t i = 0; i < r.w; i++)
p[i] = shade(ctx, (int16_t)(r.x + i), y);
t->bus->pixels(p, (uint32_t)r.w);
}
}
d->n = 0;
}
/* ── пример: прибор с заголовком, числом и полосой ────────────────────── */
uint16_t meter_shade(const void *ctx, int16_t x, int16_t y)
{
const meter_t *m = ctx;
if (y >= 6 && y < 22 && x >= 4 && x < 52) /* «TEMP», масштаб 2 */
return tft_glyph("TEMP"[(x - 4) / 12], (x - 4) % 12 / 2, (y - 6) / 2) ? m->fg : m->bg;
if (y >= METER_Y && y < METER_Y + METER_CELL_H && x >= METER_X && x < METER_X + METER_CHARS * METER_CELL_W)
return tft_glyph(m->text[(x - METER_X) / METER_CELL_W], (x - METER_X) % METER_CELL_W / 3, (y - METER_Y) / 3)
? m->fg : m->bg;
if (y >= BAR_Y && y < BAR_Y + BAR_H && x >= BAR_X && x < BAR_X + BAR_W) {
if (y == BAR_Y || y == BAR_Y + BAR_H - 1 || x == BAR_X || x == BAR_X + BAR_W - 1)
return m->fg; /* рамка */
if (x >= BAR_X + 2 && x < BAR_X + 2 + m->bar && y >= BAR_Y + 2 && y < BAR_Y + BAR_H - 2)
return m->accent;
}
return m->bg;
}
void meter_set(meter_t *m, tft_dirty_t *d, const char *text, uint16_t bar)
{
int end = 0;
for (int i = 0; i < METER_CHARS; i++) {
char c = end ? ' ' : text[i];
if (!c) {
end = 1;
c = ' ';
}
if (c != m->text[i]) { /* изменился знак — грязна только его клетка */
m->text[i] = c;
tft_dirty_add(d, (tft_rect_t){(int16_t)(METER_X + i * METER_CELL_W), METER_Y, METER_CELL_W, METER_CELL_H});
}
}
if (bar > BAR_W - 4)
bar = BAR_W - 4;
if (bar != m->bar) { /* полоса — только полоска между старым и новым концом */
uint16_t a = bar < m->bar ? bar : m->bar, b = bar < m->bar ? m->bar : bar;
tft_dirty_add(d, (tft_rect_t){(int16_t)(BAR_X + 2 + a), BAR_Y + 2, (int16_t)(b - a), BAR_H - 4});
m->bar = bar;
}
}
void meter_value(meter_t *m, tft_dirty_t *d, int tenths)
{
char s[METER_CHARS + 1] = "00.0 C";
if (tenths < 0)
tenths = 0;
if (tenths > 999)
tenths = 999;
s[0] = (char)('0' + tenths / 100);
s[1] = (char)('0' + tenths / 10 % 10);
s[3] = (char)('0' + tenths % 10);
meter_set(m, d, s, (uint16_t)(tenths * (BAR_W - 4) / 400)); /* полоса: 40,0 — вся */
}
/* ── пример: картинка из прямоугольников, линий и текста ──────────────── */
void tft_demo(const tft_t *t)
{
uint16_t bg = tft_rgb(0, 0, 64), white = tft_rgb(255, 255, 255), yellow = tft_rgb(255, 255, 0);
int16_t w = (int16_t)t->w, h = (int16_t)t->h;
tft_fill_rect(t, 0, 0, w, h, bg);
tft_text(t, 4, 4, "ST7735", 2, white, bg);
tft_fill_rect(t, 4, 24, 36, 20, tft_rgb(255, 0, 0));
tft_fill_rect(t, 46, 24, 36, 20, tft_rgb(0, 255, 0));
tft_fill_rect(t, 88, 24, 36, 20, tft_rgb(0, 0, 255));
tft_text(t, 4, 48, "R G B", 1, white, bg);
tft_line(t, 0, 60, (int16_t)(w - 1), (int16_t)(h - 13), yellow);
tft_line(t, (int16_t)(w - 1), 60, 0, (int16_t)(h - 13), yellow);
tft_line(t, (int16_t)(w / 2), 60, (int16_t)(w / 2 + 9), (int16_t)(h - 13), white);
tft_text(t, 4, (int16_t)(h - 10), w > h ? "160X128" : "128X160", 1, white, bg);
}
fw22i.c — прошивка STM32F103: SPI1 и DMA1 канал 3, D/C, RST, подсветка; прибор обновляется раз в 0,5 с.
/* fw22i.c — раздел 22з: TFT 128×160 на ST7735 у STM32F103 (BluePill), 72 МГц.
* SPI1 ведущим: SCK — PA5, MOSI — PA7 (MISO не нужен), режим 3, как у платы ST с этим дисплеем;
* CS — PA4 (на шине один дисплей: держим выбранным), D/C — PA3, RST — PA2, подсветка — PA1.
* Пиксели и заливки идут через DMA1 канал 3 (SPI1_TX) 16-битными словами: старший байт первым сам.
* На экране — прибор из tft22i.c: число и полоса, раз в 0,5 с перерисовывается только изменившееся.
* Тактирование до 72 МГц — как в разделе 6 (здесь не повторяется); SPI1 на APB2 — 72 МГц. */
#include "stm32f10x.h"
#include "tft22i.h"
#define SPI_BR SPI_CR1_BR_1 /* 72 / 8 = 9 МГц; SPI_CR1_BR_0 — 72 / 4 = 18 МГц */
#define DC_LOW() (GPIOA->BSRR = 1u << (3 + 16))
#define DC_HIGH() (GPIOA->BSRR = 1u << 3)
#define RST_LOW() (GPIOA->BSRR = 1u << (2 + 16))
#define RST_HIGH() (GPIOA->BSRR = 1u << 2)
static volatile uint32_t ticks;
static uint8_t wide; /* SPI сейчас 16-битный */
static uint16_t fill_word; /* цвет заливки: DMA читает его без шага по памяти */
void SysTick_Handler(void)
{
ticks++;
}
static void delay_ms(uint32_t ms)
{
uint32_t t0 = ticks;
while (ticks - t0 < ms) {
}
}
/* Конец передачи: DMA отдал последнее слово, потом TXE = 1 и BSY = 0 (RM0008, с. 719). Только после
* этого можно трогать D/C и DFF: по флагу DMA в SPI ещё лежат два слова. */
static void spi_idle(void)
{
if (DMA1_Channel3->CCR & DMA_CCR1_EN) {
while (!(DMA1->ISR & DMA_ISR_TCIF3)) {
}
DMA1->IFCR = DMA_IFCR_CTCIF3;
DMA1_Channel3->CCR = 0;
}
while (!(SPI1->SR & SPI_SR_TXE)) {
}
while (SPI1->SR & SPI_SR_BSY) {
}
(void)SPI1->DR; /* приёма нет: снять RXNE и OVR (раздел 18.6) */
(void)SPI1->SR;
}
static void spi_width(uint8_t w16) /* DFF — только при выключенном SPI (RM0008, с. 742) */
{
if (wide == w16)
return;
SPI1->CR1 &= ~SPI_CR1_SPE;
if (w16)
SPI1->CR1 |= SPI_CR1_DFF;
else
SPI1->CR1 &= ~SPI_CR1_DFF;
SPI1->CR1 |= SPI_CR1_SPE;
wide = w16;
}
static void dma_go(const void *src, uint32_t n, uint32_t minc)
{
DMA1_Channel3->CMAR = (uint32_t)src;
DMA1_Channel3->CNDTR = n; /* не больше 65 535 (RM0008, с. 286) */
DMA1_Channel3->CCR = DMA_CCR1_DIR | minc | DMA_CCR1_PSIZE_0 | DMA_CCR1_MSIZE_0 | DMA_CCR1_EN;
}
static void bus_reset(void)
{
RST_LOW();
delay_ms(10);
RST_HIGH();
delay_ms(10);
}
static void bus_cmd(uint8_t c)
{
spi_idle();
spi_width(0);
DC_LOW();
SPI1->DR = c;
}
static void bus_data(const uint8_t *p, uint32_t n)
{
spi_idle();
spi_width(0);
DC_HIGH();
while (n--) {
while (!(SPI1->SR & SPI_SR_TXE)) {
}
SPI1->DR = *p++;
}
}
static void bus_pixels(const uint16_t *px, uint32_t n)
{
spi_idle(); /* прошлая строка ушла целиком */
spi_width(1);
DC_HIGH();
dma_go(px, n, DMA_CCR1_MINC); /* и сразу назад: строку шлёт DMA */
}
static void bus_fill(uint16_t c, uint32_t n)
{
spi_idle();
spi_width(1);
DC_HIGH();
fill_word = c;
while (n) {
uint32_t k = n > 65535u ? 65535u : n;
spi_idle();
dma_go(&fill_word, k, 0); /* MINC = 0: одно слово k раз */
n -= k;
}
}
static const tft_bus_t bus = {bus_reset, delay_ms, bus_cmd, bus_data, bus_pixels, bus_fill};
static tft_t tft;
static meter_t meter;
static tft_dirty_t dirty;
int main(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_SPI1EN;
RCC->AHBENR |= RCC_AHBENR_DMA1EN;
GPIOA->BSRR = 1u << 2 | 1u << 3 | 1u << (4 + 16); /* RST и D/C в 1, CS в 0 */
GPIOA->CRL = (GPIOA->CRL & 0x0F00000Fu) /* PA0 и PA6 не трогаем */
| 0x2u << 4 | 0x3u << 8 | 0x3u << 12 | 0x3u << 16 /* PA1–PA4 — выходы */
| 0xBu << 20 | 0xBu << 28; /* PA5, PA7 — SPI1, 50 МГц */
SPI1->CR1 = SPI_CR1_MSTR | SPI_CR1_SSM | SPI_CR1_SSI | SPI_BR | SPI_CR1_CPOL | SPI_CR1_CPHA;
SPI1->CR2 = SPI_CR2_TXDMAEN;
SPI1->CR1 |= SPI_CR1_SPE;
DMA1_Channel3->CPAR = (uint32_t)&SPI1->DR;
SysTick_Config(72000u); /* 1 мс */
GPIOA->BSRR = 1u << 1; /* подсветка */
tft_init(&tft, &bus, 0, 1, 2, 1); /* как модуль Б модели: сдвиг 2 и 1, стекло BGR */
tft_demo(&tft);
delay_ms(2000);
meter.fg = tft_rgb(255, 255, 255);
meter.bg = tft_rgb(0, 0, 64);
meter.accent = tft_rgb(255, 128, 0);
for (int i = 0; i < METER_CHARS; i++)
meter.text[i] = ' ';
tft_dirty_add(&dirty, (tft_rect_t){0, 0, (int16_t)tft.w, (int16_t)tft.h});
int v = 200;
for (;;) {
meter_value(&meter, &dirty, v); /* 20,0 … 29,9 по кругу */
tft_flush(&tft, &dirty, meter_shade, &meter);
v = v < 299 ? v + 1 : 200;
delay_ms(500);
}
}
Дальше: раздел 22и «Расширитель портов MCP23017 по I2C: 16 линий, прерывания по изменению, клавиатура и дребезг» — от экрана к вводу: 16 линий по двум проводам I2C, клавиатура и прерывание по изменению.