1
0
forked from ROMEO/nexosim
nexosim/asynchronix/examples/stepper_motor.rs
Serge Barral 1be2f48a00 Revert "Merge pull request #12 from asynchronics/feature/event-sinks"
This reverts commit 7e881afb638ccc0dbcfc7b539fc152dc923d63e1, reversing
changes made to 9d78e4f72a4c6ff459fc386b2f25beae40b94429.
2024-03-06 16:16:55 +01:00

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));
}