# tft_app: Phase 3.3 RGB 565 & 888 Hybrid

This commit is contained in:
Dmitry Akimov 2026-07-22 18:47:12 +03:00
parent 2662111e1b
commit 958fd9d5e5
19 changed files with 2874 additions and 1285 deletions

View file

@ -52,6 +52,13 @@ bsp_status_t bsp_display_init(bsp_display_type_t type,
uint32_t framebuffer_addr, uint32_t framebuffer_addr,
bsp_display_frame_cb_t p_on_frame_done); bsp_display_frame_cb_t p_on_frame_done);
/* То же + явный формат пикселя framebuffer'а (bsp_display_init — обёртка
* с BSP_DISPLAY_PIXEL_XRGB8888, совместимость со старыми потребителями). */
bsp_status_t bsp_display_init_ex(bsp_display_type_t type,
uint32_t framebuffer_addr,
bsp_display_frame_cb_t p_on_frame_done,
bsp_display_pixel_format_t format);
bsp_status_t bsp_display_deinit(void); bsp_status_t bsp_display_deinit(void);
bsp_status_t bsp_display_set_rotation(bsp_display_rotation_t rotation); bsp_status_t bsp_display_set_rotation(bsp_display_rotation_t rotation);
void bsp_display_set_next_buffer(uint32_t framebuffer_addr); void bsp_display_set_next_buffer(uint32_t framebuffer_addr);
@ -60,6 +67,27 @@ const bsp_display_size_t *bsp_display_get_size(void);
bsp_display_type_t bsp_display_get_type(void); bsp_display_type_t bsp_display_get_type(void);
``` ```
**Формат пикселя framebuffer'а** (что ELCDIF читает из памяти):
```c
typedef enum {
BSP_DISPLAY_PIXEL_XRGB8888 = 0U, /* 32 бита/пиксель — по умолчанию */
BSP_DISPLAY_PIXEL_RGB565, /* 16 бит/пиксель — ½ полосы сканаута */
} bsp_display_pixel_format_t;
```
Формат задаёт ТОЛЬКО ширину слова в памяти (`LCDIF CTRL.WORD_LENGTH`) — то
есть нагрузку непрерывного DMA-сканаута на SDRAM. Ширина шины пинов панели —
всегда 24 бита и от формата не зависит: для RGB565 ELCDIF расширяет 565→24
на пинах (проверено на TFT8: белый — чистый белый, чистые R/G/B корректны).
Оба режима сосуществуют: старый потребитель (`firmware_test/test_display.c`)
работает через `bsp_display_init()` (XRGB8888), `tft_app` (`services/gfx`,
гибрид bpp) — через `bsp_display_init_ex(..., BSP_DISPLAY_PIXEL_RGB565)`.
Практика по полосе (TFT8 800×600 @ ~65 Гц, SDRAM 16 бит @ ~136 МГц ≈ 272 МБ/с):
сканаут XRGB8888 ≈ 126 МБ/с (46% всей полосы), RGB565 ≈ 63 МБ/с — вдвое
меньше давления на SDRAM для всех остальных мастеров (CPU/PXP).
**Типы дисплея:** **Типы дисплея:**
```c ```c
@ -83,7 +111,7 @@ typedef enum {
} bsp_display_rotation_t; } bsp_display_rotation_t;
``` ```
**Коды возврата `bsp_display_init()`:** **Коды возврата `bsp_display_init()` / `bsp_display_init_ex()`:**
| Код | Условие | | Код | Условие |
| ----------------------- | ------------------------------ | | ----------------------- | ------------------------------ |
@ -91,7 +119,7 @@ typedef enum {
| `BSP_ERR_PARAM` | `type >= BSP_DISPLAY_COUNT` | | `BSP_ERR_PARAM` | `type >= BSP_DISPLAY_COUNT` |
| `BSP_ERR_NOT_SUPPORTED` | TFT4 — Video PLL не реализован | | `BSP_ERR_NOT_SUPPORTED` | TFT4 — Video PLL не реализован |
`bsp_display_init()` идемпотентен: повторный вызов без `deinit` возвращает Обе init-функции идемпотентны: повторный вызов без `deinit` возвращает
`BSP_OK` без побочных эффектов. Callback `p_on_frame_done` должен быть `BSP_OK` без побочных эффектов. Callback `p_on_frame_done` должен быть
ISR-safe (`NULL` допускается). ISR-safe (`NULL` допускается).

View file

@ -41,6 +41,22 @@ typedef enum bsp_display_rotation_e
BSP_DISPLAY_FLIP_HORIZONTAL, /**< LR=0 UD=0. Горизонтальный флип (SHLR). */ BSP_DISPLAY_FLIP_HORIZONTAL, /**< LR=0 UD=0. Горизонтальный флип (SHLR). */
} bsp_display_rotation_t; } bsp_display_rotation_t;
/* ── Формат пикселя framebuffer'а (что ELCDIF читает из памяти) ───────── */
/**
* @brief Формат пикселя в памяти framebuffer'а.
*
* Определяет ширину слова, которое ELCDIF читает по DMA из SDRAM (=нагрузку
* на полосу). Ширина шины пинов панели (24-бит) отдельный параметр драйвера,
* от этого НЕ зависит: RGB565 (16 бит/пиксель в памяти) корректно выводится и
* на 24-битную панель (ELCDIF расширяет 56524 на пинах).
*/
typedef enum bsp_display_pixel_format_e
{
BSP_DISPLAY_PIXEL_XRGB8888 = 0U, /**< 32 бита/пиксель (X игнорируется). По умолчанию. */
BSP_DISPLAY_PIXEL_RGB565, /**< 16 бит/пиксель — вдвое меньше полосы сканаута. */
} bsp_display_pixel_format_t;
/* ── Размер дисплея ──────────────────────────────────────────────────── */ /* ── Размер дисплея ──────────────────────────────────────────────────── */
typedef struct bsp_display_size_s typedef struct bsp_display_size_s
@ -77,10 +93,26 @@ typedef void (*bsp_display_frame_cb_t)(void);
* Должен быть выровнен по 64 байт, в NonCacheable SDRAM. * Должен быть выровнен по 64 байт, в NonCacheable SDRAM.
* @param on_frame_done ISR-safe callback по завершении кадра; NULL без callback. * @param on_frame_done ISR-safe callback по завершении кадра; NULL без callback.
* @return BSP_OK | BSP_ERR_PARAM | BSP_ERR_NOT_SUPPORTED * @return BSP_OK | BSP_ERR_PARAM | BSP_ERR_NOT_SUPPORTED
*
* @note Формат пикселя XRGB8888 (обёртка над bsp_display_init_ex()).
*/ */
bsp_status_t bsp_display_init(bsp_display_type_t type, uint32_t framebuffer_addr, bsp_status_t bsp_display_init(bsp_display_type_t type, uint32_t framebuffer_addr,
bsp_display_frame_cb_t p_on_frame_done); bsp_display_frame_cb_t p_on_frame_done);
/**
* @brief То же, что bsp_display_init(), но с явным форматом пикселя framebuffer'а.
*
* Позволяет потребителю выбрать RGB565 (вдвое меньше полосы сканаута) вместо
* XRGB8888, не меняя остальную настройку. bsp_display_init() обёртка с
* форматом BSP_DISPLAY_PIXEL_XRGB8888 (совместимость со старыми потребителями).
*
* @param format Формат пикселя в памяти (см. bsp_display_pixel_format_t).
* @return BSP_OK | BSP_ERR_PARAM | BSP_ERR_NOT_SUPPORTED
*/
bsp_status_t bsp_display_init_ex(bsp_display_type_t type, uint32_t framebuffer_addr,
bsp_display_frame_cb_t p_on_frame_done,
bsp_display_pixel_format_t format);
/** /**
* @brief Остановить ELCDIF, выключить подсветку, деинициализировать. * @brief Остановить ELCDIF, выключить подсветку, деинициализировать.
* *

View file

@ -270,9 +270,19 @@ static void enable_lcd_interrupt(void)
ELCDIF_EnableInterrupts(LCDIF, kELCDIF_CurFrameDoneInterruptEnable); ELCDIF_EnableInterrupts(LCDIF, kELCDIF_CurFrameDoneInterruptEnable);
} }
/** @brief Заполнить конфигурацию ELCDIF из таблицы + адрес буфера. */ /** @brief Смаппить BSP-формат пикселя на формат памяти ELCDIF. */
static elcdif_pixel_format_t map_pixel_format(bsp_display_pixel_format_t format)
{
/* dataBus остаётся 24-бит независимо: pixelFormat задаёт лишь ширину слова в
* памяти (WORD_LENGTH), ELCDIF расширяет 56524 на пинах (см. fsl_elcdif.c
* s_pixelFormatReg). */
return (format == BSP_DISPLAY_PIXEL_RGB565) ? kELCDIF_PixelFormatRGB565
: kELCDIF_PixelFormatXRGB8888;
}
/** @brief Заполнить конфигурацию ELCDIF из таблицы + адрес буфера + формат. */
static void build_elcdif_cfg(const display_hw_cfg_t *p_cfg, uint32_t framebuffer_addr, static void build_elcdif_cfg(const display_hw_cfg_t *p_cfg, uint32_t framebuffer_addr,
elcdif_rgb_mode_config_t *p_out) bsp_display_pixel_format_t format, elcdif_rgb_mode_config_t *p_out)
{ {
p_out->panelWidth = p_cfg->width; p_out->panelWidth = p_cfg->width;
p_out->panelHeight = p_cfg->height; p_out->panelHeight = p_cfg->height;
@ -284,7 +294,7 @@ static void build_elcdif_cfg(const display_hw_cfg_t *p_cfg, uint32_t framebuffer
p_out->vbp = p_cfg->vbp; p_out->vbp = p_cfg->vbp;
p_out->polarityFlags = p_cfg->pol_flags; p_out->polarityFlags = p_cfg->pol_flags;
p_out->bufferAddr = framebuffer_addr; p_out->bufferAddr = framebuffer_addr;
p_out->pixelFormat = kELCDIF_PixelFormatXRGB8888; p_out->pixelFormat = map_pixel_format(format);
p_out->dataBus = kELCDIF_DataBus24Bit; p_out->dataBus = kELCDIF_DataBus24Bit;
} }
@ -292,6 +302,15 @@ static void build_elcdif_cfg(const display_hw_cfg_t *p_cfg, uint32_t framebuffer
bsp_status_t bsp_display_init(bsp_display_type_t type, uint32_t framebuffer_addr, bsp_status_t bsp_display_init(bsp_display_type_t type, uint32_t framebuffer_addr,
bsp_display_frame_cb_t p_on_frame_done) bsp_display_frame_cb_t p_on_frame_done)
{
/* Обёртка совместимости — формат по умолчанию XRGB8888 (старые потребители). */
return bsp_display_init_ex(type, framebuffer_addr, p_on_frame_done,
BSP_DISPLAY_PIXEL_XRGB8888);
}
bsp_status_t bsp_display_init_ex(bsp_display_type_t type, uint32_t framebuffer_addr,
bsp_display_frame_cb_t p_on_frame_done,
bsp_display_pixel_format_t format)
{ {
if ((uint32_t) type >= (uint32_t) BSP_DISPLAY_COUNT) if ((uint32_t) type >= (uint32_t) BSP_DISPLAY_COUNT)
{ {
@ -324,7 +343,7 @@ bsp_status_t bsp_display_init(bsp_display_type_t type, uint32_t framebuffer_addr
g_s_display.frame_cb = p_on_frame_done; g_s_display.frame_cb = p_on_frame_done;
elcdif_rgb_mode_config_t elcdif_cfg; elcdif_rgb_mode_config_t elcdif_cfg;
build_elcdif_cfg(p_cfg, framebuffer_addr, &elcdif_cfg); build_elcdif_cfg(p_cfg, framebuffer_addr, format, &elcdif_cfg);
ELCDIF_RgbModeInit(LCDIF, &elcdif_cfg); ELCDIF_RgbModeInit(LCDIF, &elcdif_cfg);
enable_lcd_interrupt(); enable_lcd_interrupt();
ELCDIF_RgbModeStart(LCDIF); ELCDIF_RgbModeStart(LCDIF);

View file

@ -53,9 +53,12 @@ flowchart TB
## 2. Путь одного кадра ## 2. Путь одного кадра
`sul_rx`-задача приложения опрашивает CAN, прогоняет кадр через активный декодер и передаёт `sul_rx_task` опрашивает CAN, прогоняет кадр через активный декодер и передаёт результат
результат контроллеру; `render`-задача применяет diff. Очередь между ними — глубины 1 контроллеру; `render_task` применяет diff. **Данные** между ними — очередь глубины 1
(`xQueueOverwrite`: важно только последнее состояние, не история). (`xQueueOverwrite`: важно только последнее состояние, не история); **пробуждение**
`xTaskNotifyGive` (render_task — event-driven, не поллит). Пока открыто меню, `sul_rx_task`
находится в «мягкой паузе» (decode/controller/очередь пропускаются, WDOG кормится). Полная
картина задач — [TASKS.md](TASKS.md).
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
@ -116,10 +119,21 @@ flowchart LR
## 4. НКУ-CAN: пакет → поля модели ## 4. НКУ-CAN: пакет → поля модели
Порт боевого декодера `OLD_PROJECT/source/main_programm.c` (`msg_receiver_task`) в чистую Порт боевого декодера `OLD_PROJECT/source/main_programm.c` (`msg_receiver_task`) в чистую
функцию. Адрес станции = 0 (базовые ID без сдвига группы; параметризация из настроек — Фаза 3). функцию. Реализация — [nku_can.c](../../firmware/tft_app/src/domain/sul/nku_can/src/nku_can.c).
Реализация — [nku_can.c](../../firmware/tft_app/src/domain/sul/nku_can/src/nku_can.c).
| Пакет | ID | Байты/маски → поля `sul_result_t` | **Адрес станции — из настроек (Фаза 3), применяется на ОБА конца одновременно** (боевая
находка: правка только декодера бесполезна — HW-фильтры FlexCAN отбрасывают кадры чужого
адреса до всякого софта):
- декодер: `nku_can_set_address()` — ID пакетов сдвигаются на `group4 = addr<<4` (PACKET1..4,
биты [7:4]) и `group6 = addr<<6` (PACKET5, биты [8:6], протокол отводит 3 бита);
- транспорт: `sul_transport_can_set_address()` — переконфигурация RX-фильтров Message
Buffer'ов под те же ID (diff-защита: реальная переконфигурация только при смене адреса).
`sul_rx_task` вызывает оба на каждой итерации (дёшево) — правка адреса в меню подхватывается
без межзадачного сигнала. В таблице ниже ID приведены для адреса 0 (базовые).
| Пакет | ID (адрес 0) | Байты/маски → поля `sul_result_t` |
| --- | --- | --- | | --- | --- | --- |
| **PACKET1** | `0x506` | `d6[1:0]``direction` · `d6[3:2]``movement` · `d6[7:4]` код режима → `fire_alarm`/`maintenance`/`fireman`/погрузка-инстр. · `d3[5:0]` → уровень остановки (внутр., гейт PACKET5) | | **PACKET1** | `0x506` | `d6[1:0]``direction` · `d6[3:2]``movement` · `d6[7:4]` код режима → `fire_alarm`/`maintenance`/`fireman`/погрузка-инстр. · `d3[5:0]` → уровень остановки (внутр., гейт PACKET5) |
| **PACKET2** | `0x408` | `d7&0x40` → перегруз (источник 1) | | **PACKET2** | `0x408` | `d7&0x40` → перегруз (источник 1) |
@ -138,8 +152,8 @@ flowchart LR
seismic — PACKET4), пакет-владелец распоряжается напрямую: сбрасывает в начале и выставляет по seismic — PACKET4), пакет-владелец распоряжается напрямую: сбрасывает в начале и выставляет по
условию, гася устаревший режим. условию, гася устаревший режим.
Удалённая установка адреса (кадры `0x4X1`/`0x5XB`) — **отложена в Фазу 3** (нужен `settings_store` Удалённая установка адреса (кадры `0x4X1`/`0x5XB`) — **под-шаг 3.5** (см. PLAN.md). Сейчас эти
для записи `nku_address`; чистый декодер не пишет настройки). Сейчас эти ID игнорируются. ID игнорируются (wildcard-фильтры под них ещё не настраиваются).
--- ---

View file

@ -26,7 +26,7 @@
```mermaid ```mermaid
flowchart TD flowchart TD
START([render mode, result]) --> CLR["gfx_clear(BLACK)"] START([render mode, result]) --> CLR["gfx_clear() — AS прозрачен,<br/>фон даёт чёрный PS"]
CLR --> Q{"mode == NORMAL?"} CLR --> Q{"mode == NORMAL?"}
Q -->|да| POS["этаж (FloorFontFallback), по центру"] Q -->|да| POS["этаж (FloorFontFallback), по центру"]
POS --> ARR{"direction UP/DOWN?"} POS --> ARR{"direction UP/DOWN?"}
@ -87,12 +87,15 @@ flowchart LR
--- ---
## 5. Известное ограничение — один framebuffer ## 5. Как кадр попадает на экран (Фаза 3.2.4 — фундамент рендера)
Сейчас один буфер, без double buffering: `render()` пишет напрямую в буфер, который ELCDIF в этот Ограничение одного framebuffer'а Фазы 1 **снято**. `render()` рисует off-screen в
момент сканирует по DMA. Если перерисовка не укладывается в кадр развёртки, виден сам процесс альфа-поверхность **AS** (ARGB8888); показ — отдельным вызовом `gfx_present()` у владельца
закраски (глиф «набирается» за несколько кадров) — **не** tearing и **не** порча пикселей (каждая дисплея ([task_render.c](../../firmware/tft_app/src/app/task_render.c)): PXP компонует AS над
запись валидна), просто заметен процесс. чёрным фоном PS и пишет **RGB565** в задний framebuffer, затем tear-free свап синхронно с
ELCDIF (`FRAME_DONE`). Гибрид bpp: рисование и AA — в полных 8 битах (AS), 565 — только на
самом выходе (сплошные цвета/текст квантуются незаметно; проверено на панели).
Фикс — double buffering (второй буфер + swap по `FRAME_DONE`), запланирован в **Фазе 4** вместе с Индикация всегда рендерится **полным кадром** (в отличие от меню, где после открытия
PXP-компоновщиком. Подробнее — [PLAN.md, Фаза 4](../../firmware/tft_app/PLAN.md). перекомпоновывается только окно — см. [MENU.md](MENU.md) §5). Подробности компоновщика и
разбивка по задачам — [TASKS.md](TASKS.md).

View file

@ -142,11 +142,18 @@ sequenceDiagram
любым цветом со сглаживанием, и оно корректно ложится на полосу-курсор. Значения SELECT/BOOL — из любым цветом со сглаживанием, и оно корректно ложится на полосу-курсор. Значения SELECT/BOOL — из
`options[]` дескриптора, BYTE — числом. `options[]` дескриптора, BYTE — числом.
**Полнокадровый рендер** (double-buffer + PXP, Фаза 3.2.4): любое изменение — `menu_view_render()` **Оконный рендер** (double-buffer + PXP + гибрид bpp, Фаза 3.2.4): `menu_view_render()` рисует
рисует **весь кадр** off-screen в альфа-поверхность AS (обнуление + окно), затем владелец дисплея off-screen в альфа-поверхность AS (ARGB8888) **только окно** `MENU_VIEW_WIN_W×H` (windowed clear +
зовёт `gfx_present()` (PXP-композит AS над чёрным PS → задний framebuffer + атомарный свап). Рисуем отрисовка); показ — у владельца дисплея ([task_render.c](../../firmware/tft_app/src/app/task_render.c)):
вне экрана, показываем атомарно → tear-free. Компоновщик — `services/gfx` (эталон
`OLD_PROJECT_TFT8_UKL/source/display/`). - **открытие меню** — полная очистка AS (стереть индикацию вне окна) + **два** полных
`gfx_present()` подряд: из-за double buffering ОБА framebuffer'а обязаны получить корректный
кадр вне окна (контракт `gfx_present_rect`, см. gfx.h);
- **навигация/правка**`gfx_present_rect(0,0,окно)`: PXP перекомпоновывает только 480×272
(~27% кадра) — пропорционально дешевле полного кадра.
Рисуем вне экрана, показываем атомарным свапом → tear-free. Компоновщик — `services/gfx`
(эталон `OLD_PROJECT_TFT8_UKL/source/display/`).
**Меню и рендер — РАЗНЫЕ задачи** ([task_menu.c](../../firmware/tft_app/src/app/task_menu.c) / **Меню и рендер — РАЗНЫЕ задачи** ([task_menu.c](../../firmware/tft_app/src/app/task_menu.c) /
[task_render.c](../../firmware/tft_app/src/app/task_render.c)). Найдено на HW-верификации Фазы [task_render.c](../../firmware/tft_app/src/app/task_render.c)). Найдено на HW-верификации Фазы
@ -154,7 +161,8 @@ sequenceDiagram
разделение было безвредным упущением — но `gfx_present()` (double-buffer + PXP) внёс блокирующее разделение было безвредным упущением — но `gfx_present()` (double-buffer + PXP) внёс блокирующее
ожидание кадра, и в объединённой задаче это ожидание попутно блокировало вход в меню (ноль реакции ожидание кадра, и в объединённой задаче это ожидание попутно блокировало вход в меню (ноль реакции
на кнопки). Эталон разделения — `OLD_PROJECT_TFT8_UKL`: `BUTTONS_TASK`/`menu_task` отдельно от на кнопки). Эталон разделения — `OLD_PROJECT_TFT8_UKL`: `BUTTONS_TASK`/`menu_task` отдельно от
`REFRESH_TASK`/`tft_refresh_task`. `REFRESH_TASK`/`tft_refresh_task`. Полная картина задач/приоритетов/взаимодействия —
[TASKS.md](TASKS.md).
```mermaid ```mermaid
flowchart LR flowchart LR

View file

@ -1,25 +1,28 @@
# tft-app — документация домена # tft-app — документация домена
Dev-документация по **реализованному** доменному слою `firmware/tft_app` (Фазы 12): как данные Dev-документация по **реализованному** слою `firmware/tft_app` (Фазы 13.2): как данные
СУЛ проходят от протокола до экрана. Технические доки «как это работает в коде», в дополнение к СУЛ проходят от протокола до экрана, и как устроено приложение поверх FreeRTOS. Технические
проектным ARCH/PLAN. доки «как это работает в коде», в дополнение к проектным ARCH/PLAN.
| Документ | О чём | | Документ | О чём |
| --- | --- | | --- | --- |
| [DOMAIN_DATAFLOW.md](DOMAIN_DATAFLOW.md) | Путь данных: транспорт → декодер → контроллер → презентация. Слои, контракт `decode()`, карта пакетов НКУ-CAN → поля модели, таймаут→default. | | [DOMAIN_DATAFLOW.md](DOMAIN_DATAFLOW.md) | Путь данных: транспорт → декодер → контроллер → презентация. Слои, контракт `decode()`, карта пакетов НКУ-CAN → поля модели, адресация (decode + HW-фильтры), таймаут→default. |
| [MODE_PRIORITY.md](MODE_PRIORITY.md) | Свёртка ортогональных сигналов в один экранный режим. Таблица приоритетов как данные, резолвер, как менять/кастомизировать. | | [MODE_PRIORITY.md](MODE_PRIORITY.md) | Свёртка ортогональных сигналов в один экранный режим. Таблица приоритетов как данные, резолвер, как менять/кастомизировать. |
| [FALLBACK.md](FALLBACK.md) | Safe-mode рендер: этаж/стрелка/метка режима, по-символьный fallback шрифта, триггеры перерисовки, ограничение single-buffer. | | [FALLBACK.md](FALLBACK.md) | Safe-mode рендер: этаж/стрелка/метка режима, по-символьный fallback шрифта, триггеры перерисовки, путь кадра через компоновщик. |
| [MENU.md](MENU.md) | Движок меню (Фаза 3.2): дерево-данные, редакторы по типу, чистая модель, связь с `settings_store` (offset-привязка, `proto_slice`, поток сохранения). | | [MENU.md](MENU.md) | Движок меню (Фаза 3.2): дерево-данные, редакторы по типу, чистая модель, связь с `settings_store`, оконный рендер, раскладка кнопок. |
| [TASKS.md](TASKS.md) | Задачи FreeRTOS: состав/приоритеты (и почему такие), старт системы, MPSC-взаимодействие, мягкая пауза, разделяемое состояние. |
## Связанные документы ## Связанные документы
- [ARCH.md](../../firmware/tft_app/ARCH.md) — архитектура и проектное обоснование (источник истины по дизайну). - [ARCH.md](../../firmware/tft_app/ARCH.md) — архитектура и проектное обоснование (источник истины по дизайну).
- [PLAN.md](../../firmware/tft_app/PLAN.md) — статус фаз разработки (источник истины по статусу). - [PLAN.md](../../firmware/tft_app/PLAN.md) — статус фаз разработки (источник истины по статусу).
- [DEV_ARCH.md](../DEV_ARCH.md) — устройство репозитория и сборки. - [DEV_ARCH.md](../DEV_ARCH.md) — устройство репозитория и сборки.
- [bsp/display/README.md](../../bsp/display/README.md) — драйвер ELCDIF (два формата пикселя: XRGB8888/RGB565).
## Границы ## Границы
Описаны домен, fallback-презентация, ядро настроек (`settings_store`) и модель меню. Описаны домен, fallback-презентация, ядро настроек (`settings_store`), меню (модель + оконный
В процессе (Фаза 3): рендер меню + wiring кнопок (3.2.2/3.2.3), per-protocol дескрипторы (3.3), рендер + задачи) и фундамент рендера (double-buffer + PXP + гибрид bpp, Фаза 3.2.4). Остаток
opto-входы (3.4), удалённая адресация НКУ-CAN (3.5). Ещё не реализовано (см. PLAN.md): ассеты и Фазы 3: per-protocol дескрипторы (3.3), opto-входы (3.4), удалённая адресация НКУ-CAN (3.5),
layout-движок со спрайтами (Фазы 45), аудио (Фаза 6), мультипротокол (Фаза 8). тумблер логов (3.6). Ещё не реализовано (см. PLAN.md): ассеты и layout-движок со спрайтами
(Фазы 45), аудио (Фаза 6), мультипротокол (Фаза 8).

137
docs/tft_app/TASKS.md Normal file
View file

@ -0,0 +1,137 @@
# tft-app — задачи FreeRTOS и их взаимодействие
Документ описывает **реализованную** (Фаза 3.2.4) многозадачную структуру `tft_app`: какие
задачи существуют, кто кого создаёт, как они обмениваются данными и почему приоритеты именно
такие. Контракт между задачами — [app_tasks.h](../../firmware/tft_app/src/app/app_tasks.h)
(единственный источник истины по приоритетам/разделяемому состоянию); тела задач —
`firmware/tft_app/src/app/task_*.c`.
Структура выстрадана на HW-верификации (два боевых регресса — см. PLAN.md, Фаза 3.2.4):
объединение меню и рендера в одну задачу блокировало ввод, а неверный относительный приоритет
`sul_rx`/`render` морозил экран при отсутствии CAN-трафика. Эталон разделения —
`OLD_PROJECT_TFT8_UKL` (`BUTTONS_TASK` отдельно от `REFRESH_TASK`).
---
## 1. Состав
| Задача | Файл | Приоритет (`app_tasks.h`) | Роль | Блокировки |
| --- | --- | --- | --- | --- |
| `bringup_task` | task_bringup.c | `+4` (высший, недолгоживущая) | одноразовая инициализация → создаёт три остальные → `vTaskDelete(NULL)` | QSPI/flash-операции |
| `menu_task` | task_menu.c | `+3` | модель меню: кнопки, вход/навигация/правка/сохранение. **НЕ рисует** | `settings_store_save()` (flash) на выходе из меню |
| `render_task` | task_render.c | `+2` | **единственный** владелец дисплея и вызывающий `gfx_present*()` | PXP busy-wait + ожидание FRAME_DONE |
| `sul_rx_task` | task_sul_rx.c | `+1` (низший) | приём CAN → decode → controller; WDOG/heartbeat | busy-spin в `bsp_can_receive()` до 100 мс без трафика |
| — демон таймеров | (FreeRTOS) | `configTIMER_TASK_PRIORITY` (высший в системе) | `input_poll_cb` каждые 5 мс: `bsp_button_poll()` (debounce); Фаза 3.4 — сюда же opto | нет (колбэк короткий) |
`main()` создаёт только очередь, софт-таймер ввода и `bringup_task` — остальное wiring делает
сам `bringup_task`.
---
## 2. Старт системы
```mermaid
sequenceDiagram
participant M as main()
participant B as bringup_task (+4)
participant R as render_task (+2)
participant S as sul_rx_task (+1)
participant U as menu_task (+3)
M->>M: board_hw_init, очередь, таймер ввода
M->>B: xTaskCreate + vTaskStartScheduler
B->>B: log_mutex → UART/лог → QSPI → settings → self-confirm
B->>B: SDRAM → gfx (PXP+ELCDIF) → CAN
B->>B: g_display_ready = true
B->>R: xTaskCreate (хэндл → g_render_task_handle)
B->>S: xTaskCreate
B->>U: xTaskCreate
B->>B: vTaskDelete(NULL)
R->>R: первый кадр («--») + gfx_present()
```
Порядок создания (render первым) документирует зависимость: его хэндл нужен продюсерам для
`xTaskNotifyGive`. Формально гонки нет — `bringup_task` выше всех по приоритету и монополизирует
CPU, пока не создаст всех троих.
---
## 3. Взаимодействие (MPSC)
Два продюсера, один консюмер. **Данные** и **сигнал пробуждения** разделены:
```mermaid
flowchart LR
TMR["демон таймеров<br/>input_poll_cb 5 мс<br/>bsp_button_poll (debounce)"]
MT["menu_task (+3)<br/>модель меню g_menu"]
RX["sul_rx_task (+1)<br/>CAN→decode→controller<br/>WDOG безусловно"]
RT["render_task (+2)<br/>gfx_present*()"]
TMR -."залатанные события кнопок".-> MT
MT -->|"xTaskNotifyGive<br/>(любое изменение)"| RT
RX -->|"xQueueOverwrite (render_msg_t)<br/>+ xTaskNotifyGive"| RT
MT -."g_menu_active (мягкая пауза)".-> RX
```
- **Очередь `g_render_queue`** (глубина 1, `xQueueOverwrite`) — только плечо
`sul_rx→render`, несёт `render_msg_t` (diff + результат). Семантика «важно только
последнее состояние»: рендер не обязан успевать за каждым кадром CAN.
- **`ulTaskNotifyTake(pdTRUE, portMAX_DELAY)`** в `render_task` — event-driven, без
поллинга; несколько notify от обоих продюсеров схлопываются в одно пробуждение (та же
семантика «важно только последнее»).
- **Приоритет «меню важнее индикации» не кодируется в уведомлении**: проснувшись,
`render_task` первым делом проверяет `menu_is_open(&g_menu)` — если меню открыто,
очередь индикации даже не читается.
- **Модель меню `g_menu`** мутирует только `menu_task`; `render_task` читает её для
отрисовки после notify (happens-before через нотификацию — как у очереди).
### Мягкая пауза `sul_rx_task` на время меню
`menu_task` держит `g_menu_active=true`, пока меню открыто; `sul_rx_task` под этим флагом
пропускает CAN-работу (decode/controller/очередь), но **WDOG/heartbeat кормит безусловно**
задача не suspend'ится, поэтому сторожевой таймер в безопасности по конструкции, что бы ни
происходило с меню. Флаг обновляется **до** `settings_store_save()` (flash-запись небыстрая —
иначе пауза держалась бы дольше нужного). На закрытие меню `render_task` немедленно
восстанавливает индикацию из последнего известного состояния, не дожидаясь свежего CAN-кадра.
---
## 4. Почему приоритеты именно такие
`menu (+3) > render (+2) > sul_rx (+1)`оба соотношения выстраданы на железе:
1. **`render` ВЫШЕ `sul_rx`.** `bsp_can_receive()` — busy-spin без единого блокирующего
FreeRTOS-вызова: без CAN-трафика `sul_rx_task` занимает CPU весь таймаут (100 мс) каждую
итерацию, и `xTaskDelayUntil()` при просроченном дедлайне не блокирует вовсе — задача
непрерывно READY. Более низкоприоритетный `render_task` в этой ситуации голодал:
пустой экран при старте без связи, залипание индикации при обрыве. Обратный порядок
приоритетов чинит это, не трогая общий bare-metal модуль `bsp_can`.
2. **`menu` ВЫШЕ `render`.** Блокировки рендера (PXP busy-wait + ожидание кадра) не должны
придерживать обработку кнопок — то самое свойство, ради которого меню и рендер разведены
по задачам (в объединённой задаче ввод был мёртв).
3. **`bringup` выше всех** — монополизирует CPU на время одноразовой инициализации.
4. **Демон таймеров — наивысший в системе** (`configTIMER_TASK_PRIORITY`): debounce-сэмплы
не теряются, чем бы ни были заняты остальные.
---
## 5. Разделяемое состояние (`app_tasks.h`)
| Объект | Пишет | Читает | Синхронизация |
| --- | --- | --- | --- |
| `g_render_queue` | sul_rx | render | FreeRTOS queue |
| `g_render_task_handle` | bringup (до создания продюсеров) | sul_rx, menu | создание-до-использования |
| `g_display_ready` | bringup | render, sul_rx | `volatile bool`, одно-writer |
| `g_menu_active` | menu | sul_rx | `volatile bool`, одно-writer |
| `g_menu` | menu | render | notify (happens-before) |
| лог-буфер `utils/log` | все задачи | — | мьютекс `port/log/src/log_mutex.c` (FreeRTOS strong-override; включён в сборку `app` — до Фазы 3.2.4 был `#if 0`, гонка) |
---
## 6. Кадр на экран (сводка)
Рендер-пайплайн (детали — [FALLBACK.md](FALLBACK.md) §5, [MENU.md](MENU.md) §5): CPU рисует в
AS (ARGB8888) → `gfx_present*()` в `render_task` = PXP-композит AS над чёрным PS → задний
framebuffer **RGB565** → tear-free свап по FRAME_DONE. Гибрид bpp держит сканаут ELCDIF на
половинной полосе SDRAM (63 МБ/с вместо 126). Индикация — всегда полный кадр (~31 мс PXP);
меню после открытия — только окно 480×272 (~8 мс расчётно).

View file

@ -0,0 +1,155 @@
# Хвост Фазы 3 — бриф для постановки задачи (перед Фазой 4)
Рабочий документ для **отдельного треда**. Фаза 3.2.4 (фундамент рендера: double-buffer + PXP +
гибрид bpp + оконный композит меню) закрыта ✅ — см. [PLAN.md](PLAN.md). Ниже — пять пунктов,
которые по плану остаются перед Фазой 4 (ассеты), с фактическим состоянием кода (проверено на
момент написания, не по памяти) и открытыми вопросами, которые нужно решать **совместно**, не
молча.
**Прочитать сначала** (источники истины, не пересказывать здесь): [ARCH.md](ARCH.md) §6-8
(домен/контроллер/настройки), [PLAN.md](PLAN.md) Фаза 3 (декомпозиция 3.1-3.6),
[docs/tft_app/TASKS.md](../../docs/tft_app/TASKS.md) (задачи FreeRTOS, приоритеты, MPSC-паттерн —
пункт 2 ниже в него встраивается), [docs/tft_app/DOMAIN_DATAFLOW.md](../../docs/tft_app/DOMAIN_DATAFLOW.md).
---
## 1. Выбор протокола + демо-протокол (под-шаг 3.3)
**Замысел уже зафиксирован в ARCH.md §8** (не изобретать заново): каждый протокол регистрирует
`sul_settings_desc_t` — свои параметры/диапазоны/подписи для меню; `sul_driver_t` несёт
`const sul_settings_desc_t *settings`; активный протокол выбирается из `settings`.
**Фактическое состояние (пробел):**
- `sul_driver_t` (`domain/sul/include/domain/sul.h`) поля `.settings` **ещё не имеет**
только `id`/`p_name`/`decode`. Тип `sul_settings_desc_t` **нигде не определён**, встречается
только в комментариях (`settings_store.h`, ARCH.md §8).
- `sul_registry_active()` (`domain/sul/src/sul_registry.c`) **захардкожена на индекс 0**:
`/* Фаза 1: единственная запись, хардкод. Фаза 3 выберет по настройкам. */`.
`sul_registry_find(id)` — уже generic (ищет по id в таблице), лишь `active()` не читает
`settings_device_t.protocol_id`.
- В меню пункт **«Протокол» уже существует** (`menu_tree.c`, `T_PROTO`, `MENU_SELECT`,
`value_offset = offsetof(settings_t, device.protocol_id)`), но `K_PROTO_LABELS = {"НКУ-CAN"}`
и `max = 0` — выбирать физически не из чего.
- `settings_device_t.protocol_id` (`settings_store.h`) — поле уже есть и персистится, просто
ничего его не читает на стороне `sul`.
**Демо-протокол — предложение пользователя, не в исходном PLAN.md.** Идея: второй, полностью
синтетический декодер/источник данных для демонстрации возможностей устройства (не требует
реальной шины/станции). Открытые вопросы, требующие совместного решения:
- Что именно демонстрирует демо-протокол? (перебор всех режимов/этажей по таймеру? фиксированный
сценарий по кругу? управляемый через меню сценарий?)
- Нужен ли ему «транспорт» вообще, или это decode-less источник (генерирует `sul_result_t`
напрямую, минуя `sul_frame_t`/`sul_decode_fn`)? Если он не вписывается в существующий контракт
`sul_decode_fn` — это сигнал, что контракт реестра надо аккуratно обобщить, не подгонять под
него демо-протокол.
- Даёт ли демо-протокол `.settings` (свой `sul_settings_desc_t`) — если да, это ХОРОШИЙ тест на
то, что дескрипторный механизм действительно протокол-агностичен (не NKU-CAN-specific), а не
только на бумаге.
- Как переключение протокола в меню взаимодействует с `sul_rx_task` (сейчас она пишет адрес в
`nku_can_ctx_t` каждую итерацию безусловно — при реальном переключении протокола нужно решить,
пересоздаётся ли decoder-context или живёт постоянно для каждого зарегистрированного протокола).
## 2. Задача обработки оптовходов → уведомление render_task (под-шаг 3.4)
**BSP-драйвер уже полностью реализован** (`bsp/opto/include/bsp/opto.h` + `src/opto.c`) — не
писать заново. Два канала (`BSP_OPTO_CH_IN1/IN2`, уровневый режим с дебаунсом) + опциональный
`BSP_OPTO_CH_RS` (протокольный режим, коллбэк из ISR). `bsp_opto_process()` — обязана вызываться
из некой периодической точки (аналог `bsp_button_poll()`); это уже **предвиделось** в
`task_menu.c`: `input_poll_cb` содержит закомментированный `/* Фаза 3.4: bsp_opto_process(); */`
ровно на месте `bsp_button_poll()`.
**Референс поведения (не архитектуры) — `OLD_PROJECT_TFT8_UKL/source/main_programm.c`**,
`tft_refresh_task`: IN1/IN2 → `show_call_icon`/`show_answer_icon` → выбор иконки
(`icon_img_ptr`) поверх/вместо этажа. У нас в fallback (asset-free) это будет текст/примитив —
спрайты-иконки только с Фазы 4/5 (см. [FALLBACK.md](../../docs/tft_app/FALLBACK.md)).
**Открытые вопросы (обсудить совместно, есть развилка с реальными последствиями для
[TASKS.md](../../docs/tft_app/TASKS.md)):**
- **Где живёт вызов `bsp_opto_process()`** — в существующем софт-таймере `input_poll_cb`
(5 мс, вместе с debounce кнопок — симметрично, минимальный дифф) или в отдельной задаче?
Софт-таймер — самый высокий приоритет в системе, колбэк должен оставаться коротким;
`bsp_opto_process()` для LEVEL-режима — судя по докстроке API, короткая (просто дебаунс+
колбэк), похоже на `bsp_button_poll()` по характеру.
- **Кто ВЛАДЕЕТ состоянием «звонок»/«ответ»** и решает, что показывать — новый доменный слой
(аналог `controller`, т.к. это не протокольные данные СУЛ — ARCH §8 п.3 явно относит опто к
«локальным входам», не к `sul_result_t`) или это чисто presentation-слой стейт внутри
`fallback.c` (как `icon_img_ptr` в старом проекте)? От этого зависит, нужен ли новый
`indication_task_t`-подобный diff или простой уровневый флаг.
- **Как будить `render_task`.** Сейчас MPSC — два продюсера (`sul_rx_task`, `menu_task`) →
один консюмер через `xTaskNotifyGive`/`ulTaskNotifyTake(pdTRUE,...)` (см. TASKS.md §3). Опто
становится **третьим продюсером** — сам механизм (notify) масштабируется без переделки, но
нужно решить: пишет ли опто-обработчик прямо в `render_task`'овские структуры (как
`g_menu`/очередь) или заводит свою пару «состояние + notify»; и как это соотносится с
«мягкой паузой» на время меню (опто-события во время открытого меню — игнорировать, копить,
или отрабатывать сразу?).
- Debounce у `bsp_opto` уже встроен (`debounce_ms` в конфиге) — доп. дебаунс на уровне задачи не
нужен, в отличие от кнопок (там `BUTTON_DEBOUNCE_SAMPLES` в `bsp_button.c` — разные модули,
не путать подходы).
## 3. Тумблер логов (под-шаг 3.6)
**Фактическое состояние:**
- `settings_device_t.log_enabled` — персистится, есть пункт меню «Логи» (`menu_tree.c`, `T_LOG`,
`MENU_BOOL`) — **уже работает как UI и хранение**, но ни на что не влияет: `log_set_enabled()`/
`log_is_enabled()` **не существуют** в `utils/log/log.h` — рантайм-гейта поверх компайл-тайм
`LOG_LEVEL` нет вообще. Сейчас переключение пункта «Логи» — чистый no-op по факту.
- PLAN.md уже фиксирует важный нюанс: продакшн-сборка должна собираться с `LOG_LEVEL >= INFO`
(иначе `LOG_I`/`LOG_E` разворачиваются в `((void)0)` на этапе компиляции — рантайм-гейт
гейтить нечего, макросы физически вырезаны).
- **Найдено и починено в эту сессию** (контекст для обсуждения «слоёв»): FreeRTOS strong-override
мьютекса логгера (`port/log/src/log_mutex.c`) был под `#if 0` и не собирался ни в один таргет —
общий `static`-буфер логгера (`utils/log/log.c`) писался из нескольких задач без синхронизации.
Уже включено в сборку `app`. Держать в уме при обсуждении «слоёв» логов — это фундамент
(потокобезопасность), тумблер (3.6) — уровень выше него.
**Обсудить:** глобальный вкл/выкл (один бит, как сейчас в `settings_device_t`) или гейт по тегам
(`LOG_TAG` — сейчас `"bringup"`/`"menu"`/`"sul_rx"` и т.д.) — второе гибче для будущей диагностики
в поле, но за пределами однобитового поля потребует более сложного хранения. Также: временная
диагностика этой сессии (тайминги `gfx_present()`) была снесена целиком (H4) — стоит ли новый
рантайм-тумблер логов проектировать так, чтобы **подобную точечную диагностику** можно было
включать в поле без пересборки (актуально для будущих HW-расследований), а не только глушить/
пускать весь поток.
## 4. Ревизия движка настроек по плану
Не новая реализация — **аудит текущего `services/settings_store` против замысла ARCH §8 /
PLAN.md Фаза 3** перед тем, как Фаза 4 добавит ярус C (клиентский TLV). Что уже проверено в этой
сессии:
| Ярус | Замысел (ARCH §8) | Факт сейчас |
| --- | --- | --- |
| **A** железобетонные | вес/вместимость/громкости/серийник/год | Поля есть в `settings_user_t`, персистятся; реальный **эффект** (рендер/звук) — Фазы 5/6, ещё не подключён |
| **B** протокольные | `sul_settings_desc_t` на протокол, меню строится из дескриптора | `proto_slice[0]` = адрес НКУ-CAN — единственное поле с реальным эффектом (decode + HW-фильтры CAN, оба конца). Дескрипторного механизма (см. п.1) физически нет — меню сейчас **хардкодит** привязку по `offsetof`, не строится из данных протокола |
| **C** клиентский TLV | лого/шаблон/сдвиги этажей, рядом с layout | Не реализовано — Фаза 4/5, ожидаемо |
Ключевой пробел для ревизии: **дескрипторный принцип «настройка = строка данных»** пока
реализован только для меню-дерева (`menu_item_desc_t[]`), но НЕ для протокольного яруса B —
секция «Протокол» в дереве прописана руками (`T_PROTO`/`T_ADDR` как отдельные статичные записи),
а не сгенерирована из `sul_settings_desc_t` активного протокола. Это ровно то, что должен закрыть
п.1 (3.3) — после чего эта ревизия по факту и должна показать честную картину.
## 5. `docs/tft_app/SETTINGS.md` (обязательный выход Фазы 3)
Дословно из PLAN.md: «без воды: состав `settings_t`, ярусы A/B/C и их взаимосвязи,
`proto_slice`/дескрипторы (§8), гибридное хранение (ядро-struct + клиентский TLV), карта QSPI
(§10, размер-независимость). По паттерну "доки по реализованному"».
**Писать ПОСЛЕДНИМ в этом хвосте** — репозиторий последовательно держит паттерн «документация
по факту реализованного» (см. существующие `docs/tft_app/*.md` — каждый описывает код как он
есть, не архитектурные намерения). Если написать `SETTINGS.md` до п.1 (дескрипторы), придётся
переписывать сразу же после — `proto_slice`/дескрипторы буквально в required-списке документа.
---
## Предлагаемый порядок (не более чем предложение — решать в целевом треде)
1. **П.4** (ревизия) — короткий, даёт общую картину, ничего не ломает.
2. **П.1** (протокол-дескрипторы + демо) — самый большой архитектурный кусок, разблокирует
честную секцию «B» для ревизии и для будущего SETTINGS.md.
3. **П.2** (опто) — независим от 1/3, можно параллельно.
4. **П.3** (тумблер логов) — маленький, изолированный.
5. **П.5** (SETTINGS.md) — после того как 1 реально закрыт (не раньше).
Все пять пунктов явно помечены пользователем как «согласуем/обсудим совместно» — начинать
реализацию любого из них без обсуждения конкретной развилки (см. «Открытые вопросы» в каждом
пункте) не нужно.

View file

@ -18,7 +18,7 @@
| 0 | Каркас проекта и сборка | ✅ | | 0 | Каркас проекта и сборка | ✅ |
| 1 | Walking skeleton (CAN → модель → этаж + стрелки) | ✅ | | 1 | Walking skeleton (CAN → модель → этаж + стрелки) | ✅ |
| 2 | Домен: полный `sul_result_t`, приоритеты, таймаут | 🟨 | | 2 | Домен: полный `sul_result_t`, приоритеты, таймаут | 🟨 |
| 3 | Настройки и меню, локальные входы (opto) | | | 3 | Настройки и меню, локальные входы (opto) | 🟨 |
| 4 | Ассеты: TLV-бандл, паковщик, XIP-блит | ⬜ | | 4 | Ассеты: TLV-бандл, паковщик, XIP-блит | ⬜ |
| 5 | Layout-движок: схема, солвер, темы, injected-регион | ⬜ | | 5 | Layout-движок: схема, солвер, темы, injected-регион | ⬜ |
| 6 | Аудио: движок над MQS, `audio_policy` | ⬜ | | 6 | Аудио: движок над MQS, `audio_policy` | ⬜ |
@ -330,13 +330,13 @@ marks_both_pending`, аппаратно на реальном обрыве св
| --- | --- | | --- | --- |
| **3.1 `services/settings_store`** ✅ | Персист ядра (magic/version/CRC, load/save/get/defaults) через `bsp_qspi_flash`, фикс-сектор `0x450000` (§10, размер-независимо). Сразу: `proto_slice[0]``nku_address` в декодер (замена хардкода `=0`). | | **3.1 `services/settings_store`** ✅ | Персист ядра (magic/version/CRC, load/save/get/defaults) через `bsp_qspi_flash`, фикс-сектор `0x450000` (§10, размер-независимо). Сразу: `proto_slice[0]``nku_address` в декодер (замена хардкода `=0`). |
| **3.2 движок меню + рендер** ✅ (рендер — на одном буфере, см. 3.2.4) | **Чистая модель** (дерево-данные, навигация, edit, offset-привязка) — оперирует переданным `settings_t*`, **сама не сохраняет** (выставляет флаг save, app зовёт `settings_store_save()`) → host-тест без QSPI. **Рендер** (примитивы `gfx`: список/курсор/значение) в **переносимом окне 480×272 @ логич.(0,0)** (одинаково на всех панелях; на больших — левый-верхний угол, остальное чёрное). Вход — **долгое нажатие BUTTON_2** (~1.52 с); BUTTON_1=следующий, короткое BUTTON_2=выбор/инкремент. Опрос ввода — **софт-таймер 5 мс** (не задача; масштабируется на opto). Модальный экран. | | **3.2 движок меню + рендер** ✅ (рендер — на одном буфере, см. 3.2.4) | **Чистая модель** (дерево-данные, навигация, edit, offset-привязка) — оперирует переданным `settings_t*`, **сама не сохраняет** (выставляет флаг save, app зовёт `settings_store_save()`) → host-тест без QSPI. **Рендер** (примитивы `gfx`: список/курсор/значение) в **переносимом окне 480×272 @ логич.(0,0)** (одинаково на всех панелях; на больших — левый-верхний угол, остальное чёрное). Вход — **долгое нажатие BUTTON_2** (~1.52 с); BUTTON_1=следующий, короткое BUTTON_2=выбор/инкремент. Опрос ввода — **софт-таймер 5 мс** (не задача; масштабируется на opto). Модальный экран. |
| **3.2.4 фундамент рендера (double-buffer + PXP)** 🟨 код-комплит, ожидает железо | Перенос из Фазы 4. Убирает tearing/тормоза, снимает костыль частичной отрисовки. Эталон — TFT8_UKL. R1→R4 реализованы (ниже), Debug/Release + host зелёные; ✅ после проверки на стенде. | | **3.2.4 фундамент рендера (double-buffer + PXP)** | Перенос из Фазы 4. Убирает tearing/тормоза, снимает костыль частичной отрисовки. Гибрид bpp (AS=8888/PS+FB=565) + оконный композит меню — см. раздел ниже. Подтверждено на стенде (4 цикла) + реальной СУЛ. |
| **3.3 per-protocol дескрипторы** | `sul_settings_desc_t`: протокол регистрирует параметры (NKU-CAN: адрес 0..15); секция меню строится из дескриптора. | | **3.3 per-protocol дескрипторы** | `sul_settings_desc_t`: протокол регистрирует параметры (NKU-CAN: адрес 0..15); секция меню строится из дескриптора. |
| **3.4 opto-входы** | IN1/IN2 → вызов/ответ диспетчера → презентация (в fallback — примитив/текст; иконки-спрайты — Фаза 4/5). | | **3.4 opto-входы** | IN1/IN2 → вызов/ответ диспетчера → презентация (в fallback — примитив/текст; иконки-спрайты — Фаза 4/5). |
| **3.5 удалённая адресация NKU-CAN** | `0x4X1`/`0x5XB` → запись `nku_address` в настройки (долг Фазы 2). | | **3.5 удалённая адресация NKU-CAN** | `0x4X1`/`0x5XB` → запись `nku_address` в настройки (долг Фазы 2). |
| **3.6 тумблер логов** | `log_enabled` в настройках + пункт меню + рантайм-гейт `log_set_enabled()` поверх компайл-тайм `LOG_LEVEL`. NB: продакшн-сборка — с `LOG_LEVEL >= INFO`, иначе гейтить нечего (макросы вырезаны). | | **3.6 тумблер логов** | `log_enabled` в настройках + пункт меню + рантайм-гейт `log_set_enabled()` поверх компайл-тайм `LOG_LEVEL`. NB: продакшн-сборка — с `LOG_LEVEL >= INFO`, иначе гейтить нечего (макросы вырезаны). |
### 3.2.4 — Фундамент рендера: double-buffer + PXP (🟨 код-комплит, ожидает железо) ### 3.2.4 — Фундамент рендера: double-buffer + PXP ()
> **Статус: R1→R4 реализованы, Debug/Release + host (21/21) зелёные; ожидает > **Статус: R1→R4 реализованы, Debug/Release + host (21/21) зелёные; ожидает
> подтверждения на стенде.** По прецеденту Фаз 1/2 ✅ ставится только после > подтверждения на стенде.** По прецеденту Фаз 1/2 ✅ ставится только после
@ -453,15 +453,66 @@ marks_both_pending`, аппаратно на реальном обрыве св
> `render_task` не получал CPU для самого первого рендера, пока `sul_rx_task` > `render_task` не получал CPU для самого первого рендера, пока `sul_rx_task`
> непрерывно спинила. Должно закрыться тем же фиксом приоритетов. > непрерывно спинила. Должно закрыться тем же фиксом приоритетов.
> >
> _Осталось для ✅ (третий цикл проверки):_ прошить подписанный > _Итог третьего цикла (проверено на стенде + реальной станции, адрес 1):_
> `build/Debug/signed/app_slot_a.bin` и на TFT8: (а) старт без связи со > (а)-(в) подтверждены; данные на реальной СУЛ отображаются с приемлемой
> станцией — прочерки видны сразу, без захода в меню; (б) обрыв связи > задержкой (после фикса HW-фильтров транспорта, см. Фазу 2). Остались 66 мс
> мид-сессии → «--» сразу, без залипания; восстановление — сразу; (в) вход и > `gfx_present()` → следующий блок.
> выход из меню — короткими BUTTON_1/действием, срабатывают стабильно, в т.ч. >
> без связи со станцией. Плюс субъективно оценить навигацию в меню (66 мс/шаг > ---
> — приемлемо или нужно отдельно разбираться со снижением стоимости >
> `gfx_present()` для модального окна меню) — открытый вопрос, решить по > **Ускорение рендера (2026-07-22, продолжение): гибрид bpp + оконный композит.**
> итогам этой проверки. >
> Диагностика 66 мс (AN12437 + расчёт): полоса SDRAM 16-бит @ ~136 МГц ≈ 272 МБ/с;
> непрерывный сканаут ELCDIF 800×600××65 Гц ≈ 126 МБ/с (46%!); полнокадровый
> PXP-композит двигает AS+PS+out = 5.76 МБ → ~66 мс на остатке полосы. НЕ XIP
> (PXP — аппаратный DMA-мастер, FlexSPI — отдельная шина) и НЕ конфиг SEMC
> (наш `bsp_sdram` — побитовый порт golden-DCD: те же тайминги, ~136 vs 133 МГц;
> единственный дифф — AXI-QoS у нас мёртвый код под `__NIC301_EXPERIMENTS_`,
> на скорость PXP не влияет). Перекройка загрузчика под выполнение из SDRAM
> эту проблему бы НЕ решила.
>
> **Решение 1 (согласовано, ФИНАЛЬНОЕ): гибрид bpp.** AS остаётся ARGB8888
> (примитивы/блендинг/палитра НЕ тронуты — полные 8 бит альфы/цвета);
> PS + оба FB + ELCDIF → RGB565. Форматы поверхностей PXP независимы →
> 565-выход не ломает альфа-смешивание (бленд внутри PXP в ≥8 бит, квантование
> только на записи). Реализация: `bsp_display` расширен АДДИТИВНО
> (`bsp_display_pixel_format_t` + `bsp_display_init_ex()`; старый
> `bsp_display_init()` = обёртка XRGB8888 — `firmware_test` не тронут);
> `services/gfx` — PS/FB `uint16`, PXP PS/out RGB565. **HW-подтверждено:**
> тест-паттерн (чистые R/G/B, белый — ровно белый, серый-рамп без ступенек,
> тинты палитры) корректен; `pxp=30-32 мс` стабильно (было 66); ncache
> 7.5→4.8 МБ (и 1024×600 теперь влезает в 8 МБ — было 9.8). Единственный
> остаточный риск 565 — бандинг на будущих градиентах/фото-лого (dither у PXP
> RT1052 нет) — закрывается host-side dithering'ом в паковщике ассетов
> (**требование к Фазе 4 — паковщик обязан уметь Floyd-Steinberg при
> конверсии в 565**). `bsp_pxp` как отдельный BSP-слой — обсуждён, отложен
> (API прояснится в Фазе 4/5 на многопроходной композиции).
> - Побочный урок для Фазы 4 (ассеты): формат ассета per-asset тегом в TLV —
> авторинг всегда PNG; непрозрачные фоны → RGB565, альфа-спрайты →
> ARGB8888/4444. Выходной 565 форматы ассетов не ограничивает.
>
> **Решение 2 (согласовано): оконный композит для меню** — 31 мс/шаг всё ещё
> ощущались как прежде (бюджет: debounce 20 + tick 5 + draw ~8 + pxp 31 +
> vsync ~8 ≈ 75 мс). Реализовано: `pxp_configure_rect()` (полная конфигурация
> поверхностей на каждый композит, без скрытого состояния), публичные
> `gfx_present_rect()`/`gfx_clear_rect()`; `menu_view_render()` рисует только
> окно (`MENU_VIEW_WIN_W/H` публичны в menu_view.h); оркестрация в
> `task_render.c`: открытие меню = полный clear AS + ДВА полных present'а
> (double buffering: оба FB обязаны стать корректны вне окна — контракт
> `gfx_present_rect`), навигация = только окно 480×272 (~27% кадра, ожидаем
> ~8 мс → шаг ≈ 45 мс). Индикация — полные кадры, как была. Тест-паттерн
> верификации 565 снят (отработал).
>
> **Четвёртый цикл подтверждён на стенде** (2026-07-22): навигация меню
> заметно быстрее, открытие/закрытие корректны (контракт двух полных
> present'ов на открытии — работает, мусора вне окна нет), индикация не
> регрессировала. **H4-очистка сделана:** временные таймер-логи убраны из
> `present_rect_internal()` (gfx.c), вместе с ними — `#include log/log.h`+
> `task.h` (были только ради диагностики) и `port_log_uart` из
> `gfx/CMakeLists.txt`. Debug/Release/host (21/21) — зелёные, подписан
> финальный образ.
>
> **Фаза 3.2.4 — ✅.**

View file

@ -1,6 +1,6 @@
/** /**
* @file task_render.c * @file task_render.c
* @brief Презентация: единственный владелец дисплея и вызывающий gfx_present(). * @brief Презентация: единственный владелец дисплея и вызывающий gfx_present*().
* *
* Event-driven (xTaskNotifyGive от sul_rx_task И menu_task MPSC, будят оба * Event-driven (xTaskNotifyGive от sul_rx_task И menu_task MPSC, будят оба
* продюсера, ulTaskNotifyTake(pdTRUE,...) схлопывает несколько notify в одно * продюсера, ulTaskNotifyTake(pdTRUE,...) схлопывает несколько notify в одно
@ -8,10 +8,12 @@
* менялось). НЕ содержит кнопочной логики разделено от menu_task на Фазе * менялось). НЕ содержит кнопочной логики разделено от menu_task на Фазе
* 3.2.4 (см. task_menu.c про причину). * 3.2.4 (см. task_menu.c про причину).
* *
* На каждое пробуждение: меню открыто рисует меню; иначе если меню ТОЛЬКО * Меню оконный рендер (Фаза 3.2.4, ускорение навигации): на ОТКРЫТИИ полная
* ЧТО закрылось, сразу восстанавливает последнее известное состояние индикации * очистка AS (стереть индикацию) + два полных gfx_present() подряд (double
* (не дожидаясь свежего сообщения sul_rx_task мог простаивать под * buffering: оба FB обязаны получить корректный кадр вне окна контракт
* g_menu_active), и дренирует g_render_queue, если там свежий diff. * gfx_present_rect); дальше НАВИГАЦИЯ перерисовка и композит только окна
* 480×272 (~27% кадра, пропорционально дешевле). Индикация полные кадры,
* как и была.
*/ */
#include "app_tasks.h" #include "app_tasks.h"
@ -48,15 +50,30 @@ void render_task(void *p_arg)
(void) ulTaskNotifyTake(pdTRUE, portMAX_DELAY); (void) ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
const bool MENU_OPEN_NOW = menu_is_open(&g_menu); const bool MENU_OPEN_NOW = menu_is_open(&g_menu);
bool present_needed = false;
if (MENU_OPEN_NOW) if (MENU_OPEN_NOW)
{ {
if (!was_menu_open)
{
/* Открытие: стереть индикацию из ВСЕГО AS и прогнать полный
* кадр в ОБА FB после этого вне окна оба буфера корректны
* (чёрные), и навигация может обновлять только окно. */
gfx_clear();
menu_view_render(&g_menu); menu_view_render(&g_menu);
present_needed = true; gfx_present();
gfx_present(); /* тот же AS — во второй FB (double buffering) */
} }
else else
{ {
/* Навигация/правка: только окно (~27% кадра). */
menu_view_render(&g_menu);
gfx_present_rect(0U, 0U, MENU_VIEW_WIN_W, MENU_VIEW_WIN_H);
}
}
else
{
bool present_needed = false;
if (was_menu_open) if (was_menu_open)
{ {
/* Меню только что закрылось — восстановить индикацию /* Меню только что закрылось — восстановить индикацию
@ -74,11 +91,11 @@ void render_task(void *p_arg)
ui_fallback_render(&msg.task, &msg.result); ui_fallback_render(&msg.task, &msg.result);
present_needed = true; present_needed = true;
} }
}
if (present_needed) if (present_needed)
{ {
gfx_present(); /* PXP-композит AS+PS → задний FB + свап (tear-free) */ gfx_present(); /* индикация — всегда полный кадр */
}
} }
was_menu_open = MENU_OPEN_NOW; was_menu_open = MENU_OPEN_NOW;

View file

@ -243,9 +243,8 @@ sul_status_t nku_can_decode(void *p_ctx, const sul_frame_t *p_frame, sul_result_
const uint32_t ID = p_frame->id; const uint32_t ID = p_frame->id;
/* ID известного пакета совпал, но DLC не тот — малформированный кадр. */ /* ID известного пакета совпал, но DLC не тот — малформированный кадр. */
if ((ID == (PACKET1_BASE | G4)) || (ID == (PACKET2_BASE | G4)) || if ((ID == (PACKET1_BASE | G4)) || (ID == (PACKET2_BASE | G4)) || (ID == (PACKET3_BASE | G4)) ||
(ID == (PACKET3_BASE | G4)) || (ID == (PACKET4_BASE | G4)) || (ID == (PACKET4_BASE | G4)) || (ID == (PACKET5_BASE | G6)))
(ID == (PACKET5_BASE | G6)))
{ {
if (p_frame->len != PROTO_DLC) if (p_frame->len != PROTO_DLC)
{ {

View file

@ -24,8 +24,6 @@ set_source_files_properties(
COMPILE_OPTIONS "-include;${CMAKE_CURRENT_SOURCE_DIR}/fonts/include/fonts.h;-w") COMPILE_OPTIONS "-include;${CMAKE_CURRENT_SOURCE_DIR}/fonts/include/fonts.h;-w")
# sdk_common — fsl_common.h (AT_NONCACHEABLE_SECTION_ALIGN); sdk_pxp — PXP-компоновщик; # sdk_common — fsl_common.h (AT_NONCACHEABLE_SECTION_ALIGN); sdk_pxp — PXP-компоновщик;
# freertos_kernel — семафор FRAME_DONE в gfx_present/ISR-колбэке (Фаза 3.2.4); # freertos_kernel — семафор FRAME_DONE в gfx_present/ISR-колбэке (Фаза 3.2.4).
# port_log_uart — ВРЕМЕННО, тайминг-диагностика в gfx_present() (см. её тело) —
# убрать вместе с диагностикой, когда причина лага индикации найдена.
target_link_libraries(tft_app_gfx PUBLIC bsp_display bsp_sdram sdk_common PRIVATE sdk_pxp target_link_libraries(tft_app_gfx PUBLIC bsp_display bsp_sdram sdk_common PRIVATE sdk_pxp
freertos_kernel port_log_uart) freertos_kernel)

File diff suppressed because it is too large Load diff

View file

@ -395,6 +395,9 @@
<char code="042f" character="Я"> <char code="042f" character="Я">
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAuklEQVQYlb2OIQrDMBhGP5ILlEJqCoWa2qqKuqhAXI9Q3QPlCNE9QlRsq3OJmIoQAv/MBuumt2c/Hu8DfofWWp/neZZSCj3puq5jVVVV+77vzjk3DMMwjuMIAJxzDimlJCKq67oGgKZpGiKivu97dhzHUUop67qunHP+1dy2bUsppeu6rhhjfJkAAMYYCyEEY4xRSqnbuCzLknPObdu2700AgPfeW2vt5yHM8zwTEU3TNAGAEELczD/zABNrZ21n8NN3AAAAAElFTkSuQmCC</picture> <picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAuklEQVQYlb2OIQrDMBhGP5ILlEJqCoWa2qqKuqhAXI9Q3QPlCNE9QlRsq3OJmIoQAv/MBuumt2c/Hu8DfofWWp/neZZSCj3puq5jVVVV+77vzjk3DMMwjuMIAJxzDimlJCKq67oGgKZpGiKivu97dhzHUUop67qunHP+1dy2bUsppeu6rhhjfJkAAMYYCyEEY4xRSqnbuCzLknPObdu2700AgPfeW2vt5yHM8zwTEU3TNAGAEELczD/zABNrZ21n8NN3AAAAAElFTkSuQmCC</picture>
</char> </char>
<char code="0020" character=" ">
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAADklEQVQYlWNgGAXDCQAAAdAAAYH02hIAAAAASUVORK5CYII=</picture>
</char>
<char code="0430" character="а"> <char code="0430" character="а">
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAu0lEQVQYlc2QIQrEMBBFJ8NSqKgKFCLqajKmIvcoOU18XA4RV5UL5AC1tT1AbhBTURGYFcsu7AEW9tn/xOMD/CPGGJNzztd1Xfu+7+u6rp/xOI4jxhiJiEIIoZRSEBHFW5imaVJKKSmlzDlnrbV+AACklJK11p7neQohBAAAIiIQETEzL8uyAACM4zgyMxMRYa21ttbaPM/zMAyDc8591Xrvfa213vd9b9u2MTNrrfVH6Lqu6/u+/+lnL55FK0k0AK3doAAAAABJRU5ErkJggg==</picture> <picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAu0lEQVQYlc2QIQrEMBBFJ8NSqKgKFCLqajKmIvcoOU18XA4RV5UL5AC1tT1AbhBTURGYFcsu7AEW9tn/xOMD/CPGGJNzztd1Xfu+7+u6rp/xOI4jxhiJiEIIoZRSEBHFW5imaVJKKSmlzDlnrbV+AACklJK11p7neQohBAAAIiIQETEzL8uyAACM4zgyMxMRYa21ttbaPM/zMAyDc8591Xrvfa213vd9b9u2MTNrrfVH6Lqu6/u+/+lnL55FK0k0AK3doAAAAABJRU5ErkJggg==</picture>
</char> </char>

View file

@ -106,6 +106,25 @@ void gfx_clear(void);
*/ */
void gfx_present(void); void gfx_present(void);
/**
* @brief Показать ТОЛЬКО прямоугольник (x,y,w,h): PXP-композит региона AS над
* PS тот же регион заднего FB, затем tear-free свап.
*
* Дешевле полного gfx_present() пропорционально площади (окно меню 480×272
* ~27% кадра). Клампится по экрану.
*
* @warning КОНТРАКТ ВЫЗЫВАЮЩЕГО: вне прямоугольника задний FB не
* перекомпоновывается из-за double buffering там содержимое ДВУХ present'ов
* назад. Использовать только когда ОБА FB уже содержат корректный кадр вне
* прямоугольника (модальное меню: на открытии два полных gfx_present()
* подряд с одним AS, затем навигация только окно; см. task_render.c).
*/
void gfx_present_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h);
/** Обнулить (сделать прозрачным) только прямоугольник AS — дешёвая замена
* полного gfx_clear() для оконной перерисовки (окно меню). Клампится. */
void gfx_clear_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h);
/** /**
* @brief Нарисовать строку заданным цветом (тинтинг с альфа-сглаживанием). * @brief Нарисовать строку заданным цветом (тинтинг с альфа-сглаживанием).
* *

View file

@ -3,15 +3,11 @@
#include "FreeRTOS.h" #include "FreeRTOS.h"
#include "fsl_common.h" /* AT_NONCACHEABLE_SECTION_ALIGN */ #include "fsl_common.h" /* AT_NONCACHEABLE_SECTION_ALIGN */
#include "fsl_pxp.h" #include "fsl_pxp.h"
#include "log/log.h" /* ВРЕМЕННО (Фаза 3.2.4 HW-расследование лага) — см. gfx_present() */
#include "semphr.h" #include "semphr.h"
#include "task.h" /* xTaskGetTickCount — тайминг-инструментация ниже */
#include <stddef.h> #include <stddef.h>
#include <string.h> #include <string.h>
#define LOG_TAG "gfx"
/* ── Поверхности компоновщика (SDRAM, non-cacheable) ──────────────────────── /* ── Поверхности компоновщика (SDRAM, non-cacheable) ────────────────────────
* *
* AT_NONCACHEABLE_SECTION_ALIGN размещает переменную в линкер-секции * AT_NONCACHEABLE_SECTION_ALIGN размещает переменную в линкер-секции
@ -28,19 +24,26 @@
* FB[2] (выход PXP = вход ELCDIF, double buffer). Размер под ТЕКУЩУЮ панель * FB[2] (выход PXP = вход ELCDIF, double buffer). Размер под ТЕКУЩУЮ панель
* стенда (TFT8, 800×600), не под BSP_DISPLAY_MAX_*: когда app-big (ARCH §9) * стенда (TFT8, 800×600), не под BSP_DISPLAY_MAX_*: когда app-big (ARCH §9)
* станет рантайм-выбирать между TFT7/8/10 в одном бинарнике, размер поверхностей * станет рантайм-выбирать между TFT7/8/10 в одном бинарнике, размер поверхностей
* и m_sdram_ncache придётся поднять до максимума панели. */ * и m_sdram_ncache придётся поднять до максимума панели.
*
* ГИБРИД bpp (Фаза 3.2.4): AS ARGB8888 (8-бит альфа полноценный AA, примитивы
* пишут сюда без изменений); PS + оба FB RGB565 (вдвое меньше полосы: ELCDIF
* сканирует FB непрерывно, это доминирующая нагрузка на 16-бит SDRAM). PXP читает
* AS(8888)/PS(565), блендит внутри в 8 бит, пишет выход в 565. Форматы поверхностей
* PXP независимы, поэтому 565-выход НЕ ломает альфа-смешивание. */
#define FRAMEBUFFER_ALIGN 64U /* см. OLD_PROJECT FRAME_BUFFER_ALIGN — типичное ELCDIF/PXP/AXI выравнивание */ #define FRAMEBUFFER_ALIGN 64U /* см. OLD_PROJECT FRAME_BUFFER_ALIGN — типичное ELCDIF/PXP/AXI выравнивание */
#define SURFACE_PIXELS (800U * 600U) #define SURFACE_PIXELS (800U * 600U)
#define BYTES_PER_PIXEL 4U #define AS_BYTES_PER_PIXEL 4U /* AS: ARGB8888 */
#define FB_BYTES_PER_PIXEL 2U /* PS + выходные FB: RGB565 */
#define ALPHA_OPAQUE 0xFF000000U /* AS: alpha=0xFF → пиксель непрозрачен для PXP-блендинга */ #define ALPHA_OPAQUE 0xFF000000U /* AS: alpha=0xFF → пиксель непрозрачен для PXP-блендинга */
#define FALLBACK_CHAR '-' #define FALLBACK_CHAR '-'
/* AS — CPU рисует сюда (ARGB8888, alpha значим). */ /* AS — CPU рисует сюда (ARGB8888, alpha значим). Гибрид: остаётся 32-бит. */
AT_NONCACHEABLE_SECTION_ALIGN(static uint32_t s_alpha_buffer[SURFACE_PIXELS], FRAMEBUFFER_ALIGN); AT_NONCACHEABLE_SECTION_ALIGN(static uint32_t s_alpha_buffer[SURFACE_PIXELS], FRAMEBUFFER_ALIGN);
/* PS — фон под AS (сейчас сплошной чёрный, заливается однократно в gfx_init). */ /* PS — фон под AS (сейчас сплошной чёрный, заливается однократно в gfx_init). RGB565. */
AT_NONCACHEABLE_SECTION_ALIGN(static uint32_t s_processing_buffer[SURFACE_PIXELS], FRAMEBUFFER_ALIGN); AT_NONCACHEABLE_SECTION_ALIGN(static uint16_t s_processing_buffer[SURFACE_PIXELS], FRAMEBUFFER_ALIGN);
/* Выходные буферы PXP = сканируемые ELCDIF (double buffer, свап в gfx_present). */ /* Выходные буферы PXP = сканируемые ELCDIF (double buffer, свап в gfx_present). RGB565. */
AT_NONCACHEABLE_SECTION_ALIGN(static uint32_t s_framebuffer[2][SURFACE_PIXELS], FRAMEBUFFER_ALIGN); AT_NONCACHEABLE_SECTION_ALIGN(static uint16_t s_framebuffer[2][SURFACE_PIXELS], FRAMEBUFFER_ALIGN);
static uint16_t s_fb_width; static uint16_t s_fb_width;
static uint16_t s_fb_height; static uint16_t s_fb_height;
@ -326,34 +329,66 @@ void gfx_draw_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, gfx_color_t c
/* ── PXP-компоновщик (порт OLD_PROJECT_TFT8_UKL/source/display/pxp_config.c) ── */ /* ── PXP-компоновщик (порт OLD_PROJECT_TFT8_UKL/source/display/pxp_config.c) ── */
/* PS-формат: для RT1052 (расширенная таблица форматов, FSL_FEATURE_PXP_HAS_NO_ /**
* EXTEND_PIXEL_FORMAT не определён) 32-битный формат без реального альфа-канала * @brief Настроить поверхности PXP под композит прямоугольника (x,y,w,h).
* называется kPXP_PsPixelFormatARGB8888 (0x4) фон, альфа PS в блендинге не *
* участвует (значима альфа AS, kPXP_AlphaEmbedded). */ * Адреса PS/AS/выхода смещаются к (x,y); pitch остаётся полной строкой
* поверхности (страйд не меняется) PXP пишет w×h с шагом полного ряда,
* попадая ровно в прямоугольник кадра. Позиции PS/AS (0,0,w,h) относительно
* выходного кадра w×h (та же конвенция, что была для полного кадра эталон
* TFT8_UKL, проверено на железе).
*
* Полная конфигурация на КАЖДЫЙ композит (без скрытого состояния между
* полным и оконным present'ом) записи регистров PXP дёшевы.
*
* @note Текущий потребитель оконного композита окно меню @ (0,0): смещения
* по y кратны полной строке (800×4=3200 Б AS / 800×2=1600 Б FB кратны
* 64), выравнивание сохраняется. Для произвольного x следить за
* выравниванием адресов под требования PXP.
*/
static void pxp_configure_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint32_t out_addr)
{
const uint32_t OFFSET = (uint32_t) y * s_fb_width + x;
/* PS — фон RGB565 (гибрид). Альфа PS в блендинге не участвует (значима альфа AS). */
const pxp_ps_buffer_config_t ps_cfg = {
.pixelFormat = kPXP_PsPixelFormatRGB565,
.swapByte = false,
.bufferAddr = (uint32_t) &s_processing_buffer[OFFSET],
.bufferAddrU = 0U,
.bufferAddrV = 0U,
.pitchBytes = (uint16_t) (s_fb_width * FB_BYTES_PER_PIXEL),
};
PXP_SetProcessSurfaceBufferConfig(PXP, &ps_cfg);
/* AS — ARGB8888 (гибрид: остаётся 32-бит ради полной 8-бит альфы/AA). */
const pxp_as_buffer_config_t as_cfg = {
.pixelFormat = kPXP_AsPixelFormatARGB8888,
.bufferAddr = (uint32_t) &s_alpha_buffer[OFFSET],
.pitchBytes = (uint16_t) (s_fb_width * AS_BYTES_PER_PIXEL),
};
PXP_SetAlphaSurfaceBufferConfig(PXP, &as_cfg);
/* Выход RGB565 (гибрид): PXP квантует результат бленда в 565 на записи. */
s_output_cfg.pixelFormat = kPXP_OutputPixelFormatRGB565;
s_output_cfg.interlacedMode = kPXP_OutputProgressive;
s_output_cfg.buffer0Addr = out_addr;
s_output_cfg.buffer1Addr = 0U;
s_output_cfg.pitchBytes = (uint16_t) (s_fb_width * FB_BYTES_PER_PIXEL);
s_output_cfg.width = w;
s_output_cfg.height = h;
PXP_SetOutputBufferConfig(PXP, &s_output_cfg);
PXP_SetProcessSurfacePosition(PXP, 0U, 0U, w, h);
PXP_SetAlphaSurfacePosition(PXP, 0U, 0U, w, h);
}
static void gfx_pxp_init(void) static void gfx_pxp_init(void)
{ {
PXP_Init(PXP); PXP_Init(PXP);
const uint16_t PITCH = (uint16_t) (s_fb_width * BYTES_PER_PIXEL); /* Embedded alpha: доля AS-пикселя над PS берётся из его альфа-байта.
* Одноразовая часть конфигурации (per-композит pxp_configure_rect). */
const pxp_ps_buffer_config_t ps_cfg = {
.pixelFormat = kPXP_PsPixelFormatARGB8888,
.swapByte = false,
.bufferAddr = (uint32_t) s_processing_buffer,
.bufferAddrU = 0U,
.bufferAddrV = 0U,
.pitchBytes = PITCH,
};
PXP_SetProcessSurfaceBufferConfig(PXP, &ps_cfg);
const pxp_as_buffer_config_t as_cfg = {
.pixelFormat = kPXP_AsPixelFormatARGB8888,
.bufferAddr = (uint32_t) s_alpha_buffer,
.pitchBytes = PITCH,
};
PXP_SetAlphaSurfaceBufferConfig(PXP, &as_cfg);
/* Embedded alpha: доля AS-пикселя над PS берётся из его альфа-байта. */
const pxp_as_blend_config_t blend_cfg = { const pxp_as_blend_config_t blend_cfg = {
.alpha = 0xFFU, .alpha = 0xFFU,
.invertAlpha = false, .invertAlpha = false,
@ -362,19 +397,9 @@ static void gfx_pxp_init(void)
}; };
PXP_SetAlphaSurfaceBlendConfig(PXP, &blend_cfg); PXP_SetAlphaSurfaceBlendConfig(PXP, &blend_cfg);
s_output_cfg.pixelFormat = kPXP_OutputPixelFormatARGB8888;
s_output_cfg.interlacedMode = kPXP_OutputProgressive;
s_output_cfg.buffer0Addr = (uint32_t) s_framebuffer[0];
s_output_cfg.buffer1Addr = 0U;
s_output_cfg.pitchBytes = PITCH;
s_output_cfg.width = s_fb_width;
s_output_cfg.height = s_fb_height;
PXP_SetOutputBufferConfig(PXP, &s_output_cfg);
PXP_EnableCsc1(PXP, false); /* включён по умолчанию — фон RGB, конверсия не нужна */ PXP_EnableCsc1(PXP, false); /* включён по умолчанию — фон RGB, конверсия не нужна */
PXP_SetProcessSurfacePosition(PXP, 0U, 0U, s_fb_width, s_fb_height); pxp_configure_rect(0U, 0U, s_fb_width, s_fb_height, (uint32_t) s_framebuffer[0]);
PXP_SetAlphaSurfacePosition(PXP, 0U, 0U, s_fb_width, s_fb_height);
} }
/* Запустить PXP и дождаться завершения композиции (busy-wait, как в эталоне). */ /* Запустить PXP и дождаться завершения композиции (busy-wait, как в эталоне). */
@ -417,8 +442,10 @@ bsp_status_t gfx_init(bsp_display_type_t type)
(void) memset(s_alpha_buffer, 0, sizeof(s_alpha_buffer)); (void) memset(s_alpha_buffer, 0, sizeof(s_alpha_buffer));
(void) memset(s_framebuffer, 0, sizeof(s_framebuffer)); (void) memset(s_framebuffer, 0, sizeof(s_framebuffer));
/* Семафор создан и колбэк готов ДО включения IRQ внутри bsp_display_init. */ /* Семафор создан и колбэк готов ДО включения IRQ внутри bsp_display_init_ex.
const bsp_status_t st = bsp_display_init(type, (uint32_t) s_framebuffer[0], on_frame_done); * RGB565 (гибрид): выходные FB 16-бит ELCDIF-сканаут вдвое дешевле. */
const bsp_status_t st = bsp_display_init_ex(type, (uint32_t) s_framebuffer[0], on_frame_done,
BSP_DISPLAY_PIXEL_RGB565);
if (st != BSP_OK) if (st != BSP_OK)
{ {
return st; return st;
@ -436,38 +463,75 @@ bsp_status_t gfx_init(bsp_display_type_t type)
void gfx_clear(void) void gfx_clear(void)
{ {
/* Прозрачно (alpha 0) → непрорисованные области покажут фон PS (чёрный). */ /* Прозрачно (alpha 0) → непрорисованные области покажут фон PS (чёрный).
(void) memset(s_alpha_buffer, 0, (size_t) s_fb_width * s_fb_height * BYTES_PER_PIXEL); * Чистим AS (ARGB8888) 4 байта/пиксель. */
(void) memset(s_alpha_buffer, 0, (size_t) s_fb_width * s_fb_height * AS_BYTES_PER_PIXEL);
} }
void gfx_present(void) /* Общий путь полного и оконного present'а: композит прямоугольника в задний
* FB + tear-free свап. Прямоугольник должен быть предварительно заклампен. */
static void present_rect_internal(uint16_t x, uint16_t y, uint16_t w, uint16_t h)
{ {
/* Компонуем в НЕ показываемый сейчас буфер, показываем атомарным свапом. */ /* Компонуем в НЕ показываемый сейчас буфер, показываем атомарным свапом. */
s_back_index ^= 1U; s_back_index ^= 1U;
s_output_cfg.buffer0Addr = (uint32_t) s_framebuffer[s_back_index]; const uint32_t OUT_ADDR =
PXP_SetOutputBufferConfig(PXP, &s_output_cfg); (uint32_t) &s_framebuffer[s_back_index][(uint32_t) y * s_fb_width + x];
pxp_configure_rect(x, y, w, h, OUT_ADDR);
/* ВРЕМЕННО (Фаза 3.2.4, HW-расследование лага индикации ~1-2 c — gfx_pxp_run(); /* AS над PS → прямоугольник заднего FB */
* PLAN.md): раздельный замер PXP busy-wait и ожидания FRAME_DONE. ELCDIF
* по clock_config.c должен давать ~65 Гц (528 МГц PLL2 / 12 / кадр)
* ожидаем pxp единицы мс, vsync до ~15 мс. Если на железе один из них
* систематически большой вот прямой ответ, что именно тормозит. Убрать
* после диагностики (см. include log/log.h и task.h выше тоже под снос
* вместе с этим). */
const TickType_t T0 = xTaskGetTickCount();
gfx_pxp_run(); /* AS над PS → s_framebuffer[s_back_index] */
const TickType_t T1 = xTaskGetTickCount();
/* Синхронизация с развёрткой: дождаться конца кадра, затем отдать ELCDIF /* Синхронизация с развёрткой: дождаться конца кадра, затем отдать ELCDIF
* новый буфер он переключится аппаратно на границе кадра (tear-free). */ * новый буфер он переключится аппаратно на границе кадра (tear-free). */
(void) xSemaphoreTake(s_frame_done, portMAX_DELAY); (void) xSemaphoreTake(s_frame_done, portMAX_DELAY);
const TickType_t T2 = xTaskGetTickCount(); /* ELCDIF всегда получает БАЗОВЫЙ адрес кадра (сканирует весь FB). */
bsp_display_set_next_buffer((uint32_t) s_framebuffer[s_back_index]); bsp_display_set_next_buffer((uint32_t) s_framebuffer[s_back_index]);
}
LOG_I(LOG_TAG, "present: pxp=%u ms vsync=%u ms", (unsigned) (T1 - T0), (unsigned) (T2 - T1));
void gfx_present(void)
{
present_rect_internal(0U, 0U, s_fb_width, s_fb_height);
}
void gfx_present_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h)
{
if ((x >= s_fb_width) || (y >= s_fb_height) || (w == 0U) || (h == 0U))
{
return;
}
/* Клампинг по экрану — как у примитивов. */
if ((uint32_t) x + w > s_fb_width)
{
w = (uint16_t) (s_fb_width - x);
}
if ((uint32_t) y + h > s_fb_height)
{
h = (uint16_t) (s_fb_height - y);
}
present_rect_internal(x, y, w, h);
}
void gfx_clear_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h)
{
if ((x >= s_fb_width) || (y >= s_fb_height) || (w == 0U) || (h == 0U))
{
return;
}
if ((uint32_t) x + w > s_fb_width)
{
w = (uint16_t) (s_fb_width - x);
}
if ((uint32_t) y + h > s_fb_height)
{
h = (uint16_t) (s_fb_height - y);
}
/* Построчный memset региона AS (страйд — полная строка поверхности). */
for (uint16_t row = 0U; row < h; row++)
{
(void) memset(&s_alpha_buffer[(uint32_t) (y + row) * s_fb_width + x], 0,
(size_t) w * AS_BYTES_PER_PIXEL);
}
} }

View file

@ -22,6 +22,11 @@ extern "C"
{ {
#endif #endif
/* Габариты окна меню @ логич.(0,0) — публичны: владелец дисплея (task_render)
* использует их для оконного present'а (gfx_present_rect) при навигации. */
#define MENU_VIEW_WIN_W 480U
#define MENU_VIEW_WIN_H 272U
/** /**
* @brief Отрисовать полный кадр меню в AS (очистка + заголовок + строки + футер). * @brief Отрисовать полный кадр меню в AS (очистка + заголовок + строки + футер).
* *

View file

@ -4,10 +4,11 @@
#include <stdio.h> #include <stdio.h>
/* Окно 480×272 @ (0,0). Метрики под реальные шрифты: JBMono24 h=31 (заголовок/ /* Окно @ (0,0) — габариты публичны (menu_view.h: MENU_VIEW_WIN_W/H, нужны
* строки), JBMono12 h=16 (футер). 36 + 6×36 + 20 = 272. */ * task_render для оконного present'а). Метрики под реальные шрифты: JBMono24
#define WIN_W 480U * h=31 (заголовок/строки), JBMono12 h=16 (футер). 36 + 6×36 + 20 = 272. */
#define WIN_H 272U #define WIN_W MENU_VIEW_WIN_W
#define WIN_H MENU_VIEW_WIN_H
#define TITLE_H 36U #define TITLE_H 36U
#define ROW_H 36U #define ROW_H 36U
#define FOOTER_H 20U #define FOOTER_H 20U
@ -72,7 +73,10 @@ static bool format_value(const menu_ctx_t *p_ctx, uint8_t idx, char *p_buf, size
static void draw_row(const menu_ctx_t *p_ctx, uint8_t idx, uint16_t row_y, bool selected) static void draw_row(const menu_ctx_t *p_ctx, uint8_t idx, uint16_t row_y, bool selected)
{ {
const gfx_color_t BG_COL = selected ? COL_SEL_BG : COL_BG; const gfx_color_t BG_COL = selected ? COL_SEL_BG : COL_BG;
const gfx_color_t TEXT_COL = selected ? COL_SEL_TEXT : COL_LABEL; const gfx_color_t TEXT_COL =
selected
? COL_SEL_TEXT
: COL_LABEL; // FIXME: Conditional operator with identical true and false expressions
gfx_fill_rect(CONTENT_X, row_y, CONTENT_W, ROW_H, BG_COL); gfx_fill_rect(CONTENT_X, row_y, CONTENT_W, ROW_H, BG_COL);
(void) gfx_draw_string(&SystemFont, p_ctx->items[idx].label, LEFT_MARGIN, (void) gfx_draw_string(&SystemFont, p_ctx->items[idx].label, LEFT_MARGIN,
@ -92,7 +96,7 @@ static void draw_footer(const menu_ctx_t *p_ctx, uint8_t level_first, uint8_t le
{ {
gfx_fill_rect(CONTENT_X, (uint16_t) (FOOTER_Y - 1U), CONTENT_W, 1U, COL_SEP); gfx_fill_rect(CONTENT_X, (uint16_t) (FOOTER_Y - 1U), CONTENT_W, 1U, COL_SEP);
(void) gfx_draw_string(&SystemFontSmall, "Кн.1 - далее Кн.2 - выбор", LEFT_MARGIN, (void) gfx_draw_string(&SystemFontSmall, "IN - далее, SEL - выбор", LEFT_MARGIN,
(uint16_t) (FOOTER_Y + TEXT_DY12), COL_FOOTER); (uint16_t) (FOOTER_Y + TEXT_DY12), COL_FOOTER);
const uint8_t TOTAL = (uint8_t) (level_last - level_first + 1U); const uint8_t TOTAL = (uint8_t) (level_last - level_first + 1U);
@ -106,9 +110,11 @@ static void draw_footer(const menu_ctx_t *p_ctx, uint8_t level_first, uint8_t le
void menu_view_render(const menu_ctx_t *p_ctx) void menu_view_render(const menu_ctx_t *p_ctx)
{ {
/* Полный кадр off-screen: обнулить AS (вне окна 480×272 → чёрный фон PS), /* Обнулить и нарисовать ТОЛЬКО окно (дёшево — оконная перерисовка на
* затем нарисовать окно. Свап gfx_present() у владельца дисплея. */ * навигации). Полную очистку AS от индикации вне окна делает владелец
gfx_clear(); * дисплея (task_render) один раз на ОТКРЫТИИ меню вместе с двумя
* полными present'ами (контракт gfx_present_rect). */
gfx_clear_rect(0U, 0U, WIN_W, WIN_H);
if (!p_ctx->open) if (!p_ctx->open)
{ {
@ -118,7 +124,8 @@ void menu_view_render(const menu_ctx_t *p_ctx)
/* Заголовок = подпись текущего уровня (родитель выделенного пункта). */ /* Заголовок = подпись текущего уровня (родитель выделенного пункта). */
const char *p_title = p_ctx->items[p_ctx->items[p_ctx->cur].parent].label; const char *p_title = p_ctx->items[p_ctx->items[p_ctx->cur].parent].label;
const uint16_t TW = gfx_string_width(&SystemFont, p_title); const uint16_t TW = gfx_string_width(&SystemFont, p_title);
(void) gfx_draw_string(&SystemFont, p_title, (uint16_t) ((WIN_W - TW) / 2U), TEXT_DY24, COL_TITLE); (void) gfx_draw_string(&SystemFont, p_title, (uint16_t) ((WIN_W - TW) / 2U), TEXT_DY24,
COL_TITLE);
gfx_fill_rect(CONTENT_X, (uint16_t) (TITLE_H - 1U), CONTENT_W, 1U, COL_SEP); gfx_fill_rect(CONTENT_X, (uint16_t) (TITLE_H - 1U), CONTENT_W, 1U, COL_SEP);
uint8_t first; uint8_t first;
@ -138,6 +145,5 @@ void menu_view_render(const menu_ctx_t *p_ctx)
draw_footer(p_ctx, first, last); draw_footer(p_ctx, first, last);
/* Рамка — последней, поверх содержимого. */
gfx_draw_rect(0U, 0U, WIN_W, WIN_H, COL_SEP); gfx_draw_rect(0U, 0U, WIN_W, WIN_H, COL_SEP);
} }