Skip to main content

Motor Commands

MSP_MOTOR (104)​

Query current motor output values.

Request​

Empty payload (0 bytes).

Response Structure​

8 motor outputs as U16 values:

OffsetTypeFieldRangeDescription
0-1U16motor_11000-2000Motor 1 PWM output
2-3U16motor_21000-2000Motor 2 PWM output
4-5U16motor_31000-2000Motor 3 PWM output
6-7U16motor_41000-2000Motor 4 PWM output
8-9U16motor_50 or 1000-2000Motor 5 (0 if unused)
10-11U16motor_60 or 1000-2000Motor 6 (0 if unused)
12-13U16motor_70 or 1000-2000Motor 7 (0 if unused)
14-15U16motor_80 or 1000-2000Motor 8 (0 if unused)

Total size: 16 bytes

Motor Numbering​

Standard quadcopter motor layout (X configuration):

    Front
1 2 CW: 1, 4
\ / CCW: 2, 3
\ /
X
/ \
/ \
4 3
Rear

Rust Implementation​

/// Motor output values
#[derive(Debug, Clone, Copy, Default)]
pub struct MspMotor {
/// Motor outputs in PWM microseconds (1000-2000)
pub motors: [u16; 8],
}

impl MspMotor {
pub const COMMAND: u8 = 104;
pub const SIZE: usize = 16;

/// Parse from MSP response payload
pub fn from_payload(data: &[u8]) -> Option<Self> {
if data.len() < Self::SIZE {
return None;
}

let mut motors = [0u16; 8];
for i in 0..8 {
motors[i] = u16::from_le_bytes([data[i * 2], data[i * 2 + 1]]);
}

Some(MspMotor { motors })
}

/// Get motor value by index (0-based)
pub fn motor(&self, index: usize) -> u16 {
self.motors.get(index).copied().unwrap_or(0)
}

/// Get active motor count (non-zero outputs)
pub fn active_count(&self) -> usize {
self.motors.iter().filter(|&&m| m > 0).count()
}

/// Get throttle percentage for a motor (0-100%)
pub fn throttle_percent(&self, index: usize) -> f32 {
let value = self.motors.get(index).copied().unwrap_or(1000);
if value == 0 {
return 0.0;
}
((value.saturating_sub(1000)) as f32 / 10.0).clamp(0.0, 100.0)
}

/// Get average throttle across all active motors
pub fn average_throttle(&self) -> f32 {
let active: Vec<_> = self.motors.iter().filter(|&&m| m > 0).collect();
if active.is_empty() {
return 0.0;
}

let sum: u32 = active.iter().map(|&&m| m as u32).sum();
let avg = sum as f32 / active.len() as f32;
((avg - 1000.0) / 10.0).clamp(0.0, 100.0)
}

/// Check if any motor is running
pub fn is_running(&self) -> bool {
self.motors.iter().any(|&m| m > 1050)
}

/// Get maximum motor output
pub fn max_output(&self) -> u16 {
self.motors.iter().copied().max().unwrap_or(0)
}

/// Get minimum motor output (among active motors)
pub fn min_output(&self) -> u16 {
self.motors
.iter()
.copied()
.filter(|&m| m > 0)
.min()
.unwrap_or(0)
}
}

MSP_SET_MOTOR (214)​

Override motor outputs directly. Only works when disarmed unless in motor test mode.

Request Structure​

8 motor values as U16:

OffsetTypeFieldDescription
0-1U16motor_1Motor 1 PWM (1000-2000)
2-3U16motor_2Motor 2 PWM
............
14-15U16motor_8Motor 8 PWM

Response​

Empty (ACK only).

Rust Implementation​

/// Motor control command
#[derive(Debug, Clone, Copy)]
pub struct MspSetMotor {
pub motors: [u16; 8],
}

impl MspSetMotor {
pub const COMMAND: u8 = 214;
pub const SIZE: usize = 16;

/// All motors stopped
pub fn stop() -> Self {
MspSetMotor { motors: [1000; 8] }
}

/// Set all motors to same value
pub fn uniform(value: u16) -> Self {
MspSetMotor {
motors: [value.clamp(1000, 2000); 8],
}
}

/// Set individual motor value
pub fn with_motor(mut self, index: usize, value: u16) -> Self {
if index < 8 {
self.motors[index] = value.clamp(1000, 2000);
}
self
}

/// Test single motor (others at minimum)
pub fn test_motor(motor_index: usize, value: u16) -> Self {
let mut motors = [1000u16; 8];
if motor_index < 8 {
motors[motor_index] = value.clamp(1000, 2000);
}
MspSetMotor { motors }
}

/// Convert to payload bytes
pub fn to_payload(&self) -> [u8; 16] {
let mut payload = [0u8; 16];
for i in 0..8 {
let bytes = self.motors[i].to_le_bytes();
payload[i * 2] = bytes[0];
payload[i * 2 + 1] = bytes[1];
}
payload
}

/// Build complete MSP request frame
pub fn to_frame(&self) -> Vec<u8> {
let payload = self.to_payload();
let size = Self::SIZE as u8;

let mut frame = vec![b'$', b'M', b'<', size, Self::COMMAND];
frame.extend_from_slice(&payload);

let mut checksum = size ^ Self::COMMAND;
for byte in &payload {
checksum ^= byte;
}
frame.push(checksum);

frame
}
}

Raw Hex Examples​

MSP_MOTOR Request​

24 4D 3C 00 68 68

MSP_MOTOR Response (quad at 50% throttle)​

24 4D 3E 10 68
DC 05 ← Motor 1 = 1500 (50%)
DC 05 ← Motor 2 = 1500 (50%)
DC 05 ← Motor 3 = 1500 (50%)
DC 05 ← Motor 4 = 1500 (50%)
00 00 ← Motor 5 = 0 (unused)
00 00 ← Motor 6 = 0 (unused)
00 00 ← Motor 7 = 0 (unused)
00 00 ← Motor 8 = 0 (unused)
XX ← Checksum

MSP_SET_MOTOR Request (test motor 1)​

24 4D 3C 10 D6
58 06 ← Motor 1 = 1624 (62.4%)
E8 03 ← Motor 2 = 1000 (0%)
E8 03 ← Motor 3 = 1000 (0%)
E8 03 ← Motor 4 = 1000 (0%)
E8 03 ← Motor 5 = 1000
E8 03 ← Motor 6 = 1000
E8 03 ← Motor 7 = 1000
E8 03 ← Motor 8 = 1000
XX ← Checksum

Usage Example​

use std::io::{Read, Write};
use std::time::Duration;

/// Motor test sequence
fn motor_test<T: Read + Write>(port: &mut T) -> Result<(), &'static str> {
println!("Starting motor test sequence...");
println!("WARNING: Ensure propellers are removed!");

// Test each motor individually
for motor_index in 0..4 {
println!("Testing motor {}...", motor_index + 1);

// Ramp up
for throttle in (1000..=1200).step_by(50) {
let cmd = MspSetMotor::test_motor(motor_index, throttle);
port.write_all(&cmd.to_frame()).map_err(|_| "Write failed")?;
std::thread::sleep(Duration::from_millis(100));
}

// Hold
std::thread::sleep(Duration::from_millis(500));

// Ramp down
for throttle in (1000..=1200).rev().step_by(50) {
let cmd = MspSetMotor::test_motor(motor_index, throttle);
port.write_all(&cmd.to_frame()).map_err(|_| "Write failed")?;
std::thread::sleep(Duration::from_millis(100));
}

// Stop
let stop = MspSetMotor::stop();
port.write_all(&stop.to_frame()).map_err(|_| "Write failed")?;

std::thread::sleep(Duration::from_millis(500));
}

println!("Motor test complete");
Ok(())
}

fn main() {
// Parse motor response
let payload = [
0x9C, 0x05, // Motor 1 = 1436
0x9C, 0x05, // Motor 2 = 1436
0xA0, 0x05, // Motor 3 = 1440
0x98, 0x05, // Motor 4 = 1432
0x00, 0x00, // Motor 5 = 0
0x00, 0x00, // Motor 6 = 0
0x00, 0x00, // Motor 7 = 0
0x00, 0x00, // Motor 8 = 0
];

if let Some(motors) = MspMotor::from_payload(&payload) {
println!("Active motors: {}", motors.active_count());
println!("Average throttle: {:.1}%", motors.average_throttle());

for i in 0..4 {
println!(
"Motor {}: {} ({:.1}%)",
i + 1,
motors.motor(i),
motors.throttle_percent(i)
);
}

println!("Motors running: {}", motors.is_running());
println!("Max output: {}", motors.max_output());
println!("Min output: {}", motors.min_output());
}
}

Output:

Active motors: 4
Average throttle: 43.6%
Motor 1: 1436 (43.6%)
Motor 2: 1436 (43.6%)
Motor 3: 1440 (44.0%)
Motor 4: 1432 (43.2%)
Motors running: true
Max output: 1440
Min output: 1432

Motor Protocol Considerations​

PWM vs DShot​

ProtocolRangeResolutionFeatures
PWM1000-2000 µs~1000 stepsUniversal
OneShot125125-250 µs~1000 stepsFaster
DShot150-12000-20472048 stepsTelemetry, commands

MSP always uses the 1000-2000 range internally; the flight controller converts to the configured protocol.

DShot Commands​

For DShot-equipped ESCs, use MSP2_SEND_DSHOT_COMMAND (0x3003) for advanced features like:

  • Direction reversal
  • LED control
  • Beeping
  • Settings save

Safety Notes​

  1. Propellers: Always remove propellers when testing motors via MSP.

  2. Arming: MSP_SET_MOTOR only works when disarmed. This is a safety feature.

  3. Motor Test Mode: Some firmwares have a motor test mode that bypasses arming checks. Use with extreme caution.

  4. Timeout: If MSP_SET_MOTOR commands stop, motors revert to normal FC control.

  5. Current Limits: Monitor battery current during motor testing to avoid overcurrent conditions.