# tft_app: Phase 1
This commit is contained in:
parent
a9e668196a
commit
7f3170a7c0
36 changed files with 11693 additions and 67 deletions
|
|
@ -168,7 +168,9 @@
|
||||||
"test_slot_version",
|
"test_slot_version",
|
||||||
"test_update_policy",
|
"test_update_policy",
|
||||||
"test_recovery",
|
"test_recovery",
|
||||||
"test_tft_app_smoke"
|
"test_tft_app_smoke",
|
||||||
|
"test_sul_nku_can",
|
||||||
|
"test_tft_app_controller"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -192,7 +194,9 @@
|
||||||
"test_slot_version",
|
"test_slot_version",
|
||||||
"test_update_policy",
|
"test_update_policy",
|
||||||
"test_recovery",
|
"test_recovery",
|
||||||
"test_tft_app_smoke"
|
"test_tft_app_smoke",
|
||||||
|
"test_sul_nku_can",
|
||||||
|
"test_tft_app_controller"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -45,14 +45,22 @@ MEMORY
|
||||||
m_text (RX) : ORIGIN = SLOT_BASE + IMGTOOL_HDR_SZ + VECTOR_SIZE, LENGTH = SLOT_SIZE - IMGTOOL_HDR_SZ - VECTOR_SIZE
|
m_text (RX) : ORIGIN = SLOT_BASE + IMGTOOL_HDR_SZ + VECTOR_SIZE, LENGTH = SLOT_SIZE - IMGTOOL_HDR_SZ - VECTOR_SIZE
|
||||||
m_qacode (RX) : ORIGIN = 0x00000000, LENGTH = 0x00020000 /* SRAM_ITC 128 КБ — ramfunc */
|
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_data (RW) : ORIGIN = 0x20000000, LENGTH = 0x00020000 /* SRAM_DTC 128 КБ — .data/.bss/heap/stack */
|
||||||
m_data2 (RW) : ORIGIN = 0x20200000, LENGTH = 0x00040000 /* SRAM_OC 256 КБ — ncache */
|
m_data2 (RW) : ORIGIN = 0x20200000, LENGTH = 0x00040000 /* SRAM_OC 256 КБ */
|
||||||
m_sdram (RW) : ORIGIN = 0x80000000, LENGTH = 0x02000000 /* SDRAM 32 МБ — Фаза 0 не используется */
|
/* 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 не использует */
|
||||||
}
|
}
|
||||||
|
|
||||||
SECTIONS
|
SECTIONS
|
||||||
{
|
{
|
||||||
__NCACHE_REGION_START = ORIGIN(m_data2);
|
__NCACHE_REGION_START = ORIGIN(m_sdram_ncache);
|
||||||
__NCACHE_REGION_SIZE = 0x2000; /* 8 КБ non-cacheable */
|
__NCACHE_REGION_SIZE = LENGTH(m_sdram_ncache); /* 2 МБ non-cacheable — framebuffer */
|
||||||
|
|
||||||
/* Vector table (imgtool header зарезервирован до неё в MEMORY). */
|
/* Vector table (imgtool header зарезервирован до неё в MEMORY). */
|
||||||
.interrupts :
|
.interrupts :
|
||||||
|
|
@ -175,6 +183,10 @@ SECTIONS
|
||||||
} > m_qacode
|
} > m_qacode
|
||||||
|
|
||||||
__NDATA_ROM = __ram_function_flash_start + (__ram_function_end__ - __ram_function_start__);
|
__NDATA_ROM = __ram_function_flash_start + (__ram_function_end__ - __ram_function_start__);
|
||||||
|
/* framebuffer (gfx, AT_NONCACHEABLE_SECTION_ALIGN, без инициализатора) —
|
||||||
|
* в .ncache (bare/zero-init), НЕ .ncache.init (copy-from-flash, сейчас
|
||||||
|
* ничего не использует, оставлена под будущие initialized non-cacheable
|
||||||
|
* буферы). Обе — в m_sdram_ncache, см. MEMORY выше. */
|
||||||
.ncache.init :
|
.ncache.init :
|
||||||
{
|
{
|
||||||
. = ALIGN(32);
|
. = ALIGN(32);
|
||||||
|
|
@ -182,20 +194,21 @@ SECTIONS
|
||||||
*(NonCacheable.init)
|
*(NonCacheable.init)
|
||||||
. = ALIGN(4);
|
. = ALIGN(4);
|
||||||
__noncachedata_init_end__ = .;
|
__noncachedata_init_end__ = .;
|
||||||
} > m_data2
|
} > m_sdram_ncache
|
||||||
. = __noncachedata_init_end__;
|
. = __noncachedata_init_end__;
|
||||||
.ncache :
|
.ncache :
|
||||||
{
|
{
|
||||||
*(NonCacheable)
|
*(NonCacheable)
|
||||||
. = ALIGN(4);
|
. = ALIGN(4);
|
||||||
__noncachedata_end__ = .;
|
__noncachedata_end__ = .;
|
||||||
} > m_data2
|
} > m_sdram_ncache
|
||||||
|
|
||||||
__DATA_END = __NDATA_ROM;
|
__DATA_END = __NDATA_ROM;
|
||||||
text_end = ORIGIN(m_text) + LENGTH(m_text);
|
text_end = ORIGIN(m_text) + LENGTH(m_text);
|
||||||
ASSERT(__DATA_END <= text_end, "region m_text overflowed with text and data")
|
ASSERT(__DATA_END <= text_end, "region m_text overflowed with text and data")
|
||||||
ASSERT(text_end <= SLOT_BASE + SLOT_SIZE, "app вышел за пределы слота (2 МБ)")
|
ASSERT(text_end <= SLOT_BASE + SLOT_SIZE, "app вышел за пределы слота (2 МБ)")
|
||||||
ASSERT((__noncachedata_end__ - ORIGIN(m_data2)) <= LENGTH(m_data2), "m_data2 ncache overflow")
|
ASSERT((__noncachedata_end__ - ORIGIN(m_sdram_ncache)) <= LENGTH(m_sdram_ncache),
|
||||||
|
"m_sdram_ncache overflow — framebuffer(ы) не влезли в 2 МБ")
|
||||||
|
|
||||||
.bss :
|
.bss :
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,14 @@ target_link_libraries(freertos_kernel PUBLIC sdk_device)
|
||||||
# Вендоренное ядро — не наш стиль/warnings.
|
# Вендоренное ядро — не наш стиль/warnings.
|
||||||
target_compile_options(freertos_kernel PRIVATE -w)
|
target_compile_options(freertos_kernel PRIVATE -w)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Доменные/сервисные/презентационные слои — каждый своя статическая либа
|
||||||
|
# (ARCH.md §3, конвенция bsp/<module>). tft_app_* префикс — коллизии имён.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
add_subdirectory(src/domain)
|
||||||
|
add_subdirectory(src/services)
|
||||||
|
add_subdirectory(src/ui)
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# app — исполняемый образ слота
|
# app — исполняемый образ слота
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
@ -70,11 +78,15 @@ target_include_directories(
|
||||||
|
|
||||||
# __STARTUP_INITIALIZE_RAMFUNCTION — startup копирует .ram_function из flash в
|
# __STARTUP_INITIALIZE_RAMFUNCTION — startup копирует .ram_function из flash в
|
||||||
# ITCM. БЕЗ него ramfunc-код bsp_qspi (AT_QUICKACCESS_SECTION_CODE) в ITCM не
|
# ITCM. БЕЗ него ramfunc-код bsp_qspi (AT_QUICKACCESS_SECTION_CODE) в ITCM не
|
||||||
# попадает → запись QSPI виснет (см. bsp/qspi_flash/README.md). Без USB CDC
|
# попадает → запись QSPI виснет (см. bsp/qspi_flash/README.md).
|
||||||
# __STARTUP_INITIALIZE_NONCACHEDATA не нужен — ни один линкуемый модуль не
|
# __STARTUP_INITIALIZE_NONCACHEDATA — аналогично для .ncache/.ncache.init:
|
||||||
# кладёт данные в NonCacheable/NonCacheable.init (проверено).
|
# framebuffer (services/gfx, AT_NONCACHEABLE_SECTION_ALIGN) размещён в SDRAM
|
||||||
|
# non-cacheable регионе (cmake/linker/..._app_slot.ld, m_sdram_ncache) — по
|
||||||
|
# рекомендации NXP для буферов, которые ELCDIF читает по DMA (см.
|
||||||
|
# bsp/sdram/README.md и OLD_PROJECT/source/display/image_cache.c).
|
||||||
target_compile_definitions(
|
target_compile_definitions(
|
||||||
app PRIVATE APP_OWN_SLOT_ID=0 __STARTUP_CLEAR_BSS __STARTUP_INITIALIZE_RAMFUNCTION)
|
app PRIVATE APP_OWN_SLOT_ID=0 __STARTUP_CLEAR_BSS __STARTUP_INITIALIZE_RAMFUNCTION
|
||||||
|
__STARTUP_INITIALIZE_NONCACHEDATA)
|
||||||
|
|
||||||
# Вендоренный bootutil_public.c — под своими warnings (как в bootloader/stub).
|
# Вендоренный bootutil_public.c — под своими warnings (как в bootloader/stub).
|
||||||
set_source_files_properties(
|
set_source_files_properties(
|
||||||
|
|
@ -97,9 +109,16 @@ target_link_libraries(
|
||||||
bsp_boot_state
|
bsp_boot_state
|
||||||
bsp_qspi_flash
|
bsp_qspi_flash
|
||||||
bsp_uart_host
|
bsp_uart_host
|
||||||
|
bsp_sdram
|
||||||
bsp_boot_xip_no_dcd
|
bsp_boot_xip_no_dcd
|
||||||
freertos_kernel
|
freertos_kernel
|
||||||
port_log_uart) # UART диагностика (utils_log + bsp_uart_host, LPUART1/MCU-Link VCOM)
|
port_log_uart # UART диагностика (utils_log + bsp_uart_host, LPUART1/MCU-Link VCOM)
|
||||||
|
# Фаза 1: walking skeleton (CAN → декодер → контроллер → fallback-рендер)
|
||||||
|
tft_app_sul
|
||||||
|
tft_app_sul_transport_can
|
||||||
|
tft_app_controller
|
||||||
|
tft_app_gfx
|
||||||
|
tft_app_ui_fallback)
|
||||||
|
|
||||||
target_link_options(
|
target_link_options(
|
||||||
app
|
app
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
| # | Фаза | Статус |
|
| # | Фаза | Статус |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 0 | Каркас проекта и сборка | ✅ |
|
| 0 | Каркас проекта и сборка | ✅ |
|
||||||
| 1 | Walking skeleton (CAN → модель → этаж + стрелки) | ⬜ |
|
| 1 | Walking skeleton (CAN → модель → этаж + стрелки) | ✅ |
|
||||||
| 2 | Домен: полный `sul_result_t`, приоритеты, таймаут | ⬜ |
|
| 2 | Домен: полный `sul_result_t`, приоритеты, таймаут | ⬜ |
|
||||||
| 3 | Настройки и меню, локальные входы (opto) | ⬜ |
|
| 3 | Настройки и меню, локальные входы (opto) | ⬜ |
|
||||||
| 4 | Ассеты: TLV-бандл, паковщик, XIP-блит | ⬜ |
|
| 4 | Ассеты: TLV-бандл, паковщик, XIP-блит | ⬜ |
|
||||||
|
|
@ -104,7 +104,53 @@ heartbeat; host-тест-харнесс на месте. Пустой, но жи
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Фаза 1 — Walking skeleton ⬜
|
## Фаза 1 — Walking skeleton ✅
|
||||||
|
|
||||||
|
> **Аппаратно подтверждено.** Реальный НКУ-CAN трафик на стенде (TFT8) → корректный номер
|
||||||
|
> этажа и стрелка направления на экране; self-confirm пережил очередной power cycle
|
||||||
|
> (`image_ok=1` уже на `before-confirm`). Host-тесты 19/19 (16 декодер + 6 контроллер +
|
||||||
|
> существующие). Debug и Release оба собираются/подписываются чисто, идентичная структура
|
||||||
|
> трейлера (`image_ok` UNSET до рантайм-confirm, как и в Фазе 0). Работа Фазы 1 не
|
||||||
|
> закоммичена (в рабочем дереве поверх коммита `# tft_app: Phase 0`).
|
||||||
|
>
|
||||||
|
> ⚠️ **Известное ограничение — зафиксировано, не блокирует, чинится в Фазе 4.** Один
|
||||||
|
> framebuffer, без double buffering: `ui_fallback_render()` пишет (`gfx_clear` + RLE-decode
|
||||||
|
> глифов + стрелка) напрямую в буфер, который ELCDIF в этот момент сканирует по DMA. Если
|
||||||
|
> перерисовка не укладывается в один кадр развёртки, видно сам процесс закраски (глиф
|
||||||
|
> «набирается» по частям за несколько кадров) — не tearing/не порча пикселей (каждая запись
|
||||||
|
> валидна), просто видимый процесс. Фикс — double buffering (не требует PXP отдельно;
|
||||||
|
> PXP — для многослойной композиции реальных ассетов, та же Фаза 4) — см. её раздел.
|
||||||
|
>
|
||||||
|
> Находки по ходу реализации:
|
||||||
|
> - **Root-cause не пойман, найден заранее.** Framebuffer в SDRAM обязан идти через
|
||||||
|
> `AT_NONCACHEABLE_SECTION_ALIGN` (порт из `OLD_PROJECT image_cache.c`, прямая
|
||||||
|
> рекомендация NXP) — MPU держит SDRAM как `Normal Write-Back Cacheable`
|
||||||
|
> (`board_mpu_init()`, Region 8), ELCDIF читает по DMA напрямую; без non-cacheable
|
||||||
|
> региона CPU писал бы через кэш, DMA видел бы устаревшие данные. Механизм уже
|
||||||
|
> параметризован в `board_mpu_init()` через линкер-символы `__NCACHE_REGION_START/
|
||||||
|
> SIZE` (Region 9) — только переопределены в `app_slot.ld` на новый `m_sdram_ncache`
|
||||||
|
> (2 МБ в начале SDRAM) вместо унаследованного из bootloader OCRAM-региона (256 КБ,
|
||||||
|
> для framebuffer ~1.83 МБ не подошёл бы). При первой попытке framebuffer был сырым
|
||||||
|
> указателем на константный адрес (совпадал с началом региона случайно, линкер
|
||||||
|
> репортил `0 B` в `m_sdram_ncache`) — исправлено на настоящее объявление
|
||||||
|
> переменной через макрос; линкер теперь честно показывает `1875 KB / 2 MB`.
|
||||||
|
> - Формат шрифта — RLE ARGB8888 (lcd-image-converter, "Color A8R8G8B8"),
|
||||||
|
> `tImage`/`tChar`/`tFont`. Пользователь сгенерировал `FloorFontFallback.c`
|
||||||
|
> (Inter 215pt, 0-9 и "-") и `SystemFont.c` (JetBrains Mono 24pt, ASCII+кириллица)
|
||||||
|
> инструментом напрямую — не пришлось изобретать плейсхолдер. RLE-декодер в
|
||||||
|
> `services/gfx/gfx.c` — порт проверенного в проде алгоритма из
|
||||||
|
> `OLD_PROJECT/source/fonts/fonts.c` (UNIQUE/REPEATABLE блоки, бинарный поиск по
|
||||||
|
> отсортированному коду символа), не переизобретён с нуля. Символ вне таблицы
|
||||||
|
> шрифта → подстановка `'-'` (по-символьный fallback, ARCH §11 — политика
|
||||||
|
> подтверждена пользователем).
|
||||||
|
> - Файлы сгенерированных шрифтов ожидают типы из bare-name `fonts.h`
|
||||||
|
> (`SystemFont.c` — явным `#include`, `FloorFontFallback.c` — вообще без include,
|
||||||
|
> более новая версия конвертера) — закрыто force-include в CMake, сами
|
||||||
|
> сгенерированные файлы не редактировались (конвертер их перезапишет при
|
||||||
|
> регенерации шрифта).
|
||||||
|
> - Перенёс `FloorFontFallback.c/.xml`, `SystemFont.c` из `domain/` (где их положил
|
||||||
|
> пользователь) в `services/gfx/fonts/` — по ARCH §4 шрифты относятся к gfx, не
|
||||||
|
> к домену.
|
||||||
|
|
||||||
**Цель.** Сквозной срез: реальный CAN-кадр НКУ → чистый декодер → контроллер → **fallback-рендер
|
**Цель.** Сквозной срез: реальный CAN-кадр НКУ → чистый декодер → контроллер → **fallback-рендер
|
||||||
этажа и стрелки** на экране. Никаких ассетов, FS, TLV, меню.
|
этажа и стрелки** на экране. Никаких ассетов, FS, TLV, меню.
|
||||||
|
|
@ -125,7 +171,10 @@ heartbeat; host-тест-харнесс на месте. Пустой, но жи
|
||||||
таймаут→default.
|
таймаут→default.
|
||||||
|
|
||||||
**Критерий выхода.** На стенде: подан НКУ-CAN трафик → на экране меняется номер этажа и стрелка
|
**Критерий выхода.** На стенде: подан НКУ-CAN трафик → на экране меняется номер этажа и стрелка
|
||||||
направления; при пропадании трафика — «--». Домен-тесты зелёные в CI.
|
направления; при пропадании трафика — «--». Домен-тесты зелёные в CI. ✅ Выполнено (проверено на
|
||||||
|
TFT8; таймаут→«--» логически проверен host-тестами `test_feeding_default_state_after_real_data_
|
||||||
|
marks_both_pending`, аппаратно на реальном обрыве связи отдельно не гонялся — низкий риск, тот же
|
||||||
|
код-путь, что и для обычного кадра).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -169,6 +218,11 @@ per-protocol настроек (§8); `menu/` (навигация, редакти
|
||||||
|
|
||||||
**Объём.** Формат TLV (индекс, per-entry CRC, версия); host-утилита `tools/` (PNG→ARGB8888, WAV,
|
**Объём.** Формат TLV (индекс, per-entry CRC, версия); host-утилита `tools/` (PNG→ARGB8888, WAV,
|
||||||
пак); `services/assets` (TLV-ридер, XIP-mmap, image cache); partition-заголовок (§10).
|
пак); `services/assets` (TLV-ридер, XIP-mmap, image cache); partition-заголовок (§10).
|
||||||
|
**Double buffering в `gfx`** — второй framebuffer (`m_sdram_ncache` 2→4 МБ), swap через
|
||||||
|
`bsp_display_set_next_buffer()` + семафор на `FRAME_DONE` (сейчас `gfx_init()` передаёт `NULL`
|
||||||
|
вместо колбэка). Закрывает известное ограничение Фазы 1 (см. её раздел) — рисуем в задний буфер,
|
||||||
|
переключаем только когда кадр готов целиком. PXP-компоновщик (многослойная композиция для
|
||||||
|
реальных спрайтов из этой же фазы) — следом, тот же `gfx`.
|
||||||
|
|
||||||
**Тесты (host).** Раунд-трип паковщик↔ридер; целостность индекса/CRC; кросс-валидация id.
|
**Тесты (host).** Раунд-трип паковщик↔ридер; целостность индекса/CRC; кросс-валидация id.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,57 +1,47 @@
|
||||||
/**
|
/**
|
||||||
* @file main.c
|
* @file main.c
|
||||||
* @brief tft_app — точка входа. Фаза 0 (каркас): FreeRTOS + heartbeat + WDOG +
|
* @brief tft_app — точка входа.
|
||||||
* рантайм self-confirm слота MCUboot + UART диагностика.
|
|
||||||
*
|
*
|
||||||
* Каркас walking-skeleton: образ линкуется как валидный MCUboot-слот
|
* Фаза 0 (каркас, подтверждено на железе): образ линкуется как валидный
|
||||||
* (Direct-XIP), bootloader в него прыгает, FreeRTOS стартует, LED мигает,
|
* MCUboot-слот (Direct-XIP), bootloader в него прыгает, FreeRTOS стартует,
|
||||||
* WDOG кормится, образ ПОДТВЕРЖДАЕТ СЕБЯ в рантайме (нет revert на повторной
|
* WDOG кормится, образ ПОДТВЕРЖДАЕТ СЕБЯ в рантайме (нет revert на повторной
|
||||||
* загрузке — anti-brick сохранён). Бизнес-логика — со следующих фаз (PLAN.md).
|
* загрузке — anti-brick сохранён). Контракт с bootloader и обоснование
|
||||||
|
* self-confirm/QSPI-ramfunc/UART-диагностики — см. PLAN.md, Фаза 0.
|
||||||
*
|
*
|
||||||
* Контракт со стороны bootloader (см. firmware/bootloader/test_stub/main.c):
|
* Фаза 1 (walking skeleton): реальный CAN-кадр НКУ-CAN → чистый декодер →
|
||||||
* - WDOG взведён загрузчиком, WDE write-once — образ ОБЯЗАН кормить его.
|
* контроллер → fallback-рендер (позиция + стрелка). Две задачи:
|
||||||
* - Образ подписан imgtool БЕЗ --confirm → обязан подтвердить себя рантаймом
|
* - sul_rx_task — WDOG/heartbeat (унаследовано от Фазы 0) + приём CAN +
|
||||||
* (boot_set_next на своём слоте), иначе MCUboot откатит его.
|
* decode + controller_process(); на изменение — в очередь render_task.
|
||||||
*
|
* - render_task — потребляет очередь, зовёт ui_fallback_render().
|
||||||
* Записью QSPI из app под Direct-XIP можно пользоваться только потому, что
|
* Очередь глубиной 1 с xQueueOverwrite() — важно только ПОСЛЕДНЕЕ состояние,
|
||||||
* ramfunc-код bsp_qspi реально копируется в ITCM — для этого потребитель
|
* не история промежуточных кадров (render не обязан успевать за каждым).
|
||||||
* ОБЯЗАН задать __STARTUP_INITIALIZE_RAMFUNCTION (см. CMakeLists и
|
|
||||||
* bsp/qspi_flash/README.md). Без него запись виснет в qspi_write_fifo.
|
|
||||||
*
|
|
||||||
* ── Диагностика: UART (LPUART1 / MCU-Link VCOM), НЕ USB CDC ──────────────
|
|
||||||
* USB CDC (target-side EHCI/PHY) под FreeRTOS не заработал стабильно —
|
|
||||||
* несколько заходов, последний раз плата падала в recovery загрузчика сразу
|
|
||||||
* после добавления wait-for-host цикла (вероятная причина — не сама USB, а
|
|
||||||
* инкрементально узкий стек boot_task, но целый класс неопределённости
|
|
||||||
* "первый в репозитории FreeRTOS-потребитель USB device stack" остаётся).
|
|
||||||
* LPUART1 — самый проверенный канал в репозитории (используется во ВСЕХ
|
|
||||||
* tests/target/* HIL-образах), идёт через уже подключённый для SWD кабель
|
|
||||||
* MCU-Link, не требует enumeration/wait, пины уже смуксены в BOARD_InitPins().
|
|
||||||
* Если понадобится USB CDC под FreeRTOS (например, Фаза 7 — provisioning) —
|
|
||||||
* отдельная сфокусированная задача, не диагностика Фазы 0.
|
|
||||||
*
|
|
||||||
* main() — минимальный bare-metal (board bring-up + запуск планировщика). Всё
|
|
||||||
* остальное — в boot_task, после старта планировщика (унификация с QSPI/
|
|
||||||
* confirm-логикой; UART сам по себе не требует тика для init/write).
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "FreeRTOS.h"
|
#include "FreeRTOS.h"
|
||||||
#include "board.h"
|
#include "board.h"
|
||||||
#include "bootutil/bootutil_public.h"
|
#include "bootutil/bootutil_public.h"
|
||||||
#include "bsp/boot_state.h"
|
#include "bsp/boot_state.h"
|
||||||
|
#include "bsp/display.h"
|
||||||
#include "bsp/led.h"
|
#include "bsp/led.h"
|
||||||
#include "bsp/qspi_flash.h"
|
#include "bsp/qspi_flash.h"
|
||||||
|
#include "bsp/sdram.h"
|
||||||
#include "bsp/uart_host.h"
|
#include "bsp/uart_host.h"
|
||||||
#include "bsp/wdog.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 "flash_map.h"
|
||||||
#include "log/log.h"
|
#include "log/log.h"
|
||||||
#include "port/log_uart.h"
|
#include "port/log_uart.h"
|
||||||
|
#include "queue.h"
|
||||||
|
#include "services/gfx.h"
|
||||||
#include "task.h"
|
#include "task.h"
|
||||||
|
#include "ui/fallback.h"
|
||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
||||||
/* Фаза 0: диагностика self-confirm по UART (LPUART1 / MCU-Link VCOM,
|
|
||||||
* 115200). Сократить/убрать, когда подтверждено на железе многократно. */
|
|
||||||
#define LOG_TAG "app"
|
#define LOG_TAG "app"
|
||||||
|
|
||||||
/* Собственный слот образа. Slot A = 0 (primary), Slot Б = 1 (secondary). */
|
/* Собственный слот образа. Slot A = 0 (primary), Slot Б = 1 (secondary). */
|
||||||
|
|
@ -59,11 +49,27 @@
|
||||||
#define APP_OWN_SLOT_ID 0
|
#define APP_OWN_SLOT_ID 0
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/* Панель текущего стенда — Фаза 1 хардкод (Фаза 9: provisioning выбирает из
|
||||||
|
* settings, см. ARCH §9 — TFT7/8/10 рантайм-выбор внутри профиля app-big). */
|
||||||
|
#define PANEL_TYPE BSP_DISPLAY_TFT8
|
||||||
|
|
||||||
#define HEARTBEAT_PERIOD_MS 500U
|
#define HEARTBEAT_PERIOD_MS 500U
|
||||||
#define WDOG_FEED_PERIOD_MS 100U /* кормим чаще периода мигания — таймаут WDOG >= 1 c */
|
#define WDOG_FEED_PERIOD_MS 100U /* кормим чаще периода мигания — таймаут WDOG >= 1 c */
|
||||||
#define STATUS_LOG_PERIOD_MS 2000U /* периодический re-log трейлера — виден независимо
|
#define STATUS_LOG_PERIOD_MS 2000U /* периодический re-log трейлера — виден независимо
|
||||||
* от момента подключения терминала (UART без
|
* от момента подключения терминала */
|
||||||
* handshake — писать можно сразу, слушают или нет) */
|
#define CAN_RX_TIMEOUT_MS 100U /* держит цикл отзывчивым к WDOG/heartbeat-каденции */
|
||||||
|
#define CONNECTION_TIMEOUT_MS 3000U /* «пропадание трафика» — см. ARCH, поток данных:
|
||||||
|
* poll + timeout→default. Порядок величины — как
|
||||||
|
* в OLD_PROJECT (там ~3 с на отметку потери связи) */
|
||||||
|
|
||||||
|
/* Передача render_task самого свежего состояния — не истории. */
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
indication_task_t task;
|
||||||
|
sul_result_t result;
|
||||||
|
} render_msg_t;
|
||||||
|
|
||||||
|
static QueueHandle_t s_render_queue;
|
||||||
|
|
||||||
/* ── Диагностика трейлера слота (read-only, безопасно звать многократно) ── */
|
/* ── Диагностика трейлера слота (read-only, безопасно звать многократно) ── */
|
||||||
|
|
||||||
|
|
@ -113,21 +119,73 @@ static void confirm_self(void)
|
||||||
log_slot_status("after-confirm"); /* ожидаем image_ok=1(SET) */
|
log_slot_status("after-confirm"); /* ожидаем image_ok=1(SET) */
|
||||||
}
|
}
|
||||||
|
|
||||||
static void boot_task(void *p_arg)
|
/**
|
||||||
|
* @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(s_render_queue, &msg, portMAX_DELAY) == pdTRUE)
|
||||||
|
{
|
||||||
|
ui_fallback_render(&msg.task, &msg.result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void sul_rx_task(void *p_arg)
|
||||||
{
|
{
|
||||||
(void) p_arg;
|
(void) p_arg;
|
||||||
|
|
||||||
/* LPUART1/MCU-Link VCOM — доступен сразу, без enumeration/wait (в отличие
|
/* LPUART1/MCU-Link VCOM — доступен сразу, без enumeration/wait (в отличие
|
||||||
* от target-side USB CDC). log_mutex не нужен — единственная задача. */
|
* от target-side USB CDC). log_mutex не нужен — единственный писатель. */
|
||||||
(void) bsp_uart_host_init(115200U);
|
(void) bsp_uart_host_init(115200U);
|
||||||
log_uart_init();
|
log_uart_init();
|
||||||
|
|
||||||
/* flash_map_backend требует bsp_qspi_init() ДО любой flash_area_*. */
|
/* flash_map_backend требует bsp_qspi_init() ДО любой flash_area_*. */
|
||||||
const bool QSPI_OK = (bsp_qspi_init() == BSP_OK);
|
const bool QSPI_OK = (bsp_qspi_init() == BSP_OK);
|
||||||
LOG_I(LOG_TAG, "tft_app phase0 boot: qspi=%s", QSPI_OK ? "OK" : "FAIL");
|
LOG_I(LOG_TAG, "tft_app phase1 boot: qspi=%s", QSPI_OK ? "OK" : "FAIL");
|
||||||
|
|
||||||
/* «Дошёл до устойчивого состояния» — сбрасывает счётчик попыток загрузки
|
/* «Дошёл до устойчивого состояния» — сбрасывает счётчик попыток загрузки
|
||||||
* (recovery загрузчика). SRC GPR, без flash. */
|
* (recovery загрузчика). SRC GPR, без flash. Безусловно, до потенциально
|
||||||
|
* рискованного bring-up дисплея/CAN ниже. */
|
||||||
bsp_boot_health_mark();
|
bsp_boot_health_mark();
|
||||||
|
|
||||||
if (QSPI_OK)
|
if (QSPI_OK)
|
||||||
|
|
@ -139,10 +197,19 @@ static void boot_task(void *p_arg)
|
||||||
LOG_E(LOG_TAG, "qspi_init FAILED — self-confirm skipped, slot will revert");
|
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);
|
||||||
|
|
||||||
|
controller_ctx_t ctrl_ctx;
|
||||||
|
controller_init(&ctrl_ctx);
|
||||||
|
|
||||||
const TickType_t FEED_PERIOD = pdMS_TO_TICKS(WDOG_FEED_PERIOD_MS);
|
const TickType_t FEED_PERIOD = pdMS_TO_TICKS(WDOG_FEED_PERIOD_MS);
|
||||||
uint32_t elapsed_ms = 0U;
|
uint32_t elapsed_ms = 0U;
|
||||||
uint32_t since_status_ms = 0U;
|
uint32_t since_status_ms = 0U;
|
||||||
TickType_t last_wake = xTaskGetTickCount();
|
TickType_t last_wake = xTaskGetTickCount();
|
||||||
|
TickType_t last_frame_tick = xTaskGetTickCount();
|
||||||
|
|
||||||
for (;;)
|
for (;;)
|
||||||
{
|
{
|
||||||
|
|
@ -162,6 +229,45 @@ static void boot_task(void *p_arg)
|
||||||
log_slot_status("periodic");
|
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(s_render_queue, &MSG);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
vTaskDelayUntil(&last_wake, FEED_PERIOD);
|
vTaskDelayUntil(&last_wake, FEED_PERIOD);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -171,8 +277,15 @@ int main(void)
|
||||||
board_hw_init(); /* BOARD_ConfigMPU + BOARD_InitPins + BOARD_BootClockRUN */
|
board_hw_init(); /* BOARD_ConfigMPU + BOARD_InitPins + BOARD_BootClockRUN */
|
||||||
bsp_led_init();
|
bsp_led_init();
|
||||||
|
|
||||||
/* x4 (2 КБ) с запасом: логирование (vsnprintf) + чтение трейлера. */
|
s_render_queue = xQueueCreate(1, sizeof(render_msg_t));
|
||||||
(void) xTaskCreate(boot_task, "boot", configMINIMAL_STACK_SIZE * 4U, NULL,
|
configASSERT(s_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);
|
tskIDLE_PRIORITY + 1U, NULL);
|
||||||
|
|
||||||
vTaskStartScheduler();
|
vTaskStartScheduler();
|
||||||
|
|
|
||||||
3
firmware/tft_app/src/domain/CMakeLists.txt
Normal file
3
firmware/tft_app/src/domain/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
add_subdirectory(elevator_model)
|
||||||
|
add_subdirectory(sul)
|
||||||
|
add_subdirectory(controller)
|
||||||
5
firmware/tft_app/src/domain/controller/CMakeLists.txt
Normal file
5
firmware/tft_app/src/domain/controller/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
add_library(tft_app_controller STATIC src/controller.c)
|
||||||
|
|
||||||
|
target_include_directories(tft_app_controller PUBLIC include/)
|
||||||
|
|
||||||
|
target_link_libraries(tft_app_controller PUBLIC tft_app_elevator_model)
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
/**
|
||||||
|
* @file controller.h
|
||||||
|
* @brief Редьюсер: sul_result_t (+кэш) → indication_task_t (diff).
|
||||||
|
*
|
||||||
|
* Чистый C, без единого HAL-вызова. НЕ владеет временем/таймаутами сам —
|
||||||
|
* caller (app-level sul_rx задача, у неё есть доступ к тику) при обнаружении
|
||||||
|
* таймаута связи просто вызывает controller_process() с sul_default_state(),
|
||||||
|
* тем же путём, что и любой обычный кадр — специального API для timeout не
|
||||||
|
* нужно (см. ARCH.md, поток данных: «poll + timeout→default» — это
|
||||||
|
* обязанность вызывающей задачи, не контроллера).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef DOMAIN_CONTROLLER_H_
|
||||||
|
#define DOMAIN_CONTROLLER_H_
|
||||||
|
|
||||||
|
#include "domain/elevator_model.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/** Состояние редьюсера — кэш последнего примененного результата. */
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
sul_result_t cache;
|
||||||
|
} controller_ctx_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Что изменилось с прошлого вызова — presentation перерисовывает
|
||||||
|
* только помеченные поля.
|
||||||
|
*/
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
bool pos_pending;
|
||||||
|
bool direction_pending;
|
||||||
|
} indication_task_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Сброс кэша к sul_default_state().
|
||||||
|
*
|
||||||
|
* Кэш засеивается дефолтом ДО первого реального кадра — если первый кадр от
|
||||||
|
* СУЛ совпадёт с дефолтом (напр. пришёл со всё ещё пустой позицией), diff
|
||||||
|
* будет "ничего не изменилось". Первую отрисовку (пустой экран → что-то на
|
||||||
|
* экране) presentation обязана сделать безусловно один раз при старте, не
|
||||||
|
* дожидаясь pending-флагов (см. app: render-задача).
|
||||||
|
*/
|
||||||
|
void controller_init(controller_ctx_t *p_ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Применить новый результат, вернуть diff относительно кэша.
|
||||||
|
*
|
||||||
|
* Обновляет кэш на p_result безусловно (следующий вызов будет сравнивать
|
||||||
|
* именно с этим состоянием).
|
||||||
|
*/
|
||||||
|
indication_task_t controller_process(controller_ctx_t *p_ctx, const sul_result_t *p_result);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* DOMAIN_CONTROLLER_H_ */
|
||||||
20
firmware/tft_app/src/domain/controller/src/controller.c
Normal file
20
firmware/tft_app/src/domain/controller/src/controller.c
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
#include "domain/controller.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
void controller_init(controller_ctx_t *p_ctx)
|
||||||
|
{
|
||||||
|
p_ctx->cache = sul_default_state();
|
||||||
|
}
|
||||||
|
|
||||||
|
indication_task_t controller_process(controller_ctx_t *p_ctx, const sul_result_t *p_result)
|
||||||
|
{
|
||||||
|
const indication_task_t task = {
|
||||||
|
.pos_pending = (strcmp(p_ctx->cache.pos, p_result->pos) != 0),
|
||||||
|
.direction_pending = (p_ctx->cache.direction != p_result->direction),
|
||||||
|
};
|
||||||
|
|
||||||
|
p_ctx->cache = *p_result;
|
||||||
|
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
add_library(tft_app_elevator_model STATIC src/elevator_model.c)
|
||||||
|
|
||||||
|
target_include_directories(tft_app_elevator_model PUBLIC include/)
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
/**
|
||||||
|
* @file elevator_model.h
|
||||||
|
* @brief Канонический результат декодирования СУЛ (Часть II §6 ARCH.md).
|
||||||
|
*
|
||||||
|
* Чистый C, без единого HAL-вызова — host-тестируется тем же способом, что
|
||||||
|
* модули bootloader. `sul_result_t` — надмножество полей для ЛЮБОГО протокола
|
||||||
|
* (НКУ-CAN, УИМ, SD7, УЭЛ, УКЛ, …); конкретный декодер заполняет то, что несёт
|
||||||
|
* его протокол, остальное остаётся дефолтным.
|
||||||
|
*
|
||||||
|
* Фаза 1 — подмножество: `pos` + `direction` (см. PLAN.md). Остальные поля
|
||||||
|
* ARCH §6 (next, сигналы, lading_secs, floor_num) добавляются в Фазе 2 вместе
|
||||||
|
* с полным декодером НКУ-CAN и таблицей приоритетов режимов.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef DOMAIN_ELEVATOR_MODEL_H_
|
||||||
|
#define DOMAIN_ELEVATOR_MODEL_H_
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/** Максимум значащих символов позиции сейчас — 2; запас до 4 (см. ARCH §6). */
|
||||||
|
#define SUL_POS_MAX 4U
|
||||||
|
|
||||||
|
/** UTF-8, худший случай — 4 байта/кодпойнт + '\0'. */
|
||||||
|
#define SUL_POS_BUF_LEN (SUL_POS_MAX * 4U + 1U)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Направление движения кабины.
|
||||||
|
*
|
||||||
|
* Канонический словарь — НЕ протокол-специфичный набор битов. Декодер
|
||||||
|
* конкретного СУЛ транслирует свои сырые коды в эти значения (для НКУ-CAN
|
||||||
|
* PACKET1 ARROW_MASK совпадает по значению случайно — не полагаться на это
|
||||||
|
* в других протоколах).
|
||||||
|
*/
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
SUL_DIR_NONE = 0, /**< нет движения / нет данных */
|
||||||
|
SUL_DIR_UP = 1,
|
||||||
|
SUL_DIR_DOWN = 2,
|
||||||
|
SUL_DIR_DOUBLE = 3, /**< двойная стрелка (спецрежим индикации) */
|
||||||
|
} sul_direction_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Канонический результат декодирования кадра/пакета СУЛ.
|
||||||
|
*
|
||||||
|
* Позиция — UTF-8 строка (реальные шрифты ASCII + кириллица), не число:
|
||||||
|
* универсально для всех протоколов, host-тест сравнивает строки напрямую.
|
||||||
|
* Декодер ничего не знает про шрифт/рендер — валидация рендеримости
|
||||||
|
* (покрытие активным шрифтом) — задача UI-слоя (ARCH §11), не decode().
|
||||||
|
*/
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
char pos[SUL_POS_BUF_LEN]; /**< позиция кабины, напр. "12", "-1", "П" */
|
||||||
|
sul_direction_t direction;
|
||||||
|
} sul_result_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Состояние по умолчанию / при потере связи.
|
||||||
|
*
|
||||||
|
* pos="--" (нет данных), direction=SUL_DIR_NONE. Вызывающий (контроллер через
|
||||||
|
* sul_rx-задачу при таймауте, либо init перед первым кадром) использует это
|
||||||
|
* как отправную точку — не HAL-зависимо, чистая функция.
|
||||||
|
*/
|
||||||
|
sul_result_t sul_default_state(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* DOMAIN_ELEVATOR_MODEL_H_ */
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
#include "domain/elevator_model.h"
|
||||||
|
|
||||||
|
sul_result_t sul_default_state(void)
|
||||||
|
{
|
||||||
|
return (sul_result_t){.pos = "--", .direction = SUL_DIR_NONE};
|
||||||
|
}
|
||||||
16
firmware/tft_app/src/domain/sul/CMakeLists.txt
Normal file
16
firmware/tft_app/src/domain/sul/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# tft_app_sul_headers — INTERFACE, только domain/sul.h (типы sul_frame_t/
|
||||||
|
# sul_status_t/sul_driver_t). Разделено от реализации реестра (ниже), чтобы
|
||||||
|
# tft_app_sul_nku_can мог использовать типы без циклической зависимости:
|
||||||
|
# nku_can нужны только типы sul.h, а sul_registry.c сам зовёт nku_can_decode.
|
||||||
|
add_library(tft_app_sul_headers INTERFACE)
|
||||||
|
target_include_directories(tft_app_sul_headers INTERFACE include/)
|
||||||
|
target_link_libraries(tft_app_sul_headers INTERFACE tft_app_elevator_model)
|
||||||
|
|
||||||
|
add_subdirectory(nku_can)
|
||||||
|
add_subdirectory(transport/can)
|
||||||
|
|
||||||
|
# tft_app_sul — реализация реестра. Явно знает обо всех драйверах (сейчас —
|
||||||
|
# один, nku_can); добавление протокола (Фаза 8) — новая запись в
|
||||||
|
# sul_registry.c + новая зависимость здесь.
|
||||||
|
add_library(tft_app_sul STATIC src/sul_registry.c)
|
||||||
|
target_link_libraries(tft_app_sul PUBLIC tft_app_sul_headers tft_app_sul_nku_can)
|
||||||
95
firmware/tft_app/src/domain/sul/include/domain/sul.h
Normal file
95
firmware/tft_app/src/domain/sul/include/domain/sul.h
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
/**
|
||||||
|
* @file sul.h
|
||||||
|
* @brief Реестр драйверов СУЛ (ARCH.md §6) — «одна прошивка — много протоколов».
|
||||||
|
*
|
||||||
|
* Драйвер = чистый декодер (host-тестируемый, без HAL) + отдельный тонкий
|
||||||
|
* транспорт-адаптер (HW, живёт в sul/transport/<bus>). Декодер НЕ владеет
|
||||||
|
* состоянием сам — состояние (например, накопленная позиция между PACKET1 и
|
||||||
|
* PACKET3 у НКУ-CAN) держит caller в ctx и передаёт указатель на каждый вызов
|
||||||
|
* decode(). Это позволяет декодеру оставаться описанным одной чистой функцией
|
||||||
|
* и не тянуть за собой выделение памяти/жизненный цикл.
|
||||||
|
*
|
||||||
|
* Фаза 1 — одна запись реестра (nku_can), активный драйвер хардкожен.
|
||||||
|
* Фаза 3 добавит выбор активного протокола из настроек — сигнатура
|
||||||
|
* `sul_registry_active()` не изменится, изменится только то, что она
|
||||||
|
* возвращает.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef DOMAIN_SUL_H_
|
||||||
|
#define DOMAIN_SUL_H_
|
||||||
|
|
||||||
|
#include "domain/elevator_model.h"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/** Результат decode() одного кадра/пакета. */
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
SUL_STATUS_OK = 0, /**< кадр распознан, ctx и *p_out обновлены */
|
||||||
|
SUL_STATUS_IGNORED, /**< кадр не для этого драйвера — *p_out не тронут */
|
||||||
|
SUL_STATUS_ERR, /**< кадр совпал по ID, но малформирован (DLC и т.п.) */
|
||||||
|
} sul_status_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Кадр транспортного уровня, нейтральный к шине (CAN/UART/...).
|
||||||
|
*
|
||||||
|
* Транспорт-адаптер (HW) заполняет её из своего протокола (для CAN — id и
|
||||||
|
* data/len из bsp_can_frame_t); декодер (чистый C) её только читает.
|
||||||
|
*/
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
uint32_t id; /**< CAN ID либо адрес/маркер кадра другого транспорта */
|
||||||
|
uint8_t bus; /**< на будущее — несколько шин одного типа (0 = единственная) */
|
||||||
|
const uint8_t *p_data;
|
||||||
|
uint16_t len;
|
||||||
|
} sul_frame_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Чистая функция декодирования — БЕЗ единого HAL-вызова.
|
||||||
|
*
|
||||||
|
* @param p_ctx состояние драйвера (владеет caller, см. докстрок файла)
|
||||||
|
* @param p_frame один кадр транспортного уровня
|
||||||
|
* @param p_out заполняется только при SUL_STATUS_OK
|
||||||
|
*/
|
||||||
|
typedef sul_status_t (*sul_decode_fn)(void *p_ctx, const sul_frame_t *p_frame, sul_result_t *p_out);
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
uint8_t id; /**< стабильный идентификатор протокола */
|
||||||
|
const char *p_name; /**< для меню (Фаза 3) */
|
||||||
|
sul_decode_fn decode;
|
||||||
|
} sul_driver_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Идентификаторы протоколов — стабильны, не переиспользовать.
|
||||||
|
*
|
||||||
|
* Список открыт (ARCH §2.2): УЭЛ, УКЛ, НКУ-SD7, УИМ добавляются в Фазе 8.
|
||||||
|
*/
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
SUL_PROTOCOL_NKU_CAN = 0U,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Активный драйвер. Фаза 1 — единственная запись, хардкод.
|
||||||
|
* @return указатель на статический дескриптор, никогда NULL.
|
||||||
|
*/
|
||||||
|
const sul_driver_t *sul_registry_active(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Найти драйвер по id (для будущего выбора по настройкам, Фаза 3).
|
||||||
|
* @return NULL, если протокол не зарегистрирован.
|
||||||
|
*/
|
||||||
|
const sul_driver_t *sul_registry_find(uint8_t id);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* DOMAIN_SUL_H_ */
|
||||||
5
firmware/tft_app/src/domain/sul/nku_can/CMakeLists.txt
Normal file
5
firmware/tft_app/src/domain/sul/nku_can/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
add_library(tft_app_sul_nku_can STATIC src/nku_can.c)
|
||||||
|
|
||||||
|
target_include_directories(tft_app_sul_nku_can PUBLIC include/)
|
||||||
|
|
||||||
|
target_link_libraries(tft_app_sul_nku_can PUBLIC tft_app_elevator_model tft_app_sul_headers)
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
/**
|
||||||
|
* @file nku_can.h
|
||||||
|
* @brief Чистый декодер протокола НКУ-CAN (порт из OLD_PROJECT msg_receiver_task).
|
||||||
|
*
|
||||||
|
* Фаза 1 — подмножество: PACKET1 (направление) + PACKET3 (позиция кабины).
|
||||||
|
* Остальные пакеты (PACKET2/4/5, режимы, гонг, погрузка, удалённая установка
|
||||||
|
* адреса) — Фаза 2, см. PLAN.md.
|
||||||
|
*
|
||||||
|
* Адрес станции (nku_address) захардкожен в 0 — фильтры/ID без сдвига группы.
|
||||||
|
* Фаза 3 параметризует через настройки; сигнатура decode() не изменится.
|
||||||
|
*
|
||||||
|
* Чистый C, без единого HAL-вызова — host-тестируется golden-векторами
|
||||||
|
* CAN-кадров (tests/host/tft_app_sul_nku).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef DOMAIN_SUL_NKU_CAN_H_
|
||||||
|
#define DOMAIN_SUL_NKU_CAN_H_
|
||||||
|
|
||||||
|
#include "domain/elevator_model.h"
|
||||||
|
#include "domain/sul.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Состояние декодера — накопленный текущий sul_result_t.
|
||||||
|
*
|
||||||
|
* PACKET1 и PACKET3 несут РАЗНЫЕ поля (направление / позицию) в разных
|
||||||
|
* кадрах — decode() обновляет только своё поле в state и отдаёт наружу
|
||||||
|
* ПОЛНУЮ накопленную копию, а не только то, что пришло в этом кадре.
|
||||||
|
*
|
||||||
|
* Фаза 2 добавит сюда edge-detection состояние для гонга/погрузки/режимов
|
||||||
|
* (аналог event_edge_t в legacy) — структура специально не голый sul_result_t,
|
||||||
|
* чтобы не менять сигнатуру при расширении.
|
||||||
|
*/
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
sul_result_t state;
|
||||||
|
} nku_can_ctx_t;
|
||||||
|
|
||||||
|
/** Сброс к состоянию по умолчанию (sul_default_state()). Вызвать перед первым decode(). */
|
||||||
|
void nku_can_init(nku_can_ctx_t *p_ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief decode() для реестра sul (см. sul_decode_fn в domain/sul.h).
|
||||||
|
*
|
||||||
|
* @param p_ctx nku_can_ctx_t*, инициализированный nku_can_init()
|
||||||
|
* @param p_frame сырой CAN-кадр (id + до 8 байт data)
|
||||||
|
* @param p_out заполняется полной накопленной state только при SUL_STATUS_OK
|
||||||
|
*/
|
||||||
|
sul_status_t nku_can_decode(void *p_ctx, const sul_frame_t *p_frame, sul_result_t *p_out);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* DOMAIN_SUL_NKU_CAN_H_ */
|
||||||
115
firmware/tft_app/src/domain/sul/nku_can/src/nku_can.c
Normal file
115
firmware/tft_app/src/domain/sul/nku_can/src/nku_can.c
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
#include "domain/sul/nku_can.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
/* Фаза 1: адрес станции захардкожен в 0 — базовые ID без сдвига группы
|
||||||
|
* (легаси: 0x506|group4, group4 = nku_address<<4; для address=0 group4=0).
|
||||||
|
* Фаза 3 параметризует через настройки. */
|
||||||
|
#define PACKET1_ID 0x506U
|
||||||
|
#define PACKET3_ID 0x508U
|
||||||
|
#define PROTO_DLC 8U
|
||||||
|
|
||||||
|
#define ARROW_MASK 0x03U
|
||||||
|
#define FLOOR_MASK 0x3FU
|
||||||
|
|
||||||
|
#define SYMBOL_SPACE 16U
|
||||||
|
#define SYMBOL_TOTAL 38U
|
||||||
|
|
||||||
|
/* Таблица символов НКУ-CAN — порт из OLD_PROJECT floor_string_composer()
|
||||||
|
* (source/main_programm.c). Индекс — код символа с шины (байт & FLOOR_MASK). */
|
||||||
|
static const char *const S_SYMBOL_TABLE[SYMBOL_TOTAL] = {
|
||||||
|
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "b", "C", "d", "E", "F", " ", "П", "Р",
|
||||||
|
"п", "H", "U", "-", "_", "u", "L", "У", "Б", "Г", "R", "V", "N", "S", "K", "Y", "G", "B", "T",
|
||||||
|
};
|
||||||
|
|
||||||
|
void nku_can_init(nku_can_ctx_t *p_ctx)
|
||||||
|
{
|
||||||
|
p_ctx->state = sul_default_state();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief PACKET1 (0x506) — направление движения.
|
||||||
|
*
|
||||||
|
* ARROW_MASK=0x03 на data[6] (DATA7): 0=нет/1=вверх/2=вниз/3=двойная стрелка.
|
||||||
|
* Легаси (msg_receiver_task, PACKET1) обрабатывал только 0/1/2 — case 3
|
||||||
|
* отсутствовал (направление молча не менялось). 3 = «двойная стрелка»
|
||||||
|
* (спецрежим индикации, словарь special/DisplayArrowIcon) — уточнено отдельно.
|
||||||
|
*
|
||||||
|
* Маска покрывает ровно 0..3 — весь диапазон sul_direction_t, прямое
|
||||||
|
* приведение типа корректно без switch/default.
|
||||||
|
*/
|
||||||
|
static void decode_packet1(nku_can_ctx_t *p_ctx, const uint8_t *p_data)
|
||||||
|
{
|
||||||
|
p_ctx->state.direction = (sul_direction_t) (p_data[6] & ARROW_MASK);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief PACKET3 (0x508) — позиция кабины (left/right символы).
|
||||||
|
*
|
||||||
|
* FLOOR_MASK=0x3F на data[5] (left) и data[6] (right) — порт
|
||||||
|
* floor_string_composer() из OLD_PROJECT. left==SPACE или left==0 →
|
||||||
|
* однозначный этаж, выводится только правый символ (легаси трактует байт
|
||||||
|
* 0x00 так же, как пробел — станция может слать нулевой байт вместо явного
|
||||||
|
* кода пробела для «нет левого символа»; порт без изменений поведения).
|
||||||
|
*
|
||||||
|
* В отличие от легаси (молча оставляет буфер как есть при выходе за
|
||||||
|
* SYMBOL_TOTAL — маска даёт до 63 сырых значений при 38 валидных символах),
|
||||||
|
* здесь это ошибка (false) — не полагаемся на предыдущее содержимое буфера.
|
||||||
|
*
|
||||||
|
* @return false — left/right вне таблицы символов (малформированный кадр).
|
||||||
|
*/
|
||||||
|
static bool decode_packet3(nku_can_ctx_t *p_ctx, const uint8_t *p_data)
|
||||||
|
{
|
||||||
|
const uint8_t LEFT = p_data[5] & FLOOR_MASK;
|
||||||
|
const uint8_t RIGHT = p_data[6] & FLOOR_MASK;
|
||||||
|
|
||||||
|
if ((LEFT >= SYMBOL_TOTAL) || (RIGHT >= SYMBOL_TOTAL))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((LEFT == SYMBOL_SPACE) || (LEFT == 0U))
|
||||||
|
{
|
||||||
|
(void) snprintf(p_ctx->state.pos, SUL_POS_BUF_LEN, "%s", S_SYMBOL_TABLE[RIGHT]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
(void) snprintf(p_ctx->state.pos, SUL_POS_BUF_LEN, "%s%s", S_SYMBOL_TABLE[LEFT],
|
||||||
|
S_SYMBOL_TABLE[RIGHT]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
sul_status_t nku_can_decode(void *p_ctx, const sul_frame_t *p_frame, sul_result_t *p_out)
|
||||||
|
{
|
||||||
|
nku_can_ctx_t *p_state = (nku_can_ctx_t *) p_ctx;
|
||||||
|
|
||||||
|
if (p_frame->id == PACKET1_ID)
|
||||||
|
{
|
||||||
|
if (p_frame->len != PROTO_DLC)
|
||||||
|
{
|
||||||
|
return SUL_STATUS_ERR;
|
||||||
|
}
|
||||||
|
decode_packet1(p_state, p_frame->p_data);
|
||||||
|
*p_out = p_state->state;
|
||||||
|
return SUL_STATUS_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p_frame->id == PACKET3_ID)
|
||||||
|
{
|
||||||
|
if (p_frame->len != PROTO_DLC)
|
||||||
|
{
|
||||||
|
return SUL_STATUS_ERR;
|
||||||
|
}
|
||||||
|
if (!decode_packet3(p_state, p_frame->p_data))
|
||||||
|
{
|
||||||
|
return SUL_STATUS_ERR;
|
||||||
|
}
|
||||||
|
*p_out = p_state->state;
|
||||||
|
return SUL_STATUS_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SUL_STATUS_IGNORED;
|
||||||
|
}
|
||||||
33
firmware/tft_app/src/domain/sul/src/sul_registry.c
Normal file
33
firmware/tft_app/src/domain/sul/src/sul_registry.c
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
#include "domain/sul.h"
|
||||||
|
|
||||||
|
#include "domain/sul/nku_can.h"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
static const sul_driver_t s_registry[] = {
|
||||||
|
{
|
||||||
|
.id = SUL_PROTOCOL_NKU_CAN,
|
||||||
|
.p_name = "НКУ-CAN",
|
||||||
|
.decode = nku_can_decode,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#define REGISTRY_COUNT (sizeof(s_registry) / sizeof(s_registry[0]))
|
||||||
|
|
||||||
|
const sul_driver_t *sul_registry_active(void)
|
||||||
|
{
|
||||||
|
/* Фаза 1: единственная запись, хардкод. Фаза 3 выберет по настройкам. */
|
||||||
|
return &s_registry[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const sul_driver_t *sul_registry_find(uint8_t id)
|
||||||
|
{
|
||||||
|
for (size_t i = 0; i < REGISTRY_COUNT; i++)
|
||||||
|
{
|
||||||
|
if (s_registry[i].id == id)
|
||||||
|
{
|
||||||
|
return &s_registry[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
add_library(tft_app_sul_transport_can STATIC src/can_transport.c)
|
||||||
|
|
||||||
|
target_include_directories(tft_app_sul_transport_can PUBLIC include/)
|
||||||
|
|
||||||
|
target_link_libraries(tft_app_sul_transport_can PUBLIC tft_app_sul_headers bsp_can)
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
/**
|
||||||
|
* @file can.h
|
||||||
|
* @brief Тонкий транспорт-адаптер bsp_can → sul_frame_t (HW, не host-тестируется).
|
||||||
|
*
|
||||||
|
* Не декодирует протокол — только инициализация CAN + фильтры и перевод
|
||||||
|
* bsp_can_frame_t в нейтральный sul_frame_t. Декодирование — domain/sul/nku_can.h.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef DOMAIN_SUL_TRANSPORT_CAN_H_
|
||||||
|
#define DOMAIN_SUL_TRANSPORT_CAN_H_
|
||||||
|
|
||||||
|
#include "bsp/status.h"
|
||||||
|
#include "domain/sul.h"
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief bsp_can_init() + RX-фильтры под активные ID НКУ-CAN.
|
||||||
|
*
|
||||||
|
* Фаза 1: фильтры на PACKET1(0x506)/PACKET3(0x508), адрес станции 0
|
||||||
|
* (хардкод — совпадает с decode-стороной nku_can.c). Фаза 3 параметризует
|
||||||
|
* оба конца из настроек одновременно.
|
||||||
|
*/
|
||||||
|
bsp_status_t sul_transport_can_init(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Принять один кадр и перевести в sul_frame_t.
|
||||||
|
*
|
||||||
|
* @warning p_out->p_data валиден ТОЛЬКО до следующего вызова этой функции
|
||||||
|
* (внутренний статический буфер, единственный ожидаемый вызыватель — задача
|
||||||
|
* sul_rx с последовательным receive→decode). Не сохранять sul_frame_t между
|
||||||
|
* итерациями цикла приёма.
|
||||||
|
*
|
||||||
|
* @param timeout_ms таймаут ожидания кадра, 0 = не блокироваться
|
||||||
|
* @param p_out заполняется только при возврате BSP_OK
|
||||||
|
* @return BSP_OK, BSP_ERR_TIMEOUT
|
||||||
|
*/
|
||||||
|
bsp_status_t sul_transport_can_receive(uint32_t timeout_ms, sul_frame_t *p_out);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* DOMAIN_SUL_TRANSPORT_CAN_H_ */
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
#include "domain/sul/transport/can.h"
|
||||||
|
|
||||||
|
#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
|
||||||
|
#define STD_ID_MASK 0x7FFU /* 11-bit STD — проверять все биты */
|
||||||
|
|
||||||
|
/* Хранилище последнего принятого кадра — см. предупреждение в can.h про
|
||||||
|
* время жизни p_out->p_data, возвращаемого sul_transport_can_receive(). */
|
||||||
|
static bsp_can_frame_t s_last_frame;
|
||||||
|
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
bsp_status_t sul_transport_can_receive(uint32_t timeout_ms, sul_frame_t *p_out)
|
||||||
|
{
|
||||||
|
const bsp_status_t st = bsp_can_receive(&s_last_frame, timeout_ms);
|
||||||
|
if (st != BSP_OK)
|
||||||
|
{
|
||||||
|
return st;
|
||||||
|
}
|
||||||
|
|
||||||
|
p_out->id = s_last_frame.id;
|
||||||
|
p_out->bus = 0U;
|
||||||
|
p_out->p_data = s_last_frame.data;
|
||||||
|
p_out->len = s_last_frame.dlc;
|
||||||
|
|
||||||
|
return BSP_OK;
|
||||||
|
}
|
||||||
1
firmware/tft_app/src/services/CMakeLists.txt
Normal file
1
firmware/tft_app/src/services/CMakeLists.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
add_subdirectory(gfx)
|
||||||
25
firmware/tft_app/src/services/gfx/CMakeLists.txt
Normal file
25
firmware/tft_app/src/services/gfx/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# services/gfx — Фаза 1 минимальный gfx: framebuffer (SDRAM non-cacheable) +
|
||||||
|
# RLE-шрифты (lcd-image-converter) + примитивы. См. include/services/gfx.h.
|
||||||
|
|
||||||
|
add_library(
|
||||||
|
tft_app_gfx STATIC
|
||||||
|
src/gfx.c
|
||||||
|
fonts/FloorFontFallback.c
|
||||||
|
fonts/SystemFont.c)
|
||||||
|
|
||||||
|
target_include_directories(tft_app_gfx PUBLIC include/)
|
||||||
|
|
||||||
|
# Сгенерированные lcd-image-converter файлы ожидают tImage/tChar/tFont из
|
||||||
|
# bare-name "fonts.h": SystemFont.c явным #include <fonts.h> (резолвится
|
||||||
|
# через -I ниже), FloorFontFallback.c вообще без include-строки (более новая
|
||||||
|
# версия конвертера, полагается на внешний force-include). Один и тот же
|
||||||
|
# заголовок (fonts/include/fonts.h) закрывает оба случая. НЕ редактируем
|
||||||
|
# сгенерированные файлы напрямую — конвертер их перегенерирует при
|
||||||
|
# обновлении шрифта, ручная правка потеряется.
|
||||||
|
target_include_directories(tft_app_gfx PRIVATE fonts/include)
|
||||||
|
set_source_files_properties(
|
||||||
|
fonts/FloorFontFallback.c fonts/SystemFont.c
|
||||||
|
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
|
||||||
2192
firmware/tft_app/src/services/gfx/fonts/FloorFontFallback.c
Normal file
2192
firmware/tft_app/src/services/gfx/fonts/FloorFontFallback.c
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
7624
firmware/tft_app/src/services/gfx/fonts/SystemFont.c
Normal file
7624
firmware/tft_app/src/services/gfx/fonts/SystemFont.c
Normal file
File diff suppressed because it is too large
Load diff
41
firmware/tft_app/src/services/gfx/fonts/include/fonts.h
Normal file
41
firmware/tft_app/src/services/gfx/fonts/include/fonts.h
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
/**
|
||||||
|
* @file fonts.h
|
||||||
|
* @brief Layout типов, ожидаемый файлами lcd-image-converter (../FloorFontFallback.c,
|
||||||
|
* ../SystemFont.c) — SystemFont.c явно делает #include <fonts.h> (bare name,
|
||||||
|
* так сгенерировал конвертер), FloorFontFallback.c рассчитывает на те же
|
||||||
|
* типы через force-include (см. CMakeLists.txt этой библиотеки).
|
||||||
|
*
|
||||||
|
* Порядок/типы полей — НЕ менять без пересборки обоих шрифтов в конвертере:
|
||||||
|
* они определяют бинарную раскладку структур в сгенерированных .c.
|
||||||
|
*
|
||||||
|
* Публичный API поверх этого формата — services/gfx.h (там те же типы
|
||||||
|
* переобъявлены под неймспейс services/, этот файл — только для компиляции
|
||||||
|
* сгенерированных файлов, наружу библиотеки gfx не торчит).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef FONTS_H_
|
||||||
|
#define FONTS_H_
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
const uint32_t *data;
|
||||||
|
uint16_t width;
|
||||||
|
uint16_t height;
|
||||||
|
uint8_t dataSize;
|
||||||
|
} tImage;
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
long int code;
|
||||||
|
const tImage *image;
|
||||||
|
} tChar;
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
int length;
|
||||||
|
const tChar *chars;
|
||||||
|
} tFont;
|
||||||
|
|
||||||
|
#endif /* FONTS_H_ */
|
||||||
107
firmware/tft_app/src/services/gfx/include/services/gfx.h
Normal file
107
firmware/tft_app/src/services/gfx/include/services/gfx.h
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
/**
|
||||||
|
* @file gfx.h
|
||||||
|
* @brief Фаза 1 — минимальный gfx: framebuffer (SDRAM, non-cacheable) +
|
||||||
|
* RLE-шрифты (lcd-image-converter) + примитив стрелки. Без PXP/
|
||||||
|
* компоновщика/альфа-слоёв — прямая запись в единственный framebuffer,
|
||||||
|
* который ELCDIF сканирует по DMA (см. gfx.c про non-cacheable SDRAM).
|
||||||
|
*
|
||||||
|
* Формат tImage/tChar/tFont и RLE-декодирование — порт проверенного в проде
|
||||||
|
* алгоритма из OLD_PROJECT (source/fonts/fonts.c), тот же формат данных, что
|
||||||
|
* реально экспортирует lcd-image-converter (см. services/gfx/fonts/).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef SERVICES_GFX_H_
|
||||||
|
#define SERVICES_GFX_H_
|
||||||
|
|
||||||
|
#include "bsp/display.h"
|
||||||
|
#include "bsp/status.h"
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Точный layout, экспортируемый lcd-image-converter — копия
|
||||||
|
* services/gfx/fonts/include/fonts.h (тот форсированно инклудится в
|
||||||
|
* сгенерированные .c, здесь — то же самое для остального приложения; два
|
||||||
|
* файла держать в синхроне, формат внешний и стабилен). */
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
const uint32_t *data;
|
||||||
|
uint16_t width;
|
||||||
|
uint16_t height;
|
||||||
|
uint8_t dataSize;
|
||||||
|
} tImage;
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
long int code;
|
||||||
|
const tImage *image;
|
||||||
|
} tChar;
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
int length;
|
||||||
|
const tChar *chars;
|
||||||
|
} tFont;
|
||||||
|
|
||||||
|
/* Компилируемые в прошивку шрифты (services/gfx/fonts/, сгенерированы
|
||||||
|
* lcd-image-converter пользователем). */
|
||||||
|
extern const tFont FloorFontFallback; /* 0-9, "-", пробел — fallback (§11 ARCH) */
|
||||||
|
extern const tFont SystemFont; /* ASCII + кириллица — логи/меню */
|
||||||
|
|
||||||
|
/** XRGB8888 (X игнорируется ELCDIF) — X-байт значения не имеет. */
|
||||||
|
typedef uint32_t gfx_color_t;
|
||||||
|
|
||||||
|
#define GFX_COLOR_BLACK 0x00000000U
|
||||||
|
#define GFX_COLOR_WHITE 0x00FFFFFFU
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief framebuffer (SDRAM non-cacheable) + bsp_display.
|
||||||
|
*
|
||||||
|
* SDRAM (SEMC) должна быть уже поднята вызывающим (bsp_sdram_configure() +
|
||||||
|
* bsp_sdram_init()) — gfx не владеет SEMC-инициализацией, только framebuffer
|
||||||
|
* внутри уже готовой 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);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Нарисовать строку.
|
||||||
|
*
|
||||||
|
* Символ вне таблицы шрифта (отсутствует в font->chars[]) → подстановка '-'
|
||||||
|
* ("-" — согласованный fallback-глиф, см. FloorFontFallback) — по-символьный
|
||||||
|
* fallback (ARCH §11), не обрыв/пропуск на первом неизвестном символе. Если
|
||||||
|
* даже '-' не найден в переданном шрифте — символ пропускается (ширина 0).
|
||||||
|
*
|
||||||
|
* Понимает 2-байтовые UTF-8 последовательности (ведущий байт 0xD0/0xD1 —
|
||||||
|
* кириллица) как ОДИН символ для подстановки/позиционирования — как и
|
||||||
|
* lcd-image-converter кодирует code в tChar для таких шрифтов.
|
||||||
|
*
|
||||||
|
* @return суммарная ширина отрисованной строки, пиксели.
|
||||||
|
*/
|
||||||
|
uint16_t gfx_draw_string(const tFont *p_font, const char *p_str, uint16_t x, uint16_t y);
|
||||||
|
|
||||||
|
/** Ширина строки без отрисовки (для центрирования и т.п.). */
|
||||||
|
uint16_t gfx_string_width(const tFont *p_font, const char *p_str);
|
||||||
|
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
GFX_ARROW_UP,
|
||||||
|
GFX_ARROW_DOWN,
|
||||||
|
} gfx_arrow_dir_t;
|
||||||
|
|
||||||
|
/** Простая треугольная стрелка — примитив, без спрайтов (ARCH §11: fallback asset-free). */
|
||||||
|
void gfx_draw_arrow(gfx_arrow_dir_t dir, uint16_t x, uint16_t y, uint16_t size, gfx_color_t color);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* SERVICES_GFX_H_ */
|
||||||
268
firmware/tft_app/src/services/gfx/src/gfx.c
Normal file
268
firmware/tft_app/src/services/gfx/src/gfx.c
Normal file
|
|
@ -0,0 +1,268 @@
|
||||||
|
#include "services/gfx.h"
|
||||||
|
|
||||||
|
#include "fsl_common.h" /* AT_NONCACHEABLE_SECTION_ALIGN */
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
/* ── Framebuffer (SDRAM, non-cacheable) ────────────────────────────────────
|
||||||
|
*
|
||||||
|
* AT_NONCACHEABLE_SECTION_ALIGN размещает переменную в линкер-секции
|
||||||
|
* NonCacheable/.ncache → cmake/linker/..._app_slot.ld отображает её на
|
||||||
|
* m_sdram_ncache (2 МБ в начале 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,
|
||||||
|
* тот же макрос, framebuffer/alpha_buffer/processing_buffer). Без этого CPU
|
||||||
|
* писал бы через Write-Back D-Cache (Region 8: SDRAM WB Cacheable), и ELCDIF
|
||||||
|
* читал бы устаревшие данные, пока кэш-линия не вытеснится сама.
|
||||||
|
*
|
||||||
|
* Размер — под ТЕКУЩУЮ панель стенда (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)
|
||||||
|
#define FALLBACK_CHAR '-'
|
||||||
|
|
||||||
|
AT_NONCACHEABLE_SECTION_ALIGN(static uint32_t s_framebuffer[FRAMEBUFFER_PIXELS], FRAMEBUFFER_ALIGN);
|
||||||
|
|
||||||
|
static uint16_t s_fb_width;
|
||||||
|
static uint16_t s_fb_height;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Поиск глифа (бинарный — chars[] отсортирован по code, гарантия формата
|
||||||
|
* lcd-image-converter) — порт draw_char()+get_character_width() из
|
||||||
|
* OLD_PROJECT source/fonts/fonts.c, унифицировано в одну функцию (там был
|
||||||
|
* бинарный поиск в draw_char() и отдельный линейный в get_character_width()
|
||||||
|
* — один и тот же инвариант сортировки, лишнее дублирование). ─────────── */
|
||||||
|
static const tImage *find_glyph(const tFont *p_font, long code)
|
||||||
|
{
|
||||||
|
int low = 0;
|
||||||
|
int high = p_font->length - 1;
|
||||||
|
|
||||||
|
while (low <= high)
|
||||||
|
{
|
||||||
|
const int mid = low + (high - low) / 2;
|
||||||
|
if (p_font->chars[mid].code == code)
|
||||||
|
{
|
||||||
|
return p_font->chars[mid].image;
|
||||||
|
}
|
||||||
|
if (p_font->chars[mid].code < code)
|
||||||
|
{
|
||||||
|
low = mid + 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
high = mid - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── RLE-декодирование глифа — порт draw_char() из OLD_PROJECT
|
||||||
|
* source/fonts/fonts.c (проверенный в проде алгоритм, формат — как
|
||||||
|
* реально экспортирует lcd-image-converter, "RLE compression enabled").
|
||||||
|
*
|
||||||
|
* Поток uint32_t, каждый блок начинается с заголовка:
|
||||||
|
* (header & 0xFFFFFF00) == 0xFFFFFF00 → UNIQUE: len = 0x100-(header&0xFF)
|
||||||
|
* уникальных пикселей подряд следуют в потоке (по одному слову каждый).
|
||||||
|
* иначе → REPEATABLE: len = header&0xFFFF
|
||||||
|
* повторений ОДНОГО пикселя (следующее слово потока, читается один раз).
|
||||||
|
* Пиксели — ARGB8888, порядок row-major, перенос строки на границе width
|
||||||
|
* (advance_pixel в оригинале). ──────────────────────────────────────────── */
|
||||||
|
#define UNIQUE_BLOCK_MASK 0xFFFFFF00U
|
||||||
|
|
||||||
|
static void draw_glyph(const tImage *p_image, uint16_t x_pos, uint16_t y_pos)
|
||||||
|
{
|
||||||
|
const uint32_t total = (uint32_t) p_image->width * p_image->height;
|
||||||
|
|
||||||
|
uint32_t in_idx = 0U;
|
||||||
|
uint32_t out_n = 0U;
|
||||||
|
uint32_t col = 0U;
|
||||||
|
uint32_t row = 0U;
|
||||||
|
|
||||||
|
while (out_n < total)
|
||||||
|
{
|
||||||
|
const uint32_t header = p_image->data[in_idx++];
|
||||||
|
|
||||||
|
if ((header & UNIQUE_BLOCK_MASK) == UNIQUE_BLOCK_MASK)
|
||||||
|
{
|
||||||
|
const uint32_t len = 0x100U - (header & 0xFFU);
|
||||||
|
for (uint32_t i = 0U; (i < len) && (out_n < total); i++)
|
||||||
|
{
|
||||||
|
set_pixel((uint16_t) (x_pos + col), (uint16_t) (y_pos + row), p_image->data[in_idx]);
|
||||||
|
col++;
|
||||||
|
if (col >= p_image->width)
|
||||||
|
{
|
||||||
|
col = 0U;
|
||||||
|
row++;
|
||||||
|
}
|
||||||
|
out_n++;
|
||||||
|
in_idx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const uint32_t len = header & 0xFFFFU;
|
||||||
|
const uint32_t pixel = p_image->data[in_idx];
|
||||||
|
for (uint32_t i = 0U; (i < len) && (out_n < total); i++)
|
||||||
|
{
|
||||||
|
set_pixel((uint16_t) (x_pos + col), (uint16_t) (y_pos + row), pixel);
|
||||||
|
col++;
|
||||||
|
if (col >= p_image->width)
|
||||||
|
{
|
||||||
|
col = 0U;
|
||||||
|
row++;
|
||||||
|
}
|
||||||
|
out_n++;
|
||||||
|
}
|
||||||
|
in_idx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Декодирование одного символа строки, включая 2-байтовый UTF-8
|
||||||
|
* (кириллица) — порт логики из OLD_PROJECT draw_string(): ведущие байты
|
||||||
|
* 0xD0/0xD1 комбинируются со следующим байтом в один code, как их кодирует
|
||||||
|
* lcd-image-converter в tChar.code для таких шрифтов. ──────────────────── */
|
||||||
|
static long next_codepoint(const char *p_str, uint8_t *p_consumed)
|
||||||
|
{
|
||||||
|
const uint8_t lead = (uint8_t) p_str[0];
|
||||||
|
|
||||||
|
if (((lead == 0xD0U) || (lead == 0xD1U)) && (p_str[1] != '\0'))
|
||||||
|
{
|
||||||
|
*p_consumed = 2U;
|
||||||
|
return ((long) lead << 8) | (uint8_t) p_str[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
*p_consumed = 1U;
|
||||||
|
return (long) lead;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* find_glyph() с fallback-подстановкой '-' (ARCH §11: по-символьный fallback
|
||||||
|
* на отсутствующий в шрифте символ — политика подтверждена: '-' — общий
|
||||||
|
* заменитель во FloorFontFallback). Общая для draw_string/string_width. */
|
||||||
|
static const tImage *find_glyph_with_fallback(const tFont *p_font, long code)
|
||||||
|
{
|
||||||
|
const tImage *p_glyph = find_glyph(p_font, code);
|
||||||
|
if (p_glyph == NULL)
|
||||||
|
{
|
||||||
|
p_glyph = find_glyph(p_font, (long) FALLBACK_CHAR);
|
||||||
|
}
|
||||||
|
return p_glyph; /* NULL, если даже '-' нет в этом шрифте */
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t gfx_draw_string(const tFont *p_font, const char *p_str, uint16_t x, uint16_t y)
|
||||||
|
{
|
||||||
|
if ((p_font == NULL) || (p_str == NULL))
|
||||||
|
{
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t offset = 0U;
|
||||||
|
|
||||||
|
while (*p_str != '\0')
|
||||||
|
{
|
||||||
|
uint8_t consumed = 1U;
|
||||||
|
const long code = next_codepoint(p_str, &consumed);
|
||||||
|
p_str += consumed;
|
||||||
|
|
||||||
|
const tImage *p_glyph = find_glyph_with_fallback(p_font, code);
|
||||||
|
if (p_glyph == NULL)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_glyph(p_glyph, (uint16_t) (x + offset), y);
|
||||||
|
offset = (uint16_t) (offset + p_glyph->width);
|
||||||
|
}
|
||||||
|
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t gfx_string_width(const tFont *p_font, const char *p_str)
|
||||||
|
{
|
||||||
|
if ((p_font == NULL) || (p_str == NULL))
|
||||||
|
{
|
||||||
|
return 0U;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t width = 0U;
|
||||||
|
|
||||||
|
while (*p_str != '\0')
|
||||||
|
{
|
||||||
|
uint8_t consumed = 1U;
|
||||||
|
const long code = next_codepoint(p_str, &consumed);
|
||||||
|
p_str += consumed;
|
||||||
|
|
||||||
|
const tImage *p_glyph = find_glyph_with_fallback(p_font, code);
|
||||||
|
if (p_glyph != NULL)
|
||||||
|
{
|
||||||
|
width = (uint16_t) (width + p_glyph->width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Стрелка — примитив (без спрайтов, ARCH §11 fallback asset-free) ────── */
|
||||||
|
|
||||||
|
void gfx_draw_arrow(gfx_arrow_dir_t dir, uint16_t x, uint16_t y, uint16_t size, gfx_color_t color)
|
||||||
|
{
|
||||||
|
if (size < 2U)
|
||||||
|
{
|
||||||
|
return; /* вырожденный размер — не рисуем, не делим на (size-1)=0 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Равнобедренный треугольник построчной заливкой: half_width растёт
|
||||||
|
* линейно от 0 (вершина) до size/2 (основание). */
|
||||||
|
for (uint16_t row = 0U; row < size; row++)
|
||||||
|
{
|
||||||
|
const uint16_t half_width = (uint16_t) (((uint32_t) row * (size / 2U)) / (size - 1U));
|
||||||
|
const uint16_t center = (uint16_t) (x + size / 2U);
|
||||||
|
const uint16_t py = (dir == GFX_ARROW_UP) ? (uint16_t) (y + row) : (uint16_t) (y + (size - 1U) - row);
|
||||||
|
|
||||||
|
for (uint16_t dx = 0U; dx <= half_width; dx++)
|
||||||
|
{
|
||||||
|
set_pixel((uint16_t) (center - dx), py, color);
|
||||||
|
set_pixel((uint16_t) (center + dx), py, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Framebuffer / 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);
|
||||||
|
if (st != BSP_OK)
|
||||||
|
{
|
||||||
|
return st;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bsp_display_size_t *p_size = bsp_display_get_size();
|
||||||
|
s_fb_width = p_size->width;
|
||||||
|
s_fb_height = p_size->height;
|
||||||
|
|
||||||
|
return BSP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void gfx_clear(gfx_color_t color)
|
||||||
|
{
|
||||||
|
const uint32_t count = (uint32_t) s_fb_width * s_fb_height;
|
||||||
|
|
||||||
|
for (uint32_t i = 0U; i < count; i++)
|
||||||
|
{
|
||||||
|
s_framebuffer[i] = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
firmware/tft_app/src/ui/CMakeLists.txt
Normal file
1
firmware/tft_app/src/ui/CMakeLists.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
add_subdirectory(fallback)
|
||||||
8
firmware/tft_app/src/ui/fallback/CMakeLists.txt
Normal file
8
firmware/tft_app/src/ui/fallback/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
add_library(tft_app_ui_fallback STATIC src/fallback.c)
|
||||||
|
|
||||||
|
target_include_directories(tft_app_ui_fallback PUBLIC include/)
|
||||||
|
|
||||||
|
target_link_libraries(
|
||||||
|
tft_app_ui_fallback
|
||||||
|
PUBLIC tft_app_elevator_model tft_app_controller
|
||||||
|
PRIVATE tft_app_gfx)
|
||||||
42
firmware/tft_app/src/ui/fallback/include/ui/fallback.h
Normal file
42
firmware/tft_app/src/ui/fallback/include/ui/fallback.h
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
/**
|
||||||
|
* @file fallback.h
|
||||||
|
* @brief Fallback-рендер (ARCH §11): позиция кабины + стрелка направления,
|
||||||
|
* asset-free, FS-free. Один вкомпилированный шрифт (FloorFontFallback),
|
||||||
|
* примитив стрелки — без TLV/layout-движка (те — Фаза 4/5).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef UI_FALLBACK_H_
|
||||||
|
#define UI_FALLBACK_H_
|
||||||
|
|
||||||
|
#include "domain/controller.h"
|
||||||
|
#include "domain/elevator_model.h"
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Безусловная первая отрисовка при старте.
|
||||||
|
*
|
||||||
|
* controller_init() засеивает кэш дефолтом — если первый реальный кадр
|
||||||
|
* совпадёт с дефолтом, indication_task_t придёт «ничего не изменилось», и
|
||||||
|
* экран останется пустым, если полагаться только на ui_fallback_render().
|
||||||
|
* Вызвать один раз при старте до входа в цикл получения кадров.
|
||||||
|
*/
|
||||||
|
void ui_fallback_render_initial(const sul_result_t *p_result);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Инкрементальная перерисовка по diff.
|
||||||
|
*
|
||||||
|
* Фаза 1: перерисовывает весь кадр целиком при ЛЮБОМ pending-поле (нет
|
||||||
|
* partial-update — gfx минимален, без dirty-rect). No-op, если ничего не
|
||||||
|
* помечено.
|
||||||
|
*/
|
||||||
|
void ui_fallback_render(const indication_task_t *p_task, const sul_result_t *p_result);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* UI_FALLBACK_H_ */
|
||||||
44
firmware/tft_app/src/ui/fallback/src/fallback.c
Normal file
44
firmware/tft_app/src/ui/fallback/src/fallback.c
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
#include "ui/fallback.h"
|
||||||
|
|
||||||
|
#include "services/gfx.h"
|
||||||
|
|
||||||
|
/* Фаза 1: позиции захардкожены под TFT8 (800×600, panel текущего стенда).
|
||||||
|
* Цифра '0' FloorFontFallback — 132×162 px (проверено по сгенерированным
|
||||||
|
* данным шрифта). Фаза 5 (layout-движок) заменит на якорное позиционирование,
|
||||||
|
* независимое от разрешения панели. */
|
||||||
|
#define PANEL_WIDTH 800U
|
||||||
|
#define POS_Y 150U
|
||||||
|
#define ARROW_Y 380U
|
||||||
|
#define ARROW_SIZE 80U
|
||||||
|
|
||||||
|
static void render(const sul_result_t *p_result)
|
||||||
|
{
|
||||||
|
gfx_clear(GFX_COLOR_BLACK);
|
||||||
|
|
||||||
|
const uint16_t str_width = gfx_string_width(&FloorFontFallback, p_result->pos);
|
||||||
|
const uint16_t pos_x = (uint16_t) ((PANEL_WIDTH - str_width) / 2U);
|
||||||
|
(void) gfx_draw_string(&FloorFontFallback, p_result->pos, pos_x, POS_Y);
|
||||||
|
|
||||||
|
/* SUL_DIR_NONE — без стрелки. SUL_DIR_DOUBLE — тоже (не путать с
|
||||||
|
* up/down одной стрелкой; отдельная индикация — Фаза 2). */
|
||||||
|
if ((p_result->direction == SUL_DIR_UP) || (p_result->direction == SUL_DIR_DOWN))
|
||||||
|
{
|
||||||
|
const gfx_arrow_dir_t dir = (p_result->direction == SUL_DIR_UP) ? GFX_ARROW_UP : GFX_ARROW_DOWN;
|
||||||
|
const uint16_t arrow_x = (uint16_t) ((PANEL_WIDTH - ARROW_SIZE) / 2U);
|
||||||
|
gfx_draw_arrow(dir, arrow_x, ARROW_Y, ARROW_SIZE, GFX_COLOR_WHITE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ui_fallback_render_initial(const sul_result_t *p_result)
|
||||||
|
{
|
||||||
|
render(p_result);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ui_fallback_render(const indication_task_t *p_task, const sul_result_t *p_result)
|
||||||
|
{
|
||||||
|
if (!p_task->pos_pending && !p_task->direction_pending)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
render(p_result);
|
||||||
|
}
|
||||||
|
|
@ -323,3 +323,31 @@ add_host_test(
|
||||||
test_tft_app_smoke
|
test_tft_app_smoke
|
||||||
SOURCES
|
SOURCES
|
||||||
tft_app_smoke/test_tft_app_smoke.c)
|
tft_app_smoke/test_tft_app_smoke.c)
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# tft_app — domain (Фаза 1: чистый декодер НКУ-CAN + контроллер)
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
set(TFT_APP_DOMAIN_DIR ${PROJECT_SOURCE_DIR}/firmware/tft_app/src/domain)
|
||||||
|
|
||||||
|
add_host_test(
|
||||||
|
NAME
|
||||||
|
test_sul_nku_can
|
||||||
|
SOURCES
|
||||||
|
tft_app_sul_nku/test_sul_nku_can.c
|
||||||
|
${TFT_APP_DOMAIN_DIR}/elevator_model/src/elevator_model.c
|
||||||
|
${TFT_APP_DOMAIN_DIR}/sul/nku_can/src/nku_can.c
|
||||||
|
INCLUDES
|
||||||
|
${TFT_APP_DOMAIN_DIR}/elevator_model/include
|
||||||
|
${TFT_APP_DOMAIN_DIR}/sul/include
|
||||||
|
${TFT_APP_DOMAIN_DIR}/sul/nku_can/include)
|
||||||
|
|
||||||
|
add_host_test(
|
||||||
|
NAME
|
||||||
|
test_tft_app_controller
|
||||||
|
SOURCES
|
||||||
|
tft_app_controller/test_controller.c
|
||||||
|
${TFT_APP_DOMAIN_DIR}/elevator_model/src/elevator_model.c
|
||||||
|
${TFT_APP_DOMAIN_DIR}/controller/src/controller.c
|
||||||
|
INCLUDES
|
||||||
|
${TFT_APP_DOMAIN_DIR}/elevator_model/include
|
||||||
|
${TFT_APP_DOMAIN_DIR}/controller/include)
|
||||||
|
|
|
||||||
124
tests/host/tft_app_controller/test_controller.c
Normal file
124
tests/host/tft_app_controller/test_controller.c
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
/**
|
||||||
|
* @file test_controller.c
|
||||||
|
* @brief Host unit-тесты controller.c — diff между кэшем и новым sul_result_t.
|
||||||
|
*
|
||||||
|
* Чистый C, без единого HAL-вызова — fff-моков не нужно.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "unity.h"
|
||||||
|
|
||||||
|
#include "domain/controller.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
void setUp(void) {}
|
||||||
|
void tearDown(void) {}
|
||||||
|
|
||||||
|
static sul_result_t make_result(const char *p_pos, sul_direction_t dir)
|
||||||
|
{
|
||||||
|
sul_result_t r = sul_default_state();
|
||||||
|
(void) snprintf(r.pos, SUL_POS_BUF_LEN, "%s", p_pos);
|
||||||
|
r.direction = dir;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Кэш засеян дефолтом при init — результат, совпадающий с дефолтом, не
|
||||||
|
* считается изменением (см. докстрок controller_init()). */
|
||||||
|
static void test_first_frame_matching_default_has_no_pending(void)
|
||||||
|
{
|
||||||
|
controller_ctx_t ctx;
|
||||||
|
controller_init(&ctx);
|
||||||
|
|
||||||
|
const sul_result_t r = sul_default_state();
|
||||||
|
const indication_task_t task = controller_process(&ctx, &r);
|
||||||
|
|
||||||
|
TEST_ASSERT_FALSE(task.pos_pending);
|
||||||
|
TEST_ASSERT_FALSE(task.direction_pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_first_frame_differing_from_default_marks_both_pending(void)
|
||||||
|
{
|
||||||
|
controller_ctx_t ctx;
|
||||||
|
controller_init(&ctx);
|
||||||
|
|
||||||
|
const sul_result_t r = make_result("12", SUL_DIR_UP);
|
||||||
|
const indication_task_t task = controller_process(&ctx, &r);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(task.pos_pending);
|
||||||
|
TEST_ASSERT_TRUE(task.direction_pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_repeated_identical_result_has_no_pending(void)
|
||||||
|
{
|
||||||
|
controller_ctx_t ctx;
|
||||||
|
controller_init(&ctx);
|
||||||
|
|
||||||
|
const sul_result_t r = make_result("12", SUL_DIR_UP);
|
||||||
|
(void) controller_process(&ctx, &r);
|
||||||
|
|
||||||
|
const indication_task_t task = controller_process(&ctx, &r);
|
||||||
|
|
||||||
|
TEST_ASSERT_FALSE(task.pos_pending);
|
||||||
|
TEST_ASSERT_FALSE(task.direction_pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_only_pos_change_marks_only_pos_pending(void)
|
||||||
|
{
|
||||||
|
controller_ctx_t ctx;
|
||||||
|
controller_init(&ctx);
|
||||||
|
|
||||||
|
const sul_result_t r1 = make_result("12", SUL_DIR_UP);
|
||||||
|
(void) controller_process(&ctx, &r1);
|
||||||
|
|
||||||
|
const sul_result_t r2 = make_result("13", SUL_DIR_UP);
|
||||||
|
const indication_task_t task = controller_process(&ctx, &r2);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(task.pos_pending);
|
||||||
|
TEST_ASSERT_FALSE(task.direction_pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_only_direction_change_marks_only_direction_pending(void)
|
||||||
|
{
|
||||||
|
controller_ctx_t ctx;
|
||||||
|
controller_init(&ctx);
|
||||||
|
|
||||||
|
const sul_result_t r1 = make_result("12", SUL_DIR_UP);
|
||||||
|
(void) controller_process(&ctx, &r1);
|
||||||
|
|
||||||
|
const sul_result_t r2 = make_result("12", SUL_DIR_DOWN);
|
||||||
|
const indication_task_t task = controller_process(&ctx, &r2);
|
||||||
|
|
||||||
|
TEST_ASSERT_FALSE(task.pos_pending);
|
||||||
|
TEST_ASSERT_TRUE(task.direction_pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Таймаут связи = caller зовёт controller_process(sul_default_state()) тем
|
||||||
|
* же путём, что и обычный кадр — отдельного API не существует (см. header). */
|
||||||
|
static void test_feeding_default_state_after_real_data_marks_both_pending(void)
|
||||||
|
{
|
||||||
|
controller_ctx_t ctx;
|
||||||
|
controller_init(&ctx);
|
||||||
|
|
||||||
|
const sul_result_t r1 = make_result("12", SUL_DIR_UP);
|
||||||
|
(void) controller_process(&ctx, &r1);
|
||||||
|
|
||||||
|
const sul_result_t timeout_result = sul_default_state();
|
||||||
|
const indication_task_t task = controller_process(&ctx, &timeout_result);
|
||||||
|
|
||||||
|
TEST_ASSERT_TRUE(task.pos_pending);
|
||||||
|
TEST_ASSERT_TRUE(task.direction_pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
UNITY_BEGIN();
|
||||||
|
|
||||||
|
RUN_TEST(test_first_frame_matching_default_has_no_pending);
|
||||||
|
RUN_TEST(test_first_frame_differing_from_default_marks_both_pending);
|
||||||
|
RUN_TEST(test_repeated_identical_result_has_no_pending);
|
||||||
|
RUN_TEST(test_only_pos_change_marks_only_pos_pending);
|
||||||
|
RUN_TEST(test_only_direction_change_marks_only_direction_pending);
|
||||||
|
RUN_TEST(test_feeding_default_state_after_real_data_marks_both_pending);
|
||||||
|
|
||||||
|
return UNITY_END();
|
||||||
|
}
|
||||||
262
tests/host/tft_app_sul_nku/test_sul_nku_can.c
Normal file
262
tests/host/tft_app_sul_nku/test_sul_nku_can.c
Normal file
|
|
@ -0,0 +1,262 @@
|
||||||
|
/**
|
||||||
|
* @file test_sul_nku_can.c
|
||||||
|
* @brief Host unit-тесты декодера НКУ-CAN (Фаза 1: PACKET1 + PACKET3).
|
||||||
|
*
|
||||||
|
* Чистый декодер, без единого HAL-вызова — никаких fff-моков не нужно,
|
||||||
|
* golden-векторы CAN-кадров строятся прямо в тесте.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "unity.h"
|
||||||
|
|
||||||
|
#include "domain/sul/nku_can.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
void setUp(void) {}
|
||||||
|
void tearDown(void) {}
|
||||||
|
|
||||||
|
/* ── Вспомогательные конструкторы кадров ─────────────────────────────── */
|
||||||
|
|
||||||
|
#define PACKET1_ID 0x506U
|
||||||
|
#define PACKET3_ID 0x508U
|
||||||
|
#define UNKNOWN_ID 0x123U
|
||||||
|
|
||||||
|
static sul_frame_t make_packet1(uint8_t arrow_bits)
|
||||||
|
{
|
||||||
|
static uint8_t s_data[8];
|
||||||
|
memset(s_data, 0, sizeof(s_data));
|
||||||
|
s_data[6] = arrow_bits; /* ARROW_MASK=0x03 на data[6] (DATA7) */
|
||||||
|
return (sul_frame_t){.id = PACKET1_ID, .bus = 0, .p_data = s_data, .len = 8U};
|
||||||
|
}
|
||||||
|
|
||||||
|
static sul_frame_t make_packet3(uint8_t left, uint8_t right)
|
||||||
|
{
|
||||||
|
static uint8_t s_data[8];
|
||||||
|
memset(s_data, 0, sizeof(s_data));
|
||||||
|
s_data[5] = left; /* FLOOR_MASK=0x3F на data[5] (left) */
|
||||||
|
s_data[6] = right; /* FLOOR_MASK=0x3F на data[6] (right) */
|
||||||
|
return (sul_frame_t){.id = PACKET3_ID, .bus = 0, .p_data = s_data, .len = 8U};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── PACKET1 — направление ───────────────────────────────────────────── */
|
||||||
|
|
||||||
|
static void test_packet1_none(void)
|
||||||
|
{
|
||||||
|
nku_can_ctx_t ctx;
|
||||||
|
nku_can_init(&ctx);
|
||||||
|
sul_result_t out;
|
||||||
|
const sul_frame_t frame = make_packet1(0U);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &frame, &out));
|
||||||
|
TEST_ASSERT_EQUAL(SUL_DIR_NONE, out.direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_packet1_up(void)
|
||||||
|
{
|
||||||
|
nku_can_ctx_t ctx;
|
||||||
|
nku_can_init(&ctx);
|
||||||
|
sul_result_t out;
|
||||||
|
const sul_frame_t frame = make_packet1(1U);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &frame, &out));
|
||||||
|
TEST_ASSERT_EQUAL(SUL_DIR_UP, out.direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_packet1_down(void)
|
||||||
|
{
|
||||||
|
nku_can_ctx_t ctx;
|
||||||
|
nku_can_init(&ctx);
|
||||||
|
sul_result_t out;
|
||||||
|
const sul_frame_t frame = make_packet1(2U);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &frame, &out));
|
||||||
|
TEST_ASSERT_EQUAL(SUL_DIR_DOWN, out.direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_packet1_double(void)
|
||||||
|
{
|
||||||
|
nku_can_ctx_t ctx;
|
||||||
|
nku_can_init(&ctx);
|
||||||
|
sul_result_t out;
|
||||||
|
const sul_frame_t frame = make_packet1(3U);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &frame, &out));
|
||||||
|
TEST_ASSERT_EQUAL(SUL_DIR_DOUBLE, out.direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Верхние биты data[6] (выше ARROW_MASK) не должны влиять на результат. */
|
||||||
|
static void test_packet1_ignores_bits_outside_mask(void)
|
||||||
|
{
|
||||||
|
nku_can_ctx_t ctx;
|
||||||
|
nku_can_init(&ctx);
|
||||||
|
sul_result_t out;
|
||||||
|
const sul_frame_t frame = make_packet1(0xFCU | 1U); /* мусор в старших битах + arrow=1 */
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &frame, &out));
|
||||||
|
TEST_ASSERT_EQUAL(SUL_DIR_UP, out.direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── PACKET3 — позиция ───────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
static void test_packet3_two_digit_floor(void)
|
||||||
|
{
|
||||||
|
nku_can_ctx_t ctx;
|
||||||
|
nku_can_init(&ctx);
|
||||||
|
sul_result_t out;
|
||||||
|
const sul_frame_t frame = make_packet3(1U, 2U); /* "1","2" -> "12" */
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &frame, &out));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("12", out.pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_packet3_single_digit_via_space(void)
|
||||||
|
{
|
||||||
|
nku_can_ctx_t ctx;
|
||||||
|
nku_can_init(&ctx);
|
||||||
|
sul_result_t out;
|
||||||
|
const sul_frame_t frame = make_packet3(16U, 5U); /* left=SPACE, right="5" -> "5" */
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &frame, &out));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("5", out.pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Легаси-квирк: left==0 трактуется как «пусто», как и SPACE — не «0» + right. */
|
||||||
|
static void test_packet3_single_digit_via_zero_byte(void)
|
||||||
|
{
|
||||||
|
nku_can_ctx_t ctx;
|
||||||
|
nku_can_init(&ctx);
|
||||||
|
sul_result_t out;
|
||||||
|
const sul_frame_t frame = make_packet3(0U, 7U);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &frame, &out));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("7", out.pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_packet3_negative_floor(void)
|
||||||
|
{
|
||||||
|
nku_can_ctx_t ctx;
|
||||||
|
nku_can_init(&ctx);
|
||||||
|
sul_result_t out;
|
||||||
|
const sul_frame_t frame = make_packet3(22U, 1U); /* '-','1' -> "-1" */
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &frame, &out));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("-1", out.pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_packet3_cyrillic_single_char(void)
|
||||||
|
{
|
||||||
|
nku_can_ctx_t ctx;
|
||||||
|
nku_can_init(&ctx);
|
||||||
|
sul_result_t out;
|
||||||
|
const sul_frame_t frame = make_packet3(16U, 17U); /* SPACE, symbol_P -> "П" */
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &frame, &out));
|
||||||
|
TEST_ASSERT_EQUAL_STRING("П", out.pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_packet3_out_of_range_symbol_is_error(void)
|
||||||
|
{
|
||||||
|
nku_can_ctx_t ctx;
|
||||||
|
nku_can_init(&ctx);
|
||||||
|
sul_result_t out;
|
||||||
|
const sul_frame_t frame = make_packet3(1U, 63U); /* 63 > SYMBOL_TOTAL-1(37) */
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(SUL_STATUS_ERR, nku_can_decode(&ctx, &frame, &out));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Кадры не по протоколу ────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
static void test_unknown_id_is_ignored_and_out_untouched(void)
|
||||||
|
{
|
||||||
|
nku_can_ctx_t ctx;
|
||||||
|
nku_can_init(&ctx);
|
||||||
|
sul_result_t out;
|
||||||
|
out.direction = SUL_DIR_UP; /* сентинел, decode() не должен его тронуть */
|
||||||
|
(void) memset(out.pos, 'X', sizeof(out.pos));
|
||||||
|
|
||||||
|
uint8_t data[8] = {0};
|
||||||
|
const sul_frame_t frame = {.id = UNKNOWN_ID, .bus = 0, .p_data = data, .len = 8U};
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(SUL_STATUS_IGNORED, nku_can_decode(&ctx, &frame, &out));
|
||||||
|
TEST_ASSERT_EQUAL(SUL_DIR_UP, out.direction); /* не тронут */
|
||||||
|
TEST_ASSERT_EQUAL_CHAR('X', out.pos[0]); /* не тронут */
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_wrong_dlc_on_packet1_id_is_error(void)
|
||||||
|
{
|
||||||
|
nku_can_ctx_t ctx;
|
||||||
|
nku_can_init(&ctx);
|
||||||
|
sul_result_t out;
|
||||||
|
uint8_t data[8] = {0};
|
||||||
|
const sul_frame_t frame = {.id = PACKET1_ID, .bus = 0, .p_data = data, .len = 6U};
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(SUL_STATUS_ERR, nku_can_decode(&ctx, &frame, &out));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_wrong_dlc_on_packet3_id_is_error(void)
|
||||||
|
{
|
||||||
|
nku_can_ctx_t ctx;
|
||||||
|
nku_can_init(&ctx);
|
||||||
|
sul_result_t out;
|
||||||
|
uint8_t data[8] = {0};
|
||||||
|
const sul_frame_t frame = {.id = PACKET3_ID, .bus = 0, .p_data = data, .len = 3U};
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL(SUL_STATUS_ERR, nku_can_decode(&ctx, &frame, &out));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Накопление состояния между разными пакетами ─────────────────────── */
|
||||||
|
|
||||||
|
/* PACKET1 и PACKET3 несут разные поля — второй вызов обязан вернуть ПОЛНОЕ
|
||||||
|
* накопленное состояние (и pos, и direction), а не только то, что пришло в
|
||||||
|
* последнем кадре. Это ключевое свойство stateful-ctx декодера. */
|
||||||
|
static void test_state_accumulates_across_packet_types(void)
|
||||||
|
{
|
||||||
|
nku_can_ctx_t ctx;
|
||||||
|
nku_can_init(&ctx);
|
||||||
|
sul_result_t out;
|
||||||
|
|
||||||
|
const sul_frame_t f1 = make_packet1(1U); /* UP */
|
||||||
|
TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &f1, &out));
|
||||||
|
|
||||||
|
const sul_frame_t f3 = make_packet3(1U, 2U); /* "12" */
|
||||||
|
TEST_ASSERT_EQUAL(SUL_STATUS_OK, nku_can_decode(&ctx, &f3, &out));
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_STRING("12", out.pos);
|
||||||
|
TEST_ASSERT_EQUAL(SUL_DIR_UP, out.direction); /* пережило кадр PACKET3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_init_resets_to_default(void)
|
||||||
|
{
|
||||||
|
nku_can_ctx_t ctx;
|
||||||
|
nku_can_init(&ctx);
|
||||||
|
|
||||||
|
TEST_ASSERT_EQUAL_STRING("--", ctx.state.pos);
|
||||||
|
TEST_ASSERT_EQUAL(SUL_DIR_NONE, ctx.state.direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
UNITY_BEGIN();
|
||||||
|
|
||||||
|
RUN_TEST(test_packet1_none);
|
||||||
|
RUN_TEST(test_packet1_up);
|
||||||
|
RUN_TEST(test_packet1_down);
|
||||||
|
RUN_TEST(test_packet1_double);
|
||||||
|
RUN_TEST(test_packet1_ignores_bits_outside_mask);
|
||||||
|
|
||||||
|
RUN_TEST(test_packet3_two_digit_floor);
|
||||||
|
RUN_TEST(test_packet3_single_digit_via_space);
|
||||||
|
RUN_TEST(test_packet3_single_digit_via_zero_byte);
|
||||||
|
RUN_TEST(test_packet3_negative_floor);
|
||||||
|
RUN_TEST(test_packet3_cyrillic_single_char);
|
||||||
|
RUN_TEST(test_packet3_out_of_range_symbol_is_error);
|
||||||
|
|
||||||
|
RUN_TEST(test_unknown_id_is_ignored_and_out_untouched);
|
||||||
|
RUN_TEST(test_wrong_dlc_on_packet1_id_is_error);
|
||||||
|
RUN_TEST(test_wrong_dlc_on_packet3_id_is_error);
|
||||||
|
|
||||||
|
RUN_TEST(test_state_accumulates_across_packet_types);
|
||||||
|
RUN_TEST(test_init_resets_to_default);
|
||||||
|
|
||||||
|
return UNITY_END();
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue