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):
| Index | Axis | P Offset | I Offset | D Offset |
|---|---|---|---|---|
| 0 | Roll | 0 | 1 | 2 |
| 1 | Pitch | 3 | 4 | 5 |
| 2 | Yaw | 6 | 7 | 8 |
| 3+ | Additional | ... | ... | ... |
Typical size: 30 bytes (10 items × 3 bytes)
PID Items (Betaflight)
| Index | Item | Description |
|---|---|---|
| 0 | Roll | Roll axis PID |
| 1 | Pitch | Pitch axis PID |
| 2 | Yaw | Yaw axis PID |
| 3 | Level | Self-leveling mode strength |
| 4 | Mag | Heading 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
| Axis | P Range | I Range | D Range |
|---|---|---|---|
| Roll | 30-70 | 30-80 | 15-40 |
| Pitch | 30-70 | 30-80 | 15-40 |
| Yaw | 50-120 | 30-80 | 0-20 |
Tuning Order
- Zero I and D: Start with I=0, D=0
- Set P: Increase until oscillation, then reduce 20%
- Set D: Increase until motors get hot or noise appears
- Set I: Increase until drift stops without overshoot
Common Issues
| Symptom | Likely Cause |
|---|---|
| Oscillation on quick moves | P too high |
| Slow response | P too low |
| Drift over time | I too low |
| Bounce back | I too high |
| Hot motors | D too high |
| Propwash oscillation | D 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")
}