forked from ROMEO/nexosim

This reverts commit 7e881afb638ccc0dbcfc7b539fc152dc923d63e1, reversing changes made to 9d78e4f72a4c6ff459fc386b2f25beae40b94429.
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(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(
|
|
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));
|
|
}
|