forked from ROMEO/nexosim

This is a pretty large patch that impacts the API. Until now, it was not possible to cancel events that were scheduled for the current simulation time slice, making it necessary for the user to use complex workarounds (see former version of the espresso machine example). The new implementation makes this possible but the generation of a key associated to an event has now a non-negligible cost (basicaly it creates three references to an Arc). For this reason, the API now defaults to NOT creating a key, and new methods were added for situations when the event may need to be cancelled and a key is necessary. See the much simplified implementation of the espresso machine example for a motivating case.
316 lines
10 KiB
Rust
316 lines
10 KiB
Rust
//! Example: current-controlled stepper motor and its driver.
|
|
//!
|
|
//! This example demonstrates in particular:
|
|
//!
|
|
//! * self-scheduling methods,
|
|
//! * model initialization,
|
|
//! * simulation monitoring with event streams.
|
|
//!
|
|
//! ```text
|
|
//! ┌──────────┐ ┌──────────┐
|
|
//! PPS │ │ coil currents │ │ position
|
|
//! Pulse rate ●─────────▶│ Driver ├───────────────▶│ Motor ├──────────▶
|
|
//! (±freq) │ │ (IA, IB) │ │ (0:199)
|
|
//! └──────────┘ └──────────┘
|
|
//! ```
|
|
|
|
use std::future::Future;
|
|
use std::pin::Pin;
|
|
use std::time::Duration;
|
|
|
|
use asynchronix::model::{InitializedModel, Model, Output};
|
|
use asynchronix::simulation::{Mailbox, SimInit};
|
|
use asynchronix::time::{MonotonicTime, Scheduler};
|
|
|
|
/// Stepper motor.
|
|
pub struct Motor {
|
|
/// Position [-] -- output port.
|
|
pub position: Output<u16>,
|
|
|
|
/// Position [-] -- internal state.
|
|
pos: u16,
|
|
/// Torque applied by the load [N·m] -- internal state.
|
|
torque: f64,
|
|
}
|
|
|
|
impl Motor {
|
|
/// Number of steps per revolution.
|
|
pub const STEPS_PER_REV: u16 = 200;
|
|
/// Torque constant of the motor [N·m·A⁻¹].
|
|
pub const TORQUE_CONSTANT: f64 = 1.0;
|
|
|
|
/// Creates a motor with the specified initial position.
|
|
fn new(position: u16) -> Self {
|
|
Self {
|
|
position: Default::default(),
|
|
pos: position % Self::STEPS_PER_REV,
|
|
torque: 0.0,
|
|
}
|
|
}
|
|
|
|
/// Coil currents [A] -- input port.
|
|
///
|
|
/// For the sake of simplicity, we do as if the rotor rotates
|
|
/// instantaneously. If the current is too weak to overcome the load or when
|
|
/// attempting to move to an opposite phase, the position remains unchanged.
|
|
pub async fn current_in(&mut self, current: (f64, f64)) {
|
|
assert!(!current.0.is_nan() && !current.1.is_nan());
|
|
|
|
let (target_phase, abs_current) = match (current.0 != 0.0, current.1 != 0.0) {
|
|
(false, false) => return,
|
|
(true, false) => (if current.0 > 0.0 { 0 } else { 2 }, current.0.abs()),
|
|
(false, true) => (if current.1 > 0.0 { 1 } else { 3 }, current.1.abs()),
|
|
_ => panic!("current detected in both coils"),
|
|
};
|
|
|
|
if abs_current < Self::TORQUE_CONSTANT * self.torque {
|
|
return;
|
|
}
|
|
let pos_delta = match target_phase - (self.pos % 4) as i8 {
|
|
0 | 2 | -2 => return,
|
|
1 | -3 => 1,
|
|
-1 | 3 => Self::STEPS_PER_REV - 1,
|
|
_ => unreachable!(),
|
|
};
|
|
|
|
self.pos = (self.pos + pos_delta) % Self::STEPS_PER_REV;
|
|
self.position.send(self.pos).await;
|
|
}
|
|
|
|
/// Torque applied by the load [N·m] -- input port.
|
|
pub fn load(&mut self, torque: f64) {
|
|
assert!(torque >= 0.0);
|
|
|
|
self.torque = torque;
|
|
}
|
|
}
|
|
|
|
impl Model for Motor {
|
|
/// Broadcasts the initial position of the motor.
|
|
fn init(
|
|
mut self,
|
|
_scheduler: &Scheduler<Self>,
|
|
) -> Pin<Box<dyn Future<Output = InitializedModel<Self>> + Send + '_>> {
|
|
Box::pin(async move {
|
|
self.position.send(self.pos).await;
|
|
|
|
self.into()
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Stepper motor driver.
|
|
pub struct Driver {
|
|
/// Coil A and coil B currents [A] -- output port.
|
|
pub current_out: Output<(f64, f64)>,
|
|
|
|
/// Requested pulse rate (pulse per second) [Hz] -- internal state.
|
|
pps: f64,
|
|
/// Phase for the next pulse (= 0, 1, 2 or 3) -- internal state.
|
|
next_phase: u8,
|
|
/// Nominal coil current (absolute value) [A] -- constant.
|
|
current: f64,
|
|
}
|
|
|
|
impl Driver {
|
|
/// Minimum supported pulse rate [Hz].
|
|
const MIN_PPS: f64 = 1.0;
|
|
/// Maximum supported pulse rate [Hz].
|
|
const MAX_PPS: f64 = 1_000.0;
|
|
|
|
/// Creates a new driver with the specified nominal current.
|
|
pub fn new(nominal_current: f64) -> Self {
|
|
Self {
|
|
current_out: Default::default(),
|
|
pps: 0.0,
|
|
next_phase: 0,
|
|
current: nominal_current,
|
|
}
|
|
}
|
|
|
|
/// Sets the pulse rate (sign = direction) [Hz] -- input port.
|
|
pub async fn pulse_rate(&mut self, pps: f64, scheduler: &Scheduler<Self>) {
|
|
let pps = pps.signum() * pps.abs().clamp(Self::MIN_PPS, Self::MAX_PPS);
|
|
if pps == self.pps {
|
|
return;
|
|
}
|
|
|
|
let is_idle = self.pps == 0.0;
|
|
self.pps = pps;
|
|
|
|
// Trigger the rotation if the motor is currently idle. Otherwise the
|
|
// new value will be accounted for at the next pulse.
|
|
if is_idle {
|
|
self.send_pulse((), scheduler).await;
|
|
}
|
|
}
|
|
|
|
/// Sends a pulse and schedules the next one.
|
|
///
|
|
/// Note: self-scheduling async methods must be for now defined with an
|
|
/// explicit signature instead of `async fn` due to a rustc issue.
|
|
fn send_pulse<'a>(
|
|
&'a mut self,
|
|
_: (),
|
|
scheduler: &'a Scheduler<Self>,
|
|
) -> impl Future<Output = ()> + Send + 'a {
|
|
async move {
|
|
let current_out = match self.next_phase {
|
|
0 => (self.current, 0.0),
|
|
1 => (0.0, self.current),
|
|
2 => (-self.current, 0.0),
|
|
3 => (0.0, -self.current),
|
|
_ => unreachable!(),
|
|
};
|
|
self.current_out.send(current_out).await;
|
|
|
|
if self.pps == 0.0 {
|
|
return;
|
|
}
|
|
|
|
self.next_phase = (self.next_phase + (self.pps.signum() + 4.0) as u8) % 4;
|
|
|
|
let pulse_duration = Duration::from_secs_f64(1.0 / self.pps.abs());
|
|
|
|
// Schedule the next pulse.
|
|
scheduler
|
|
.schedule_event_in(pulse_duration, Self::send_pulse, ())
|
|
.unwrap();
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Model for Driver {}
|
|
|
|
fn main() {
|
|
// ---------------
|
|
// Bench assembly.
|
|
// ---------------
|
|
|
|
// Models.
|
|
let init_pos = 123;
|
|
let mut motor = Motor::new(init_pos);
|
|
let mut driver = Driver::new(1.0);
|
|
|
|
// Mailboxes.
|
|
let motor_mbox = Mailbox::new();
|
|
let driver_mbox = Mailbox::new();
|
|
|
|
// Connections.
|
|
driver.current_out.connect(Motor::current_in, &motor_mbox);
|
|
|
|
// Model handles for simulation.
|
|
let mut position = motor.position.connect_stream().0;
|
|
let motor_addr = motor_mbox.address();
|
|
let driver_addr = driver_mbox.address();
|
|
|
|
// Start time (arbitrary since models do not depend on absolute time).
|
|
let t0 = MonotonicTime::EPOCH;
|
|
|
|
// Assembly and initialization.
|
|
let mut simu = SimInit::new()
|
|
.add_model(driver, driver_mbox)
|
|
.add_model(motor, motor_mbox)
|
|
.init(t0);
|
|
|
|
// ----------
|
|
// Simulation.
|
|
// ----------
|
|
|
|
// Check initial conditions.
|
|
let mut t = t0;
|
|
assert_eq!(simu.time(), t);
|
|
assert_eq!(position.next(), Some(init_pos));
|
|
assert!(position.next().is_none());
|
|
|
|
// Start the motor in 2s with a PPS of 10Hz.
|
|
simu.schedule_event_in(
|
|
Duration::from_secs(2),
|
|
Driver::pulse_rate,
|
|
10.0,
|
|
&driver_addr,
|
|
)
|
|
.unwrap();
|
|
|
|
// Advance simulation time to two next events.
|
|
simu.step();
|
|
t += Duration::new(2, 0);
|
|
assert_eq!(simu.time(), t);
|
|
simu.step();
|
|
t += Duration::new(0, 100_000_000);
|
|
assert_eq!(simu.time(), t);
|
|
|
|
// Whichever the starting position, after two phase increments from the
|
|
// driver the rotor should have synchronized with the driver, with a
|
|
// position given by this beautiful formula.
|
|
let mut pos = (((init_pos + 1) / 4) * 4 + 1) % Motor::STEPS_PER_REV;
|
|
assert_eq!(position.by_ref().last().unwrap(), pos);
|
|
|
|
// Advance simulation time by 0.9s, which with a 10Hz PPS should correspond to
|
|
// 9 position increments.
|
|
simu.step_by(Duration::new(0, 900_000_000));
|
|
t += Duration::new(0, 900_000_000);
|
|
assert_eq!(simu.time(), t);
|
|
for _ in 0..9 {
|
|
pos = (pos + 1) % Motor::STEPS_PER_REV;
|
|
assert_eq!(position.next(), Some(pos));
|
|
}
|
|
assert!(position.next().is_none());
|
|
|
|
// Increase the load beyond the torque limit for a 1A driver current.
|
|
simu.send_event(Motor::load, 2.0, &motor_addr);
|
|
|
|
// Advance simulation time and check that the motor is blocked.
|
|
simu.step();
|
|
t += Duration::new(0, 100_000_000);
|
|
assert_eq!(simu.time(), t);
|
|
assert!(position.next().is_none());
|
|
|
|
// Do it again.
|
|
simu.step();
|
|
t += Duration::new(0, 100_000_000);
|
|
assert_eq!(simu.time(), t);
|
|
assert!(position.next().is_none());
|
|
|
|
// Decrease the load below the torque limit for a 1A driver current and
|
|
// advance simulation time.
|
|
simu.send_event(Motor::load, 0.5, &motor_addr);
|
|
simu.step();
|
|
t += Duration::new(0, 100_000_000);
|
|
|
|
// The motor should start moving again, but since the phase was incremented
|
|
// 3 times (out of 4 phases) while the motor was blocked, the motor actually
|
|
// makes a step backward before it moves forward again.
|
|
assert_eq!(simu.time(), t);
|
|
pos = (pos + Motor::STEPS_PER_REV - 1) % Motor::STEPS_PER_REV;
|
|
assert_eq!(position.next(), Some(pos));
|
|
|
|
// Advance simulation time by 0.7s, which with a 10Hz PPS should correspond to
|
|
// 7 position increments.
|
|
simu.step_by(Duration::new(0, 700_000_000));
|
|
t += Duration::new(0, 700_000_000);
|
|
assert_eq!(simu.time(), t);
|
|
for _ in 0..7 {
|
|
pos = (pos + 1) % Motor::STEPS_PER_REV;
|
|
assert_eq!(position.next(), Some(pos));
|
|
}
|
|
assert!(position.next().is_none());
|
|
|
|
// Now make the motor rotate in the opposite direction. Note that this
|
|
// driver only accounts for a new PPS at the next pulse.
|
|
simu.send_event(Driver::pulse_rate, -10.0, &driver_addr);
|
|
simu.step();
|
|
t += Duration::new(0, 100_000_000);
|
|
assert_eq!(simu.time(), t);
|
|
pos = (pos + 1) % Motor::STEPS_PER_REV;
|
|
assert_eq!(position.next(), Some(pos));
|
|
|
|
// Advance simulation time by 1.9s, which with a -10Hz PPS should correspond
|
|
// to 19 position decrements.
|
|
simu.step_by(Duration::new(1, 900_000_000));
|
|
t += Duration::new(1, 900_000_000);
|
|
assert_eq!(simu.time(), t);
|
|
pos = (pos + Motor::STEPS_PER_REV - 19) % Motor::STEPS_PER_REV;
|
|
assert_eq!(position.by_ref().last(), Some(pos));
|
|
}
|