Motor Commands
MSP_MOTOR (104)
Query current motor output values.
Request
Empty payload (0 bytes).
Response Structure
8 motor outputs as U16 values:
| Offset | Type | Field | Range | Description |
|---|---|---|---|---|
| 0-1 | U16 | motor_1 | 1000-2000 | Motor 1 PWM output |
| 2-3 | U16 | motor_2 | 1000-2000 | Motor 2 PWM output |
| 4-5 | U16 | motor_3 | 1000-2000 | Motor 3 PWM output |
| 6-7 | U16 | motor_4 | 1000-2000 | Motor 4 PWM output |
| 8-9 | U16 | motor_5 | 0 or 1000-2000 | Motor 5 (0 if unused) |
| 10-11 | U16 | motor_6 | 0 or 1000-2000 | Motor 6 (0 if unused) |
| 12-13 | U16 | motor_7 | 0 or 1000-2000 | Motor 7 (0 if unused) |
| 14-15 | U16 | motor_8 | 0 or 1000-2000 | Motor 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:
| Offset | Type | Field | Description |
|---|---|---|---|
| 0-1 | U16 | motor_1 | Motor 1 PWM (1000-2000) |
| 2-3 | U16 | motor_2 | Motor 2 PWM |
| ... | ... | ... | ... |
| 14-15 | U16 | motor_8 | Motor 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
| Protocol | Range | Resolution | Features |
|---|---|---|---|
| PWM | 1000-2000 µs | ~1000 steps | Universal |
| OneShot125 | 125-250 µs | ~1000 steps | Faster |
| DShot150-1200 | 0-2047 | 2048 steps | Telemetry, 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
-
Propellers: Always remove propellers when testing motors via MSP.
-
Arming: MSP_SET_MOTOR only works when disarmed. This is a safety feature.
-
Motor Test Mode: Some firmwares have a motor test mode that bypasses arming checks. Use with extreme caution.
-
Timeout: If MSP_SET_MOTOR commands stop, motors revert to normal FC control.
-
Current Limits: Monitor battery current during motor testing to avoid overcurrent conditions.