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
//! # The mnemos kernel
//!
//! The mnemos kernel is implemented as a library, with platform-specific code depending on the
//! kernel library and producing the final binary.
//!
//! ## The "kernelspace" entry point
//!
//! At the moment, the kernel requires some "entry point" code, more or less the `main()` function
//! that runs when the system boots, to perform any system-specific initialization.
//!
//! This entry point code is responsible for setting up any hardware or subsystems that exist
//! outside the kernel (or are required by any kernel services), as well as starting and running
//! the kernel itself.
//!
//! ## Setting up the Allocator
//!
//! Before creating the kernel, a [MnemosAlloc][mnemos_alloc::heap::MnemosAlloc] instance must be
//! registered as a [GlobalAlloc][core::alloc::GlobalAlloc], and if the
//! [UnderlyingAllocator][mnemos_alloc::heap::UnderlyingAllocator] requires initialization,
//! it must occur before creating the kernel.
//!
//! The kernel will be allocated using the provided allocator.
//!
//! ## Creating the kernel
//!
//! The allocator and any kernel settings are provided to the kernel constructor. The kernel
//! will allocate itself, and return an owned kernel entity.
//!
//! After creation, the executor is *not* running yet.
//!
//! ## Initialization phase
//!
//! After creating the kernel, we need to register kernel services, which are expected to act as
//! drivers for various system components.
//!
//! Since we are not running the executor yet, the kernel provides an interface,
//! [`Kernel::initialize()`], which takes a future and spawns it on the executor. Futures added with
//! `initialize` still do not run until the later "running" phase.
//!
//! Right now, it is generally suggested you use one or more `initialize`
//! calls to register all (initial) kernel services.
//!
//! ## Running mode
//!
//! Once everything is prepared and initialized, the startup code is expected to call
//! [`Kernel::tick()`] repeatedly. On each call to tick:
//!
//! * The allocator frees any synchronously dropped allocations, making them available for
//! asynchronous allocation
//! * The async executor is polled
//!
//! AT THE MOMENT, there is no indication of whether all tasks are blocked, which could be use to
//! inform whether we should put the CPU into some kind of sleep mode until a hardware event (like
//! a timer or DMA transaction) is triggered, and an async task has potentially been awoken.
//!
//! ## Not covered: "userspace"
//!
//! At the moment, there is SOME concept of a userspace, which interacts with the kernel via a
//! bidirectional IPC ringbuffer. Space for this ringbuffer is allocated when calling
//! [`Kernel::new()`]. Additionally this ringbuffer is polled on each call to `tick`, after freeing
//! allocations and before calling `tick` on the scheduler.
//!
//! This is an artifact of how mnemos 0.1 worked, where there was a single userspace executor that
//! existed and interacted with the kernel executor.
//!
//! As of 2023-05-30, I don't think this is the right abstraction for multiple userspace processes.
//! The pieces that exist currently are likely to be removed or reworked heavily before they are
//! usable, and should be considered nonfunctional at the moment.
#![cfg_attr(not(test), no_std)]
#![allow(clippy::missing_safety_doc)]
#![feature(impl_trait_in_assoc_type)]
extern crate alloc;
pub mod comms;
pub mod daemons;
pub(crate) mod fmt;
pub mod forth;
pub mod isr;
pub mod registry;
pub mod retry;
#[cfg(feature = "serial-trace")]
pub mod serial_trace;
pub mod services;
#[cfg(test)]
pub(crate) mod test_util;
use core::{convert::identity, future::Future, ptr::NonNull};
use abi::{
bbqueue_ipc::BBBuffer,
syscall::{KernelResponse, UserRequest},
};
use comms::kchannel::KChannel;
pub use embedded_hal_async;
pub use maitake;
use maitake::{
scheduler::LocalScheduler,
task::{BoxStorage, JoinHandle, Storage},
time::{Duration, Sleep, Timeout, Timer},
};
pub use mnemos_alloc;
use mnemos_alloc::containers::Box;
use registry::Registry;
use serde::{Deserialize, Serialize};
use services::{
forth_spawnulator::{SpawnulatorServer, SpawnulatorSettings},
keyboard::mux::{KeyboardMuxServer, KeyboardMuxSettings},
serial_mux::{SerialMuxServer, SerialMuxSettings},
};
pub use tracing;
pub struct Rings {
pub u2k: NonNull<BBBuffer>,
pub k2u: NonNull<BBBuffer>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct KernelSettings {
pub max_drivers: usize,
pub timer_granularity: Duration,
}
pub struct Message {
pub request: UserRequest,
pub response: KChannel<KernelResponse>,
}
pub struct Kernel {
/// Items that do not require a lock to access, and must only
/// be accessed with shared refs
inner: KernelInner,
/// The run-time driver registry.
registry: Registry,
}
unsafe impl Sync for Kernel {}
pub struct KernelInner {
/// MnemOS currently only targets single-threaded platforms, so we can use a
/// `maitake` scheduler capable of running `!Send` futures.
scheduler: LocalScheduler,
/// Maitake timer wheel.
timer: Timer,
}
/// Settings for all services spawned by default.
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct KernelServiceSettings {
pub keyboard_mux: KeyboardMuxSettings,
pub serial_mux: SerialMuxSettings,
pub spawnulator: SpawnulatorSettings,
pub sermux_loopback: daemons::sermux::LoopbackSettings,
pub sermux_hello: daemons::sermux::HelloSettings,
#[cfg(feature = "serial-trace")]
pub sermux_trace: serial_trace::SerialTraceSettings,
}
impl Kernel {
/// Create a new kernel with the given settings.
///
/// The allocator MUST be initialized if required, and be ready to allocate
/// data.
pub unsafe fn new(settings: KernelSettings) -> Result<Box<Self>, &'static str> {
let registry = registry::Registry::new(settings.max_drivers);
let scheduler = LocalScheduler::new();
let inner = KernelInner {
scheduler,
timer: Timer::new(settings.timer_granularity),
};
let new_kernel =
Box::try_new(Kernel { inner, registry }).map_err(|_| "Kernel allocation failed.")?;
Ok(new_kernel)
}
fn inner(&'static self) -> &'static KernelInner {
&self.inner
}
#[inline]
#[must_use]
pub fn timer(&'static self) -> &'static Timer {
&self.inner.timer
}
pub fn tick(&'static self) -> maitake::scheduler::Tick {
let inner = self.inner();
inner.scheduler.tick()
// TODO: Send time to userspace?
}
/// Initialize the kernel's `maitake` timer as the global default timer.
///
/// This allows the use of `sleep` and `timeout` free functions.
/// TODO(eliza): can the kernel just "do this" once it becomes active? Or,
/// have a "kernel.init()" or something that does this and other global inits?
pub fn set_global_timer(&'static self) -> Result<(), maitake::time::AlreadyInitialized> {
maitake::time::set_global_timer(self.timer())
}
#[track_caller]
pub fn initialize<F>(&'static self, fut: F) -> Result<JoinHandle<F::Output>, &'static str>
where
F: Future + 'static,
{
Ok(self.inner.scheduler.spawn(fut))
}
pub async fn spawn<F>(&'static self, fut: F) -> JoinHandle<F::Output>
where
F: Future + 'static,
{
let bx = Box::new(maitake::task::Task::new(fut))
.await
.into_alloc_box();
self.spawn_allocated(bx)
}
/// Immutably borrow the kernel's [`Registry`].
#[inline]
#[must_use]
pub fn registry(&self) -> &Registry {
&self.registry
}
#[track_caller]
pub fn spawn_allocated<F>(
&'static self,
task: <BoxStorage as Storage<LocalScheduler, F>>::StoredTask,
) -> JoinHandle<F::Output>
where
F: Future + 'static,
{
self.inner.scheduler.spawn_allocated(task)
}
/// Returns a [`Sleep`] future that sleeps for the specified [`Duration`].
#[inline]
pub fn sleep(&'static self, duration: Duration) -> Sleep<'static> {
self.inner.timer.sleep(duration)
}
/// Returns a [`Timeout`] future that cancels `F` if the specified
/// [`Duration`] has elapsed before it completes.
#[inline]
pub fn timeout<F: Future>(&'static self, duration: Duration, f: F) -> Timeout<'static, F> {
self.inner.timer.timeout(duration, f)
}
/// Initialize the default set of cross-platform kernel [`services`] that
/// are spawned on all hardware platforms.
///
/// Calling this method is not *mandatory* for a hardware platform
/// implementation. The platform implementation may manually spawn these
/// services individually, or choose not to spawn them at all. However, this
/// method is provided to ensure that a consistent set of cross-platform
/// services are initialized on all hardware platforms *if they are
/// desired*.
///
/// Services spawned by this method include:
///
/// - The [`KeyboardMuxService`], which multiplexes keyboard input from
/// multiple keyboards to tasks that depend on keyboard input,
/// - The [`SerialMuxService`], which multiplexes serial I/O to virtual
/// serial ports
/// - The [`SpawnulatorService`], which is responsible for spawning
/// new Forth tasks
///
/// In addition, this method will initialize the following non-service
/// daemons:
///
/// - [`daemons::sermux::loopback`], which serves a loopback service on a
/// configured loopback port
/// - [`daemons::sermux::hello`], which sends periodic "hello world" pings
/// to a configured serial mux port
/// - If the "serial-trace" feature flag is enabled, the
/// [`serial_trace::SerialSubscriber`] worker task, which sends `tracing`
/// events over the serial port.
///
/// If the kernel's [`maitake::time::Timer`] has not been set as the global
/// timer, this method will also ensure that the global timer is set as the
/// default.
///
/// [`KeyboardMuxService`]:
/// crate::services::keyboard::mux::KeyboardMuxService
/// [`SerialMuxService`]: crate::services::serial_mux::SerialMuxService
/// [`SpawnulatorService`]:
/// crate::services::forth_spawnulator::SpawnulatorService
pub fn initialize_default_services(&'static self, settings: KernelServiceSettings) {
// Set the kernel timer as the global timer.
// Disregard errors --- they just mean someone else has already set up
// the global timer.
let _ = self.set_global_timer();
if settings.serial_mux.enabled {
// Initialize tracing first, so that we can collect more traces from
// the initialization process.
#[cfg(feature = "serial-trace")]
if settings.sermux_trace.enabled {
self.initialize(async move {
let subscriber =
crate::serial_trace::SerialSubscriber::start(self, settings.sermux_trace)
.await;
tracing::subscriber::set_global_default(subscriber)
.expect("default tracing subscriber already set!");
})
.expect("failed to start tracing subscriber!");
}
// Initialize the SerialMuxServer
self.initialize(SerialMuxServer::register(self, settings.serial_mux))
.expect("failed to spawn SerialMuxService initialization");
// Initialize Serial Mux daemons.
if settings.sermux_loopback.enabled {
self.initialize(daemons::sermux::loopback(self, settings.sermux_loopback))
.expect("failed to spawn SerMux loopback");
}
if settings.sermux_hello.enabled {
self.initialize(daemons::sermux::hello(self, settings.sermux_hello))
.expect("failed to spawn SerMux hello world daemon");
}
} else {
let deps = [
#[cfg(feature = "serial-trace")]
settings.sermux_trace.enabled,
settings.sermux_loopback.enabled,
settings.sermux_hello.enabled,
];
if deps.into_iter().any(identity) {
tracing::error!("Sermux services configured without sermux! Skipping.");
}
}
// Initialize the kernel keyboard mux service.
if settings.keyboard_mux.enabled {
self.initialize(KeyboardMuxServer::register(self, settings.keyboard_mux))
.expect("failed to spawn KeyboardMuxService initialization");
}
// Initialize the Forth spawnulator.
if settings.spawnulator.enabled {
self.initialize(SpawnulatorServer::register(self, settings.spawnulator))
.expect("failed to spawn SpawnulatorService initialization");
}
}
}