1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
|
Eclipse Foundation's RTOS, ThreadX for RISC-V32
Using the GNU Tools
1. Building the ThreadX run-time Library
Prerequisites
- Install a RISC-V32 bare-metal GNU toolchain with riscv32-unknown-elf prefix
- Common source: https://github.com/riscv-collab/riscv-gnu-toolchain
Verify the toolchain:
riscv32-unknown-elf-gcc --version
riscv32-unknown-elf-objdump --version
CMake-based build (recommended)
From the ThreadX top-level directory:
cmake -Bbuild -GNinja -DCMAKE_TOOLCHAIN_FILE=cmake/riscv32_gnu.cmake .
cmake --build ./build/
This uses cmake/riscv32_gnu.cmake and ports/risc-v32/gnu/CMakeLists.txt to
configure the cross-compiler flags and produce the ThreadX run-time library
and example binaries.
Example build script
The example demonstration contains a build script. See:
ports/risc-v32/gnu/example_build/qemu_virt/build_libthreadx.sh
This script builds the library and the demo application kernel.elf.
2. Demonstration System (QEMU)
The provided example is targeted at QEMU's virt platform. After building the
example, the produced kernel.elf can be executed in QEMU:
qemu-system-riscv32 -nographic -smp 1 -bios none -m 128M -machine virt -kernel kernel.elf
Typical QEMU features used:
- Single-core CPU
- UART serial console
- PLIC (Platform-Level Interrupt Controller)
- CLINT (Core-Local Interruptor) for timer
3. System Initialization
Entry Point
The example startup code begins at the _start label in entry.s. This startup
code performs hardware initialization including:
- Check hart ID (only hart 0 continues; others enter WFI loop)
- Zero general-purpose registers
- Set up initial stack pointer
- Clear BSS section
- Jump to main()
Low-Level Port Initialization (tx_initialize_low_level.S)
The _tx_initialize_low_level function:
- Saves the system stack pointer to _tx_thread_system_stack_ptr
- Records first free RAM address from __tx_free_memory_start symbol
- Initializes floating-point control/status register (FCSR) if floating point enabled
Board Initialization (board.c)
After tx_initialize_low_level returns, main() calls board_init() to:
- Initialize PLIC (Platform-Level Interrupt Controller)
- Initialize UART
- Initialize hardware timer (CLINT)
- Set trap vector (mtvec) to point to trap handler
4. Register Usage and Stack Frames
The RISC-V32 ABI defines t0-t6 and a0-a7 as caller-saved (scratch) registers.
All other registers used by a function must be preserved by the function.
ThreadX takes advantage of this: when a context switch happens during a
function call, only the non-scratch registers need to be saved.
Stack Frame Types
Two types of stack frames exist:
A. Interrupt Frame (stack type = 1)
Created when an interrupt occurs during thread execution.
Saves all registers including caller-saved registers.
Size: 65*4 = 260 bytes (with FP), or 32*4 = 128 bytes (without FP)
B. Solicited Frame (stack type = 0)
Created when a thread voluntarily yields via ThreadX service calls.
Saves only callee-saved registers (s0-s11) and mstatus.
Size: 29*4 = 116 bytes (with FP), or 16*4 = 64 bytes (without FP)
Stack Layout for Interrupt Frame (with FP enabled):
Index Offset Register Description
─────────────────────────────────────────────────
0 0x00 -- Stack type (1 = interrupt)
1 0x04 s11 Preserved register
2 0x08 s10 Preserved register
3 0x0C s9 Preserved register
4 0x10 s8 Preserved register
5 0x14 s7 Preserved register
6 0x18 s6 Preserved register
7 0x1C s5 Preserved register
8 0x20 s4 Preserved register
9 0x24 s3 Preserved register
10 0x28 s2 Preserved register
11 0x2C s1 Preserved register
12 0x30 s0 Preserved register
13 0x34 t6 Scratch register
14 0x38 t5 Scratch register
15 0x3C t4 Scratch register
16 0x40 t3 Scratch register
17 0x44 t2 Scratch register
18 0x48 t1 Scratch register
19 0x4C t0 Scratch register
20 0x50 a7 Argument register
21 0x54 a6 Argument register
22 0x58 a5 Argument register
23 0x5C a4 Argument register
24 0x60 a3 Argument register
25 0x64 a2 Argument register
26 0x68 a1 Argument register
27 0x6C a0 Argument register
28 0x70 ra Return address
29 0x74 -- Reserved
30 0x78 mepc Machine exception PC
31-46 0x7C-0xB8 fs0-fs7 Preserved FP registers*
47-62 0xBC-0xF8 ft0-ft11 Scratch FP registers*
63 0xFC fcsr FP control/status register
─────────────────────────────────────────────────
*Note: In ilp32d ABI, FP registers are 8 bytes each, but current
port implementation uses 4-byte indexing which may cause
overlap if fsd/fld are used.
5. Interrupt Handling
Machine Mode Operation
ThreadX operates in machine mode (M-mode), the highest privilege level.
All interrupts and exceptions trap to machine mode.
Interrupt Sources
1. Machine Timer Interrupt (MTI):
- Triggered by CLINT when mtime >= mtimecmp
- Handled by _tx_timer_interrupt (src/tx_timer_interrupt.S)
- Called from trap handler in trap.c
2. External Interrupts (MEI):
- Routed through PLIC
- Handler in trap.c calls registered ISR callbacks
3. Software Interrupts (MSI):
- Supported but not actively used in this port
Interrupt Flow
1. Hardware trap entry (automatic):
- mepc <- PC (address of interrupted instruction)
- mcause <- exception/interrupt code
- mstatus.MPIE <- mstatus.MIE (save interrupt-enable state)
- mstatus.MIE <- 0 (disable interrupts)
- mstatus.MPP <- Machine mode
- PC <- mtvec (points to trap_entry in entry.s)
2. Trap entry (entry.s):
- Allocates interrupt stack frame (32*4 or 65*4 bytes depending on FP)
- Saves RA (x1) on stack
- Calls _tx_thread_context_save
3. Context save (_tx_thread_context_save.S):
- Increments _tx_thread_system_state (nested interrupt counter)
- If nested interrupt: saves remaining registers and returns to ISR
- If first interrupt: saves full context, switches to system stack
4. Trap handler (trap.c):
- Examines mcause to determine interrupt type
- Dispatches to appropriate handler (_tx_timer_interrupt or PLIC handler)
- Returns to context restore
5. Context restore (_tx_thread_context_restore.S):
- Decrements _tx_thread_system_state
- Checks if preemption needed
- Restores thread context or switches to next ready thread via scheduler
- Returns to interrupted thread or executes new thread
Interrupt Control Macros
TX_DISABLE and TX_RESTORE macros atomically manage the MIE bit in mstatus:
TX_DISABLE: Saves and clears MIE bit via csrrci (CSR read-clear immediate)
TX_RESTORE: Restores only MIE bit via csrrs (CSR read-set)
Other mstatus bits remain unchanged
These are defined in ports/risc-v32/gnu/inc/tx_port.h and use the
_tx_thread_interrupt_control() function.
6. Thread Scheduling and Context Switching
Thread Scheduler (src/tx_thread_schedule.S)
The scheduler:
1. Enables interrupts while waiting for next thread
2. Spins until _tx_thread_execute_ptr becomes non-NULL
3. Disables interrupts (critical section)
4. Sets _tx_thread_current_ptr = _tx_thread_execute_ptr
5. Increments thread's run count
6. Switches to thread's stack
7. Determines stack frame type and restores context:
- Interrupt frame: full context restored, returns via mret
- Solicited frame: minimal context restored, returns via ret
Initial Thread Stack Frame (src/tx_thread_stack_build.S)
New threads start with a fake interrupt frame containing:
- All registers initialized to 0
- ra (x1) = 0
- mepc = entry function pointer
- Stack type = 1 (interrupt frame)
- Floating-point registers initialized based on ABI
7. Port Configuration and Macros
Default Configurations (in ports/risc-v32/gnu/inc/tx_port.h):
TX_MINIMUM_STACK 1024 /* Minimum thread stack size */
TX_TIMER_THREAD_STACK_SIZE 1024 /* Timer thread stack size */
TX_TIMER_THREAD_PRIORITY 0 /* Timer thread priority */
TX_MAX_PRIORITIES 32 /* Must be multiple of 32 */
These can be overridden in tx_user.h or on the compiler command line.
8. Build Configuration
CMake Toolchain File: cmake/riscv32_gnu.cmake
Compiler Flags:
-march=rv32gc RV32 with IMAFD+C extensions
-mabi=ilp32d 32-bit integers/pointers, double-precision FP in registers
-mcmodel=medany ±2GB addressability
-D__ASSEMBLER__ For assembly files
ABI Selection
The port uses ilp32d ABI which includes:
- 32-bit integers and pointers
- Double-precision floating-point arguments in registers
- Floating-point registers f0-f31
When building with floating-point ABI:
- FP registers and FCSR are saved/restored in context switches
- Stack frames expand from 32*REGBYTES to 65*REGBYTES
- Conditional compilation uses __riscv_float_abi_double / __riscv_float_abi_single
9. File Organization
Port-specific files (ports/risc-v32/gnu/):
Core assembly files (src/):
- tx_initialize_low_level.S Initial setup and system state
- tx_thread_context_save.S Save context on interrupt entry
- tx_thread_context_restore.S Restore context on interrupt exit
- tx_thread_schedule.S Thread scheduler
- tx_thread_system_return.S Solicited context save for voluntary yield
- tx_thread_stack_build.S Build initial stack frame for new thread
- tx_thread_interrupt_control.S Interrupt enable/disable control
- tx_timer_interrupt.S Timer interrupt handler
Header file (inc/):
- tx_port.h Port-specific defines and macros
Example files (example_build/qemu_virt/):
- entry.s Startup code, trap entry point
- board.c, uart.c, hwtimer.c Platform-specific initialization
- plic.c PLIC interrupt controller driver
- trap.c Trap/exception dispatcher
- link.lds Linker script for QEMU virt
- build_libthreadx.sh Build script
10. Linker Script Requirements
The linker script must provide:
1. Entry point:
ENTRY(_start)
2. Memory layout:
- .text section (code)
- .rodata section (read-only data)
- .data section (initialized data)
- .bss section (uninitialized data)
3. Symbols:
- _end: First free memory address (used by ThreadX allocation)
- _bss_start, _bss_end: For zero initialization
- Initial stack space (example: 4KB)
4. Alignment:
- 16-byte alignment throughout (RISC-V requirement)
Example from QEMU virt build:
SECTIONS
{
. = 0x80000000; /* QEMU virt base address */
.text : { *(.text .text.*) }
.rodata : { *(.rodata .rodata.*) }
.data : { *(.data .data.*) }
.bss : { *(.bss .bss.*) }
.stack : {
. = ALIGN(4096);
_sysstack_start = .;
. += 0x1000; /* 4KB initial stack */
_sysstack_end = .;
}
PROVIDE(_end = .);
}
11. Floating-Point Support
When building with ilp32d ABI and FP enabled:
- FP registers f0-f31 and FCSR are saved/restored during context switches
- Stack frames increase from 32*REGBYTES to 65*REGBYTES (128 to 260 bytes)
- MSTATUS.FS (floating-point state) field is set to indicate dirty FP state
Stack frame differences:
- Without FP: 32*4 = 128 bytes (interrupt), 16*4 = 64 bytes (solicited)
- With FP: 65*4 = 260 bytes (interrupt), 29*4 = 116 bytes (solicited)
12. Performance and Debugging
Performance Optimization
Build optimizations:
- Use -O2 or -O3 for production (example uses -O0 for debugging)
- Enable -Wl,--gc-sections to remove unused code
- Define TX_DISABLE_ERROR_CHECKING to remove parameter checks
- Consider -flto for link-time optimization
Debugging with QEMU and GDB
Start QEMU in debug mode:
qemu-system-riscv32 -nographic -smp 1 -bios none -m 128M \
-machine virt -kernel kernel.elf -s -S
-s: Enable GDB server on TCP port 1234
-S: Pause at startup waiting for GDB
Connect GDB:
riscv32-unknown-elf-gdb kernel.elf
(gdb) target remote :1234
(gdb) break main
(gdb) continue
Useful GDB commands:
(gdb) info registers # View general registers
(gdb) info all-registers # Include CSR and FP registers
(gdb) p/x $mstatus # View machine status register
(gdb) x/32xw $sp # Examine stack memory
(gdb) p *_tx_thread_current_ptr # View current thread control block
13. Platform-Specific Notes (QEMU virt)
PLIC Configuration
The PLIC (Platform-Level Interrupt Controller) is memory-mapped at 0x0C000000:
- Enables up to 1024 interrupt sources
- Supports priority levels 0-7 (0 = disabled)
- Requires per-hart priority threshold and enable register configuration
Example PLIC usage (from plic.c):
plic_irq_enable(irq_number); # Enable specific interrupt
plic_prio_set(irq_number, priority);# Set priority level
CLINT Configuration
The CLINT (Core-Local Interruptor) is memory-mapped at 0x02000000:
- CLINT_MSIP(hartid): 0x0000 + 4*hartid (software interrupt)
- CLINT_MTIMECMP(hartid): 0x4000 + 8*hartid (timer compare)
- CLINT_MTIME: 0xBFF8 (timer value, read-only)
Timer frequency is platform-dependent (example uses 10MHz).
Multi-Core Considerations
The current port is single-core focused:
- Only hart 0 continues from reset; others enter WFI loop
- _tx_thread_system_state is a global variable
- No per-hart data structures
14. Revision History
For generic code revision information, refer to readme_threadx_generic.txt.
The following details the revision history for this RISC-V32 GNU port:
01-26-2026 Akif Ejaz Brief rewrite with accurate
technical details matching implementation,
register naming per RISC-V ABI, and
complete interrupt flow documentation
(Adapted from RISC-V64 port)
Copyright (c) 1996-2026 Microsoft Corporation
https://azure.com/rtos
|