System Services
Cross-cutting runtime services: logging, clocks, GC introspection, threading, and executors. Packages: picodroid.util, picodroid.os, picodroid.concurrent. See Java API overview for the full API index.
picodroid.util.Log
import picodroid.util.Log;
Log.i("TAG", "message"); // info log → defmt::info! over RTTpicodroid.os.SystemClock
import picodroid.os.SystemClock;
SystemClock.sleep(500); // sleep for 500 mslong t = SystemClock.elapsedRealtimeNanos(); // nanoseconds since boot (monotonic)java.lang.System.currentTimeMillis()
Convenience for the common Android idiom long now = System.currentTimeMillis();. Returns milliseconds elapsed since boot — there is no wall-clock RTC on the Pico, so the value is monotonic but not Unix-epoch-relative. Equivalent to SystemClock.elapsedRealtimeNanos() / 1_000_000.
long start = System.currentTimeMillis();doWork();long elapsed = System.currentTimeMillis() - start;See examples/clockdemo/.
picodroid.os.Runtime
GC and heap introspection. All methods are static.
import picodroid.os.Runtime;
long nanos = Runtime.gcTimeNanos(); // total time spent in GC so far (ns)int count = Runtime.gcCount(); // number of GC cycles runint freed = Runtime.gcFreed(); // total heap entries freed across all cyclesRuntime.resetGcStats(); // reset all three counters to zero
long used = Runtime.usedMemory(); // current heap usage (bytes)long peak = Runtime.peakMemory(); // high-water heap usage so far (bytes)Runtime.resetPeakMemory(); // reset the peak counter to the current usageusedMemory / peakMemory / resetPeakMemory are handy for profiling — bracket a workload with
resetPeakMemory() then read peakMemory() to capture its high-water allocation.
picodroid.concurrent.Thread
The java.lang.Thread API on a FreeRTOS task. Import it — there is no java.lang.Thread here.
import picodroid.concurrent.Thread;
Thread t = new Thread(new MyRunnable(), "worker");t.start(); // spawns a FreeRTOS task that calls MyRunnable.run()t.join(); // or join(ms); InterruptedException if this thread is interruptedt.isAlive(); // false once run() has returned
Thread.currentThread().getName(); // "main" on the UI threadThread.sleep(250); // interruptible; throws InterruptedExceptiont.interrupt(); // wakes sleep/join/wait on t, or sets its flag
// Subclass form, uncaught-exception handlerThread u = new Thread("u") { @Override public void run() { /* ... */ } };Thread.setDefaultUncaughtExceptionHandler((th, e) -> Log.e("app", th.getName() + ": " + e));
// Monitors: synchronized blocks AND methods lock; Object.wait/notify worksynchronized (lock) { while (!ready) lock.wait(); }Second start() throws IllegalThreadStateException; wait/notify outside synchronized throw IllegalMonitorStateException. SystemClock.sleep(int) is the non-interruptible sleep, as on Android.
Complete Runnable example
import picodroid.concurrent.Thread;import picodroid.util.Log;import picodroid.os.SystemClock;
public class MyApp { public static void main(String[] args) { Thread worker = new Thread(new Runnable() { public void run() { for (int i = 0; i < 3; i++) { Log.i("Worker", "tick " + String.valueOf(i)); SystemClock.sleep(1000); } } }); worker.setPriority(Thread.MAX_PRIORITY); worker.start();
Log.i("Main", "Worker started, main continues"); }}Priority
Thread.MIN_PRIORITY (1), NORM_PRIORITY (5) and MAX_PRIORITY (10) exist and setPriority/getPriority round-trip them, but the value is advisory: every task that interprets Java — the UI thread, every Thread, the background executor pool — runs at the single JVM tier (FreeRTOS priority 15), below real-time native tasks (21–30) and above background native services (1–10). The shared JVM heap is lock-free on the strength of “a running JVM task keeps the core until it blocks”, and a Java thread one notch above the UI thread would preempt it at any instruction. Android itself only treats setPriority as a scheduling hint; here the hint is recorded and not applied.
Each call to t.start() creates a dedicated FreeRTOS task with a 4096-word stack. When MyRunnable.run() returns, the task self-deletes and its stack is reclaimed automatically.
All JVM child threads are pinned to core 0, the same core as the jvm task. This keeps the single-core safety assumption of SharedJvmState intact — no JVM state is ever accessed from core 1.
On hot-swap, any thread blocked inside SystemClock.sleep(), Thread.sleep(), join() or Object.wait() is woken immediately so it can see the stop signal and exit cleanly before the new app starts.
For fire-and-forget work, prefer Executors.backgroundExecutor() over spawning a dedicated Thread: the pool amortises stack allocation across jobs and keeps per-task overhead bounded.
picodroid.concurrent.Executors
Android-style java.util.concurrent.Executor bindings for posting Runnables onto the framework’s own threads. Two executors are exposed:
- Main-thread executor — runs Runnables on the JVM task’s main loop, interleaved with LVGL ticks on a 16 ms frame budget. Use this to touch widgets or any other state that must only be read/written from the main thread.
- Background pool — a fixed-size FreeRTOS thread pool with its own worker tasks. Use this for short blocking work (I/O, sensor reads, crypto) that you don’t want to stall the UI.
import picodroid.concurrent.Executor;import picodroid.concurrent.Executors;import picodroid.util.Log;
Executor main = Executors.mainExecutor();Executor bg = Executors.backgroundExecutor();
bg.execute(() -> { String result = fetchSomethingSlow(); main.execute(() -> label.setText(result)); // hop back to the UI thread});The Executor interface is a single method:
public interface Executor { void execute(Runnable command);}execute() is non-blocking and returns immediately. If the target queue is full, the Runnable is dropped with a defmt::warn and no exception — plan for occasional backpressure rather than relying on every post to land. The main queue has capacity 64; the background queue’s depth is configurable per board.
Background pool configuration
The pool is tuned via a [background_pool] section in board.toml. All keys optional:
[background_pool]threads = 4 # 1..=32 (default 4)priority = 5 # 1..=10 FreeRTOS BG tier (default 5)stack_bytes = 4096 # per-worker stack (default 4 KiB)queue_depth = 32 # shared job queue depth (default 32)Each worker owns its own Jvm instance, so Runnables posted to the background pool run with a separate JVM state from the main loop. Treat any shared object references as if they crossed a thread boundary.
See examples/executordemo/ for a worked example.
See also: Core language · Services & DI · Peripherals · Storage · Networking · Sensors · Graphics & UI