avr-oxide 0.3.1

An extremely simple Rusty operating system for AVR microcontrollers
/* thread.rs
 *
 * Developed by Tim Walls <tim.walls@snowgoons.com>
 * Copyright (c) All Rights Reserved, Tim Walls
 */
//! Multithreading primitives.
//!
//! AVRoxide allows you to create multiple threads, which will be scheduled
//! cooperatively (through the [`yield_now()`] method, or by calling any
//! blocking I/O routine), or pre-emptively if a suitable interrupt source is
//! nominated for pre-emption via a crate feature flag.
//!
//! # Limitations
//! There is a limit to the number of threads you can create, and additionally
//! since each thread gets a stack allocated from the heap, the available
//! heap memory is a limit.
//!
//! The initial (main()) thread is allocated a relatively large default stack
//! size, in recognition that it may be the only thread in the program and that
//! it is also likely to allocate a lot of 'global' data on its own stack.
//! This stack size can be changed by passing the `stacksize` attribute to
//! the `avr_oxide::main` macro.
//!
//! Subsequent threads are given a smaller default stack size, although this
//! can be overridden using the [`Builder::stack_size()`] method to create
//! the thread.
//!
//! The following table summarises the default thread limits:
//!
//! | For processor | Max threads | default main() stack | default new thread stack |
//! | ------------- | ----------- | ------------------ | ------------------------------ |
//! | `atmega4809`  | 5           | 512 bytes          | 128 bytes |
//! | `atmega328p`  | 3           | 384 bytes          | 64 bytes  |
//!
//! > Note: The maximum number of threads in the table *includes* the `main()`
//! >       thread, but not the default `Idle` thread that is created by the
//! >       kernel and must always exist.
//!
//! # Thread completion and cleanup
//! When a thread completes, it will enter a *Zombie* state.  It will remain
//! in this state - and the thread context and stack will not be cleaned up,
//! releasing any associated memory - until another thread joins it using
//! the [`JoinHandle::join()`] method.
//!
//! # Pre-Emptive Multithreading
//! Thread pre-emption depends on one (or more) interrupt sources being
//! nominated to drive the scheduler.  This is done by enabling a
//! `pmt_<interrupt_name>` feature when including the AVRoxide crate in your
//! `cargo.toml`.
//!
//! Typically, you would nominate a timer interrupt; which interrupt will
//! depend on the device:
//!
//! | For processor | Typical Feature flag   | Effect |
//! | ------------- | -----------------------| ------ |
//! | `atmega4809`  | `pmt_tcb0_int`         | Threads will be rescheduled every time TimerControlBlock 0 generates an interrupt |
//!
//! # Example
//! ```rust,no_run
//! #![no_std]
//! #![no_main]
//!
//! use avr_oxide::devices::{ Handle, UsesPin, OxideLed, OxideMasterClock };
//! use avr_oxide::thread;
//! use avr_oxide::hardware;
//! use avr_oxide::boards::board;
//!
//! #[avr_oxide::main(chip="atmega4809",stacksize=1024)]
//! pub fn main() {
//!   let supervisor = avr_oxide::oxide::instance();
//!
//!   // Configure a 50Hz master clock device on TCB0
//!   let master_clock = Handle::new(OxideMasterClock::with_timer::<50>(hardware::timer::tcb0::instance()));
//!   supervisor.listen_handle(master_clock);
//!
//!   // If our code builds AVRoxide using the `pmt_tcb0_int` feature, threads
//!   // will now be pre-emptively multitasked, and this code will work without
//!   // locking up:
//!   let _jh = thread::Builder::new().stack_size(32).spawn(||{
//!     let white_led = OxideLed::with_pin(board::pin_d(10));
//!
//!     loop {
//!       white_led.toggle();
//!     }
//!   });
//!
//!   // Note that all the usual functionality of the MasterClock device
//!   // (delay timers, regular Oxide event handlers) remains available -
//!   // it will be used for thread preemption *in addition* to its usual
//!   // function, not instead.
//!
//!   supervisor.run();
//! }
//! ```

// Imports ===================================================================
use avr_oxide::alloc::boxed::Box;
use avr_oxide::concurrency::scheduler::{ThreadContext, ThreadState};
use avr_oxide::concurrency::stack::{ThreadStack, DynamicThreadStack};
use avr_oxide::concurrency::util::{ThreadId, ThreadSet};
use avr_oxide::concurrency::{interrupt, scheduler};
use avr_oxide::cpu;
use avr_oxide::deviceconsts::oxide::{DEFAULT_THREAD_STACK_SIZE};
use avr_oxide::hal::generic::cpu::ProcessorContext;
use avr_oxide::util::datatypes::{BitField, BitIndex};
use avr_oxide::hal::generic::cpu::Cpu;

// Declarations ==============================================================
/**
 * Handle that allows us to join a thread
 */
