FreeRTOS on RISC-V: Diagnose a Scheduler That Starts but Never Advances Its Tick

A RISC-V migration can reach its first FreeRTOS task and still fail at the first timer interrupt. Correct-looking timer addresses do not establish that the compiled trap path recognizes that interrupt or advances the kernel tick.

FreeRTOS Kernel V11.3.1 makes this a concrete integration problem: C code selects timer initialization from address definitions, while assembly independently selects part of its interrupt dispatch through a chip-specific header. Diagnose those decisions together with the actual installed entry point. The procedures below are proposed checks, not results from an executed port.

Diagram contrasting C timer initialization, the assembly chip-header choice, conditional unified trap dispatch and the separately compiled dedicated timer entry.
Original integration diagram for the inspected FreeRTOS version. No kernel or target test was executed.

C initialization and assembly dispatch have separate inputs

In the versioned RISC-V port.c, the default vPortSetupTimerInterrupt() implementation is compiled when both configMTIME_BASE_ADDRESS and configMTIMECMP_BASE_ADDRESS are nonzero. It prepares a compare deadline and the state used for subsequent ticks. Under the same address condition, scheduler startup sets the machine timer and machine external interrupt-enable bits in mie.

That condition does not configure every assembly decision. portASM.S includes portContext.h, which selects freertos_risc_v_chip_specific_extensions.h through the assembler's include path. In the unified trap handler, recognition of machine timer interrupt cause 7 is conditional on portasmHAS_MTIME. The recognized path updates the compare register, calls xTaskIncrementTick() and switches context if the kernel requests it.

The supplied no-extensions header defines portasmHAS_MTIME as zero; the MTIME/CLINT header defines it as one. Their identical filenames make include-path selection significant. Setting addresses in FreeRTOSConfig.h does not prove that assembly selected the latter header.

Identify the installed trap entry before diagnosing the macro

Consider a hypothetical build using nonzero timer addresses but the no-MTIME header. If interrupts enter freertos_risc_v_trap_handler, its timer-specific dispatch is omitted. The interrupt falls through to freertos_risc_v_application_interrupt_handler. The default weak implementation reads diagnostic CSRs and loops indefinitely. An application override can behave differently, but it does not automatically become a correct kernel tick handler.

There is an important exception: the separately exported freertos_risc_v_mtimer_interrupt_handler directly updates the compare register and calls xTaskIncrementTick(). Its body is not guarded by portasmHAS_MTIME. A platform that routes timer interrupts directly to that entry therefore has a different dispatch contract.

Inspect the effective vector or trap wiring and final disassembly before applying the unified-handler diagnosis. A symbol present in the ELF does not identify which entry receives the hardware event. Conversely, finding portasmHAS_MTIME=0 alone does not prove that every possible timer entry omits the tick call.

Compare the two compiled decisions

Obtain the actual commands that compile port.c and preprocess/assemble portASM.S. Preserve their target flags, definitions and include order when producing diagnostic preprocessor output. Trace the chip-header path used by the assembly invocation, rather than opening whichever same-named header the editor finds first.

Then answer two specific questions: did C include the default timer setup, and did the selected interrupt route include the expected tick path? Inspect the preprocessed assembly and linked branch destinations together. This comparison is more decisive than changing a shared configuration header and assuming both compilation paths consumed it.

For a non-MTIME timer, the port's weak setup hook allows an application implementation. That replacement must configure the actual source and connect its interrupt to appropriate kernel tick handling. Supplying only a timer initializer leaves the interrupt side unresolved; selecting a dedicated timer entry without initializing the state it expects is also incomplete.

Distinguish an absent tick from a wrong tick period

Once dispatch reaches the kernel, check the time-base assumptions separately. This source computes its increment from configCPU_CLOCK_HZ / configTICK_RATE_HZ. Compare that assumption with the hardware timer's real input clock; an advancing counter at the wrong rate is a different failure from never reaching xTaskIncrementTick().

The default setup also derives a compare-register address from mhartid with an eight-byte stride. Confirm that layout against the SoC rather than treating it as a universal RISC-V register map. These are port assumptions, not architectural promises.

Use an observation independent of the delayed task to distinguish timer events, entry into the selected handler and progression of the kernel tick. Finally, exercise a finite task delay and verify its expected time scale. A passing result should establish that complete path for the chosen machine-mode port; an SBI supervisor-mode launch requires a different privilege integration, not merely different timer addresses.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *