# Motor axis
Single-axis motor controller with homing support. Drives a motor to a target position using encoder pulse counting or time-based step control.
Motor axis
TG
E
H
HS
S
HB
FQ
F
R
P
AT
HD
M
PP
RPM
# Inputs
| ID | Abbrev | Name | Type | Default | Description |
|---|---|---|---|---|---|
target | TG | Target | NUMBER | 0 | Target position in encoder counts. Movement starts automatically after homing. |
encoder | E | Encoder | BOOLEAN | false | Raw encoder pulse input. Each rising edge counts one step in the current motor direction. |
home | H | Home | BOOLEAN | false | Pulse input. Rising edge starts the homing sequence, moving the motor in the configured direction until the home switch activates. |
home_switch | HS | Home switch | BOOLEAN | false | Active (true) when the axis is at the home/reference position. Used during homing to detect arrival at the reference point. |
stop | S | Stop | BOOLEAN | false | Pulse input. Rising edge stops all movement and cancels homing. Dominant input. Position and homed status are preserved. |
home_before_move | HB | Home before move | BOOLEAN | false | When active, the motor automatically homes before moving to each new target. If the home switch is already active, movement starts immediately from position zero. Otherwise, the motor homes first and then proceeds to the target. |
freq | FQ | Frequency | NUMBER | 0 | Encoder pulse frequency in Hz. Used in Frequency control mode. The block calculates pulse count from frequency and elapsed time between inputs, switching to time-based estimation for the final approach. |
# Outputs
| ID | Abbrev | Name | Type | Default | Description |
|---|---|---|---|---|---|
forward | F | Forward | BOOLEAN | false | Active when the motor should drive in the positive direction. |
reverse | R | Reverse | BOOLEAN | false | Active when the motor should drive in the negative direction. |
position | P | Position | NUMBER | 0 | Current position in encoder counts. |
at_target | AT | At target | BOOLEAN | false | True when the current position is within the configured tolerance of the target. |
homed | HD | Homed | BOOLEAN | false | True after a successful homing sequence has completed. |
moving | M | Moving | BOOLEAN | false | True when the motor is actively driving in either direction. |
position_pct | PP | Position % | NUMBER | 0 | Current position as a percentage (0-100) of the configured full travel length. |
rpm | RPM | RPM | NUMBER | 0 | Current motor speed in revolutions per minute. |
# Configuration
| ID | Name | Type | Default | Unit | Description |
|---|---|---|---|---|---|
home_direction | Home direction | ENUM | 0 | Motor direction during the homing sequence. Details: Values: Forward, Reverse | |
position_tolerance | Position tolerance | NUMBER | 0 | Maximum acceptable distance from the target position to be considered at target. 0 means exact match. Details: ≥ 0 | |
control_mode | Control mode | ENUM | 0 | Encoder mode uses external encoder pulses to track position. Time-based mode uses internally timed steps. Frequency mode tracks position from periodic frequency (Hz) inputs, with automatic time-based finish for the final approach. Details: Values: Encoder, Time-based, Frequency | |
pulse_time_forward | Pulse time forward | NUMBER | 0.1 | s | Duration of one position step when moving forward. Used in Time-based and Frequency modes. In Frequency mode serves as initial estimate before real data arrives. Details: Visible when control_mode = Time-based, Frequency> 0 |
pulse_time_reverse | Pulse time reverse | NUMBER | 0.1 | s | Duration of one position step when moving in reverse. Used in Time-based and Frequency modes. In Frequency mode serves as initial estimate before real data arrives. Details: Visible when control_mode = Time-based, Frequency> 0 |
motor_lock_duration | Motor lock duration | NUMBER | 0 | s | Pause duration between direction changes to protect the motor. The motor stops for this period before starting in the opposite direction. Set to 0 to disable. Details: ≥ 0 |
full_travel_steps | Full travel steps | NUMBER | 0 | Number of steps that represent 100% of the axis travel. When set to 0, the position percentage output is disabled. Details: ≥ 0 | |
steps_per_revolution | Steps per revolution | NUMBER | 0 | Number of encoder/frequency pulses per one full shaft revolution. Set to 0 to disable RPM output. Details: ≥ 0 |
# State
| ID | Name | Type | Default | Unit | Description |
|---|---|---|---|---|---|
prev_encoder | Previous encoder state | BOOLEAN | false | Stores the previous state of the encoder input to detect rising edges. | |
prev_home | Previous home state | BOOLEAN | false | Stores the previous state of the home input to detect rising edges. | |
prev_stop | Previous stop state | BOOLEAN | false | Stores the previous state of the stop input to detect rising edges. | |
is_homing | Is homing | BOOLEAN | false | True while the homing sequence is in progress. | |
pending_target | Pending target | NUMBER | 0 | Stores the target position to move to after homing completes when home-before-move is active. | |
move_after_home | Move after home | BOOLEAN | false | True when the motor should automatically move to the pending target after homing completes. | |
move_start_time | Move start time | NUMBER | 0 | System uptime when the current time-based movement started. Used to compute elapsed time for position tracking. | |
move_start_position | Move start position | NUMBER | 0 | Position when the current time-based movement started. Used with elapsed time to compute the current position. | |
move_target | Move target | NUMBER | 0 | Target position for the current time-based movement. | |
was_forward | Was forward | BOOLEAN | false | True if the last motor movement was in the forward direction. Used for motor lock delay calculation. | |
last_stop_time | Last stop time | NUMBER | 0 | System uptime when the motor last stopped. Used for motor lock delay calculation. | |
lock_target | Lock target | NUMBER | 0 | Target position stored while waiting for the motor lock delay to complete. | |
lock_position | Lock position | NUMBER | 0 | Current position stored while waiting for the motor lock delay to complete. | |
last_freq_time | Last frequency input time | NUMBER | 0 | System uptime when the last frequency input was received or movement started. | |
avg_pulse_time_ms | Average pulse time | NUMBER | 0.0 | Running average of pulse duration in milliseconds, calculated from frequency inputs during travel. | |
freq_sample_count | Frequency sample count | NUMBER | 0 | Number of frequency input samples received during the current movement. | |
freq_finishing | Frequency finishing | BOOLEAN | false | True when the block has switched to time-based estimation for the final approach in Frequency mode. | |
fractional_pulses | Fractional pulses | NUMBER | 0 | Accumulated fractional pulse remainder from frequency-to-pulse calculations. | |
last_encoder_pulse_time | Last encoder pulse time | NUMBER | 0 | System uptime of the last encoder rising edge. Used to calculate inter-pulse timing for RPM. |
# Source Code
View Volang source
channel = input::channel()
value = input::value()
fn stop_motors() {
callback::clear()
if (output::get("forward") or output::get("reverse")) {
state::set("was_forward", output::get("forward"))
state::set("last_stop_time", time::uptime())
}
output::set("forward", false)
output::set("reverse", false)
output::set("moving", false)
output::set("rpm", 0)
state::set("last_encoder_pulse_time", 0)
}
fn start_forward() {
output::set("forward", true)
output::set("reverse", false)
output::set("moving", true)
}
fn start_reverse() {
output::set("forward", false)
output::set("reverse", true)
output::set("moving", true)
}
fn set_position(pos) {
output::set("position", pos)
full = config::get("full_travel_steps")
if (full > 0) {
pct = pos * 100.0 / full
if (pct < 0) {
pct = 0
}
if (pct > 100) {
pct = 100
}
output::set("position_pct", pct)
}
}
fn set_rpm(pulses_per_second) {
spr = config::get("steps_per_revolution")
if (spr > 0) {
output::set("rpm", pulses_per_second * 60.0 / spr)
}
}
extern fn onStep(v) {
start_time = state::get("move_start_time")
start_pos = state::get("move_start_position")
target = state::get("move_target")
is_forward = output::get("forward")
pulse_ms = 0
if (is_forward) {
pulse_ms = math::round(config::get("pulse_time_forward") * 1000)
} else {
pulse_ms = math::round(config::get("pulse_time_reverse") * 1000)
}
total_steps = target - start_pos
if (total_steps < 0) {
total_steps = 0 - total_steps
}
total_ms = total_steps * pulse_ms
elapsed = time::uptime() - start_time
if (elapsed >= total_ms) {
set_position(target)
stop_motors()
output::set("at_target", true)
return
}
steps_float = elapsed / pulse_ms
steps_int = math::round(steps_float)
if (steps_int * pulse_ms > elapsed) {
steps_int = steps_int - 1
}
position = start_pos
if (is_forward) {
position = start_pos + steps_int
if (position > target) {
position = target
}
} else {
position = start_pos - steps_int
if (position < target) {
position = target
}
}
set_position(position)
remaining_ms = total_ms - elapsed
if (remaining_ms <= 100) {
callback::set(math::round(remaining_ms), "onStep", 0)
} else {
callback::set(100, "onStep", 0)
}
}
extern fn onFreqFinish(v) {
target = state::get("move_target")
set_position(target)
stop_motors()
output::set("at_target", true)
}
fn init_freq_state() {
state::set("last_freq_time", time::uptime())
state::set("avg_pulse_time_ms", 0)
state::set("freq_sample_count", 0)
state::set("freq_finishing", false)
state::set("fractional_pulses", 0)
}
fn evaluate_target_now(target, position) {
tolerance = config::get("position_tolerance")
diff = target - position
control_mode = config::get("control_mode")
if (diff > tolerance) {
start_forward()
output::set("at_target", false)
if (control_mode == 1) {
state::set("move_start_time", time::uptime())
state::set("move_start_position", position)
state::set("move_target", target)
steps = math::round(diff)
pulse_ms = math::round(config::get("pulse_time_forward") * 1000)
set_rpm(1000.0 / pulse_ms)
total_ms = steps * pulse_ms
first_cb = 100
if (total_ms < first_cb) {
first_cb = total_ms
}
callback::set(first_cb, "onStep", 0)
}
if (control_mode == 2) {
state::set("move_target", target)
init_freq_state()
pulse_ms = math::round(config::get("pulse_time_forward") * 1000)
state::set("avg_pulse_time_ms", pulse_ms * 1.0)
total_ms = math::round(diff) * pulse_ms
callback::set(math::round(total_ms), "onFreqFinish", 0)
}
} else if (diff < (0 - tolerance)) {
start_reverse()
output::set("at_target", false)
if (control_mode == 1) {
state::set("move_start_time", time::uptime())
state::set("move_start_position", position)
state::set("move_target", target)
steps = math::round(0 - diff)
pulse_ms = math::round(config::get("pulse_time_reverse") * 1000)
set_rpm(1000.0 / pulse_ms)
total_ms = steps * pulse_ms
first_cb = 100
if (total_ms < first_cb) {
first_cb = total_ms
}
callback::set(first_cb, "onStep", 0)
}
if (control_mode == 2) {
state::set("move_target", target)
init_freq_state()
pulse_ms = math::round(config::get("pulse_time_reverse") * 1000)
state::set("avg_pulse_time_ms", pulse_ms * 1.0)
total_ms = math::round(0 - diff) * pulse_ms
callback::set(math::round(total_ms), "onFreqFinish", 0)
}
} else {
stop_motors()
output::set("at_target", true)
}
}
extern fn onMotorLock(v) {
evaluate_target_now(state::get("lock_target"), state::get("lock_position"))
}
fn evaluate_target(target, position) {
callback::clear()
lock_ms = math::round(config::get("motor_lock_duration") * 1000)
if (lock_ms <= 0) {
evaluate_target_now(target, position)
return
}
tolerance = config::get("position_tolerance")
diff = target - position
going_forward = diff > tolerance
going_reverse = diff < (0 - tolerance)
if (!going_forward and !going_reverse) {
evaluate_target_now(target, position)
return
}
direction_changed = false
if (going_forward and !state::get("was_forward")) {
direction_changed = true
}
if (going_reverse and state::get("was_forward")) {
direction_changed = true
}
if (!direction_changed) {
evaluate_target_now(target, position)
return
}
elapsed_ms = time::uptime() - state::get("last_stop_time")
if (elapsed_ms >= lock_ms) {
evaluate_target_now(target, position)
return
}
remaining_ms = lock_ms - elapsed_ms
output::set("at_target", false)
state::set("lock_target", target)
state::set("lock_position", position)
callback::set(math::round(remaining_ms), "onMotorLock", 0)
}
fn complete_homing() {
stop_motors()
set_position(0)
output::set("homed", true)
state::set("is_homing", false)
if (state::get("move_after_home")) {
state::set("move_after_home", false)
pending_target = state::get("pending_target")
evaluate_target(pending_target, 0)
} else {
output::set("at_target", true)
}
}
fn start_homing_sequence() {
callback::clear()
output::set("homed", false)
output::set("at_target", false)
state::set("is_homing", true)
if (input::get("home_switch")) {
complete_homing()
} else {
home_direction = config::get("home_direction") // 0=Forward, 1=Reverse
if (home_direction == 0) {
start_forward()
} else {
start_reverse()
}
}
}
// Handle "stop" input first (dominant input)
if (channel == "stop") {
prev_stop = state::get("prev_stop")
if (value and !prev_stop) {
stop_motors()
state::set("is_homing", false)
}
state::set("prev_stop", value)
return
}
// Check home_switch on every execution during homing (level-triggered)
// This ensures homing completes even if the switch was already active
if (state::get("is_homing") and input::get("home_switch")) {
complete_homing()
return
}
// Handle "home_switch" input - stop motor when home switch activates outside homing
if (channel == "home_switch") {
if (value and output::get("moving")) {
stop_motors()
set_position(0)
output::set("homed", true)
output::set("at_target", true)
}
return
}
// Handle "home" input
if (channel == "home") {
prev_home = state::get("prev_home")
if (value and !prev_home) {
start_homing_sequence()
}
state::set("prev_home", value)
return
}
// Handle "encoder" input
if (channel == "encoder") {
if (config::get("control_mode") != 0) {
return
}
prev_encoder = state::get("prev_encoder")
if (value and !prev_encoder) {
// Rising edge on encoder - count if motor is driving and not homing
is_homing = state::get("is_homing")
is_forward = output::get("forward")
is_reverse = output::get("reverse")
if (!is_homing and (is_forward or is_reverse)) {
position = output::get("position")
if (is_forward) {
position = position + 1
} else {
position = position - 1
}
set_position(position)
enc_now = time::uptime()
last_enc = state::get("last_encoder_pulse_time")
if (last_enc > 0) {
enc_elapsed = enc_now - last_enc
if (enc_elapsed > 0) {
set_rpm(1000.0 / enc_elapsed)
}
}
state::set("last_encoder_pulse_time", enc_now)
// Check if at target
target = input::get("target")
tolerance = config::get("position_tolerance")
diff = target - position
if (diff >= (0 - tolerance) and diff <= tolerance) {
stop_motors()
output::set("at_target", true)
}
}
}
state::set("prev_encoder", value)
return
}
// Handle "freq" input (Frequency control mode)
if (channel == "freq") {
if (config::get("control_mode") != 2) {
return
}
if (state::get("is_homing")) {
return
}
if (state::get("freq_finishing")) {
return
}
is_forward = output::get("forward")
is_reverse = output::get("reverse")
if (!is_forward and !is_reverse) {
return
}
callback::clear()
now = time::uptime()
last_time = state::get("last_freq_time")
elapsed_ms = now - last_time
state::set("last_freq_time", now)
if (value <= 0 or elapsed_ms <= 0) {
return
}
pulse_count = value * elapsed_ms / 1000.0 + state::get("fractional_pulses")
whole_steps = math::round(pulse_count)
if (pulse_count < whole_steps) {
whole_steps = whole_steps - 1.0
}
state::set("fractional_pulses", pulse_count - whole_steps)
pulse_time = 1000.0 / value
set_rpm(value)
sample_count = state::get("freq_sample_count") + 1
state::set("freq_sample_count", sample_count)
avg_pt = state::get("avg_pulse_time_ms")
config_pt = config::get("pulse_time_forward") * 1000.0
if (is_reverse) {
config_pt = config::get("pulse_time_reverse") * 1000.0
}
if (pulse_time > config_pt / 2.0 and pulse_time < config_pt * 2.0) {
avg_pt = avg_pt + (pulse_time - avg_pt) / (sample_count * 1.0)
state::set("avg_pulse_time_ms", avg_pt)
}
position = output::get("position")
target = state::get("move_target")
if (is_forward) {
position = position + whole_steps
if (position >= target) {
position = target
}
} else {
position = position - whole_steps
if (position <= target) {
position = target
}
}
set_position(position)
remaining = target - position
if (remaining < 0) {
remaining = 0 - remaining
}
tolerance = config::get("position_tolerance")
if (remaining <= tolerance) {
stop_motors()
output::set("at_target", true)
return
}
remaining_time_ms = remaining * avg_pt
callback::set(math::round(remaining_time_ms), "onFreqFinish", 0)
if (remaining_time_ms < elapsed_ms) {
state::set("freq_finishing", true)
}
return
}
// Handle "target" input
if (channel == "target") {
is_homing = state::get("is_homing")
home_before_move = input::get("home_before_move")
if (config::get("control_mode") != 0 and output::get("moving")) {
stop_motors()
state::set("is_homing", false)
is_homing = false
}
if (home_before_move) {
if (is_homing) {
state::set("pending_target", value)
state::set("move_after_home", true)
return
}
if (output::get("moving")) {
stop_motors()
}
if (input::get("home_switch")) {
set_position(0)
output::set("homed", true)
evaluate_target(value, 0)
} else {
state::set("pending_target", value)
state::set("move_after_home", true)
start_homing_sequence()
}
} else {
homed = output::get("homed")
if (homed and !is_homing) {
position = output::get("position")
evaluate_target(value, position)
}
}
return
}
