/* * Revera — a real-time kernel with hardware-enforced isolation. * Copyright (c) 2026 VIACERTO LLC. All rights reserved. * * This file is dual-licensed under the PolyForm Noncommercial License 1.0.0 OR * a commercial license from VIACERTO LLC. You may choose to use this file under * the terms of either license. * * For the full license terms, see the LICENSE and LICENSE-COMMERCIAL.md files in * the root directory of this source tree. * * SPDX-FileCopyrightText: © 2026 VIACERTO LLC * SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 OR LicenseRef-VIACERTO-Commercial */ /* * threads/main.c — GETTING STARTED: bring up the kernel and run two threads. * * A first tour of the micro-kernel for someone who has never used it. It shows, in order: * 1. initialise the board hardware (tk_board_init) * 2. route a console to a UART (tk_console_init + tk_print) * 3. initialise the kernel (tk_kernel_init) * 4. start the millisecond clock (tk_time_init) * 5. create threads (tk_thread_create) * 6. hand control to the scheduler (tk_kernel_start) * ...plus how a thread finds its own identity, reads its argument, prints, and sleeps. * * ONE thread FUNCTION, TWO threads: both threads run the same worker(), each created with a different * argument. That makes the difference between an ARGUMENT and an IDENTITY concrete: * - the ARGUMENT (arg) is data YOU choose and pass in — here, the thread's print period in ms. * - the IDENTITY (tid) is assigned by the KERNEL (tk_thread_id) — a distinct number per thread that * you never chose. Two threads could even share the same argument yet still differ in tid. * * WHAT YOU SHOULD SEE: two lines repeating — one about every 100 ms, one about every 200 ms — with * DIFFERENT tid values, their timestamps (ms since start) advancing as the sleeps are honoured. * * A WORD ON SCHEDULING: it is cooperative on the host (a thread runs until it sleeps or yields) and * preemptive on the MCU ports (a timer interrupt can switch at any moment). Either way, * tk_sleep_ms() is what lets the OTHER thread run: it blocks this thread and gives the CPU away * (it is NOT a busy-wait). When every thread is asleep the kernel runs a built-in idle thread (a * low-power WFI on an MCU). * * make BOARD=host run * make BOARD=nrf52832_pca10040 flash * make BOARD=qemu_riscv32_virt run # Ctrl-A X to quit qemu */ #include /* uint32_t */ #include "tk_kernel.h" /* tk_kernel_* / tk_thread_* / tk_thread_self / tk_thread_id / TK_PRIO_* */ #include "tk_time.h" /* tk_time_init / tk_time_ms — milliseconds since tk_time_init() */ #include "tk_board.h" /* tk_board_init — clock + pins + UART */ #include "tk_console.h" /* tk_console_init / tk_print / tk_print_u32 (also enables the UART hdr) */ /* The argument we hand each thread: APPLICATION data of our choosing — here, the print period in ms. * `static` so it outlives the thread that reads it. Note this is NOT the thread's identity. */ static uint32_t period_fast = 100u; static uint32_t period_slow = 200u; /* Priority is a deliberate NUMBER, not a vague level: higher = higher priority, equal = round-robin. * We express it relative to the fixed anchor TK_PRIO_MIN (one step above the idle floor) rather than * reaching for a "normal" — a real system assigns each task a distinct, chosen value from its own * priority map. Both workers share ONE priority here so they round-robin; a higher number would let * one preempt the other. */ #define WORKER_PRIO (TK_PRIO_MIN + 1u) /* Every thread needs its OWN stack. Size it for the work the thread does: the context-switch frame, * plus how deep it calls, plus its locals. 1024 cells is comfortable here. * NOTE: do not size a working thread at ARCH_MIN_STACK_SIZE — that is only the bare context-switch * floor (enough to save/restore registers), far too small to call into (tk_print, etc.). */ static tk_stack_cell_t stack_fast[1024]; static tk_stack_cell_t stack_slow[1024]; /* One worker function, run by BOTH threads. It recovers its argument (the period we passed) and asks * the kernel who it is, then loops printing. It never returns — returning is how a thread would end. */ static void* worker(void* arg) { uint32_t period_ms = *(const uint32_t*)arg; /* the value main() passed us */ uint32_t tid = tk_thread_id(tk_thread_self()); /* our identity, assigned by the kernel */ for (uint32_t i = 0u; ; i++) { tk_print("[tid="); tk_print_u32(tid); /* kernel identity — differs between the two threads */ tk_print(" arg="); tk_print_u32(period_ms); /* the argument WE chose — here, our period in ms */ tk_print("] iter="); tk_print_u32(i); tk_print(" t="); tk_print_u32(tk_time_ms()); /* ms since tk_time_init() */ tk_print(" ms\n"); /* one '\n' — the console adds the CR for you */ tk_sleep_ms(period_ms); /* blocks us for period_ms; the other thread runs meanwhile */ } return 0; /* unreachable here; returning is how a thread would end */ } int main(void) { /* 1. Bring up the board: system clock, pins, and the UART. Do this first — nothing works before it. */ tk_board_init(); /* 2. Point the console at the board's debug UART, so tk_print() has somewhere to go. */ tk_console_init(TK_UART_CH_DEBUG); tk_print("\n[main] TK getting-started: two threads on one kernel\n"); /* 3. Initialise the kernel. This must come before creating any thread. */ tk_kernel_init(); /* 4. Zero the millisecond clock to "now", so tk_time_ms() reads time-since-here. */ tk_time_init(); /* 5. Create two threads FROM THE SAME worker(), each with a different argument (its period). Each * call takes: the entry function, the argument (a void* — here the address of a period), its * stack + size, and a priority (WORKER_PRIO above — the same for both, so they round-robin). * tk_thread_create returns a handle, or NULL if it failed — always check. */ tk_thread_t* t1 = tk_thread_create(worker, &period_fast, stack_fast, sizeof stack_fast, WORKER_PRIO); tk_thread_t* t2 = tk_thread_create(worker, &period_slow, stack_slow, sizeof stack_slow, WORKER_PRIO); if ((t1 == NULL) || (t2 == NULL)) { tk_print("[main] tk_thread_create() failed\n"); return -1; } /* 6. Start scheduling. This enters the kernel and does NOT return while threads run — on an MCU * main() effectively never returns; the scheduler runs forever. */ tk_kernel_start(); return 0; }