pub struct JoinHandle {
  thread: Thread
}

/**
 * The 'userland facing' representation of a Thread
 */
#[repr(C)]
#[derive(Clone,Copy)]
pub struct Thread {
  thread_id: ThreadId
}

/**
 * A thread factory that allows us to set the stack size for the thread that
 * we create.
 */
pub struct Builder {
  stack_size: usize
}

// Code ======================================================================
/**
 * Spawn a new thread, returning a JoinHandle for it.  Panics if the thread
 * cannot be spawned.
 */
pub fn spawn<F>(f: F) -> JoinHandle
where
  F: FnOnce() -> u8,
  F: Send + 'static
{
  Builder::new().spawn(f)
}

/**
 * Cooperatively have the current thread yield to another.
 */
pub fn yield_now() {
  unsafe {
    if cpu!().interrupts_enabled() && !cpu!().in_isr() {
      scheduler::userland_schedule_and_switch();
    } else {
      avr_oxide::oserror::halt(avr_oxide::oserror::OsError::CannotYield)
    }
  }
}

/**
 * Spawn a new thread, returning a JoinHandle for it.  Panics if the thread
 * cannot be spawned.
 */
pub(crate) fn spawn_with_stack(_isotoken: avr_oxide::concurrency::interrupt::token::Isolated, code: Box<dyn FnOnce() -> u8>, stack: Box<dyn ThreadStack>) -> JoinHandle {
  unsafe {
    let scheduler = scheduler::instance();

    for i in ThreadId::MIN..ThreadId::MAX {
      match scheduler.threads[i] {
        None => {
          let stack_top = stack.get_stack_top();

          let thread = ThreadContext {
            state: ThreadState::Schedulable,
            entrypoint: Some(code),
            returncode: 0,
            waiting_threads: ThreadSet::new(),
            stack: Some(stack),
            cpu_context: ProcessorContext {
              sreg: BitField::with_bits_set(&[BitIndex::bit_c(7)]), // We enable interrupts in userland
              gpregs: [0x00; 32],
              pc: scheduler::thread_entrypoint as u16,
              sp: stack_top as u16,
              tid: i,
              #[cfg(feature="extended_addressing")]
              rampx: 0,
              #[cfg(feature="extended_addressing")]
              rampy: 0,
              #[cfg(feature="extended_addressing")]
              rampz: 0,
              #[cfg(feature="extended_addressing")]
              eind: 0
            },
            guard: 0xf0.into()
          };
          scheduler.threads[i] = Some(thread);

          return JoinHandle {
            thread: Thread {
              thread_id: i,
            }
          };
        },
        Some(_) => {}
      }
    }
    // If we got here, no free threads
    avr_oxide::oserror::halt(avr_oxide::oserror::OsError::OutOfThreads);
  }
}

impl JoinHandle {
  /**
   * Wait for the associated thread to complete execution.
   */
  pub fn join(self) -> u8 {
    unsafe {
      loop {
        let return_code = interrupt::isolated(|isotoken|{
          let target_thread = scheduler::get_thread_by_id(self.thread.thread_id);

          if target_thread.state == ThreadState::Zombie {
            // Aha!  The thread is ready to die...
            scheduler::set_thread_state(isotoken, self.thread.thread_id, ThreadState::Dead);
            Some(target_thread.returncode)
          } else {
            // OK, it's not dead yet.  We need to wait for it
            target_thread.waiting_threads.add_current_thread(isotoken);
            scheduler::set_current_thread_state(isotoken, ThreadState::BlockedOnThread);
            None
          }
        });

        match return_code {
          Some(value) => {
            return value
          },
          None => {
            yield_now();
          }
        }
      }
    }
  }

  /**
   * Return a reference to the underlying thread object
   */
  pub fn thread(&self) -> &Thread {
    &self.thread
  }
}

impl Thread {
  /**
   * Get the thread's unique identifier.  Note that thread IDs are only
   * unique as long as the thread is running, and may be recycled.
   */
  pub fn id(&self) -> ThreadId {
    self.thread_id
  }
}

impl Builder {
  pub fn new() -> Builder {
    Builder {
      stack_size: DEFAULT_THREAD_STACK_SIZE
    }
  }

  pub fn stack_size(mut self, size: usize) -> Builder {
    self.stack_size = size;
    self
  }

  pub fn spawn<F>(self, f: F) -> JoinHandle
  where
    F: FnOnce() -> u8,
    F: Send + 'static
  {
    interrupt::isolated(|isotoken|{
      // First, let's clean up any dead threads
      scheduler::reap_dead_threads(isotoken);

      let stack = Box::new(DynamicThreadStack::new(self.stack_size));
      let code = Box::new(f);

      spawn_with_stack(isotoken, code, stack)
    })
  }
}

// Tests =====================================================================