Skip to main content

PID Commands

MSP_PID (112)​

Query PID coefficients for all controller axes.

Request​

Empty payload (0 bytes).

Response Structure​

Array of PID tuples (3 bytes each):

IndexAxisP OffsetI OffsetD Offset
0Roll012
1Pitch345
2Yaw678
3+Additional.........

Typical size: 30 bytes (10 items × 3 bytes)

PID Items (Betaflight)​

IndexItemDescription
0RollRoll axis PID
1PitchPitch axis PID
2YawYaw axis PID
3LevelSelf-leveling mode strength
4MagHeading hold (if applicable)

Rust Implementation​

/// Single PID tuple
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct PidCoefficients {
pub p: u8,
pub i: u8,
pub d: u8,
}

impl PidCoefficients {
pub fn new(p: u8, i: u8, d: u8) -> Self {
PidCoefficients { p, i, d }
}

/// Create from slice
pub fn from_bytes(data: &[u8]) -> Option<Self> {
if data.len() < 3 {
return None;
}
Some(PidCoefficients {
p: data[0],
i: data[1],
d: data[2],
})
}

/// Convert to bytes
pub fn to_bytes(&self) -> [u8; 3] {
[self.p, self.i, self.d]
}

/// Check if PID values are in typical range
pub fn is_valid(&self) -> bool {
// Typical ranges for Betaflight
self.p <= 200 && self.i <= 200 && self.d <= 200
}
}

impl core::fmt::Display for PidCoefficients {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "P:{} I:{} D:{}", self.p, self.i, self.d)
}
}

/// Complete PID configuration
#[derive(Debug, Clone)]
pub struct MspPid {
pub items: Vec<PidCoefficients>,
}

impl MspPid {
pub const COMMAND: u8 = 112;

/// Parse from MSP response payload
pub fn from_payload(data: &[u8]) -> Self {
let item_count = data.len() / 3;
let items = (0..item_count)
.filter_map(|i| PidCoefficients::from_bytes(&data[i * 3..]))
.collect();

MspPid { items }
}

/// Get Roll PID
pub fn roll(&self) -> Option<PidCoefficients> {
self.items.get(0).copied()
}

/// Get Pitch PID
pub fn pitch(&self) -> Option<PidCoefficients> {
self.items.get(1).copied()
}

/// Get Yaw PID
pub fn yaw(&self) -> Option<PidCoefficients> {
self.items.get(2).copied()
}

/// Get Level mode strength
pub fn level(&self) -> Option<PidCoefficients> {
self.items.get(3).copied()
}

/// Set Roll PID
pub fn set_roll(&mut self, pid: PidCoefficients) {
if !self.items.is_empty() {
self.items[0] = pid;
}
}

/// Set Pitch PID
pub fn set_pitch(&mut self, pid: PidCoefficients) {
if self.items.len() > 1 {
self.items[1] = pid;
}
}

/// Set Yaw PID
pub fn set_yaw(&mut self, pid: PidCoefficients) {
if self.items.len() > 2 {
self.items[2] = pid;
}
}

/// Convert to payload bytes
pub fn to_payload(&self) -> Vec<u8> {
let mut payload = Vec::with_capacity(self.items.len() * 3);
for item in &self.items {
payload.extend_from_slice(&item.to_bytes());
}
payload
}
}

MSP_SET_PID (202)​

Write PID coefficients.

Request Structure​

Same format as MSP_PID response.

Response​

Empty (ACK only).

Important​

After writing PIDs, call MSP_EEPROM_WRITE (250) to persist changes.

Rust Implementation​

/// PID write command
pub struct MspSetPid {
pub pid: MspPid,
}

