# tft_app: Phase 3.2 - menu & indicator
This commit is contained in:
parent
b9b46154cc
commit
2662111e1b
22 changed files with 1355 additions and 431 deletions
|
|
@ -46,21 +46,24 @@ MEMORY
|
|||
m_qacode (RX) : ORIGIN = 0x00000000, LENGTH = 0x00020000 /* SRAM_ITC 128 КБ — ramfunc */
|
||||
m_data (RW) : ORIGIN = 0x20000000, LENGTH = 0x00020000 /* SRAM_DTC 128 КБ — .data/.bss/heap/stack */
|
||||
m_data2 (RW) : ORIGIN = 0x20200000, LENGTH = 0x00040000 /* SRAM_OC 256 КБ */
|
||||
/* SDRAM 32 МБ, разбита на 2: некэшируемый framebuffer (ELCDIF DMA читает
|
||||
* его напрямую — MPU Region 9 в board_mpu_init() параметризован ЭТИМИ
|
||||
* __NCACHE_REGION_* символами, C-код не трогаем) + остаток под кэшируемые
|
||||
* буферы будущих фаз (ассеты, аудио). Framebuffer TFT8 800×600×4 ≈ 1.83 МБ
|
||||
* — 2 МБ (степень двойки, MPU-регион обязан быть степенью двойки и
|
||||
* выровнен по своему размеру) с запасом. См. bsp/sdram/README.md про NXP-
|
||||
* рекомендацию некэшируемых DMA-буферов в SDRAM. */
|
||||
m_sdram_ncache (RW) : ORIGIN = 0x80000000, LENGTH = 0x00200000 /* 2 МБ — framebuffer (non-cacheable) */
|
||||
m_sdram (RW) : ORIGIN = 0x80200000, LENGTH = 0x01E00000 /* ~30 МБ — кэшируемый остаток, Фаза 1 не использует */
|
||||
/* SDRAM 32 МБ, разбита на 2: некэшируемые DMA-буферы компоновщика (ELCDIF
|
||||
* читает framebuffer, PXP — AS/PS напрямую; MPU Region 9 в board_mpu_init()
|
||||
* параметризован ЭТИМИ __NCACHE_REGION_* символами, C-код не трогаем) +
|
||||
* остаток под кэшируемые буферы будущих фаз (ассеты, аудио).
|
||||
*
|
||||
* Фаза 3.2.4 (double-buffer + PXP): 4 буфера TFT8 800×600×4 ≈ 1.92 МБ каждый
|
||||
* — AS (alpha_buffer) + PS (processing_buffer) + framebuffer[2] = 7.68 МБ
|
||||
* → 8 МБ (степень двойки; MPU-регион обязан быть степенью двойки и выровнен
|
||||
* по своему размеру — 8 МБ @ 0x80000000 удовлетворяет). См. bsp/sdram/
|
||||
* README.md про NXP-рекомендацию некэшируемых DMA-буферов в SDRAM. */
|
||||
m_sdram_ncache (RW) : ORIGIN = 0x80000000, LENGTH = 0x00800000 /* 8 МБ — AS+PS+2×FB (non-cacheable) */
|
||||
m_sdram (RW) : ORIGIN = 0x80800000, LENGTH = 0x01800000 /* 24 МБ — кэшируемый остаток, пока не используется */
|
||||
}
|
||||
|
||||
SECTIONS
|
||||
{
|
||||
__NCACHE_REGION_START = ORIGIN(m_sdram_ncache);
|
||||
__NCACHE_REGION_SIZE = LENGTH(m_sdram_ncache); /* 2 МБ non-cacheable — framebuffer */
|
||||
__NCACHE_REGION_SIZE = LENGTH(m_sdram_ncache); /* 8 МБ non-cacheable — AS+PS+2×FB */
|
||||
|
||||
/* Vector table (imgtool header зарезервирован до неё в MEMORY). */
|
||||
.interrupts :
|
||||
|
|
@ -208,7 +211,7 @@ SECTIONS
|
|||
ASSERT(__DATA_END <= text_end, "region m_text overflowed with text and data")
|
||||
ASSERT(text_end <= SLOT_BASE + SLOT_SIZE, "app вышел за пределы слота (2 МБ)")
|
||||
ASSERT((__noncachedata_end__ - ORIGIN(m_sdram_ncache)) <= LENGTH(m_sdram_ncache),
|
||||
"m_sdram_ncache overflow — framebuffer(ы) не влезли в 2 МБ")
|
||||
"m_sdram_ncache overflow — AS+PS+2×FB не влезли в 8 МБ")
|
||||
|
||||
.bss :
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
# tft-app — движок меню и связь с настройками
|
||||
|
||||
Документ описывает **реализованный** движок меню (Фаза 3.2.1: чистая модель) и его
|
||||
связь с модулем настроек `settings_store`. Проектная основа — [ARCH.md §8](../../firmware/tft_app/ARCH.md);
|
||||
реализация — [menu.c](../../firmware/tft_app/src/menu/src/menu.c),
|
||||
Документ описывает **реализованный** движок меню (Фаза 3.2.1: чистая модель; 3.2.2–3.2.4: рендер,
|
||||
wiring, разделение на задачи) и его связь с модулем настроек `settings_store`. Проектная основа —
|
||||
[ARCH.md §8](../../firmware/tft_app/ARCH.md); реализация —
|
||||
[menu.c](../../firmware/tft_app/src/menu/src/menu.c),
|
||||
[settings_store](../../firmware/tft_app/src/services/settings_store/).
|
||||
|
||||
Движущее требование (§8): клиент приносит уникальные настройки, и добавление их **не должно
|
||||
|
|
@ -17,7 +18,7 @@
|
|||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
BTN["bsp_button<br/>долгое BUTTON_2 / next / action"]
|
||||
BTN["bsp_button<br/>короткое BUTTON_1 (вход/next) / BUTTON_2 (action)"]
|
||||
subgraph MODEL["menu (чистая модель, host-тест)"]
|
||||
TREE["дерево-данные<br/>menu_item_desc_t[]"]
|
||||
NAV["навигация + редактор<br/>menu_next / menu_action"]
|
||||
|
|
@ -68,8 +69,10 @@ typedef struct {
|
|||
|
||||
## 3. Навигация
|
||||
|
||||
Две кнопки: BUTTON_1 → `menu_next` (следующий пункт уровня, с заворотом); короткое BUTTON_2 →
|
||||
`menu_action` (по типу пункта). Вход в меню — долгое BUTTON_2 (app-слой).
|
||||
Две кнопки, обе коротким нажатием (без удержания — раскладка `OLD_PROJECT_TFT8_UKL`): BUTTON_1 →
|
||||
`menu_next` (следующий пункт уровня, с заворотом) когда меню открыто, **вход в меню** (app-слой,
|
||||
`menu_open`) когда закрыто; BUTTON_2 → `menu_action` (по типу пункта) когда открыто, намеренный
|
||||
no-op когда закрыто.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
|
|
@ -101,32 +104,100 @@ flowchart TD
|
|||
на дескриптор протокола `sul_settings_desc_t` (§8) — меню строит раздел «Настройки протокола» из
|
||||
дескриптора активного протокола, не хардкодом.
|
||||
|
||||
**Поток сохранения** (app-слой связывает модель и flash):
|
||||
**Поток сохранения** (`menu_task` связывает модель и flash — см. §5):
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as Пользователь (кнопки)
|
||||
participant M as menu (модель)
|
||||
participant S as settings_t (RAM)
|
||||
participant A as app
|
||||
participant MT as menu_task
|
||||
participant St as settings_store
|
||||
|
||||
U->>M: menu_action (правка / выход)
|
||||
M->>S: запись поля по offset (dirty=true)
|
||||
M-->>A: open=false, save_requested=true
|
||||
A->>St: settings_store_save()
|
||||
M-->>MT: open=false, save_requested=true
|
||||
MT->>St: settings_store_save()
|
||||
St->>St: serialize + CRC → QSPI 0x450000
|
||||
A->>A: пере-применить настройки (напр. адрес → декодер)
|
||||
```
|
||||
|
||||
После сохранения app пере-применяет изменившиеся настройки к рантайму (например, новый адрес —
|
||||
в `nku_can_set_address()`), т.к. декодер держит свою копию адреса.
|
||||
Адрес станции (`nku_can_set_address()`) `sul_rx_task` пере-применяет из настроек сам, на следующей
|
||||
итерации — не требует отдельного сигнала (декодер читает `settings_store_get()` каждую итерацию).
|
||||
|
||||
---
|
||||
|
||||
## 5. Рендер (Фаза 3.2.2)
|
||||
## 5. Рендер и wiring (Фаза 3.2.2–3.2.4)
|
||||
|
||||
Рендер — отдельный слой, читает модель запросами (`menu_current`, `menu_level_range`,
|
||||
`menu_read_value`) и рисует в **фиксированном окне 480×272 в логических (0,0)** — одинаково на всех
|
||||
панелях (на больших — левый-верхний угол, остальное чёрное). Меню модально: пока `menu_is_open()`,
|
||||
индикация под ним не рисуется. Детали — [PLAN.md, Фаза 3.2](../../firmware/tft_app/PLAN.md).
|
||||
Рендер ([menu_view.c](../../firmware/tft_app/src/ui/menu/src/menu_view.c)) — отдельный слой, читает
|
||||
модель запросами (`menu_current`, `menu_level_range`, `menu_read_value`) и рисует в **фиксированном
|
||||
окне 480×272 @ логич.(0,0)** — одинаково на всех панелях (на больших — левый-верхний угол, остальное
|
||||
чёрное). Логики навигации не содержит.
|
||||
|
||||
**Раскладка** (под реальные шрифты: `SystemFont`/JBMono24 h=31, `SystemFontSmall`/JBMono12 h=16):
|
||||
заголовок 36 + 6 строк × 36 + футер 20 = **272**, обрамление — тонкая серая рамка 1 px по периметру.
|
||||
Заголовок = подпись текущего уровня; строки «подпись слева / значение справа»; **курсор — сплошная
|
||||
полоса-заливка** (`gfx_fill_rect`) + белый текст; футер — легенда кнопок + «N/M».
|
||||
|
||||
**Цвет — тинтингом** (`gfx_draw_string(..., color)` с альфа-блендингом): один белый шрифт рисуется
|
||||
любым цветом со сглаживанием, и оно корректно ложится на полосу-курсор. Значения 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/`).
|
||||
|
||||
**Меню и рендер — РАЗНЫЕ задачи** ([task_menu.c](../../firmware/tft_app/src/app/task_menu.c) /
|
||||
[task_render.c](../../firmware/tft_app/src/app/task_render.c)). Найдено на HW-верификации Фазы
|
||||
3.2.4: в объединённой задаче (Фазы 3.2.1–3.2.3, один framebuffer, без ожиданий) блокировок не было,
|
||||
разделение было безвредным упущением — но `gfx_present()` (double-buffer + PXP) внёс блокирующее
|
||||
ожидание кадра, и в объединённой задаче это ожидание попутно блокировало вход в меню (ноль реакции
|
||||
на кнопки). Эталон разделения — `OLD_PROJECT_TFT8_UKL`: `BUTTONS_TASK`/`menu_task` отдельно от
|
||||
`REFRESH_TASK`/`tft_refresh_task`.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
BTN["bsp_button<br/>софт-таймер 5 мс<br/>(debounce, независимо от обеих задач)"]
|
||||
MT["menu_task<br/>модель + мгновенный вход/навигация + save<br/>НЕ рисует"]
|
||||
RX["sul_rx_task<br/>CAN → decode → controller<br/>WDOG безусловно"]
|
||||
RT["render_task<br/>ЕДИНСТВЕННЫЙ вызывающий gfx_present()<br/>event-driven"]
|
||||
|
||||
BTN --> MT
|
||||
MT -->|"xTaskNotifyGive<br/>(любое изменение)"| RT
|
||||
RX -->|"xQueueOverwrite (данные)<br/>+ xTaskNotifyGive (сигнал)"| RT
|
||||
MT -.g_menu_active.-> RX
|
||||
```
|
||||
|
||||
**Связь — MPSC.** Два продюсера (`sul_rx_task`, `menu_task`), один консюмер (`render_task`).
|
||||
Данные (какой этаж/диф) идут только по плечу `sul_rx_task`→`render_task` — однослотовая
|
||||
`xQueueOverwrite`-очередь (важно только последнее). Пробуждение — `xTaskNotifyGive()`/
|
||||
`ulTaskNotifyTake(pdTRUE, portMAX_DELAY)` от ОБОИХ продюсеров: `render_task` не поллит, спит между
|
||||
изменениями; несколько notify схлопываются в одно пробуждение (та же семантика «важно только
|
||||
последнее»). Приоритет «меню важнее индикации» не кодируется в уведомлении — `render_task`,
|
||||
проснувшись, всегда СНАЧАЛА проверяет `menu_is_open()`.
|
||||
|
||||
**Мягкая пауза `sul_rx_task` на время меню.** Пока меню открыто, `menu_task` держит
|
||||
`g_menu_active=true`; `sul_rx_task` под этим флагом пропускает decode/controller/запись в очередь —
|
||||
но WDOG/heartbeat кормятся БЕЗУСЛОВНО (вне флага), задача не suspend'ится. При выходе из меню
|
||||
`render_task` (по признаку «меню только что закрылось») сразу перерисовывает последнее известное
|
||||
состояние индикации, не дожидаясь свежего CAN-кадра.
|
||||
|
||||
**Ввод.** Опрос кнопок — **софт-таймер** (`input_poll_cb`, 5 мс; демон таймеров на высшем приоритете
|
||||
в системе → нажатия не теряются, пока заняты остальные задачи; в Фазе 3.4 туда же
|
||||
`bsp_opto_process()`). Раскладка — как в `OLD_PROJECT_TFT8_UKL`, оба нажатия короткие, без
|
||||
удержания: BUTTON_1 = вход в меню (закрыто) / следующий пункт (открыто); BUTTON_2 = выбор/инкремент
|
||||
(открыто), намеренный no-op (закрыто).
|
||||
|
||||
**Приоритеты задач** (`app_tasks.h` — единая точка правды, `tskIDLE_PRIORITY`-относительно):
|
||||
`bringup_task` (одноразовая, самый высокий из четырёх) → `menu_task` → `render_task` → `sul_rx_task`
|
||||
(самый низкий). **Важно:** `render_task` НАМЕРЕННО выше `sul_rx_task`, не наоборот — `bsp_can_receive()`
|
||||
busy-spin без yield (`bsp/can/src/can.c`) занимает CPU весь `CAN_RX_TIMEOUT_MS` (100 мс) при
|
||||
отсутствии трафика, и `xTaskDelayUntil()` в этом случае не блокирует вовсе (дедлайн уже в прошлом —
|
||||
см. `sdk/rtos/freertos/freertos-kernel/tasks.c`), т.е. `sul_rx_task` не отдаёт CPU добровольно.
|
||||
Если `sul_rx_task` окажется выше `render_task`, последняя будет голодать всё время отсутствия
|
||||
CAN-трафика (найдено на HW-верификации — экран не обновлялся при старте без связи и при обрыве
|
||||
связи; см. PLAN.md, Фаза 3.2.4). `menu_task` по-прежнему выше `render_task` — её PXP busy-wait
|
||||
(~60-100 мс) не должен придерживать ввод. Логгер (`utils/log`) под FreeRTOS — с мьютексом
|
||||
(`port/log/src/log_mutex.c`, до Фазы 3.2.4 был `#if 0` и не собирался — гонка на общем static-буфере
|
||||
логгера между несколькими пишущими задачами).
|
||||
|
|
|
|||
|
|
@ -58,7 +58,11 @@ include(${CMAKE_SOURCE_DIR}/firmware/bootloader/mcuboot_port/bootutil_sources.cm
|
|||
|
||||
add_executable(
|
||||
app
|
||||
src/app/main.c
|
||||
src/app/main.c # main() + wiring + FreeRTOS-хуки
|
||||
src/app/task_bringup.c # одноразовый bring-up (QSPI/settings/self-confirm/SDRAM+gfx+CAN) → создаёт остальные задачи
|
||||
src/app/task_sul_rx.c # приём CAN → decode → controller (WDOG/heartbeat безусловно, CAN-работа под !g_menu_active)
|
||||
src/app/task_menu.c # модель меню: кнопки/hold-to-enter/nav/edit/save — НЕ рисует
|
||||
src/app/task_render.c # презентация: единственный вызывающий gfx_present(), event-driven
|
||||
${BSP_GENERATED}/clock_config.c # BOARD_BootClockRUN (зовётся board_hw_init)
|
||||
${BSP_STARTUP_FILE}
|
||||
${BSP_SYSCALLS_FILE}
|
||||
|
|
@ -70,6 +74,12 @@ add_executable(
|
|||
# ниже (BSP_TICK_FREERTOS_MODE), чтобы не тянуть SysTick_Handler,
|
||||
# конфликтующий с FreeRTOS-портом. Приходит транзитивно через bsp_uart_host.
|
||||
${CMAKE_SOURCE_DIR}/bsp/tick/src/tick.c
|
||||
# log_mutex.c — FreeRTOS strong-override весов log_mutex_{init,lock,unlock}
|
||||
# (weak NOP в utils/log/log.c). Компилируется ПРЯМО в app (не отдельной
|
||||
# либой) — как tick.c: FreeRTOS-специфика только этой цели, нужна во ВСЕХ
|
||||
# сборках tft_app (без неё — гонка на общем static-буфере логгера между
|
||||
# bringup_task/sul_rx_task/menu_task/render_task, см. PLAN.md).
|
||||
${CMAKE_SOURCE_DIR}/port/log/src/log_mutex.c
|
||||
${CMAKE_SOURCE_DIR}/firmware/bootloader/mcuboot_port/flash_map_backend.c
|
||||
${MCUBOOT_BOOTUTIL_DIR}/src/bootutil_public.c)
|
||||
|
||||
|
|
@ -106,6 +116,7 @@ target_link_libraries(
|
|||
app
|
||||
PRIVATE bsp_board
|
||||
bsp_led
|
||||
bsp_button
|
||||
bsp_wdog
|
||||
bsp_boot_state
|
||||
bsp_qspi_flash
|
||||
|
|
|
|||
|
|
@ -210,6 +210,27 @@ marks_both_pending`, аппаратно на реальном обрыве св
|
|||
> подстановка «-»); недостаёт выделенной чистой функции проверки покрытия и её теста —
|
||||
> требует небольшого выноса из `gfx.c` (зависит от `bsp_display`).
|
||||
>
|
||||
> 🐛 **Найден и починен на реальной станции (не симулятор, 2026-07-22) — транспорт,
|
||||
> не декодер.** `domain/sul/nku_can.c` (decode) корректно считает адресный сдвиг
|
||||
> `group4=addr<<4`/`group6=addr<<6` на каждый вызов — декодер никогда не был
|
||||
> виноват. Баг — в `domain/sul/transport/can/` (HW, host не тестируется, отдельный
|
||||
> слой): (1) HW RX-фильтры FlexCAN выставлялись ОДИН РАЗ при bring-up на
|
||||
> захардкоженные ID адреса 0 (0x506/0x508) — правка адреса в меню обновляла только
|
||||
> декодер, физически кадры с другим адресом отбрасывались CAN-контроллером до
|
||||
> всякого софта; воспроизводилось на реальной станции (адрес 1) — heartbeat живой,
|
||||
> экран пуст; на симуляторе (адрес 0) работало, поэтому не всплывало раньше. (2)
|
||||
> Фильтровались только PACKET1/PACKET3 — PACKET2/4/5 (перегруз, сейсмоопасность,
|
||||
> следующий этаж) не принимались НИКОГДА, вне зависимости от адреса. Т.е. критерий
|
||||
> выхода Фазы 2 «все режимы НКУ-CAN корректно доходят» ни разу не выполнялся для
|
||||
> этих трёх пакетов на железе. **Фикс** (по образцу `OLD_PROJECT/source/main_programm.c`
|
||||
> `apply_nku_can_filters()`/`msg_receiver_task` — та же bsp_can-прослойка, дословно
|
||||
> портирован паттерн): новая `sul_transport_can_set_address(addr)` переконфигурирует
|
||||
> все 5 MB-фильтров под текущий адрес (diff+сентинел `0xFFU`, как в референсе —
|
||||
> дёшево звать каждую итерацию); `sul_transport_can_init()` фильтры больше не
|
||||
> трогает. `task_sul_rx.c` зовёт её рядом с `nku_can_set_address()`. Домен
|
||||
> (`domain/sul.h`, `nku_can.h`/`.c`) не тронут — фикс целиком внутри NKU-CAN-
|
||||
> транспорта; для других протоколов (Фаза 8: УИМ/SD7/УЭЛ/УКЛ, в основном UART) своя
|
||||
> адресация — не переиспользуется автоматически.
|
||||
> Побочная находка: `SystemFont.c` (сгенерирован lcd-image-converter) экспортирует tFont
|
||||
> под именем `JBMono24` (из .xml), а `gfx.h` обещал `SystemFont` — рассинхрон был
|
||||
> латентным (Фаза 1 линковала только `FloorFontFallback`), вскрылся при первом
|
||||
|
|
@ -231,10 +252,44 @@ marks_both_pending`, аппаратно на реальном обрыве св
|
|||
|
||||
---
|
||||
|
||||
## Фаза 3 — Настройки и меню ⬜
|
||||
## Фаза 3 — Настройки и меню 🟨
|
||||
|
||||
**Цель.** Персистентные настройки на QSPI, меню на двух кнопках, локальные входы (opto).
|
||||
|
||||
> ## ⏸️ ТОЧКА ОСТАНОВКИ (переход в новый тред) — 2026-07-21
|
||||
>
|
||||
> **Сделано, на железе подтверждено:**
|
||||
> - **3.1 `services/settings_store`** ✅ — персист ядра (magic/version/CRC32 в `0x450000` через
|
||||
> `bsp_qspi_flash`), `proto_slice[0]`→`nku_address` в декодер. HW: load/save, адрес переживает
|
||||
> power-cycle. _Латентный баг починен:_ 4-КБ `settings_page_t` на ~3-КБ стеке задачи → stack
|
||||
> overflow → bootloader recovery; сделал буфер `static`, стеки задач x6→x8.
|
||||
> - **3.2.1 модель меню** ✅ — чистая (`src/menu/menu.c`: дерево-данные + навигация + edit +
|
||||
> offset-привязка, `save_requested`), host-тест `tests/host/tft_app_menu` (10).
|
||||
> - **3.2.2 рендер + gfx** ✅ — тинтинг `gfx_draw_string(...,color)` (альфа-блендинг: белый шрифт
|
||||
> любым цветом со сглаживанием, корректно на цветной полосе), `gfx_fill_rect`/`gfx_draw_rect`,
|
||||
> шрифт `JBMono12`→алиас `SystemFontSmall`; окно 480×272 @ (0,0); боевое дерево `menu_tree.c`
|
||||
> (Протокол/Адрес/Логи/Выход, метки `options[]`).
|
||||
> - **3.2.3 wiring** ✅ — **софт-таймер опроса ввода** (`input_poll_cb`, 5 мс; демон таймеров на
|
||||
> высшем приоритете → нажатия не теряются во время рендера; opto добавится сюда в 3.4);
|
||||
> `ui_task` — потребитель событий + владелец дисплея; вход долгим BUTTON_2; save на выходе;
|
||||
> адрес `sul_rx_task` пере-применяет из настроек. **Макет/шрифты/тинтинг на железе — как в
|
||||
> макете; адрес персистится.**
|
||||
>
|
||||
> **ПОЧЕМУ ОСТАНОВИЛИСЬ — один framebuffer не годится.** Единственный буфер, который ELCDIF
|
||||
> сканирует по DMA пока CPU в него пишет → **tearing / частичная отрисовка**; полный `gfx_clear`
|
||||
> 800×600 в некэшируемую SDRAM (~40 мс) на кадр → **тормоза**; вход нестабилен. Инкрементальная
|
||||
> перерисовка (только изменившиеся строки) — костыль, дала баг (пропал футер). **Костыль убрать.**
|
||||
>
|
||||
> **РЕШЕНИЕ (согласовано): перенести фундамент рендера (double-buffer + PXP) из Фазы 4 в конец
|
||||
> Фазы 3.** Эталон — `OLD_PROJECT_TFT8_UKL/source/display/{pxp_config.c,display.c}` +
|
||||
> `main_programm.c` (см. ниже «3.2.4»). Ассеты/спрайты/layout остаются в Фазе 4. Дальше — под-шаг
|
||||
> **3.2.4** (R1→R4), затем 3.3–3.6.
|
||||
>
|
||||
> _Флеш-грабли (в памяти):_ `host::flash-swd-app-slot-debug` шьёт **подписанный** образ
|
||||
> `build/Debug/signed/app_slot_a.bin` → после правок обязательно `build::sign-app-debug`, иначе
|
||||
> прошивается старый код. Вся работа Фаз 2–3 — в рабочем дереве (ветка `tft-app-dev`), не
|
||||
> закоммичена (пользователь коммитит сам).
|
||||
|
||||
> **Согласованный дизайн (решено с пользователем; референсы — все старые проекты:
|
||||
> settings_manager ×3, menu ×7 вкл. ALPACA, main_programm/menu_task).**
|
||||
>
|
||||
|
|
@ -273,13 +328,180 @@ 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 движок меню + рендер** | **Чистая модель** (дерево-данные, навигация, 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 мс. Модальный экран. |
|
||||
| **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.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 (🟨 код-комплит, ожидает железо)
|
||||
|
||||
> **Статус: R1→R4 реализованы, Debug/Release + host (21/21) зелёные; ожидает
|
||||
> подтверждения на стенде.** По прецеденту Фаз 1/2 ✅ ставится только после
|
||||
> проверки на железе (меню/индикация без tearing, вход в меню стабилен).
|
||||
>
|
||||
> Что сделано (решения — как согласовано):
|
||||
> - **R1.** `add_sdk_driver(pxp fsl_pxp.c)` (+`sdk_clock`), gfx линкует `sdk_pxp`;
|
||||
> `m_sdram_ncache` 2→8 МБ (`m_sdram` origin/len сдвинут). `fsl_memory.h` не нужен
|
||||
> — `FSL_FEATURE_MEMORY_HAS_ADDRESS_OFFSET` для RT1052 не определён (identity).
|
||||
> - **R2.** 4 поверхности в ncache (AS+PS+2×FB, 800×600×4 ≈ 1.92 МБ, итого 7500 КБ);
|
||||
> порт `init_pxp`/`pxp_start_operation` в `services/gfx`; примитивы пишут в AS
|
||||
> (alpha 0xFF), `gfx_clear()`→memset 0 (прозрачно); `gfx_present()` = PXP-композит
|
||||
> AS над PS → задний FB + свап по семафору FRAME_DONE (ISR-колбэк ELCDIF, priority 2
|
||||
> == max-syscall → `xSemaphoreGiveFromISR` допустим). PS-формат — `kPXP_PsPixel
|
||||
> FormatARGB8888` (расширенная таблица RT1052). Владение present — согласовано:
|
||||
> **явный `gfx_present()`, оркеструет `ui_task`** (menu_view/fallback только рисуют).
|
||||
> - **R3.** Костыль снят: `menu_view_render_row` + full/row-ветвление удалены,
|
||||
> `menu_view_render()` рисует полный кадр; `ui_task` зовёт `gfx_present()` на
|
||||
> изменение. Fallback уже полнокадровый.
|
||||
> - **R4 (первая версия).** `main.c` разбит: `app/task_sul_rx.c` + `app/task_ui.c` —
|
||||
> `ui_task` совмещал меню и рендер в одной задаче.
|
||||
>
|
||||
> **Первая HW-проверка (2026-07-22) вскрыла регресс от R1→R4:** меню не реагировало
|
||||
> на кнопки вообще (ноль реакции), индикация запаздывала на 1-2 с относительно
|
||||
> реального CAN-сигнала, при обрыве связи экран не сбрасывался на «--» (только
|
||||
> после восстановления). Root-cause — связка меню+рендер в одной задаче: до R1→R4
|
||||
> (3.2.1–3.2.3, один framebuffer) блокировок в этой задаче не было, разделение было
|
||||
> безвредным упущением; `gfx_present()` внёс блокирующее ожидание кадра
|
||||
> (PXP busy-wait + semaphore-wait на FRAME_DONE), и в объединённой задаче это
|
||||
> ожидание попутно блокировало проверку кнопок — `menu_task`-логика (hold-детект)
|
||||
> физически не успевала выполняться между вызовами `gfx_present()`. Побочно
|
||||
> вскрылась гонка: FreeRTOS-мьютекс логгера (`port/log/src/log_mutex.c`) был под
|
||||
> `#if 0` и не собирался ни в один таргет — `LOG_*` из разных задач писали в общий
|
||||
> `static` буфер (`utils/log/log.c`) без синхронизации.
|
||||
>
|
||||
> **R4 (исправлено) — 4 задачи вместо 2**, эталон разделения —
|
||||
> `OLD_PROJECT_TFT8_UKL` (`BUTTONS_TASK`/`menu_task` отдельно от
|
||||
> `REFRESH_TASK`/`tft_refresh_task`):
|
||||
> - `bringup_task` — одноразовая инициализация (лог-мьютекс → UART/лог → QSPI/
|
||||
> settings/self-confirm → SDRAM+gfx+CAN), создаёт три нижеследующие задачи,
|
||||
> удаляет себя (`vTaskDelete(NULL)`).
|
||||
> - `sul_rx_task` (`task_sul_rx.c`) — CAN→decode→controller. WDOG/heartbeat —
|
||||
> **безусловно**; сама CAN-работа — под `!g_menu_active` (**мягкая пауза**, не
|
||||
> `vTaskSuspend` — WDOG остаётся в безопасности по конструкции, не завязан на
|
||||
> suspend-состояние).
|
||||
> - `menu_task` (новый, `task_menu.c`) — модель меню, потребление кнопок,
|
||||
> hold-to-enter, save. НЕ рисует. Владеет `g_menu`, пишет `g_menu_active`.
|
||||
> - `render_task` (был `ui_task`, `task_render.c`) — единственный вызывающий
|
||||
> `gfx_present()`. **Event-driven**: `ulTaskNotifyTake(pdTRUE, portMAX_DELAY)`,
|
||||
> будят `sul_rx_task` и `menu_task` (MPSC) через `xTaskNotifyGive` — не поллит.
|
||||
> На «меню только что закрылось» — восстанавливает индикацию из последнего
|
||||
> известного состояния сразу, не дожидаясь свежего CAN-кадра.
|
||||
> - Приоритеты (`app_tasks.h`, единая точка правды, менялись — см. вторую HW-
|
||||
> проверку ниже): изначально `bringup` > `menu` > `sul_rx` > `render`. Демон
|
||||
> программных таймеров (`input_poll_cb`, debounce) — отдельно,
|
||||
> `configTIMER_TASK_PRIORITY` (наивысший в системе), не задет.
|
||||
> - `port/log/src/log_mutex.c` — снят `#if 0`, включён в сборку `app` (как
|
||||
> `tick.c` — компилируется прямо в таргет, FreeRTOS-специфика); `log_mutex_init()`
|
||||
> первой строкой `bringup_task`, до первого `LOG_*` где-либо в системе.
|
||||
>
|
||||
> **Временная диагностика** (не убрана, ждёт решения по 66 мс ниже):
|
||||
> `gfx_present()` логирует раздельно длительность PXP busy-wait и
|
||||
> semaphore-wait на FRAME_DONE (`LOG_I("gfx", "present: pxp=%u ms vsync=%u ms")`).
|
||||
> Замерено на стенде: **pxp≈66 мс, vsync≈0 мс** — стабильно, независимо от
|
||||
> содержимого кадра (похоже на фиксированную стоимость полнокадрового PXP-
|
||||
> композита на этом железе/тактировании, не на баг в коде отрисовки — тактовые
|
||||
> частоты (ARM PLL/AHB/IPG) идентичны `OLD_PROJECT_TFT8_UKL`, который использует
|
||||
> тот же PXP+vsync паттерн). Причина ещё не тормознее ELCDIF (65 Гц по расчёту
|
||||
> из `clock_config.c` — не бутылочное горлышко, vsync≈0 подтверждает). Открытый
|
||||
> вопрос — см. вторую HW-проверку.
|
||||
>
|
||||
> ---
|
||||
>
|
||||
> **Вторая HW-проверка (2026-07-22, тот же день) — три новых находки:**
|
||||
>
|
||||
> 1. **Обрыв связи: экран не сбрасывается на «--», залипает на старом этаже**
|
||||
> (по логу — НИ ОДНОГО `[gfx] present:` за 8+ с после обрыва, хотя WDOG/
|
||||
> heartbeat продолжают штатно). **Root-cause найден и подтверждён кодом:**
|
||||
> `bsp_can_receive()` (`bsp/can/src/can.c`) — busy-spin БЕЗ единого
|
||||
> блокирующего FreeRTOS-вызова внутри (`for(;;){poll_rx_mailboxes(); ...}`).
|
||||
> Без CAN-трафика `sul_rx_task` занимает весь `CAN_RX_TIMEOUT_MS` (100 мс)
|
||||
> КАЖДУЮ итерацию. Проверено по вендоренному `xTaskDelayUntil()`
|
||||
> (`sdk/rtos/freertos/freertos-kernel/tasks.c`): если дедлайн уже в прошлом
|
||||
> (а он в прошлом, когда спин съедает весь период) — `xShouldDelay` остаётся
|
||||
> `false`, задача в delayed-list НЕ добавляется, т.е. **не блокирует вовсе**.
|
||||
> Более низкоприоритетная задача никогда не выполнится, пока такая задача
|
||||
> непрерывно READY — `render_task` (был ниже `sul_rx_task`) физически не
|
||||
> получал CPU ни для первого рендера при старте без связи, ни для обработки
|
||||
> diff'а на «--» при обрыве. При живом трафике невидимо (`receive` почти
|
||||
> всегда быстрый, `sul_rx_task` реально блокируется) — поэтому не всплывало
|
||||
> раньше ни разу (PLAN.md, Фаза 1, прямо отмечал: обрыв связи на реальном
|
||||
> железе отдельно не гонялся).
|
||||
> **Фикс:** переставлены приоритеты — `menu > render > sul_rx`
|
||||
> (было `menu > sul_rx > render`). `render_task` больше не может быть
|
||||
> голодом заморожен `sul_rx_task`; `menu_task` по-прежнему выше `render_task`
|
||||
> (её PXP busy-wait не должен придерживать ввод). Корень (busy-spin в
|
||||
> `bsp_can_receive`) НЕ тронут — общий bare-metal+FreeRTOS модуль, добавление
|
||||
> yield потребовало бы условной компиляции; обсуждается отдельно.
|
||||
> 2. **Меню: вход теперь работает** (подтверждает фикс R4). Остальное:
|
||||
> навигация медленная (см. pxp≈66 мс выше — при открытом меню `sul_rx_task`
|
||||
> не спинит, `g_menu_active` гасит её блок целиком, так что 66 мс — это
|
||||
> чистая стоимость одного `gfx_present()`, не голодание); выход иногда не
|
||||
> срабатывает (могло быть тем же голоданием, что и находка 1 — при отсутствии
|
||||
> связи `sul_rx_task` возобновляет спин сразу по выходу из меню и мог не
|
||||
> пускать `render_task` дорисовать «выход»; должно закрыться тем же фиксом
|
||||
> приоритетов, перепроверить на стенде); пожелание — другая кнопка входа,
|
||||
> мгновенно (без удержания), как в `OLD_PROJECT_TFT8_UKL`.
|
||||
> **Согласовано и сделано:** раскладка `OLD_PROJECT_TFT8_UKL` дословно —
|
||||
> короткое BUTTON_1 = вход в меню (закрыто) / следующий пункт (открыто);
|
||||
> короткое BUTTON_2 = выбор/действие (открыто), намеренный no-op (закрыто).
|
||||
> `MENU_ENTER_HOLD_MS`/`hold_start` убраны из `task_menu.c` целиком.
|
||||
> 3. **Пустой экран при старте без связи со станцией** (прочерки появляются
|
||||
> только после захода в меню) — объясняется тем же голоданием (находка 1):
|
||||
> `render_task` не получал CPU для самого первого рендера, пока `sul_rx_task`
|
||||
> непрерывно спинила. Должно закрыться тем же фиксом приоритетов.
|
||||
>
|
||||
> _Осталось для ✅ (третий цикл проверки):_ прошить подписанный
|
||||
> `build/Debug/signed/app_slot_a.bin` и на TFT8: (а) старт без связи со
|
||||
> станцией — прочерки видны сразу, без захода в меню; (б) обрыв связи
|
||||
> мид-сессии → «--» сразу, без залипания; восстановление — сразу; (в) вход и
|
||||
> выход из меню — короткими BUTTON_1/действием, срабатывают стабильно, в т.ч.
|
||||
> без связи со станцией. Плюс субъективно оценить навигацию в меню (66 мс/шаг
|
||||
> — приемлемо или нужно отдельно разбираться со снижением стоимости
|
||||
> `gfx_present()` для модального окна меню) — открытый вопрос, решить по
|
||||
> итогам этой проверки.
|
||||
|
||||
|
||||
|
||||
> **Эталон — `OLD_PROJECT_TFT8_UKL/source/display/`** (`pxp_config.c`, `display.c`) +
|
||||
> `main_programm.c` (refresh-цикл ~строки 1052–1095). Наш `bsp_display` уже даёт нужный API:
|
||||
> `bsp_display_init(type, fb, on_frame_done)` (ISR-колбэк конца кадра) + `bsp_display_set_next_buffer(addr)`.
|
||||
> `fsl_pxp.{c,h}` есть в SDK (`sdk/devices/MIMXRT1052/drivers/`), но **не собран** как target.
|
||||
|
||||
**Модель (как в TFT8_UKL):** CPU рисует **весь кадр** в **AS** (`alpha_buffer`, ARGB8888, alpha
|
||||
значим: opaque=0xFF, «пусто»=0). **PS** (`processing_buffer`) — фон (сейчас сплошной чёрный, раз
|
||||
заливается; style-картинка — Фаза 5). PXP блендит **AS+PS → задний framebuffer** (`pxp_start_operation`,
|
||||
busy-wait Complete), затем свап: `xSemaphoreTake(frame_done)` (синх с ELCDIF) → `bsp_display_set_next_buffer(back)` →
|
||||
`current_buffer_index ^= 1`. Double-buffer = tear-free (рисуем off-screen, свап атомарный) → **костыль
|
||||
инкрементальной отрисовки удаляем, рисуем полный кадр.**
|
||||
|
||||
**Буферы** (некэшируемая SDRAM, `AT_NONCACHEABLE_SECTION_ALIGN`): `alpha_buffer` (AS) +
|
||||
`processing_buffer` (PS) + `framebuffer[2]` — каждый 800×600×4 = 1.92 МБ → **растим `m_sdram_ncache`
|
||||
2→8 МБ** в `cmake/linker/MIMXRT1052xxxxx_app_slot.ld` (степень 2, MPU-выравнивание через
|
||||
`__NCACHE_REGION_SIZE`).
|
||||
|
||||
**Sub-шаги (собирать после каждого; сборка/host-тесты в devcontainer, флеш — пользователь):**
|
||||
- **R1.** SDK: добавить `fsl_pxp` в сборку (новый target либо в gfx). Линкер: `m_sdram_ncache` 2→8 МБ.
|
||||
- **R2. gfx → компоновщик:** AS/PS/2×FB в ncache; порт `init_pxp`/`pxp_start_operation`; gfx рисует
|
||||
в **AS** (примитивы пишут alpha 0xFF; `gfx_clear` → AS прозрачный, memset 0); `gfx_present()` =
|
||||
PXP-композит AS+PS → задний FB + свап по `frame_done`-семафору; `gfx_init` регистрирует колбэк
|
||||
(даёт семафор), заливает PS чёрным, стартует ELCDIF на FB[0].
|
||||
- **R3. Презентация:** рисуем полный кадр в AS → `gfx_present()`. **Удалить костыль** `menu_view_render_row`
|
||||
/ логику «full/row» в `ui_task`; `menu_view` рисует полный кадр. Fallback — так же в AS.
|
||||
- **R4. Разбить `main.c`.** Первая версия — `app/task_sul_rx.c` + `app/task_ui.c` (презентация+
|
||||
меню+таймер ввода в одной задаче) — HW-проверка вскрыла регресс (меню не реагирует на кнопки,
|
||||
см. статус выше). Исправлено на 4 задачи: `bringup_task`/`sul_rx_task`/`menu_task`/`render_task`
|
||||
(`app/task_{bringup,sul_rx,menu,render}.c`), контракт — `app/app_tasks.h`. `main.c` = только
|
||||
`main()` (board init, объекты, ОДИН `bringup_task`, scheduler) + FreeRTOS-хуки. (ARCH §4: app =
|
||||
задачи + wiring + main.)
|
||||
|
||||
**Решения (согласованы):** PXP-код — внутри `services/gfx` (gfx владеет компоновщиком, как `display/`
|
||||
в TFT8_UKL); PS сейчас — сплошной чёрный; ассеты/спрайты/PNG/layout-компоновка остаются в Фазе 4
|
||||
(там же §PLAN Фаза 4 «Double buffering в gfx» — теперь закрывается здесь, в Фазе 4 остаётся только
|
||||
PXP-блит спрайтов и layout).
|
||||
|
||||
**Тесты (host).** Сериализация/дефолты/CRC ядра настроек; логика навигации меню; связывание
|
||||
дескрипторов; распознавание команды удалённой адресации; рантайм-гейт логов.
|
||||
|
||||
|
|
|
|||
127
firmware/tft_app/src/app/app_tasks.h
Normal file
127
firmware/tft_app/src/app/app_tasks.h
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
/**
|
||||
* @file app_tasks.h
|
||||
* @brief Контракт между задачами app-слоя (wiring, ARCH §4).
|
||||
*
|
||||
* Четыре задачи (найдено на HW-верификации Фазы 3.2.4 — меню и render в одной
|
||||
* задаче оказались взаимно неотзывчивы, см. PLAN.md):
|
||||
* - bringup_task — одноразовая инициализация (UART/QSPI/settings/self-confirm/
|
||||
* SDRAM+gfx+CAN), затем создаёт три нижеследующие задачи и удаляет себя.
|
||||
* - sul_rx_task — приём CAN → decode → controller. WDOG/heartbeat кормятся
|
||||
* БЕЗУСЛОВНО; сама CAN-работа — под !g_menu_active (мягкая пауза на время
|
||||
* меню — WDOG остаётся в безопасности по конструкции).
|
||||
* - menu_task — модель меню: потребление кнопок, hold-to-enter,
|
||||
* навигация/edit/exit+save. НЕ рисует.
|
||||
* - render_task — единственный владелец дисплея/вызывающий gfx_present().
|
||||
* Event-driven (будится xTaskNotifyGive от sul_rx_task И menu_task —
|
||||
* MPSC), не поллит, не содержит кнопочной логики.
|
||||
*
|
||||
* main.c создаёt только очередь/софт-таймер ввода/bringup_task — сами задачи
|
||||
* друг друга создают/не создают по схеме выше, main.c про это не знает.
|
||||
*/
|
||||
|
||||
#ifndef APP_TASKS_H_
|
||||
#define APP_TASKS_H_
|
||||
|
||||
#include "FreeRTOS.h"
|
||||
#include "domain/controller.h" /* indication_task_t */
|
||||
#include "domain/elevator_model.h" /* sul_result_t */
|
||||
#include "menu/menu.h" /* menu_ctx_t */
|
||||
#include "queue.h"
|
||||
#include "task.h"
|
||||
#include "timers.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
/* ── Приоритеты (единая точка правды, tskIDLE_PRIORITY-относительно) ────────
|
||||
*
|
||||
* menu_task > render_task > sul_rx_task. Найдено на HW-верификации (см.
|
||||
* PLAN.md): `bsp_can_receive()` — busy-spin БЕЗ yield (bsp/can/src/can.c,
|
||||
* никаких блокирующих FreeRTOS-вызовов внутри `for(;;){poll;...}`); пока нет
|
||||
* CAN-трафика, sul_rx_task занимает весь CAN_RX_TIMEOUT_MS каждую итерацию, и
|
||||
* xTaskDelayUntil() в этом случае НЕ блокирует вовсе (дедлайн уже в прошлом —
|
||||
* см. tasks.c, xShouldDelay остаётся false). Более низкоприоритетная задача
|
||||
* никогда не получает CPU, пока такая задача продолжает быть READY —
|
||||
* render_task, будучи ниже sul_rx_task, банально не мог выполниться (ни
|
||||
* первый рендер при старте без связи, ни обработка «--» при обрыве связи).
|
||||
* При живом трафике это было невидимо (receive почти всегда быстрый,
|
||||
* sul_rx_task реально блокируется), поэтому не всплывало раньше.
|
||||
*
|
||||
* menu_task остаётся ВЫШЕ render_task: её собственный busy-wait внутри PXP
|
||||
* (gfx_pxp_run, ~60-100 мс по замерам) не должен придерживать ввод — то же
|
||||
* свойство, ради которого меню и рендер разведены по разным задачам.
|
||||
* bringup_task — выше всех троих (монополизирует CPU на время одноразовой
|
||||
* инициализации, пока остальные задачи ещё не созданы). Демон программных
|
||||
* таймеров (bsp_button debounce, input_poll_cb) — отдельно, на
|
||||
* configTIMER_TASK_PRIORITY (см. FreeRTOSConfig.h — наивысший в системе), не
|
||||
* отсюда.
|
||||
*
|
||||
* Корень (busy-spin в bsp_can_receive без yield) НЕ тронут — это отдельный,
|
||||
* более рискованный шаг (bsp_can — общий модуль, используется и bare-metal
|
||||
* таргетами; добавить FreeRTOS-yield внутрь потребует условной компиляции,
|
||||
* как bsp_tick/log_mutex). Обсудить отдельно, если приоритетов недостаточно. */
|
||||
#define APP_PRIORITY_BRINGUP (tskIDLE_PRIORITY + 4U)
|
||||
#define APP_PRIORITY_MENU (tskIDLE_PRIORITY + 3U)
|
||||
#define APP_PRIORITY_RENDER (tskIDLE_PRIORITY + 2U)
|
||||
#define APP_PRIORITY_SUL_RX (tskIDLE_PRIORITY + 1U)
|
||||
|
||||
/** Общий стек-бюджет app-тасков (см. PLAN.md, Фаза 0 — тонкий стек уже
|
||||
* маскировался под похожий на зависание симптом; x8 — с запасом,
|
||||
* проверено на всех четырёх ролях). */
|
||||
#define APP_TASK_STACK_WORDS (configMINIMAL_STACK_SIZE * 8U)
|
||||
|
||||
/**
|
||||
* @brief Самое свежее состояние индикации (не история): sul_rx_task → render_task.
|
||||
*
|
||||
* Очередь глубины 1 с xQueueOverwrite() — важно только ПОСЛЕДНЕЕ состояние,
|
||||
* не промежуточные кадры (рендер не обязан успевать за каждым). Пробуждение
|
||||
* render_task — отдельно, через xTaskNotifyGive (см. g_render_task_handle).
|
||||
*/
|
||||
typedef struct
|
||||
{
|
||||
indication_task_t task;
|
||||
sul_result_t result;
|
||||
} render_msg_t;
|
||||
|
||||
/** Очередь глубины 1 (создаётся в main). Продюсер — sul_rx_task, консюмер — render_task. */
|
||||
extern QueueHandle_t g_render_queue;
|
||||
|
||||
/** Дисплей+CAN подняты (bring-up в bringup_task) — sul_rx_task/render_task ждут этого. */
|
||||
extern volatile bool g_display_ready;
|
||||
|
||||
/** true, пока меню открыто. Мягкая пауза sul_rx_task (CAN-decode/controller/
|
||||
* очередь пропускаются, WDOG/heartbeat — нет, см. task_sul_rx.c). Единственный
|
||||
* писатель — menu_task, единственный читатель — sul_rx_task. */
|
||||
extern volatile bool g_menu_active;
|
||||
|
||||
/** Модель меню. Владеет и мутирует ТОЛЬКО menu_task; render_task только читает
|
||||
* для отрисовки (после xTaskNotifyGive — happens-before через нотификацию,
|
||||
* как у g_render_queue). */
|
||||
extern menu_ctx_t g_menu;
|
||||
|
||||
/** Хэндл render_task — sul_rx_task и menu_task (MPSC-продюсеры) будят его
|
||||
* xTaskNotifyGive() на любое изменение состояния. Устанавливается
|
||||
* bringup_task ДО создания sul_rx_task/menu_task. */
|
||||
extern TaskHandle_t g_render_task_handle;
|
||||
|
||||
/** Одноразовая инициализация (UART/QSPI/settings/confirm_self/SDRAM+gfx+CAN),
|
||||
* затем создаёт sul_rx_task/menu_task/render_task и удаляет себя. */
|
||||
void bringup_task(void *p_arg);
|
||||
|
||||
/** Приём CAN + WDOG/heartbeat (безусловно) + decode/controller (под !g_menu_active). */
|
||||
void sul_rx_task(void *p_arg);
|
||||
|
||||
/** Модель меню: потребление кнопок, hold-to-enter, навигация/edit/exit+save. */
|
||||
void menu_task(void *p_arg);
|
||||
|
||||
/** Презентация: владелец дисплея, единственный вызывающий gfx_present(). */
|
||||
void render_task(void *p_arg);
|
||||
|
||||
/** Колбэк софт-таймера debounce (bsp_button_poll) — main создаёт таймер, каденция в main. */
|
||||
void input_poll_cb(TimerHandle_t x_timer);
|
||||
|
||||
/** Диагностика трейлера слота (read-only, безопасно звать многократно) —
|
||||
* общая для bringup_task (before/after-confirm) и sul_rx_task (периодический
|
||||
* re-log). Определена в task_bringup.c. */
|
||||
void log_slot_status(const char *p_when);
|
||||
|
||||
#endif /* APP_TASKS_H_ */
|
||||
|
|
@ -1,336 +1,59 @@
|
|||
/**
|
||||
* @file main.c
|
||||
* @brief tft_app — точка входа.
|
||||
* @brief tft_app — точка входа: board init, объекты, bringup_task, планировщик, хуки.
|
||||
*
|
||||
* ARCH §4: app-слой = задачи + wiring + main. main() создаёт только очередь,
|
||||
* софт-таймер ввода и ОДНУ задачу (bringup_task) — она сама создаёт
|
||||
* sul_rx_task/menu_task/render_task после одноразовой инициализации и
|
||||
* удаляет себя (см. app_tasks.h — полный контракт между задачами).
|
||||
*
|
||||
* Фаза 0 (каркас, подтверждено на железе): образ линкуется как валидный
|
||||
* MCUboot-слот (Direct-XIP), bootloader в него прыгает, FreeRTOS стартует,
|
||||
* WDOG кормится, образ ПОДТВЕРЖДАЕТ СЕБЯ в рантайме (нет revert на повторной
|
||||
* загрузке — anti-brick сохранён). Контракт с bootloader и обоснование
|
||||
* self-confirm/QSPI-ramfunc/UART-диагностики — см. PLAN.md, Фаза 0.
|
||||
*
|
||||
* Фаза 1 (walking skeleton): реальный CAN-кадр НКУ-CAN → чистый декодер →
|
||||
* контроллер → fallback-рендер (позиция + стрелка). Две задачи:
|
||||
* - sul_rx_task — WDOG/heartbeat (унаследовано от Фазы 0) + приём CAN +
|
||||
* decode + controller_process(); на изменение — в очередь render_task.
|
||||
* - render_task — потребляет очередь, зовёт ui_fallback_render().
|
||||
* Очередь глубиной 1 с xQueueOverwrite() — важно только ПОСЛЕДНЕЕ состояние,
|
||||
* не история промежуточных кадров (render не обязан успевать за каждым).
|
||||
* WDOG кормится, образ подтверждает себя в рантайме — см. PLAN.md, Фаза 0.
|
||||
*/
|
||||
|
||||
#include "FreeRTOS.h"
|
||||
#include "app_tasks.h"
|
||||
#include "board.h"
|
||||
#include "bootutil/bootutil_public.h"
|
||||
#include "bsp/boot_state.h"
|
||||
#include "bsp/display.h"
|
||||
#include "bsp/button.h"
|
||||
#include "bsp/led.h"
|
||||
#include "bsp/qspi_flash.h"
|
||||
#include "bsp/sdram.h"
|
||||
#include "bsp/uart_host.h"
|
||||
#include "bsp/wdog.h"
|
||||
#include "domain/controller.h"
|
||||
#include "domain/elevator_model.h"
|
||||
#include "domain/sul.h"
|
||||
#include "domain/sul/nku_can.h"
|
||||
#include "domain/sul/transport/can.h"
|
||||
#include "flash_map.h"
|
||||
#include "log/log.h"
|
||||
#include "port/log_uart.h"
|
||||
#include "queue.h"
|
||||
#include "services/gfx.h"
|
||||
#include "services/settings_store.h"
|
||||
#include "task.h"
|
||||
#include "ui/fallback.h"
|
||||
#include "timers.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#define LOG_TAG "app"
|
||||
#define INPUT_POLL_PERIOD_MS 5U /* софт-таймер опроса ввода (bsp_button_poll; opto — Фаза 3.4) */
|
||||
|
||||
/* Собственный слот образа. Slot A = 0 (primary), Slot Б = 1 (secondary). */
|
||||
#ifndef APP_OWN_SLOT_ID
|
||||
#define APP_OWN_SLOT_ID 0
|
||||
#endif
|
||||
/* ── Разделяемое состояние задач (объявления — app_tasks.h) ──────────────── */
|
||||
|
||||
/* Панель текущего стенда — Фаза 1 хардкод (Фаза 9: provisioning выбирает из
|
||||
* settings, см. ARCH §9 — TFT7/8/10 рантайм-выбор внутри профиля app-big). */
|
||||
#define PANEL_TYPE BSP_DISPLAY_TFT8
|
||||
|
||||
#define HEARTBEAT_PERIOD_MS 500U
|
||||
#define WDOG_FEED_PERIOD_MS 100U /* кормим чаще периода мигания — таймаут WDOG >= 1 c */
|
||||
#define STATUS_LOG_PERIOD_MS \
|
||||
2000U /* периодический re-log трейлера — виден независимо
|
||||
* от момента подключения терминала */
|
||||
#define CAN_RX_TIMEOUT_MS 100U /* держит цикл отзывчивым к WDOG/heartbeat-каденции */
|
||||
#define CONNECTION_TIMEOUT_MS \
|
||||
3000U /* «пропадание трафика» — см. ARCH, поток данных:
|
||||
* poll + timeout→default. Порядок величины — как
|
||||
* в OLD_PROJECT (там ~3 с на отметку потери связи) */
|
||||
|
||||
/* TEMP (Фаза 3.1): двухзагрузочная HW-проверка пути save/персист. УБРАТЬ в 3.2,
|
||||
* когда save начнёт вызываться из меню. Сентинел в ИНЕРТНОМ поле max_load_kg
|
||||
* (рендер грузоподъёмности — Фаза 5), proto_slice/адрес НЕ трогаем — индикатор
|
||||
* продолжает работать с адресом 0. */
|
||||
#define SETTINGS_SELFTEST_SENTINEL 4242U
|
||||
|
||||
/* Передача render_task самого свежего состояния — не истории. */
|
||||
typedef struct
|
||||
{
|
||||
indication_task_t task;
|
||||
sul_result_t result;
|
||||
} render_msg_t;
|
||||
|
||||
static QueueHandle_t g_s_render_queue;
|
||||
|
||||
/* ── Диагностика трейлера слота (read-only, безопасно звать многократно) ── */
|
||||
|
||||
static void log_slot_status(const char *p_when)
|
||||
{
|
||||
const struct flash_area *p_fap;
|
||||
const int RC_OPEN = flash_area_open((uint8_t) APP_OWN_SLOT_ID, &p_fap);
|
||||
if (RC_OPEN != 0)
|
||||
{
|
||||
LOG_E(LOG_TAG, "%s: flash_area_open(slot%d) rc=%d", p_when, APP_OWN_SLOT_ID, RC_OPEN);
|
||||
return;
|
||||
}
|
||||
|
||||
struct boot_swap_state st = { 0 };
|
||||
const int RC_RD = boot_read_swap_state(p_fap, &st);
|
||||
LOG_I(LOG_TAG, "%s: slot%d magic=%d copy_done=%d image_ok=%d (rd=%d)", p_when, APP_OWN_SLOT_ID,
|
||||
st.magic, st.copy_done, st.image_ok, RC_RD);
|
||||
|
||||
flash_area_close(p_fap);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Подтвердить СОБСТВЕННЫЙ слот (APP_OWN_SLOT_ID).
|
||||
*
|
||||
* boot_set_next(fap, active=true, confirm=true), НЕ boot_set_confirmed(): та
|
||||
* жёстко пишет в FLASH_AREA_IMAGE_PRIMARY (Slot A) независимо от исполняемого
|
||||
* слота — для Direct-XIP с двумя слотами это подтвердило бы не тот при
|
||||
* исполнении из Slot Б.
|
||||
*/
|
||||
static void confirm_self(void)
|
||||
{
|
||||
log_slot_status("before-confirm"); /* ожидаем magic=1(GOOD) image_ok=3(UNSET) */
|
||||
|
||||
const struct flash_area *p_fap;
|
||||
const int RC_OPEN = flash_area_open((uint8_t) APP_OWN_SLOT_ID, &p_fap);
|
||||
if (RC_OPEN != 0)
|
||||
{
|
||||
LOG_E(LOG_TAG, "confirm: flash_area_open(slot%d) rc=%d", APP_OWN_SLOT_ID, RC_OPEN);
|
||||
return;
|
||||
}
|
||||
|
||||
const int RC_SET = boot_set_next(p_fap, true, true);
|
||||
LOG_I(LOG_TAG, "confirm: boot_set_next rc=%d", RC_SET);
|
||||
|
||||
flash_area_close(p_fap);
|
||||
|
||||
log_slot_status("after-confirm"); /* ожидаем image_ok=1(SET) */
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief SDRAM (SEMC, без DCD) + framebuffer + CAN — до создания render_task.
|
||||
*
|
||||
* bsp_sdram_configure()+init() — та же документированная в bsp/sdram.h связка
|
||||
* для «прошивок без DCD», которую использует и bootloader-smoke-test.
|
||||
*/
|
||||
static bool bring_up_display_and_can(void)
|
||||
{
|
||||
if (bsp_sdram_configure() != BSP_OK)
|
||||
{
|
||||
LOG_E(LOG_TAG, "bsp_sdram_configure FAILED");
|
||||
return false;
|
||||
}
|
||||
if (bsp_sdram_init() != BSP_OK)
|
||||
{
|
||||
LOG_E(LOG_TAG, "bsp_sdram_init FAILED");
|
||||
return false;
|
||||
}
|
||||
if (gfx_init(PANEL_TYPE) != BSP_OK)
|
||||
{
|
||||
LOG_E(LOG_TAG, "gfx_init FAILED");
|
||||
return false;
|
||||
}
|
||||
if (sul_transport_can_init() != BSP_OK)
|
||||
{
|
||||
LOG_E(LOG_TAG, "sul_transport_can_init FAILED");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_I(LOG_TAG, "display+CAN bring-up OK");
|
||||
return true;
|
||||
}
|
||||
|
||||
static void render_task(void *p_arg)
|
||||
{
|
||||
(void) p_arg;
|
||||
|
||||
/* Безусловная первая отрисовка — не ждём первого diff (см. ui/fallback.h). */
|
||||
const sul_result_t INITIAL = sul_default_state();
|
||||
ui_fallback_render_initial(&INITIAL);
|
||||
|
||||
render_msg_t msg;
|
||||
for (;;)
|
||||
{
|
||||
if (xQueueReceive(g_s_render_queue, &msg, portMAX_DELAY) == pdTRUE)
|
||||
{
|
||||
ui_fallback_render(&msg.task, &msg.result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* TEMP (Фаза 3.1): проверка save/персист через power cycle. Первый запуск —
|
||||
* пишет сентинел и просит перезагрузку; после перезагрузки load() читает его с
|
||||
* флеша → «PERSIST OK». Сентинел остаётся (инертен), затирается первым
|
||||
* сохранением из меню (3.2) или factory-reset. УБРАТЬ вместе с #define в 3.2. */
|
||||
static void settings_selftest(void)
|
||||
{
|
||||
settings_t *p_s = settings_store_get_mutable();
|
||||
if (p_s->user.max_load_kg == SETTINGS_SELFTEST_SENTINEL)
|
||||
{
|
||||
LOG_I(LOG_TAG, "settings self-test: PERSIST OK (сентинел пережил перезагрузку)");
|
||||
return;
|
||||
}
|
||||
|
||||
p_s->user.max_load_kg = SETTINGS_SELFTEST_SENTINEL;
|
||||
const bsp_status_t RC = settings_store_save();
|
||||
LOG_I(LOG_TAG,
|
||||
"settings self-test: записан сентинел rc=%d — СДЕЛАЙТЕ POWER-CYCLE для проверки персиста",
|
||||
RC);
|
||||
}
|
||||
|
||||
static void sul_rx_task(void *p_arg)
|
||||
{
|
||||
(void) p_arg;
|
||||
|
||||
/* LPUART1/MCU-Link VCOM — доступен сразу, без enumeration/wait (в отличие
|
||||
* от target-side USB CDC). log_mutex не нужен — единственный писатель. */
|
||||
(void) bsp_uart_host_init(115200U);
|
||||
log_uart_init();
|
||||
|
||||
/* flash_map_backend требует bsp_qspi_init() ДО любой flash_area_*. */
|
||||
const bool QSPI_OK = (bsp_qspi_init() == BSP_OK);
|
||||
LOG_I(LOG_TAG, "tft_app phase1 boot: qspi=%s", QSPI_OK ? "OK" : "FAIL");
|
||||
|
||||
/* Настройки ядра (§8, §10): с флеша если QSPI поднялся, иначе дефолты. */
|
||||
if (QSPI_OK)
|
||||
{
|
||||
const bsp_status_t S_RC = settings_store_load();
|
||||
LOG_I(LOG_TAG, "settings: load rc=%d proto_addr=%u", S_RC,
|
||||
settings_store_get()->user.proto_slice[0]);
|
||||
settings_selftest(); /* TEMP (3.1): HW-проверка save/персист, убрать в 3.2 */
|
||||
}
|
||||
else
|
||||
{
|
||||
settings_store_init_defaults();
|
||||
}
|
||||
|
||||
/* «Дошёл до устойчивого состояния» — сбрасывает счётчик попыток загрузки
|
||||
* (recovery загрузчика). SRC GPR, без flash. Безусловно, до потенциально
|
||||
* рискованного bring-up дисплея/CAN ниже. */
|
||||
bsp_boot_health_mark();
|
||||
|
||||
if (QSPI_OK)
|
||||
{
|
||||
confirm_self();
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_E(LOG_TAG, "qspi_init FAILED — self-confirm skipped, slot will revert");
|
||||
}
|
||||
|
||||
const bool DISPLAY_CAN_OK = bring_up_display_and_can();
|
||||
|
||||
nku_can_ctx_t nku_ctx;
|
||||
nku_can_init(&nku_ctx);
|
||||
/* Адрес станции из настроек (§8, proto_slice[0]) → сдвиг ID пакетов. */
|
||||
nku_can_set_address(&nku_ctx, settings_store_get()->user.proto_slice[0]);
|
||||
|
||||
controller_ctx_t ctrl_ctx;
|
||||
controller_init(&ctrl_ctx);
|
||||
|
||||
const TickType_t FEED_PERIOD = pdMS_TO_TICKS(WDOG_FEED_PERIOD_MS);
|
||||
uint32_t elapsed_ms = 0U;
|
||||
uint32_t since_status_ms = 0U;
|
||||
TickType_t last_wake = xTaskGetTickCount();
|
||||
TickType_t last_frame_tick = xTaskGetTickCount();
|
||||
|
||||
for (;;)
|
||||
{
|
||||
bsp_wdog_refresh();
|
||||
|
||||
elapsed_ms += WDOG_FEED_PERIOD_MS;
|
||||
if (elapsed_ms >= HEARTBEAT_PERIOD_MS)
|
||||
{
|
||||
elapsed_ms = 0U;
|
||||
bsp_led_toggle(LED_APP);
|
||||
}
|
||||
|
||||
since_status_ms += WDOG_FEED_PERIOD_MS;
|
||||
if (since_status_ms >= STATUS_LOG_PERIOD_MS)
|
||||
{
|
||||
since_status_ms = 0U;
|
||||
log_slot_status("periodic");
|
||||
}
|
||||
|
||||
if (DISPLAY_CAN_OK)
|
||||
{
|
||||
sul_result_t decoded;
|
||||
bool have_update = false;
|
||||
|
||||
sul_frame_t frame;
|
||||
if (sul_transport_can_receive(CAN_RX_TIMEOUT_MS, &frame) == BSP_OK)
|
||||
{
|
||||
const sul_driver_t *p_driver = sul_registry_active();
|
||||
if (p_driver->decode(&nku_ctx, &frame, &decoded) == SUL_STATUS_OK)
|
||||
{
|
||||
last_frame_tick = xTaskGetTickCount();
|
||||
have_update = true;
|
||||
}
|
||||
/* IGNORED/ERR — Фаза 1 их отдельно не обрабатывает, следующая итерация. */
|
||||
}
|
||||
|
||||
const uint32_t SINCE_FRAME_MS =
|
||||
(uint32_t) (xTaskGetTickCount() - last_frame_tick) * portTICK_PERIOD_MS;
|
||||
if (SINCE_FRAME_MS >= CONNECTION_TIMEOUT_MS)
|
||||
{
|
||||
/* poll + timeout→default (ARCH, поток данных) — controller сам
|
||||
* определит, реальное ли это изменение (не сработает повторно
|
||||
* на каждой итерации после первого перехода в default). */
|
||||
decoded = sul_default_state();
|
||||
have_update = true;
|
||||
}
|
||||
|
||||
if (have_update)
|
||||
{
|
||||
const indication_task_t DIFF = controller_process(&ctrl_ctx, &decoded);
|
||||
if (DIFF.pos_pending || DIFF.direction_pending)
|
||||
{
|
||||
const render_msg_t MSG = { .task = DIFF, .result = decoded };
|
||||
(void) xQueueOverwrite(g_s_render_queue, &MSG);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vTaskDelayUntil(&last_wake, FEED_PERIOD);
|
||||
}
|
||||
}
|
||||
volatile bool g_display_ready = false;
|
||||
volatile bool g_menu_active = false;
|
||||
QueueHandle_t g_render_queue = NULL;
|
||||
TaskHandle_t g_render_task_handle = NULL;
|
||||
|
||||
int main(void)
|
||||
{
|
||||
board_hw_init(); /* BOARD_ConfigMPU + BOARD_InitPins + BOARD_BootClockRUN */
|
||||
bsp_led_init();
|
||||
(void) bsp_button_init(); /* GPIO настроен в BOARD_InitPins; сброс debounce */
|
||||
|
||||
g_s_render_queue = xQueueCreate(1, sizeof(render_msg_t));
|
||||
configASSERT(g_s_render_queue != NULL);
|
||||
g_render_queue = xQueueCreate(1, sizeof(render_msg_t));
|
||||
configASSERT(g_render_queue != NULL);
|
||||
|
||||
/* sul_rx выше приоритетом render — приём CAN/WDOG важнее своевременности
|
||||
* перерисовки. Стеки x6 (3 КБ) с запасом (см. PLAN.md, Фаза 0 — тонкий
|
||||
* стек уже маскировался под похожий на зависание симптом). */
|
||||
(void) xTaskCreate(sul_rx_task, "sul_rx", configMINIMAL_STACK_SIZE * 6U, NULL,
|
||||
tskIDLE_PRIORITY + 2U, NULL);
|
||||
(void) xTaskCreate(render_task, "render", configMINIMAL_STACK_SIZE * 6U, NULL,
|
||||
tskIDLE_PRIORITY + 1U, NULL);
|
||||
/* Опрос ввода — софт-таймер (демон на высшем приоритете в системе вытесняет
|
||||
* всё остальное — нажатия не теряются). Масштабируется на opto (Фаза 3.4)
|
||||
* тем же колбэком. */
|
||||
TimerHandle_t input_timer =
|
||||
xTimerCreate("input", pdMS_TO_TICKS(INPUT_POLL_PERIOD_MS), pdTRUE, NULL, input_poll_cb);
|
||||
configASSERT(input_timer != NULL);
|
||||
(void) xTimerStart(input_timer, 0);
|
||||
|
||||
/* Единственная задача, которую создаёт main — bringup_task сама создаст
|
||||
* sul_rx_task/menu_task/render_task после инициализации и удалит себя
|
||||
* (см. app_tasks.h про приоритеты/порядок). */
|
||||
(void) xTaskCreate(bringup_task, "bringup", APP_TASK_STACK_WORDS, NULL, APP_PRIORITY_BRINGUP,
|
||||
NULL);
|
||||
|
||||
vTaskStartScheduler();
|
||||
|
||||
|
|
|
|||
186
firmware/tft_app/src/app/task_bringup.c
Normal file
186
firmware/tft_app/src/app/task_bringup.c
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
/**
|
||||
* @file task_bringup.c
|
||||
* @brief Одноразовое bring-up: лог-мьютекс, UART/лог, QSPI+settings+self-confirm,
|
||||
* SDRAM+gfx+CAN. Затем создаёт steady-state задачи и удаляет себя.
|
||||
*
|
||||
* Слот образа ПОДТВЕРЖДАЕТ СЕБЯ в рантайме (нет revert на повторной загрузке —
|
||||
* anti-brick). Контракт с bootloader и обоснование self-confirm/QSPI-ramfunc/
|
||||
* UART-диагностики — см. PLAN.md, Фаза 0.
|
||||
*
|
||||
* Выделено в отдельную задачу на Фазе 3.2.4 (было частью sul_rx_task) — по
|
||||
* итогам HW-верификации: одноразовая init-последовательность архитектурно не
|
||||
* то же самое, что вечный CAN-цикл, и разделение делает обе задачи проще
|
||||
* контролировать по отдельности.
|
||||
*/
|
||||
|
||||
#include "app_tasks.h"
|
||||
|
||||
#include "FreeRTOS.h"
|
||||
#include "bootutil/bootutil_public.h"
|
||||
#include "bsp/boot_state.h"
|
||||
#include "bsp/display.h"
|
||||
#include "bsp/qspi_flash.h"
|
||||
#include "bsp/sdram.h"
|
||||
#include "bsp/uart_host.h"
|
||||
#include "domain/sul/transport/can.h"
|
||||
#include "flash_map.h"
|
||||
#include "log/log.h"
|
||||
#include "port/log_uart.h"
|
||||
#include "services/gfx.h"
|
||||
#include "services/settings_store.h"
|
||||
#include "task.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#define LOG_TAG "bringup"
|
||||
|
||||
/* Собственный слот образа. Slot A = 0 (primary), Slot Б = 1 (secondary). */
|
||||
#ifndef APP_OWN_SLOT_ID
|
||||
#define APP_OWN_SLOT_ID 0
|
||||
#endif
|
||||
|
||||
/* Панель текущего стенда — Фаза 1 хардкод (Фаза 9: provisioning выбирает из
|
||||
* settings, см. ARCH §9 — TFT7/8/10 рантайм-выбор внутри профиля app-big). */
|
||||
#define PANEL_TYPE BSP_DISPLAY_TFT8
|
||||
|
||||
/* ── Диагностика трейлера слота (read-only, безопасно звать многократно).
|
||||
* Общая для bringup_task (before/after-confirm) и sul_rx_task (periodic) —
|
||||
* прототип в app_tasks.h. ──────────────────────────────────────────────── */
|
||||
void log_slot_status(const char *p_when)
|
||||
{
|
||||
const struct flash_area *p_fap;
|
||||
const int RC_OPEN = flash_area_open((uint8_t) APP_OWN_SLOT_ID, &p_fap);
|
||||
if (RC_OPEN != 0)
|
||||
{
|
||||
LOG_E(LOG_TAG, "%s: flash_area_open(slot%d) rc=%d", p_when, APP_OWN_SLOT_ID, RC_OPEN);
|
||||
return;
|
||||
}
|
||||
|
||||
struct boot_swap_state st = { 0 };
|
||||
const int RC_RD = boot_read_swap_state(p_fap, &st);
|
||||
LOG_I(LOG_TAG, "%s: slot%d magic=%d copy_done=%d image_ok=%d (rd=%d)", p_when, APP_OWN_SLOT_ID,
|
||||
st.magic, st.copy_done, st.image_ok, RC_RD);
|
||||
|
||||
flash_area_close(p_fap);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Подтвердить СОБСТВЕННЫЙ слот (APP_OWN_SLOT_ID).
|
||||
*
|
||||
* boot_set_next(fap, active=true, confirm=true), НЕ boot_set_confirmed(): та
|
||||
* жёстко пишет в FLASH_AREA_IMAGE_PRIMARY (Slot A) независимо от исполняемого
|
||||
* слота — для Direct-XIP с двумя слотами это подтвердило бы не тот при
|
||||
* исполнении из Slot Б.
|
||||
*/
|
||||
static void confirm_self(void)
|
||||
{
|
||||
log_slot_status("before-confirm"); /* ожидаем magic=1(GOOD) image_ok=3(UNSET) */
|
||||
|
||||
const struct flash_area *p_fap;
|
||||
const int RC_OPEN = flash_area_open((uint8_t) APP_OWN_SLOT_ID, &p_fap);
|
||||
if (RC_OPEN != 0)
|
||||
{
|
||||
LOG_E(LOG_TAG, "confirm: flash_area_open(slot%d) rc=%d", APP_OWN_SLOT_ID, RC_OPEN);
|
||||
return;
|
||||
}
|
||||
|
||||
const int RC_SET = boot_set_next(p_fap, true, true);
|
||||
LOG_I(LOG_TAG, "confirm: boot_set_next rc=%d", RC_SET);
|
||||
|
||||
flash_area_close(p_fap);
|
||||
|
||||
log_slot_status("after-confirm"); /* ожидаем image_ok=1(SET) */
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief SDRAM (SEMC, без DCD) + компоновщик gfx + CAN — перед steady-state задачами.
|
||||
*
|
||||
* bsp_sdram_configure()+init() — та же документированная в bsp/sdram.h связка
|
||||
* для «прошивок без DCD», которую использует и bootloader-smoke-test.
|
||||
*/
|
||||
static bool bring_up_display_and_can(void)
|
||||
{
|
||||
if (bsp_sdram_configure() != BSP_OK)
|
||||
{
|
||||
LOG_E(LOG_TAG, "bsp_sdram_configure FAILED");
|
||||
return false;
|
||||
}
|
||||
if (bsp_sdram_init() != BSP_OK)
|
||||
{
|
||||
LOG_E(LOG_TAG, "bsp_sdram_init FAILED");
|
||||
return false;
|
||||
}
|
||||
if (gfx_init(PANEL_TYPE) != BSP_OK)
|
||||
{
|
||||
LOG_E(LOG_TAG, "gfx_init FAILED");
|
||||
return false;
|
||||
}
|
||||
if (sul_transport_can_init() != BSP_OK)
|
||||
{
|
||||
LOG_E(LOG_TAG, "sul_transport_can_init FAILED");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_I(LOG_TAG, "display+CAN bring-up OK");
|
||||
return true;
|
||||
}
|
||||
|
||||
void bringup_task(void *p_arg)
|
||||
{
|
||||
(void) p_arg;
|
||||
|
||||
/* ДО первого LOG_* где-либо в системе — log_write() берёт мьютекс
|
||||
* безусловно (port/log/src/log_mutex.c, strong-override weak-NOP).
|
||||
* bringup_task — первая и пока единственная запущенная задача, так что
|
||||
* порядок гарантирован конструкцией (main создаёт только её). */
|
||||
log_mutex_init();
|
||||
|
||||
/* LPUART1/MCU-Link VCOM — доступен сразу, без enumeration/wait (в отличие
|
||||
* от target-side USB CDC). */
|
||||
(void) bsp_uart_host_init(115200U);
|
||||
log_uart_init();
|
||||
|
||||
/* flash_map_backend требует bsp_qspi_init() ДО любой flash_area_*. */
|
||||
const bool QSPI_OK = (bsp_qspi_init() == BSP_OK);
|
||||
LOG_I(LOG_TAG, "tft_app boot: qspi=%s", QSPI_OK ? "OK" : "FAIL");
|
||||
|
||||
/* Настройки ядра (§8, §10): с флеша если QSPI поднялся, иначе дефолты. */
|
||||
if (QSPI_OK)
|
||||
{
|
||||
const bsp_status_t S_RC = settings_store_load();
|
||||
LOG_I(LOG_TAG, "settings: load rc=%d proto_addr=%u", S_RC,
|
||||
settings_store_get()->user.proto_slice[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
settings_store_init_defaults();
|
||||
}
|
||||
|
||||
/* «Дошёл до устойчивого состояния» — сбрасывает счётчик попыток загрузки
|
||||
* (recovery загрузчика). SRC GPR, без flash. Безусловно, до потенциально
|
||||
* рискованного bring-up дисплея/CAN ниже. */
|
||||
bsp_boot_health_mark();
|
||||
|
||||
if (QSPI_OK)
|
||||
{
|
||||
confirm_self();
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_E(LOG_TAG, "qspi_init FAILED — self-confirm skipped, slot will revert");
|
||||
}
|
||||
|
||||
g_display_ready = bring_up_display_and_can(); /* sul_rx_task/render_task ждут этого */
|
||||
|
||||
/* render_task — первым: его хэндл нужен sul_rx_task/menu_task для
|
||||
* xTaskNotifyGive. Порядок формально некритичен (bringup_task —
|
||||
* наивысший приоритет из четырёх и монополизирует CPU до своего
|
||||
* удаления — никто из троих не может выполниться раньше, чем все три
|
||||
* xTaskCreate() ниже отработают), но документирует зависимость явно. */
|
||||
(void) xTaskCreate(render_task, "render", APP_TASK_STACK_WORDS, NULL, APP_PRIORITY_RENDER,
|
||||
&g_render_task_handle);
|
||||
(void) xTaskCreate(sul_rx_task, "sul_rx", APP_TASK_STACK_WORDS, NULL, APP_PRIORITY_SUL_RX, NULL);
|
||||
(void) xTaskCreate(menu_task, "menu", APP_TASK_STACK_WORDS, NULL, APP_PRIORITY_MENU, NULL);
|
||||
|
||||
vTaskDelete(NULL); /* одноразовая задача — дальше нечего делать */
|
||||
}
|
||||
114
firmware/tft_app/src/app/task_menu.c
Normal file
114
firmware/tft_app/src/app/task_menu.c
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
/**
|
||||
* @file task_menu.c
|
||||
* @brief Модель меню: потребление кнопок (debounce — bsp_button/софт-таймер,
|
||||
* независимо от этой задачи), мгновенные вход/навигация/edit/exit+save.
|
||||
*
|
||||
* Раскладка — как в OLD_PROJECT_TFT8_UKL (без удержания, короткие нажатия):
|
||||
* короткое BUTTON_1 = вход в меню (когда закрыто) / следующий пункт (когда
|
||||
* открыто); короткое BUTTON_2 = выбор/действие (когда открыто), намеренный
|
||||
* no-op вне меню.
|
||||
*
|
||||
* НЕ рисует — мутирует `g_menu` и будит render_task (xTaskNotifyGive) на любое
|
||||
* изменение состояния. Разделено от render_task на Фазе 3.2.4 (HW-находка):
|
||||
* раньше (3.2.1–3.2.3, один framebuffer, без ожиданий) потребление кнопок и
|
||||
* рендер жили в одной задаче безвредно — блокировок не было. gfx_present()
|
||||
* (double-buffer + PXP) внёс блокирующее ожидание кадра; в объединённой
|
||||
* задаче это ожидание попутно блокировало вход в меню (ноль реакции на
|
||||
* кнопки — см. PLAN.md). Эталон разделения — OLD_PROJECT_TFT8_UKL:
|
||||
* BUTTONS_TASK/menu_task отдельно от REFRESH_TASK/tft_refresh_task.
|
||||
*/
|
||||
|
||||
#include "app_tasks.h"
|
||||
|
||||
#include "FreeRTOS.h"
|
||||
#include "bsp/button.h"
|
||||
#include "log/log.h"
|
||||
#include "menu/menu.h"
|
||||
#include "menu/menu_tree.h"
|
||||
#include "services/settings_store.h"
|
||||
#include "task.h"
|
||||
#include "timers.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#define LOG_TAG "menu"
|
||||
|
||||
#define MENU_TICK_MS 5U /* каденция потребления кнопок */
|
||||
|
||||
menu_ctx_t g_menu;
|
||||
|
||||
/* Софт-таймер (высший приоритет демона) опрашивает debounce независимо от
|
||||
* menu_task/render_task — нажатия не теряются, пока кто-то из них занят.
|
||||
* Колбэк короткий, без блокировок. */
|
||||
void input_poll_cb(TimerHandle_t x_timer)
|
||||
{
|
||||
(void) x_timer;
|
||||
bsp_button_poll();
|
||||
/* Фаза 3.4: bsp_opto_process(); (диспетчерские вход/ответ) */
|
||||
}
|
||||
|
||||
void menu_task(void *p_arg)
|
||||
{
|
||||
(void) p_arg;
|
||||
|
||||
menu_init(&g_menu, menu_tree_items(), menu_tree_count(), settings_store_get_mutable());
|
||||
|
||||
TickType_t last_wake = xTaskGetTickCount();
|
||||
|
||||
for (;;)
|
||||
{
|
||||
bool changed = false;
|
||||
|
||||
/* Порядок и структура — как в OLD_PROJECT_TFT8_UKL menu_task():
|
||||
* BUTTON_2 читается и обрабатывается только "внутри", BUTTON_1 —
|
||||
* читается всегда (вход ИЛИ навигация в зависимости от open). Оба
|
||||
* события дренируются безусловно (один get_event_pressed на кнопку
|
||||
* за итерацию) — не копится устаревшее состояние. */
|
||||
if (bsp_button_get_event_pressed(BSP_BUTTON_2))
|
||||
{
|
||||
if (menu_is_open(&g_menu))
|
||||
{
|
||||
menu_action(&g_menu);
|
||||
|
||||
/* Обновить ДО settings_store_save() (флеш-запись, не
|
||||
* мгновенная) — иначе sul_rx_task ещё несколько мс видел бы
|
||||
* устаревший g_menu_active=true и держал бы мягкую паузу
|
||||
* дольше нужного. */
|
||||
g_menu_active = menu_is_open(&g_menu);
|
||||
|
||||
if (!g_menu_active && g_menu.save_requested)
|
||||
{
|
||||
/* Выход: сохранить (если менялось). Адрес подхватит
|
||||
* sul_rx_task из настроек на следующей итерации. */
|
||||
const bsp_status_t RC = settings_store_save();
|
||||
LOG_I(LOG_TAG, "settings saved rc=%d", RC);
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
/* иначе — намеренный no-op вне меню (как в референсе) */
|
||||
}
|
||||
else if (bsp_button_get_event_pressed(BSP_BUTTON_1))
|
||||
{
|
||||
if (menu_is_open(&g_menu))
|
||||
{
|
||||
menu_next(&g_menu);
|
||||
}
|
||||
else
|
||||
{
|
||||
menu_open(&g_menu); /* мгновенный вход, без удержания */
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
|
||||
/* Финальная синхронизация флага мягкой паузы с моделью — покрывает
|
||||
* вход/навигацию (выход уже обновил его выше, до save()). */
|
||||
g_menu_active = menu_is_open(&g_menu);
|
||||
|
||||
if (changed && (g_render_task_handle != NULL))
|
||||
{
|
||||
(void) xTaskNotifyGive(g_render_task_handle);
|
||||
}
|
||||
|
||||
vTaskDelayUntil(&last_wake, pdMS_TO_TICKS(MENU_TICK_MS));
|
||||
}
|
||||
}
|
||||
86
firmware/tft_app/src/app/task_render.c
Normal file
86
firmware/tft_app/src/app/task_render.c
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/**
|
||||
* @file task_render.c
|
||||
* @brief Презентация: единственный владелец дисплея и вызывающий gfx_present().
|
||||
*
|
||||
* Event-driven (xTaskNotifyGive от sul_rx_task И menu_task — MPSC, будят оба
|
||||
* продюсера, ulTaskNotifyTake(pdTRUE,...) схлопывает несколько notify в одно
|
||||
* пробуждение — важно только «есть свежее состояние», не сколько раз оно
|
||||
* менялось). НЕ содержит кнопочной логики — разделено от menu_task на Фазе
|
||||
* 3.2.4 (см. task_menu.c про причину).
|
||||
*
|
||||
* На каждое пробуждение: меню открыто → рисует меню; иначе — если меню ТОЛЬКО
|
||||
* ЧТО закрылось, сразу восстанавливает последнее известное состояние индикации
|
||||
* (не дожидаясь свежего сообщения — sul_rx_task мог простаивать под
|
||||
* g_menu_active), и дренирует g_render_queue, если там свежий diff.
|
||||
*/
|
||||
|
||||
#include "app_tasks.h"
|
||||
|
||||
#include "FreeRTOS.h"
|
||||
#include "domain/elevator_model.h"
|
||||
#include "queue.h"
|
||||
#include "services/gfx.h"
|
||||
#include "task.h"
|
||||
#include "ui/fallback.h"
|
||||
#include "ui/menu_view.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#define DISPLAY_WAIT_POLL_MS 5U /* пока g_display_ready не выставлен bringup_task'ом */
|
||||
|
||||
void render_task(void *p_arg)
|
||||
{
|
||||
(void) p_arg;
|
||||
|
||||
while (!g_display_ready)
|
||||
{
|
||||
vTaskDelay(pdMS_TO_TICKS(DISPLAY_WAIT_POLL_MS));
|
||||
}
|
||||
|
||||
sul_result_t last = sul_default_state();
|
||||
ui_fallback_render_initial(&last);
|
||||
gfx_present();
|
||||
|
||||
bool was_menu_open = false;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
(void) ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||
|
||||
const bool MENU_OPEN_NOW = menu_is_open(&g_menu);
|
||||
bool present_needed = false;
|
||||
|
||||
if (MENU_OPEN_NOW)
|
||||
{
|
||||
menu_view_render(&g_menu);
|
||||
present_needed = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (was_menu_open)
|
||||
{
|
||||
/* Меню только что закрылось — восстановить индикацию
|
||||
* последним известным состоянием немедленно, не дожидаясь
|
||||
* свежего сообщения (sul_rx_task мог простаивать под
|
||||
* g_menu_active — очередь пока пуста). */
|
||||
ui_fallback_render_initial(&last);
|
||||
present_needed = true;
|
||||
}
|
||||
|
||||
render_msg_t msg;
|
||||
if (xQueueReceive(g_render_queue, &msg, 0) == pdTRUE)
|
||||
{
|
||||
last = msg.result;
|
||||
ui_fallback_render(&msg.task, &msg.result);
|
||||
present_needed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (present_needed)
|
||||
{
|
||||
gfx_present(); /* PXP-композит AS+PS → задний FB + свап (tear-free) */
|
||||
}
|
||||
|
||||
was_menu_open = MENU_OPEN_NOW;
|
||||
}
|
||||
}
|
||||
135
firmware/tft_app/src/app/task_sul_rx.c
Normal file
135
firmware/tft_app/src/app/task_sul_rx.c
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
/**
|
||||
* @file task_sul_rx.c
|
||||
* @brief Приём CAN → decode → controller → уведомление render_task.
|
||||
*
|
||||
* WDOG/heartbeat/периодический re-log трейлера — БЕЗУСЛОВНО (housekeeping,
|
||||
* не CAN-специфика). Сама CAN-работа (decode/controller/очередь) — под
|
||||
* `!g_menu_active`: мягкая пауза на время меню (см. app_tasks.h) — задача НЕ
|
||||
* suspend'ится, поэтому WDOG остаётся в безопасности по конструкции.
|
||||
*
|
||||
* Bring-up (QSPI/settings/self-confirm/SDRAM+gfx+CAN) — bringup_task
|
||||
* (task_bringup.c); эта задача стартует уже после него, `g_display_ready`
|
||||
* решает, делать ли CAN-работу вообще.
|
||||
*/
|
||||
|
||||
#include "app_tasks.h"
|
||||
|
||||
#include "FreeRTOS.h"
|
||||
#include "bsp/led.h"
|
||||
#include "bsp/wdog.h"
|
||||
#include "domain/controller.h"
|
||||
#include "domain/elevator_model.h"
|
||||
#include "domain/sul.h"
|
||||
#include "domain/sul/nku_can.h"
|
||||
#include "domain/sul/transport/can.h"
|
||||
#include "queue.h"
|
||||
#include "services/settings_store.h"
|
||||
#include "task.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#define HEARTBEAT_PERIOD_MS 500U
|
||||
#define WDOG_FEED_PERIOD_MS 100U /* кормим чаще периода мигания — таймаут WDOG >= 1 c */
|
||||
#define STATUS_LOG_PERIOD_MS \
|
||||
2000U /* периодический re-log трейлера — виден независимо
|
||||
* от момента подключения терминала */
|
||||
#define CAN_RX_TIMEOUT_MS 100U /* держит цикл отзывчивым к WDOG/heartbeat-каденции */
|
||||
#define CONNECTION_TIMEOUT_MS \
|
||||
3000U /* «пропадание трафика» — см. ARCH, поток данных:
|
||||
* poll + timeout→default. Порядок величины — как
|
||||
* в OLD_PROJECT (там ~3 с на отметку потери связи).
|
||||
* Меряется от last_frame_tick — пауза меню не портит
|
||||
* логику: если трафик реально стоял, «--» появится
|
||||
* сразу по возврату из меню; если шёл — SINCE_FRAME_MS
|
||||
* обнулится первым же принятым кадром. */
|
||||
|
||||
void sul_rx_task(void *p_arg)
|
||||
{
|
||||
(void) p_arg;
|
||||
|
||||
nku_can_ctx_t nku_ctx;
|
||||
nku_can_init(&nku_ctx);
|
||||
|
||||
controller_ctx_t ctrl_ctx;
|
||||
controller_init(&ctrl_ctx);
|
||||
|
||||
const TickType_t FEED_PERIOD = pdMS_TO_TICKS(WDOG_FEED_PERIOD_MS);
|
||||
uint32_t elapsed_ms = 0U;
|
||||
uint32_t since_status_ms = 0U;
|
||||
TickType_t last_wake = xTaskGetTickCount();
|
||||
TickType_t last_frame_tick = xTaskGetTickCount();
|
||||
|
||||
for (;;)
|
||||
{
|
||||
bsp_wdog_refresh();
|
||||
|
||||
elapsed_ms += WDOG_FEED_PERIOD_MS;
|
||||
if (elapsed_ms >= HEARTBEAT_PERIOD_MS)
|
||||
{
|
||||
elapsed_ms = 0U;
|
||||
bsp_led_toggle(LED_APP);
|
||||
}
|
||||
|
||||
since_status_ms += WDOG_FEED_PERIOD_MS;
|
||||
if (since_status_ms >= STATUS_LOG_PERIOD_MS)
|
||||
{
|
||||
since_status_ms = 0U;
|
||||
log_slot_status("periodic");
|
||||
}
|
||||
|
||||
if (g_display_ready && !g_menu_active)
|
||||
{
|
||||
/* Адрес станции из настроек (proto_slice[0]) — подхватывает правку из
|
||||
* меню без межзадачного сигнала (запись/чтение uint8 атомарны).
|
||||
* Обе стороны: decode (nku_ctx) И HW RX-фильтры FlexCAN — вторая
|
||||
* без переприменения фильтров под реальный адрес станции кадры с
|
||||
* адресом != 0 отбрасывались бы на уровне CAN-контроллера, ещё до
|
||||
* decode (см. PLAN.md — найдено на реальной станции, адрес 1). */
|
||||
const uint8_t NKU_ADDR = settings_store_get()->user.proto_slice[0];
|
||||
nku_can_set_address(&nku_ctx, NKU_ADDR);
|
||||
(void) sul_transport_can_set_address(NKU_ADDR); /* no-op, если адрес не менялся */
|
||||
|
||||
sul_result_t decoded;
|
||||
bool have_update = false;
|
||||
|
||||
sul_frame_t frame;
|
||||
if (sul_transport_can_receive(CAN_RX_TIMEOUT_MS, &frame) == BSP_OK)
|
||||
{
|
||||
const sul_driver_t *p_driver = sul_registry_active();
|
||||
if (p_driver->decode(&nku_ctx, &frame, &decoded) == SUL_STATUS_OK)
|
||||
{
|
||||
last_frame_tick = xTaskGetTickCount();
|
||||
have_update = true;
|
||||
}
|
||||
/* IGNORED/ERR — Фаза 1 их отдельно не обрабатывает, следующая итерация. */
|
||||
}
|
||||
|
||||
const uint32_t SINCE_FRAME_MS =
|
||||
(uint32_t) (xTaskGetTickCount() - last_frame_tick) * portTICK_PERIOD_MS;
|
||||
if (SINCE_FRAME_MS >= CONNECTION_TIMEOUT_MS)
|
||||
{
|
||||
/* poll + timeout→default (ARCH, поток данных) — controller сам
|
||||
* определит, реальное ли это изменение (не сработает повторно
|
||||
* на каждой итерации после первого перехода в default). */
|
||||
decoded = sul_default_state();
|
||||
have_update = true;
|
||||
}
|
||||
|
||||
if (have_update)
|
||||
{
|
||||
const indication_task_t DIFF = controller_process(&ctrl_ctx, &decoded);
|
||||
if (DIFF.pos_pending || DIFF.direction_pending || DIFF.mode_pending)
|
||||
{
|
||||
const render_msg_t MSG = { .task = DIFF, .result = decoded };
|
||||
(void) xQueueOverwrite(g_render_queue, &MSG);
|
||||
if (g_render_task_handle != NULL)
|
||||
{
|
||||
(void) xTaskNotifyGive(g_render_task_handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vTaskDelayUntil(&last_wake, FEED_PERIOD);
|
||||
}
|
||||
}
|
||||
|
|
@ -20,14 +20,28 @@ extern "C"
|
|||
#endif
|
||||
|
||||
/**
|
||||
* @brief bsp_can_init() + RX-фильтры под активные ID НКУ-CAN.
|
||||
* @brief bsp_can_init(). RX-фильтры НЕ настраивает.
|
||||
*
|
||||
* Фаза 1: фильтры на PACKET1(0x506)/PACKET3(0x508), адрес станции 0
|
||||
* (хардкод — совпадает с decode-стороной nku_can.c). Фаза 3 параметризует
|
||||
* оба конца из настроек одновременно.
|
||||
* Вызывающий обязан сразу после этого позвать sul_transport_can_set_address()
|
||||
* — внутренний сентинел форсирует первое применение фильтров независимо от
|
||||
* переданного адреса (как в OLD_PROJECT msg_receiver_task).
|
||||
*/
|
||||
bsp_status_t sul_transport_can_init(void);
|
||||
|
||||
/**
|
||||
* @brief (Пере)настроить RX-фильтры (PACKET1..5) под адрес станции.
|
||||
*
|
||||
* НКУ-CAN кодирует адрес станции в ID: PACKET1..4 — биты [7:4] (group4 =
|
||||
* addr<<4), PACKET5 — биты [8:6] (group6 = addr<<6, протокол отводит под
|
||||
* него только 3 бита). Дёшево звать на каждой итерации приёма — реальная
|
||||
* переконфигурация Message Buffer'ов FlexCAN происходит только при
|
||||
* фактическом изменении адреса (внутренний diff, сентинел на первый вызов).
|
||||
* Эталон — OLD_PROJECT msg_receiver_task/apply_nku_can_filters().
|
||||
*
|
||||
* @param nku_address 0..15; вне диапазона — приводится к 15.
|
||||
*/
|
||||
bsp_status_t sul_transport_can_set_address(uint8_t nku_address);
|
||||
|
||||
/**
|
||||
* @brief Принять один кадр и перевести в sul_frame_t.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -2,35 +2,82 @@
|
|||
|
||||
#include "bsp/can.h"
|
||||
|
||||
/* Фаза 1: адрес станции захардкожен в 0 — совпадает с базовыми ID в
|
||||
* domain/sul/nku_can/src/nku_can.c (0x506|group4, group4=address<<4=0).
|
||||
* Фаза 3 параметризует оба конца одновременно из настроек. */
|
||||
#define PACKET1_ID 0x506U
|
||||
#define PACKET3_ID 0x508U
|
||||
/* Базовые ID НКУ-CAN (адрес станции 0) — та же протокольная раскладка, что и
|
||||
* decode-сторона (domain/sul/nku_can/src/nku_can.c): PACKET1..4 кодируют
|
||||
* адрес в битах [7:4] ID (group4 = addr<<4), PACKET5 — в битах [8:6]
|
||||
* (group6 = addr<<6, протокол отводит под него только 3 бита). Decode и
|
||||
* transport намеренно НЕ шарят общий заголовок с этими константами — каждый
|
||||
* владеет своей копией протокольного факта (декодер ничего не знает про
|
||||
* транспорт и наоборот, ARCH). */
|
||||
#define PACKET1_BASE 0x506U
|
||||
#define PACKET2_BASE 0x408U
|
||||
#define PACKET3_BASE 0x508U
|
||||
#define PACKET4_BASE 0x50BU
|
||||
#define PACKET5_BASE 0x606U
|
||||
#define STD_ID_MASK 0x7FFU /* 11-bit STD — проверять все биты */
|
||||
|
||||
#define NKU_ADDRESS_MAX 15U /* 4-битный адрес, group4 = addr<<4 */
|
||||
|
||||
/* MB index 0..4 — PACKET1..5 соответственно (см. sul_transport_can_set_address). */
|
||||
|
||||
/* Хранилище последнего принятого кадра — см. предупреждение в can.h про
|
||||
* время жизни p_out->p_data, возвращаемого sul_transport_can_receive(). */
|
||||
static bsp_can_frame_t s_last_frame;
|
||||
|
||||
/* Сентинел вне диапазона 0..15 — форсирует применение фильтров на первый
|
||||
* вызов sul_transport_can_set_address(), независимо от переданного адреса
|
||||
* (порт OLD_PROJECT msg_receiver_task: last_nku_address = 0xFFU). */
|
||||
static uint8_t s_last_applied_address = 0xFFU;
|
||||
|
||||
bsp_status_t sul_transport_can_init(void)
|
||||
{
|
||||
const bsp_can_config_t cfg = {.bitrate = 125000U}; /* см. OLD_PROJECT msg_receiver_task */
|
||||
|
||||
bsp_status_t st = bsp_can_init(&cfg);
|
||||
return bsp_can_init(&cfg);
|
||||
/* Фильтры не настраиваем здесь — вызывающий (sul_rx_task) обязан сразу
|
||||
* позвать sul_transport_can_set_address(), см. can.h. */
|
||||
}
|
||||
|
||||
bsp_status_t sul_transport_can_set_address(uint8_t nku_address)
|
||||
{
|
||||
const uint8_t ADDR = (nku_address <= NKU_ADDRESS_MAX) ? nku_address : NKU_ADDRESS_MAX;
|
||||
|
||||
if (ADDR == s_last_applied_address)
|
||||
{
|
||||
return BSP_OK; /* не менялось — переконфигурация MB не нужна */
|
||||
}
|
||||
|
||||
const uint32_t GROUP4 = (uint32_t) ADDR << 4U;
|
||||
const uint32_t GROUP6 = (uint32_t) ADDR << 6U;
|
||||
|
||||
bsp_status_t st = bsp_can_set_filter(0U, PACKET1_BASE | GROUP4, STD_ID_MASK, false);
|
||||
if (st != BSP_OK)
|
||||
{
|
||||
return st;
|
||||
}
|
||||
st = bsp_can_set_filter(1U, PACKET2_BASE | GROUP4, STD_ID_MASK, false);
|
||||
if (st != BSP_OK)
|
||||
{
|
||||
return st;
|
||||
}
|
||||
st = bsp_can_set_filter(2U, PACKET3_BASE | GROUP4, STD_ID_MASK, false);
|
||||
if (st != BSP_OK)
|
||||
{
|
||||
return st;
|
||||
}
|
||||
st = bsp_can_set_filter(3U, PACKET4_BASE | GROUP4, STD_ID_MASK, false);
|
||||
if (st != BSP_OK)
|
||||
{
|
||||
return st;
|
||||
}
|
||||
st = bsp_can_set_filter(4U, PACKET5_BASE | GROUP6, STD_ID_MASK, false);
|
||||
if (st != BSP_OK)
|
||||
{
|
||||
return st;
|
||||
}
|
||||
|
||||
st = bsp_can_set_filter(0U, PACKET1_ID, STD_ID_MASK, false);
|
||||
if (st != BSP_OK)
|
||||
{
|
||||
return st;
|
||||
}
|
||||
|
||||
/* Остальные MB (2..15) свободны под Фазу 2 (PACKET2/4/5, remote-address). */
|
||||
return bsp_can_set_filter(1U, PACKET3_ID, STD_ID_MASK, false);
|
||||
s_last_applied_address = ADDR;
|
||||
return BSP_OK;
|
||||
}
|
||||
|
||||
bsp_status_t sul_transport_can_receive(uint32_t timeout_ms, sul_frame_t *p_out)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
*
|
||||
* Навигация: BUTTON_1 → menu_next() (следующий пункт уровня, с заворотом);
|
||||
* короткое BUTTON_2 → menu_action() (вход в подменю / инкремент значения /
|
||||
* выход-с-сохранением). Вход в меню (долгое BUTTON_2) и модальность — уровень app.
|
||||
* выход-с-сохранением). Вход в меню (короткое BUTTON_1, вне меню) и модальность — уровень app.
|
||||
*
|
||||
* Соглашение: items[MENU_ROOT_INDEX] — корневое SUBMENU; его дети — верхний
|
||||
* уровень (их parent == MENU_ROOT_INDEX). Дети одного уровня — непрерывный
|
||||
|
|
|
|||
|
|
@ -23,4 +23,9 @@ set_source_files_properties(
|
|||
PROPERTIES
|
||||
COMPILE_OPTIONS "-include;${CMAKE_CURRENT_SOURCE_DIR}/fonts/include/fonts.h;-w")
|
||||
|
||||
target_link_libraries(tft_app_gfx PUBLIC bsp_display bsp_sdram sdk_common) # fsl_common.h — AT_NONCACHEABLE_SECTION_ALIGN
|
||||
# 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() (см. её тело) —
|
||||
# убрать вместе с диагностикой, когда причина лага индикации найдена.
|
||||
target_link_libraries(tft_app_gfx PUBLIC bsp_display bsp_sdram sdk_common PRIVATE sdk_pxp
|
||||
freertos_kernel port_log_uart)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
/**
|
||||
* @file gfx.h
|
||||
* @brief Фаза 1 — минимальный gfx: framebuffer (SDRAM, non-cacheable) +
|
||||
* RLE-шрифты (lcd-image-converter) + примитив стрелки. Без PXP/
|
||||
* компоновщика/альфа-слоёв — прямая запись в единственный framebuffer,
|
||||
* который ELCDIF сканирует по DMA (см. gfx.c про non-cacheable SDRAM).
|
||||
* @brief Фаза 3.2.4 — gfx как компоновщик: double-buffer + PXP.
|
||||
*
|
||||
* Модель (эталон OLD_PROJECT_TFT8_UKL/source/display/): CPU рисует ВЕСЬ кадр в
|
||||
* альфа-поверхность **AS** (`alpha_buffer`, ARGB8888; примитивы пишут alpha
|
||||
* 0xFF, `gfx_clear` обнуляет → прозрачно). PXP блендит AS над фоновой
|
||||
* поверхностью **PS** (`processing_buffer`, сейчас сплошной чёрный) в один из
|
||||
* двух задних framebuffer'ов, затем свап синхронно с ELCDIF (семафор FRAME_DONE).
|
||||
* Рисуем off-screen, показываем атомарным свапом → tear-free. Стиль-картинка в
|
||||
* PS и спрайты в AS — Фаза 4/5; сейчас PS чёрный, композиция = чёрный фон +
|
||||
* нарисованное в AS.
|
||||
*
|
||||
* Формат tImage/tChar/tFont и RLE-декодирование — порт проверенного в проде
|
||||
* алгоритма из OLD_PROJECT (source/fonts/fonts.c), тот же формат данных, что
|
||||
|
|
@ -73,18 +79,32 @@ typedef uint32_t gfx_color_t;
|
|||
#define GFX_COLOR_WHITE 0x00FFFFFFU
|
||||
|
||||
/**
|
||||
* @brief framebuffer (SDRAM non-cacheable) + bsp_display.
|
||||
* @brief Поднять компоновщик: AS/PS/2×FB (SDRAM non-cacheable) + PXP + ELCDIF.
|
||||
*
|
||||
* Создаёт семафор FRAME_DONE и регистрирует ISR-колбэк ELCDIF (даёт семафор),
|
||||
* заливает PS сплошным чёрным, инициализирует PXP (AS над PS → выходной FB) и
|
||||
* стартует ELCDIF на FB[0]. До первого gfx_present() экран чёрный.
|
||||
*
|
||||
* SDRAM (SEMC) должна быть уже поднята вызывающим (bsp_sdram_configure() +
|
||||
* bsp_sdram_init()) — gfx не владеет SEMC-инициализацией, только framebuffer
|
||||
* bsp_sdram_init()) — gfx не владеет SEMC-инициализацией, только буферами
|
||||
* внутри уже готовой SDRAM.
|
||||
*
|
||||
* @param type тип панели (Фаза 1 — хардкод из app; Фаза 9 — provisioning)
|
||||
*/
|
||||
bsp_status_t gfx_init(bsp_display_type_t type);
|
||||
|
||||
/** Залить весь кадр цветом (обычно GFX_COLOR_BLACK перед перерисовкой). */
|
||||
void gfx_clear(gfx_color_t color);
|
||||
/** Обнулить AS (весь кадр становится прозрачным). Вызывать перед отрисовкой
|
||||
* нового полного кадра; непрорисованные области покажут фон PS (чёрный). */
|
||||
void gfx_clear(void);
|
||||
|
||||
/**
|
||||
* @brief Показать нарисованный в AS кадр: PXP-композит AS над PS → задний FB,
|
||||
* затем свап синхронно с ELCDIF (tear-free).
|
||||
*
|
||||
* Блокирующий (busy-wait завершения PXP + ожидание FRAME_DONE) — звать из
|
||||
* задачи-владельца дисплея после того, как полный кадр нарисован в AS.
|
||||
*/
|
||||
void gfx_present(void);
|
||||
|
||||
/**
|
||||
* @brief Нарисовать строку заданным цветом (тинтинг с альфа-сглаживанием).
|
||||
|
|
|
|||
|
|
@ -1,47 +1,69 @@
|
|||
#include "services/gfx.h"
|
||||
|
||||
#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>
|
||||
|
||||
/* ── Framebuffer (SDRAM, non-cacheable) ────────────────────────────────────
|
||||
#define LOG_TAG "gfx"
|
||||
|
||||
/* ── Поверхности компоновщика (SDRAM, non-cacheable) ────────────────────────
|
||||
*
|
||||
* AT_NONCACHEABLE_SECTION_ALIGN размещает переменную в линкер-секции
|
||||
* NonCacheable/.ncache → cmake/linker/..._app_slot.ld отображает её на
|
||||
* m_sdram_ncache (2 МБ в начале SDRAM), а board_mpu_init() (bsp/generated/
|
||||
* m_sdram_ncache (8 МБ в начале SDRAM), а board_mpu_init() (bsp/generated/
|
||||
* board.c, Region 9) конфигурирует ЭТОТ ЖЕ диапазон как non-cacheable через
|
||||
* линкер-символы __NCACHE_REGION_START/SIZE — рекомендация NXP для буферов,
|
||||
* которые ELCDIF читает по DMA (см. OLD_PROJECT source/display/image_cache.c,
|
||||
* которые ELCDIF/PXP читают по DMA (см. OLD_PROJECT source/display/image_cache.c,
|
||||
* тот же макрос, framebuffer/alpha_buffer/processing_buffer). Без этого CPU
|
||||
* писал бы через Write-Back D-Cache (Region 8: SDRAM WB Cacheable), и ELCDIF
|
||||
* писал бы через Write-Back D-Cache (Region 8: SDRAM WB Cacheable), и DMA
|
||||
* читал бы устаревшие данные, пока кэш-линия не вытеснится сама.
|
||||
*
|
||||
* Размер — под ТЕКУЩУЮ панель стенда (TFT8, 800×600, известна на Фазе 1), не
|
||||
* под BSP_DISPLAY_MAX_*: когда app-big (ARCH §9) станет рантайм-выбирать
|
||||
* между TFT7/8/10 в одном бинарнике, размер и m_sdram_ncache (сейчас 2 МБ)
|
||||
* придётся поднять до максимума (1024×600×4 ≈ 2.34 МБ → 4 МБ регион). */
|
||||
#define FRAMEBUFFER_ALIGN 64U /* см. OLD_PROJECT FRAME_BUFFER_ALIGN — типичное ELCDIF/AXI выравнивание */
|
||||
#define FRAMEBUFFER_PIXELS (800U * 600U)
|
||||
* Четыре поверхности (эталон TFT8_UKL): AS (рисует CPU), PS (фон, чёрный),
|
||||
* FB[2] (выход PXP = вход ELCDIF, double buffer). Размер — под ТЕКУЩУЮ панель
|
||||
* стенда (TFT8, 800×600), не под BSP_DISPLAY_MAX_*: когда app-big (ARCH §9)
|
||||
* станет рантайм-выбирать между TFT7/8/10 в одном бинарнике, размер поверхностей
|
||||
* и m_sdram_ncache придётся поднять до максимума панели. */
|
||||
#define FRAMEBUFFER_ALIGN 64U /* см. OLD_PROJECT FRAME_BUFFER_ALIGN — типичное ELCDIF/PXP/AXI выравнивание */
|
||||
#define SURFACE_PIXELS (800U * 600U)
|
||||
#define BYTES_PER_PIXEL 4U
|
||||
#define ALPHA_OPAQUE 0xFF000000U /* AS: alpha=0xFF → пиксель непрозрачен для PXP-блендинга */
|
||||
#define FALLBACK_CHAR '-'
|
||||
|
||||
AT_NONCACHEABLE_SECTION_ALIGN(static uint32_t s_framebuffer[FRAMEBUFFER_PIXELS], FRAMEBUFFER_ALIGN);
|
||||
/* AS — CPU рисует сюда (ARGB8888, alpha значим). */
|
||||
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);
|
||||
|
||||
static uint16_t s_fb_width;
|
||||
static uint16_t s_fb_height;
|
||||
|
||||
static uint8_t s_back_index; /* индекс FB, в который PXP компонует следующий кадр */
|
||||
static SemaphoreHandle_t s_frame_done; /* даётся из ELCDIF ISR по завершении кадра */
|
||||
static pxp_output_buffer_config_t s_output_cfg; /* хранится: gfx_present меняет buffer0Addr */
|
||||
|
||||
static inline void set_pixel(uint16_t x, uint16_t y, gfx_color_t color)
|
||||
{
|
||||
if ((x >= s_fb_width) || (y >= s_fb_height))
|
||||
{
|
||||
return; /* примитивы могут частично выходить за экран — не UB, просто обрезка */
|
||||
}
|
||||
s_framebuffer[(uint32_t) y * s_fb_width + x] = color;
|
||||
/* AS: пишем непрозрачно (alpha 0xFF), RGB из color (X-байт игнорируем). */
|
||||
s_alpha_buffer[(uint32_t) y * s_fb_width + x] = (color & 0x00FFFFFFU) | ALPHA_OPAQUE;
|
||||
}
|
||||
|
||||
/* Альфа-блендинг: наложить @p color с покрытием @p a (0..255) на фон
|
||||
* framebuffer'а. a=0 — фон нетронут (прозрачный край глифа), a=255 — полная
|
||||
* замена. Так сглаживание глифа корректно ложится на любой фон (в т.ч.
|
||||
* полосу-курсор), а не штампует чёрный бокс. */
|
||||
/* Альфа-блендинг: наложить @p color с покрытием @p a (0..255) на текущее
|
||||
* содержимое AS. a=0 — пиксель нетронут (прозрачный край глифа), a=255 —
|
||||
* полная замена. Так сглаживание глифа корректно ложится на уже нарисованный
|
||||
* фон (в т.ч. полосу-курсор), а не штампует чёрный бокс. Результат всегда
|
||||
* непрозрачен (alpha 0xFF): PXP покажет его поверх PS. */
|
||||
static inline void blend_pixel(uint16_t x, uint16_t y, gfx_color_t color, uint8_t a)
|
||||
{
|
||||
if ((x >= s_fb_width) || (y >= s_fb_height) || (a == 0U))
|
||||
|
|
@ -52,16 +74,16 @@ static inline void blend_pixel(uint16_t x, uint16_t y, gfx_color_t color, uint8_
|
|||
const uint32_t idx = (uint32_t) y * s_fb_width + x;
|
||||
if (a == 0xFFU)
|
||||
{
|
||||
s_framebuffer[idx] = color;
|
||||
s_alpha_buffer[idx] = (color & 0x00FFFFFFU) | ALPHA_OPAQUE;
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t bg = s_framebuffer[idx];
|
||||
const uint32_t bg = s_alpha_buffer[idx];
|
||||
const uint32_t inv = 255U - a;
|
||||
const uint32_t r = (((color >> 16) & 0xFFU) * a + ((bg >> 16) & 0xFFU) * inv) / 255U;
|
||||
const uint32_t g = (((color >> 8) & 0xFFU) * a + ((bg >> 8) & 0xFFU) * inv) / 255U;
|
||||
const uint32_t b = (((color) & 0xFFU) * a + ((bg) & 0xFFU) * inv) / 255U;
|
||||
s_framebuffer[idx] = (r << 16) | (g << 8) | b;
|
||||
s_alpha_buffer[idx] = (r << 16) | (g << 8) | b | ALPHA_OPAQUE;
|
||||
}
|
||||
|
||||
/* ── Поиск глифа (бинарный — chars[] отсортирован по code, гарантия формата
|
||||
|
|
@ -302,13 +324,101 @@ void gfx_draw_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, gfx_color_t c
|
|||
}
|
||||
}
|
||||
|
||||
/* ── Framebuffer / init ──────────────────────────────────────────────────── */
|
||||
/* ── 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). */
|
||||
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 берётся из его альфа-байта. */
|
||||
const pxp_as_blend_config_t blend_cfg = {
|
||||
.alpha = 0xFFU,
|
||||
.invertAlpha = false,
|
||||
.alphaMode = kPXP_AlphaEmbedded,
|
||||
.ropMode = kPXP_RopMaskAs,
|
||||
};
|
||||
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 и дождаться завершения композиции (busy-wait, как в эталоне). */
|
||||
static void gfx_pxp_run(void)
|
||||
{
|
||||
PXP_ClearStatusFlags(PXP, kPXP_CommandLoadFlag);
|
||||
PXP_ClearStatusFlags(PXP, kPXP_Axi0ReadErrorFlag);
|
||||
PXP_ClearStatusFlags(PXP, kPXP_Axi0WriteErrorFlag);
|
||||
PXP_ClearStatusFlags(PXP, kPXP_CompleteFlag);
|
||||
|
||||
PXP_Start(PXP);
|
||||
while ((kPXP_CompleteFlag & PXP_GetStatusFlags(PXP)) == 0U)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/* ISR-safe: конец кадра ELCDIF → отпустить семафор (синхронизация свапа). */
|
||||
static void on_frame_done(void)
|
||||
{
|
||||
BaseType_t hp_task_woken = pdFALSE;
|
||||
(void) xSemaphoreGiveFromISR(s_frame_done, &hp_task_woken);
|
||||
portYIELD_FROM_ISR(hp_task_woken);
|
||||
}
|
||||
|
||||
/* ── Компоновщик / init ──────────────────────────────────────────────────── */
|
||||
|
||||
bsp_status_t gfx_init(bsp_display_type_t type)
|
||||
{
|
||||
/* SDRAM (SEMC) — забота вызывающего (bsp_sdram_configure()+init()), gfx
|
||||
* владеет только framebuffer'ом внутри уже готовой SDRAM. */
|
||||
const bsp_status_t st = bsp_display_init(type, (uint32_t) s_framebuffer, NULL);
|
||||
* владеет только поверхностями внутри уже готовой SDRAM. */
|
||||
s_frame_done = xSemaphoreCreateBinary();
|
||||
if (s_frame_done == NULL)
|
||||
{
|
||||
return BSP_ERR_INIT;
|
||||
}
|
||||
|
||||
/* PS — чёрный фон; AS — прозрачно; оба выходных FB — чёрные (ELCDIF стартует
|
||||
* на FB[0] ещё до первого gfx_present, иначе на экране был бы мусор). */
|
||||
(void) memset(s_processing_buffer, 0, sizeof(s_processing_buffer));
|
||||
(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);
|
||||
if (st != BSP_OK)
|
||||
{
|
||||
return st;
|
||||
|
|
@ -317,16 +427,47 @@ bsp_status_t gfx_init(bsp_display_type_t type)
|
|||
const bsp_display_size_t *p_size = bsp_display_get_size();
|
||||
s_fb_width = p_size->width;
|
||||
s_fb_height = p_size->height;
|
||||
s_back_index = 0U; /* FB[0] показывается; первый present уйдёт в FB[1] */
|
||||
|
||||
gfx_pxp_init();
|
||||
|
||||
return BSP_OK;
|
||||
}
|
||||
|
||||
void gfx_clear(gfx_color_t color)
|
||||
void gfx_clear(void)
|
||||
{
|
||||
const uint32_t count = (uint32_t) s_fb_width * s_fb_height;
|
||||
/* Прозрачно (alpha 0) → непрорисованные области покажут фон PS (чёрный). */
|
||||
(void) memset(s_alpha_buffer, 0, (size_t) s_fb_width * s_fb_height * BYTES_PER_PIXEL);
|
||||
}
|
||||
|
||||
for (uint32_t i = 0U; i < count; i++)
|
||||
void gfx_present(void)
|
||||
{
|
||||
s_framebuffer[i] = color;
|
||||
}
|
||||
/* Компонуем в НЕ показываемый сейчас буфер, показываем атомарным свапом. */
|
||||
s_back_index ^= 1U;
|
||||
|
||||
s_output_cfg.buffer0Addr = (uint32_t) s_framebuffer[s_back_index];
|
||||
PXP_SetOutputBufferConfig(PXP, &s_output_cfg);
|
||||
|
||||
/* ВРЕМЕННО (Фаза 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();
|
||||
|
||||
/* Синхронизация с развёрткой: дождаться конца кадра, затем отдать ELCDIF
|
||||
* новый буфер — он переключится аппаратно на границе кадра (tear-free). */
|
||||
(void) xSemaphoreTake(s_frame_done, portMAX_DELAY);
|
||||
|
||||
const TickType_t T2 = xTaskGetTickCount();
|
||||
|
||||
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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,15 +14,17 @@ void settings_store_init_defaults(void)
|
|||
|
||||
bsp_status_t settings_store_load(void)
|
||||
{
|
||||
settings_page_t page;
|
||||
/* static: страница = сектор (4 КБ) — на стеке задачи (~3 КБ) переполнит его.
|
||||
* Однократный вызов при старте, не реентерабельно (как s_page в save()). */
|
||||
static settings_page_t s_page;
|
||||
|
||||
if (bsp_qspi_read(TFT_APP_QSPI_SETTINGS_OFFSET, (uint8_t *) &page, sizeof(page)) != BSP_OK)
|
||||
if (bsp_qspi_read(TFT_APP_QSPI_SETTINGS_OFFSET, (uint8_t *) &s_page, sizeof(s_page)) != BSP_OK)
|
||||
{
|
||||
g_settings = settings_defaults();
|
||||
return BSP_ERR_HW;
|
||||
}
|
||||
|
||||
if (!settings_deserialize(&page, &g_settings))
|
||||
if (!settings_deserialize(&s_page, &g_settings))
|
||||
{
|
||||
g_settings = settings_defaults();
|
||||
return BSP_ERR_INVALID;
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ static void render_normal(const sul_result_t *p_result)
|
|||
|
||||
static void render(sul_mode_t mode, const sul_result_t *p_result)
|
||||
{
|
||||
gfx_clear(GFX_COLOR_BLACK);
|
||||
gfx_clear();
|
||||
|
||||
const char *label = mode_label(mode);
|
||||
if (label == NULL)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@
|
|||
* заголовок уровня, список пунктов с полосой-курсором, значения, футер-подсказка.
|
||||
* Читает только модель (menu_ctx_t) через её запросы; логики навигации не содержит.
|
||||
* HIL — проверяется на железе.
|
||||
*
|
||||
* Рисует ПОЛНЫЙ кадр в AS (double-buffer + PXP, Фаза 3.2.4) — инкрементальной
|
||||
* построчной отрисовки больше нет: off-screen перерисовка полного кадра +
|
||||
* атомарный свап дёшевы и tear-free. Показ — gfx_present() у владельца дисплея.
|
||||
*/
|
||||
|
||||
#ifndef UI_MENU_VIEW_H_
|
||||
|
|
@ -18,7 +22,11 @@ extern "C"
|
|||
{
|
||||
#endif
|
||||
|
||||
/** Отрисовать текущий кадр меню по состоянию модели. Меню должно быть открыто. */
|
||||
/**
|
||||
* @brief Отрисовать полный кадр меню в AS (очистка + заголовок + строки + футер).
|
||||
*
|
||||
* Рисует off-screen (AS); на экран попадёт после gfx_present() владельца дисплея.
|
||||
*/
|
||||
void menu_view_render(const menu_ctx_t *p_ctx);
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
|
|
|||
|
|
@ -7,17 +7,23 @@
|
|||
/* Окно 480×272 @ (0,0). Метрики под реальные шрифты: JBMono24 h=31 (заголовок/
|
||||
* строки), JBMono12 h=16 (футер). 36 + 6×36 + 20 = 272. */
|
||||
#define WIN_W 480U
|
||||
#define WIN_H 272U
|
||||
#define TITLE_H 36U
|
||||
#define ROW_H 36U
|
||||
#define FOOTER_H 20U
|
||||
#define ROWS_Y0 TITLE_H
|
||||
#define FOOTER_Y (272U - FOOTER_H) /* 252 */
|
||||
#define TEXT_DY24 2U /* (36-31)/2 — центрирование JBMono24 в полосе 36 */
|
||||
#define TEXT_DY12 2U /* (20-16)/2 — JBMono12 в футере 20 */
|
||||
#define FOOTER_Y (WIN_H - FOOTER_H) /* 252 */
|
||||
#define TEXT_DY24 2U /* центрирование JBMono24 (h31) в полосе 36 */
|
||||
#define TEXT_DY12 2U /* JBMono12 (h16) в футере 20 */
|
||||
#define LEFT_MARGIN 16U
|
||||
#define RIGHT_MARGIN 16U
|
||||
#define EDGE_MARGIN 8U
|
||||
|
||||
/* Рамка 1 px по периметру — строки/футер вписаны внутрь неё (не затирают). */
|
||||
#define BORDER 1U
|
||||
#define CONTENT_X BORDER
|
||||
#define CONTENT_W (WIN_W - 2U * BORDER)
|
||||
|
||||
/* Палитра (XRGB). Собирается тинтингом из белых шрифтов. */
|
||||
#define COL_BG 0x000000U
|
||||
#define COL_TITLE 0x00CFE0FFU
|
||||
|
|
@ -26,7 +32,7 @@
|
|||
#define COL_SEL_BG 0x0017406BU
|
||||
#define COL_SEL_TEXT 0x00FFFFFFU
|
||||
#define COL_FOOTER 0x006F6F6FU
|
||||
#define COL_SEP 0x00333333U
|
||||
#define COL_SEP 0x00333333U /* разделители + рамка */
|
||||
|
||||
/* Строка значения пункта. @return false — у пункта нет значения (BACK). */
|
||||
static bool format_value(const menu_ctx_t *p_ctx, uint8_t idx, char *p_buf, size_t buf_len)
|
||||
|
|
@ -39,7 +45,7 @@ static bool format_value(const menu_ctx_t *p_ctx, uint8_t idx, char *p_buf, size
|
|||
(void) snprintf(p_buf, buf_len, ">");
|
||||
return true;
|
||||
case MENU_BYTE:
|
||||
(void) snprintf(p_buf, buf_len, "%u", menu_read_value(p_ctx, idx));
|
||||
(void) snprintf(p_buf, buf_len, "%u", (unsigned) menu_read_value(p_ctx, idx));
|
||||
return true;
|
||||
case MENU_SELECT:
|
||||
case MENU_BOOL:
|
||||
|
|
@ -51,7 +57,7 @@ static bool format_value(const menu_ctx_t *p_ctx, uint8_t idx, char *p_buf, size
|
|||
}
|
||||
else
|
||||
{
|
||||
(void) snprintf(p_buf, buf_len, "%u", V);
|
||||
(void) snprintf(p_buf, buf_len, "%u", (unsigned) V);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -61,14 +67,14 @@ static bool format_value(const menu_ctx_t *p_ctx, uint8_t idx, char *p_buf, size
|
|||
}
|
||||
}
|
||||
|
||||
/* Отрисовать одну строку по её экранному y (fill фона + подпись + значение).
|
||||
* Заливает CONTENT_X..CONTENT_W — рамку (x=0, x=WIN_W-1) не трогает. */
|
||||
static void draw_row(const menu_ctx_t *p_ctx, uint8_t idx, uint16_t row_y, bool selected)
|
||||
{
|
||||
if (selected)
|
||||
{
|
||||
gfx_fill_rect(0U, row_y, WIN_W, ROW_H, COL_SEL_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;
|
||||
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,
|
||||
(uint16_t) (row_y + TEXT_DY24), TEXT_COL);
|
||||
|
||||
|
|
@ -84,9 +90,9 @@ static void draw_row(const menu_ctx_t *p_ctx, uint8_t idx, uint16_t row_y, bool
|
|||
|
||||
static void draw_footer(const menu_ctx_t *p_ctx, uint8_t level_first, uint8_t level_last)
|
||||
{
|
||||
gfx_fill_rect(0U, (uint16_t) (FOOTER_Y - 1U), WIN_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 - выбор", EDGE_MARGIN,
|
||||
(void) gfx_draw_string(&SystemFontSmall, "Кн.1 - далее Кн.2 - выбор", LEFT_MARGIN,
|
||||
(uint16_t) (FOOTER_Y + TEXT_DY12), COL_FOOTER);
|
||||
|
||||
const uint8_t TOTAL = (uint8_t) (level_last - level_first + 1U);
|
||||
|
|
@ -100,8 +106,10 @@ 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)
|
||||
{
|
||||
/* Меню модально и переносимо: чёрный весь экран, окно — верхний-левый 480×272. */
|
||||
gfx_clear(COL_BG);
|
||||
/* Полный кадр off-screen: обнулить AS (вне окна 480×272 → чёрный фон PS),
|
||||
* затем нарисовать окно. Свап — gfx_present() у владельца дисплея. */
|
||||
gfx_clear();
|
||||
|
||||
if (!p_ctx->open)
|
||||
{
|
||||
return;
|
||||
|
|
@ -111,9 +119,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);
|
||||
gfx_fill_rect(0U, (uint16_t) (TITLE_H - 1U), WIN_W, 1U, COL_SEP);
|
||||
gfx_fill_rect(CONTENT_X, (uint16_t) (TITLE_H - 1U), CONTENT_W, 1U, COL_SEP);
|
||||
|
||||
/* Список пунктов текущего уровня, окно страницы. */
|
||||
uint8_t first;
|
||||
uint8_t last;
|
||||
menu_level_range(p_ctx, &first, &last);
|
||||
|
|
@ -126,9 +133,11 @@ void menu_view_render(const menu_ctx_t *p_ctx)
|
|||
{
|
||||
break;
|
||||
}
|
||||
const uint16_t ROW_Y = (uint16_t) (ROWS_Y0 + slot * ROW_H);
|
||||
draw_row(p_ctx, IDX, ROW_Y, (IDX == p_ctx->cur));
|
||||
draw_row(p_ctx, IDX, (uint16_t) (ROWS_Y0 + slot * ROW_H), (IDX == p_ctx->cur));
|
||||
}
|
||||
|
||||
draw_footer(p_ctx, first, last);
|
||||
|
||||
/* Рамка — последней, поверх содержимого. */
|
||||
gfx_draw_rect(0U, 0U, WIN_W, WIN_H, COL_SEP);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@
|
|||
* @note Вызов LOG_* из ISR запрещён: xSemaphoreTake с portMAX_DELAY
|
||||
* недопустим в ISR-контексте.
|
||||
*/
|
||||
#if 0
|
||||
#include "FreeRTOS.h"
|
||||
#include "log/log.h"
|
||||
#include "semphr.h"
|
||||
|
|
@ -37,4 +36,3 @@ void log_mutex_unlock(void)
|
|||
{
|
||||
xSemaphoreGive(s_log_mutex);
|
||||
}
|
||||
#endif
|
||||
|
|
@ -56,6 +56,7 @@ add_sdk_driver(lpi2c fsl_lpi2c.c)
|
|||
add_sdk_driver(pwm fsl_pwm.c)
|
||||
add_sdk_driver(adc fsl_adc.c)
|
||||
add_sdk_driver(elcdif fsl_elcdif.c)
|
||||
add_sdk_driver(pxp fsl_pxp.c)
|
||||
add_sdk_driver(cache fsl_cache.c)
|
||||
add_sdk_driver(sai fsl_sai.c)
|
||||
add_sdk_driver(sai_edma fsl_sai_edma.c)
|
||||
|
|
@ -73,6 +74,7 @@ target_link_libraries(sdk_lpi2c PUBLIC sdk_clock)
|
|||
target_link_libraries(sdk_pwm PUBLIC sdk_clock)
|
||||
target_link_libraries(sdk_adc PUBLIC sdk_clock)
|
||||
target_link_libraries(sdk_elcdif PUBLIC sdk_clock)
|
||||
target_link_libraries(sdk_pxp PUBLIC sdk_clock) # PXP_Init → CLOCK_EnableClock(kCLOCK_Pxp)
|
||||
target_link_libraries(sdk_sai PUBLIC sdk_clock)
|
||||
target_link_libraries(sdk_sai_edma PUBLIC sdk_sai sdk_edma)
|
||||
target_link_libraries(sdk_edma PUBLIC sdk_common)
|
||||
|
|
|
|||
Loading…
Reference in a new issue