diff --git a/CMakeLists.txt b/CMakeLists.txt index cebe958..5565f76 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,6 +71,16 @@ endif() find_program(ULMK_PYTHON NAMES python3 python REQUIRED) +# Flash profile must be visible to C too: the arch picks secondary reset +# vectors (RAM banks vs flash banks) from it, not only the linker script. +# Declare as CACHE BOOL so -DULMK_C29_FLASH=1 is not left UNINITIALIZED +# (which silently broke if() / add_compile_definitions for board_init). +set(ULMK_C29_FLASH OFF CACHE BOOL "C29 flash profile (XIP + POR cert)") +if(ULMK_C29_FLASH) + add_compile_definitions(ULMK_C29_FLASH=1) + message(STATUS "ULMK_C29_FLASH=1 (flash profile compile defs)") +endif() + execute_process( COMMAND "${ULMK_PYTHON}" "${CMAKE_SOURCE_DIR}/tools/gen_config.py" --out-dir "${CMAKE_BINARY_DIR}/generated" @@ -115,9 +125,16 @@ add_library(ulmk_kernel STATIC kernel/syscall/syscall_wcet.c ${ULMK_ARCH_KERNEL_SOURCES}) +if(ULMK_C29_FLASH) + target_compile_definitions(ulmk_kernel PRIVATE ULMK_C29_FLASH=1) +endif() + if(EXISTS "${ULMK_CHIP_DIR}/qemu_printk_hook.c") target_sources(ulmk_kernel PRIVATE "${ULMK_CHIP_DIR}/qemu_printk_hook.c") endif() +if(EXISTS "${ULMK_CHIP_DIR}/printk_hook.c") + target_sources(ulmk_kernel PRIVATE "${ULMK_CHIP_DIR}/printk_hook.c") +endif() target_include_directories(ulmk_kernel PUBLIC "${CMAKE_SOURCE_DIR}/include" @@ -130,14 +147,17 @@ target_include_directories(ulmk_kernel PRIVATE target_compile_definitions(ulmk_kernel PRIVATE ULMK_KERNEL_BUILD=1) -target_compile_options(ulmk_kernel PRIVATE - -fno-data-sections - ${ULMK_KERNEL_OPT_FLAGS}) +if(NOT "${ULMK_COMPILER_FAMILY}" STREQUAL "ticlang") + target_compile_options(ulmk_kernel PRIVATE -fno-data-sections) +endif() +target_compile_options(ulmk_kernel PRIVATE ${ULMK_KERNEL_OPT_FLAGS}) # init.c runs before .data/.bss are initialised — keep GCC from lowering its -# copy/zero loops into memcpy/memset calls. -set_source_files_properties(kernel/init/init.c PROPERTIES - COMPILE_OPTIONS "-fno-tree-loop-distribute-patterns") +# copy/zero loops into memcpy/memset calls. TIClang does not support this flag. +if(NOT "${ULMK_COMPILER_FAMILY}" STREQUAL "ticlang") + set_source_files_properties(kernel/init/init.c PROPERTIES + COMPILE_OPTIONS "-fno-tree-loop-distribute-patterns") +endif() if(DEFINED ULMK_BOARD_CFLAGS) target_compile_options(ulmk_kernel PRIVATE ${ULMK_BOARD_CFLAGS}) @@ -215,7 +235,9 @@ if(ULMK_SDK) "${ULMK_CHIP_DIR}" ${ULMK_BOARD_INCLUDES}) - target_compile_options(ulmk_board PRIVATE -fno-data-sections) + if(NOT "${ULMK_COMPILER_FAMILY}" STREQUAL "ticlang") + target_compile_options(ulmk_board PRIVATE -fno-data-sections) + endif() if(DEFINED ULMK_BOARD_CFLAGS) target_compile_options(ulmk_board PRIVATE ${ULMK_BOARD_CFLAGS}) @@ -240,16 +262,22 @@ if(ULMK_SDK) "${ULMK_CHIP_DIR}" ${ULMK_BOARD_INCLUDES}) - target_compile_options(ulmk PRIVATE -fno-data-sections) + if(NOT "${ULMK_COMPILER_FAMILY}" STREQUAL "ticlang") + target_compile_options(ulmk PRIVATE -fno-data-sections) + endif() if(DEFINED ULMK_BOARD_CFLAGS) target_compile_options(ulmk PRIVATE ${ULMK_BOARD_CFLAGS}) endif() - # kernel and board reference each other's symbols — group to resolve the - # circular dependency between the two archives. - target_link_libraries(ulmk PRIVATE - -Wl,--start-group ulmk_board ulmk_kernel -Wl,--end-group) + if("${ULMK_COMPILER_FAMILY}" STREQUAL "ticlang") + target_link_libraries(ulmk PRIVATE ulmk_board ulmk_kernel) + else() + # kernel and board reference each other's symbols — group to resolve the + # circular dependency between the two archives. + target_link_libraries(ulmk PRIVATE + -Wl,--start-group ulmk_board ulmk_kernel -Wl,--end-group) + endif() _ulmk_finalize_build(ulmk "${ULMK_CHIP_DIR}") else() @@ -274,7 +302,13 @@ else() "${ULMK_CHIP_DIR}" ${ULMK_BOARD_INCLUDES}) - target_compile_options(ulmk PRIVATE -fno-data-sections) + if(ULMK_C29_FLASH) + target_compile_definitions(ulmk PRIVATE ULMK_C29_FLASH=1) + endif() + + if(NOT "${ULMK_COMPILER_FAMILY}" STREQUAL "ticlang") + target_compile_options(ulmk PRIVATE -fno-data-sections) + endif() if(DEFINED ULMK_BOARD_CFLAGS) target_compile_options(ulmk PRIVATE ${ULMK_BOARD_CFLAGS}) diff --git a/arch/arm/arch.c b/arch/arm/arch.c index 238e1c9..efa05c7 100644 --- a/arch/arm/arch.c +++ b/arch/arm/arch.c @@ -596,3 +596,13 @@ void ulmk_arch_smp_park(void) void ulmk_arch_smp_mark_ready(void) { } + +uint32_t ulmk_arch_smp_ready_mask(void) +{ + return 0u; +} + +void ulmk_arch_smp_wait_ready(uint32_t mask) +{ + (void)mask; +} diff --git a/arch/arm/include/ulmk_arch.h b/arch/arm/include/ulmk_arch.h index c175a14..4d97886 100644 --- a/arch/arm/include/ulmk_arch.h +++ b/arch/arm/include/ulmk_arch.h @@ -85,6 +85,8 @@ void ulmk_arch_secondary_init(void); void ulmk_arch_secondary_mark_ready(void); void ulmk_arch_start_secondary(uint32_t cpu_id, void (*entry)(void)); void ulmk_arch_smp_mark_ready(void); +uint32_t ulmk_arch_smp_ready_mask(void); +void ulmk_arch_smp_wait_ready(uint32_t mask); void ulmk_arch_smp_park(void); void ulmk_arch_cycle_enable(void); diff --git a/arch/c29/arch.c b/arch/c29/arch.c new file mode 100644 index 0000000..1e88586 --- /dev/null +++ b/arch/c29/arch.c @@ -0,0 +1,399 @@ +/* SPDX-License-Identifier: MIT */ +/* + * C29 architecture implementation — arch/c29/arch.c + * + * Covers: CPU control, spinlocks, atomics, context fabrication, MPU stubs, + * and the arch-init entry point. IRQ/tick lives in irq.c; SSU in mpu.c; + * SMP stubs in smp.c; context switch in ctx_switch.S. + */ + +#include +#include +#include + +#include +#include +#include +#include + +/* Forward declarations from ctx_switch.S and irq.c */ +extern void ulmk_c29_tick_isr(void); +extern void ulmk_c29_syscall_isr(void); +extern void ulmk_c29_yield_isr(void); +extern void ulmk_c29_thread_trampoline(void); + +/* Staged by ulmk_arch_sched_switch; consumed by INT veneer on RETI.INT. */ +extern ulmk_arch_ctx_t *g_c29_preempt_old_ctx; +extern ulmk_arch_ctx_t *g_c29_preempt_new_ctx; +extern uint32_t g_c29_int_depth; + +void ulmk_c29_run_thread(void *arg, void (*entry)(void *arg)) +{ + entry(arg); + ulmk_thread_exit(); +} + +/* + * NMI path: INT-nested faults panic; otherwise restore APR shadow and + * let the kernel kill the current user thread if any. + */ +void ulmk_c29_nmi_dispatch(void) +{ + if (g_c29_int_depth != 0u) { + ulmk_kern_trap_panic(); + return; + } + ulmk_kern_trap_mpu_restore(); + ulmk_kern_trap_recoverable(); +} + +/* + * PIPE INT_CTL_H register for a given channel: force flag (FLAG_FRC) is bit 0. + * PIPE base is overrideable from board_config.h. + */ +#define PIPE_INT_CTL_H(n) \ + (*((volatile uint32_t *)(ULMK_ARCH_PIPE_INT_CTL_H(n)))) + +#define PIPE_INT_VECT(n) \ + (*((volatile uint32_t *)(ULMK_ARCH_PIPE_INT_VECT(n)))) + +/* DSTS register (read via __builtin or memory-mapped alias). */ +#define C29_DSTS_INTE (1u << 0) + +/* --------------------------------------------------------------------------- + * CPU control + * --------------------------------------------------------------------------- + */ + +ulmk_arch_irq_key_t ulmk_arch_cpu_irq_save(void) +{ + ulmk_arch_irq_key_t key; + + __asm__ volatile( + "ST.32 %0, DSTS\n\t" + "DISINT" + : "=m"(key) : : "memory"); + return key; +} + +void ulmk_arch_cpu_irq_restore(ulmk_arch_irq_key_t key) +{ + if (key & C29_DSTS_INTE) + __asm__ volatile("ENINT" ::: "memory"); +} + +void ulmk_arch_cpu_irq_enable(void) +{ + __asm__ volatile("ENINT" ::: "memory"); +} + +void ulmk_arch_cpu_irq_disable(void) +{ + __asm__ volatile("DISINT" ::: "memory"); +} + +void ulmk_arch_cpu_idle(void) +{ + __asm__ volatile("IDLE" ::: "memory"); +} + +void ulmk_arch_cpu_halt(void) +{ + for (;;) + __asm__ volatile("IDLE" ::: "memory"); +} + +uint32_t ulmk_arch_cpu_clz(uint32_t val) +{ + return (uint32_t)__builtin_c29_i32_clzeros_d((int32_t)val); +} + +#if ULMK_CONFIG_ENABLE_SMP +/* + * CORE_ID CSFR is Link-2 only and awkward to read from C with this + * toolchain. Boot stubs stamp g_c29_my_cpu before park; CPU0 sets 0 + * in ulmk_arch_init(). + */ +volatile uint32_t g_c29_my_cpu; +#endif + +uint32_t ulmk_arch_cpu_id(void) +{ +#if ULMK_CONFIG_ENABLE_SMP + return g_c29_my_cpu; +#else + return 0u; +#endif +} + +/* --------------------------------------------------------------------------- + * Spinlocks + * --------------------------------------------------------------------------- + */ + +void ulmk_arch_spin_lock(ulmk_spinlock_t *lock) +{ + uint32_t old; + + do { + __builtin_c29_atomic_mem_enter(); + old = lock->locked; + if (!old) { + lock->locked = 1u; + } + __builtin_c29_atomic_leave(); + } while (old); +} + +void ulmk_arch_spin_unlock(ulmk_spinlock_t *lock) +{ + __asm__ volatile("" ::: "memory"); + lock->locked = 0u; +} + +ulmk_arch_irq_key_t ulmk_arch_spin_lock_irqsave(ulmk_spinlock_t *lock) +{ + ulmk_arch_irq_key_t key = ulmk_arch_cpu_irq_save(); + + ulmk_arch_spin_lock(lock); + return key; +} + +void ulmk_arch_spin_unlock_irqrestore(ulmk_spinlock_t *lock, + ulmk_arch_irq_key_t key) +{ + ulmk_arch_spin_unlock(lock); + ulmk_arch_cpu_irq_restore(key); +} + +/* --------------------------------------------------------------------------- + * Atomics + * --------------------------------------------------------------------------- + */ + +uint32_t ulmk_arch_atomic_cas(volatile uint32_t *ptr, + uint32_t expected, uint32_t desired) +{ + uint32_t old; + + __builtin_c29_atomic_mem_enter(); + old = *ptr; + if (old == expected) { + *ptr = desired; + } + __builtin_c29_atomic_leave(); + return old; +} + +uint32_t ulmk_arch_atomic_add(volatile uint32_t *ptr, uint32_t val) +{ + uint32_t old; + + __builtin_c29_atomic_mem_enter(); + old = *ptr; + *ptr = old + val; + __builtin_c29_atomic_leave(); + return old; +} + +/* --------------------------------------------------------------------------- + * Cycle counter (CPUTIMER2 free-run) + * --------------------------------------------------------------------------- + */ + +static volatile uint32_t *_timer2_tim(void) +{ + /* TIM register is at offset 0 from CPUTIMER2_BASE. */ + return (volatile uint32_t *)ULMK_BOARD_CPUTIMER2_BASE; +} + +void ulmk_arch_cycle_enable(void) +{ + /* + * CPUTIMER2 is started in ulmk_arch_tick_init(); nothing extra needed + * for the cycle-read path — TIM counts down continuously. + */ +} + +uint32_t ulmk_arch_cycle_read(void) +{ + return *_timer2_tim(); +} + +/* --------------------------------------------------------------------------- + * CSA pool init — no-op for C29 (no CSA mechanism) + * --------------------------------------------------------------------------- + */ + +void ulmk_arch_csa_pool_init(uintptr_t pool_base, size_t pool_size) +{ + (void)pool_base; + (void)pool_size; +} + +/* --------------------------------------------------------------------------- + * Context fabrication + * + * Software INT frame (ULMK_SAVE_CONTEXT), stack grows upward: + * +0 A14 + * +4 RPC + * +8 DSTS + * +12 ESTS + * +16 XA0..XA12 (56 bytes) + * +72 XD0..XD14 (64 bytes) + * +136 XM0..XM30 (128 bytes) + * Total software frame = 264 bytes. + * + * RET / RETI.INT both consume an extra 8-byte slot below A14 (previous RPC + * pushed by CALL or by INT entry). Fabricated threads never took a real INT, + * so we reserve that slot explicitly — same idea as FreeRTOS C29 + * pxPortInitialiseStack. + * --------------------------------------------------------------------------- + */ + +#define CTX_SOFT_SIZE 264u +#define CTX_RETI_PAD 8u +#define CTX_TOTAL_SIZE (CTX_SOFT_SIZE + CTX_RETI_PAD) + +/* Offsets within the soft frame (after the RETI pad). */ +#define TF_RPC 4u +#define TF_DSTS 8u +#define TF_ESTS 12u +/* XA0 at +16; A4 is word index 4 of A0..A13 → offset 16+16 = 32 */ +#define TF_A4 32u +#define TF_A5 36u + +/* Match FreeRTOS C29 fabricated DSTS/ESTS (INT-enabled leaf context). */ +#define C29_FAB_DSTS 0x07F90001u +#define C29_FAB_ESTS 0x00020101u + +void ulmk_arch_ctx_init(ulmk_arch_ctx_t *ctx, + void (*entry)(void *arg), void *arg, + uintptr_t stack_top, ulmk_privilege_t priv) +{ + uint8_t *base; + uint8_t *frame; + uint32_t *w; + uint32_t i; + + (void)priv; /* privilege enforced by SSU Links/APRs */ + + /* + * Stack grows upward. Caller passes the low address of the stack + * region (see ULMK_ARCH_STACK_GROWS_UP). Place RETI pad + soft + * frame at the base; SP starts just past the soft frame. + */ + base = (uint8_t *)((stack_top + 7u) & ~(uintptr_t)7u); + + for (i = 0u; i < CTX_TOTAL_SIZE; i++) + base[i] = 0; + + w = (uint32_t *)base; + /* Slot popped by RET/RETI.INT after restoring the soft frame. */ + w[0] = (uint32_t)(uintptr_t)ulmk_c29_thread_trampoline & ~1u; + w[1] = C29_FAB_DSTS; + + frame = base + CTX_RETI_PAD; + w = (uint32_t *)frame; + + /* + * RPC = trampoline. Trampoline CALLs ulmk_c29_run_thread with + * A4=arg and A5=entry already in the fabricated frame. + */ + w[TF_RPC / 4u] = (uint32_t)(uintptr_t)ulmk_c29_thread_trampoline & + ~1u; + w[TF_DSTS / 4u] = C29_FAB_DSTS; + w[TF_ESTS / 4u] = C29_FAB_ESTS; + w[TF_A4 / 4u] = (uint32_t)(uintptr_t)arg; + w[TF_A5 / 4u] = (uint32_t)(uintptr_t)entry; + + ctx->sp = (uint32_t)((uintptr_t)frame + CTX_SOFT_SIZE); +} + +void ulmk_arch_ctx_free(ulmk_arch_ctx_t *ctx) +{ + /* No dynamic allocation — nothing to release. */ + (void)ctx; +} + +/* --------------------------------------------------------------------------- + * Scheduler switch helpers + * --------------------------------------------------------------------------- + */ + +bool ulmk_arch_sched_isr_preempt_deferred(void) +{ + /* INT veneer already saved the frame; switch applies on RETI.INT. */ + return true; +} + +void ulmk_arch_sched_switch(ulmk_arch_ctx_t *from, const ulmk_arch_ctx_t *to, + unsigned int flags) +{ + /* + * Syscall and IRQ share the INT veneer. Any switch requested while + * an INT is active must be deferred to RETI.INT — a direct RET coop + * switch would leave the PIPE INT active and block further ticks. + */ + if (g_c29_int_depth != 0u || + flags == ULMK_SCHED_SWITCH_PREEMPT_ISR) { + g_c29_preempt_old_ctx = from; + g_c29_preempt_new_ctx = (ulmk_arch_ctx_t *)to; + return; + } + ulmk_arch_ctx_switch(from, to); +} + +/* --------------------------------------------------------------------------- + * Arch init — called by ulmk_kern_start() after .data/.bss are ready + * --------------------------------------------------------------------------- + */ + +extern void ulmk_kern_main(const ulmk_boot_info_t *info); + +void ulmk_arch_init(ulmk_boot_info_t *info) +{ + /* + * Clear the Data Line Buffer enable bits before any shared RAM use. + * See errata SPRZ569E; arch_config.h defines the register address. + */ + volatile uint32_t *dlb = (volatile uint32_t *)ULMK_ARCH_MEM_DLB_CONFIG; + + *dlb &= ~(ULMK_ARCH_MEM_DLB_CPU1_EN | + ULMK_ARCH_MEM_DLB_CPU2_EN | + ULMK_ARCH_MEM_DLB_CPU3_EN | + (1u << 6)); /* SYNCBRIDGE_DLB_EN */ + + /* + * Fill boot_info with the CPU1 RAM region and tick clock. + * Linker symbols for the user pool bound come from the generated .cmd. + */ + extern char _ulmk_user_pool_start[]; + extern char _ulmk_user_pool_end[]; + extern char _ulmk_isr_stack_top[]; + + info->mem[0].base = (uintptr_t)_ulmk_user_pool_start; + info->mem[0].size = (size_t)(_ulmk_user_pool_end - _ulmk_user_pool_start); + info->mem_count = 1; + + /* No CSA pool on C29. */ + info->csa_pool_base = 0; + info->csa_pool_size = 0; + +#if ULMK_CONFIG_ENABLE_SMP + g_c29_my_cpu = 0u; +#endif + + ulmk_arch_mpu_init(); + ulmk_arch_irq_vectors_init(0u, 0u, (uintptr_t)_ulmk_isr_stack_top); +} + +/* --------------------------------------------------------------------------- + * Printk character output — board provides the UART write + * --------------------------------------------------------------------------- + */ + +__attribute__((weak)) void ulmk_printk_char_out(char c) +{ + (void)c; +} diff --git a/arch/c29/cert_placeholder.c b/arch/c29/cert_placeholder.c new file mode 100644 index 0000000..384268e --- /dev/null +++ b/arch/c29/cert_placeholder.c @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: MIT */ +/* + * Flash-profile boot certificate slot at 0x10000000. package-seccfg.sh + * overwrites this section post-link (--update-section cert=...). + */ + +#include + +#if defined(ULMK_C29_FLASH) && ULMK_C29_FLASH +__attribute__((retain, section("cert"))) +const uint8_t ulmk_c29_certificate[4096] = { 0 }; +#endif diff --git a/arch/c29/ctx_switch.S b/arch/c29/ctx_switch.S new file mode 100644 index 0000000..b211b5d --- /dev/null +++ b/arch/c29/ctx_switch.S @@ -0,0 +1,190 @@ +/* SPDX-License-Identifier: MIT */ +/* + * C29 context switch and INT veneer. + * + * Stack grows upward. Software INT frame (264 bytes): + * +0 A14 + * +4 RPC + * +8 DSTS + * +12 ESTS + * +16 XA0..XA12 (A0-A13) 56 bytes + * +72 XD0..XD14 (D0-D15) 64 bytes + * +136 XM0..XM30 (M0-M31) 128 bytes + * Total = 264. Inspired by TI FreeRTOS C29 frame; not copied. + */ + + .global ulmk_arch_ctx_switch + .global ulmk_c29_int_veneer + .global ulmk_c29_tick_isr + .global ulmk_c29_syscall_isr + .global ulmk_c29_yield_isr + .global ulmk_c29_ipi1_isr + .global ulmk_c29_ipi2_isr + .global ulmk_arch_idle_entry + .global ulmk_c29_thread_trampoline + .global g_c29_preempt_old_ctx + .global g_c29_preempt_new_ctx + .extern ulmk_c29_int_dispatch + .extern ulmk_c29_run_thread + + .equ CTX_SIZE, 264 + + .macro ULMK_SAVE_CONTEXT + ST.32 *(A15++#8), A14 + || MV A14, RPC + ST.32 *(A15-#4), A14 + ST.32 *(A15++#8), DSTS + ST.32 *(A15-#4), ESTS + ST.64 *(A15++#8), XA0 + ST.64 *(A15++#8), XA2 + ST.64 *(A15++#8), XA4 + ST.64 *(A15++#8), XA6 + ST.64 *(A15++#8), XA8 + ST.64 *(A15++#8), XA10 + ST.64 *(A15++#8), XA12 + ST.64 *(A15++#8), XD0 + ST.64 *(A15++#8), XD2 + ST.64 *(A15++#8), XD4 + ST.64 *(A15++#8), XD6 + ST.64 *(A15++#8), XD8 + ST.64 *(A15++#8), XD10 + ST.64 *(A15++#8), XD12 + ST.64 *(A15++#8), XD14 + ST.64 *(A15++#8), XM0 + ST.64 *(A15++#8), XM2 + ST.64 *(A15++#8), XM4 + ST.64 *(A15++#8), XM6 + ST.64 *(A15++#8), XM8 + ST.64 *(A15++#8), XM10 + ST.64 *(A15++#8), XM12 + ST.64 *(A15++#8), XM14 + ST.64 *(A15++#8), XM16 + ST.64 *(A15++#8), XM18 + ST.64 *(A15++#8), XM20 + ST.64 *(A15++#8), XM22 + ST.64 *(A15++#8), XM24 + ST.64 *(A15++#8), XM26 + ST.64 *(A15++#8), XM28 + ST.64 *(A15++#8), XM30 + .endm + + .macro ULMK_RESTORE_CONTEXT + LD.64 XM30, *(A15-=#8) + LD.64 XM28, *(A15-=#8) + LD.64 XM26, *(A15-=#8) + LD.64 XM24, *(A15-=#8) + LD.64 XM22, *(A15-=#8) + LD.64 XM20, *(A15-=#8) + LD.64 XM18, *(A15-=#8) + LD.64 XM16, *(A15-=#8) + LD.64 XM14, *(A15-=#8) + LD.64 XM12, *(A15-=#8) + LD.64 XM10, *(A15-=#8) + LD.64 XM8, *(A15-=#8) + LD.64 XM6, *(A15-=#8) + LD.64 XM4, *(A15-=#8) + LD.64 XM2, *(A15-=#8) + LD.64 XM0, *(A15-=#8) + LD.64 XD14, *(A15-=#8) + LD.64 XD12, *(A15-=#8) + LD.64 XD10, *(A15-=#8) + LD.64 XD8, *(A15-=#8) + LD.64 XD6, *(A15-=#8) + LD.64 XD4, *(A15-=#8) + LD.64 XD2, *(A15-=#8) + LD.64 XD0, *(A15-=#8) + LD.64 XA12, *(A15-=#8) + LD.64 XA10, *(A15-=#8) + LD.64 XA8, *(A15-=#8) + LD.64 XA6, *(A15-=#8) + LD.64 XA4, *(A15-=#8) + LD.64 XA2, *(A15-=#8) + LD.64 XA0, *(A15-=#8) + LD.32 ESTS, *(A15-#4) + LD.32 DSTS, *(A15-=#8) + LD.32 RPC, *(A15-#4) + LD.32 A14, *(A15-=#8) + .endm + + .section .text.ulmk_arch_ctx_switch, "ax" +ulmk_arch_ctx_switch: + BCMPZ .Lfirst_launch, A.EQ, A4 + ULMK_SAVE_CONTEXT + ST.32 *A4, A15 + +.Lfirst_launch: + LD.32 A15, *A5 + ULMK_RESTORE_CONTEXT + RET + + .section .text.ulmk_c29_int_veneer, "ax" +ulmk_c29_int_veneer: + ULMK_SAVE_CONTEXT + /* ABI: uint32 args in D0/D1 — frame_sp, srpn. */ + MV D1, D0 + MV D0, A15 + MV D2, #CTX_SIZE + SUB D0, D0, D2 + CALL @ulmk_c29_int_dispatch + + LD.32 A4, @g_c29_preempt_new_ctx + BCMPZ .Lveneer_restore, A.EQ, A4 + LD.32 A5, @g_c29_preempt_old_ctx + BCMPZ .Lload_new, A.EQ, A5 + ST.32 *A5, A15 +.Lload_new: + LD.32 A15, *A4 + MV A4, #0 + ST.32 @g_c29_preempt_new_ctx, A4 + ST.32 @g_c29_preempt_old_ctx, A4 + +.Lveneer_restore: + ULMK_RESTORE_CONTEXT + RETI.INT + + .section .text.ulmk_c29_tick_isr, "ax" +ulmk_c29_tick_isr: + MV D0, #8 + LB @ulmk_c29_int_veneer + + .section .text.ulmk_c29_syscall_isr, "ax" +ulmk_c29_syscall_isr: + MV D0, #254 + LB @ulmk_c29_int_veneer + + .section .text.ulmk_c29_yield_isr, "ax" +ulmk_c29_yield_isr: + MV D0, #255 + LB @ulmk_c29_int_veneer + + .section .text.ulmk_c29_ipi1_isr, "ax" +ulmk_c29_ipi1_isr: + MV D0, #12 + LB @ulmk_c29_int_veneer + + .section .text.ulmk_c29_ipi2_isr, "ax" +ulmk_c29_ipi2_isr: + MV D0, #16 + LB @ulmk_c29_int_veneer + + .section .text.ulmk_arch_idle_entry, "ax" +ulmk_arch_idle_entry: + ENINT +.Lidle: + IDLE + LB @.Lidle + + .section .text.ulmk_c29_thread_trampoline, "ax" +ulmk_c29_thread_trampoline: + /* A4=arg, A5=entry already restored from the fabricated frame. */ + CALL @ulmk_c29_run_thread +.Ltramp_halt: + IDLE + LB @.Ltramp_halt + + .section .data, "aw" + .align 4 +g_c29_preempt_old_ctx: + .long 0 +g_c29_preempt_new_ctx: + .long 0 diff --git a/arch/c29/include/arch_config.h b/arch/c29/include/arch_config.h new file mode 100644 index 0000000..1bc2708 --- /dev/null +++ b/arch/c29/include/arch_config.h @@ -0,0 +1,186 @@ +/* SPDX-License-Identifier: MIT */ +/* + * C29 architecture constants — arch/c29/include/arch_config.h + */ + +#ifndef ULMK_ARCH_C29_CONFIG_H +#define ULMK_ARCH_C29_CONFIG_H + +#include + +#ifndef ULMK_ARCH_NUM_CPU +#define ULMK_ARCH_NUM_CPU 1 +#endif + +#ifndef ULMK_ARCH_HAVE_FPU +#define ULMK_ARCH_HAVE_FPU 1 +#endif + +/* APR / region alignment (SSU normal APR granularity). */ +#define ULMK_ARCH_MAX_REGIONS 16 +#define ULMK_ARCH_REGION_ALIGN 4096u + +#define ULMK_ARCH_PRS_KERNEL 0u +#define ULMK_ARCH_PRS_USER 1u + +#ifndef ULMK_ARCH_IDLE_IS_WFI +#define ULMK_ARCH_IDLE_IS_WFI 1 +#endif + +/* + * Fabricate contexts on the affinity CPU so entry IDs resolve through the + * core-local table (distinct LPA/CPA addresses). + */ +#define ULMK_ARCH_CTX_FABRICATE_ON_AFFINITY_CPU 1 + +/* C29 software stack grows upward (A15 increases). */ +#define ULMK_ARCH_STACK_GROWS_UP 1 + +/* + * Idle must run on the INTSP/runtime Stack so Supervisor INT can enter. + * Default ports keep kernel-privileged idle; C29 overrides both. + */ +#ifndef ULMK_ARCH_IDLE_ENTRY +void ulmk_arch_idle_entry(void *arg); +#define ULMK_ARCH_IDLE_ENTRY ulmk_arch_idle_entry +#endif + +#ifndef ULMK_ARCH_IDLE_PRIVILEGE +#define ULMK_ARCH_IDLE_PRIVILEGE ULMK_PRIV_USER +#endif + +/* Full software INT frame: A14/RPC/DSTS/ESTS + A0-13 + D0-15 + M0-31. */ +#define ULMK_ARCH_CTX_FRAME_SIZE 264u + +/* PIPE / timer / IPC / SSU bases — board may override via platform.h. */ +#ifndef ULMK_BOARD_PIPE_BASE +#define ULMK_BOARD_PIPE_BASE 0x30020000u +#endif +#ifndef ULMK_BOARD_CPUTIMER2_BASE +#define ULMK_BOARD_CPUTIMER2_BASE 0x3021A000u +#endif +#ifndef ULMK_BOARD_MEMSSMISCI_BASE +#define ULMK_BOARD_MEMSSMISCI_BASE 0x301D8E00u +#endif +#ifndef ULMK_BOARD_SSUGEN_BASE +#define ULMK_BOARD_SSUGEN_BASE 0x30080000u +#endif +#ifndef ULMK_BOARD_SSUCPU1CFG_BASE +#define ULMK_BOARD_SSUCPU1CFG_BASE 0x30081000u +#endif +#ifndef ULMK_BOARD_SSUCPU2CFG_BASE +#define ULMK_BOARD_SSUCPU2CFG_BASE 0x30082000u +#endif +#ifndef ULMK_BOARD_SSUCPU3CFG_BASE +#define ULMK_BOARD_SSUCPU3CFG_BASE 0x30083000u +#endif +#ifndef ULMK_BOARD_SSUCPU1AP_BASE +#define ULMK_BOARD_SSUCPU1AP_BASE 0x30087000u +#endif +#ifndef ULMK_BOARD_DEVCFG_BASE +#define ULMK_BOARD_DEVCFG_BASE 0x30180000u +#endif +#ifndef ULMK_BOARD_WD_DISABLE_ADDR +#define ULMK_BOARD_WD_DISABLE_ADDR 0x30208C52u +#endif +#ifndef ULMK_BOARD_WD_DISABLE_VAL +#define ULMK_BOARD_WD_DISABLE_VAL 0x68u +#endif + +#ifndef ULMK_BOARD_TICK_CLOCK_HZ +#define ULMK_BOARD_TICK_CLOCK_HZ 200000000u +#endif + +/* PIPE channel numbers (logical SRPN). */ +#ifndef ULMK_BOARD_IRQ_TIMER2 +#define ULMK_BOARD_IRQ_TIMER2 8u +#endif +#ifndef ULMK_BOARD_IRQ_SWINT_YIELD +#define ULMK_BOARD_IRQ_SWINT_YIELD 255u /* INT_SW1 */ +#endif +#ifndef ULMK_BOARD_IRQ_SWINT_SYSCALL +#define ULMK_BOARD_IRQ_SWINT_SYSCALL 254u /* INT_SW2 */ +#endif +#ifndef ULMK_BOARD_IRQ_IPC_1 +#define ULMK_BOARD_IRQ_IPC_1 12u /* INT_IPC_1_1 */ +#endif +#ifndef ULMK_BOARD_IRQ_IPC_2 +#define ULMK_BOARD_IRQ_IPC_2 16u /* INT_IPC_2_1 */ +#endif + +/* + * Secondary reset vectors (TI affinity: CPU2 fetches LPA, CPU3 fetches CPA). + * Release always targets LPA1 / CPA0. Flash builds keep stub LMAs in the + * contiguous CPU1 FLASH_RP0 image (symbols ulmk_c29_cpu{2,3}_start); CPU1 + * copies them into those banks before release (CPU2 has no flash XIP). + */ +#define ULMK_C29_CPU2_RESET_ADDR 0x20108000u +#define ULMK_C29_CPU3_RESET_ADDR 0x20110000u +#if defined(ULMK_C29_FLASH) && ULMK_C29_FLASH +#define ULMK_C29_SECONDARY_STUB_BYTES 0x80u +#endif + +/* IPC FLAG0 is the only flag that vectors. */ +#define ULMK_ARCH_IPC_FLAG0 0x1u + +/* + * PIPE register map (SPRUJ79 / hw_pipe.h): + * GLOBAL_EN +0x08 + * RTINT_THRESHOLD +0x00 + * INTSP +0x6C + * TASK_CTRL +0x90 + * INT_CTL_L(n) +0x1000 + n*4 EN / FLAG + * INT_CTL_H(n) +0x2000 + n*4 FLAG_FRC / FLAG_CLR + * INT_CONFIG(n) +0x3000 + n*4 PRI_LEVEL / CONTEXT_ID + * INT_LINK_OWNER +0x4000 + n*4 + * INT_VECT_ADDR(n) +0x5000 + n*4 + */ +#define ULMK_ARCH_PIPE_MMR_CLR \ + (ULMK_BOARD_PIPE_BASE + 0xA0u) +#define ULMK_ARCH_PIPE_MEM_INIT \ + (ULMK_BOARD_PIPE_BASE + 0x44u) +#define ULMK_ARCH_PIPE_MEM_INIT_STS \ + (ULMK_BOARD_PIPE_BASE + 0x48u) +#define ULMK_ARCH_PIPE_MEM_INIT_KEY 0x5A5A0000u +#define ULMK_ARCH_PIPE_GLOBAL_EN \ + (ULMK_BOARD_PIPE_BASE + 0x08u) +#define ULMK_ARCH_PIPE_RTINT_THRESHOLD \ + (ULMK_BOARD_PIPE_BASE + 0x00u) +#define ULMK_ARCH_PIPE_INTSP \ + (ULMK_BOARD_PIPE_BASE + 0x6Cu) +#define ULMK_ARCH_PIPE_TASK_CTRL \ + (ULMK_BOARD_PIPE_BASE + 0x90u) +#define ULMK_ARCH_PIPE_INT_CTL_L(n) \ + (ULMK_BOARD_PIPE_BASE + 0x1000u + ((uint32_t)(n) * 4u)) +#define ULMK_ARCH_PIPE_INT_CTL_H(n) \ + (ULMK_BOARD_PIPE_BASE + 0x2000u + ((uint32_t)(n) * 4u)) +#define ULMK_ARCH_PIPE_INT_CONFIG(n) \ + (ULMK_BOARD_PIPE_BASE + 0x3000u + ((uint32_t)(n) * 4u)) +#define ULMK_ARCH_PIPE_INT_LINK_OWNER(n) \ + (ULMK_BOARD_PIPE_BASE + 0x4000u + ((uint32_t)(n) * 4u)) +#define ULMK_ARCH_PIPE_INT_VECT(n) \ + (ULMK_BOARD_PIPE_BASE + 0x5000u + ((uint32_t)(n) * 4u)) + +#define ULMK_ARCH_PIPE_GLOBAL_EN_KEY 0xFACE0000u +#define ULMK_ARCH_PIPE_GLOBAL_EN_VAL 0x3u +#define ULMK_ARCH_PIPE_TASK_CTRL_KEY 0xCAFE0000u +#define ULMK_ARCH_PIPE_SUP_IGN_INTE_EN 0x100u + +#define ULMK_ARCH_PIPE_CTL_L_EN 0x1u +#define ULMK_ARCH_PIPE_FLAG_FRC 0x1u +#define ULMK_ARCH_PIPE_FLAG_CLR 0x2u + +#define ULMK_ARCH_PIPE_OWNER_LINK2 2u +#define ULMK_ARCH_PIPE_SUP_PRI 255u +#define ULMK_ARCH_PIPE_INTSP_STACK 2u /* SSU_STACK2 — matches TI init + ESTS */ + +#define ULMK_ARCH_MEM_DLB_CONFIG \ + (ULMK_BOARD_MEMSSMISCI_BASE + 0x0u) +#define ULMK_ARCH_MEM_DLB_CPU1_EN (1u << 0) +#define ULMK_ARCH_MEM_DLB_CPU2_EN (1u << 1) +#define ULMK_ARCH_MEM_DLB_CPU3_EN (1u << 2) + +#define ULMK_ARCH_SYSCTL_LSEN \ + (ULMK_BOARD_DEVCFG_BASE + 0x348u) + +#endif /* ULMK_ARCH_C29_CONFIG_H */ diff --git a/arch/c29/include/ulmk_arch.h b/arch/c29/include/ulmk_arch.h new file mode 100644 index 0000000..ef5dacb --- /dev/null +++ b/arch/c29/include/ulmk_arch.h @@ -0,0 +1,148 @@ +/* SPDX-License-Identifier: MIT */ +/* + * Architecture abstraction layer — TI C29x / F29H85x + * Contract: docs/arch_api_spec.md + docs/c29f_port_plan.md + */ + +#ifndef ULMK_ARCH_H +#define ULMK_ARCH_H + +#include +#include +#include +#include +#include + +typedef struct { + uint32_t sp; /* A15 of the suspended thread frame */ +} ulmk_arch_ctx_t; + +typedef uint32_t ulmk_arch_irq_key_t; + +typedef struct { + volatile uint32_t locked; +} ulmk_spinlock_t; + +#define ULMK_SPINLOCK_INIT { 0u } + +typedef struct { + uintptr_t base; + size_t size; + uint32_t perms; + uint8_t type; +} ulmk_arch_region_t; + +#define ULMK_REGION_CODE 0 +#define ULMK_REGION_DATA 1 +#define ULMK_REGION_STACK 2 +#define ULMK_REGION_HEAP 3 +#define ULMK_REGION_PERIPH 4 +#define ULMK_REGION_SHARED 5 + +ulmk_arch_irq_key_t ulmk_arch_cpu_irq_save(void); +void ulmk_arch_cpu_irq_restore(ulmk_arch_irq_key_t key); +void ulmk_arch_cpu_irq_enable(void); +void ulmk_arch_cpu_irq_disable(void); +void ulmk_arch_cpu_idle(void); +void ulmk_arch_cpu_halt(void); +uint32_t ulmk_arch_cpu_clz(uint32_t val); +uint32_t ulmk_arch_cpu_id(void); + +void ulmk_arch_spin_lock(ulmk_spinlock_t *lock); +void ulmk_arch_spin_unlock(ulmk_spinlock_t *lock); +ulmk_arch_irq_key_t ulmk_arch_spin_lock_irqsave(ulmk_spinlock_t *lock); +void ulmk_arch_spin_unlock_irqrestore(ulmk_spinlock_t *lock, + ulmk_arch_irq_key_t key); + +void ulmk_arch_send_ipi(uint32_t cpu_id); +void ulmk_arch_ipi_clear_self(void); +void ulmk_arch_ipi_note_enter(void); +void ulmk_arch_ipi_pulse_self(void); +void ulmk_arch_secondary_init(void); +void ulmk_arch_secondary_mark_ready(void); +void ulmk_arch_start_secondary(uint32_t cpu_id, void (*entry)(void)); +void ulmk_arch_smp_mark_ready(void); +void ulmk_arch_smp_park(void); + +void ulmk_arch_cycle_enable(void); +uint32_t ulmk_arch_cycle_read(void); + +void ulmk_arch_csa_pool_init(uintptr_t pool_base, size_t pool_size); + +void ulmk_arch_ctx_init(ulmk_arch_ctx_t *ctx, + void (*entry)(void *arg), void *arg, + uintptr_t stack_top, ulmk_privilege_t priv); + +void ulmk_arch_ctx_switch(ulmk_arch_ctx_t *from, const ulmk_arch_ctx_t *to); +void ulmk_arch_ctx_free(ulmk_arch_ctx_t *ctx); + +#define ULMK_SCHED_SWITCH_COOP 0u +#define ULMK_SCHED_SWITCH_PREEMPT_ISR 1u + +bool ulmk_arch_sched_isr_preempt_deferred(void); +void ulmk_arch_sched_switch(ulmk_arch_ctx_t *from, const ulmk_arch_ctx_t *to, + unsigned int flags); + +void ulmk_arch_mpu_init(void); +void ulmk_arch_mpu_enable(void); +void ulmk_arch_mpu_disable(void); +void ulmk_arch_mpu_configure(uint8_t prs, const ulmk_arch_region_t *regions, + uint8_t count); +void ulmk_arch_mpu_switch(const ulmk_arch_region_t *regions, uint8_t count, + uint8_t prs); +bool ulmk_arch_mpu_addr_permitted(uintptr_t addr, size_t size, uint32_t perms); +bool ulmk_arch_ssu_is_enforcing(void); +uint32_t ulmk_arch_ssu_mode(void); +uint32_t ulmk_arch_smp_ready_mask(void); +void ulmk_arch_smp_wait_ready(uint32_t mask); + +void ulmk_arch_irq_vectors_init(uintptr_t btv, uintptr_t biv, uintptr_t isp_top); +void ulmk_arch_irq_src_configure(uint8_t srpn, uint8_t priority, uint8_t cpu_id); +void ulmk_arch_irq_src_register(uint8_t srpn, uint32_t src_reg_addr); +void ulmk_arch_irq_src_enable(uint8_t srpn); +void ulmk_arch_irq_src_disable(uint8_t srpn); +void ulmk_arch_irq_src_ack(uint8_t srpn); +bool ulmk_arch_irq_src_is_pending(uint8_t srpn); +void ulmk_arch_irq_src_trigger(uint8_t srpn); + +bool ulmk_arch_irq_attach_call(ulmk_irq_attach_fn_t fn, void *data, + const ulmk_arch_region_t *regions, + uint8_t count); + +uint32_t ulmk_arch_atomic_cas(volatile uint32_t *ptr, + uint32_t expected, uint32_t desired); +uint32_t ulmk_arch_atomic_add(volatile uint32_t *ptr, uint32_t val); + +void ulmk_kern_start(void); +void ulmk_board_init(void); +void ulmk_arch_init(ulmk_boot_info_t *info); + +void ulmk_arch_tick_init(uint32_t tick_hz); +void ulmk_arch_tick_ack(void); +uint32_t ulmk_arch_timer_wheel_cpu(void); + +void ulmk_arch_idle_entry(void *arg); + +void ulmk_arch_syscall_entry(void); +void ulmk_arch_trap_entry(uint8_t trap_class, uint8_t tin); +void ulmk_arch_trap_dump(uint8_t trap_class, uint8_t tin); + +void ulmk_printk_char_out(char c); + +void ulmk_kern_irq_dispatch(uint8_t srpn); +void ulmk_kern_ipi_resched(void); +void ulmk_kern_timer_tick(void); +#if ULMK_CONFIG_ENABLE_SMP +void ulmk_kern_ipi_from_isr(void); +#endif +void ulmk_kern_sched_dispatch(bool from_isr); +void ulmk_kern_secondary_main(void); +uint32_t ulmk_kern_syscall_ret_resolve(uint32_t ret); +uint32_t ulmk_kern_trap_syscall(uint8_t tin, uint32_t args[4]); +void ulmk_kern_trap_recoverable(void); +void ulmk_kern_trap_panic(void); +bool ulmk_irq_in_attach(void); +void ulmk_kern_trap_mpu_restore(void); +void ulmk_kern_main(const ulmk_boot_info_t *info); + +#endif /* ULMK_ARCH_H */ diff --git a/arch/c29/include/ulmk_syscall_abi.h b/arch/c29/include/ulmk_syscall_abi.h new file mode 100644 index 0000000..5eeeafe --- /dev/null +++ b/arch/c29/include/ulmk_syscall_abi.h @@ -0,0 +1,103 @@ +/* SPDX-License-Identifier: MIT */ +/* + * Syscall ABI — TI C29x + * + * Userspace forces Supervisor software INT (INT_SW2) after loading: + * D4 = syscall number + * D0..D3 = args + * Return value is written back into the saved D0 slot by the INT epilogue. + */ + +#ifndef ULMK_SYSCALL_ABI_C29_H +#define ULMK_SYSCALL_ABI_C29_H + +#include + +/* PIPE INT_CTL_H(254) = 0x30020000 + 0x2000 + 254*4 = 0x300223F8? + * Wait: 0x2000 + 254*4 = 0x2000 + 0x3F8 = 0x23F8 → 0x300223F8 + * Earlier macro said 0x30022278 — that would be channel 158. Fix to 254. + */ +#define ULMK_C29_PIPE_SWINT_CTL_ADDR 0x300223F8u + +#define ULMK_C29_FORCE_SYSCALL_INT() \ + __asm__ volatile( \ + "MV D7, #1\n\t" \ + "ST.W0 @%0, D7\n\t" \ + "NOP #8\n\t" \ + "NOP #5\n\t" \ + : \ + : "i"(ULMK_C29_PIPE_SWINT_CTL_ADDR) \ + : "d7", "memory") + +#define ULMK_SYSCALL_0(nr, ret) \ + do { \ + register uint32_t _d0 __asm__("d0"); \ + register uint32_t _d4 __asm__("d4") = (uint32_t)(nr); \ + __asm__ volatile("" :: "r"(_d4) : "memory"); \ + ULMK_C29_FORCE_SYSCALL_INT(); \ + __asm__ volatile("" : "=r"(_d0) : : "memory"); \ + (ret) = _d0; \ + } while (0) + +#define ULMK_SYSCALL_1(nr, a0v, ret) \ + do { \ + register uint32_t _d0 __asm__("d0") = (uint32_t)(uintptr_t)(a0v); \ + register uint32_t _d4 __asm__("d4") = (uint32_t)(nr); \ + __asm__ volatile("" : "+r"(_d0) : "r"(_d4) : "memory"); \ + ULMK_C29_FORCE_SYSCALL_INT(); \ + __asm__ volatile("" : "+r"(_d0) : : "memory"); \ + (ret) = _d0; \ + } while (0) + +#define ULMK_SYSCALL_2(nr, a0v, a1v, ret) \ + do { \ + register uint32_t _d0 __asm__("d0") = (uint32_t)(uintptr_t)(a0v); \ + register uint32_t _d1 __asm__("d1") = (uint32_t)(uintptr_t)(a1v); \ + register uint32_t _d4 __asm__("d4") = (uint32_t)(nr); \ + __asm__ volatile("" : "+r"(_d0) : "r"(_d1), "r"(_d4) : "memory"); \ + ULMK_C29_FORCE_SYSCALL_INT(); \ + __asm__ volatile("" : "+r"(_d0) : : "memory"); \ + (ret) = _d0; \ + } while (0) + +#define ULMK_SYSCALL_3(nr, a0v, a1v, a2v, ret) \ + do { \ + register uint32_t _d0 __asm__("d0") = (uint32_t)(uintptr_t)(a0v); \ + register uint32_t _d1 __asm__("d1") = (uint32_t)(uintptr_t)(a1v); \ + register uint32_t _d2 __asm__("d2") = (uint32_t)(uintptr_t)(a2v); \ + register uint32_t _d4 __asm__("d4") = (uint32_t)(nr); \ + __asm__ volatile("" : "+r"(_d0) \ + : "r"(_d1), "r"(_d2), "r"(_d4) : "memory"); \ + ULMK_C29_FORCE_SYSCALL_INT(); \ + __asm__ volatile("" : "+r"(_d0) : : "memory"); \ + (ret) = _d0; \ + } while (0) + +#define ULMK_SYSCALL_4(nr, a0v, a1v, a2v, a3v, ret) \ + do { \ + register uint32_t _d0 __asm__("d0") = (uint32_t)(uintptr_t)(a0v); \ + register uint32_t _d1 __asm__("d1") = (uint32_t)(uintptr_t)(a1v); \ + register uint32_t _d2 __asm__("d2") = (uint32_t)(uintptr_t)(a2v); \ + register uint32_t _d3 __asm__("d3") = (uint32_t)(uintptr_t)(a3v); \ + register uint32_t _d4 __asm__("d4") = (uint32_t)(nr); \ + __asm__ volatile("" : "+r"(_d0) \ + : "r"(_d1), "r"(_d2), "r"(_d3), "r"(_d4) : "memory"); \ + ULMK_C29_FORCE_SYSCALL_INT(); \ + __asm__ volatile("" : "+r"(_d0) : : "memory"); \ + (ret) = _d0; \ + } while (0) + +typedef struct { + ulmk_msg_t msg; + ulmk_tid_t sender; + uint32_t notif_bits; + int is_notif; +} ulmk_recv_or_notif_result_t; + +typedef struct { + const ulmk_msg_t *reply; + ulmk_msg_t *next; + ulmk_tid_t *next_sender; +} ulmk_reply_recv_args_t; + +#endif /* ULMK_SYSCALL_ABI_C29_H */ diff --git a/arch/c29/irq.c b/arch/c29/irq.c new file mode 100644 index 0000000..5aaf056 --- /dev/null +++ b/arch/c29/irq.c @@ -0,0 +1,261 @@ +/* SPDX-License-Identifier: MIT */ +/* + * C29 IRQ and tick — PIPE programming matches TI driverlib/hw_pipe.h. + */ + +#include +#include + +#include +#include +#include + +extern void ulmk_c29_int_veneer(void); +extern void ulmk_c29_tick_isr(void); +extern void ulmk_c29_syscall_isr(void); +extern void ulmk_c29_yield_isr(void); +extern void ulmk_c29_ipi1_isr(void); +extern void ulmk_c29_ipi2_isr(void); + +extern void ulmk_kern_irq_dispatch(uint8_t srpn); +extern void ulmk_kern_timer_tick(void); +extern void ulmk_kern_sched_dispatch(bool from_isr); +extern uint32_t ulmk_kern_trap_syscall(uint8_t tin, uint32_t args[4]); +extern uint32_t ulmk_kern_syscall_ret_resolve(uint32_t ret); +#if ULMK_CONFIG_ENABLE_SMP +extern void ulmk_kern_ipi_from_isr(void); +#endif + +#define MMIO8(a) (*(volatile uint8_t *)(uintptr_t)(a)) +#define MMIO32(a) (*(volatile uint32_t *)(uintptr_t)(a)) + +#define TIMER2_BASE ULMK_BOARD_CPUTIMER2_BASE +#define TIMER2_TIM ((volatile uint32_t *)(TIMER2_BASE + 0x00u)) +#define TIMER2_PRD ((volatile uint32_t *)(TIMER2_BASE + 0x04u)) +#define TIMER2_TCR ((volatile uint16_t *)(TIMER2_BASE + 0x08u)) + +#define TCR_TIE (1u << 14) +#define TCR_TIF (1u << 15) +#define TCR_TSS (1u << 4) +#define TCR_TRB (1u << 5) +#define TCR_FREE (1u << 11) +#define TCR_SOFT (1u << 10) + +/* Diagnostic: CPUTIMER2 ISR hit count (Gate B tick bring-up). */ +volatile uint32_t g_c29_tick_hits; + +/* Nesting depth — also used by arch.c / NMI to defer coop switches to RETI. */ +uint32_t g_c29_int_depth; + +static void pipe_config_channel(uint8_t srpn, void (*handler)(void), + uint8_t priority) +{ + MMIO32(ULMK_ARCH_PIPE_INT_VECT(srpn)) = + (uint32_t)(uintptr_t)handler; + /* PRI in [7:0]; CONTEXT_ID 0 (same as FreeRTOS default path). */ + MMIO32(ULMK_ARCH_PIPE_INT_CONFIG(srpn)) = + (uint32_t)priority; + MMIO32(ULMK_ARCH_PIPE_INT_LINK_OWNER(srpn)) = + ULMK_ARCH_PIPE_OWNER_LINK2; + MMIO8(ULMK_ARCH_PIPE_INT_CTL_L(srpn)) = 0u; + MMIO8(ULMK_ARCH_PIPE_INT_CTL_H(srpn)) = + (uint8_t)ULMK_ARCH_PIPE_FLAG_CLR; + MMIO8(ULMK_ARCH_PIPE_INT_CTL_L(srpn)) = + (uint8_t)ULMK_ARCH_PIPE_CTL_L_EN; +} + +uint32_t ulmk_c29_int_dispatch(uint32_t frame_sp, uint32_t srpn) +{ + uint32_t args[4]; + uint32_t *frame; + uint32_t ret; + uint8_t tin; + bool outer; + + frame = (uint32_t *)(uintptr_t)frame_sp; + outer = (g_c29_int_depth == 0u); + g_c29_int_depth++; + + if (srpn == ULMK_BOARD_IRQ_TIMER2) { + *TIMER2_TCR |= (uint16_t)TCR_TIF; + g_c29_tick_hits++; + if (outer) + ulmk_kern_timer_tick(); + } else if (srpn == ULMK_BOARD_IRQ_SWINT_SYSCALL) { + args[0] = frame[72u / 4u]; + args[1] = frame[76u / 4u]; + args[2] = frame[80u / 4u]; + args[3] = frame[84u / 4u]; + tin = (uint8_t)frame[88u / 4u]; + ret = ulmk_kern_trap_syscall(tin, args); + ret = ulmk_kern_syscall_ret_resolve(ret); + frame[72u / 4u] = ret; + if (outer) + ulmk_kern_sched_dispatch(false); + } else if (srpn == ULMK_BOARD_IRQ_SWINT_YIELD) { + if (outer) + ulmk_kern_sched_dispatch(false); +#if ULMK_CONFIG_ENABLE_SMP + } else if (srpn == ULMK_BOARD_IRQ_IPC_1 || + srpn == ULMK_BOARD_IRQ_IPC_2) { + ulmk_arch_ipi_clear_self(); + if (outer) + ulmk_kern_ipi_from_isr(); +#endif + } else { + ulmk_kern_irq_dispatch((uint8_t)srpn); + if (outer) + ulmk_kern_sched_dispatch(true); + } + + ulmk_arch_irq_src_ack((uint8_t)srpn); + g_c29_int_depth--; + return 0; +} + +void ulmk_arch_irq_vectors_init(uintptr_t btv, uintptr_t biv, uintptr_t isp_top) +{ + uint32_t spins; + + (void)btv; + (void)biv; + (void)isp_top; + + /* PIPE MMR + vector RAM init (TI Interrupt_initModule). */ + MMIO32(ULMK_ARCH_PIPE_MMR_CLR) = 0x3u; + MMIO32(ULMK_ARCH_PIPE_MEM_INIT) = + ULMK_ARCH_PIPE_MEM_INIT_KEY | 0x3u; + for (spins = 0u; spins < 1000000u; spins++) { + if (MMIO32(ULMK_ARCH_PIPE_MEM_INIT_STS) == 0x2u) + break; + } + + /* Keep RTINT_THRESHOLD at 0 so priority-255 channels stay INT + * (RETI.INT). Supervisor delivery uses SUP_IGN_INTE_EN instead. + */ + MMIO32(ULMK_ARCH_PIPE_RTINT_THRESHOLD) = 0u; + MMIO32(ULMK_ARCH_PIPE_TASK_CTRL) = + ULMK_ARCH_PIPE_SUP_IGN_INTE_EN | ULMK_ARCH_PIPE_TASK_CTRL_KEY; + MMIO8(ULMK_ARCH_PIPE_INTSP) = (uint8_t)ULMK_ARCH_PIPE_INTSP_STACK; + + pipe_config_channel(ULMK_BOARD_IRQ_TIMER2, ulmk_c29_tick_isr, + ULMK_ARCH_PIPE_SUP_PRI); + pipe_config_channel(ULMK_BOARD_IRQ_SWINT_SYSCALL, ulmk_c29_syscall_isr, + ULMK_ARCH_PIPE_SUP_PRI); + pipe_config_channel(ULMK_BOARD_IRQ_SWINT_YIELD, ulmk_c29_yield_isr, + ULMK_ARCH_PIPE_SUP_PRI); +#if ULMK_CONFIG_ENABLE_SMP + pipe_config_channel(ULMK_BOARD_IRQ_IPC_1, ulmk_c29_ipi1_isr, + ULMK_ARCH_PIPE_SUP_PRI); + pipe_config_channel(ULMK_BOARD_IRQ_IPC_2, ulmk_c29_ipi2_isr, + ULMK_ARCH_PIPE_SUP_PRI); +#endif + + MMIO32(ULMK_ARCH_PIPE_GLOBAL_EN) = + ULMK_ARCH_PIPE_GLOBAL_EN_VAL | ULMK_ARCH_PIPE_GLOBAL_EN_KEY; +} + +void ulmk_arch_irq_src_configure(uint8_t srpn, uint8_t priority, uint8_t cpu_id) +{ + (void)cpu_id; + pipe_config_channel(srpn, ulmk_c29_int_veneer, priority); +} + +void ulmk_arch_irq_src_register(uint8_t srpn, uint32_t src_reg_addr) +{ + (void)src_reg_addr; + MMIO32(ULMK_ARCH_PIPE_INT_VECT(srpn)) = + (uint32_t)(uintptr_t)ulmk_c29_int_veneer; +} + +void ulmk_arch_irq_src_enable(uint8_t srpn) +{ + MMIO8(ULMK_ARCH_PIPE_INT_CTL_L(srpn)) = + (uint8_t)ULMK_ARCH_PIPE_CTL_L_EN; +} + +void ulmk_arch_irq_src_disable(uint8_t srpn) +{ + MMIO8(ULMK_ARCH_PIPE_INT_CTL_L(srpn)) = 0u; +} + +void ulmk_arch_irq_src_ack(uint8_t srpn) +{ + MMIO8(ULMK_ARCH_PIPE_INT_CTL_H(srpn)) = + (uint8_t)ULMK_ARCH_PIPE_FLAG_CLR; +} + +bool ulmk_arch_irq_src_is_pending(uint8_t srpn) +{ + return (MMIO8(ULMK_ARCH_PIPE_INT_CTL_L(srpn)) & 0x2u) != 0u; +} + +void ulmk_arch_irq_src_trigger(uint8_t srpn) +{ + MMIO8(ULMK_ARCH_PIPE_INT_CTL_H(srpn)) = + (uint8_t)ULMK_ARCH_PIPE_FLAG_FRC; +} + +bool ulmk_arch_irq_attach_call(ulmk_irq_attach_fn_t fn, void *data, + const ulmk_arch_region_t *regions, uint8_t count) +{ + (void)fn; + (void)data; + (void)regions; + (void)count; + return false; +} + +void ulmk_arch_tick_init(uint32_t tick_hz) +{ + uint32_t period; + uint16_t tcr; + + if (tick_hz == 0u) + tick_hz = 1000u; + period = ULMK_BOARD_TICK_CLOCK_HZ / tick_hz; + if (period < 2u) + period = 2u; + + /* Stop, load period, enable IRQ, free-run, reload, start. */ + tcr = *TIMER2_TCR; + *TIMER2_TCR = (uint16_t)((tcr & (uint16_t)~TCR_TIF) | TCR_TSS); + *TIMER2_PRD = period; + *TIMER2_TIM = period; + tcr = *TIMER2_TCR; + *TIMER2_TCR = (uint16_t)((tcr & (uint16_t)~TCR_TIF) | + TCR_TIE | TCR_FREE); + tcr = *TIMER2_TCR; + *TIMER2_TCR = (uint16_t)((tcr & (uint16_t)~TCR_TIF) | TCR_TRB); + *TIMER2_TCR = (uint16_t)(*TIMER2_TCR & (uint16_t)~TCR_TSS); + + /* Ensure PIPE channel still enabled after timer bring-up. */ + MMIO8(ULMK_ARCH_PIPE_INT_CTL_L(ULMK_BOARD_IRQ_TIMER2)) = + (uint8_t)ULMK_ARCH_PIPE_CTL_L_EN; +} + +void ulmk_arch_tick_ack(void) +{ +} + +uint32_t ulmk_arch_timer_wheel_cpu(void) +{ + return ulmk_arch_cpu_id(); +} + +void ulmk_arch_syscall_entry(void) +{ +} + +void ulmk_arch_trap_entry(uint8_t trap_class, uint8_t tin) +{ + (void)trap_class; + (void)tin; + ulmk_kern_trap_panic(); +} + +void ulmk_arch_trap_dump(uint8_t trap_class, uint8_t tin) +{ + (void)trap_class; + (void)tin; +} diff --git a/arch/c29/linker/prologue.cmd.in b/arch/c29/linker/prologue.cmd.in new file mode 100644 index 0000000..5ea6268 --- /dev/null +++ b/arch/c29/linker/prologue.cmd.in @@ -0,0 +1,29 @@ +/* arch/c29/linker/prologue.cmd.in + * Layer 2 — arch-owned, C29-specific. + */ + +--entry_point=code_start + +--retain="*(codestart)" +--retain="*(cert)" +--retain="*(resetvector)" +--retain="*(.cpu2_codestart)" +--retain="*(.cpu2_nmivector)" +--retain="*(.cpu3_codestart)" +--retain="*(.cpu3_nmivector)" +--retain="*(.TI.bound:CPU1_Cfg)" +--retain="*(.TI.bound:CPU2_Cfg)" +--retain="*(.TI.bound:CPU3_Cfg)" +--retain="*(.TI.bound:CPU4_Cfg)" +--retain="*(.text.ulmk_c29_int_veneer)" +--retain="*(.text.ulmk_c29_tick_isr)" +--retain="*(.text.ulmk_c29_syscall_isr)" +--retain="*(.text.ulmk_c29_yield_isr)" +--retain="*(.text.ulmk_c29_ipi1_isr)" +--retain="*(.text.ulmk_c29_ipi2_isr)" +--retain="*(.text.ulmk_arch_nmi_handler)" +--retain="*(.text.ulmk_arch_fault_handler)" +--retain="*(.text.ulmk_arch_idle_entry)" +--retain="*(.text.ulmk_c29_thread_trampoline)" +--retain="*(.text.ulmk_c29_cpu2_start)" +--retain="*(.text.ulmk_c29_cpu3_start)" diff --git a/arch/c29/linker/vectors.cmd.in b/arch/c29/linker/vectors.cmd.in new file mode 100644 index 0000000..e2ece98 --- /dev/null +++ b/arch/c29/linker/vectors.cmd.in @@ -0,0 +1,15 @@ +/* arch/c29/linker/vectors.cmd.in + * Layer 2 — startup and reset-vector sections for C29 (RAM profile). + * + * Matches TI multicore map: CPU2 @ LPA1, CPU3 @ CPA0. CPU1 code spills + * from LPA0 into CPA1 via generate_ld.py (>> KERNEL_FLASH | CPU1_CPA1). + */ + + codestart : {} > 0x20100000 + resetvector : {} > KERNEL_FLASH + + /* Secondary CPU payloads — must match ULMK_C29_CPU{2,3}_RESET_ADDR. */ + .cpu2_codestart : {} > 0x20108000 + .cpu2_nmivector : {} > 0x20108040 + .cpu3_codestart : {} > 0x20110000 + .cpu3_nmivector : {} > 0x20110040 diff --git a/arch/c29/linker/vectors_flash.cmd.in b/arch/c29/linker/vectors_flash.cmd.in new file mode 100644 index 0000000..b42dc15 --- /dev/null +++ b/arch/c29/linker/vectors_flash.cmd.in @@ -0,0 +1,12 @@ +/* arch/c29/linker/vectors_flash.cmd.in — startup sections, FLASH profile. + * + * CPU1 boots XIP from FLASH_RP0 (0x10001000). Boot certificate at + * 0x10000000..0x10000FFF (filled post-build). + * + * resetvector is emitted after .text/.data (see generate_ld.py) so secondary + * stubs can follow it without punching a hole in the signed cert image. + */ + + codestart : {} > 0x10001000, palign(8) + /* Filled by cert_placeholder.c; package-seccfg --update-section replaces. */ + cert : {} > CERT, palign(8) diff --git a/arch/c29/linker/vectors_flash_seccfg.cmd.in b/arch/c29/linker/vectors_flash_seccfg.cmd.in new file mode 100644 index 0000000..4b969b2 --- /dev/null +++ b/arch/c29/linker/vectors_flash_seccfg.cmd.in @@ -0,0 +1,10 @@ +/* arch/c29/linker/vectors_flash_seccfg.cmd.in — NonMain SECCFG (BANKMODE0). + * + * Blobs come from board seccfg/seccfg_cpuN.c (tools/c29_seccfg_gen.py). + * package-seccfg strips these unless ULMK_C29_SECCFG_COMMIT=1. + */ + + .TI.bound:CPU1_Cfg : { *(.TI.bound:CPU1_Cfg) } > SECCFG_CPU1 + .TI.bound:CPU2_Cfg : { *(.TI.bound:CPU2_Cfg) } > SECCFG_CPU2 + .TI.bound:CPU3_Cfg : { *(.TI.bound:CPU3_Cfg) } > SECCFG_CPU3 + .TI.bound:CPU4_Cfg : { *(.TI.bound:CPU4_Cfg) } > SECCFG_CPU4 diff --git a/arch/c29/linker/vectors_flash_smp.cmd.in b/arch/c29/linker/vectors_flash_smp.cmd.in new file mode 100644 index 0000000..f80e5b4 --- /dev/null +++ b/arch/c29/linker/vectors_flash_smp.cmd.in @@ -0,0 +1,21 @@ +/* arch/c29/linker/vectors_flash_smp.cmd.in — secondary stubs, FLASH+SMP. + * + * Appended at the end of SECTIONS (after .text/.data) so stubs sit past the + * signed CPU1 image. package-seccfg keeps them out of the cert BIN and + * programs them only when ULMK_C29_FLASH_SECONDARY=1. Layout matches + * release RAM: +0 codestart, +0x40 nmivector, 0x80 bytes total. + */ + + .cpu2_stub : { + *(.cpu2_codestart) + . = 0x40; + *(.cpu2_nmivector) + . = 0x80; + } > KERNEL_FLASH, palign(8) + + .cpu3_stub : { + *(.cpu3_codestart) + . = 0x40; + *(.cpu3_nmivector) + . = 0x80; + } > KERNEL_FLASH, palign(8) diff --git a/arch/c29/linker/vectors_flash_tail.cmd.in b/arch/c29/linker/vectors_flash_tail.cmd.in new file mode 100644 index 0000000..ea2f187 --- /dev/null +++ b/arch/c29/linker/vectors_flash_tail.cmd.in @@ -0,0 +1,7 @@ +/* arch/c29/linker/vectors_flash_tail.cmd.in — after .text/.data (FLASH). + * + * resetvector must come *before* any SMP stubs in this file so the signed + * cert image ends at resetvector and stubs sit past it (outside the BIN). + */ + + resetvector : {} > KERNEL_FLASH diff --git a/arch/c29/mpu.c b/arch/c29/mpu.c new file mode 100644 index 0000000..77e3c39 --- /dev/null +++ b/arch/c29/mpu.c @@ -0,0 +1,229 @@ +/* SPDX-License-Identifier: MIT */ +/* + * C29 SSU / APR — dynamic region programming for the current thread. + * + * SSUMODE1 + Link2 AP override remains for bring-up until a CRC-valid SECCFG + * boots SSUMODE2 (Gate D). Dynamic APR slots are still programmed so the + * shadow used by ulmk_arch_mpu_addr_permitted() matches the TCB region list. + */ + +#include +#include +#include + +#include + +#define MMIO32(a) (*(volatile uint32_t *)(uintptr_t)(a)) + +#define SSU_O_MODE 0x08u +#define SSU_O_LINK2_AP_OVERRIDE 0x0Cu +#define SSU_AP_STRIDE 0x20u +#define SSU_AP_CFG_APD (1u << 6) +#define SSU_AP_CFG_XE (1u << 7) +#define SSU_LINK_RW(link) (3u << ((link) * 2u)) +#define SSU_LINK_RD(link) (1u << ((link) * 2u)) +#define SSU_EXE_LINK_RUNTIME 1u /* common runtime Link */ +#define SSU_DYN_APR_BASE 8u /* reserve 0-7 for static */ + +static ulmk_arch_region_t g_active_regions[ULMK_ARCH_MAX_REGIONS]; +static uint8_t g_active_count; +static uint8_t g_ssu_enforcing; +static uint8_t g_ssu_mode2; + +static uint32_t ssu_ap_base(void) +{ + uint32_t cpu = ulmk_arch_cpu_id(); + + return ULMK_BOARD_SSUCPU1AP_BASE + (cpu * 0x1000u); +} + +static void apr_disable(uint32_t ap_base, uint8_t region) +{ + uint32_t cfg_addr = ap_base + (region * SSU_AP_STRIDE); + + MMIO32(cfg_addr) |= SSU_AP_CFG_APD; +} + +static void apr_program(uint32_t ap_base, uint8_t region, + uintptr_t start, uintptr_t end, + uint32_t access, bool executable) +{ + uint32_t cfg_addr = ap_base + (region * SSU_AP_STRIDE); + uint32_t cfg; + + /* Align to 4 KiB APR rules. */ + start &= ~(uintptr_t)0xFFFu; + end |= (uintptr_t)0xFFFu; + + apr_disable(ap_base, region); + MMIO32(cfg_addr + 0x04u) = (uint32_t)start; + MMIO32(cfg_addr + 0x08u) = (uint32_t)end; + MMIO32(cfg_addr + 0x14u) = access; + + cfg = (uint32_t)SSU_EXE_LINK_RUNTIME; + if (executable) + cfg |= SSU_AP_CFG_XE; + /* Clear APD last — enable the slot. */ + MMIO32(cfg_addr) = cfg; +} + +void ulmk_arch_mpu_init(void) +{ + uint32_t mode; + + g_active_count = 0u; + g_ssu_enforcing = 0u; + mode = MMIO32(ULMK_BOARD_SSUGEN_BASE + SSU_O_MODE) & 0x3Fu; + g_ssu_mode2 = (mode == 0x0Cu) ? 1u : 0u; + + /* + * Keep Link2 override while SSUMODE1 (or unknown). Clear it only + * after SSUMODE2 is confirmed and static APRs are installed. + */ + if (!g_ssu_mode2) { + MMIO32(ULMK_BOARD_SSUGEN_BASE + SSU_O_LINK2_AP_OVERRIDE) |= + (1u << 0) | (1u << 1) | (1u << 2); + } +} + +static void ssu_install_static_aprs(uint32_t ap_base) +{ + extern char _ulmk_kernel_text_start[]; + extern char _ulmk_kernel_text_end[]; + extern char _ulmk_kernel_data_start[]; + extern char _ulmk_kernel_bss_end[]; + extern char _ulmk_isr_stack_base[]; + extern char _ulmk_isr_stack_top[]; + + /* Slot 0: kernel + user text (RX). */ + apr_program(ap_base, 0u, + (uintptr_t)_ulmk_kernel_text_start, + (uintptr_t)_ulmk_kernel_text_end - 1u, + SSU_LINK_RD(SSU_EXE_LINK_RUNTIME) | SSU_LINK_RD(2u), + true); + /* Slot 1: kernel data/bss (RW). */ + apr_program(ap_base, 1u, + (uintptr_t)_ulmk_kernel_data_start, + (uintptr_t)_ulmk_kernel_bss_end - 1u, + SSU_LINK_RW(SSU_EXE_LINK_RUNTIME) | SSU_LINK_RW(2u), + false); + /* Slot 2: ISR / kernel stacks (RW). */ + apr_program(ap_base, 2u, + (uintptr_t)_ulmk_isr_stack_base, + (uintptr_t)_ulmk_isr_stack_top - 1u, + SSU_LINK_RW(SSU_EXE_LINK_RUNTIME) | SSU_LINK_RW(2u), + false); + /* Slot 3: PIPE + SSU MMIO window (RW, Link2). */ + apr_program(ap_base, 3u, + ULMK_BOARD_PIPE_BASE, + ULMK_BOARD_PIPE_BASE + 0xFFFFu, + SSU_LINK_RW(2u), + false); + apr_program(ap_base, 4u, + ULMK_BOARD_SSUGEN_BASE, + ULMK_BOARD_SSUGEN_BASE + 0xFFFFu, + SSU_LINK_RW(2u), + false); +} + +void ulmk_arch_mpu_enable(void) +{ + if (g_ssu_mode2) { + ssu_install_static_aprs(ssu_ap_base()); + MMIO32(ULMK_BOARD_SSUGEN_BASE + SSU_O_LINK2_AP_OVERRIDE) = 0u; + g_ssu_enforcing = 1u; + } else { + g_ssu_enforcing = 0u; + } +} + +bool ulmk_arch_ssu_is_enforcing(void) +{ + return g_ssu_enforcing != 0u; +} + +uint32_t ulmk_arch_ssu_mode(void) +{ + return MMIO32(ULMK_BOARD_SSUGEN_BASE + SSU_O_MODE) & 0x3Fu; +} + +void ulmk_arch_mpu_disable(void) +{ + g_ssu_enforcing = 0u; + MMIO32(ULMK_BOARD_SSUGEN_BASE + SSU_O_LINK2_AP_OVERRIDE) |= + (1u << 0) | (1u << 1) | (1u << 2); +} + +void ulmk_arch_mpu_configure(uint8_t prs, const ulmk_arch_region_t *regions, + uint8_t count) +{ + uint8_t i; + uint8_t slot; + uint32_t ap_base; + uint32_t access; + bool exec; + + (void)prs; + ap_base = ssu_ap_base(); + + if (!regions || count > ULMK_ARCH_MAX_REGIONS) + count = 0u; + + for (i = 0u; i < count; i++) + g_active_regions[i] = regions[i]; + g_active_count = count; + + /* Tear down previous dynamic slots before programming. */ + for (slot = 0u; slot < ULMK_ARCH_MAX_REGIONS; slot++) + apr_disable(ap_base, (uint8_t)(SSU_DYN_APR_BASE + slot)); + + for (i = 0u; i < count; i++) { + const ulmk_arch_region_t *r = ®ions[i]; + uintptr_t end; + + if (r->size == 0u) + continue; + end = r->base + r->size - 1u; + exec = (r->type == ULMK_REGION_CODE) || + ((r->perms & ULMK_PERM_EXEC) != 0u); + if (r->perms & ULMK_PERM_WRITE) + access = SSU_LINK_RW(SSU_EXE_LINK_RUNTIME) | + SSU_LINK_RW(2u); + else + access = SSU_LINK_RD(SSU_EXE_LINK_RUNTIME) | + SSU_LINK_RD(2u); + apr_program(ap_base, (uint8_t)(SSU_DYN_APR_BASE + i), + r->base, end, access, exec); + } +} + +void ulmk_arch_mpu_switch(const ulmk_arch_region_t *regions, uint8_t count, + uint8_t prs) +{ + ulmk_arch_mpu_configure(prs, regions, count); +} + +bool ulmk_arch_mpu_addr_permitted(uintptr_t addr, size_t size, uint32_t perms) +{ + uint8_t i; + uintptr_t end; + + if (!g_ssu_enforcing) + return true; + if (size == 0u) + return false; + end = addr + size - 1u; + for (i = 0u; i < g_active_count; i++) { + const ulmk_arch_region_t *r = &g_active_regions[i]; + uintptr_t r_end; + + if ((r->perms & perms) != perms) + continue; + if (r->size == 0u) + continue; + r_end = r->base + r->size - 1u; + if (addr >= r->base && end <= r_end) + return true; + } + return false; +} diff --git a/arch/c29/smp.c b/arch/c29/smp.c new file mode 100644 index 0000000..c09aab6 --- /dev/null +++ b/arch/c29/smp.c @@ -0,0 +1,447 @@ +/* SPDX-License-Identifier: MIT */ +/* + * C29 SMP — IPC FLAG0 IPI and secondary bring-up via SSU reset vectors. + */ + +#include +#include + +#include +#include + +#define MMIO32(a) (*(volatile uint32_t *)(uintptr_t)(a)) +#define MMIO16(a) (*(volatile uint16_t *)(uintptr_t)(a)) + +/* Local-CPU IPC send bases (hw_memmap.h). */ +#define IPC_CPU1_TO_CPU2 0x30220000u +#define IPC_CPU1_TO_CPU3 0x30222000u +#define IPC_CPU2_TO_CPU1 0x30228000u +#define IPC_CPU2_TO_CPU3 0x3022A000u +#define IPC_CPU3_TO_CPU1 0x30230000u +#define IPC_CPU3_TO_CPU2 0x30232000u + +#define IPC_O_SET 0x00u +#define IPC_O_CLR 0x04u +#define IPC_O_ACK_OFF 0x20004u /* receive-view ACK relative */ + +#define SSU_O_RST_VECT 0x00u +#define SSU_O_RST_LINK 0x04u +#define SSU_O_CPU_RST_CTRL 0x08u +#define SSU_O_DEF_NMI_VECT 0x10u +#define SSU_O_DEF_NMI_LINK 0x14u +#define SSU_CORE_RESET_DEACTIVE 0x36u +#define SSU_LINK2 2u + +/* + * SSUGEN LINK2 access-protection override. The RTS _c_int00 sets this for + * the running core so LINK2 code may reach every APR; a secondary released + * with a hand-written stub never runs _c_int00, so CPU1 must grant the + * override for the target before release or the stub's first store faults. + */ +#define SSUGEN_LINK2_AP_OVERRIDE 0x3008000Cu + +/* + * Handshake in SHARED_RAM (LDA1 @ 0x200F8000) — visible to every C29. + * Do not use LDA0/high KERNEL_RAM: CPU2/3 data windows are LDA5/CPA-side; + * absolute stores there silently fault under APR and the stub never + * completes the ready handshake. + */ +#define C29_CPU2_MAGIC_ADDR 0x200F8000u +#define C29_CPU3_MAGIC_ADDR 0x200F8004u +#define C29_CPU2_MAGIC_VAL 0xC0DE0002u +#define C29_CPU3_MAGIC_VAL 0xC0DE0003u + +#define SYSCTL_O_RSTSTAT 0x3B0u +#define SYSCTL_RSTSTAT_CPU2 0x1u +#define SYSCTL_RSTSTAT_CPU3 0x2u + +#define SMP_GATE_WAIT 0x11111111u +#define SMP_GATE_READY 0xC0DEC0DEu + +extern void ulmk_c29_cpu2_start(void); +extern void ulmk_c29_cpu3_start(void); + +#if ULMK_CONFIG_ENABLE_SMP + +volatile uint32_t g_c29_smp_gate = SMP_GATE_WAIT; +volatile uint32_t g_c29_secondary_release[ULMK_ARCH_NUM_CPU]; +volatile uint32_t g_c29_ready_mask; +/* Bring-up diagnostics for HIL (filled on secondary timeout). */ +volatile uint32_t g_c29_smp_diag[6]; +static void (*g_secondary_entry[ULMK_ARCH_NUM_CPU])(void); + +static uint32_t ipc_send_base(uint32_t from, uint32_t to) +{ + if (from == 0u && to == 1u) + return IPC_CPU1_TO_CPU2; + if (from == 0u && to == 2u) + return IPC_CPU1_TO_CPU3; + if (from == 1u && to == 0u) + return IPC_CPU2_TO_CPU1; + if (from == 1u && to == 2u) + return IPC_CPU2_TO_CPU3; + if (from == 2u && to == 0u) + return IPC_CPU3_TO_CPU1; + if (from == 2u && to == 1u) + return IPC_CPU3_TO_CPU2; + return 0u; +} + +static uint32_t ipc_ack_base(uint32_t local_cpu) +{ + /* + * Receive-view bases: CPU1 0x30240000, CPU2 0x30248000, CPU3 0x30250000. + * ACK for FLAG0 from either peer is channel 0 at +0x4 of the pair. + */ + static const uint32_t rcv[3] = { + 0x30240000u, 0x30248000u, 0x30250000u + }; + + if (local_cpu >= 3u) + return 0u; + return rcv[local_cpu]; +} + +void ulmk_arch_smp_mark_ready(void) +{ + uint32_t id = ulmk_arch_cpu_id(); + + __asm__ volatile("" ::: "memory"); + g_c29_smp_gate = SMP_GATE_READY; + g_c29_ready_mask |= (1u << id); +} + +uint32_t ulmk_arch_smp_ready_mask(void) +{ + return g_c29_ready_mask; +} + +void ulmk_arch_smp_wait_ready(uint32_t mask) +{ + uint32_t spins; + + for (spins = 0u; spins < 50000000u; spins++) { + if ((g_c29_ready_mask & mask) == mask) + return; + } +} + +void ulmk_arch_send_ipi(uint32_t cpu_id) +{ + uint32_t self = ulmk_arch_cpu_id(); + uint32_t base; + + if (cpu_id >= (uint32_t)ULMK_ARCH_NUM_CPU || cpu_id == self) + return; + base = ipc_send_base(self, cpu_id); + if (!base) + return; + __asm__ volatile("" ::: "memory"); + MMIO32(base + IPC_O_SET) = ULMK_ARCH_IPC_FLAG0; +} + +void ulmk_arch_ipi_clear_self(void) +{ + uint32_t self = ulmk_arch_cpu_id(); + uint32_t rcv = ipc_ack_base(self); + + if (!rcv) + return; + /* Ack FLAG0 from both peer directions (CH0 and the +0x2000 pair). */ + MMIO32(rcv + 0x04u) = ULMK_ARCH_IPC_FLAG0; + MMIO32(rcv + 0x2004u) = ULMK_ARCH_IPC_FLAG0; +} + +void ulmk_arch_ipi_note_enter(void) +{ +} + +void ulmk_arch_ipi_pulse_self(void) +{ + /* No self-MSIP equivalent; force local IPC is not used. */ +} + +void ulmk_arch_secondary_init(void) +{ + extern char _ulmk_isr_stack_top[]; + + ulmk_arch_irq_vectors_init(0u, 0u, (uintptr_t)_ulmk_isr_stack_top); + ulmk_arch_mpu_init(); +} + +void ulmk_arch_secondary_mark_ready(void) +{ + uint32_t id = ulmk_arch_cpu_id(); + + g_c29_ready_mask |= (1u << id); +} + +#define MEMSSLCFG_BASE 0x301D8000u +#define MEMSSCCFG_BASE 0x301D8400u +#define MEMSS_INIT 0x10000u +#define MEMSS_INIT_STS 0x1000000u +#define MEMSS_LPA1_CFG (MEMSSLCFG_BASE + 0x10u) +#define MEMSS_LDA1_CFG (MEMSSLCFG_BASE + 0x30u) +#define MEMSS_CPA0_CFG (MEMSSCCFG_BASE + 0x0u) + +static void memss_init_bank(uint32_t cfg) +{ + uint32_t t; + + MMIO32(cfg) |= MEMSS_INIT; + for (t = 0u; t < 1000000u; t++) { + if ((MMIO32(cfg) & MEMSS_INIT_STS) != 0u) + break; + } +} + +#if defined(ULMK_C29_FLASH) && ULMK_C29_FLASH +/* + * Flash POR has no GEL ram_init: ECC-init the secondary fetch bank and the + * SHARED_RAM handshake window, then copy the stub out of contiguous FLASH_RP0. + */ +static void plant_secondary_from_flash(uint32_t cpu_id, uint32_t dest) +{ + static uint8_t lda1_ready; + uint32_t src; + uint32_t i; + uint32_t nwords; + + if (cpu_id == 1u) { + src = (uint32_t)(uintptr_t)&ulmk_c29_cpu2_start; + memss_init_bank(MEMSS_LPA1_CFG); + } else { + src = (uint32_t)(uintptr_t)&ulmk_c29_cpu3_start; + memss_init_bank(MEMSS_CPA0_CFG); + } + if (!lda1_ready) { + memss_init_bank(MEMSS_LDA1_CFG); + lda1_ready = 1u; + } + + nwords = ULMK_C29_SECONDARY_STUB_BYTES / 4u; + for (i = 0u; i < nwords; i++) + MMIO32(dest + (i * 4u)) = MMIO32(src + (i * 4u)); +} +#endif + +__attribute__((section(".text.link2.ulmk_c29_release"))) +static void release_secondary_cpu(uint32_t cpu_id, uint32_t reset_addr) +{ + uint32_t cfg; + uint32_t nmi; + uint32_t i; + + if (cpu_id == 1u) + cfg = ULMK_BOARD_SSUCPU2CFG_BASE; + else if (cpu_id == 2u) + cfg = ULMK_BOARD_SSUCPU3CFG_BASE; + else + return; + + /* Split-lock before first independent CPU2 use (≥24 cycles). */ + if (cpu_id == 1u) { + MMIO32(ULMK_ARCH_SYSCTL_LSEN) = 0u; + __asm__ volatile("NOP #1\n\tNOP #1\n\tNOP #1\n\tNOP #1\n\t" + "NOP #1\n\tNOP #1\n\tNOP #1\n\tNOP #1\n\t" + "NOP #1\n\tNOP #1\n\tNOP #1\n\tNOP #1\n\t" + "NOP #1\n\tNOP #1\n\tNOP #1\n\tNOP #1\n\t" + "NOP #1\n\tNOP #1\n\tNOP #1\n\tNOP #1\n\t" + "NOP #1\n\tNOP #1\n\tNOP #1\n\tNOP #1" + ::: "memory"); + } + +#if defined(ULMK_C29_FLASH) && ULMK_C29_FLASH + plant_secondary_from_flash(cpu_id, reset_addr); +#endif + + /* + * Grant LINK2 AP override for every C29 (and the target) so the + * reset stub may store its handshake. Bit index = SSU_CPUID. + */ + MMIO32(SSUGEN_LINK2_AP_OVERRIDE) |= 0x7u; + + /* + * Point DEF_NMI at the same stub as RST_VECT (GEL Release_CPUx_Reset + * does this). A distinct +0x40 NMI veneer faults NMI_ISR_ENTRY under + * bring-up (INT_TYPE.NMI_ISR_ENTRY_ERR) before the handshake store. + */ + nmi = reset_addr; + /* Program vectors, then release out of reset (matches TI SDK order). */ + MMIO32(cfg + SSU_O_RST_VECT) = reset_addr; + MMIO32(cfg + SSU_O_RST_LINK) = SSU_LINK2; + MMIO32(cfg + SSU_O_DEF_NMI_VECT) = nmi; + MMIO32(cfg + SSU_O_DEF_NMI_LINK) = SSU_LINK2; + MMIO32(cfg + SSU_O_CPU_RST_CTRL) = SSU_CORE_RESET_DEACTIVE; + + /* + * SysCtl_isCPUxReset(): true while RSTSTAT bit is clear. + * Wait until the bit sets (core out of reset), matching TI SDK. + */ + for (i = 0u; i < 1000000u; i++) { + uint16_t st = MMIO16(ULMK_BOARD_DEVCFG_BASE + SYSCTL_O_RSTSTAT); + uint16_t bit = (cpu_id == 1u) ? SYSCTL_RSTSTAT_CPU2 + : SYSCTL_RSTSTAT_CPU3; + + if ((st & bit) != 0u) + break; + } +} + +void ulmk_arch_start_secondary(uint32_t cpu_id, void (*entry)(void)) +{ + uint32_t reset_addr; + + if (cpu_id == 0u || cpu_id >= (uint32_t)ULMK_ARCH_NUM_CPU || !entry) + return; + + g_secondary_entry[cpu_id] = entry; + __asm__ volatile("" ::: "memory"); + g_c29_secondary_release[cpu_id] = 1u; + + g_c29_smp_diag[0] = cpu_id; + g_c29_smp_diag[1] = + MMIO16(ULMK_BOARD_DEVCFG_BASE + SYSCTL_O_RSTSTAT); + + reset_addr = (cpu_id == 1u) ? ULMK_C29_CPU2_RESET_ADDR + : ULMK_C29_CPU3_RESET_ADDR; + { + uint32_t magic_addr = + (cpu_id == 1u) ? C29_CPU2_MAGIC_ADDR + : C29_CPU3_MAGIC_ADDR; + + MMIO32(magic_addr) = 0u; + } + release_secondary_cpu(cpu_id, reset_addr); + + /* Poll shared-RAM magic written by the secondary reset stub. */ + { + uint32_t bit = 1u << cpu_id; + uint32_t spins; + uint16_t st; + uint32_t cfg; + uint32_t word0; + uint32_t magic_addr; + uint32_t magic_exp; + uint32_t magic; + volatile uint32_t *dlb = + (volatile uint32_t *)ULMK_ARCH_MEM_DLB_CONFIG; + + magic_addr = (cpu_id == 1u) ? C29_CPU2_MAGIC_ADDR + : C29_CPU3_MAGIC_ADDR; + magic_exp = (cpu_id == 1u) ? C29_CPU2_MAGIC_VAL + : C29_CPU3_MAGIC_VAL; + + MMIO32(magic_addr) = 0u; + *dlb &= ~(ULMK_ARCH_MEM_DLB_CPU1_EN | + ULMK_ARCH_MEM_DLB_CPU2_EN | + ULMK_ARCH_MEM_DLB_CPU3_EN | + (1u << 6)); + + for (spins = 0u; spins < 5000000u; spins++) { + magic = MMIO32(magic_addr); + if (magic == magic_exp) { + g_c29_ready_mask |= bit; + g_c29_smp_diag[0] = 0u; + return; + } + } + + st = MMIO16(ULMK_BOARD_DEVCFG_BASE + SYSCTL_O_RSTSTAT); + cfg = (cpu_id == 1u) ? ULMK_BOARD_SSUCPU2CFG_BASE + : ULMK_BOARD_SSUCPU3CFG_BASE; + word0 = MMIO32(reset_addr); + magic = MMIO32(magic_addr); + /* Pack: hi=pre-release rststat, lo=post */ + g_c29_smp_diag[1] = (g_c29_smp_diag[1] << 16) | st; + g_c29_smp_diag[2] = MMIO32(cfg + SSU_O_CPU_RST_CTRL); + g_c29_smp_diag[3] = MMIO32(cfg + SSU_O_RST_VECT); + g_c29_smp_diag[4] = word0; + g_c29_smp_diag[5] = magic; + } +} + +void ulmk_arch_smp_park(void) +{ + uint32_t cpu = ulmk_arch_cpu_id(); + void (*entry)(void); + + /* + * CPU2/CPU3 must not enter here via an LPA0 function pointer — local + * program RAMs are not interchangeable. Secondary stubs mark ready + * in LPA1/CPA0 and either idle or run a local payload. + */ + if (cpu == 0u || cpu >= (uint32_t)ULMK_ARCH_NUM_CPU) { + for (;;) + __asm__ volatile("IDLE" ::: "memory"); + } + + while (g_c29_smp_gate != SMP_GATE_READY) + ; + while (g_c29_secondary_release[cpu] == 0u) + ; + + entry = g_secondary_entry[cpu]; + if (!entry) { + for (;;) + __asm__ volatile("IDLE" ::: "memory"); + } + entry(); + for (;;) + __asm__ volatile("IDLE" ::: "memory"); +} + +#else /* !SMP */ + +void ulmk_arch_send_ipi(uint32_t cpu_id) +{ + (void)cpu_id; +} + +void ulmk_arch_ipi_clear_self(void) +{ +} + +void ulmk_arch_ipi_note_enter(void) +{ +} + +void ulmk_arch_ipi_pulse_self(void) +{ +} + +void ulmk_arch_secondary_init(void) +{ +} + +void ulmk_arch_secondary_mark_ready(void) +{ +} + +void ulmk_arch_start_secondary(uint32_t cpu_id, void (*entry)(void)) +{ + (void)cpu_id; + (void)entry; +} + +void ulmk_arch_smp_mark_ready(void) +{ +} + +uint32_t ulmk_arch_smp_ready_mask(void) +{ + return 0u; +} + +void ulmk_arch_smp_wait_ready(uint32_t mask) +{ + (void)mask; +} + +void ulmk_arch_smp_park(void) +{ + for (;;) + __asm__ volatile("IDLE" ::: "memory"); +} + +#endif /* ULMK_CONFIG_ENABLE_SMP */ diff --git a/arch/c29/startup.S b/arch/c29/startup.S new file mode 100644 index 0000000..3ffbfd0 --- /dev/null +++ b/arch/c29/startup.S @@ -0,0 +1,37 @@ +/* SPDX-License-Identifier: MIT */ +/* + * C29 startup — CPU1 prologue only, then ulmk_kern_start. + * Pattern follows TI device_support codestartbranch.asm (not copied). + * + * Stack grows upward: A15 starts at _ulmk_kernel_stack_base. + */ + + .global code_start + .global _start + .global main + .global reset_vector + .extern ulmk_kern_start + .extern _ulmk_kernel_stack_base + + .section codestart, "ax" +code_start: +_start: +main: + ENTRY1.PROT + || ENTRY2.PROT + MV D0, 0x68 + ST.W0 @0x30208C52, D0 + /* RTS _c_int00 does this before any C; required under APR after POR. */ + MV D0, #0xffffffff + ST.32 @0x3008000C, D0 + MV A15, #_ulmk_kernel_stack_base + LB @ulmk_kern_start + + .section resetvector, "ax" +reset_vector: + ISR1.PROT + || ISR2.PROT + MV D0, 0x68 + ST.W0 @0x30208C52, D0 + MV A15, #_ulmk_kernel_stack_base + LB @ulmk_kern_start diff --git a/arch/c29/startup_cpu2.S b/arch/c29/startup_cpu2.S new file mode 100644 index 0000000..d97592a --- /dev/null +++ b/arch/c29/startup_cpu2.S @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: MIT */ +/* + * Secondary CPU2 reset stub — LPA1 @ 0x20108000. + * Minimal: ISR1.PROT then store handshake into SHARED_RAM / LPA1. + */ + + .global ulmk_c29_cpu2_start + + .section .cpu2_codestart, "ax" +ulmk_c29_cpu2_start: + ISR1.PROT + || ISR2.PROT + MV D1, #0xC0DE0002 + ST.32 @0x200F8000, D1 + ST.32 @0x20108FF0, D1 +1: + IDLE + LB 1b + + .section .cpu2_nmivector, "ax" + ISR1.PROT + || ISR2.PROT +2: + IDLE + LB 2b diff --git a/arch/c29/startup_cpu3.S b/arch/c29/startup_cpu3.S new file mode 100644 index 0000000..603e07d --- /dev/null +++ b/arch/c29/startup_cpu3.S @@ -0,0 +1,24 @@ +/* SPDX-License-Identifier: MIT */ +/* + * Secondary CPU3 reset stub — CPA0 @ 0x20110000. + */ + + .global ulmk_c29_cpu3_start + + .section .cpu3_codestart, "ax" +ulmk_c29_cpu3_start: + ISR1.PROT + || ISR2.PROT + MV D1, #0xC0DE0003 + ST.32 @0x200F8004, D1 + ST.32 @0x20110FF0, D1 +1: + IDLE + LB 1b + + .section .cpu3_nmivector, "ax" + ISR1.PROT + || ISR2.PROT +2: + IDLE + LB 2b diff --git a/arch/c29/trap.S b/arch/c29/trap.S new file mode 100644 index 0000000..30bbf64 --- /dev/null +++ b/arch/c29/trap.S @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: MIT */ +/* + * C29 NMI / fault entry — SSU violations arrive via ESM → NMI. + * Attempt recoverable kill when a user thread is current; otherwise panic. + */ + + .global ulmk_arch_nmi_handler + .global ulmk_arch_fault_handler + .extern ulmk_c29_nmi_dispatch + .extern ulmk_kern_trap_panic + + .section .text.ulmk_arch_nmi_handler, "ax" +ulmk_arch_nmi_handler: + ISR1.PROT + || ISR2.PROT + CALL @ulmk_c29_nmi_dispatch +.Lhalt_nmi: + IDLE + LB @.Lhalt_nmi + + .section .text.ulmk_arch_fault_handler, "ax" +ulmk_arch_fault_handler: + CALL @ulmk_kern_trap_panic +.Lhalt_fault: + IDLE + LB @.Lhalt_fault diff --git a/arch/riscv/include/ulmk_arch.h b/arch/riscv/include/ulmk_arch.h index 00cf26b..55578af 100644 --- a/arch/riscv/include/ulmk_arch.h +++ b/arch/riscv/include/ulmk_arch.h @@ -62,6 +62,8 @@ void ulmk_arch_secondary_init(void); void ulmk_arch_secondary_mark_ready(void); void ulmk_arch_start_secondary(uint32_t cpu_id, void (*entry)(void)); void ulmk_arch_smp_mark_ready(void); +uint32_t ulmk_arch_smp_ready_mask(void); +void ulmk_arch_smp_wait_ready(uint32_t mask); void ulmk_arch_smp_park(void); void ulmk_arch_cycle_enable(void); diff --git a/arch/riscv/smp.c b/arch/riscv/smp.c index 4323a60..568c7e9 100644 --- a/arch/riscv/smp.c +++ b/arch/riscv/smp.c @@ -47,6 +47,17 @@ void ulmk_arch_smp_mark_ready(void) g_smp_gate = SMP_GATE_READY; } +uint32_t ulmk_arch_smp_ready_mask(void) +{ + /* RISC-V secondary mark uses online bits in percpu; expose gate only. */ + return (g_smp_gate == SMP_GATE_READY) ? 0x1u : 0u; +} + +void ulmk_arch_smp_wait_ready(uint32_t mask) +{ + (void)mask; +} + uint32_t ulmk_arch_cpu_id(void) { uint32_t id; diff --git a/arch/tricore/include/ulmk_arch.h b/arch/tricore/include/ulmk_arch.h index c415e78..e95797d 100644 --- a/arch/tricore/include/ulmk_arch.h +++ b/arch/tricore/include/ulmk_arch.h @@ -93,6 +93,8 @@ void ulmk_arch_secondary_mark_ready(void); void ulmk_arch_secondary_init(void); void ulmk_arch_start_secondary(uint32_t cpu_id, void (*entry)(void)); void ulmk_arch_smp_mark_ready(void); +uint32_t ulmk_arch_smp_ready_mask(void); +void ulmk_arch_smp_wait_ready(uint32_t mask); void ulmk_arch_smp_park(void); void ulmk_arch_cycle_enable(void); diff --git a/arch/tricore/smp.c b/arch/tricore/smp.c index a2b002e..e04f860 100644 --- a/arch/tricore/smp.c +++ b/arch/tricore/smp.c @@ -113,6 +113,16 @@ void ulmk_arch_smp_mark_ready(void) g_smp_gate = SMP_GATE_READY; } +uint32_t ulmk_arch_smp_ready_mask(void) +{ + return (g_smp_gate == SMP_GATE_READY) ? 0x1u : 0u; +} + +void ulmk_arch_smp_wait_ready(uint32_t mask) +{ + (void)mask; +} + void ulmk_arch_send_ipi(uint32_t cpu_id) { volatile uint32_t *src; @@ -279,6 +289,8 @@ void ulmk_arch_smp_park(void) #else /* !ENABLE_SMP */ void ulmk_arch_smp_mark_ready(void) {} +uint32_t ulmk_arch_smp_ready_mask(void) { return 0u; } +void ulmk_arch_smp_wait_ready(uint32_t mask) { (void)mask; } void ulmk_arch_send_ipi(uint32_t cpu_id) { (void)cpu_id; } void ulmk_arch_ipi_clear_self(void) {} void ulmk_arch_ipi_note_enter(void) {} diff --git a/cmake/arch.cmake b/cmake/arch.cmake index 7b1c887..339b409 100644 --- a/cmake/arch.cmake +++ b/cmake/arch.cmake @@ -5,7 +5,7 @@ if(NOT DEFINED UL_BOARD_ARCH OR UL_BOARD_ARCH STREQUAL "") endif() set(ULMK_ARCH "${UL_BOARD_ARCH}" CACHE STRING - "Target architecture (tricore|riscv|arm)" FORCE) + "Target architecture (tricore|riscv|arm|c29)" FORCE) get_filename_component(_ULMK_REPO_ROOT "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE) set(ULMK_ARCH_DIR "${_ULMK_REPO_ROOT}/arch/${ULMK_ARCH}") @@ -22,6 +22,8 @@ elseif(ULMK_ARCH STREQUAL "riscv") set(ULMK_DEFAULT_TOOLCHAIN "${_ULMK_REPO_ROOT}/cmake/toolchain-riscv-gcc.cmake") elseif(ULMK_ARCH STREQUAL "arm") set(ULMK_DEFAULT_TOOLCHAIN "${_ULMK_REPO_ROOT}/cmake/toolchain-arm-gcc.cmake") +elseif(ULMK_ARCH STREQUAL "c29") + set(ULMK_DEFAULT_TOOLCHAIN "${_ULMK_REPO_ROOT}/cmake/toolchain-c29-ticlang.cmake") else() message(FATAL_ERROR "Unsupported ULMK_ARCH=${ULMK_ARCH}") endif() diff --git a/cmake/arch_sources.cmake b/cmake/arch_sources.cmake index 5fad1aa..002dd78 100644 --- a/cmake/arch_sources.cmake +++ b/cmake/arch_sources.cmake @@ -33,6 +33,25 @@ elseif(ULMK_ARCH STREQUAL "arm") set(ULMK_ARCH_EXE_SOURCES ${ULMK_ARCH_DIR}/startup.S ${ULMK_ARCH_DIR}/vectors.S) +elseif(ULMK_ARCH STREQUAL "c29") + set(ULMK_ARCH_KERNEL_SOURCES + ${ULMK_ARCH_DIR}/arch.c + ${ULMK_ARCH_DIR}/irq.c + ${ULMK_ARCH_DIR}/mpu.c + ${ULMK_ARCH_DIR}/smp.c + ${ULMK_ARCH_DIR}/ctx_switch.S) + set(ULMK_ARCH_EXE_SOURCES + ${ULMK_ARCH_DIR}/startup.S + ${ULMK_ARCH_DIR}/trap.S + ${ULMK_ARCH_DIR}/cert_placeholder.c) + # Secondary reset stubs only when SMP — otherwise orphan .cpu2_* sections + # after resetvector punch a cert-BIN hole (objcopy strip leaves stale + # PT_LOADs) and flash POR goes silent. + if("${ULMK_CONFIG_ENABLE_SMP}" STREQUAL "1") + list(APPEND ULMK_ARCH_EXE_SOURCES + ${ULMK_ARCH_DIR}/startup_cpu2.S + ${ULMK_ARCH_DIR}/startup_cpu3.S) + endif() else() message(FATAL_ERROR "Unsupported ULMK_ARCH=${ULMK_ARCH}") endif() diff --git a/cmake/config.cmake b/cmake/config.cmake index 4881043..40d8cd0 100644 --- a/cmake/config.cmake +++ b/cmake/config.cmake @@ -20,4 +20,13 @@ if("${ULMK_CONFIG_ENABLE_SMP}" STREQUAL "1") message(FATAL_ERROR "ULMK_CONFIG_ENABLE_SMP=1 is not supported on ARM Cortex-M") endif() + if("${ULMK_ARCH}" STREQUAL "c29") + if(NOT DEFINED ULMK_BOARD_C29_DLB_WORKAROUND OR + NOT "${ULMK_BOARD_C29_DLB_WORKAROUND}" STREQUAL "1") + message(FATAL_ERROR + "ULMK_CONFIG_ENABLE_SMP=1 on C29 requires board to declare " + "ULMK_BOARD_C29_DLB_WORKAROUND=1 in board.cmake " + "(errata SPRZ569E: shared-RAM stale reads under SMP)") + endif() + endif() endif() diff --git a/cmake/generate_ld.py b/cmake/generate_ld.py index b3f0185..0d59b49 100644 --- a/cmake/generate_ld.py +++ b/cmake/generate_ld.py @@ -37,6 +37,185 @@ import re import sys +# --------------------------------------------------------------------------- +# TI C29 .cmd renderer +# --------------------------------------------------------------------------- + +_TI_C29_PROLOGUE = """\ +/* Generated by cmake/generate_ld.py — TI C29 linker command file. + * Do not edit manually; changes are overwritten at configure time. + */ +""" + +_TI_SECTION_TPLS = { + "vectors_note": "", + "all_text": """\ + + /* Program: startup vectors already placed; remaining code. + * .rodata/.const stay in KERNEL_RAM. Split LPA0→CPA1 — no '.' + * assignments inside the {} (TI ignores >> when present). + * LPA1 is CPU2-only; CPA0 is CPU3-only. */ + .text : { + *(.text) + *(.text.*) + } >> KERNEL_FLASH | CPU1_CPA1, RUN_START(_ulmk_kernel_text_start), RUN_END(_ulmk_kernel_text_end) +""", + "data": """\ + + /* RAM-load profile: LMA == VMA (debugger loads .data into RAM). */ + .data : { + _ulmk_kernel_data_start = .; + __ulmk_kernel_data_load = .; + _ulmk_user_data_start = .; + __ulmk_user_data_load = .; + *(.data) + *(.data.*) + *(.rodata) + *(.rodata.*) + *(.const) + *(.const.*) + _ulmk_kernel_data_end = .; + _ulmk_user_data_end = .; + } > KERNEL_RAM +""", + "all_text_flash": """\ + + /* FLASH profile: code + rodata execute-in-place from flash bank RP0. */ + .text : { + _ulmk_kernel_text_start = .; + _ulmk_user_text_start = .; + *(.text) + *(.text.*) + *(.rodata) + *(.rodata.*) + *(.const) + *(.const.*) + _ulmk_kernel_text_end = .; + _ulmk_user_text_end = .; + } > KERNEL_FLASH, palign(8) +""", + "data_flash": """\ + + /* FLASH profile: .data RUNs in RAM, LMA in flash; init.c relocates it. + * LOAD_START(sym) is a TI section-specifier operator that DEFINES sym as + * the load (flash) address; init.c copies from there into the run VMA. + * User data gets its own (empty for kernel-only) section so it has its + * own LOAD_START symbol without a cross-section assignment. */ + .data : palign(8), load = KERNEL_FLASH, run = KERNEL_RAM, LOAD_START(__ulmk_kernel_data_load) { + _ulmk_kernel_data_start = .; + *(.data) + *(.data.*) + _ulmk_kernel_data_end = .; + } + .ulmk_user_data : palign(8), load = KERNEL_FLASH, run = KERNEL_RAM, LOAD_START(__ulmk_user_data_load) { + _ulmk_user_data_start = .; + *(.ulmk_user_data) + *(.ulmk_user_data.*) + _ulmk_user_data_end = .; + } +""", + "bss": """\ + + /* .user_bss is collected here so it always has an explicit placement in + * data RAM: left unplaced, the TI linker drops it into the first free + * range, which in the flash profile is the boot certificate area. */ + .bss : { + _ulmk_kernel_bss_start = .; + *(.bss) + *(.bss.*) + *(COMMON) + *(.user_bss) + *(.user_bss.*) + _ulmk_kernel_bss_end = .; + } > KERNEL_RAM, type = NOLOAD, palign(8) +""", + "stacks": """\ + + .ulmk_kernel_stacks : { + _ulmk_kernel_stack_base = .; + . += ULMK_KERNEL_STACK_SIZE; + _ulmk_kernel_stack_top = .; + _ulmk_isr_stack_base = .; + . += ULMK_ISR_STACK_SIZE; + _ulmk_isr_stack_top = .; + } > KERNEL_RAM, type = NOLOAD, palign(8) +""", + "user_pool": """\ + + .ulmk_user_pool : { + _ulmk_user_pool_start = .; + . += ULMK_USER_POOL_SIZE; + _ulmk_user_pool_end = .; + } > KERNEL_RAM, type = NOLOAD +""", +} + + +def _render_ti_cmd(args: argparse.Namespace, chip_memory: str) -> str: + """Render a TI C29 .cmd linker file from chip memory.cmd and templates.""" + sections = [] + flash = getattr(args, "profile", "ram") == "flash" + + vectors_name = "vectors_flash.cmd.in" if flash else "vectors.cmd.in" + vectors_path = os.path.join(args.arch_dir, vectors_name) + if not flash and not os.path.exists(vectors_path): + vectors_path = os.path.join(args.arch_dir, "vectors.cmd.in") + if os.path.exists(vectors_path): + sections.append("\n" + read_fragment(vectors_path)) + + sections.extend([ + _TI_SECTION_TPLS["all_text_flash" if flash else "all_text"], + _TI_SECTION_TPLS["data_flash" if flash else "data"], + _TI_SECTION_TPLS["bss"], + _TI_SECTION_TPLS["stacks"], + ]) + + # Component archives are linked on the command line; their .text/.data + # land in the wildcard collectors above. Named MPU ranges come later. + + for entry in args.domain: + parts = entry.split(":", 1) + dname = parts[0] + dregion = parts[1] if len(parts) > 1 else "KERNEL_RAM" + sections.append(f""" + .domain_{dname} : {{ + _ulmk_domain_{dname}_start = .; + *(.data.domain_{dname}*) + *(.bss.domain_{dname}*) + _ulmk_domain_{dname}_end = .; + }} > {dregion}, type = NOLOAD +""") + + sections.append(_TI_SECTION_TPLS["user_pool"]) + + if flash: + # resetvector after .data so SMP stubs can follow without a cert hole. + tail_path = os.path.join(args.arch_dir, "vectors_flash_tail.cmd.in") + if os.path.exists(tail_path): + sections.append("\n" + read_fragment(tail_path)) + if getattr(args, "smp", False): + smp_path = os.path.join(args.arch_dir, "vectors_flash_smp.cmd.in") + if os.path.exists(smp_path): + sections.append("\n" + read_fragment(smp_path)) + seccfg_path = os.path.join(args.arch_dir, "vectors_flash_seccfg.cmd.in") + if os.path.exists(seccfg_path): + sections.append("\n" + read_fragment(seccfg_path)) + + prologue_path = os.path.join(args.arch_dir, "prologue.cmd.in") + if os.path.exists(prologue_path): + prologue = read_fragment(prologue_path) + else: + prologue = _TI_C29_PROLOGUE + + return ( + prologue + + "\n" + + chip_memory + + "\nSECTIONS\n{\n" + + "".join(sections) + + "\n}\n" + ) + def read_fragment(path: str) -> str: with open(path, "r", encoding="utf-8") as f: @@ -67,6 +246,16 @@ def main(): ap.add_argument("--kernel-dir", required=True) ap.add_argument("--snippets", required=True) ap.add_argument("--output", required=True) + ap.add_argument("--dialect", default="gnu", + choices=["gnu", "ti-c29"], + help="Linker dialect: gnu (default) or ti-c29") + ap.add_argument("--core", default="1", + help="Target core index for ti-c29 dialect (1|2|3)") + ap.add_argument("--profile", default="ram", + choices=["ram", "flash"], + help="C29 memory profile: ram (default) or flash") + ap.add_argument("--smp", action="store_true", + help="C29 flash: append secondary stub sections after CPU1") ap.add_argument("--app", action="append", default=[], metavar="NAME") ap.add_argument("--comp", action="append", default=[], @@ -75,6 +264,27 @@ def main(): metavar="NAME:REGION") args = ap.parse_args() + # TI C29 path: different renderer, different input files. + if args.dialect == "ti-c29": + if getattr(args, "profile", "ram") == "flash": + chip_memory_path = os.path.join(args.chip_dir, "memory_flash.cmd") + if not os.path.exists(chip_memory_path): + raise SystemExit( + f"flash profile requested but {chip_memory_path} missing") + else: + chip_memory_path = os.path.join(args.chip_dir, "memory.cmd") + if not os.path.exists(chip_memory_path): + # Fall back to memory.ld name if board uses it. + chip_memory_path = os.path.join(args.chip_dir, "memory.ld") + chip_memory = read_fragment(chip_memory_path) + result = _render_ti_cmd(args, chip_memory) + os.makedirs(os.path.dirname(args.output), exist_ok=True) + with open(args.output, "w", encoding="utf-8") as f: + f.write(result) + print(f"[generate_ld.py] wrote {args.output} (dialect=ti-c29 core={args.core})", + file=sys.stderr) + return + out = [] # 1. Arch prologue diff --git a/cmake/linker_api.cmake b/cmake/linker_api.cmake index 99adffb..11e8332 100644 --- a/cmake/linker_api.cmake +++ b/cmake/linker_api.cmake @@ -84,6 +84,25 @@ function(_ulmk_finalize_build kernel_target chip_dir) list(APPEND domain_args "--domain" "${dname}:${dregion}") endforeach() + if("${ULMK_LINKER_DIALECT}" STREQUAL "ti-c29") + set(_ld_dialect_args "--dialect" "ti-c29") + if(ULMK_C29_FLASH) + list(APPEND _ld_dialect_args "--profile" "flash") + if("${ULMK_CONFIG_ENABLE_SMP}" STREQUAL "1") + list(APPEND _ld_dialect_args "--smp") + endif() + set(_ld_memory_dep "${chip_dir}/memory_flash.cmd") + # memory.cmd is the TI board input; fall back to memory.ld if absent. + elseif(EXISTS "${chip_dir}/memory.cmd") + set(_ld_memory_dep "${chip_dir}/memory.cmd") + else() + set(_ld_memory_dep "${chip_dir}/memory.ld") + endif() + else() + set(_ld_dialect_args "") + set(_ld_memory_dep "${chip_dir}/memory.ld") + endif() + add_custom_command( OUTPUT "${generated_ld}" COMMAND "${Python3_EXECUTABLE}" @@ -93,21 +112,39 @@ function(_ulmk_finalize_build kernel_target chip_dir) "--kernel-dir" "${CMAKE_SOURCE_DIR}/linker/kernel" "--snippets" "${CMAKE_SOURCE_DIR}/linker/snippets" "--output" "${generated_ld}" + ${_ld_dialect_args} ${app_args} ${comp_args} ${domain_args} DEPENDS - "${chip_dir}/memory.ld" + "${_ld_memory_dep}" COMMENT "Generating linker script ${generated_ld}" ) add_custom_target(ulmk_linker_script DEPENDS "${generated_ld}") - target_link_options("${kernel_target}" PRIVATE - "-T${generated_ld}" - "-nostartfiles" - "-Wl,--gc-sections" - "-Wl,-Map=${CMAKE_BINARY_DIR}/ulmk.map") + if("${ULMK_LINKER_DIALECT}" STREQUAL "ti-c29") + # Freestanding: no TI ROM autoinit / RTS. Pull only compiler-rt + # builtins (e.g. __udivsi3) from the CGT package. + set(_c29_builtins + "${TI_C29_CGT_ROOT}/lib/c29.c0-ti-none-eabi/libclang_rt.builtins.a") + if(NOT EXISTS "${_c29_builtins}") + set(_c29_builtins + "${TI_C29_CGT_ROOT}/lib/libclang_rt.builtins.a") + endif() + target_link_options("${kernel_target}" PRIVATE + "LINKER:--map_file=${CMAKE_BINARY_DIR}/ulmk.map" + "LINKER:--reread_libs" + "LINKER:--disable_auto_rts" + "${generated_ld}" + "${_c29_builtins}") + else() + target_link_options("${kernel_target}" PRIVATE + "-T${generated_ld}" + "-nostartfiles" + "-Wl,--gc-sections" + "-Wl,-Map=${CMAKE_BINARY_DIR}/ulmk.map") + endif() add_dependencies("${kernel_target}" ulmk_linker_script) diff --git a/cmake/optimize.cmake b/cmake/optimize.cmake index 8e1f9d7..bb38a09 100644 --- a/cmake/optimize.cmake +++ b/cmake/optimize.cmake @@ -18,6 +18,11 @@ option(ULMK_OPTIMIZE_SIZE if(ULMK_OPTIMIZE_SIZE) set(ULMK_KERNEL_OPT_FLAGS -Os) message(STATUS "Kernel/arch optimisation: -Os (size)") +elseif("${ULMK_COMPILER_FAMILY}" STREQUAL "ticlang") + # TIClang: -Ofast not supported; -fno-inline has no effect on inlining + # across protected-call boundaries (C29 has no CSA-style UL bit hazard). + set(ULMK_KERNEL_OPT_FLAGS -O3) + message(STATUS "Kernel/arch optimisation: -O3 (TIClang)") else() set(ULMK_KERNEL_OPT_FLAGS -Ofast -fno-inline) message(STATUS "Kernel/arch optimisation: -Ofast -fno-inline (speed)") diff --git a/cmake/toolchain-c29-ticlang.cmake b/cmake/toolchain-c29-ticlang.cmake new file mode 100644 index 0000000..d4432d3 --- /dev/null +++ b/cmake/toolchain-c29-ticlang.cmake @@ -0,0 +1,110 @@ +# cmake/toolchain-c29-ticlang.cmake +# TI C29 Clang (TIClang) freestanding toolchain for ULMK. +# +# Discovery (first match wins): +# TI_C29_CGT_ROOT / TI_CCS_ROOT / TI_INSTALL_ROOT +# Container mount convention: /ti (see tools/dev.py) +# +# Native CMake TIClang ID needs CMake >= 3.29. This file works with the +# project's 3.20+ minimum by forcing STATIC_LIBRARY try_compile and an +# explicit Clang ID. + +include("${CMAKE_CURRENT_LIST_DIR}/board_resolve.cmake") + +if(CMAKE_VERSION VERSION_LESS "3.20") + message(FATAL_ERROR "C29 port requires CMake >= 3.20") +endif() + +set(CMAKE_SYSTEM_NAME Generic) +set(CMAKE_SYSTEM_PROCESSOR c29) + +# --------------------------------------------------------------------------- +# Locate C29 Clang package +# --------------------------------------------------------------------------- + +function(_ulmk_c29_find_cgt out_var) + set(_candidates "") + if(DEFINED ENV{TI_C29_CGT_ROOT} AND NOT "$ENV{TI_C29_CGT_ROOT}" STREQUAL "") + list(APPEND _candidates "$ENV{TI_C29_CGT_ROOT}") + endif() + if(DEFINED TI_C29_CGT_ROOT AND NOT "${TI_C29_CGT_ROOT}" STREQUAL "") + list(APPEND _candidates "${TI_C29_CGT_ROOT}") + endif() + if(DEFINED ENV{TI_CCS_ROOT} AND NOT "$ENV{TI_CCS_ROOT}" STREQUAL "") + file(GLOB _ccs_cgts "$ENV{TI_CCS_ROOT}/tools/compiler/ti-cgt-c29*") + list(APPEND _candidates ${_ccs_cgts}) + endif() + if(DEFINED TI_CCS_ROOT AND NOT "${TI_CCS_ROOT}" STREQUAL "") + file(GLOB _ccs_cgts "${TI_CCS_ROOT}/tools/compiler/ti-cgt-c29*") + list(APPEND _candidates ${_ccs_cgts}) + endif() + if(DEFINED ENV{TI_INSTALL_ROOT} AND NOT "$ENV{TI_INSTALL_ROOT}" STREQUAL "") + file(GLOB _inst_cgts + "$ENV{TI_INSTALL_ROOT}/ccs*/ccs/tools/compiler/ti-cgt-c29*" + "$ENV{TI_INSTALL_ROOT}/tools/compiler/ti-cgt-c29*") + list(APPEND _candidates ${_inst_cgts}) + endif() + if(EXISTS "/ti") + file(GLOB _ti_cgts + "/ti/ccs*/ccs/tools/compiler/ti-cgt-c29*" + "/ti/tools/compiler/ti-cgt-c29*") + list(APPEND _candidates ${_ti_cgts}) + endif() + + list(REMOVE_DUPLICATES _candidates) + foreach(_root IN LISTS _candidates) + if(EXISTS "${_root}/bin/c29clang") + set(${out_var} "${_root}" PARENT_SCOPE) + return() + endif() + endforeach() + set(${out_var} "" PARENT_SCOPE) +endfunction() + +_ulmk_c29_find_cgt(_ULMK_C29_CGT) +if("${_ULMK_C29_CGT}" STREQUAL "") + message(FATAL_ERROR + "C29 Clang not found. Set TI_C29_CGT_ROOT or TI_CCS_ROOT " + "(or mount the TI install at /ti).") +endif() + +set(TI_C29_CGT_ROOT "${_ULMK_C29_CGT}" CACHE PATH "TI C29 Clang package root" FORCE) +set(_ULMK_C29_BIN "${TI_C29_CGT_ROOT}/bin") + +set(CMAKE_C_COMPILER "${_ULMK_C29_BIN}/c29clang" CACHE FILEPATH "" FORCE) +set(CMAKE_ASM_COMPILER "${_ULMK_C29_BIN}/c29clang" CACHE FILEPATH "" FORCE) +set(CMAKE_AR "${_ULMK_C29_BIN}/c29ar" CACHE FILEPATH "" FORCE) +set(CMAKE_RANLIB "${_ULMK_C29_BIN}/c29ar" CACHE FILEPATH "" FORCE) +# LLVM-style ar: ranlib is `c29ar s `, not bare `c29ar `. +set(CMAKE_C_ARCHIVE_CREATE " qc ") +set(CMAKE_C_ARCHIVE_FINISH " s ") +set(CMAKE_CXX_ARCHIVE_CREATE " qc ") +set(CMAKE_CXX_ARCHIVE_FINISH " s ") +find_program(CMAKE_OBJCOPY NAMES c29objcopy PATHS "${_ULMK_C29_BIN}" NO_DEFAULT_PATH REQUIRED) +find_program(CMAKE_OBJDUMP NAMES c29objdump PATHS "${_ULMK_C29_BIN}" NO_DEFAULT_PATH REQUIRED) +find_program(CMAKE_SIZE NAMES c29size PATHS "${_ULMK_C29_BIN}" NO_DEFAULT_PATH REQUIRED) +find_program(CMAKE_NM NAMES c29nm PATHS "${_ULMK_C29_BIN}" NO_DEFAULT_PATH) +find_program(CMAKE_READELF NAMES c29readelf PATHS "${_ULMK_C29_BIN}" NO_DEFAULT_PATH) + +set(CMAKE_C_COMPILER_ID "Clang" CACHE STRING "" FORCE) +set(CMAKE_C_COMPILER_VERSION "21.0.0" CACHE STRING "" FORCE) +set(CMAKE_ASM_COMPILER_ID "Clang" CACHE STRING "" FORCE) +set(CMAKE_C_COMPILER_FORCED TRUE) +set(CMAKE_ASM_COMPILER_FORCED TRUE) +set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) + +set(CMAKE_C_FLAGS_INIT + "-mcpu=c29.c0 -mfpu=f32 -ffunction-sections -fdata-sections -ffreestanding") +set(CMAKE_ASM_FLAGS_INIT + "-mcpu=c29.c0 -mfpu=f32 -x assembler-with-cpp") +# -nostartfiles is ignored by this driver; freestanding uses -nostdlib. +set(CMAKE_EXE_LINKER_FLAGS_INIT "-nostdlib") + +set(ULMK_COMPILER_FAMILY "ticlang" CACHE STRING "" FORCE) +set(ULMK_LINKER_DIALECT "ti-c29" CACHE STRING "" FORCE) + +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) + +message(STATUS "C29 toolchain: ${TI_C29_CGT_ROOT}") diff --git a/docs/arch_c29_notes.md b/docs/arch_c29_notes.md new file mode 100644 index 0000000..b92f60b --- /dev/null +++ b/docs/arch_c29_notes.md @@ -0,0 +1,33 @@ +# C29 architecture notes (ULMK) + +See `docs/c29f_port_plan.md` for the full design and +`docs/gates/c29f_gate_evidence.md` for silicon results. + +## Quick facts + +- ILP32 / ELF32, stack grows **up** (`ULMK_ARCH_STACK_GROWS_UP`). +- Context frame: 264 bytes (A14/RPC/DSTS/ESTS + XA/XD/XM). +- Switches only at INT/syscall exit via deferred `RETI.INT` staging. +- PIPE vectors at `PIPE_BASE + 0x5000 + n*4`; enable `INT_CTL_L`, force/clear `INT_CTL_H`. +- Must `MEM_INIT` PIPE before programming vectors; INTSP = Stack2. +- Secondary stubs (RAM): CPU2 LPA1 `0x20108000`, CPU3 CPA0 `0x20110000` + (CPU1 may spill into CPA1 `0x20118000`; CPU2 cannot fetch CPA). +- Secondary reset stubs must start with `ISR1.PROT||ISR2.PROT`; handshake + words in SHARED_RAM (`0x200F8000`/`04`). Point `DEF_NMI_VECT` at the same + address as `RST_VECT` during bring-up (GEL style) — a distinct `+0x40` NMI + veneer hits `NMI_ISR_ENTRY_ERR` before the store. +- Flash SMP (BANKMODE0): stubs linked after CPU1 `.text`/`.data` as + `.cpu2_stub`/`.cpu3_stub` (only when `ULMK_CONFIG_ENABLE_SMP=1`); CPU1 + copies into LPA1/CPA0 before release. `package-seccfg` keeps stubs out of + the cert BIN and programs them only with `ULMK_C29_FLASH_SECONDARY=1`. + HIL: open UART *before* DSS reset; `dss-preflash-halt.js` before DSLite + (idle XIP → `wr_pll.alg` timeout otherwise). +- Clear `MEM_DLB_CONFIG` CPU1/2/3 bits before shared RAM / SMP (SPRZ569E). + +## Open follow-ups + +- SSUMODE2 NonMain flash (`ULMK_C29_SECCFG_COMMIT=1` + `hil-ssu-mode2.sh`) — + host CRC/checker OK; silicon commit still pending (brick risk). +- Full recoverable fault continue-after-kill on NMI (Gate D). +- Full `C29SMP_PASS` (IPI/sleep after ready) when tick path is complete on flash. +- Physical XRSn POR (nSRST via `xds110reset` does not reboot C29 on LAUNCHXL). diff --git a/docs/c29f_port_plan.md b/docs/c29f_port_plan.md new file mode 100644 index 0000000..3dbff12 --- /dev/null +++ b/docs/c29f_port_plan.md @@ -0,0 +1,1163 @@ +# C29x/F29H85x Architecture Port Plan + +**Status:** implementation in progress on `plan/c29f-port` +**Planning branch:** `plan/c29f-port` in `ulmk`, `ulmk_boards`, and +`ulmk_apps` +**Reference target:** TI LAUNCHXL-F29H85X with F29H850TU9 +**Validation probe:** XDS110 serial `CL850001` +**Gate evidence:** `docs/gates/c29f_gate_evidence.md` +**BSP:** `ulmk_boards/launchxl_f29h85x/README.md` + +## 1. Goal + +Add a production-quality `c29` architecture port and a minimal +`launchxl_f29h85x` BSP, including three-core SMP, SSU isolation, PIPE +interrupts, CPU timers, IPC reschedule IPIs, and command-line silicon +validation through Code Composer Studio. + +The finished build must retain the ulmk single-artifact model. Internal +per-core images are allowed during the build, but the shipped result must be +one CPU1-loadable `ulmk.out` containing the CPU2 and CPU3 payloads. + +The C29 CPU has 64-bit execution resources and can issue up to eight +instructions in parallel, but the C EABI is ILP32: pointers, `int`, `long`, +`size_t`, and `ptrdiff_t` are 32 bits. The port must therefore keep the +kernel's existing 32-bit address and syscall ABI assumptions. + +## 2. Non-negotiable kernel contracts + +- Context switches occur only at low-priority interrupt exit or syscall + interrupt exit through `ulmk_kern_sched_dispatch()`. +- Idle only waits for an interrupt. It must not poll IPC state, drain a + mailbox, or schedule. +- A remote wake must generate a real interrupt on the destination CPU. +- Kernel code depends on C29 only through `ulmk_arch.h`; C29 code must not + include kernel-internal headers. +- All thread state needed after an interrupt must live in the thread's saved + frame. A shared kernel interrupt stack may not retain a blocked thread's + continuation. +- User and driver code must not gain kernel memory, PIPE configuration, SSU + configuration, or unrestricted peripheral access by branching into a + privileged section. +- SMP atomics must be implemented by verified hardware exclusion. Falling + back to interrupt masking or compiler runtime locks is not acceptable. +- The C29 build must remain freestanding and must not acquire a dependency on + the TI C runtime startup sequence, heap, constructors, or `main()`. + +## 3. Authoritative references + +References must be rechecked when implementation starts because the device +documentation and SDK are still changing. + +- C29x CPU Reference Guide, `SPRUIY2A`, revision A. +- F29H85x and F29P58x Technical Reference Manual, `SPRUJ79`, revision B, + published July 2026. +- F29H85x/F29P58x/F29P32x datasheet, revision D, published May 2026. +- F29H85x/F29P58x/F29P32x silicon errata, `SPRZ569`, revision E, published + July 2026. +- LAUNCHXL-F29H85X user's guide, `SPRUJE5B`. +- Implementing Run-Time Safety and Security With the C29x SSU, + `SPRADK2A`. +- TI C29x Clang Compiler Tools User's Guide, especially the EABI, + intrinsics, assembler, and linker chapters. +- F29H85x SDK System Security Configuration and multicore examples. +- Local TI FreeRTOS C29 port, used as an ABI and context-frame reference, + not copied into ulmk: + `source/kernel/freertos/Source/portable/CCS/C2000_C29x`. + +The implementation must audit the connected silicon revision against +`SPRZ569E` before clocks, reset behavior, flash prefetch, SSU, PIPE, or fault +recovery are finalized. + +The public CPU guide does not replace the complete C29 instruction-set +material referenced by the compiler documentation. Obtain the complete +material through TI before production assembly is approved, particularly for +protected return, NMI/fault return, atomic windows, and status-register side +effects. + +## 4. Facts already verified + +### 4.1 Local tools + +- CCS: `/home/ulipe/ti/ccs2040`. +- Compiler: TI C29 Clang `2.0.0.STS`. +- Compiler target: `c29-ti-none-eabi`. +- Linker: `c29lnk` `2.0.0.STS`; assembly is integrated into `c29clang` + (`-x assembler-with-cpp`), with no separate `c29as`. +- Installed inspection/conversion tools include `c29readelf`, `c29objdump`, + `c29nm`, `c29size`, `c29ofd`, `c29objcopy`, and `c29strip`. +- Local SDK: F29H85x SDK `1.02.01.00`. +- SysConfig: `1.26.0+4407`. +- Host CMake: `3.22.5`; native TIClang support requires CMake 3.29 or newer. +- Headless tools are present: + - `ccs_base/scripting/bin/dss.sh` + - `ccs_base/scripting/examples/loadti/loadti.sh` + - `ccs_base/common/uscif/xds110/xdsdfu` + - `ccs_base/common/uscif/xds110/xds110reset` + +The current F29 SDK release, `26.00.00` STS, is newer than the local +installation and uses CCS 20.5, C29 Clang 2.2 LTS, and SysConfig 1.27. The +implementation must support configurable installation roots and must test at +least the local toolchain plus the current LTS compiler before release. + +The compiler driver enables function/data sections and links TI libc, libc++, +libsys, and compiler-rt by default. `-nostartfiles` is ignored by the installed +driver; the freestanding backend must use a verified `-nostdlib` or +`-nodefaultlibs` flow and explicitly add only required compiler helpers. + +### 4.2 ABI + +Compiler predefined macros confirmed: + +- `__TI_EABI__ == 1` +- `__SIZEOF_POINTER__ == 4` +- `__SIZEOF_INT__ == 4` +- `__SIZEOF_LONG__ == 4` +- `__SIZEOF_SIZE_T__ == 4` +- little-endian ELF output +- 8-byte stack alignment + +The compiler can use M registers even in code that contains no explicit +floating-point operation. Every schedulable context must therefore preserve +all A, D, and M working registers. + +### 4.3 Board and debugger + +The connected XDS110 was verified in runtime/standard mode: + +- USB VID:PID `0451:bef3` +- firmware `3.0.0.41` +- serial `CL850001` +- application UART: + `/dev/serial/by-id/usb-Texas_Instruments_XDS110__03.00.00.41__Embed_with_CMSIS-DAP_CL850001-if00` +- auxiliary port, not for application logs: + the corresponding `if03` device +- UARTA is routed to the XDS110 by default through GPIO42 TX and GPIO43 RX +- LED4 is GPIO19 and LED5 is GPIO62; both are active-low +- boot switch S3 selects flash with GPIO72/GPIO84 both high + +The SDK LAUNCHXL blinky example was compiled with the installed compiler and +loaded to the board with `loadti.sh` and +`examples/device_support/targetconfigs/F29H850TU9.ccxml`. + +This proves compiler, linker, XDS110 discovery, target connection, program +load, and run control. It does not yet prove ulmk context switching, SSU +isolation, flash boot, or SMP. + +The older SDK makefile expects CCS 20.3 and SysConfig 1.25. The local +installation required explicit `CCS_PATH` and `SYSCFG_ROOT` overrides. +Its dummy-certificate post-build utility is not executable in the current +installation. RAM loading is unaffected; the production flash pipeline must +fix this without mutating a user's SDK installation. + +`SPRZ569E` contains a mandatory SMP workaround: the RAM Data Line Buffer is +enabled by default and can return stale data when CPUs concurrently access a +shared RAM location. `MEM_DLB_CONFIG` is global and has enable bits for the +CPU1, CPU2, and CPU3 initiators. CPU1 must clear all three bits before shared +RAM is reinitialized, tested, or first used as shared state by ulmk, and +before secondaries are released. Boot ROM activity before CPU1 entry is +outside this software ordering claim. + +## 5. Proposed architecture + +The following choices are provisional until the feasibility gates in +Section 6 pass. + +### 5.1 Execution and context model + +Use Supervisor low-priority `INT` for every kernel-visible scheduling event: + +- syscall doorbell +- scheduler tick +- device IRQ delivery +- IPC reschedule IPI +- voluntary yield and thread exit + +Reserve `RTINT` and NMI for bounded non-scheduling work, fault capture, or +panic paths. Their protected hardware stacks are not used to retain thread +continuations. + +C29 `INT` is not a privilege transition: the CPU accepts it only when +`CURRSP == INTSP`, and its vector must execute in that same SSU Stack. The +candidate layout therefore uses: + +- one scheduler SSU Stack and one common unprivileged runtime Link for all + schedulable user/driver code and the INT veneer +- each CPU's local Link 2 as the kernel Link in a separate protected Stack +- dynamic APRs to isolate the currently running thread's data, stack, grants, + and driver MMIO + +The first port does not assign stronger static authority to a driver Link: +explicit branches between Links in one Stack can change the active Link. +Driver privilege comes from capabilities plus dynamic APRs installed only for +the current thread. Gate D must reject any design where branching into another +component's code acquires static MMIO or memory authority. + +The INT veneer has no kernel APR access. Sharing its Link and Stack with user code +means user code can branch to it, so every privileged entry validates that an +INT is active and that the frame/channel is a legitimate kernel source. + +On `INT` entry: + +1. The veneer saves a complete thread frame on the interrupted thread stack. +2. It records the resulting A15 in `ulmk_arch_ctx_t`. +3. It invokes the exact protected kernel dispatch entry with `CALL.PROT`. +4. Protected entry switches to the per-CPU kernel Stack/A15, validates the + saved frame and source, and runs the C dispatcher. +5. `ulmk_kern_sched_dispatch()` only stages the selected context. +6. Kernel C returns through `RET.PROT`; the mandatory first caller packet is + `EXIT.PROT`, which restores the scheduler-Stack A15 and completes the + protected return. +7. Only after `EXIT.PROT`, the unprivileged INT epilogue applies the staged + selection, restores that thread's A15 and full frame, then executes + `RETI.INT`. + +No protected-call frame or kernel stack continuation survives a context +switch. Initial launch, blocked syscall, voluntary yield, and IRQ preemption +must all converge on this same epilogue model. + +Dynamic APR handoff is part of the context-switch proof. The old stack is +needed through `EXIT.PROT`, while the selected stack must be writable before +its frame is restored. A candidate two-phase epilogue maps both stacks only +while executing trusted veneer code, switches A15, then uses a validated short +protected finalizer to remove the old mapping before any selected-thread +instruction executes. Gate C/D must prove this window is interrupt-safe and +unreachable as an authority gain from ordinary user flow. If no bounded +handoff can remove the old stack before `RETI.INT`, the isolation design is +blocked. + +The initial implementation saves all restorable thread state on every +kernel-visible interrupt. Based on the TI reference frame, the complete frame +is expected to consume 272 bytes including the hardware INT words. Hardware +transition fields such as `DSTS.INTS`, ISR priority, `CLINK`, and `RLINK` are +validated at each phase rather than compared as invariant thread sentinels. +The exact frame must be confirmed from generated assembly and measured on +silicon. + +The candidate `ulmk_arch_ctx_t` contains only a 32-bit saved stack pointer, +but Gate B must prove that no other resumable state lives outside the saved +frame before this layout is frozen. A synthetic frame starts a new thread and +returns to a trampoline that calls `ulmk_thread_exit()` if the entry function +returns. + +The initial launch is a separate one-way transition from boot Link 2/kernel +Stack into the common runtime Link/scheduler Stack. Gate B must prove the +exact assembly sequence establishes `CURRSP == INTSP`, the intended PSP/A15, +and a synthetic return frame without leaving a protected-call or boot-stack +continuation. + +### 5.2 Protected INT bridge and syscall doorbell + +C29 has no conventional unprivileged syscall instruction. A normal +`CALL.PROT` cannot itself be a blocking syscall continuation because the +protected-call stack is CPU-local and LIFO; arbitrary thread blocking and +wake order would violate that model. + +The candidate syscall ABI is: + +1. Userspace normalizes the syscall number and four 32-bit arguments into + fixed registers and requests one reserved Supervisor software INT. +2. Every PIPE channel remains owned by the local kernel Link 2. Gate C must + prove that the runtime Link's API permission for the doorbell permits only + force—not read, clear, enable, disable, or force of any tick/IPI/device + source. If the hardware API permission is broader, a tiny non-blocking + protected stub validates and forces the doorbell under Link 2, then + completes `RET.PROT`/`EXIT.PROT` before the INT can be serviced. +3. The normal INT veneer saves all arguments before any protected call. +4. The protected kernel entry begins with the required + `ENTRY1.PROT || ENTRY2.PROT` packet, validates `DSTS.INTS`, source identity, + frame bounds/alignment, and ownership, then receives only a saved-frame + pointer through an explicitly preserved argument register. +5. Kernel C writes the return value into the saved result-register slot and + stages any context switch. +6. `RET.PROT` and the caller's mandatory `EXIT.PROT` packet complete before + the INT epilogue selects and resumes a thread. + +Protected calls and returns reset the C29 atomic counter, so an atomic window +cannot be used to bridge a protected return. Exact `PRESERVE`, register-zeroing, +`ENTRY`/`EXIT`, PIPE owner/API Link, and Supervisor-INT behavior must be +confirmed from compiler output and silicon. + +“Supervisor INT” is not a privilege class or arbitrary per-channel mode. The +candidate requires `PRI_LEVEL == INT_RTINT_THRESHOLD` together with +`SUP_IGN_INTE_EN=1`, allowing the event through even when user code clears +`DSTS.INTE`. Tick, syscall, IPC IPI, and any device source required for +scheduling progress use that exact configuration. + +These sources share the threshold priority and may enter while another INT is +active despite ordinary INT masking. Gate C must characterize ties and +nesting, then implement a bounded per-CPU outermost guard: nested Supervisor +arrivals may save/ack/coalesce work but never independently call the +scheduler. Before the outer epilogue applies a staged frame it drains the +coalesced state and reruns or revalidates dispatch, so a nested wake cannot +make the selection stale. Gate C must also determine whether repeated user +`ATOMIC.REG` can indefinitely defer Supervisor INT; if so, the supported +threat model and mitigation must be explicit. + +### 5.3 SSU model + +Use SSU runtime isolation as the C29 MPU backend: + +- Link 2 is the secure-root context of each C29 CPU. CPU1 coordinates boot, + but this does not imply unrestricted global access. +- Each CPU programs only the SSU/APR resources that the device ownership + tables permit. Cross-CPU configuration is performed by the owning core or + by an explicitly documented CPU1 boot path. +- Kernel text, data, PIPE configuration, SSU configuration, and kernel stacks + are kernel-only APRs. +- User/driver executable text and the INT veneer use the common non-kernel + runtime Link in the scheduler Stack selected by `INTSP`. +- Driver peripheral reach is granted by APR permissions, not by exposing + secure-root access. +- One or more uncommitted APR slots are reprogrammed on context switch to + represent the current thread's stack, data, shared mappings, and grants. +- Static linker sections define kernel, the veneer, and only genuinely common + shared-memory APRs. Per-component executable text and driver MMIO are + dynamic current-thread mappings. +- `ulmk_arch_mpu_addr_permitted()` validates against the current TCB region + list; it does not infer permission merely from the current Link. + +All CPUs have their own Link/Stack/AP configuration. The same logical layout +must be installed on CPU1, CPU2, and CPU3 before userspace starts. + +Normal APR placement is 4 KiB granular, so the C29 linker cannot reuse the +64-byte TriCore domain alignment. Budget the 64 APRs per CPU explicitly. +Static Link-to-Stack assignments are fixed at boot; only Link 2 may update +APR address, executable-Link, and permission fields at runtime. Update +dynamic slots only while executing from static kernel code/stack: disable the +slot, program and validate the complete descriptor, then enable it. No +intermediate state may overlap static kernel or PIPE/SSU ranges. + +SSU mode 1 is sufficient only for mechanical CPU/context bring-up; APR +enforcement is disabled there. Gate D requires a minimal CRC-valid `SECCFG` +and reboot into SSU mode 2 after a tested recovery procedure exists. +`COMMIT`, SSU mode 3, debug-zone locking, and irreversible security settings +remain deferred. Link 2 AP override is restricted to controlled boot +configuration and is disabled before the first user thread. + +SSU denial does not guarantee that every peripheral read lacks side effects; +some accesses can reach read-clear/FIFO logic even when the return data is +blocked. The BSP must classify such peripherals, and Gate D must not claim +complete MMIO containment solely from APR read denial. + +### 5.4 PIPE and IRQ mapping + +Treat the F29 PIPE channel number as ulmk's logical `srpn`. + +- The board maps peripheral events to PIPE channel numbers. +- `ulmk_arch_irq_src_configure()` keeps every channel's OWNER_LINK at local + kernel Link 2, applies fixed arch-private API-Link rules, and programs + priority, context, and CPU-local PIPE state. SSU concepts do not leak into + the generic kernel API. +- Userspace never receives access to PIPE configuration registers. +- The runtime Link receives no operational access to tick, IPI, or device + channels. Its syscall-doorbell access is accepted only after Gate C proves + force-only semantics; otherwise the short protected force stub is mandatory. +- Every scheduling source is a Supervisor INT targeting the scheduler Stack + and unprivileged INT-veneer Link. +- Ack ordering is peripheral flag clear first, then PIPE flag clear when the + source requires both. +- The tick and IPC IPI use reserved kernel channels that cannot be bound by + userspace. +- `ulmk_irq_attach` starts disabled for the first protected bring-up. Its + callback trampoline is implemented only after the base SSU and fault paths + are proven. + +The first port disables ordinary INT nesting. Threshold-priority Supervisor +arrivals that bypass `DSTS.INTE` follow the bounded nested path established by +Gate C. If a higher-priority event requires RTINT, it performs bounded +non-scheduling work and requests a Supervisor INT after returning. Only the +outermost INT epilogue may stage and apply a context switch. + +### 5.5 Tick and idle + +Use one CPU timer per active CPU, initially CPU timer 2 as in the TI FreeRTOS +port. Every CPU advances its own ulmk timing wheel. + +The current generic idle thread is kernel-privileged, which would leave C29 on +the kernel Stack and prevent `INT` while `CURRSP != INTSP`. Add +`ULMK_ARCH_IDLE_ENTRY` and `ULMK_ARCH_IDLE_PRIVILEGE` hooks whose default +values preserve the existing ports. C29 selects `ulmk_arch_idle_entry`, a +user-privileged leaf assembly trampoline in the common runtime Link/scheduler +Stack; normal thread initialization then represents its stack as an explicit +dynamic region. The trampoline only enables interrupts and executes the +documented low-power wait instruction if IPC and timer events wake it +reliably; otherwise it uses the architecturally safe idle instruction sequence +proven by Gate B. It never polls an IPC flag or schedules. + +### 5.6 Atomics + +Do not use C11, `__atomic`, or `__sync` operations directly. The installed +compiler advertises zero always-lock-free sizes, and some builtins call +runtime helpers. + +The candidate 32-bit CAS and fetch-add wrapper uses: + +- `__builtin_c29_atomic_mem_enter()` +- one aligned shared-memory read/modify/write sequence +- `__builtin_c29_atomic_leave()` +- compiler `"memory"` barriers around the window + +Before this mechanism is accepted, Gate E records the exact mnemonic, +instruction-packet count, memory operation, and termination sequence emitted +by every supported compiler. TI documentation uses both `ATOMIC.M`-family +terminology and compiler intrinsic names; the plan does not assume a specific +assembler spelling. No `__atomic_*` helper may remain in the image. + +The public CPU guide and current TRM disagree on whether the relevant atomic +counter permits 64 or 256 instruction packets, while the no-argument memory +intrinsic does not expose that bound directly. Treat 64 as the conservative +architectural ceiling when auditing generated windows. If the compiler +sequence cannot be proven to complete within it, use reviewed assembly backed +by the complete ISA material or reject the mechanism. + +Memory-controller exclusion does not by itself define a portable C +acquire/release model across different controllers. Release/acquire semantics +require an explicit TI architectural/compiler guarantee or a more restricted +IPC-based ordering contract. Stress tests are necessary evidence, not a +substitute for that contract. Atomic tests run without debugger halts because +a halt releases the hardware exclusion window; NMI behavior must also be +tested. + +### 5.7 SMP boot and image layout + +HSM/ROM is the initial boot authority. After it releases CPU1, CPU1 +coordinates application memory setup and the secondary C29 cores. + +For an SMP build: + +1. Build internal CPU1, CPU2, and CPU3 images from the same source graph. +2. Place CPU1 code in LPA0 or CPU1 flash. +3. Place CPU2 code in LPA1; CPU2 has no direct flash execution path. +4. Place CPU3 code in CPA0 for RAM tests and its assigned flash region for + flash builds. +5. Link the required CRC-valid CPU1, CPU2, and CPU3 SSU configuration sections + at addresses derived from the selected BANKMODE. +6. Embed CPU2 and CPU3 payload sections in the final CPU1 `ulmk.out`; the + flash profile also carries the CPU1/CPU3 boot certificate and metadata + sections required by the current device lifecycle. +7. CPU1 switches CPU1/CPU2 to split mode at the documented point and waits at + least 24 cycles before using CPU2 independently. +8. CPU1 clears all CPU1/CPU2/CPU3 DLB enable bits before ulmk performs + RAM/ECC reinitialization, tests, or first shared use. +9. CPU1 initializes clocks, RAM/ECC, shared kernel state, and SSU. +10. CPU1 copies payloads when the selected boot mode requires it. +11. CPU1 configures boot and NMI vectors, releases CPU2 and CPU3, and waits + for a shared ready mask. + +Shared kernel data uses a fixed LDA address range visible to all CPUs. CPU1 +initializes shared `.data`/`.bss` exactly once before releasing secondaries. +Per-CPU stacks and private state use separate linker sections. + +The per-core program memories have different physical addresses. No code +pointer created on CPU1 may be sent to another core unchanged. Inventory every +cross-core code pointer, including thread entries, IRQ-attach callbacks, +function tables, and pointers stored in read-only data. + +The preferred model assigns generated IDs to every cross-core-callable entry +and resolves the ID through a core-local table. C29 sets +`ULMK_ARCH_CTX_FABRICATE_ON_AFFINITY_CPU=1`, reusing the kernel's existing +deferred context-fabrication path. `ulmk_arch_ctx_init()` therefore runs on +the destination CPU. Generated source-address tables cover CPU1, CPU2, and +CPU3, mapping a raw entry from whichever CPU created the thread to an ID; a +core-local table maps that ID to the destination address. The arch does not +inspect kernel-internal TCB fields. The same resolver applies to +`ulmk_arch_start_secondary()` and every supported cross-core callback. +Gate F tests all nine creator-to-destination combinations. Offset translation +is allowed only if the build proves equal offsets for every exported symbol +and disables transformations such as LTO/ICF that can make layouts diverge. + +If the device provides a documented CPU-local program alias suitable for all +three cores, that is preferable and must be tested before retaining software +translation. + +LPAx totals 64 KiB, but the simultaneous reference RAM layout gives CPU2 +LPA1 32 KiB while CPU1 uses LPA0; CPU3 similarly receives 32 KiB CPA0. These +32 KiB limits apply to the initial tri-core RAM profile. A flash profile may +allocate more of LPAx to CPU2 only when the memory allocator proves that CPU1 +does not require the displaced region. Every profile fails at link time on +overflow. Common compiler options target the CPU1/CPU2-compatible 32-bit FPU +subset; CPU3-only FPU64 optimization is deferred. + +A UP profile must explicitly choose between verified CPU1/CPU2 lockstep or +split CPU1 with secondaries held in reset. `LSEN=1` alone is not a safety +profile because the lockstep comparator has separate enable/configuration. +SMP always uses split mode, and returning to reset defaults requires the +documented reset class. + +### 5.8 IPI + +Use the F29 IPC blocks for directed reschedule interrupts. + +- Reserve one channel/flag per CPU pair for the kernel. +- `ulmk_arch_send_ipi(cpu)` sets the destination's reserved IPC flag. +- The destination PIPE INT clears/acknowledges the IPC event, calls + `ulmk_kern_ipi_from_isr()`, and dispatches at ISR exit. +- Validate all six directed paths: 1→2, 1→3, 2→1, 2→3, 3→1, and 3→2. +- Coalesce repeated kicks with the existing per-CPU ulmk pending state. + +## 6. Feasibility gates + +These are acceptance gates, not a demand to finish the whole port before +Phase 1. Phase 0 runs the small blocking probes from A, C, D, E, and G before +their mechanisms are frozen; the complete B, D, E, F, and G criteria close in +the implementation phase that owns them. A failed gate updates this plan +before dependent work expands. + +### Gate A — compiler, linker, and freestanding ABI + +Deliver: + +- CMake 3.29+ TIClang smoke project. +- C, assembly, archive, link, map, ELF inspection, binary extraction, and size + operations through the tools actually shipped with the selected C29 + compiler package. +- TI `.cmd` script containing ulmk-required boundary symbols. +- Compile-time ABI assertions. +- Link with no TI CRT startup, heap, C++ initialization, or unresolved atomic + helper. +- Link trace for `uint64_t` add/subtract/compare/multiply/divide operations + used by the timer wheel, with every required runtime helper identified. + +Pass: + +- one RAM `.out` loads and prints a deterministic banner +- the selected ELF inspector confirms C29 ELF/EABI +- the map contains every required ulmk linker symbol +- no implicit libc/libc++/libsys dependency remains + +### Gate B — complete context and interrupt-exit switch + +Deliver: + +- full C29 context frame and synthetic initial frame +- same-Stack unprivileged INT veneer +- protected bridge that enters and leaves the per-CPU kernel Stack before the + context handoff +- `INT` epilogue using a per-thread frame +- one-way first launch into the runtime Link/scheduler Stack +- runtime-Link idle trampoline and explicit idle-stack region +- cooperative yield plus timer preemption + +Pass: + +- sentinels survive in every A, D, and M register +- RPC, restorable DSTS/ESTS fields, flags, A15, automatic INT words, and + 8-byte alignment survive; `INTS`, ISR priority, `CLINK`, and `RLINK` match + their expected transition state at every phase +- at least one million mixed cooperative/preemptive switches at optimized + builds +- ordinary INT nesting remains disabled; nested Supervisor arrivals are + bounded/coalesced; RTINT/NMI never schedule +- first launch, normal return-to-exit, block, wake, kill, and reuse pass +- first launch and idle both satisfy `CURRSP == INTSP` with no boot/protected + frame residue; tick and IPI preempt idle +- no switch is performed from idle or ordinary kernel C code +- no kernel/protected stack frame survives the handoff +- a one-word `ulmk_arch_ctx_t` is accepted only after all resumable state is + proven to reside in the thread frame + +### Gate C — INTSP bridge and protected syscall entry + +Deliver: + +- one scheduler Stack satisfying `CURRSP == INTSP` +- unprivileged assembly INT veneer and exact protected kernel entry +- normalized four-argument syscall ABI +- dedicated Supervisor PIPE software interrupt +- proven force-only runtime API access or the short protected force fallback +- bounded old-stack/new-stack APR handoff with protected cleanup +- return-value injection into the saved frame + +Pass: + +- user code can force only the intended doorbell source +- user code cannot read, clear, enable, disable, or force tick/IPI/device + channels through PIPE operational registers +- a direct branch/call into veneer or kernel code without active INT cannot + gain kernel authority +- the first kernel packet and caller return packet satisfy + `ENTRY1.PROT || ENTRY2.PROT` and `EXIT.PROT` rules +- the protected call returns before context selection is applied +- two or more threads may block in syscalls and wake in non-LIFO order +- malformed entry, branch, stack, and pointer tests fault only the caller +- back-to-back optimized syscalls preserve all declared clobbers +- `DISINT` cannot suppress syscall, tick, or IPI +- every Supervisor source uses the threshold-priority plus + `SUP_IGN_INTE_EN` contract, and tie/nesting tests prove only the outermost + epilogue schedules +- a nested wake after initial staging forces the outer epilogue to rerun or + revalidate dispatch before applying the selected frame +- no selected-thread instruction executes while both old and new private + stacks are mapped; direct calls to the cleanup entry cannot alter APRs +- repeated `ATOMIC.REG` behavior is measured and either mitigated or recorded + as a threat-model limit + +### Gate D — SSU isolation and recoverable faults + +Deliver: + +- linker-generated Link/Stack/AP layout +- per-thread dynamic APR programming +- fault decode and user-vs-kernel attribution +- exact allowlist for driver MMIO apertures + +Pass: + +- user read/write/execute attempts against kernel memory fault +- cross-thread stack/data access faults +- driver MMIO works only with an explicit mapped region +- branches among every candidate user/driver/veneer Link pair cannot acquire + static authority; the common-runtime-Link design remains mandatory unless + this is disproved +- representative unauthorized writes to SSU, PIPE, clock/reset, and + peripheral control registers are denied or trapped +- registers that can latch or acknowledge a write before fault delivery are + absent from the user/driver attack surface +- grants appear and disappear at the correct syscall boundaries +- repeated switches prove no previous-thread stack/APR remains reachable +- kernel, ISR, NMI, or ambiguous faults panic +- a user fault kills one thread and the rest of the system continues +- the faulting instruction is not re-executed after the thread is killed +- no interrupt, protected-call, or context frame leaks during recovery +- recovery is proven separately on CPU1, CPU2, and CPU3; CPU1 reset + propagation is not accepted as CPU-local recovery + +The public material routes protection faults through NMI/error aggregation, +whose protected frame is not an ordinary writable software frame. Gate D must +prove a supported return redirection or a bounded core-local recovery +mechanism. If hardware permits only device reset, stop and revise the product +fault contract before implementing the remaining isolation layer. + +MMIO denial can be reported after a peripheral observed the transaction. +Therefore Gate D treats SSU primarily as a memory/code/stack boundary and +keeps system-control apertures permanently kernel-only. Driver MMIO uses +deny-by-default dynamic exact apertures. The port does not claim transactional +rollback for hostile MMIO writes. + +### Gate E — three-core atomics and IPIs + +Deliver: + +- reviewed CAS, fetch-add, spinlock, and ordering primitives +- directed IPC IPI implementation +- shared LDA test area + +Pass: + +- no compiler atomic helper in the image +- the emitted atomic mnemonic/window/termination sequence is approved for + every supported compiler +- all CPU1/CPU2/CPU3 enable bits in global `MEM_DLB_CONFIG` are confirmed + clear before ulmk RAM reinitialization/test/first shared use and secondary + release +- debugger halts are disabled during atomic stress +- three-core contended counter has no lost updates +- CAS winner tests never report two winners +- release/acquire message-passing stress reports no stale payload +- the TI architectural/compiler ordering contract is cited for every + acquire/release primitive, or the primitive is rejected +- NMI injection does not violate the accepted exclusion contract +- all six directed IPI paths preempt a lower-priority destination thread +- remote wake works while the destination is idle +- long scheduler/pool stress has no deadlock or starvation + +### Gate F — one-artifact multicore boot + +Deliver: + +- three internal core-specific linked images +- one final CPU1 `.out` +- CPU2/CPU3 payload packaging and boot +- CRC-valid per-core SSU configuration sections plus lifecycle-appropriate + CPU1/CPU3 certificate and boot metadata packaging +- affinity-CPU context fabrication through the existing deferred-init + contract +- generated cross-core entry IDs and per-core resolver tables +- build-time cross-core pointer inventory + +Pass: + +- one `loadti` invocation starts all three CPUs +- CPU1 reports ready mask `0x7` +- each CPU runs a pinned ulmk thread, tick, and IPI handler +- the initial CPU2 and CPU3 RAM payloads each remain within their 32 KiB + profile budget +- no unresolved raw code pointer crosses a core boundary +- entry/callback resolution passes all nine creator-to-destination CPU + combinations +- RAM mode, flash mode, reset, and repeated reload pass +- the one-artifact flow passes under SSUMODE2 with the selected BANKMODE +- flash mode boots without an attached debug session + +### Gate G — repeatable HIL control + +Deliver: + +- CCS root/version discovery with no hard-coded home directory +- XDS110 serial selection +- DSS load, attach, run, halt, symbol, register, and memory operations +- `xds110reset` integration +- read-only capture of silicon revision, device lifecycle, active BANKMODE, + and reset capabilities before any flash operation +- controlled power-cycle or manual POR procedure for reset-sensitive tests +- stable application-UART capture +- timeout, cleanup, and exclusive board locking + +Pass: + +- scripts reject the wrong probe or auxiliary UART +- flash commands refuse to run when lifecycle/BANKMODE is unknown or differs + from the reviewed test profile +- a failed test leaves the board recoverable +- test output names case, build ID, core mask, and PASS/FAIL +- the same test can run repeatedly without CCS GUI state +- split/lockstep and SSUMODE tests start from their required reset class, not + merely a warm debugger reset + +## 7. Build-system work + +### 7.1 CMake and tool discovery + +Require CMake 3.29 or newer only when the selected board uses C29. Native +TIClang compiler identification starts there; the current 3.20 project +minimum remains valid for existing boards. The C29 toolchain fails before +`project()` with a clear version error instead of forcing a fake compiler ID. +Gate A pins the exact working CMake/TIClang tuple. + +Add `cmake/toolchain-c29-ticlang.cmake` with cache/environment discovery for: + +- `TI_CCS_ROOT` +- `TI_C29_CGT_ROOT` +- `TI_F29H85X_SDK_ROOT` +- `TI_SYSCONFIG_ROOT` + +The toolchain locates `c29clang`, `c29lnk`, `c29ar`, `c29readelf`, +`c29objdump`, `c29nm`, `c29size`, `c29ofd`, and `c29objcopy`; sets +`CMAKE_TRY_COMPILE_TARGET_TYPE=STATIC_LIBRARY`; proves `.S` preprocessing and +C29 integrated-assembly compilation; and records versions in the build +manifest. Gate A repeats the capability check for every supported compiler +release rather than assuming the local STS file set. + +No source or script may contain `/home/ulipe/ti`. + +Wire C29 through all current selectors, not only the toolchain: + +- `cmake/arch.cmake` accepts `UL_BOARD_ARCH=c29` and selects + `toolchain-c29-ticlang.cmake` +- `cmake/arch_sources.cmake` defines the C29 kernel/executable source sets +- `tools/dev.py` stops deriving every toolchain as + `toolchain--gcc.cmake` and accepts the external TI mount +- SDK-suite selection gains a silicon runner instead of a QEMU command + +`board.cmake` and `board_config.h` both declare +`ULMK_BOARD_C29_DLB_WORKAROUND=1` for their respective configure-time and C +consumers. `cmake/config.cmake` rejects a C29 SMP configure unless the CMake +declaration is present; runtime boot still reads back the hardware bits. + +Audit every GCC/GNU-specific assumption in `CMakeLists.txt`, +`cmake/optimize.cmake`, `cmake/component_api.cmake`, and +`cmake/linker_api.cmake`. This includes `-fno-data-sections`, +`-fno-tree-loop-distribute-patterns`, `-Ofast`, `-fno-inline`, +`-nostartfiles`, `-Wl,*`, start/end groups, archive naming, section naming, +and `.S` behavior. Add compiler-family helpers for compile, archive, and link +options; do not scatter `if(C29)` conditionals over targets. Do not hide +unsupported options globally with `-Qunused-arguments`. + +The link backend must not rely on `-nostartfiles`: the installed driver +ignores it. Gate A compares `-nostdlib` and `-nodefaultlibs`, verifies the +resulting library list from the link trace/map, and explicitly selects any +required 64-bit arithmetic or compiler-runtime helpers. C29 leading-zero +count uses the verified `__builtin_c29_i32_clzeros_d` intrinsic rather than +assuming generic `__builtin_clz` lowering. + +### 7.2 TI linker backend + +GNU linker fragments cannot be passed to the TI linker. Keep the existing GNU +renderer untouched and add an explicit TI-C29 renderer selected by +`--dialect ti-c29 --core <1|2|3>`: + +- board input is `memory.cmd` +- dedicated TI templates reproduce the symbols that kernel code actually + consumes +- component, application, domain, stack, user-pool, and shared sections are + generated from the same CMake registrations +- CPU1, CPU2, and CPU3 scripts receive an explicit core parameter +- per-core SSU configuration and BANKMODE-dependent addresses are generated + from one reviewed policy input +- the canonical-address/entry-ID and per-core local resolver tables are + generated and compared +- generated maps are checked for region overflow, executable/data placement, + and the cross-core entry-ID table + +Gate A must first prove TI linker syntax for archive-member selection, +retention, alignment, NOLOAD-like sections, fill/ECC behavior, load/run +addresses, symbol definitions, and unused-section elimination. Existing GNU +fragments hard-code selectors such as `*libulmk_kernel.a:(...)`; the TI +backend must not reuse that syntax blindly. + +The TI path never calls the GNU `parse_flags()` routine or interprets +`HAVE_CSA/HAVE_SMALL_DATA/HAVE_BMHD`. Core, BANKMODE, SSU policy, and C29 +feature inputs arrive as explicit generator arguments; `memory.cmd` supplies +only named device regions that the TI renderer validates. + +Final entry addresses require a deterministic two-pass link: + +1. first-link CPU1/CPU2/CPU3 and emit symbol manifests +2. run a CMake custom command that assigns stable IDs and generates all + creator-address-to-ID plus local-ID-to-address tables +3. final-link with those tables in reserved terminal sections that cannot move + executable symbols +4. compare final symbols to the first-pass manifests and fail on drift or an + unregistered cross-core entry + +`_ulmk_finalize_build()` must select linker suffix, generator dialect, map +flags, library search/order semantics, runtime libraries, and dead-section +elimination from a compiler-family backend. The C29 multicore path creates +three internal executable targets with core-specific scripts and exposes only +the packed CPU1 target as `ulmk`. The flash post-link backend adds the +lifecycle-appropriate certificate/metadata without modifying files inside the +TI SDK installation. + +Update `tools/sdk_build.sh` as part of this work: it currently assumes host +`ar`, a `_gcc` artifact tag, GNU `.ld`, `-T`, textual archive-name rewriting, +GNU `grep` validation, and one directly linkable image. C29 SDK output uses +the discovered C29 archiver, a `ticlang` tag, processed core-specific `.cmd` +files, and a manifest that identifies the CPU1 load artifact plus embedded +secondary payloads. The SDK consumer rules in +`tests/sdk_suite/sdk_case.mk` need a C29 compile/link backend and DSS/HIL +runner; changing `sdk_build.sh` alone is insufficient. Its C29 interface uses +`C29_DSS_RUNNER`, `C29_CCXML`, `C29_PROBE_SERIAL`, `C29_UART`, and +`C29_HIL_TIMEOUT` instead of `QEMU_RUNNER`, `MACHINE`, and `QEMU_EXTRA`. + +### 7.3 Proprietary tools in the dev environment + +Do not redistribute CCS or the SDK in the repository image. + +Extend `tools/dev.py` with an optional read-only TI installation mount and +explicit USB/serial device forwarding for a self-hosted HIL runner. Normal +QEMU and host-unit jobs must remain usable without TI software. + +The host path selected by `TI_INSTALL_ROOT` mounts read-only at `/ti`. +Container-side discovery derives CCS, SDK, SysConfig, and compiler roots +beneath `/ti`, and extends `CONTAINER_PATH` with +`${TI_C29_CGT_ROOT}/bin`. HIL mode alone forwards the selected +`/dev/bus/usb` XDS110 node and by-id application UART; compile-only C29 jobs +receive neither device. + +The C29 compile and HIL lanes run on a licensed self-hosted runner. The board +is an exclusive resource keyed by XDS110 serial. + +## 8. Repository changes + +### 8.1 `ulmk` + +Planned areas: + +- `arch/c29/include/arch_config.h` +- `arch/c29/include/ulmk_arch.h` +- `arch/c29/include/ulmk_syscall_abi.h` +- `arch/c29/startup.S` +- `arch/c29/trap.S` +- `arch/c29/ctx_switch.S` +- `arch/c29/arch.c` +- `arch/c29/irq.c` +- `arch/c29/mpu.c` +- `arch/c29/smp.c` +- `arch/c29/linker/` +- `kernel/kernel_main.c` arch-neutral idle entry/privilege hooks +- TIClang toolchain and TI linker generation +- two-pass cross-core entry-table generator +- C29 compile/disassembly tests +- C29 HIL runner integration +- architecture/API/build/test documentation updates + +The split among C files may change after the spikes, but kernel/arch layering +must not. The C29 `ulmk_arch.h` is checked against the complete symbol set used +by the current kernel, including SMP/IPI, cycle counter, scheduler-switch, MPU, +IRQ-attach, fault-restore, and boot callbacks; the older prose specification +alone is not treated as an exhaustive header. + +### 8.2 `ulmk_boards` + +Add `launchxl_f29h85x/` with: + +- `board.cmake` with `UL_BOARD_ARCH=c29` and + `ULMK_BOARD_C29_DLB_WORKAROUND=1` +- `board_config.h` with `ULMK_ARCH_NUM_CPU=3`, PIPE/timer/IPC addresses, and a + matching `ULMK_BOARD_C29_DLB_WORKAROUND=1` +- `memory.cmd` +- pinned SysConfig source/configuration inputs +- early clock, watchdog, ECC RAM, pinmux, UART, and LED initialization +- XDS110/CCS environment discovery +- target `.ccxml` +- load, flash, reset, debug, serial capture, and HIL scripts +- recovery and jumper/boot-mode documentation + +C29 SMP configure fails unless the selected board declares the DLB workaround; +early boot also reads back all three global enable bits before releasing a +secondary. The build-time declaration never substitutes for the runtime +check. + +Use the XDS110 application UART, initially at 115200 8N1. CPU1 is the only +normal console writer; other CPUs report through shared result records that +CPU1 prints. + +The reference SysConfig pins UARTA frame 0 (`0x60070000`) and emits the +peripheral-frame mapping consumed by the BSP and SSU policy. Peripheral frames +are relocatable in `0x00400000` steps, so neither the arch layer nor MMIO +allowlist may assume frame 0 globally. + +Early RAM tests may use generated TI driverlib/SysConfig output. The final +BSP should link only the required vendor pieces and must document their +licenses and exact SDK compatibility. + +### 8.3 `ulmk_apps` + +Add narrowly scoped components in this order: + +- C29 board banner, LED, and UART smoke +- register-context stress +- syscall/block/wake stress +- SSU negative-access tests +- three-core atomic and IPC IPI smoke +- three-core scheduler stress +- baseline hello/ping-pong +- existing silicon SDK-suite cases adapted to the C29 HIL protocol + +Bring-up-only components are removed or clearly marked once equivalent +permanent tests cover them. + +## 9. Implementation sequence + +### Phase 0 — feasibility + +Run the blocking compiler/linker, INT/protected-entry, recoverable-fault, +atomic-ordering, and HIL-control probes from Gates A, C, D, E, and G as small, +reviewable spikes. Keep each spike separate from broad kernel changes. Record +generated assembly, linker maps, silicon revision, measured behavior, and the +exact TI tool versions. + +Exit: every blocking question has executable evidence and this plan reflects +the selected mechanisms. + +### Phase 1 — build and linker foundation + +- land TIClang discovery +- land TI `.cmd` generation +- produce one-core RAM `ulmk.out` +- keep all existing GNU boards unchanged + +Exit: Gate A passes and C29 startup reaches early UART from +`ulmk_kern_start()`. + +### Phase 2 — UP kernel + +- complete context entry/exit +- add tick, yield, syscalls, IRQ routing, and root thread +- run with SSU permissive only long enough to debug the mechanics + +Exit: Gates B and C pass; baseline plus UP HIL +scheduler/IPC/notif/timer tests pass. + +### Phase 3 — SSU isolation + +- generate static Link/Stack/AP layout +- add dynamic thread regions and grants +- add recoverable fault policy +- enable driver/user separation + +Exit: Gate D passes without Link2 override outside controlled +boot/configuration code. + +### Phase 4 — three-core SMP + +- package and boot CPU2/CPU3 +- add shared state, atomics, IPC IPI, per-CPU ticks, and ready barriers +- land the two-pass post-link entry-ID/resolver table generator +- enforce CPU affinity, all creator/destination mappings, and pointer-inventory + checks + +Exit: Gate E and the RAM portion of Gate F pass; all three CPUs schedule +concurrently and pass stress. + +### Phase 5 — flash and operational tooling + +- add signed/dummy-certificate development image flow as appropriate for the + board life-cycle state +- add flash-safe DSS operations and cold boot +- add command-line multicore debug + +Exit: flash/SSUMODE2 Gate F and all Gate G criteria pass, including repeatable +reset, attach, symbol load, fault dump, and standalone boot. + +### Phase 6 — regression and documentation + +- run all existing TriCore, RISC-V, ARMv7-M, and ARMv8-M regressions +- add C29 compile lane and C29 HIL lane +- update porting, architecture API, linker, build, and application guides +- document supported TI tool versions and recovery procedures + +Exit: the complete release matrix passes. + +## 10. HIL protocol + +Tests should emit machine-readable lines on the CPU1 UART: + +```text +ULMK-HIL::START build= silicon= +ULMK-HIL::CORE_READY mask=0x7 +ULMK-HIL::PASS +``` + +On failure, CPU1 prints the failing core, stage, last fault/PIPE/IPC state, +and a stable numeric reason before halting or entering the debugger-safe +failure loop. + +The HIL runner: + +1. acquires the board lock +2. verifies XDS110 serial and firmware +3. verifies the application UART interface +4. builds or validates the requested artifact +5. starts UART capture +6. loads/flashes and runs through DSS +7. waits for the exact PASS marker +8. captures timeout diagnostics from all CPUs +9. resets or releases the target to a documented state +10. releases the lock + +RAM loading is the default during bring-up. Flash and security configuration +tests are separate and opt-in until board recovery is proven. Before the first +flash command, the runner records the actual device lifecycle and BANKMODE; +it must not infer GP/HS-FS/HS-SE state from the board model or debugger +availability. + +## 11. Regression policy + +Every C29 kernel change must run: + +- architecture-independent host unit tests +- C29 compile/link/disassembly checks +- C29 UP HIL +- C29 three-core SMP HIL when SMP, scheduler, IRQ, atomic, IPC, memory, or + linker code is touched +- existing four-board unit, SDK, and baseline QEMU regression + +Before merge, the self-hosted C29 HIL lane is mandatory. A compiler-only job +does not substitute for the absent emulator. + +## 12. Principal risks + +### Protected syscall entry + +Highest risk. The protected-call stack and PIPE ownership rules may prevent a +safe software-interrupt doorbell. Gate C decides feasibility before the +public syscall layer is ported. Threshold-priority Supervisor nesting and the +mandatory post-`RET.PROT` `EXIT.PROT` packet are part of the same gate. + +### Cross-Link authority + +Branching between Links assigned to one Stack can change active authority. +The initial common runtime Link and dynamic driver MMIO policy are mandatory +until Gate D proves an alternative cannot be used for escalation. + +### Dynamic APR handoff + +Returning from Link 2 restores the old scheduler-Stack A15 before the veneer +can adopt the selected A15. The port needs an interrupt-safe two-stack window +and protected cleanup with no user execution in between; otherwise stack +isolation cannot survive a context switch. + +### Cross-core atomic exclusion + +Compiler atomics are not a valid contract. The memory-wrapper behavior of +the emitted `ATOMIC.M`-family sequence must be demonstrated across all three +CPUs under contention, and its memory-ordering scope must be established +separately from mutual exclusion. + +### Shared-RAM stale reads + +The current errata affects every listed silicon revision. SMP is invalid +unless all three global initiator-enable bits are clear before ulmk's first +shared use and secondary release, and the workaround is covered by a +message-passing stress test. + +### Recoverable SSU faults + +Protection violations enter the NMI/error path, not a conventional writable +trap frame. The port must prove that it can prevent re-execution of the +faulting instruction and continue on another thread; otherwise the current +recoverable-fault contract cannot be claimed. + +### Multicore program addresses + +Different LPA/CPA addresses make shared function pointers unsafe. Link-map +equivalence alone is insufficient; generated entry IDs and the cross-core +pointer inventory must be enforced. + +### CPU2 code budget + +The simultaneous tri-core RAM profile budgets 32 KiB LPA1 for CPU2 and 32 KiB +CPA0 for CPU3. Any larger profile must prove non-overlap explicitly. Overflow +is a hard product limit and fails at link time. + +### SSU configuration lifecycle + +Committed/flash security configuration can remove debug or recovery access. +Early work must remain in reversible RAM-mode configuration and follow the +current silicon errata. + +### Toolchain drift and licensing + +The local compiler is STS and older than the current LTS release. Discovery, +version checks, and a self-hosted licensed runner are required; TI binaries +must not be committed. + +### Interrupt cost + +Saving the entire C29 register file is large. Correctness comes first; later +optimization requires measured WCET and proof that compiler-used M registers +remain preserved. + +### Single physical target + +The board cannot service parallel jobs. HIL needs serialization, hard +timeouts, recovery, and enough diagnostics to distinguish target failure from +probe/UART failure. + +## 13. Definition of done + +The C29/F29H85x port is complete only when: + +- UP and three-core SMP builds produce one final `ulmk.out`. +- RAM load and standalone flash boot both pass. +- SSUMODE2 images contain valid per-core security configuration at the + BANKMODE-selected addresses and the required boot certificate/metadata. +- Root thread, syscalls, IPC, notifications, timers, IRQ binding, thread + lifecycle, capabilities, memory grants, and fault policy pass on silicon. +- Three per-CPU ticks and all directed reschedule IPIs pass. +- Atomic, scheduler, pool, and remote-wake stress pass on all three CPUs. +- SSU negative tests prove kernel/thread isolation, deny privileged control + apertures, and enforce exact dynamic driver MMIO mappings without claiming + rollback of side-effecting MMIO. +- CPU2 size and every linker/AP limit are checked automatically. +- Command-line load, flash, reset, attach, run, halt, symbol load, memory + inspection, and fault capture work without CCS GUI interaction. +- No path or test depends on `/home/ulipe`, `ttyACM0`, or probe enumeration + order. +- Current silicon errata and supported TI tool versions are documented. +- Existing TriCore, RISC-V, ARMv7-M, and ARMv8-M regression remains green. + +## 14. Review checkpoints + +Request focused review after: + +1. syscall/INT/SSU gate design +2. full context-frame assembly +3. TI linker and three-image packaging +4. emitted `ATOMIC.M`-family sequence and memory-ordering evidence +5. SSU fault and grant model +6. final HIL and flash recovery procedure + +Do not combine these checkpoints into one large architecture PR. diff --git a/docs/gates/c29f_gate_evidence.md b/docs/gates/c29f_gate_evidence.md new file mode 100644 index 0000000..2c696d8 --- /dev/null +++ b/docs/gates/c29f_gate_evidence.md @@ -0,0 +1,92 @@ +# C29/F29H85x Feasibility Gate Evidence + +**Branch:** `plan/c29f-port` +**Date:** 2026-07-23 +**Probe:** XDS110 `CL850001` +**Toolchain:** TI C29 Clang `2.0.0.STS` (`c29-ti-none-eabi`) + +## Gate A — compiler, linker, freestanding ABI + +| Check | Result | +|-------|--------| +| ILP32 (`sizeof(void*)/int/long/size_t == 4`) | PASS (`_Static_assert`) | +| ELF32 little-endian Machine 92 | PASS (`c29readelf`) | +| Integrated asm via `c29clang` | PASS | +| Freestanding `-nostdlib` + TI `.cmd` | PASS | +| No TI CRT / no libc in bare image | PASS | +| `ATOMIC.MEM` / `ATOMIC.END` for CAS | PASS (no `__atomic_*` helper) | +| CLZ via `__builtin_c29_i32_clzeros_d` | PASS | +| Host CMake 3.22.5 vs TIClang 3.29 | WORKAROUND: custom toolchain | + +## Gate B — context / INT exit + +| Check | Result | +|-------|--------| +| Full A/D/M frame (264 B) + `RETI.INT` veneer | PASS (code) | +| First launch via `RET` | PASS (root prints) | +| Idle on runtime Link + `ENINT`/`IDLE` | PASS (code) | +| PIPE `MEM_INIT` + vector @ `+0x5000` | PASS (syscall INT works) | +| Deferred coop switch while INT active | PASS (required; direct `RET` blocked further INTs) | +| CPUTIMER2 periodic wake of sleeper | **PARTIAL** — ISR hits during boot (`g_c29_tick_hits>0`); sleep after `RETI` to idle does not yet resume. Banner HIL uses `yield` heartbeat. | + +## Gate C — INTSP / syscall doorbell + +| Check | Result | +|-------|--------| +| INTSP = Stack2 (TI `Interrupt_initModule`) | PASS | +| INT_SW2 force doorbell @ `0x300223F8` | PASS (syscalls return) | +| `SUP_IGN_INTE_EN` + `GLOBAL_EN` | PASS | +| Nesting outermost-only schedule | PASS (`g_c29_int_depth`) | + +## Gate D — SSU isolation + +| Check | Result | +|-------|--------| +| Dynamic APR program/disable API | CODE READY (`mpu.c`) | +| `addr_permitted` shadow | PASS (logic) | +| Link2 override clear on SSUMODE2 | CODE READY (`ulmk_arch_mpu_enable`) | +| SECCFG MODE2 blob (CRC + necessary APRs) | **HOST PASS** (`tools/c29_seccfg_gen.py` + `seccfgChecker.py` Error count=0; board `seccfg/seccfg_cpu*.c`) | +| Packaging strips NonMain by default | **PASS** (`package-seccfg.sh`; `ULMK_C29_SECCFG_COMMIT=1` opt-in) | +| Negative isolation HIL (MODE1) | **PASS SKIP** (`hil-ssu-neg.sh` → `C29SSU_SKIP` / mode=0x30) | +| NonMain SECCFG program (silicon) | **PARTIAL / FRAGILE** — first MODE2 write OK (`SECCFG+0x7F8=0x0C` via loadti+`dss-flash-norest`). XRSn proves flash boot again under MODE1. Later `0x30→0x0C` needs NonMain erase (NOR 0→1); Entire Flash erase left APR area `0xFF…FE` but **program no longer sticks** (plugin reports Full load; dump still blank head + residual `MODE=0x30`). TI FAQ: disable verify for SECCFG; still BAD with `VerifyAfterProgramLoad=No verification`. | +| Negative MMIO isolation HIL (MODE2) | **BLOCKED** — XRSn required for SECCFG apply; MODE2 re-program currently fails after erase. RAM HIL still PASS (`C29SSU_SKIP`). | +| Fault recovery on CPU1/2/3 | **DEFERRED** — NMI → `ulmk_kern_trap_recoverable` prints kill then loops; full continue-on-idle not proven | + +## Gate E — atomics / IPI / DLB + +| Check | Result | +|-------|--------| +| CAS/add via atomic builtins | PASS (Gate A) | +| DLB clear all three CPU bits | PASS (`ulmk_arch_init` + board_init) | +| Directed IPC FLAG0 IPI (6 pairs) | CODE READY (`smp.c`); smoke sends IPIs after mask=0x7 | +| Three-core atomic stress HIL | **DEFERRED** — needs longer SMP stress | + +## Gate F — one-artifact multicore + +| Check | Result | +|-------|--------| +| CPU2 @ LPA1 `0x20108000` / CPU3 @ CPA0 `0x20110000` stubs | PASS (`c29nm` / map) | +| SSU reset vector release | PASS — `ISR1.PROT`, handshake in SHARED_RAM `0x200F8000`, `DEF_NMI=RST_VECT` (GEL) | +| Split-lock before CPU2 | PASS | +| Ready mask `0x7` HIL | **PASS** (`hil-smp-smoke.sh` / `c29_smp_smoke:CORE_READY mask=0x7`) | +| Flash SMP POR (BANKMODE0) | **PASS** (`hil-smp-flash.sh`: DSLite Main + contiguous stubs, DSS reset, `smp ready mask=0x7` / `CORE_READY mask=0x7`) | + +## Gate G — HIL control + +| Check | Result | +|-------|--------| +| Probe serial `CL850001`, UART `if00` only | PASS | +| Board lock + loadti timeout | PASS | +| Reject flash without BANKMODE/lifecycle | PASS (`hil-flash-por.sh`) | +| SECCFG packaging script | **PASS** host path — MODE2 blobs + strip-by-default; NonMain commit opt-in | +| Flash autonomous after JTAG sysreset (no reload, no session) | **PASS** (`hil-flash-por.sh` + `dss-reset-run.js`; UP `C29SLEEP` / `ulmk:`; INTOSC2 UART) | +| Flash UART capture before DSS reset | **PASS** — open `cat` on UART before `dss-reset-run` (boot banners otherwise lost within ~100ms) | +| Pre-flash halt before DSLite | **PASS** — `dss-preflash-halt.js` avoids `wr_pll.alg` timeout on free-running XIP idle | +| Physical button / XRSn POR | **DEFERRED** (nSRST via `xds110reset` does not reboot C29 on LAUNCHXL) | + +## Decisions locked + +- Freestanding link uses `-nostdlib`. +- PIPE vectors at `PIPE_BASE+0x5000+n*4`; enable in `INT_CTL_L`; force/clear in `INT_CTL_H`. +- Coop switches inside INT are deferred to `RETI.INT`. +- Secondary payloads live in the single CPU1-loadable `ulmk.out`. diff --git a/kernel/kernel_main.c b/kernel/kernel_main.c index 96737bb..68f7e47 100644 --- a/kernel/kernel_main.c +++ b/kernel/kernel_main.c @@ -201,6 +201,13 @@ void ulmk_kern_trap_panic(void) * Thread entries * ========================================================================= */ +#ifndef ULMK_ARCH_IDLE_ENTRY +#define ULMK_ARCH_IDLE_ENTRY idle_thread_entry +#endif +#ifndef ULMK_ARCH_IDLE_PRIVILEGE +#define ULMK_ARCH_IDLE_PRIVILEGE ULMK_PRIV_KERNEL +#endif + static void idle_thread_entry(void *arg) { (void)arg; @@ -319,11 +326,11 @@ void ulmk_kern_main(const ulmk_boot_info_t *info) */ for (cpu = 0u; cpu < (uint32_t)ULMK_NR_CPUS; cpu++) { attr.name = "idle"; - attr.entry = idle_thread_entry; + attr.entry = ULMK_ARCH_IDLE_ENTRY; attr.arg = NULL; attr.priority = 255u; attr.stack_size = ULMK_IDLE_STACK_SIZE; - attr.privilege = ULMK_PRIV_KERNEL; + attr.privilege = ULMK_ARCH_IDLE_PRIVILEGE; attr.cpu = (uint8_t)cpu; ulmk_thread_init(&idle_thread_g[cpu], &attr, idle_stack_g[cpu]); ulmk_percpu_of(cpu)->idle = &idle_thread_g[cpu]; @@ -354,19 +361,46 @@ void ulmk_kern_main(const ulmk_boot_info_t *info) } #endif - ulmk_arch_mpu_enable(); - UL_LOG_DBG("mpu enabled"); - - ulmk_arch_smp_mark_ready(); #if ULMK_CONFIG_ENABLE_SMP + /* + * Bring up secondaries before MPU enforce so arch boot stubs can + * still write the ready handshake under a permissive protection + * window. + */ for (cpu = 1u; cpu < (uint32_t)ULMK_NR_CPUS; cpu++) { ulmk_printk("ulmk: starting CPU%u\n", (unsigned)cpu); ulmk_arch_start_secondary(cpu, ulmk_kern_secondary_main); } + { + uint32_t want = (1u << ULMK_NR_CPUS) - 1u; + + ulmk_arch_smp_wait_ready(want); + ulmk_printk("ulmk: smp ready mask=0x%x\n", + (unsigned)ulmk_arch_smp_ready_mask()); + { + extern volatile uint32_t g_c29_smp_diag[6] + __attribute__((weak)); + + if (&g_c29_smp_diag[0] != NULL && + g_c29_smp_diag[0] != 0u) { + ulmk_printk( + "ulmk: c29 smp diag cpu=%u rststat=0x%x rstctrl=0x%x vect=0x%x mem=0x%x ready=0x%x\n", + (unsigned)g_c29_smp_diag[0], + (unsigned)g_c29_smp_diag[1], + (unsigned)g_c29_smp_diag[2], + (unsigned)g_c29_smp_diag[3], + (unsigned)g_c29_smp_diag[4], + (unsigned)g_c29_smp_diag[5]); + } + } + } #endif + ulmk_arch_mpu_enable(); + UL_LOG_DBG("mpu enabled"); + ulmk_printk("ulmk: switching to root thread\n"); /* diff --git a/kernel/sched/sched.c b/kernel/sched/sched.c index ec483b3..232ee7e 100644 --- a/kernel/sched/sched.c +++ b/kernel/sched/sched.c @@ -98,7 +98,13 @@ static void sched_switch_to(ulmk_thread_t *prev, ulmk_thread_t *next) pc->current = next; next->state = UL_THREAD_STATE_RUNNING; - ulmk_arch_ctx_switch(from, to); + /* + * Always go through arch_sched_switch so arches that share INT with + * syscall (C29) can defer the handoff to RETI.INT. A direct + * ctx_switch from inside INT abandons the veneer and leaves the + * controller active — further ticks never arrive. + */ + ulmk_arch_sched_switch(from, to, ULMK_SCHED_SWITCH_COOP); } void ulmk_sched_set_dead_for_cleanup(ulmk_thread_t *th) diff --git a/kernel/thread/thread.c b/kernel/thread/thread.c index 46be9e5..fc980cd 100644 --- a/kernel/thread/thread.c +++ b/kernel/thread/thread.c @@ -142,7 +142,11 @@ int ulmk_thread_init(ulmk_thread_t *th, const ulmk_thread_attr_t *attr, void *st ulmk_arch_ctx_init(&th->ctx, attr->entry, attr->arg, +#if defined(ULMK_ARCH_STACK_GROWS_UP) && ULMK_ARCH_STACK_GROWS_UP + (uintptr_t)stack, +#else (uintptr_t)stack + attr->stack_size, +#endif attr->privilege); th->ctx_ready = 1u; } @@ -150,7 +154,11 @@ int ulmk_thread_init(ulmk_thread_t *th, const ulmk_thread_attr_t *attr, void *st ulmk_arch_ctx_init(&th->ctx, attr->entry, attr->arg, +#if defined(ULMK_ARCH_STACK_GROWS_UP) && ULMK_ARCH_STACK_GROWS_UP + (uintptr_t)stack, +#else (uintptr_t)stack + attr->stack_size, +#endif attr->privilege); #endif @@ -172,7 +180,11 @@ void ulmk_thread_ensure_ctx(ulmk_thread_t *th) ulmk_arch_ctx_init(&th->ctx, th->start_entry, th->start_arg, +#if defined(ULMK_ARCH_STACK_GROWS_UP) && ULMK_ARCH_STACK_GROWS_UP + (uintptr_t)th->stack_base, +#else (uintptr_t)th->stack_base + th->stack_size, +#endif th->privilege); th->ctx_ready = 1u; } diff --git a/tools/c29_seccfg_gen.py b/tools/c29_seccfg_gen.py new file mode 100644 index 0000000..4d020e6 --- /dev/null +++ b/tools/c29_seccfg_gen.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +""" +Generate CRC-valid SSUMODE2 SECCFG binaries for F29H85x BANKMODE0. + +Starts from TI SDK default MODE1 SECCFG (zone/debug/stack/link intact), +rewrites APR slots for MODE2 necessary + kernel ranges, sets SSU_MODE=0x0C, +recomputes CRC with the same algorithm as seccfgChecker.py. + +Usage: + tools/c29_seccfg_gen.py --sdk --out-dir +""" + +from __future__ import annotations + +import argparse +import os +import struct +import subprocess +import sys + +SECCFG_SIZE = 2048 +SSUMODE_2 = 0x0C +SECCFG_STATUS = 0x55555555 +SECCFG_CRC_OFF = 0x7F4 +SSU_MODE_OFF = 0x7F8 +STATUS1_OFF = 0x7B8 +STATUS2_OFF = 0x7BC +APR_STRIDE = 16 +NUM_APR = 64 + +CRC_RANGES = ((0x000, 0x6C0), (0x7A0, 0x7F4), (0x7F8, 0x800)) + +def load_crc_table(sdk: str) -> list[int]: + """Parse crc32Table from TI seccfgChecker.py without executing it.""" + path = os.path.join(sdk, "tools/misc/seccfgChecker.py") + with open(path, "r", encoding="utf-8") as f: + src = f.read() + start = src.index("crc32Table = [") + end = src.index("]", start) + body = src[start:end + 1] + ns: dict = {} + exec(body, ns) + return list(ns["crc32Table"]) + +def compute_crc(data: bytes, table: list[int]) -> int: + crc = 0xFFFFFFFF + for start, end in CRC_RANGES: + for i in range(start, end): + crc = table[(crc ^ data[i]) & 0xFF] ^ (crc >> 8) + return crc ^ 0xFFFFFFFF + + +def pack_apr(start: int, end: int, access: int, *, + linkid: int = 2, xe: int = 0, disable: int = 0) -> bytes: + """Pack one 16-byte APR matching SSU_SECCFG.AP_REGS bitfields.""" + start_page = (start >> 12) & 0xFFFFF + end_page = (end >> 12) & 0xFFFFF + w0 = (start_page << 12) # LOCK=0 + w1 = (end_page << 12) # COMMIT=0 + # bytes 12-13 rsvd3 = 0; bytes 14-15 config + cfg = (linkid & 0xF) + cfg |= (disable & 1) << 6 + cfg |= (xe & 1) << 7 + # APILINK/APILINKE left 0 + return struct.pack(" int: + return 1 << (link * 2) + + +def link_rw(link: int) -> int: + return 3 << (link * 2) + + +def fill_aprs(buf: bytearray) -> None: + # Unused slots: DISABLE=1, LINKID=2 (checker rejects LINKID=0) + unused = pack_apr(0, 0xFFF, 0, linkid=2, disable=1) + for i in range(NUM_APR): + buf[i * APR_STRIDE:(i + 1) * APR_STRIDE] = unused + + # Necessary APRs (seccfgChecker) — Link1 access. + # Plus kernel/runtime ranges so CPU1 can XIP and use LDA without + # Link2 AP override after MODE2 boot. + aprs = [ + # flash cert page (necessary) — L1 RD + (0x10000000, 0x10000FFF, link_rd(1) | link_rd(2), 1), + # rest of FLASH_RP0 used by ulmk — L1/L2 RD + XE on L2 + (0x10001000, 0x100FFFFF, link_rd(1) | link_rd(2), 1), + # PIPE INT CTL / CONFIG / IPC (necessary) + (0x30226000, 0x30226FFF, link_rw(1) | link_rw(2), 0), + (0x30227000, 0x30227FFF, link_rw(1) | link_rw(2), 0), + (0x302C0000, 0x302C1FFF, link_rw(1) | link_rw(2), 0), + # SSU AP regs CPU1/2/3 (necessary) + (0x60080000, 0x60080FFF, link_rw(1) | link_rw(2), 0), + (0x6008C000, 0x6008CFFF, link_rw(1) | link_rw(2), 0), + (0x60090000, 0x60090FFF, link_rw(1) | link_rw(2), 0), + # PIPE + SSUGEN MMIO windows used by kernel + (0x30020000, 0x3002FFFF, link_rw(2), 0), + (0x30080000, 0x3008FFFF, link_rw(2), 0), + # LDA / LPA / CPA RAM (kernel + shared + secondary stubs) + (0x200E0000, 0x200FFFFF, link_rw(1) | link_rw(2), 0), + (0x20100000, 0x2011FFFF, link_rw(1) | link_rd(2) | link_rw(2), 1), + ] + + for i, (start, end, access, xe) in enumerate(aprs): + if i >= NUM_APR: + raise SystemExit("too many APRs") + buf[i * APR_STRIDE:(i + 1) * APR_STRIDE] = pack_apr( + start, end, access, linkid=2, xe=xe, disable=0) + + +def make_mode2(base: bytes, table: list[int]) -> bytes: + if len(base) != SECCFG_SIZE: + raise SystemExit(f"bad seccfg size {len(base)}") + buf = bytearray(base) + fill_aprs(buf) + struct.pack_into(" dict[int, bytes]: + os.makedirs(tmp, exist_ok=True) + out = {} + for cpu in (1, 2, 3, 4): + path = os.path.join(tmp, f"cpu{cpu}.bin") + subprocess.check_call([ + objcopy, "-O", "binary", + f"--only-section=.TI.bound:CPU{cpu}_Cfg", + default_out, path, + ]) + out[cpu] = open(path, "rb").read() + return out + + +def emit_c_blob(path: str, symbol: str, section: str, data: bytes, addr: int) -> None: + lines = [ + "/* SPDX-License-Identifier: MIT */", + f"/* Auto-generated by c29_seccfg_gen.py — do not edit. */", + "#include ", + "", + f"__attribute__((retain, section(\"{section}\")))", + f"const uint8_t {symbol}[2048] = {{", + ] + for i in range(0, len(data), 16): + chunk = ", ".join(f"0x{b:02x}" for b in data[i:i + 16]) + lines.append(f"\t{chunk},") + lines.append("};") + lines.append("") + # Keep the linker location via section name; address is in memory_flash. + _ = addr + with open(path, "w", encoding="utf-8") as f: + f.write("\n".join(lines) + "\n") + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--sdk", required=True) + ap.add_argument("--objcopy", required=True) + ap.add_argument("--out-dir", required=True) + ap.add_argument("--check", action="store_true", + help="run TI seccfgChecker.py on the result") + args = ap.parse_args() + + default_out = os.path.join( + args.sdk, "source/defseccfgbin/default_seccfg_bankmode_0_ssumode1.out") + if not os.path.isfile(default_out): + print(f"missing {default_out}", file=sys.stderr) + return 1 + + table = load_crc_table(args.sdk) + tmp = os.path.join(args.out_dir, "_extract") + bases = extract_defaults(args.objcopy, default_out, tmp) + + os.makedirs(args.out_dir, exist_ok=True) + # BANKMODE0 NonMain SECCFG placements (TI default map) + addrs = {1: 0x10D85000, 2: 0x10D85800, 3: 0x10D8D000, 4: 0x10D8D800} + bins = {} + for cpu, base in bases.items(): + data = make_mode2(base, table) if cpu == 1 else make_mode2(base, table) + # CPU2/3/4: same MODE2 + APR rewrite keeps checker happy; CPU2 + # runtime APRs are still local to that core's AP block. + bins[cpu] = data + bin_path = os.path.join(args.out_dir, f"seccfg_cpu{cpu}.bin") + open(bin_path, "wb").write(data) + emit_c_blob( + os.path.join(args.out_dir, f"seccfg_cpu{cpu}.c"), + f"ulmk_c29_seccfg_cpu{cpu}", + f".TI.bound:CPU{cpu}_Cfg", + data, + addrs[cpu], + ) + print(f"wrote CPU{cpu} MODE={SSUMODE_2:#x} CRC=" + f"{struct.unpack_from(' list[str]: return [] +def _ti_mount() -> list[str]: + """Mount the TI installation read-only at /ti when TI_INSTALL_ROOT is set.""" + ti_root = os.environ.get("TI_INSTALL_ROOT", "") + if ti_root and Path(ti_root).is_dir(): + return ["--volume", f"{ti_root}:/ti:ro"] + return [] + + def _component_scan_dirs(board_host: Path | None = None) -> list[Path]: """Return directories that may contain ulmk_component_register().""" dirs: list[Path] = [] @@ -364,6 +372,8 @@ def _board_arch(board: dict) -> str: def _toolchain_for_arch(arch: str) -> str: + if arch == "c29": + return "/workspace/cmake/toolchain-c29-ticlang.cmake" return f"/workspace/cmake/toolchain-{arch}-gcc.cmake" @@ -373,11 +383,13 @@ def _build_subdir(arch: str, board_name: str) -> str: return f"ulipe-{arch}-{board_name}" -def _qemu_binary(arch: str) -> str: +def _qemu_binary(arch: str) -> str | None: if arch == "riscv": return QEMU_RISCV if arch == "arm": return QEMU_ARM + if arch == "c29": + return None # C29 has no QEMU emulator; use DSS/XDS110 HIL return QEMU_TRICORE @@ -399,18 +411,31 @@ def _resolve_board_path(board_arg: str | None) -> tuple[Path, str, list[str]]: return board_host, board_container, extra_mounts +def _c29_container_path() -> str: + """Extend CONTAINER_PATH with the TI C29 toolchain bin when mounted.""" + return ( + CONTAINER_PATH + + '; if [ -d /ti ]; then' + + ' _TI_CGT=$(ls -d /ti/ccs*/ccs/tools/compiler/ti-cgt-c29* 2>/dev/null' + + ' || ls -d /ti/tools/compiler/ti-cgt-c29* 2>/dev/null | head -1);' + + ' [ -n "$_TI_CGT" ] && export PATH="$_TI_CGT/bin:${PATH}"; fi' + ) + + def _build_shell(board_container: str, board: dict, clean: bool, run_qemu: bool, component_flags: list[str], build_subdir: str, optimize_size: bool = False, - enable_smp: bool = False) -> str: + enable_smp: bool = False, + flash: bool = False) -> str: arch = _board_arch(board) toolchain = _toolchain_for_arch(arch) qemu = _qemu_binary(arch) + path_export = _c29_container_path() if arch == "c29" else CONTAINER_PATH lines: list[str] = [ "set -e", - CONTAINER_PATH, + path_export, "", ] @@ -431,6 +456,8 @@ def _build_shell(board_container: str, board: dict, cfg.append(" -DULMK_OPTIMIZE_SIZE=ON \\") if enable_smp: cfg.append(" -DULMK_CONFIG_ENABLE_SMP=1 \\") + if flash: + cfg.append(" -DULMK_C29_FLASH=1 \\") for flag in component_flags: cfg.append(f" {flag} \\") cfg += [ @@ -447,7 +474,7 @@ def _build_shell(board_container: str, board: dict, f"echo 'Build OK → /build/{build_subdir}/ulmk'", ] - if run_qemu: + if run_qemu and qemu is not None: machine = board.get("UL_BOARD_QEMU_MACHINE", "") if isinstance(machine, list): machine = machine[0] if machine else "" @@ -563,6 +590,11 @@ def _run_build(args: argparse.Namespace) -> None: qemu_machine = board.get("UL_BOARD_QEMU_MACHINE", "") if isinstance(qemu_machine, list): qemu_machine = qemu_machine[0] if qemu_machine else "" + if run_qemu and arch == "c29": + sys.exit( + "error: C29 has no QEMU emulator — use 'build' then flash " + "via DSS/XDS110 HIL for silicon validation" + ) if run_qemu and not qemu_machine: sys.exit( "error: board does not support QEMU " @@ -589,13 +621,13 @@ def _run_build(args: argparse.Namespace) -> None: shell_cmd = _build_shell( board_container, board, args.clean, run_qemu, component_flags, build_subdir, getattr(args, "optimize_size", False), - enable_smp) + enable_smp, bool(getattr(args, "flash", False))) cmd = [ "docker", "run", "--rm", "--volume", f"{WORKSPACE_ROOT}:/workspace", "--volume", f"{BUILD_DIR}:/build", - ] + extra_mounts + _apps_mount() + ] + extra_mounts + _apps_mount() + _ti_mount() if run_qemu and sys.stdout.isatty(): cmd += ["--interactive", "--tty"] @@ -1015,6 +1047,11 @@ def _parse_args() -> argparse.Namespace: action="store_true", help="Build with ULMK_CONFIG_ENABLE_SMP=1 (requires board NUM_CPU>1)", ) + build_p.add_argument( + "--flash", + action="store_true", + help="C29: link the FLASH profile (code XIP from flash, POR boot)", + ) build_p.add_argument( "--component", diff --git a/tools/sdk_build.sh b/tools/sdk_build.sh index b284038..ae6a4ba 100755 --- a/tools/sdk_build.sh +++ b/tools/sdk_build.sh @@ -62,7 +62,22 @@ WORKSPACE="$(cd "$(dirname "$0")/.." && pwd)" export PATH="/opt/qemu-tricore/bin:/opt/tricore-gcc-bin:/opt/riscv-gcc-bin:/opt/arm-gcc-bin:${PATH}" -TAG="${ARCH}_${BOARD_NAME}_gcc" +if [ "$ARCH" = "c29" ]; then + # Discover TI C29 toolchain when mounted at /ti (see tools/dev.py _ti_mount). + _TI_CGT=$(ls -d /ti/ccs*/ccs/tools/compiler/ti-cgt-c29* 2>/dev/null \ + || ls -d /ti/tools/compiler/ti-cgt-c29* 2>/dev/null | head -1 || true) + if [ -n "$_TI_CGT" ]; then + export PATH="$_TI_CGT/bin:${PATH}" + fi +fi + +if [ "$ARCH" = "c29" ]; then + COMPILER_TAG="ticlang" +else + COMPILER_TAG="gcc" +fi + +TAG="${ARCH}_${BOARD_NAME}_${COMPILER_TAG}" if [ "$ENABLE_SMP" -eq 1 ]; then TAG="${TAG}_smp" fi @@ -71,7 +86,12 @@ if [ "$ENABLE_IRQ_ATTACH" -eq 1 ]; then fi KERNEL_A="ulmk_kernel_${TAG}.a" BOARD_A="ulmk_board_${TAG}.a" -LD="linker_${TAG}.ld" +# TI C29 uses .cmd; GNU arches use .ld. +if [ "$ARCH" = "c29" ]; then + LD="linker_${TAG}.cmd" +else + LD="linker_${TAG}.ld" +fi if [ "$CLEAN" -eq 1 ]; then echo "--- clean ---" @@ -114,8 +134,14 @@ cp "$BUILD_DIR/libulmk_board.a" "$OUT_DIR/lib/$BOARD_A" # The processed linker script selects kernel sections by archive name; # rewrite it to match the shipped archive filename. -sed "s/libulmk_kernel\.a/$KERNEL_A/g" \ - "$BUILD_DIR/generated/ulmk.ld" > "$OUT_DIR/linker/$LD" +if [ "$ARCH" = "c29" ]; then + # TI .cmd: archive references use (...) syntax. + sed "s//<${KERNEL_A}>/g" \ + "$BUILD_DIR/generated/ulmk.ld" > "$OUT_DIR/linker/$LD" +else + sed "s/libulmk_kernel\.a/$KERNEL_A/g" \ + "$BUILD_DIR/generated/ulmk.ld" > "$OUT_DIR/linker/$LD" +fi # Public microkernel API headers. cp "$WORKSPACE"/include/ulmk/*.h "$OUT_DIR/include/ulmk/" @@ -132,10 +158,15 @@ for h in "$CHIP_DIR"/*.h; do done echo "--- verify SDK ---" -ar t "$OUT_DIR/lib/$KERNEL_A" >/dev/null -ar t "$OUT_DIR/lib/$BOARD_A" >/dev/null -grep -q "$KERNEL_A" "$OUT_DIR/linker/$LD" -! grep -q "libulmk_kernel\.a" "$OUT_DIR/linker/$LD" +if [ "$ARCH" = "c29" ]; then + c29ar t "$OUT_DIR/lib/$KERNEL_A" >/dev/null + c29ar t "$OUT_DIR/lib/$BOARD_A" >/dev/null +else + ar t "$OUT_DIR/lib/$KERNEL_A" >/dev/null + ar t "$OUT_DIR/lib/$BOARD_A" >/dev/null + grep -q "$KERNEL_A" "$OUT_DIR/linker/$LD" + ! grep -q "libulmk_kernel\.a" "$OUT_DIR/linker/$LD" +fi test -f "$OUT_DIR/include/ulmk/microkernel.h" test -f "$OUT_DIR/include/ulmk_syscall_abi.h"