impl MspSetPid {
pub const COMMAND: u8 = 202;

/// Build MSP request frame
pub fn to_frame(&self) -> Vec<u8> {
let payload = self.pid.to_payload();
let size = payload.len() 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_PID Request​

24 4D 3C 00 70 70
│ └─ Checksum
└──── Command (112 = 0x70)

MSP_PID Response (typical Betaflight defaults)​

24 4D 3E 1E 70
2D 2D 17 ← Roll: P=45 I=45 D=23
30 32 1B ← Pitch: P=48 I=50 D=27
50 32 00 ← Yaw: P=80 I=50 D=0
32 00 00 ← Level: P=50 I=0 D=0
28 00 00 ← Mag: P=40 I=0 D=0
... (additional items)
XX ← Checksum

MSP_SET_PID Request​

24 4D 3C 1E CA
32 28 1E ← New Roll: P=50 I=40 D=30
32 28 1E ← New Pitch: P=50 I=40 D=30
55 32 00 ← New Yaw: P=85 I=50 D=0
... (remaining items)
XX ← Checksum

Usage Example​

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

/// Read-modify-write PID values
fn update_pid<T: Read + Write>(
port: &mut T,
roll_p: u8,
pitch_p: u8,
) -> Result<(), &'static str> {
// Step 1: Read current PIDs
let request = [0x24, 0x4D, 0x3C, 0x00, 0x70, 0x70];
port.write_all(&request).map_err(|_| "Write failed")?;

let mut buf = [0u8; 64];
let n = port.read(&mut buf).map_err(|_| "Read failed")?;

if n < 6 || &buf[0..3] != b"$M>" {
return Err("Invalid response");
}

let size = buf[3] as usize;
let payload = &buf[5..5 + size];

// Step 2: Parse and modify
let mut pid = MspPid::from_payload(payload);

if let Some(mut roll) = pid.roll() {
roll.p = roll_p;
pid.set_roll(roll);
}

if let Some(mut pitch) = pid.pitch() {
pitch.p = pitch_p;
pid.set_pitch(pitch);
}

// Step 3: Write back
let set_pid = MspSetPid { pid };
port.write_all(&set_pid.to_frame()).map_err(|_| "Write failed")?;

// Read ACK
let n = port.read(&mut buf).map_err(|_| "Read failed")?;
if n < 6 || buf[2] == b'!' {
return Err("Set PID failed");
}

// Step 4: Save to EEPROM
let eeprom = [0x24, 0x4D, 0x3C, 0x00, 0xFA, 0xFA];
port.write_all(&eeprom).map_err(|_| "Write failed")?;

let n = port.read(&mut buf).map_err(|_| "Read failed")?;
if n < 6 || buf[2] == b'!' {
return Err("EEPROM write failed");
}

Ok(())
}

fn main() {
// Parse example response
let payload = [
0x2D, 0x2D, 0x17, // Roll: P=45, I=45, D=23
0x30, 0x32, 0x1B, // Pitch: P=48, I=50, D=27
0x50, 0x32, 0x00, // Yaw: P=80, I=50, D=0
0x32, 0x00, 0x00, // Level: P=50
0x28, 0x00, 0x00, // Mag: P=40
];

let pid = MspPid::from_payload(&payload);

println!("Current PID Configuration:");
println!(" Roll: {}", pid.roll().unwrap());
println!(" Pitch: {}", pid.pitch().unwrap());
println!(" Yaw: {}", pid.yaw().unwrap());

if let Some(level) = pid.level() {
println!(" Level: {}", level);
}
}

Output:

Current PID Configuration:
Roll: P:45 I:45 D:23
Pitch: P:48 I:50 D:27
Yaw: P:80 I:50 D:0
Level: P:50 I:0 D:0

Tuning Guidelines​

Typical Value Ranges​

AxisP RangeI RangeD Range
Roll30-7030-8015-40
Pitch30-7030-8015-40
Yaw50-12030-800-20

Tuning Order​

  1. Zero I and D: Start with I=0, D=0
  2. Set P: Increase until oscillation, then reduce 20%
  3. Set D: Increase until motors get hot or noise appears
  4. Set I: Increase until drift stops without overshoot

Common Issues​

SymptomLikely Cause
Oscillation on quick movesP too high
Slow responseP too low
Drift over timeI too low
Bounce backI too high
Hot motorsD too high
Propwash oscillationD too low

PID Profile Switching​

Use MSP_SELECT_SETTING (210) to switch between PID profiles:

/// Switch to PID profile (0, 1, or 2)
fn select_pid_profile<T: Write>(port: &mut T, profile: u8) -> Result<(), &'static str> {
let payload = [profile.min(2)];
let checksum = 1u8 ^ 210u8 ^ payload[0];
let request = [0x24, 0x4D, 0x3C, 0x01, 0xD2, payload[0], checksum];
port.write_all(&request).map_err(|_| "Write failed")
}