Processes
Processes in Drone OS are special kind of fibers, that can be suspended with a
special blocking call. They use dedicated dynamically allocated stacks. On
Cortex-M platform, Drone implements processes using SVC
assembly instruction
and SVCall exception. So before using processes, a Drone supervisor should be
added to the project.
Supervisor
Create a new file at src/sv.rs
with the following content:
#![allow(unused)] fn main() { //! The supervisor. use drone_cortexm::{ sv, sv::{SwitchBackService, SwitchContextService}, }; sv::pool! { /// Pool of services. pool => SERVICES; /// Supervisor type. supervisor => pub Sv; // Attached services. services => { SwitchContextService; SwitchBackService; } } }
And register the newly created module in the src/lib.rs
:
#![allow(unused)] fn main() { pub mod sv; }
Update thr::nvic!
macro inside src/thr.rs
as follows:
#![allow(unused)] fn main() { use crate::sv::Sv; thr::nvic! { supervisor => Sv; // <-- register the supervisor type // ... other configuration is skipped ... threads => { exceptions => { /// All classes of faults. pub hard_fault; // Define an external function handler for the SV_CALL exception. naked(sv::sv_handler::<Sv>) sv_call; }; }; } }
Using processes
First, let's recall the generator fiber example from the previous chapter:
#![allow(unused)] fn main() { use core::pin::Pin; use drone_cortexm::{ fib, fib::{Fiber, FiberState}, }; let mut fiber = fib::new(|| { let mut state = 0; while state < 3 { state += 1; yield state; } state }); assert_eq!(Pin::new(&mut fiber).resume(()), FiberState::Yielded(1)); assert_eq!(Pin::new(&mut fiber).resume(()), FiberState::Yielded(2)); assert_eq!(Pin::new(&mut fiber).resume(()), FiberState::Yielded(3)); assert_eq!(Pin::new(&mut fiber).resume(()), FiberState::Complete(3)); }
This fiber can be rewritten using Drone process as follows:
#![allow(unused)] fn main() { use crate::sv::Sv; use core::pin::Pin; use drone_cortexm::{ fib, fib::{Fiber, FiberState}, }; let mut fiber = fib::new_proc::<Sv, _, _, _, _>(128, |_, yielder| { let mut state = 0; while state < 3 { state += 1; yielder.proc_yield(state); } state }); assert_eq!(Pin::new(&mut fiber).resume(()), FiberState::Yielded(1)); assert_eq!(Pin::new(&mut fiber).resume(()), FiberState::Yielded(2)); assert_eq!(Pin::new(&mut fiber).resume(()), FiberState::Yielded(3)); assert_eq!(Pin::new(&mut fiber).resume(()), FiberState::Complete(3)); }
The difference is that the code inside the closure argument is fully
synchronous. The proc_yield
call is translated to the SVC
assembly
instruction. This instruction immediately switches the execution context back to
the caller. When the resume
method of the process is called, it continues from
the last yield point, just like a generator.
The fib::new_proc
function takes a stack size as the first argument. The stack
will be immediately allocated from the heap. To make this function safe, the
processor's MPU used to protect the stack from a possible overflow. On
processors without MPU, like STM32F103, this function will panic. However it is
still possible to use processes on such systems, though without any guarantees
about stack overflows. You can use new_proc_unchecked
function, which is
marked unsafe
.
Unlike generators, a process can take input data. And unlike yield
keyword,
the proc_yield
function not necessarily returns ()
. Here is an example of
such process:
#![allow(unused)] fn main() { let mut fiber = fib::new_proc::<Sv, _, _, _, _>(128, |input, yielder| { let mut state = input; while state < 4 { state += yielder.proc_yield(state); } state }); assert_eq!(Pin::new(&mut fiber).resume(1), FiberState::Yielded(1)); assert_eq!(Pin::new(&mut fiber).resume(2), FiberState::Yielded(3)); assert_eq!(Pin::new(&mut fiber).resume(3), FiberState::Complete(6)); }