Skip to content

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 RTT

picodroid.os.SystemClock

import picodroid.os.SystemClock;
SystemClock.sleep(500); // sleep for 500 ms
long 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 run
int freed = Runtime.gcFreed(); // total heap entries freed across all cycles
Runtime.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 usage

usedMemory / 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 interrupted
t.isAlive(); // false once run() has returned
Thread.currentThread().getName(); // "main" on the UI thread
Thread.sleep(250); // interruptible; throws InterruptedException
t.interrupt(); // wakes sleep/join/wait on t, or sets its flag
// Subclass form, uncaught-exception handler
Thread 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 work
synchronized (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