| /* | |
| * SPDX-FileCopyrightText: Copyright (c) 2019 Ha Thach (tinyusb.org) | |
| * SPDX-License-Identifier: MIT | |
| * | |
| * This file is part of the TinyUSB stack. | |
| */ | |
| #include "tusb_option.h" | |
| #if CFG_TUD_SYSVIEW || CFG_TUH_SYSVIEW | |
| #include <stdbool.h> | |
| #include <stddef.h> | |
| #include <stdint.h> | |
| #include "common/tusb_common.h" // TU_ARRAY_SIZE | |
| #include "common/tusb_sysview.h" | |
| #if CFG_TUSB_OS == OPT_OS_FREERTOS | |
| #include "FreeRTOS.h" | |
| #include "task.h" | |
| extern const SEGGER_SYSVIEW_OS_API SYSVIEW_X_OS_TraceAPI; | |
| #define SYSVIEW_OS_API (&SYSVIEW_X_OS_TraceAPI) | |
| #define SYSVIEW_OS_DESC ",O=FreeRTOS" | |
| #else | |
| #define SYSVIEW_OS_API 0 | |
| #define SYSVIEW_OS_DESC "" | |
| #endif | |
| #ifndef SYSVIEW_APP_NAME | |
| #define SYSVIEW_APP_NAME "TinyUSB" | |
| #endif | |
| #ifndef SYSVIEW_DEVICE_NAME | |
| #define SYSVIEW_DEVICE_NAME "Cortex-M" | |
| #endif | |
| #ifndef SYSVIEW_RAM_BASE | |
| #define SYSVIEW_RAM_BASE (0x20000000) | |
| #endif | |
| // CPU clock reported to SystemView (and, on DWT cores, the timestamp frequency). SystemCoreClock | |
| // is a CMSIS global that non-CMSIS SDKs do not have -- the Pico SDK for one -- so route it through | |
| // a weak accessor those BSPs override (hw/bsp/rp2040/family.c). Unused weak definitions are | |
| // dropped by --gc-sections, taking the SystemCoreClock reference with them. | |
| // Most targets' vendor CMSIS/device header already declares SystemCoreClock, but there is no | |
| // single header this file can include to get it across vendors, so declare it and silence the | |
| // duplicate for this line alone. Two narrower attempts do NOT work: gating on | |
| // CFG_TUSB_OS != OPT_OS_FREERTOS (bare-metal mimxrt1064_evk pulls NXP's header too, and failed | |
| // to build at SYSVIEW=4), and moving the extern to block scope (GCC still reports it redundant). | |
| #if defined(__GNUC__) | |
| #pragma GCC diagnostic push | |
| #pragma GCC diagnostic ignored "-Wredundant-decls" | |
| #endif | |
| extern uint32_t SystemCoreClock; | |
| #if defined(__GNUC__) | |
| #pragma GCC diagnostic pop | |
| #endif | |
| TU_ATTR_WEAK uint32_t tusb_sysview_cpu_freq(void) { | |
| return SystemCoreClock; | |
| } | |
| // Kernel objects named for SystemView: sent immediately AND kept, so the names | |
| // survive a recorder that attaches later (SEGGER only re-runs the | |
| // system-description callback on connect). Names must be string literals or | |
| // otherwise static -- only the pointer is kept. | |
| #define TU_SV_NAMED_MAX 4 | |
| static struct { | |
| const void* handle; | |
| const char* name; | |
| } _sv_named[TU_SV_NAMED_MAX]; | |
| static uint8_t _sv_named_count; | |
| void tusb_sysview_name_resource(const void* handle, const char* name) { | |
| if (handle == NULL || name == NULL) { | |
| return; // silently ignored: naming is cosmetic, never worth failing a boot over | |
| } | |
| // Already registered -- e.g. tusb_deinit() + re-init (examples/dual/dynamic_switch's role | |
| // switch) re-registers the same static mutex handle every time. Update the name in place | |
| // instead of appending a duplicate: with TU_SV_NAMED_MAX this small, a few duplicates are | |
| // enough to permanently push out every other handle's name. | |
| for (uint8_t i = 0; i < _sv_named_count; i++) { | |
| if (_sv_named[i].handle == handle) { | |
| _sv_named[i].name = name; | |
| SEGGER_SYSVIEW_NameResource((U32) (uintptr_t) handle, name); | |
| return; | |
| } | |
| } | |
| if (_sv_named_count >= TU_SV_NAMED_MAX) { | |
| return; // table full and this is a genuinely new handle: drop it, cosmetic only | |
| } | |
| _sv_named[_sv_named_count].handle = handle; | |
| _sv_named[_sv_named_count].name = name; | |
| _sv_named_count++; | |
| // Send it now as well: tusb_rhport_init() runs tusb_sysview_init() -- and with it | |
| // SEGGER_SYSVIEW_Start()'s one-shot system-description callback -- BEFORE the stacks | |
| // create their mutexes, so registration always lands after that emission. The stored | |
| // copy exists for the replay on a later recorder connect, not for this first one. | |
| SEGGER_SYSVIEW_NameResource((U32) (uintptr_t) handle, name); | |
| } | |
| static void send_sys_desc(void) { | |
| SEGGER_SYSVIEW_SendSysDesc("N=" SYSVIEW_APP_NAME ",D=" SYSVIEW_DEVICE_NAME SYSVIEW_OS_DESC); | |
| SEGGER_SYSVIEW_SendSysDesc("I#15=SysTick"); | |
| for (uint8_t i = 0; i < _sv_named_count; i++) { | |
| SEGGER_SYSVIEW_NameResource((U32) (uintptr_t) _sv_named[i].handle, _sv_named[i].name); | |
| } | |
| } | |
| #if defined(CFG_TUSB_SYSVIEW_TIMESTAMP_BSP) && CFG_TUSB_SYSVIEW_TIMESTAMP_BSP | |
| // ARMv7-M part whose DWT lacks CYCCNT (MAX32665/6: DWT_CTRL.NOCYCCNT reads 1 — a part like | |
| // this LINKS fine, so the fails-to-link signal below never fires; the symptom is every | |
| // duration silently decoding as 0). Its family.cmake builds SystemView with | |
| // SEGGER_SYSVIEW_CORE_OTHER so SEGGER calls SEGGER_SYSVIEW_X_GetTimestamp(), and — because | |
| // the fixed-1MHz microsecond contract below may be unreachable (MAX32 prescalers are | |
| // powers-of-two only) — the BSP reports the counter's rate as well: | |
| // | |
| // uint32_t SEGGER_SYSVIEW_X_GetTimestamp(void); // free-running counter | |
| // uint32_t SEGGER_SYSVIEW_X_GetTimestampFreq(void); // its rate, Hz | |
| extern uint32_t SEGGER_SYSVIEW_X_GetTimestampFreq(void); | |
| #define SYSVIEW_TIMESTAMP_FREQ SEGGER_SYSVIEW_X_GetTimestampFreq() | |
| #elif !defined(SEGGER_SYSVIEW_CORE) || (SEGGER_SYSVIEW_CORE != SEGGER_SYSVIEW_CORE_CM3) | |
| // No DWT cycle counter on this core (ARMv6-M M0/M0+, RISC-V, ...), so the BSP must supply the | |
| // timestamp from a free-running hardware timer. Contract for hw/bsp/<family>/family.c: | |
| // | |
| // uint32_t SEGGER_SYSVIEW_X_GetTimestamp(void); // free-running, MICROSECONDS | |
| // | |
| // Microseconds (not core cycles) keeps one fixed frequency for every such family, so nothing | |
| // else has to be told which timer was used. A family without an implementation fails to link | |
| // naming this symbol — that is the intended "SYSVIEW not ported to this family yet" signal. | |
| #define SYSVIEW_TIMESTAMP_FREQ 1000000u | |
| #else | |
| // DWT cycle counter: SEGGER_SYSVIEW_ConfDefaults.h reads it inline, no BSP code needed. | |
| #define SYSVIEW_TIMESTAMP_FREQ tusb_sysview_cpu_freq() | |
| #endif | |
| /* Active-interrupt id. ConfDefaults reads it from the Cortex-M ICSR itself on | |
| * both CM3 (bits [8:0]) and CM0 (bits [5:0]); only cores it doesn't know | |
| * (RISC-V here) need this hook. On RISC-V the analogous "what am I currently | |
| * servicing" register is mcause, whose low bits carry the trap/interrupt code | |
| * while an ISR runs. */ | |
| #if !defined(SEGGER_SYSVIEW_CORE) || \ | |
| ((SEGGER_SYSVIEW_CORE != SEGGER_SYSVIEW_CORE_CM3) && (SEGGER_SYSVIEW_CORE != SEGGER_SYSVIEW_CORE_CM0)) | |
| U32 SEGGER_SYSVIEW_X_GetInterruptId(void) { | |
| #if defined(__riscv) || defined(__riscv__) | |
| uint32_t mcause; | |
| __asm volatile ("csrr %0, mcause" : "=r" (mcause)); | |
| return (U32) (mcause & 0xFFFu); | |
| #elif defined(__ARM_ARCH_7M__) || defined(__ARM_ARCH_7EM__) || defined(__ARM_ARCH_8M_MAIN__) || \ | |
| defined(__ARM_ARCH_8_1M_MAIN__) || defined(__ARM_ARCH_6M__) || defined(__ARM_ARCH_8M_BASE__) | |
| // A Cortex-M built as SEGGER_SYSVIEW_CORE_OTHER (CFG_TUSB_SYSVIEW_TIMESTAMP_BSP parts, | |
| // e.g. MAX3266x) lands here: same ICSR.VECTACTIVE read ConfDefaults would have inlined. | |
| // 9 bits on v7-M/v8-M-mainline, 6 on v6-M — 0x1FF covers both (upper bits read 0 there). | |
| return (*(volatile uint32_t*) 0xE000ED04u) & 0x1FFu; | |
| #else | |
| return 0; // unknown core: report "no interrupt" rather than a bogus id | |
| #endif | |
| } | |
| #endif | |
| // Depth-counted ISR enter/exit (tusb_sysview.h's TU_SYSVIEW_ISR_ENTER/_EXIT): ten dual-role | |
| // BSPs call tud_int_handler()+tuh_int_handler() back-to-back from ONE hardware ISR, each of | |
| // which self-wraps -- so one real interrupt would otherwise emit ENTER,EXIT,ENTER,EXIT to | |
| // SEGGER's recorder (double activation count, each span timed at roughly half its true | |
| // duration). This counter only collapses NESTED enter/exit pairs, though -- two back-to-back | |
| // (non-nested) pairs still emit twice on their own. The actual fix is two-part: each of those | |
| // ten BSPs' vector ISR (hw/bsp/<family>/family.c) wraps its whole tud_+tuh_int_handler() body in | |
| // one more outer TU_SYSVIEW_ISR_ENTER()/_EXIT() pair, which nests the two inner (self-wrapped) | |
| // calls inside it -- THIS counter is what then collapses that nesting down to a single | |
| // ENTER/EXIT. Only the outermost 0->1 transition records ENTER, matching the 1->0 transition | |
| // that records EXIT. | |
| // | |
| // volatile because it is written from ISR context, and on parts whose USB IRQs nest (ch32v20x | |
| // documents HP preempting LP) two different ISRs can reach it. The increment/decrement stay | |
| // non-atomic: a lost update miscounts one span and self-corrects on the next balanced pair | |
| // rather than wedging, which is not worth a critical section on every ISR entry. | |
| static volatile uint8_t _sv_isr_depth; | |
| void tusb_sysview_isr_enter(void) { | |
| if (_sv_isr_depth++ == 0) { | |
| SEGGER_SYSVIEW_RecordEnterISR(); | |
| } | |
| } | |
| // W4: SCB->ICSR.PENDSVSET (Cortex-M0/M3+ only -- see the SEGGER_SYSVIEW_CORE guard below) tells | |
| // us, at the moment the outermost ISR span ends, whether a context switch was requested while it | |
| // ran (e.g. a FreeRTOS ISR-context give/notify that pends PendSV). If it was, this interrupt will | |
| // NOT return to the task it preempted -- it falls through to PendSV and the scheduler picks the | |
| // next task, possibly a different one. RecordExitISRToScheduler() is the record that tells | |
| // SystemView that story; the plain RecordExitISR() below claims the interrupted task resumes | |
| // directly, which is wrong whenever PendSV is pending -- every FreeRTOS capture showed the USB | |
| // ISR "returning" to the interrupted task, immediately followed by a causeless switch. RISC-V | |
| // (SEGGER_SYSVIEW_CORE_OTHER here) has no PendSV analogue and no ICSR, so it always takes the | |
| // plain exit record. | |
| #if defined(SEGGER_SYSVIEW_CORE) && \ | |
| ((SEGGER_SYSVIEW_CORE == SEGGER_SYSVIEW_CORE_CM3) || (SEGGER_SYSVIEW_CORE == SEGGER_SYSVIEW_CORE_CM0)) | |
| #define SV_ICSR_PENDSVSET_PENDING() \ | |
| ((*(volatile uint32_t*) 0xE000ED04 /* SCB->ICSR */) & (1u << 28) /* PENDSVSET */) | |
| #endif | |
| void tusb_sysview_isr_exit(void) { | |
| if (_sv_isr_depth != 0 && --_sv_isr_depth == 0) { | |
| #if defined(SV_ICSR_PENDSVSET_PENDING) | |
| if (SV_ICSR_PENDSVSET_PENDING()) { | |
| SEGGER_SYSVIEW_RecordExitISRToScheduler(); | |
| } else { | |
| SEGGER_SYSVIEW_RecordExitISR(); | |
| } | |
| #else | |
| SEGGER_SYSVIEW_RecordExitISR(); | |
| #endif | |
| } | |
| } | |
| void tusb_sysview_init(void) { | |
| static bool inited = false; | |
| if (inited) { return; } | |
| inited = true; | |
| #if defined(SEGGER_SYSVIEW_CORE) && (SEGGER_SYSVIEW_CORE == SEGGER_SYSVIEW_CORE_CM3) | |
| /* DWT cycle counter drives timestamps; enable on-target so recording never | |
| * depends on a debugger-side enable (also needed post-mortem). CYCCNT is | |
| * optional even on ARMv7-M: DWT_CTRL.NOCYCCNT (bit 25) reads 1 when the | |
| * counter is absent, in which case enabling it is a no-op and timestamps | |
| * would stall — check first, as SEGGER's own sample config does. */ | |
| (*(volatile unsigned int*) 0xE000EDFC) |= (1u << 24); /* DEMCR.TRCENA */ | |
| if (((*(volatile unsigned int*) 0xE0001000) & (1u << 25)) == 0) { | |
| (*(volatile unsigned int*) 0xE0001000) |= 1u; /* DWT_CTRL.CYCCNTENA */ | |
| } | |
| #endif | |
| SEGGER_SYSVIEW_Init(SYSVIEW_TIMESTAMP_FREQ, tusb_sysview_cpu_freq(), SYSVIEW_OS_API, send_sys_desc); | |
| SEGGER_SYSVIEW_SetRAMBase(SYSVIEW_RAM_BASE); | |
| /* Deliberately no SEGGER_SYSVIEW_RegisterModule(): SystemView 4.10b (Linux) | |
| * greys out File > Save Recording / Export Data as soon as ANY module is | |
| * registered, regardless of module content — bench-proven with 5 module | |
| * configurations. Function-timing | |
| * events are recorded at the fixed TU_SV_EVENT_BASE offset instead | |
| * (tusb_sysview.h) so host-side export keeps working. */ | |
| SEGGER_SYSVIEW_Start(); /* self-start: host recorders only drain */ | |
| #ifdef SYSVIEW_DISABLE_EVENTS | |
| SEGGER_SYSVIEW_DisableEvents(SYSVIEW_DISABLE_EVENTS); | |
| #endif | |
| } | |
| // pHeap is a handle identifying the heap, not the block being (de)allocated -- it must stay the | |
| // same across every Define/Alloc/Free call or SystemView cannot associate them. A stable dummy | |
| // object's address serves that purpose (its own bytes are never touched); the actual base is | |
| // unknowable from here regardless (events still carry the real ptr+size). | |
| static uint8_t _sv_heap_handle; | |
| void tusb_sysview_heap_alloc(void* ptr, unsigned size) { | |
| // _SendPacket() (SystemView's SEGGER_SYSVIEW.c) reads SEGGER_SYSVIEW_GET_TIMESTAMP() before | |
| // it checks EnableState, so a call reaching SEGGER_SYSVIEW_HeapAlloc() below before | |
| // tusb_sysview_init() has run -- e.g. FreeRTOS's own early heap allocations -- can read a | |
| // still clock-gated timer (TIM2 on stm32f0/g0/u0, CT32B0 on lpc11): a bus fault. Guarded here | |
| // (mirrored on the free side below) instead of relying on every caller to have run | |
| // board_init()/tusb_sysview_init() first. | |
| if (!SEGGER_SYSVIEW_IsStarted()) { return; } | |
| static bool heap_defined = false; | |
| // heap_defined only ever latches true below, on a build that CAN define the heap (FreeRTOS | |
| // with a known configTOTAL_HEAP_SIZE); every other build leaves it false forever, so this | |
| // block harmlessly re-checks (and re-skips) the #if on every call -- same as before the | |
| // IsStarted() guard above existed, just now only reached once SystemView has actually | |
| // started. | |
| if (!heap_defined) { | |
| #if CFG_TUSB_OS == OPT_OS_FREERTOS && defined(configTOTAL_HEAP_SIZE) && (configTOTAL_HEAP_SIZE > 0) | |
| // Heap capacity is the FreeRTOS heap_4/heap_5 arena size -- NOT this first allocation's own | |
| // size, which used to be passed here and made SystemView's "used/free" bookkeeping wrong | |
| // for every allocation after the first. | |
| SEGGER_SYSVIEW_HeapDefine(&_sv_heap_handle, &_sv_heap_handle, configTOTAL_HEAP_SIZE, 0); | |
| heap_defined = true; | |
| #endif | |
| // else: no known capacity (heap_3, a non-FreeRTOS OS, or configTOTAL_HEAP_SIZE undefined) -- | |
| // omit the define rather than guess; SEGGER_SYSVIEW_HeapAlloc() below still records the | |
| // event, SystemView just can't show a used/free percentage for it. | |
| } | |
| SEGGER_SYSVIEW_HeapAlloc(&_sv_heap_handle, ptr, size); | |
| } | |
| void tusb_sysview_heap_free(void* ptr) { | |
| if (!SEGGER_SYSVIEW_IsStarted()) { return; } // mirrors the alloc side's guard above | |
| SEGGER_SYSVIEW_HeapFree(&_sv_heap_handle, ptr); | |
| } | |
| void tusb_sysview_stack_report(void) { | |
| #if CFG_TUSB_OS == OPT_OS_FREERTOS && (configUSE_TRACE_FACILITY == 1) | |
| // Same cap SEGGER's own FreeRTOS table uses (hw/bsp/family_support.cmake's | |
| // SYSVIEW_FREERTOS_MAX_NOF_TASKS) -- one number instead of two that can drift apart. | |
| // uxTaskGetSystemState() returns 0 -- not a truncated list -- when status[] is smaller than | |
| // the actual task count, silently emptying this table above the cap; report nothing rather | |
| // than guess. Raise SYSVIEW_FREERTOS_MAX_NOF_TASKS if that ceiling is hit. | |
| #ifndef SYSVIEW_FREERTOS_MAX_NOF_TASKS | |
| #define SYSVIEW_FREERTOS_MAX_NOF_TASKS 16 | |
| #endif | |
| /* static, not a local array: sizeof(TaskStatus_t)*16 = 704 bytes is a large | |
| * fraction of a small task's stack (e.g. usbd's default 1024-byte stack). | |
| * An automatic array here was one contributor to a real stack-overflow | |
| * HardFault during USB enumeration on hardware (root-caused on same54_xplained: | |
| * default 1024-byte usbd/cdc FreeRTOS task stacks are marginal for a | |
| * SYSVIEW=4 build in general -- removing just this array was not sufficient | |
| * by itself; see the paired USBD_STACK_SIZE/CDC_STACK_SIZE bump in | |
| * examples/device/cdc_msc_freertos/src/main.c, which is the change that | |
| * actually restores enumeration). Moving it to .bss costs the same RAM but | |
| * none of the caller's stack, which is still worth doing on its own merits. | |
| * Single writer (usbd's periodic report, or usbh's in a host-only build -- | |
| * never both, never reentered), so no locking is needed. */ | |
| static TaskStatus_t status[SYSVIEW_FREERTOS_MAX_NOF_TASKS]; | |
| /* pcTaskName points into the live TCB, which a task deleted later in the lap frees; copy the | |
| * name at snapshot time so SendTaskInfo() below never dereferences a dead TCB. */ | |
| static char names[SYSVIEW_FREERTOS_MAX_NOF_TASKS][configMAX_TASK_NAME_LEN]; | |
| /* Report one task per call instead of looping over all of them: even with | |
| * the array off the stack, up to SYSVIEW_FREERTOS_MAX_NOF_TASKS back-to-back | |
| * SEGGER_SYSVIEW_SendTaskInfo() calls (each locking + writing the RTT ring buffer) in a | |
| * single burst is still enough cumulative time inside this task-context call to disturb the | |
| * USB peripheral on hardware -- a same54_xplained | |
| * cdc_msc_freertos SYSVIEW=4 build enumerated fine but then reproducibly | |
| * dropped off the bus tens of seconds later, exactly when this call first | |
| * fired; disabling the call site entirely made that disappear (100 s | |
| * stable vs. a consistent ~30-45 s failure), isolating it to this burst. | |
| * Spreading the SendTaskInfo() calls across separate invocations (still one | |
| * every 1024 tud_task_ext events) keeps each invocation short. */ | |
| static UBaseType_t next_idx = 0; | |
| // W12: uxTaskGetSystemState() suspends the scheduler for a byte-scan of every task's stack -- | |
| // real time, even though this function only ever publishes ONE task per call. Cache its | |
| // result and refresh only when the rotation wraps back to index 0 (once per full lap over the | |
| // task list, not every call): same set of Stack Info events published, over the same rotation, | |
| // at roughly 1/SYSVIEW_FREERTOS_MAX_NOF_TASKS the scheduler-suspended time. Everything published | |
| // from a cached entry is a value copy (the name into names[] here, the rest plain integers), so a | |
| // task deleted mid-lap only makes its own entry stale, never a dangling dereference. | |
| static UBaseType_t n = 0; | |
| if (next_idx == 0) { | |
| n = uxTaskGetSystemState(status, TU_ARRAY_SIZE(status), NULL); | |
| for (UBaseType_t t = 0; t < n; t++) { | |
| strncpy(names[t], status[t].pcTaskName, configMAX_TASK_NAME_LEN - 1); | |
| names[t][configMAX_TASK_NAME_LEN - 1] = '\0'; | |
| } | |
| } | |
| if (n == 0) { return; } | |
| UBaseType_t const i = next_idx; | |
| next_idx = (next_idx + 1 < n) ? next_idx + 1 : 0; | |
| SEGGER_SYSVIEW_TASKINFO info = {0}; | |
| info.TaskID = (U32)(uintptr_t) status[i].xHandle; | |
| info.sName = names[i]; | |
| info.Prio = status[i].uxCurrentPriority; | |
| info.StackBase = (U32)(uintptr_t) status[i].pxStackBase; | |
| uint32_t const free_bytes = status[i].usStackHighWaterMark * sizeof(StackType_t); | |
| /* pxEndOfStack (highest valid stack address) is only in TaskStatus_t under | |
| * this same condition (task.h); when available, derive the true configured | |
| * stack depth so StackUsage is bytes used at peak rather than bytes free | |
| * (usStackHighWaterMark alone is headroom, the opposite of "usage"). */ | |
| #if (portSTACK_GROWTH > 0) || (configRECORD_STACK_HIGH_ADDRESS == 1) | |
| uint32_t const stack_size = (uint32_t)(uintptr_t) status[i].pxEndOfStack - | |
| (uint32_t)(uintptr_t) status[i].pxStackBase + sizeof(StackType_t); | |
| info.StackSize = stack_size; | |
| info.StackUsage = (free_bytes < stack_size) ? (stack_size - free_bytes) : 0; | |
| #else | |
| // pxEndOfStack is unavailable (this port's stack does not grow upward, and | |
| // configRECORD_STACK_HIGH_ADDRESS is 0), so "bytes used at peak" cannot be computed here. | |
| // free_bytes is headroom -- the opposite of usage -- and SEGGER_SYSVIEW_TASKINFO has no | |
| // "unknown" sentinel for StackUsage, so there is no way to send it without it being read as | |
| // a (wrong) usage figure under the "stack high-water" label. Every family wired through | |
| // hw/bsp/sysview_freertos_hooks.h forces configRECORD_STACK_HIGH_ADDRESS=1 for exactly this | |
| // reason and never reaches this branch; a family that wires SYSVIEW without going through | |
| // that shared header can still land here, so refuse to publish rather than mislabel: skip | |
| // this task's Stack Info event for this cycle (next_idx above already advanced, so it is | |
| // retried on a later call, same as every other task in rotation). | |
| return; | |
| #endif | |
| SEGGER_SYSVIEW_SendTaskInfo(&info); | |
| #endif | |
| } | |
| #endif /* CFG_TUD_SYSVIEW || CFG_TUH_SYSVIEW */ |