Bare-metal startup

SkillDev tools

Use when writing reset-to-main startup code, vector tables, VTOR, .data/.bss init, stack setup, startup.s, or crt0 for Cortex-M/RISC-V. Not for the bootloader jump: use bootloaders-embedded.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Bare-metal startup skill

What this skill tells your AI

The instructions your AI receives, as published by outlinedriven/outline-driven-development in .devin/skills/baremetal-startup/SKILL.md and read by ahel’s review.

Contract

FieldBound contract
TriggerFirmware never reaches main(), a new MCU is being brought up without a vendor HAL, or a custom startup.s, Reset_Handler, or crt0 is being written or debugged on Cortex-M or RISC-V.
AuthorityRead-only: emits startup code, linker symbol requirements, and diagnostics to chat; the user places them in the project. Rollback is not needed because no file is written. No remote mutation.
Side effectChat output only.
DoneThe startup path from the reset vector to main() is stated step by step, the vector table layout and the .data and .bss loops are given for the target core, and every linker symbol the code uses is listed with the linker script line that defines it.

Inputs

  • Target core (Cortex-M0+, M3, M4F, M7, or RISC-V RV32 or RV64) and the vendor part.
  • The linker script, or at least its MEMORY regions and the symbols it exports.
  • Whether the firmware boots directly from flash or is launched by a bootloader at an offset.
  • Which C runtime is linked (newlib, newlib-nano, none) and whether C++ static constructors exist.

Procedure

  1. State the boot sequence for the core before writing code. On Cortex-M the hardware loads SP from vector 0 and branches to vector 1, Reset_Handler. On RISC-V the core starts at the reset address with sp undefined, so the first instruction sets it. Done when: the sequence is written as an ordered list from power-on to main().

    Cortex-M vector table head:

    IndexEntry
    0Initial stack pointer (_estack)
    1Reset_Handler
    2NMI_Handler
    3HardFault_Handler
    4 and upFault handlers, then device IRQ handlers in NVIC order
  2. Write the vector table and Reset_Handler for Cortex-M. Copy .data from its load address in flash to its run address in RAM, zero .bss, then call SystemInit and main. Compare addresses with an unsigned branch (bcc), because addresses are unsigned. Done when: the assembly assembles for the target and the three symbol pairs come from the linker script.

    .syntax unified
    .thumb
    
    .section .isr_vector,"a",%progbits
    .global g_pfnVectors
    g_pfnVectors:
        .word _estack
        .word Reset_Handler
        .word NMI_Handler
        .word HardFault_Handler
        /* remaining fault and device IRQ vectors from the vendor CMSIS header order */
    
    .section .text.Reset_Handler
    .thumb_func
    .global Reset_Handler
    Reset_Handler:
        ldr r0, =_sidata        /* .data load address in flash */
        ldr r1, =_sdata         /* .data run address in RAM */
        ldr r2, =_edata
        b   copy_check
    copy_data:
        ldr r3, [r0], #4
        str r3, [r1], #4
    copy_check:
        cmp r1, r2
        bcc copy_data
        ldr r2, =_sbss
        ldr r4, =_ebss
        movs r3, #0
        b   zero_check
    zero_bss:
        str r3, [r2], #4
    zero_check:
        cmp r2, r4
        bcc zero_bss
        bl  SystemInit
        bl  main
        b   .
    
  3. Define the linker symbols the startup code names. _estack is the top of RAM, _sidata is the load address of .data, and _sdata, _edata, _sbss, _ebss bracket the sections. Done when: each symbol appears in the linker script and .data carries AT> FLASH so its load and run addresses differ.

    _estack = ORIGIN(RAM) + LENGTH(RAM);
    _sidata = LOADADDR(.data);
    
  4. Relocate the vector table when the image does not sit at the default boot address. Write SCB->VTOR, then __DSB() and __ISB(). The table base must be aligned to its own size rounded up to a power of two, with a minimum of 128 bytes on ARMv7-M; the mask below is the ARMv7-M TBLOFF field and a table with many vectors needs coarser alignment. Done when: VTOR holds the address of this image's g_pfnVectors and an interrupt reaches this image's handler.

    void relocate_vector_table(uint32_t base)
    {
        SCB->VTOR = base & 0xFFFFFF80U;   /* ARMv7-M TBLOFF field; align to table size */
        __DSB();
        __ISB();
    }
    
  5. Write the RISC-V entry when the target is RISC-V. Set sp, zero .bss with sw on RV32 or sd on RV64, set mtvec to the trap handler, and call main. Done when: the entry assembles for the target XLEN and mtvec points at a valid handler before interrupts are enabled.

    .section .text.entry
    .global _start
    _start:
        la sp, _stack_top
        la t0, _bss_start
        la t1, _bss_end
    clear_bss:
        beq t0, t1, bss_done
        sw zero, 0(t0)      /* sd on RV64 */
        addi t0, t0, 4      /* 8 on RV64 */
        j clear_bss
    bss_done:
        la t0, trap_handler
        csrw mtvec, t0
        call main
        j .
    
  6. Assign the remaining crt0 duties and confirm who owns each. Done when: every row below has an owner in the project.

    TaskOwner
    Copy .data from flash to RAMReset_Handler
    Zero .bssReset_Handler
    Initial stackVector 0 on Cortex-M; explicit sp load on RISC-V
    Heap (_sbrk)Optional; newlib syscall or a custom allocator
    C++ static constructors__libc_init_array() from newlib, called before main
    FPU enable on Cortex-M4F and M7SystemInit, by writing SCB->CPACR to grant CP10 and CP11 full access before any float instruction
  7. Keep main from returning. The b . after bl main catches a return; the body of main normally ends in a loop or __WFI(). Done when: a return from main lands in a known loop and not in the next bytes of flash.

  8. Verify on the target: halt at Reset_Handler with the debugger, step the copy and zero loops, and inspect a known initialized global and a known zero global at the first line of main. Done when: both globals hold their expected values.

Failure and recovery

SymptomCauseFix
HardFault on the first instructionVector 0 does not point into RAMSet _estack to ORIGIN(RAM) + LENGTH(RAM).
Initialized globals read as garbage.data never copiedAdd the copy loop and define _sidata with LOADADDR(.data).
Zero-initialized globals are non-zero.bss never zeroedAdd the zero loop over _sbss to _ebss.
Interrupts run the wrong handler or faultVTOR points at another image's tableWrite SCB->VTOR to this image's table and issue __DSB(); __ISB();.
Crash on the first float instructionFPU not enabled on M4F or M7Set CP10 and CP11 in SCB->CPACR in SystemInit.
C++ constructor crashes before mainConstructors ran before clocks or peripherals were readyCall SystemInit before __libc_init_array.
Execution falls off the end of mainNo loop after mainLoop or __WFI() in main; keep b . after the call.

Output

Startup code for the target core (vector table, Reset_Handler or _start, .data and .bss loops), the list of linker symbols it requires with their defining lines, the VTOR relocation routine when the image is offset, and a debugger checklist for confirming globals at the first line of main.

Signals

GitHub stars
52
Forks
9
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
baremetal-startup
Source
github.com/outlinedriven/outline-driven-development