Satoshi Yoneda 1 månad sedan
incheckning
296f91df9c

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+build
+!.vscode/*
+2.3.1/

+ 22 - 0
.vscode/c_cpp_properties.json

@@ -0,0 +1,22 @@
+{
+    "configurations": [
+        {
+            "name": "Pico",
+            "includePath": [
+                "${workspaceFolder}/**",
+                "${userHome}/.pico-sdk/sdk/2.3.1/**"
+            ],
+            "forcedInclude": [
+                "${workspaceFolder}/build/generated/pico_base/pico/config_autogen.h",
+                "${userHome}/.pico-sdk/sdk/2.3.1/src/common/pico_base_headers/include/pico.h"
+            ],
+            "defines": [],
+            "compilerPath": "${userHome}/.pico-sdk/toolchain/15_2_Rel1/bin/arm-none-eabi-gcc.exe",
+            "compileCommands": "${workspaceFolder}/build/compile_commands.json",
+            "cStandard": "c17",
+            "cppStandard": "c++14",
+            "intelliSenseMode": "linux-gcc-arm"
+        }
+    ],
+    "version": 4
+}

+ 15 - 0
.vscode/cmake-kits.json

@@ -0,0 +1,15 @@
+[
+    {
+        "name": "Pico",
+        "compilers": {
+            "C": "${command:raspberry-pi-pico.getCompilerPath}",
+            "CXX": "${command:raspberry-pi-pico.getCxxCompilerPath}"
+        },
+        "environmentVariables": {
+            "PATH": "${command:raspberry-pi-pico.getEnvPath};${env:PATH}"
+        },
+        "cmakeSettings": {
+            "Python3_EXECUTABLE": "${command:raspberry-pi-pico.getPythonPath}"
+        }
+    }
+]

+ 9 - 0
.vscode/extensions.json

@@ -0,0 +1,9 @@
+{
+    "recommendations": [
+        "marus25.cortex-debug",
+        "ms-vscode.cpptools",
+        "ms-vscode.cpptools-extension-pack",
+        "ms-vscode.vscode-serial-monitor",
+        "raspberry-pi.raspberry-pi-pico"
+    ]
+}

+ 52 - 0
.vscode/launch.json

@@ -0,0 +1,52 @@
+{
+    "version": "0.2.0",
+    "configurations": [
+        {
+            "name": "Pico Debug (Cortex-Debug)",
+            "cwd": "${userHome}/.pico-sdk/openocd/0.12.0+dev/scripts",
+            "executable": "${command:raspberry-pi-pico.launchTargetPath}",
+            "request": "launch",
+            "type": "cortex-debug",
+            "servertype": "openocd",
+            "serverpath": "${userHome}/.pico-sdk/openocd/0.12.0+dev/openocd.exe",
+            "gdbPath": "${command:raspberry-pi-pico.getGDBPath}",
+            "debuggerArgs": ["-ex", "set debug-file-directory /debug"],
+            "device": "${command:raspberry-pi-pico.getChipUppercase}",
+            "configFiles": [
+                "interface/cmsis-dap.cfg",
+                "target/${command:raspberry-pi-pico.getTarget}.cfg"
+            ],
+            "svdFile": "${userHome}/.pico-sdk/sdk/2.3.1/src/${command:raspberry-pi-pico.getChip}/hardware_regs/${command:raspberry-pi-pico.getChipUppercase}.svd",
+            "runToEntryPoint": "main",
+            // Fix for no_flash binaries, where monitor reset halt doesn't do what is expected
+            // Also works fine for flash binaries
+            "overrideLaunchCommands": [
+                "monitor reset init",
+                "load \"${command:raspberry-pi-pico.launchTargetPath}\""
+            ],
+            "openOCDLaunchCommands": [
+                "adapter speed 5000"
+            ]
+        },
+        {
+            "name": "Pico Debug (Cortex-Debug with external OpenOCD)",
+            "cwd": "${workspaceRoot}",
+            "executable": "${command:raspberry-pi-pico.launchTargetPath}",
+            "request": "launch",
+            "type": "cortex-debug",
+            "servertype": "external",
+            "gdbTarget": "localhost:3333",
+            "gdbPath": "${command:raspberry-pi-pico.getGDBPath}",
+            "debuggerArgs": ["-ex", "set debug-file-directory /debug"],
+            "device": "${command:raspberry-pi-pico.getChipUppercase}",
+            "svdFile": "${userHome}/.pico-sdk/sdk/2.3.1/src/${command:raspberry-pi-pico.getChip}/hardware_regs/${command:raspberry-pi-pico.getChipUppercase}.svd",
+            "runToEntryPoint": "main",
+            // Fix for no_flash binaries, where monitor reset halt doesn't do what is expected
+            // Also works fine for flash binaries
+            "overrideLaunchCommands": [
+                "monitor reset init",
+                "load \"${command:raspberry-pi-pico.launchTargetPath}\""
+            ]
+        }
+    ]
+}

+ 46 - 0
.vscode/settings.json

@@ -0,0 +1,46 @@
+{
+    "cmake.showSystemKits": false,
+    "cmake.options.statusBarVisibility": "hidden",
+    "cmake.options.advanced": {
+        "build": {
+            "statusBarVisibility": "hidden"
+        },
+        "launch": {
+            "statusBarVisibility": "hidden"
+        },
+        "debug": {
+            "statusBarVisibility": "hidden"
+        },
+        "variant": {
+            "statusBarVisibility": "hidden"
+        },
+        "buildTarget": {
+            "statusBarVisibility": "hidden"
+        }
+    },
+    "cmake.configureOnEdit": false,
+    "cmake.automaticReconfigure": false,
+    "cmake.configureOnOpen": false,
+    "cmake.generator": "Ninja",
+    "cmake.cmakePath": "${userHome}/.pico-sdk/cmake/v4.3.4/bin/cmake",
+    "C_Cpp.debugShortcut": false,
+    "terminal.integrated.env.windows": {
+        "PICO_SDK_PATH": "${env:USERPROFILE}/.pico-sdk/sdk/2.3.1",
+        "PICO_TOOLCHAIN_PATH": "${env:USERPROFILE}/.pico-sdk/toolchain/15_2_Rel1",
+        "Path": "${env:USERPROFILE}/.pico-sdk/toolchain/15_2_Rel1/bin;${env:USERPROFILE}/.pico-sdk/picotool/2.3.1/picotool;${env:USERPROFILE}/.pico-sdk/cmake/v4.3.4/bin;${env:USERPROFILE}/.pico-sdk/ninja/v1.13.2;${env:PATH}"
+    },
+    "terminal.integrated.env.osx": {
+        "PICO_SDK_PATH": "${env:HOME}/.pico-sdk/sdk/2.3.1",
+        "PICO_TOOLCHAIN_PATH": "${env:HOME}/.pico-sdk/toolchain/15_2_Rel1",
+        "PATH": "${env:HOME}/.pico-sdk/toolchain/15_2_Rel1/bin:${env:HOME}/.pico-sdk/picotool/2.3.1/picotool:${env:HOME}/.pico-sdk/cmake/v4.3.4/bin:${env:HOME}/.pico-sdk/ninja/v1.13.2:${env:PATH}"
+    },
+    "terminal.integrated.env.linux": {
+        "PICO_SDK_PATH": "${env:HOME}/.pico-sdk/sdk/2.3.1",
+        "PICO_TOOLCHAIN_PATH": "${env:HOME}/.pico-sdk/toolchain/15_2_Rel1",
+        "PATH": "${env:HOME}/.pico-sdk/toolchain/15_2_Rel1/bin:${env:HOME}/.pico-sdk/picotool/2.3.1/picotool:${env:HOME}/.pico-sdk/cmake/v4.3.4/bin:${env:HOME}/.pico-sdk/ninja/v1.13.2:${env:PATH}"
+    },
+    "raspberry-pi-pico.cmakeAutoConfigure": true,
+    "raspberry-pi-pico.useCmakeTools": false,
+    "raspberry-pi-pico.cmakePath": "${HOME}/.pico-sdk/cmake/v4.3.4/bin/cmake",
+    "raspberry-pi-pico.ninjaPath": "${HOME}/.pico-sdk/ninja/v1.13.2/ninja"
+}

+ 102 - 0
.vscode/tasks.json

@@ -0,0 +1,102 @@
+{
+    "version": "2.0.0",
+    "tasks": [
+        {
+            "label": "Compile Project",
+            "type": "process",
+            "isBuildCommand": true,
+            "command": "${userHome}/.pico-sdk/ninja/v1.13.2/ninja",
+            "args": ["-C", "${workspaceFolder}/build"],
+            "group": "build",
+            "presentation": {
+                "reveal": "always",
+                "panel": "dedicated"
+            },
+            "problemMatcher": "$gcc",
+            "windows": {
+                "command": "${env:USERPROFILE}/.pico-sdk/ninja/v1.13.2/ninja.exe"
+            }
+        },
+        {
+            "label": "Run Project",
+            "type": "process",
+            "command": "${env:HOME}/.pico-sdk/picotool/2.3.1/picotool/picotool",
+            "args": [
+                "load",
+                "${command:raspberry-pi-pico.launchTargetPath}",
+                "-fx"
+            ],
+            "presentation": {
+                "reveal": "always",
+                "panel": "dedicated"
+            },
+            "problemMatcher": [],
+            "windows": {
+                "command": "${env:USERPROFILE}/.pico-sdk/picotool/2.3.1/picotool/picotool.exe"
+            }
+        },
+        {
+            "label": "Flash",
+            "type": "process",
+            "command": "${userHome}/.pico-sdk/openocd/0.12.0+dev/openocd.exe",
+            "args": [
+                "-s",
+                "${userHome}/.pico-sdk/openocd/0.12.0+dev/scripts",
+                "-f",
+                "interface/cmsis-dap.cfg",
+                "-f",
+                "target/${command:raspberry-pi-pico.getTarget}.cfg",
+                "-c",
+                "adapter speed 5000; program \"${command:raspberry-pi-pico.launchTargetPath}\" verify reset exit"
+            ],
+            "problemMatcher": [],
+            "windows": {
+                "command": "${env:USERPROFILE}/.pico-sdk/openocd/0.12.0+dev/openocd.exe"
+            }
+        },
+        {
+            "label": "Rescue Reset",
+            "type": "process",
+            "command": "${userHome}/.pico-sdk/openocd/0.12.0+dev/openocd.exe",
+            "args": [
+                "-s",
+                "${userHome}/.pico-sdk/openocd/0.12.0+dev/scripts",
+                "-f",
+                "interface/cmsis-dap.cfg",
+                "-f",
+                "target/${command:raspberry-pi-pico.getChip}-rescue.cfg",
+                "-c",
+                "adapter speed 5000; reset halt; exit"
+            ],
+            "problemMatcher": [],
+            "windows": {
+                "command": "${env:USERPROFILE}/.pico-sdk/openocd/0.12.0+dev/openocd.exe"
+            }
+        },
+        {
+            "label": "RISC-V Reset (RP2350)",
+            "type": "process",
+            "command": "${userHome}/.pico-sdk/openocd/0.12.0+dev/openocd.exe",
+            "args": [
+                "-s",
+                "${userHome}/.pico-sdk/openocd/0.12.0+dev/scripts",
+                "-c",
+                "set USE_CORE { rv0 rv1 cm0 cm1 }",
+                "-f",
+                "interface/cmsis-dap.cfg",
+                "-f",
+                "target/rp2350.cfg",
+                "-c",
+                "adapter speed 5000; init;",
+                "-c",
+                "write_memory 0x40120158 8 { 0x3 }; echo [format \"Info : ARCHSEL 0x%02x\" [read_memory 0x40120158 8 1]];",
+                "-c",
+                "reset halt; targets rp2350.rv0; echo [format \"Info : ARCHSEL_STATUS 0x%02x\" [read_memory 0x4012015C 8 1]]; exit"
+            ],
+            "problemMatcher": [],
+            "windows": {
+                "command": "${env:USERPROFILE}/.pico-sdk/openocd/0.12.0+dev/openocd.exe"
+            }
+        }
+    ]
+}

+ 70 - 0
CMakeLists.txt

@@ -0,0 +1,70 @@
+# Generated Cmake Pico project file
+
+cmake_minimum_required(VERSION 3.13)
+
+set(CMAKE_C_STANDARD 11)
+set(CMAKE_CXX_STANDARD 17)
+set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
+
+# Initialise pico_sdk from installed location
+# (note this can come from environment, CMake cache etc)
+
+# == DO NOT EDIT THE FOLLOWING LINES for the Raspberry Pi Pico VS Code Extension to work ==
+if(WIN32)
+    set(USERHOME $ENV{USERPROFILE})
+else()
+    set(USERHOME $ENV{HOME})
+endif()
+set(sdkVersion 2.3.1)
+set(toolchainVersion 15_2_Rel1)
+set(picotoolVersion 2.3.1)
+set(picoVscode ${USERHOME}/.pico-sdk/cmake/pico-vscode.cmake)
+if (EXISTS ${picoVscode})
+    include(${picoVscode})
+endif()
+# ====================================================================================
+set(PICO_BOARD pico2 CACHE STRING "Board type")
+
+# Pull in Raspberry Pi Pico SDK (must be before project)
+include(pico_sdk_import.cmake)
+
+project(tinykernel C CXX ASM)
+
+# Initialise the Raspberry Pi Pico SDK
+pico_sdk_init()
+
+# Tinykernel: minimal preemptive task scheduler (Cortex-M33 / RP2350)
+
+add_library(tinykernel_lib STATIC
+        src/scheduler.cpp
+        src/port/m33/port_m33.c
+        src/port/m33/context.S
+)
+target_include_directories(tinykernel_lib PUBLIC
+        ${CMAKE_CURRENT_LIST_DIR}/include
+)
+target_link_libraries(tinykernel_lib PUBLIC pico_stdlib)
+
+# Add executable. Default name is the project name, version 0.1
+
+add_executable(tinykernel main.cpp )
+
+pico_set_program_name(tinykernel "tinykernel")
+pico_set_program_version(tinykernel "0.1")
+
+# Modify the below lines to enable/disable output over UART/USB
+pico_enable_stdio_uart(tinykernel 1)
+pico_enable_stdio_usb(tinykernel 0)
+
+# Add the standard library to the build
+target_link_libraries(tinykernel
+        pico_stdlib
+        tinykernel_lib)
+
+# Add the standard include files to the build
+target_include_directories(tinykernel PRIVATE
+        ${CMAKE_CURRENT_LIST_DIR}
+)
+
+pico_add_extra_outputs(tinykernel)
+

+ 74 - 0
include/tinykernel/kernel.hpp

@@ -0,0 +1,74 @@
+// tinykernel: minimal preemptive task scheduler for RP2350 (Pico 2, Cortex-M33)
+#pragma once
+
+#include <stdint.h>
+#include <stddef.h>
+
+#include "pico/runtime.h"  // PICO_RUNTIME_INIT_FUNC (static task registration)
+
+#ifndef TK_TICKS_PER_SEC
+#define TK_TICKS_PER_SEC 1000u
+#endif
+
+// Time slice per task at the same priority level, in milliseconds.
+#ifndef TK_TIME_SLICE_MS
+#define TK_TIME_SLICE_MS 10u
+#endif
+
+// Total number of task slots (slot 0 is reserved for the built-in idle task).
+#ifndef TK_MAX_TASKS
+#define TK_MAX_TASKS 16u
+#endif
+
+// Stack size per task, in bytes. Stacks come from a static pool (no malloc).
+#ifndef TK_DEFAULT_STACK_SIZE
+#define TK_DEFAULT_STACK_SIZE (4 * 1024)
+#endif
+
+namespace tk {
+
+using TaskFn = void (*)(void* arg);
+
+// Priority levels: numerically lower means higher priority. Level 15 is reserved for idle.
+enum Prio : uint32_t {
+    PRIO_REALTIME = 1,
+    PRIO_HIGH     = 5,
+    PRIO_NORMAL   = 9,
+    PRIO_LOW      = 13,
+};
+
+struct TaskHandle {
+    uint8_t slot;  // 0xFF on failure
+};
+
+// Create a task. Must be called before tk::start() (Phase 1 restriction).
+TaskHandle create(TaskFn fn, void* arg = nullptr, uint32_t prio = PRIO_NORMAL);
+
+// Start the scheduler and run tasks preemptively. Never returns.
+void start();
+
+// Task API: must be called from a task context.
+void yield();
+void sleep_ms(uint32_t ms);
+void exit(int code = 0);
+
+// Millisecond clock, resolution is one tick (1/TK_TICKS_PER_SEC s).
+uint64_t now_ms();
+
+// Simple critical section based on disabling interrupts.
+bool lock();
+void unlock(bool was_enabled);
+
+namespace detail {
+void static_register(const char* name, TaskFn fn, uint32_t prio);
+}  // namespace detail
+
+}  // namespace tk
+
+// Register a task at link time: just write the function and use this macro.
+// `fn` must be a plain (non-static) function named uniquely across the program.
+#define TK_TASK(fn, prio)                                              \
+    static void tk_register_##fn(void) {                              \
+        ::tk::detail::static_register(#fn, fn, (prio));               \
+    }                                                                 \
+    PICO_RUNTIME_INIT_FUNC(tk_register_##fn, "ZZ900");

+ 111 - 0
main.cpp

@@ -0,0 +1,111 @@
+#include <stdio.h>
+
+#include "hardware/gpio.h"
+#include "pico/stdlib.h"
+
+#include "tinykernel/kernel.hpp"
+
+// Atomic printf helper: stdio is shared, so wrap each call with the kernel lock.
+#define P(...)                                  \
+    do {                                       \
+        bool _lk = tk::lock();                 \
+        printf(__VA_ARGS__);                   \
+        tk::unlock(_lk);                       \
+    } while (0)
+
+// ---------------------------------------------------------------------------
+// Static task (registered at link time via TK_TASK): toggles the onboard LED.
+void task_led(void* arg) {
+    (void)arg;
+    for (;;) {
+        gpio_put(25, !gpio_get(25));
+        tk::sleep_ms(200);
+    }
+}
+TK_TASK(task_led, tk::PRIO_NORMAL)
+
+// ---------------------------------------------------------------------------
+// Static task: FPU self-check. Runs the same deterministic float/double sequence
+// twice; any corruption of FPU state across context switches shows up as a
+// mismatch or a drifting result.
+static float fpu_run_f(float v) {
+    for (int i = 0; i < 10000; ++i) v = v * 1.000001f + 0.5f;
+    return v;
+}
+
+static double fpu_run_d(double v) {
+    for (long i = 0; i < 10000; ++i) v = v * 1.0000000002 + 0.5;
+    return v;
+}
+
+void task_fpu(void* arg) {
+    (void)arg;
+    for (;;) {
+        float fa = fpu_run_f(1.5f);
+        float fb = fpu_run_f(1.5f);
+        double da = fpu_run_d(2.5);
+        double db = fpu_run_d(2.5);
+        if (fa != fb || da != db) {
+            P("FPU FAIL  f: %8.6f vs %8.6f   d: %.9f vs %.9f\n", (double)fa, (double)fb, da, db);
+        } else {
+            P("FPU ok    f: %8.6f   d: %.9f\n", (double)fa, da);
+        }
+        tk::sleep_ms(300);
+    }
+}
+TK_TASK(task_fpu, tk::PRIO_NORMAL)
+
+// ---------------------------------------------------------------------------
+// Dynamic task (higher priority): busy-spins for a few ms to demonstrate that it
+// preempts the lower-priority tasks immediately.
+static void task_hi(void* arg) {
+    (void)arg;
+    for (;;) {
+        volatile uint32_t n = 0;
+        while (n < 500000u) ++n;
+        P("HI   prio=%u busy-spin done, now=%llu ms\n", (unsigned)tk::PRIO_HIGH, (unsigned long long)tk::now_ms());
+        tk::sleep_ms(700);
+    }
+}
+
+// ---------------------------------------------------------------------------
+// Dynamic task: prints a few times and then exits.
+static void task_dying(void* arg) {
+    (void)arg;
+    for (int i = 1; i <= 3; ++i) {
+        P("DYING %d (now=%llu ms)\n", i, (unsigned long long)tk::now_ms());
+        tk::sleep_ms(250);
+    }
+    P("DYING calling tk::exit()\n");
+    tk::exit(0);
+}
+
+// ---------------------------------------------------------------------------
+// Dynamic task: plain periodic printer at normal priority.
+static void task_print(void* arg) {
+    (void)arg;
+    uint32_t count = 0;
+    for (;;) {
+        P("A    #%lu now=%llu ms\n", (unsigned long)count++, (unsigned long long)tk::now_ms());
+        tk::sleep_ms(150);
+    }
+}
+
+int main() {
+    stdio_init_all();
+    gpio_init(25);
+    gpio_set_dir(25, GPIO_OUT);
+
+    printf("tinykernel: registering dynamic tasks...\n");
+    auto h_hi = tk::create(task_hi, nullptr, tk::PRIO_HIGH);
+    auto h_dy = tk::create(task_dying, nullptr, tk::PRIO_LOW);
+    auto h_pr = tk::create(task_print, nullptr, tk::PRIO_NORMAL);
+    if (h_hi.slot == 0xFF || h_dy.slot == 0xFF || h_pr.slot == 0xFF) {
+        printf("tinykernel: task creation FAILED\n");
+        for (;;) {
+        }
+    }
+
+    printf("tinykernel: starting scheduler (preemptive, core M33 only)\n");
+    tk::start();  // never returns
+}

+ 121 - 0
pico_sdk_import.cmake

@@ -0,0 +1,121 @@
+# This is a copy of <PICO_SDK_PATH>/external/pico_sdk_import.cmake
+
+# This can be dropped into an external project to help locate this SDK
+# It should be include()ed prior to project()
+
+# Copyright 2020 (c) 2020 Raspberry Pi (Trading) Ltd.
+#
+# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+# following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+# disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+# disclaimer in the documentation and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products
+# derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH))
+    set(PICO_SDK_PATH $ENV{PICO_SDK_PATH})
+    message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')")
+endif ()
+
+if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT))
+    set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT})
+    message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')")
+endif ()
+
+if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH))
+    set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH})
+    message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')")
+endif ()
+
+if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_TAG} AND (NOT PICO_SDK_FETCH_FROM_GIT_TAG))
+    set(PICO_SDK_FETCH_FROM_GIT_TAG $ENV{PICO_SDK_FETCH_FROM_GIT_TAG})
+    message("Using PICO_SDK_FETCH_FROM_GIT_TAG from environment ('${PICO_SDK_FETCH_FROM_GIT_TAG}')")
+endif ()
+
+if (PICO_SDK_FETCH_FROM_GIT AND NOT PICO_SDK_FETCH_FROM_GIT_TAG)
+  set(PICO_SDK_FETCH_FROM_GIT_TAG "master")
+  message("Using master as default value for PICO_SDK_FETCH_FROM_GIT_TAG")
+endif()
+
+set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK")
+set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable")
+set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK")
+set(PICO_SDK_FETCH_FROM_GIT_TAG "${PICO_SDK_FETCH_FROM_GIT_TAG}" CACHE FILEPATH "release tag for SDK")
+
+if (NOT PICO_SDK_PATH)
+    if (PICO_SDK_FETCH_FROM_GIT)
+        include(FetchContent)
+        set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR})
+        if (PICO_SDK_FETCH_FROM_GIT_PATH)
+            get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}")
+        endif ()
+        FetchContent_Declare(
+                pico_sdk
+                GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
+                GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
+        )
+
+        if (NOT pico_sdk)
+            message("Downloading Raspberry Pi Pico SDK")
+            # GIT_SUBMODULES_RECURSE was added in 3.17
+            if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0")
+                FetchContent_Populate(
+                        pico_sdk
+                        QUIET
+                        GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
+                        GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
+                        GIT_SUBMODULES_RECURSE FALSE
+
+                        SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src
+                        BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build
+                        SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild
+                )
+            else ()
+                FetchContent_Populate(
+                        pico_sdk
+                        QUIET
+                        GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
+                        GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
+
+                        SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src
+                        BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build
+                        SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild
+                )
+            endif ()
+
+            set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR})
+        endif ()
+        set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE})
+    else ()
+        message(FATAL_ERROR
+                "SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git."
+                )
+    endif ()
+endif ()
+
+get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}")
+if (NOT EXISTS ${PICO_SDK_PATH})
+    message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found")
+endif ()
+
+set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake)
+if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE})
+    message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK")
+endif ()
+
+set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE)
+
+include(${PICO_SDK_INIT_CMAKE_FILE})

+ 175 - 0
src/port/m33/context.S

@@ -0,0 +1,175 @@
+// context.S - Cortex-M33 port for tinykernel (RP2350 / Pico 2)
+//
+// Stack architecture:
+//   - User tasks run in Thread mode using PSP (Process Stack Pointer).
+//   - Kernel, SysTick, and exceptions run in Handler mode using MSP (Main Stack Pointer).
+//
+// Task stack layout on PSP (low -> high addresses):
+//   [sp + 0  .. sp + 28] : R4-R11   (8 words pushed/popped by software in context.S)
+//   [sp + 32 .. sp + 60] : Core8    (R0-R3, R12, LR, PC, xPSR automatically saved/restored by hardware)
+//
+// FPU state (S0-S31) is moved between hardware registers and tk_fpu_scratch by vstm/vldm.
+// FPCCR.ASPEN and FPCCR.LSPEN are both disabled in port_m33.c, so hardware never pushes
+// VFP frames onto task stacks.
+
+    .syntax unified
+    .thumb
+
+    .extern tk_reschedule
+    .extern tk_fpu_scratch
+    .extern tk_task_return_trampoline
+    .global isr_systick
+    .global isr_svcall
+    .global tk_port_start
+    .global tk_port_make_frame
+
+// clobbers r3
+.macro save_fpu_to_scratch
+    ldr     r3, =tk_fpu_scratch
+    vstmia  r3!, {d0-d7}
+    vstmia  r3!, {d8-d15}
+.endm
+
+// clobbers r3
+.macro restore_fpu_from_scratch
+    ldr     r3, =tk_fpu_scratch
+    vldmia  r3!, {d0-d7}
+    vldmia  r3!, {d8-d15}
+.endm
+
+// Shared dispatch for SysTick (preemption tick) and SVCall (yield/sleep/exit).
+// Hardware has already pushed the Core8 frame to the current task's PSP.
+// Handler mode automatically runs on MSP.
+    .type isr_systick,%function
+    .thumb_func
+isr_systick:
+    movs    r1, #1                    // REASON_TICK
+    b       tk_dispatch_common
+
+    .type isr_svcall,%function
+    .thumb_func
+isr_svcall:
+    movs    r1, #2                    // REASON_SVC
+    b       tk_dispatch_common
+
+    .type tk_dispatch_common,%function
+    .thumb_func
+tk_dispatch_common:
+    // 1. Get current task stack pointer (PSP)
+    mrs     r0, psp
+
+    // 2. Save R4-R11 onto current task's stack (PSP)
+    stmdb   r0!, {r4-r11}
+
+    // 3. Save FPU state to global scratch buffer
+    save_fpu_to_scratch
+
+    // 4. Preserve EXC_RETURN (lr) and reason (r1) on MSP (8-byte stack alignment)
+    push    {r1, lr}
+
+    // 5. Call scheduler: r0 = cur_sp, r1 = reason
+    // Returns next task's SP in r0, or 0 to keep running current task.
+    bl      tk_reschedule
+
+    // 6. Restore FPU state for next (or continuing) task
+    restore_fpu_from_scratch
+
+    // 7. Restore EXC_RETURN (lr) and r1 from MSP
+    pop     {r1, lr}
+
+    // 8. If r0 == 0, keep current task running
+    cmp     r0, #0
+    beq     .keep_running
+
+    // 9. Switch to new task: restore R4-R11 from new task's stack
+    ldmia   r0!, {r4-r11}
+
+    // 10. Update PSP with new task's SP (points to Core8 hardware frame)
+    msr     psp, r0
+
+.keep_running:
+    // 11. Exception return: hardware pops Core8 frame from PSP and returns to Thread mode
+    bx      lr
+
+// Start the first task.
+// r0 = initial SP from tk_port_make_frame (pointing at R4-R11 area).
+    .type tk_port_start,%function
+    .thumb_func
+tk_port_start:
+    // 1. Pop initial R4-R11 (zeros)
+    ldmia   r0!, {r4-r11}
+
+    // 2. Set PSP to the start of the Core8 hardware frame
+    msr     psp, r0
+
+    // 3. Restore FPU state
+    restore_fpu_from_scratch
+
+    // 4. Switch Thread mode to use PSP (CONTROL.SPSEL = 1)
+    mrs     r1, control
+    movs    r2, #2
+    orrs    r1, r2
+    msr     control, r1
+    isb
+
+    // 5. Enable SysTick (ENABLE | TICKINT | CLKSOURCE)
+    ldr     r1, =0xE000E010           // SysTick CTRL
+    movs    r3, #7
+    str     r3, [r1]
+
+    // 6. Pop Core8 frame from PSP and start task:
+    // Pop R0-R3 (arg in R0), R12, LR (trampoline)
+    pop     {r0-r3, r12, lr}
+    // Load PC (entry fn) and discard xPSR (+8 bytes total)
+    ldr     pc, [sp], #8
+    .pool
+
+// Build a task's initial stack frame on its stack buffer.
+//   r0 = base of stack buffer, r1 = size in bytes (multiple of 8),
+//   r2 = entry fn, r3 = arg
+//
+// Stack Layout (low -> high):
+//   [r0 + 0  .. r0 + 28] : R4-R11 (zeros, 32 bytes)
+//   [r0 + 32]            : R0 (arg)
+//   [r0 + 36 .. r0 + 48] : R1-R3, R12 (zeros)
+//   [r0 + 52]            : LR (tk_task_return_trampoline)
+//   [r0 + 56]            : PC (entry fn)
+//   [r0 + 60]            : xPSR (0x01000000, Thumb bit set)
+//
+// Returns: Initial SP in r0 (pointing at R4).
+    .type tk_port_make_frame,%function
+    .thumb_func
+tk_port_make_frame:
+    adds    r0, r0, r1                // r0 = top of stack allocation
+    subs    r0, #64                   // r0 = SP pointing at R4
+
+    // Zero R4-R11
+    movs    r1, #0
+    str     r1, [r0, #0]              // R4
+    str     r1, [r0, #4]              // R5
+    str     r1, [r0, #8]              // R6
+    str     r1, [r0, #12]             // R7
+    str     r1, [r0, #16]             // R8
+    str     r1, [r0, #20]             // R9
+    str     r1, [r0, #24]             // R10
+    str     r1, [r0, #28]             // R11
+
+    // Core8 frame
+    str     r3, [r0, #32]             // R0 = arg
+    str     r1, [r0, #36]             // R1 = 0
+    str     r1, [r0, #40]             // R2 = 0
+    str     r1, [r0, #44]             // R3 = 0
+    str     r1, [r0, #48]             // R12 = 0
+
+    ldr     r1, =tk_task_return_trampoline
+    orrs    r1, #1
+    str     r1, [r0, #52]             // LR = trampoline (Thumb)
+
+    orrs    r2, #1
+    str     r2, [r0, #56]             // PC = entry fn (Thumb)
+
+    movs    r1, #0x01
+    lsls    r1, #24                   // 0x01000000 (Thumb bit)
+    str     r1, [r0, #60]             // xPSR
+
+    bx      lr

+ 34 - 0
src/port/m33/port_m33.c

@@ -0,0 +1,34 @@
+#include <stdint.h>
+
+#include "hardware/clocks.h"
+#include "hardware/structs/clocks.h"  // clock_num_t (clk_sys)
+#include "hardware/structs/m33.h"     // m33_hw->fpccr (memory-mapped FPU control)
+#include "hardware/structs/scb.h"
+#include "hardware/structs/systick.h"
+
+#include "port_m33.h"
+
+#define FPCCR_ASPEN (1u << 31)  // FPU_FPCCR_ASPEN_Pos per CMSIS core_cm33.h
+#define FPCCR_LSPEN (1u << 30)  // FPU_FPCCR_LSPEN_Pos per CMSIS core_cm33.h
+
+void tk_port_init(uint32_t ticks_per_sec) {
+    // Disable automatic state preservation and lazy stacking via FPCCR:
+    // the kernel explicitly saves/restores S0-S31 in software on every switch,
+    // so hardware must never push VFP frames onto task stacks.
+    m33_hw->fpccr = (uint32_t)(m33_hw->fpccr & ~(FPCCR_ASPEN | FPCCR_LSPEN));
+
+    uint32_t clk = clock_get_hz(clk_sys);
+    systick_hw->rvr = ((clk + ticks_per_sec / 2u) / ticks_per_sec - 1u) & 0x00FFFFFFu;
+
+    // Run SVC and SysTick at the lowest priority (15 of 15 on M33) so peripheral
+    // ISRs always preempt them. Byte offsets follow CMSIS __NVIC_SetPriority:
+    // SHPR base = SCB+0x18, byte index = ((IRQn) & 0xF) - 4.
+    uint8_t* shpr = (uint8_t*)&scb_hw->shpr[0];
+    shpr[((int32_t)(-5) & 0xF) - 4] = 15u << 4;  // SVCall
+    shpr[((int32_t)(-1) & 0xF) - 4] = 15u << 4;  // SysTick
+
+    // NOTE: SysTick is intentionally NOT enabled here. tk_port_start() enables it
+    // (ENABLE|TICKINT|CLKSOURCE) in asm, right before jumping into the first task,
+    // so a tick can never preempt the rest of tk::start() while scheduler state and
+    // MSP are not yet established.
+}

+ 20 - 0
src/port/m33/port_m33.h

@@ -0,0 +1,20 @@
+#pragma once
+
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// One-time port setup: FPU context policy + SysTick. Called from tk::start().
+void tk_port_init(uint32_t ticks_per_sec);
+
+// Build a task's initial stack frame. Returns the SP to start with.
+uintptr_t tk_port_make_frame(uint8_t* base, uint32_t size_bytes, void* entry_fn, void* arg);
+
+// Jump into the first task (r0 = SP from tk_port_make_frame). Never returns.
+void tk_port_start(uintptr_t sp);
+
+#ifdef __cplusplus
+}
+#endif

+ 273 - 0
src/scheduler.cpp

@@ -0,0 +1,273 @@
+#include "tinykernel/kernel.hpp"
+
+#include <cstring>
+
+#include "pico/platform.h"
+
+#include "port/m33/port_m33.h"
+
+// FPU state saved explicitly on every switch. Lazy stacking is disabled
+// (FPCCR.LSPEN=0 at kernel start and never re-enabled), so hardware never pushes
+// VFP frames onto task stacks; S0-S31 are the only floating point state that
+// matters (see context.S).
+struct FpuCtx {
+    float s[32];
+};
+static_assert(sizeof(FpuCtx) == 128, "FPU context layout must match context.S");
+
+namespace {
+
+enum class State : uint8_t { Free = 0, Ready, Sleeping, Terminated };
+
+// Must match the reason constants passed in r1 by context.S.
+enum Reason : int { REASON_TICK = 1, REASON_SVC = 2 };
+
+enum PendingOp : uint8_t { OP_NONE = 0, OP_YIELD, OP_SLEEP, OP_EXIT };
+
+struct TaskSlot {
+    State state;
+    tk::TaskFn fn;
+    void* arg;
+    const char* name;
+    uint32_t prio;      // 0..15, lower is higher priority
+    uintptr_t sp;       // saved SP (points at the core8 frame); 0 = not started yet
+    int64_t wake_tick;
+    int32_t slice_left;
+    bool is_idle;
+    FpuCtx fpu;
+};
+
+static_assert(TK_MAX_TASKS <= 255, "slot index must fit in a byte");
+
+alignas(16) static TaskSlot g_slots[TK_MAX_TASKS];  // slot 0 = idle (reserved)
+alignas(8) static char g_stacks[TK_MAX_TASKS][TK_DEFAULT_STACK_SIZE];
+
+static uint8_t g_cur = 0;
+static int64_t g_tick = 0;
+static bool g_started = false;
+static uint8_t g_rr_last[16] = {0};
+static PendingOp g_pending_op = OP_NONE;
+static int32_t g_pending_ticks = 0;
+
+constexpr uint32_t kIdlePrio = 15u;
+constexpr int32_t kSliceTicks = (int32_t)(((uint64_t)TK_TIME_SLICE_MS * TK_TICKS_PER_SEC + 999u) / 1000u);
+static_assert(kSliceTicks >= 1, "time slice must be at least one tick");
+
+static void idle_fn(void* arg) {
+    (void)arg;
+    for (;;) {
+        __asm__ volatile("wfi");
+    }
+}
+
+static inline bool is_runnable(const TaskSlot& s) {
+    if (s.is_idle) return true;
+    return s.state == State::Ready;
+}
+
+// Highest priority level (lowest number) that has at least one runnable task. Idle
+// guarantees that level 15 always qualifies, so this never returns -1 after start().
+static int32_t find_prio_level(void) {
+    for (uint32_t p = 0; p < 16u; ++p) {
+        for (uint32_t i = 0; i < TK_MAX_TASKS; ++i) {
+            if (g_slots[i].prio == p && is_runnable(g_slots[i])) return (int32_t)p;
+        }
+    }
+    return -1;
+}
+
+// Round-robin pick among runnable tasks of the given priority level, starting after
+// g_rr_last[level] so that equal-priority tasks rotate fairly.
+static uint32_t pick_round_robin(uint32_t level) {
+    for (uint32_t k = 1; k <= TK_MAX_TASKS; ++k) {
+        uint32_t i = (g_rr_last[level] + k) % TK_MAX_TASKS;
+        if (g_slots[i].prio == level && is_runnable(g_slots[i])) return i;
+    }
+    return g_cur;
+}
+
+// Choose the next task to run. Returns a slot index, or g_cur to keep running.
+static uint32_t select_next(bool force_switch) {
+    int32_t level = find_prio_level();
+    if (level < 0) return g_cur;
+    const TaskSlot& cur = g_slots[g_cur];
+    if (!force_switch && cur.prio == (uint32_t)level && is_runnable(cur)) return g_cur;
+    // pick_round_robin only ever returns a runnable slot, so if the current task is no
+    // longer runnable it can never be picked back here.
+    return pick_round_robin((uint32_t)level);
+}
+
+static inline void tk_svc(void) {
+    __asm__ volatile("svc #0" ::: "memory");
+}
+
+// Called by the trampoline when a task function simply returns.
+extern "C" void tk_task_return_trampoline(void) {
+    g_pending_op = OP_EXIT;
+    tk_svc();
+    for (;;) __asm__ volatile("wfi");
+}
+
+}  // namespace
+
+// Referenced (unmangled) from context.S: scratch area the port asm uses to move FPU
+// state between hardware registers and the per-task copies in g_slots[].
+extern "C" {
+    alignas(8) FpuCtx tk_fpu_scratch = {};
+}
+
+extern "C" uintptr_t tk_reschedule(uintptr_t cur_sp, int reason) {
+    TaskSlot& cur = g_slots[g_cur];
+    bool force = false;
+
+    if (reason == REASON_TICK) {
+        ++g_tick;
+        for (uint32_t i = 1; i < TK_MAX_TASKS; ++i) {
+            TaskSlot& s = g_slots[i];
+            if (s.state == State::Sleeping && g_tick >= s.wake_tick) s.state = State::Ready;
+        }
+        if (!cur.is_idle) {
+            --cur.slice_left;
+            if (cur.slice_left <= 0) {
+                cur.slice_left = kSliceTicks;
+                force = true;
+            }
+        }
+    } else if (reason == REASON_SVC) {
+        switch (g_pending_op) {
+            case OP_YIELD:
+                force = true;
+                break;
+            case OP_SLEEP:
+                cur.state = State::Sleeping;
+                cur.wake_tick = g_tick + g_pending_ticks;
+                force = true;
+                break;
+            case OP_EXIT:
+                // Phase 1: the slot and its stack are not reclaimed.
+                cur.state = State::Terminated;
+                force = true;
+                break;
+            default:
+                break;
+        }
+        g_pending_op = OP_NONE;
+    }
+
+    uint32_t next = select_next(force);
+    if (next == g_cur) return 0;  // keep the current context running
+
+    cur.sp = cur_sp;
+    memcpy(&cur.fpu, &tk_fpu_scratch, sizeof(cur.fpu));
+
+    TaskSlot& nxt = g_slots[next];
+    if (nxt.sp == 0) {
+        nxt.sp = tk_port_make_frame(reinterpret_cast<uint8_t*>(&g_stacks[next][0]),
+                                    TK_DEFAULT_STACK_SIZE, reinterpret_cast<void*>(nxt.fn), nxt.arg);
+    }
+    memcpy(&tk_fpu_scratch, &nxt.fpu, sizeof(nxt.fpu));
+    nxt.slice_left = kSliceTicks;
+    g_rr_last[nxt.prio] = (uint8_t)next;
+    g_cur = (uint8_t)next;
+    return nxt.sp;
+}
+
+namespace tk::detail {
+
+void static_register(const char* name, TaskFn fn, uint32_t prio) {
+    if (prio >= kIdlePrio) prio = 14u;
+    for (uint32_t i = 1; i < TK_MAX_TASKS; ++i) {
+        if (g_slots[i].state == State::Free) {
+            g_slots[i] = TaskSlot{};
+            g_slots[i].fn = fn;
+            g_slots[i].name = name;
+            g_slots[i].prio = prio;
+            g_slots[i].state = State::Ready;
+            return;
+        }
+    }
+}
+
+}  // namespace tk::detail
+
+tk::TaskHandle tk::create(TaskFn fn, void* arg, uint32_t prio) {
+    TaskHandle h{0xFF};
+    if (prio >= kIdlePrio) prio = 14u;
+    for (uint32_t i = 1; i < TK_MAX_TASKS; ++i) {
+        if (g_slots[i].state == State::Free) {
+            g_slots[i] = TaskSlot{};
+            g_slots[i].fn = fn;
+            g_slots[i].arg = arg;
+            g_slots[i].name = "dyn";
+            g_slots[i].prio = prio;
+            g_slots[i].state = State::Ready;
+            h.slot = (uint8_t)i;
+            return h;
+        }
+    }
+    return h;
+}
+
+void tk::start() {
+    if (g_started) for (;;) __asm__ volatile("wfi");
+    g_started = true;
+
+    TaskSlot& idle = g_slots[0];
+    idle.fn = idle_fn;
+    idle.name = "idle";
+    idle.prio = kIdlePrio;
+    idle.is_idle = true;
+    idle.state = State::Ready;
+
+    tk_port_init(TK_TICKS_PER_SEC);  // FPU policy + SysTick (enabled at the end)
+
+    uint32_t next = select_next(/*force_switch=*/true);
+    TaskSlot& nxt = g_slots[next];
+    if (nxt.sp == 0) {
+        nxt.sp = tk_port_make_frame(reinterpret_cast<uint8_t*>(&g_stacks[next][0]),
+                                    TK_DEFAULT_STACK_SIZE, reinterpret_cast<void*>(nxt.fn), nxt.arg);
+    }
+    memcpy(&tk_fpu_scratch, &nxt.fpu, sizeof(nxt.fpu));
+    nxt.slice_left = kSliceTicks;
+    g_rr_last[nxt.prio] = (uint8_t)next;
+    g_cur = (uint8_t)next;
+
+    tk_port_start(nxt.sp);  // never returns
+    for (;;) __asm__ volatile("wfi");
+}
+
+void tk::yield() {
+    g_pending_op = OP_YIELD;
+    tk_svc();
+}
+
+void tk::sleep_ms(uint32_t ms) {
+    uint64_t ticks = ((uint64_t)ms * TK_TICKS_PER_SEC + 999u) / 1000u;
+    if (ticks < 1u) ticks = 1u;
+    g_pending_ticks = (int32_t)(ticks > 0x7FFFFFFFu ? 0x7FFFFFFFu : ticks);
+    g_pending_op = OP_SLEEP;
+    tk_svc();
+}
+
+void tk::exit(int code) {
+    (void)code;
+    g_pending_op = OP_EXIT;
+    tk_svc();
+    for (;;) __asm__ volatile("wfi");  // unreachable: the scheduler switches away
+}
+
+uint64_t tk::now_ms() {
+    return (uint64_t)g_tick * 1000u / TK_TICKS_PER_SEC;
+}
+
+bool tk::lock() {
+    uint32_t primask;
+    __asm__ volatile("mrs %0, primask\n\tcpsid i" : "=r"(primask) :: "memory");
+    return primask == 0;
+}
+
+void tk::unlock(bool was_enabled) {
+    if (was_enabled) {
+        __asm__ volatile("cpsie i" ::: "memory");
+    }
+}