# tft_app: Phase 3.3 RGB 565 & 888 Hybrid
This commit is contained in:
parent
2662111e1b
commit
958fd9d5e5
19 changed files with 2874 additions and 1285 deletions
|
|
@ -52,6 +52,13 @@ bsp_status_t bsp_display_init(bsp_display_type_t type,
|
|||
uint32_t framebuffer_addr,
|
||||
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_set_rotation(bsp_display_rotation_t rotation);
|
||||
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);
|
||||
```
|
||||
|
||||
**Формат пикселя 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
|
||||
|
|
@ -83,7 +111,7 @@ typedef enum {
|
|||
} 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_NOT_SUPPORTED` | TFT4 — Video PLL не реализован |
|
||||
|
||||
`bsp_display_init()` идемпотентен: повторный вызов без `deinit` возвращает
|
||||
Обе init-функции идемпотентны: повторный вызов без `deinit` возвращает
|
||||
`BSP_OK` без побочных эффектов. Callback `p_on_frame_done` должен быть
|
||||
ISR-safe (`NULL` допускается).
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,22 @@ typedef enum bsp_display_rotation_e
|
|||
BSP_DISPLAY_FLIP_HORIZONTAL, /**< LR=0 UD=0. Горизонтальный флип (SHLR). */
|
||||
} bsp_display_rotation_t;
|
||||
|
||||
/* ── Формат пикселя framebuffer'а (что ELCDIF читает из памяти) ───────── */
|
||||
|
||||
/**
|
||||
* @brief Формат пикселя в памяти framebuffer'а.
|
||||
*
|
||||
* Определяет ширину слова, которое ELCDIF читает по DMA из SDRAM (=нагрузку
|
||||
* на полосу). Ширина шины пинов панели (24-бит) — отдельный параметр драйвера,
|
||||
* от этого НЕ зависит: RGB565 (16 бит/пиксель в памяти) корректно выводится и
|
||||
* на 24-битную панель (ELCDIF расширяет 565→24 на пинах).
|
||||
*/
|
||||
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
|
||||
|
|
@ -77,10 +93,26 @@ typedef void (*bsp_display_frame_cb_t)(void);
|
|||
* Должен быть выровнен по 64 байт, в NonCacheable SDRAM.
|
||||
* @param on_frame_done ISR-safe callback по завершении кадра; NULL — без callback.
|
||||
* @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_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, выключить подсветку, деинициализировать.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -270,9 +270,19 @@ static void enable_lcd_interrupt(void)
|
|||
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 расширяет 565→24 на пинах (см. 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,
|
||||
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->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->polarityFlags = p_cfg->pol_flags;
|
||||
p_out->bufferAddr = framebuffer_addr;
|
||||
p_out->pixelFormat = kELCDIF_PixelFormatXRGB8888;
|
||||
p_out->pixelFormat = map_pixel_format(format);
|
||||
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_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)
|
||||
{
|
||||
|
|
@ -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;
|
||||
|
||||
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);
|
||||
enable_lcd_interrupt();
|
||||
ELCDIF_RgbModeStart(LCDIF);
|
||||
|
|
|
|||
|
|
@ -53,9 +53,12 @@ flowchart TB
|
|||
|
||||
## 2. Путь одного кадра
|
||||
|
||||
`sul_rx`-задача приложения опрашивает CAN, прогоняет кадр через активный декодер и передаёт
|
||||
результат контроллеру; `render`-задача применяет diff. Очередь между ними — глубины 1
|
||||
(`xQueueOverwrite`: важно только последнее состояние, не история).
|
||||
`sul_rx_task` опрашивает CAN, прогоняет кадр через активный декодер и передаёт результат
|
||||
контроллеру; `render_task` применяет diff. **Данные** между ними — очередь глубины 1
|
||||
(`xQueueOverwrite`: важно только последнее состояние, не история); **пробуждение** —
|
||||
`xTaskNotifyGive` (render_task — event-driven, не поллит). Пока открыто меню, `sul_rx_task`
|
||||
находится в «мягкой паузе» (decode/controller/очередь пропускаются, WDOG кормится). Полная
|
||||
картина задач — [TASKS.md](TASKS.md).
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
|
|
@ -116,10 +119,21 @@ flowchart LR
|
|||
## 4. НКУ-CAN: пакет → поля модели
|
||||
|
||||
Порт боевого декодера `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) |
|
||||
| **PACKET2** | `0x408` | `d7&0x40` → перегруз (источник 1) |
|
||||
|
|
@ -138,8 +152,8 @@ flowchart LR
|
|||
seismic — PACKET4), пакет-владелец распоряжается напрямую: сбрасывает в начале и выставляет по
|
||||
условию, гася устаревший режим.
|
||||
|
||||
Удалённая установка адреса (кадры `0x4X1`/`0x5XB`) — **отложена в Фазу 3** (нужен `settings_store`
|
||||
для записи `nku_address`; чистый декодер не пишет настройки). Сейчас эти ID игнорируются.
|
||||
Удалённая установка адреса (кадры `0x4X1`/`0x5XB`) — **под-шаг 3.5** (см. PLAN.md). Сейчас эти
|
||||
ID игнорируются (wildcard-фильтры под них ещё не настраиваются).
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
START([render mode, result]) --> CLR["gfx_clear(BLACK)"]
|
||||
START([render mode, result]) --> CLR["gfx_clear() — AS прозрачен,<br/>фон даёт чёрный PS"]
|
||||
CLR --> Q{"mode == NORMAL?"}
|
||||
Q -->|да| POS["этаж (FloorFontFallback), по центру"]
|
||||
POS --> ARR{"direction UP/DOWN?"}
|
||||
|
|
@ -87,12 +87,15 @@ flowchart LR
|
|||
|
||||
---
|
||||
|
||||
## 5. Известное ограничение — один framebuffer
|
||||
## 5. Как кадр попадает на экран (Фаза 3.2.4 — фундамент рендера)
|
||||
|
||||
Сейчас один буфер, без double buffering: `render()` пишет напрямую в буфер, который ELCDIF в этот
|
||||
момент сканирует по DMA. Если перерисовка не укладывается в кадр развёртки, виден сам процесс
|
||||
закраски (глиф «набирается» за несколько кадров) — **не** tearing и **не** порча пикселей (каждая
|
||||
запись валидна), просто заметен процесс.
|
||||
Ограничение одного framebuffer'а Фазы 1 **снято**. `render()` рисует off-screen в
|
||||
альфа-поверхность **AS** (ARGB8888); показ — отдельным вызовом `gfx_present()` у владельца
|
||||
дисплея ([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).
|
||||
|
|
|
|||
|
|
@ -142,11 +142,18 @@ sequenceDiagram
|
|||
любым цветом со сглаживанием, и оно корректно ложится на полосу-курсор. Значения SELECT/BOOL — из
|
||||
`options[]` дескриптора, BYTE — числом.
|
||||
|
||||
**Полнокадровый рендер** (double-buffer + PXP, Фаза 3.2.4): любое изменение — `menu_view_render()`
|
||||
рисует **весь кадр** off-screen в альфа-поверхность AS (обнуление + окно), затем владелец дисплея
|
||||
зовёт `gfx_present()` (PXP-композит AS над чёрным PS → задний framebuffer + атомарный свап). Рисуем
|
||||
вне экрана, показываем атомарно → tear-free. Компоновщик — `services/gfx` (эталон
|
||||
`OLD_PROJECT_TFT8_UKL/source/display/`).
|
||||
**Оконный рендер** (double-buffer + PXP + гибрид bpp, Фаза 3.2.4): `menu_view_render()` рисует
|
||||
off-screen в альфа-поверхность AS (ARGB8888) **только окно** `MENU_VIEW_WIN_W×H` (windowed clear +
|
||||
отрисовка); показ — у владельца дисплея ([task_render.c](../../firmware/tft_app/src/app/task_render.c)):
|
||||
|
||||
- **открытие меню** — полная очистка 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_render.c](../../firmware/tft_app/src/app/task_render.c)). Найдено на HW-верификации Фазы
|
||||
|
|
@ -154,7 +161,8 @@ sequenceDiagram
|
|||
разделение было безвредным упущением — но `gfx_present()` (double-buffer + PXP) внёс блокирующее
|
||||
ожидание кадра, и в объединённой задаче это ожидание попутно блокировало вход в меню (ноль реакции
|
||||
на кнопки). Эталон разделения — `OLD_PROJECT_TFT8_UKL`: `BUTTONS_TASK`/`menu_task` отдельно от
|
||||
`REFRESH_TASK`/`tft_refresh_task`.
|
||||
`REFRESH_TASK`/`tft_refresh_task`. Полная картина задач/приоритетов/взаимодействия —
|
||||
[TASKS.md](TASKS.md).
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
|
|
|||
|
|
@ -1,25 +1,28 @@
|
|||
# tft-app — документация домена
|
||||
|
||||
Dev-документация по **реализованному** доменному слою `firmware/tft_app` (Фазы 1–2): как данные
|
||||
СУЛ проходят от протокола до экрана. Технические доки «как это работает в коде», в дополнение к
|
||||
проектным ARCH/PLAN.
|
||||
Dev-документация по **реализованному** слою `firmware/tft_app` (Фазы 1–3.2): как данные
|
||||
СУЛ проходят от протокола до экрана, и как устроено приложение поверх FreeRTOS. Технические
|
||||
доки «как это работает в коде», в дополнение к проектным 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) | Свёртка ортогональных сигналов в один экранный режим. Таблица приоритетов как данные, резолвер, как менять/кастомизировать. |
|
||||
| [FALLBACK.md](FALLBACK.md) | Safe-mode рендер: этаж/стрелка/метка режима, по-символьный fallback шрифта, триггеры перерисовки, ограничение single-buffer. |
|
||||
| [MENU.md](MENU.md) | Движок меню (Фаза 3.2): дерево-данные, редакторы по типу, чистая модель, связь с `settings_store` (offset-привязка, `proto_slice`, поток сохранения). |
|
||||
| [FALLBACK.md](FALLBACK.md) | Safe-mode рендер: этаж/стрелка/метка режима, по-символьный fallback шрифта, триггеры перерисовки, путь кадра через компоновщик. |
|
||||
| [MENU.md](MENU.md) | Движок меню (Фаза 3.2): дерево-данные, редакторы по типу, чистая модель, связь с `settings_store`, оконный рендер, раскладка кнопок. |
|
||||
| [TASKS.md](TASKS.md) | Задачи FreeRTOS: состав/приоритеты (и почему такие), старт системы, MPSC-взаимодействие, мягкая пауза, разделяемое состояние. |
|
||||
|
||||
## Связанные документы
|
||||
|
||||
- [ARCH.md](../../firmware/tft_app/ARCH.md) — архитектура и проектное обоснование (источник истины по дизайну).
|
||||
- [PLAN.md](../../firmware/tft_app/PLAN.md) — статус фаз разработки (источник истины по статусу).
|
||||
- [DEV_ARCH.md](../DEV_ARCH.md) — устройство репозитория и сборки.
|
||||
- [bsp/display/README.md](../../bsp/display/README.md) — драйвер ELCDIF (два формата пикселя: XRGB8888/RGB565).
|
||||
|
||||
## Границы
|
||||
|
||||
Описаны домен, fallback-презентация, ядро настроек (`settings_store`) и модель меню.
|
||||
В процессе (Фаза 3): рендер меню + wiring кнопок (3.2.2/3.2.3), per-protocol дескрипторы (3.3),
|
||||
opto-входы (3.4), удалённая адресация НКУ-CAN (3.5). Ещё не реализовано (см. PLAN.md): ассеты и
|
||||
layout-движок со спрайтами (Фазы 4–5), аудио (Фаза 6), мультипротокол (Фаза 8).
|
||||
Описаны домен, fallback-презентация, ядро настроек (`settings_store`), меню (модель + оконный
|
||||
рендер + задачи) и фундамент рендера (double-buffer + PXP + гибрид bpp, Фаза 3.2.4). Остаток
|
||||
Фазы 3: per-protocol дескрипторы (3.3), opto-входы (3.4), удалённая адресация НКУ-CAN (3.5),
|
||||
тумблер логов (3.6). Ещё не реализовано (см. PLAN.md): ассеты и layout-движок со спрайтами
|
||||
(Фазы 4–5), аудио (Фаза 6), мультипротокол (Фаза 8).
|
||||
|
|
|
|||
137
docs/tft_app/TASKS.md
Normal file
137
docs/tft_app/TASKS.md
Normal 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 мс расчётно).
|
||||
155
firmware/tft_app/HANDOFF_PHASE3_TAIL.md
Normal file
155
firmware/tft_app/HANDOFF_PHASE3_TAIL.md
Normal 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 реально закрыт (не раньше).
|
||||
|
||||
Все пять пунктов явно помечены пользователем как «согласуем/обсудим совместно» — начинать
|
||||
реализацию любого из них без обсуждения конкретной развилки (см. «Открытые вопросы» в каждом
|
||||
пункте) не нужно.
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
| 0 | Каркас проекта и сборка | ✅ |
|
||||
| 1 | Walking skeleton (CAN → модель → этаж + стрелки) | ✅ |
|
||||
| 2 | Домен: полный `sul_result_t`, приоритеты, таймаут | 🟨 |
|
||||
| 3 | Настройки и меню, локальные входы (opto) | ⬜ |
|
||||
| 3 | Настройки и меню, локальные входы (opto) | 🟨 |
|
||||
| 4 | Ассеты: TLV-бандл, паковщик, XIP-блит | ⬜ |
|
||||
| 5 | Layout-движок: схема, солвер, темы, injected-регион | ⬜ |
|
||||
| 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.2 движок меню + рендер** ✅ (рендер — на одном буфере, см. 3.2.4) | **Чистая модель** (дерево-данные, навигация, edit, offset-привязка) — оперирует переданным `settings_t*`, **сама не сохраняет** (выставляет флаг save, app зовёт `settings_store_save()`) → host-тест без QSPI. **Рендер** (примитивы `gfx`: список/курсор/значение) в **переносимом окне 480×272 @ логич.(0,0)** (одинаково на всех панелях; на больших — левый-верхний угол, остальное чёрное). Вход — **долгое нажатие BUTTON_2** (~1.5–2 с); 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.4 opto-входы** | IN1/IN2 → вызов/ответ диспетчера → презентация (в fallback — примитив/текст; иконки-спрайты — Фаза 4/5). |
|
||||
| **3.5 удалённая адресация NKU-CAN** | `0x4X1`/`0x5XB` → запись `nku_address` в настройки (долг Фазы 2). |
|
||||
| **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) зелёные; ожидает
|
||||
> подтверждения на стенде.** По прецеденту Фаз 1/2 ✅ ставится только после
|
||||
|
|
@ -453,15 +453,66 @@ marks_both_pending`, аппаратно на реальном обрыве св
|
|||
> `render_task` не получал CPU для самого первого рендера, пока `sul_rx_task`
|
||||
> непрерывно спинила. Должно закрыться тем же фиксом приоритетов.
|
||||
>
|
||||
> _Осталось для ✅ (третий цикл проверки):_ прошить подписанный
|
||||
> `build/Debug/signed/app_slot_a.bin` и на TFT8: (а) старт без связи со
|
||||
> станцией — прочерки видны сразу, без захода в меню; (б) обрыв связи
|
||||
> мид-сессии → «--» сразу, без залипания; восстановление — сразу; (в) вход и
|
||||
> выход из меню — короткими BUTTON_1/действием, срабатывают стабильно, в т.ч.
|
||||
> без связи со станцией. Плюс субъективно оценить навигацию в меню (66 мс/шаг
|
||||
> — приемлемо или нужно отдельно разбираться со снижением стоимости
|
||||
> `gfx_present()` для модального окна меню) — открытый вопрос, решить по
|
||||
> итогам этой проверки.
|
||||
> _Итог третьего цикла (проверено на стенде + реальной станции, адрес 1):_
|
||||
> (а)-(в) подтверждены; данные на реальной СУЛ отображаются с приемлемой
|
||||
> задержкой (после фикса HW-фильтров транспорта, см. Фазу 2). Остались 66 мс
|
||||
> `gfx_present()` → следующий блок.
|
||||
>
|
||||
> ---
|
||||
>
|
||||
> **Ускорение рендера (2026-07-22, продолжение): гибрид bpp + оконный композит.**
|
||||
>
|
||||
> Диагностика 66 мс (AN12437 + расчёт): полоса SDRAM 16-бит @ ~136 МГц ≈ 272 МБ/с;
|
||||
> непрерывный сканаут ELCDIF 800×600×4Б×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 — ✅.**
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* @file task_render.c
|
||||
* @brief Презентация: единственный владелец дисплея и вызывающий gfx_present().
|
||||
* @brief Презентация: единственный владелец дисплея и вызывающий gfx_present*().
|
||||
*
|
||||
* Event-driven (xTaskNotifyGive от sul_rx_task И menu_task — MPSC, будят оба
|
||||
* продюсера, ulTaskNotifyTake(pdTRUE,...) схлопывает несколько notify в одно
|
||||
|
|
@ -8,10 +8,12 @@
|
|||
* менялось). НЕ содержит кнопочной логики — разделено от menu_task на Фазе
|
||||
* 3.2.4 (см. task_menu.c про причину).
|
||||
*
|
||||
* На каждое пробуждение: меню открыто → рисует меню; иначе — если меню ТОЛЬКО
|
||||
* ЧТО закрылось, сразу восстанавливает последнее известное состояние индикации
|
||||
* (не дожидаясь свежего сообщения — sul_rx_task мог простаивать под
|
||||
* g_menu_active), и дренирует g_render_queue, если там свежий diff.
|
||||
* Меню — оконный рендер (Фаза 3.2.4, ускорение навигации): на ОТКРЫТИИ — полная
|
||||
* очистка AS (стереть индикацию) + два полных gfx_present() подряд (double
|
||||
* buffering: оба FB обязаны получить корректный кадр вне окна — контракт
|
||||
* gfx_present_rect); дальше НАВИГАЦИЯ — перерисовка и композит только окна
|
||||
* 480×272 (~27% кадра, пропорционально дешевле). Индикация — полные кадры,
|
||||
* как и была.
|
||||
*/
|
||||
|
||||
#include "app_tasks.h"
|
||||
|
|
@ -48,15 +50,30 @@ void render_task(void *p_arg)
|
|||
(void) ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||
|
||||
const bool MENU_OPEN_NOW = menu_is_open(&g_menu);
|
||||
bool present_needed = false;
|
||||
|
||||
if (MENU_OPEN_NOW)
|
||||
{
|
||||
if (!was_menu_open)
|
||||
{
|
||||
/* Открытие: стереть индикацию из ВСЕГО AS и прогнать полный
|
||||
* кадр в ОБА FB — после этого вне окна оба буфера корректны
|
||||
* (чёрные), и навигация может обновлять только окно. */
|
||||
gfx_clear();
|
||||
menu_view_render(&g_menu);
|
||||
present_needed = true;
|
||||
gfx_present();
|
||||
gfx_present(); /* тот же AS — во второй FB (double buffering) */
|
||||
}
|
||||
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)
|
||||
{
|
||||
/* Меню только что закрылось — восстановить индикацию
|
||||
|
|
@ -74,11 +91,11 @@ void render_task(void *p_arg)
|
|||
ui_fallback_render(&msg.task, &msg.result);
|
||||
present_needed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (present_needed)
|
||||
{
|
||||
gfx_present(); /* PXP-композит AS+PS → задний FB + свап (tear-free) */
|
||||
gfx_present(); /* индикация — всегда полный кадр */
|
||||
}
|
||||
}
|
||||
|
||||
was_menu_open = MENU_OPEN_NOW;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/* ID известного пакета совпал, но DLC не тот — малформированный кадр. */
|
||||
if ((ID == (PACKET1_BASE | G4)) || (ID == (PACKET2_BASE | G4)) ||
|
||||
(ID == (PACKET3_BASE | G4)) || (ID == (PACKET4_BASE | G4)) ||
|
||||
(ID == (PACKET5_BASE | G6)))
|
||||
if ((ID == (PACKET1_BASE | G4)) || (ID == (PACKET2_BASE | G4)) || (ID == (PACKET3_BASE | G4)) ||
|
||||
(ID == (PACKET4_BASE | G4)) || (ID == (PACKET5_BASE | G6)))
|
||||
{
|
||||
if (p_frame->len != PROTO_DLC)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -24,8 +24,6 @@ set_source_files_properties(
|
|||
COMPILE_OPTIONS "-include;${CMAKE_CURRENT_SOURCE_DIR}/fonts/include/fonts.h;-w")
|
||||
|
||||
# sdk_common — fsl_common.h (AT_NONCACHEABLE_SECTION_ALIGN); sdk_pxp — PXP-компоновщик;
|
||||
# freertos_kernel — семафор FRAME_DONE в gfx_present/ISR-колбэке (Фаза 3.2.4);
|
||||
# port_log_uart — ВРЕМЕННО, тайминг-диагностика в gfx_present() (см. её тело) —
|
||||
# убрать вместе с диагностикой, когда причина лага индикации найдена.
|
||||
# freertos_kernel — семафор FRAME_DONE в gfx_present/ISR-колбэке (Фаза 3.2.4).
|
||||
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
|
|
@ -395,6 +395,9 @@
|
|||
<char code="042f" character="Я">
|
||||
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAuklEQVQYlb2OIQrDMBhGP5ILlEJqCoWa2qqKuqhAXI9Q3QPlCNE9QlRsq3OJmIoQAv/MBuumt2c/Hu8DfofWWp/neZZSCj3puq5jVVVV+77vzjk3DMMwjuMIAJxzDimlJCKq67oGgKZpGiKivu97dhzHUUop67qunHP+1dy2bUsppeu6rhhjfJkAAMYYCyEEY4xRSqnbuCzLknPObdu2700AgPfeW2vt5yHM8zwTEU3TNAGAEELczD/zABNrZ21n8NN3AAAAAElFTkSuQmCC</picture>
|
||||
</char>
|
||||
<char code="0020" character=" ">
|
||||
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAADklEQVQYlWNgGAXDCQAAAdAAAYH02hIAAAAASUVORK5CYII=</picture>
|
||||
</char>
|
||||
<char code="0430" character="а">
|
||||
<picture format="png">iVBORw0KGgoAAAANSUhEUgAAAAcAAAAQCAYAAADagWXwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAu0lEQVQYlc2QIQrEMBBFJ8NSqKgKFCLqajKmIvcoOU18XA4RV5UL5AC1tT1AbhBTURGYFcsu7AEW9tn/xOMD/CPGGJNzztd1Xfu+7+u6rp/xOI4jxhiJiEIIoZRSEBHFW5imaVJKKSmlzDlnrbV+AACklJK11p7neQohBAAAIiIQETEzL8uyAACM4zgyMxMRYa21ttbaPM/zMAyDc8591Xrvfa213vd9b9u2MTNrrfVH6Lqu6/u+/+lnL55FK0k0AK3doAAAAABJRU5ErkJggg==</picture>
|
||||
</char>
|
||||
|
|
|
|||
|
|
@ -106,6 +106,25 @@ void gfx_clear(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 Нарисовать строку заданным цветом (тинтинг с альфа-сглаживанием).
|
||||
*
|
||||
|
|
|
|||
|
|
@ -3,15 +3,11 @@
|
|||
#include "FreeRTOS.h"
|
||||
#include "fsl_common.h" /* AT_NONCACHEABLE_SECTION_ALIGN */
|
||||
#include "fsl_pxp.h"
|
||||
#include "log/log.h" /* ВРЕМЕННО (Фаза 3.2.4 HW-расследование лага) — см. gfx_present() */
|
||||
#include "semphr.h"
|
||||
#include "task.h" /* xTaskGetTickCount — тайминг-инструментация ниже */
|
||||
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
#define LOG_TAG "gfx"
|
||||
|
||||
/* ── Поверхности компоновщика (SDRAM, non-cacheable) ────────────────────────
|
||||
*
|
||||
* AT_NONCACHEABLE_SECTION_ALIGN размещает переменную в линкер-секции
|
||||
|
|
@ -28,19 +24,26 @@
|
|||
* FB[2] (выход PXP = вход ELCDIF, double buffer). Размер — под ТЕКУЩУЮ панель
|
||||
* стенда (TFT8, 800×600), не под BSP_DISPLAY_MAX_*: когда app-big (ARCH §9)
|
||||
* станет рантайм-выбирать между 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 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 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);
|
||||
/* PS — фон под AS (сейчас сплошной чёрный, заливается однократно в gfx_init). */
|
||||
AT_NONCACHEABLE_SECTION_ALIGN(static uint32_t s_processing_buffer[SURFACE_PIXELS], FRAMEBUFFER_ALIGN);
|
||||
/* Выходные буферы PXP = сканируемые ELCDIF (double buffer, свап в gfx_present). */
|
||||
AT_NONCACHEABLE_SECTION_ALIGN(static uint32_t s_framebuffer[2][SURFACE_PIXELS], FRAMEBUFFER_ALIGN);
|
||||
/* PS — фон под AS (сейчас сплошной чёрный, заливается однократно в gfx_init). RGB565. */
|
||||
AT_NONCACHEABLE_SECTION_ALIGN(static uint16_t s_processing_buffer[SURFACE_PIXELS], FRAMEBUFFER_ALIGN);
|
||||
/* Выходные буферы PXP = сканируемые ELCDIF (double buffer, свап в gfx_present). RGB565. */
|
||||
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_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) ── */
|
||||
|
||||
/* PS-формат: для RT1052 (расширенная таблица форматов, FSL_FEATURE_PXP_HAS_NO_
|
||||
* EXTEND_PIXEL_FORMAT не определён) 32-битный формат без реального альфа-канала
|
||||
* называется kPXP_PsPixelFormatARGB8888 (0x4) — фон, альфа PS в блендинге не
|
||||
* участвует (значима альфа AS, kPXP_AlphaEmbedded). */
|
||||
/**
|
||||
* @brief Настроить поверхности PXP под композит прямоугольника (x,y,w,h).
|
||||
*
|
||||
* Адреса 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)
|
||||
{
|
||||
PXP_Init(PXP);
|
||||
|
||||
const uint16_t PITCH = (uint16_t) (s_fb_width * BYTES_PER_PIXEL);
|
||||
|
||||
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 берётся из его альфа-байта. */
|
||||
/* Embedded alpha: доля AS-пикселя над PS берётся из его альфа-байта.
|
||||
* Одноразовая часть конфигурации (per-композит — pxp_configure_rect). */
|
||||
const pxp_as_blend_config_t blend_cfg = {
|
||||
.alpha = 0xFFU,
|
||||
.invertAlpha = false,
|
||||
|
|
@ -362,19 +397,9 @@ static void gfx_pxp_init(void)
|
|||
};
|
||||
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_SetProcessSurfacePosition(PXP, 0U, 0U, s_fb_width, s_fb_height);
|
||||
PXP_SetAlphaSurfacePosition(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 и дождаться завершения композиции (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_framebuffer, 0, sizeof(s_framebuffer));
|
||||
|
||||
/* Семафор создан и колбэк готов ДО включения IRQ внутри bsp_display_init. */
|
||||
const bsp_status_t st = bsp_display_init(type, (uint32_t) s_framebuffer[0], on_frame_done);
|
||||
/* Семафор создан и колбэк готов ДО включения IRQ внутри bsp_display_init_ex.
|
||||
* 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)
|
||||
{
|
||||
return st;
|
||||
|
|
@ -436,38 +463,75 @@ bsp_status_t gfx_init(bsp_display_type_t type)
|
|||
|
||||
void gfx_clear(void)
|
||||
{
|
||||
/* Прозрачно (alpha 0) → непрорисованные области покажут фон PS (чёрный). */
|
||||
(void) memset(s_alpha_buffer, 0, (size_t) s_fb_width * s_fb_height * BYTES_PER_PIXEL);
|
||||
/* Прозрачно (alpha 0) → непрорисованные области покажут фон PS (чёрный).
|
||||
* Чистим 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_output_cfg.buffer0Addr = (uint32_t) s_framebuffer[s_back_index];
|
||||
PXP_SetOutputBufferConfig(PXP, &s_output_cfg);
|
||||
const uint32_t OUT_ADDR =
|
||||
(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 —
|
||||
* 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();
|
||||
gfx_pxp_run(); /* AS над PS → прямоугольник заднего FB */
|
||||
|
||||
/* Синхронизация с развёрткой: дождаться конца кадра, затем отдать ELCDIF
|
||||
* новый буфер — он переключится аппаратно на границе кадра (tear-free). */
|
||||
(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]);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@ extern "C"
|
|||
{
|
||||
#endif
|
||||
|
||||
/* Габариты окна меню @ логич.(0,0) — публичны: владелец дисплея (task_render)
|
||||
* использует их для оконного present'а (gfx_present_rect) при навигации. */
|
||||
#define MENU_VIEW_WIN_W 480U
|
||||
#define MENU_VIEW_WIN_H 272U
|
||||
|
||||
/**
|
||||
* @brief Отрисовать полный кадр меню в AS (очистка + заголовок + строки + футер).
|
||||
*
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@
|
|||
|
||||
#include <stdio.h>
|
||||
|
||||
/* Окно 480×272 @ (0,0). Метрики под реальные шрифты: JBMono24 h=31 (заголовок/
|
||||
* строки), JBMono12 h=16 (футер). 36 + 6×36 + 20 = 272. */
|
||||
#define WIN_W 480U
|
||||
#define WIN_H 272U
|
||||
/* Окно @ (0,0) — габариты публичны (menu_view.h: MENU_VIEW_WIN_W/H, нужны
|
||||
* task_render для оконного present'а). Метрики под реальные шрифты: JBMono24
|
||||
* h=31 (заголовок/строки), JBMono12 h=16 (футер). 36 + 6×36 + 20 = 272. */
|
||||
#define WIN_W MENU_VIEW_WIN_W
|
||||
#define WIN_H MENU_VIEW_WIN_H
|
||||
#define TITLE_H 36U
|
||||
#define ROW_H 36U
|
||||
#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)
|
||||
{
|
||||
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);
|
||||
|
||||
(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);
|
||||
|
||||
(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);
|
||||
|
||||
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)
|
||||
{
|
||||
/* Полный кадр off-screen: обнулить AS (вне окна 480×272 → чёрный фон PS),
|
||||
* затем нарисовать окно. Свап — gfx_present() у владельца дисплея. */
|
||||
gfx_clear();
|
||||
/* Обнулить и нарисовать ТОЛЬКО окно (дёшево — оконная перерисовка на
|
||||
* навигации). Полную очистку AS от индикации вне окна делает владелец
|
||||
* дисплея (task_render) один раз на ОТКРЫТИИ меню — вместе с двумя
|
||||
* полными present'ами (контракт gfx_present_rect). */
|
||||
gfx_clear_rect(0U, 0U, WIN_W, WIN_H);
|
||||
|
||||
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 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);
|
||||
|
||||
uint8_t first;
|
||||
|
|
@ -138,6 +145,5 @@ void menu_view_render(const menu_ctx_t *p_ctx)
|
|||
|
||||
draw_footer(p_ctx, first, last);
|
||||
|
||||
/* Рамка — последней, поверх содержимого. */
|
||||
gfx_draw_rect(0U, 0U, WIN_W, WIN_H, COL_SEP);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue