Integrace bez RTOS: XCP on TCP z hlavní smyčky

Příklad examples/nucleo_h723zg_baremetal je tatáž deska a tentýž obsah ukázky jako examples/nucleo_h723zg, přeložený bez FreeRTOS a bez socketů: lwIP běží v režimu NO_SYS a XCP server tvoří šablona glue glue/baremetal_lwip_raw/xcp_tcp_raw_glue.c, řízená z hlavní smyčky. Vše v této kapitole je to, co se mění oproti kapitole Integrace: XCP on TCP; engine, kontrakt enginu, pravidla pro proměnné a nástroje pro adresy jsou stejné.

Stejné a odlišné

Stejné jako s FreeRTOSOdlišné bez RTOS
zdroje enginu, qfw_xcp.h, cmake/qfw_xcp.cmake, QFW_XCP_TRANSPORT=ethglue: glue/baremetal_lwip_raw/xcp_tcp_raw_glue.c místo glue/freertos_lwip/xcp_sdk_glue.c — lwIP RAW (callback) API, žádná úloha, žádný socket
port hodin a kontrakt hodinAPI glue: QfwXcpTcpRawGlue_Init(&config) jednou a navíc QfwXcpTcpRawGlue_Poll() v každém průchodu hlavní smyčky (poll nahrazuje server task)
identita, úroveň logu a port v konfigurační struktuře (tQfwXcpTcpRawGlueConfig má stejných pět polí jako tQfwXcpGlueConfig)model kontextů: transport v hlavní smyčce, spouštění DAQ z přerušení — glue dává enginu kritickou sekci PRIMASK (viz níže)
qfw_xcp_create_event / qfw_xcp_trigger_event, daqId v QInsightkde volání sídlí: main.c a stm32h7xx_it.c (sekce USER CODE) místo freertos.c; DAQ event se spouští z přerušení SysTick
opravy lwIP projektu STM32H7 (linker skript, ethernetif.c, viz Řešení potíží)volby lwIP: NO_SYS 1, žádné volby socketů, ale TCP_MSS / TCP_SND_BUF musí být zvětšeny (viz níže)

CubeMX: lwIP bez RTOS

Kde: váš soubor .ioc a LWIP/Target/lwipopts.h. Co se liší od projektu s FreeRTOS:

  1. Middleware → FREERTOS: nezapnuto. SYS → Timebase source: SysTick (HAL_GetTick pohání časovače lwIP přes sys_now i plánovač ukázky).
  2. Connectivity → ETH: jako u projektu s FreeRTOS. Priorita přerušení ETH je volná (bez omezení syscall priority FreeRTOS); v ovladači CubeMX bez RTOS ISR pouze maže příznaky DMA — rámce čte MX_LWIP_Process() z hlavní smyčky. Zachovejte to (viz model kontextů níže).
  3. Middleware → LWIP: CubeMX bez zapnutého RTOS generuje NO_SYS 1, LWIP_NETCONN 0, LWIP_SOCKET 0 automaticky. DHCP vypnuto, statická IP jako u FreeRTOS; MEM_SIZE 16384 a LWIP_RAM_HEAP_POINTER jako v příkladu (do heapu lwIP kopíruje tcp_write segmenty XCP).
  4. USER CODE BEGIN 1 v lwipopts.h — volby, na kterých glue závisí (glue je kontroluje při překladu: #error / _Static_assert):
    VolbaHodnotaProč
    LWIP_TCP_KEEPALIVE1keepalive per spojení (2 s klidu, 3 sondy po 1 s): master, který zmizel za vlastní linkou desky, je odhalen do ~5 s a port je opět volný (kabel vytažený u desky: link callback, viz Spojení níže)
    LWIP_NETIF_TX_SINGLE_PBUF1jeden pbuf na odesílaný segment — ovladač STM32H7 má jen 4 TX deskriptory
    TCP_MSS1460maximum Ethernetu (1500 − 40). S výchozí hodnotou lwIP 536 by se každý plný segment enginu dělil na tři TCP pakety
    TCP_SND_BUF4 * TCP_MSS (5840)povinné. Glue zařadí celý segment enginu (až 1480 B) jedním tcp_write, nebo vůbec; výchozí hodnota lwIP (2 × 536 = 1072) by plný segment nepřijala nikdy a engine by zůstal trvale v backpressure. Čtyři segmenty v letu jsou dobrý výchozí stav

    Všechny čtyři jsou chráněné #ifndef a nejsou v .ioc — GUI CubeMX dál ukazuje výchozí hodnoty; blok USER CODE regeneraci přežije. Hodnoty, které CubeMX generuje pro výchozí MSS (TCP_SND_QUEUELEN 9, TCP_SNDLOWAT 1071, …), s tímto nastavením kontrolami lwIP stále procházejí (TCP_SND_QUEUELEN ≥ 2 × TCP_SND_BUF / TCP_MSS = 8). Proč celé segmenty: engine zahazuje zařazené segmenty při DISCONNECT, při novém CONNECT a při resetu transportu; napůl zapsaný segment by rozsynchronizoval proud bajtů. Glue pro FreeRTOS si částečný zápis může dovolit, protože ve své úloze může čekat; hlavní smyčka nemůže.
  5. Generate code, poté proveďte opravy pro STM32H7 přesně jako u projektu s FreeRTOS (linker skript, ETH_RX_BUFFER_CNT, ochrana HAL_ETH_RxLinkCallback — REGEN-NOTES.md příkladu bez RTOS).

CMake

Kde: váš kořenový CMakeLists.txt. Jak (CMakeLists.txt příkladu bez RTOS, liší se jen cesta k SDK):

include(${SDK}/cmake/qfw_xcp.cmake)               # transport eth (default)

add_library(qfw_example STATIC
    app/xcp_demo.c                                 # your application: event + cyclic work
    app/demo_map.c
    app/demo_signals.cpp
    ${QFW_XCP_GLUE_BAREMETAL_LWIP_RAW_SOURCES}   # glue/baremetal_lwip_raw/xcp_tcp_raw_glue.c
    ${QFW_XCP_PORT_CLOCK_SOURCES}                # port/clock/clock64.c
)
target_include_directories(qfw_example PUBLIC
    ${CMAKE_CURRENT_SOURCE_DIR}/app
    ${QFW_XCP_GLUE_BAREMETAL_LWIP_RAW_INCLUDE_DIRS}
    ${QFW_XCP_PORT_CLOCK_INCLUDE_DIRS}
)
target_compile_definitions(qfw_example PUBLIC CLOCK_TICKS_PER_S=550000000UL)   # YOUR CPU clock
target_link_libraries(qfw_example PUBLIC qfw::xcp PRIVATE stm32cubemx)

target_link_libraries(${CMAKE_PROJECT_NAME} stm32cubemx qfw_example)

Glue potřebuje hlavičky lwIP vašeho projektu (lwip/tcp.h, lwip/pbuf.h, váš lwipopts.h), proto sedí ve vašem cíli a linkuje stm32cubemx privátně — přesně jako glue pro FreeRTOS.

main.c: jednou Init, pak stále Poll

Kde: Core/Src/main.c, pouze sekce USER CODE. Co: callback hodin, konfigurace glue, dvě inicializační volání a tělo smyčky. Jak (main.c příkladu):

/* USER CODE BEGIN Includes */
#include "clock64.h"
#include "qfw_xcp.h"
#include "xcp_tcp_raw_glue.h"
#include "xcp_demo.h"
/* USER CODE END Includes */

/* USER CODE BEGIN 0 */
static uint64_t XcpClockUs(void)      /* engine clock contract: monotonic 64-bit microseconds */
{
  return Clock64_Get();
}
/* USER CODE END 0 */

int main(void)
{
  ...
  MX_GPIO_Init();
  MX_LWIP_Init();                     /* generated: lwIP is up after this line (NO_SYS: no thread is started) */
  /* USER CODE BEGIN 2 */
  Clock64_Init();
  /* USER CODE END 2 */
  ... BSP LEDs, COM1 (printf) ...

  /* USER CODE BEGIN WHILE */
  tQfwXcpTcpRawGlueConfig xcpConfig =
  {
    .project_name = "baremetal",      /* shown to the master, <= 31 chars */
    .epk = "V100",                    /* software version id */
    .port = 5555U,
    .log_level = 2U,                  /* 1 error, 2 warning, 3 info (adds connection messages) */
    .get_clock_us = XcpClockUs,
  };
  if (!QfwXcpTcpRawGlue_Init(&xcpConfig))   /* engine init + listening pcb */
  {
    Error_Handler();
  }
  if (!QfwXcpDemo_Init())                   /* qfw_xcp_create_event("demo10ms", 10000) -> id 0, maps */
  {
    Error_Handler();
  }

  while (1)
  {
    MX_LWIP_Process();         /* ETH RX + lwIP timers; the glue's TCP callbacks run in here -> qfw_xcp_rx */
    Clock64_Update();          /* single writer of the 64-bit clock, must run well below every 7.8 s */
    QfwXcpTcpRawGlue_Poll();   /* qfw_xcp_poll -> transmit callback -> tcp_write, then one tcp_output */
    QfwXcpDemo_Poll();         /* every 500 ms: map tick (the 10 ms signal step + DAQ event runs in SysTick, see below) */
    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

Proč toto pořadí: QfwXcpTcpRawGlue_Init musí běžet až po MX_LWIP_Init() (vytváří naslouchající pcb) a — chcete-li vidět jeho úvodní hlášku — až po inicializaci COM portu; proto je v sekci USER CODE WHILE těsně před smyčkou, ne v USER CODE 2. Uvnitř smyčky je MX_LWIP_Process první (přijaté příkazy se vykonají a jejich odpovědi zařadí do fronty), poll je v témže průchodu odešle. QfwXcpDemo_Poll (app/xcp_demo.c) je cyklická práce aplikace: plánuje podle HAL_GetTick s termíny odolnými proti přetečení, takže opožděný průchod nedohání dávkou volání — vzor DemoDue převezměte pro vlastní cykly ve smyčce. Rychlý cyklus (krok signálů + DAQ event každých 10 ms) běží v QfwXcpDemo_Tick1ms z přerušení SysTick (USER CODE SysTick_IRQn 1 v stm32h7xx_it.c), viz model kontextů níže. K integraci patří ještě dvě sekce USER CODE:

/* Core/Src/stm32h7xx_it.c */
/* USER CODE BEGIN Includes */
#include "xcp_demo.h"
/* USER CODE END Includes */
void SysTick_Handler(void)
{
  HAL_IncTick();
  /* USER CODE BEGIN SysTick_IRQn 1 */
  QfwXcpDemo_Tick1ms();      /* every 10 ms: generators + qfw_xcp_trigger_event, in interrupt context */
  /* USER CODE END SysTick_IRQn 1 */
}

/* LWIP/App/lwip.c - stav linky do glue */
/* USER CODE BEGIN 0 */
#include "xcp_tcp_raw_glue.h"
/* USER CODE END 0 */
static void ethernet_link_status_updated(struct netif *netif)
{
  if (netif_is_up(netif)) {
/* USER CODE BEGIN 5 */
    QfwXcpTcpRawGlue_OnLinkChange(true);
/* USER CODE END 5 */
  } else {
/* USER CODE BEGIN 6 */
    QfwXcpTcpRawGlue_OnLinkChange(false);   /* master je nedosažitelný: spojení zahodit hned */
/* USER CODE END 6 */
  }
}

Model kontextů, kritická sekce a přerušení

Transport běží v jednom kontextu: v hlavní smyčce. lwIP v režimu NO_SYS běží jen uvnitř MX_LWIP_Process(), takže callback recv glue (→ qfw_xcp_rx) a QfwXcpTcpRawGlue_Poll (→ qfw_xcp_poll) jej sdílejí a pravidlo kontraktu enginu je splněno samotnou konstrukcí.

Měření přichází z druhého kontextu. Glue registruje kritickou sekci PRIMASK (uložit PRIMASK, zakázat přerušení, obnovit při vnějším opuštění, s čítačem zanoření, protože engine sekci vnořuje — viz Kritická sekce). Díky ní smí být qfw_xcp_trigger_event volán z přerušení — příklad spouští svůj event demo10ms ze SysTick_Handler (QfwXcpDemo_Tick1ms, každých 10 ms), tak jako by to dělala regulační smyčka v časovači: vzorky DAQ a zápisy STIM tohoto eventu proběhnou uvnitř ISR, synchronně s cyklem, a perioda vzorkování nezávisí na tom, jak dlouho trvá průchod hlavní smyčky. ISR jen kopíruje do fronty enginu; bajty odejdou z desky v příštím QfwXcpTcpRawGlue_Poll.

Důsledky, které musíte zachovat:

  • Z přerušení jen qfw_xcp_trigger_event. qfw_xcp_rx, qfw_xcp_poll a každé další volání enginu zůstávají v hlavní smyčce.
  • Z přerušení nic z lwIP. Ovladač CubeMX bez RTOS se tak chová už sám (ISR ETH pouze maže příznaky); nepřidávejte ethernetif_input ani volání lwIP do HAL_ETH_RxCpltCallback.
  • Práci v ISR držte krátkou. Sekce zakáže všechna přerušení na dobu režie fronty a kopie DAQ jednoho eventu (mikrosekundy); trigger velkého seznamu eventu v rychlém ISR o tolik zvýší latenci přerušení všem ostatním.
  • Log z ISR je potlačen. Hláška enginu vzniklá uvnitř triggeru se počítá, netiskne (printf blokuje a běžel by se zakázanými přerušeními); počet ohlásí příští Poll jako [XCP W] n engine log line(s) raised in interrupt context not printed. Při měření se tento řádek nesmí objevit.
  • Glue je napsaná pro Cortex-M (__get_PRIMASK / __disable_irq / __set_PRIMASK z CMSIS). Na jiném jádru obě funkce nahraďte ekvivalentní globální maskou přerušení se stejným chováním zanoření.
  • Průchod smyčky je kadence pollu. Dlouhá práce v jednom průchodu zdržuje odpovědi i vysílání DAQ: printf při log_level 3 stojí ≈ 4 ms na řádek na virtuálním COM portu ST-LINK a HAL_ETH_Transmit ovladače CubeMX bez RTOS blokuje, dokud DMA rámec neodešle (≈ 0,12 ms na plný rámec při 100 Mbit/s).

Spojení: co glue dělá a co uvidíte

Politika je stejná jako u glue pro FreeRTOS, provedená nad RAW API:

SituaceCo se stane
master se připojícallback accept: nastaví se keepalive (2 s / 1 s / 3), qfw_xcp_on_transport_reset(), pcb se stane jediným spojením; na úrovni 3 [XCP I] master connected from ip:port
druhý klient se připojí, zatímco je master připojenodmítnut okamžitě pomocí RST (tcp_abort v accept); běžící relace není narušena. [XCP W] refused a second connection from ip:port (master already connected). Druhý QInsight ukazuje jednou za sekundu connected → forcibly closed a připojí se, jakmile první relace skončí
master zavře (FIN) nebo spadne (RST)glue spojení zruší (RST) — každé zavření je abort, takže pcb i všechny zařazené segmenty se uvolní okamžitě; reset enginu; zpět do čekání na mastera
master tiše zmizí za linkou desky (kabel vytažený u switche, vypnuté nebo zamrzlé PC)keepalive spojení zruší ~5 s po posledním přijatém segmentu; do té doby je znovu se připojující master odmítán jako druhý klient — driver QInsight zkouší každou sekundu a po vypršení se připojí
proud bajtů se rozsynchronizuje (qfw_xcp_rx vrátí false)spojení je zrušeno (RST), [XCP E] XCP stream desynchronized
vysílací fronta nebo heap lwIP jsou plné (tcp_write → ERR_MEM)backpressure: callback transmit vrátí false, engine si segment ponechá a při příštím pollu nabídne tytéž bajty; qfw_xcp_is_backpressured() je mezitím pravdivé. Master, který žije, ale přestal číst, drží glue v backpressure, dokud se nepřipojí znovu
výpadek linky u desky (kabel vytažený z desky)link callback (ethernet_link_status_updated v lwip.c → QfwXcpTcpRawGlue_OnLinkChange(false)) přiměje glue spojení s masterem zahodit v příštím Poll (na úrovni 3 [XCP I] connection closed (link down), waiting for a master). Po návratu linky je nové připojení mastera přijato okamžitě; s connectionTimeoutMs 1500 v QInsight je měření zpět zhruba 3,5 s po zasunutí kabelu (vyjednání linky plus jeden pokus, který vypršel, protože začal příliš brzy)

Nagleův algoritmus je zapnutý (výchozí chování TCP, stejně jako v glue pro FreeRTOS): malý paket může čekat na zpožděný ACK mastera (≈ 200 ms ve Windows) — časové značky DAQ to neovlivní, jen příchod pomalých proudů je dávkovaný. Přepínač GLUE_TCP_NODELAY na začátku glue přepne na TCP_NODELAY.

Ukázkový projekt QInsight pro tento obraz (Qenex_TcpClient_XCP_NucleoH723_Maps_BareMetal.qproj) popisuje kapitola Kalibrační mapy po XCP on TCP.

Dále

Kontrakt enginu — pravidla, která za glue stojí; Řešení potíží — řádky pro integraci bez RTOS v tabulce příznaků.