/* * 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 #include #include #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.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.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 */