Skip to main content

Checksum Algorithms

MSP uses two different checksum algorithms depending on the protocol version.

XOR Checksum (MSPv1)​

MSPv1 uses a simple XOR-based checksum for error detection.

Algorithm​

The checksum is calculated by XOR-ing together:

  1. The payload size byte
  2. The command byte
  3. All payload bytes
checksum = size XOR command XOR payload[0] XOR payload[1] XOR ... XOR payload[n-1]

Rust Implementation​

/// Calculate MSPv1 XOR checksum
pub fn msp_v1_checksum(size: u8, command: u8, payload: &[u8]) -> u8 {
let mut checksum = size ^ command;
for byte in payload {
checksum ^= byte;
}
checksum
}

/// Verify MSPv1 checksum
pub fn verify_v1_checksum(size: u8, command: u8, payload: &[u8], received: u8) -> bool {
msp_v1_checksum(size, command, payload) == received
}

Example Calculation​

For an MSP_ATTITUDE request (command 108, empty payload):

fn example_attitude_request_checksum() {
let size: u8 = 0; // No payload
let command: u8 = 108; // MSP_ATTITUDE

let checksum = size ^ command;
// checksum = 0 ^ 108 = 108 (0x6C)

println!("Checksum: 0x{:02X}", checksum);
}

Frame bytes: 24 4D 3C 00 6C 6C

  • $M< header with request direction
  • 00 size
  • 6C command (108)
  • 6C checksum (0 ^ 108 = 108)

Parsing Response Example​

fn parse_attitude_response() {
// Example response: roll=150 (15.0°), pitch=50 (5.0°), yaw=1800 (180.0°)
let response = [
0x24, 0x4D, 0x3E, // $M>
0x06, // size = 6 bytes
0x6C, // command = 108
0x96, 0x00, // roll = 150 (little-endian)
0x32, 0x00, // pitch = 50 (little-endian)
0x08, 0x07, // yaw = 1800 (little-endian)
0xC5, // checksum
];

let size = response[3];
let command = response[4];
let payload = &response[5..11];

// Verify: 6 ^ 108 ^ 0x96 ^ 0x00 ^ 0x32 ^ 0x00 ^ 0x08 ^ 0x07
let mut checksum = size ^ command;
for byte in payload {
checksum ^= byte;
}

assert_eq!(checksum, response[11]);
println!("Checksum verified: 0x{:02X}", checksum);
}

CRC8-DVB-S2 (MSPv2)​

MSPv2 uses the CRC8-DVB-S2 polynomial for better error detection.

Algorithm Details​

  • Polynomial: 0xD5 (x^8 + x^7 + x^6 + x^4 + x^2 + 1)
  • Initial value: 0x00
  • Input reflected: No
  • Output reflected: No

Lookup Table Implementation​

/// CRC8-DVB-S2 with lookup table for optimal performance
pub struct Crc8DvbS2 {
table: [u8; 256],
}

impl Crc8DvbS2 {
/// Create new CRC calculator with precomputed table
pub const fn new() -> Self {
Crc8DvbS2 {
table: Self::generate_table(),
}
}

/// Generate lookup table at compile time
const fn generate_table() -> [u8; 256] {
let mut table = [0u8; 256];
let mut i = 0usize;

while i < 256 {
let mut crc = i as u8;
let mut bit = 0;

while bit < 8 {
if crc & 0x80 != 0 {
crc = (crc << 1) ^ 0xD5;
} else {
crc <<= 1;
}
bit += 1;
}

table[i] = crc;
i += 1;
}

table
}

/// Calculate CRC for data slice
pub fn calculate(&self, data: &[u8]) -> u8 {
let mut crc: u8 = 0;
for byte in data {
crc = self.table[(crc ^ byte) as usize];
}
crc
}

/// Update running CRC with single byte
pub fn update(&self, crc: u8, byte: u8) -> u8 {
self.table[(crc ^ byte) as usize]
}
}

// Static instance for global use
pub static CRC8: Crc8DvbS2 = Crc8DvbS2::new();

Bitwise Implementation (no_std friendly)​

/// CRC8-DVB-S2 without lookup table (smaller code size)
pub fn crc8_dvb_s2(data: &[u8]) -> u8 {
let mut crc: u8 = 0;

for byte in data {
crc ^= byte;
for _ in 0..8 {
if crc & 0x80 != 0 {
crc = (crc << 1) ^ 0xD5;
} else {
crc <<= 1;
}
}
}

crc
}

/// Incremental CRC update
pub fn crc8_dvb_s2_update(crc: u8, byte: u8) -> u8 {
let mut crc = crc ^ byte;
for _ in 0..8 {
if crc & 0x80 != 0 {
crc = (crc << 1) ^ 0xD5;
} else {
crc <<= 1;
}
}
crc
}

MSPv2 Checksum Coverage​

The CRC8 covers the following bytes:

┌─────────────────────────────────────────────────────────────┐
│ $ │ X │ dir │ flags │ cmd_lo │ cmd_hi │ sz_lo │ sz_hi │ payload │ CRC │
│ │ │ │ ✓ │ ✓ │ ✓ │ ✓ │ ✓ │ ✓ │ │
└─────────────────────────────────────────────────────────────────────────────┘
└──────────────── CRC coverage ──────────────────┘

Example Calculation​

fn example_v2_crc() {
// MSPv2 frame for MSP2_BETAFLIGHT_BIND (0x3000), empty payload
let flags: u8 = 0x00;
let command: u16 = 0x3000;
let size: u16 = 0;

// Build the data that CRC covers
let crc_data = [
flags, // 0x00
(command & 0xFF) as u8, // 0x00
(command >> 8) as u8, // 0x30
(size & 0xFF) as u8, // 0x00
(size >> 8) as u8, // 0x00
];

let crc = crc8_dvb_s2(&crc_data);
println!("CRC8: 0x{:02X}", crc);

// Full frame: $X< 00 0030 0000 [crc]
let frame = [
b'$', b'X', b'<', // Header
0x00, // Flags
0x00, 0x30, // Command (little-endian)
0x00, 0x00, // Size (little-endian)
crc, // CRC8
];

println!("Frame: {:02X?}", frame);
}

Dual Checksum (MSPv2 over V1)​

When wrapping MSPv2 in V1, both checksums are used:

  1. Inner CRC8: Covers V2 content (flags, command, size, payload)
  2. Outer XOR: Covers V1 envelope (command=255, size, and all inner bytes including CRC8)
/// Build MSPv2 over V1 with dual checksums
pub fn build_v2_over_v1(command: u16, payload: &[u8]) -> Vec<u8> {
let inner_size = payload.len();
let outer_size = 1 + 2 + 2 + inner_size + 1; // flags + cmd + size + payload + crc

let mut frame = Vec::with_capacity(6 + outer_size + 1);

// V1 header
frame.extend_from_slice(&[b'$', b'M', b'<']);
frame.push(255); // V2 frame marker
frame.push(outer_size as u8);

// V2 content
frame.push(0); // flags
frame.extend_from_slice(&command.to_le_bytes());
frame.extend_from_slice(&(inner_size as u16).to_le_bytes());
frame.extend_from_slice(payload);

// Inner CRC8 (covers flags through payload)
let inner_crc = crc8_dvb_s2(&frame[5..]);
frame.push(inner_crc);

// Outer XOR checksum
let mut xor: u8 = 255 ^ (outer_size as u8);
for byte in &frame[5..] {
xor ^= byte;
}
frame.push(xor);

frame
}

Checksum Verification Helper​

/// Unified checksum verification
pub enum MspChecksum {
V1 { size: u8, command: u8, payload: Vec<u8>, expected: u8 },
V2 { data: Vec<u8>, expected: u8 },
}

impl MspChecksum {
pub fn verify(&self) -> bool {
match self {
MspChecksum::V1 { size, command, payload, expected } => {
let mut calc = *size ^ *command;
for byte in payload {
calc ^= byte;
}
calc == *expected
}
MspChecksum::V2 { data, expected } => {
crc8_dvb_s2(data) == *expected
}
}
}

pub fn calculated(&self) -> u8 {
match self {
MspChecksum::V1 { size, command, payload, .. } => {
let mut calc = *size ^ *command;
for byte in payload {
calc ^= byte;
}
calc
}
MspChecksum::V2 { data, .. } => {
crc8_dvb_s2(data)
}
}
}
}

Performance Comparison​

AlgorithmSpeedCode SizeError Detection
XORVery FastMinimalBasic (single-bit, burst)
CRC8-DVB-S2 (bitwise)Slower~50 bytesGood (Hamming distance 4)
CRC8-DVB-S2 (table)Fast~300 bytesGood (Hamming distance 4)

For embedded systems with limited flash, the bitwise CRC implementation is preferred. For applications with more resources, the lookup table provides better performance.

Test Vectors​

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_xor_checksum() {
// Empty payload
assert_eq!(msp_v1_checksum(0, 108, &[]), 108);

// With payload
assert_eq!(msp_v1_checksum(6, 108, &[0x96, 0x00, 0x32, 0x00, 0x08, 0x07]), 0xC5);
}

#[test]
fn test_crc8_dvb_s2() {
// Known test vectors
assert_eq!(crc8_dvb_s2(&[]), 0x00);
assert_eq!(crc8_dvb_s2(&[0x00]), 0x00);
assert_eq!(crc8_dvb_s2(&[0x01]), 0xD5);
assert_eq!(crc8_dvb_s2(&[0x00, 0x00, 0x30, 0x00, 0x00]), 0x93);
}

#[test]
fn test_crc8_lookup_table() {
let crc = Crc8DvbS2::new();

// Should match bitwise implementation
for i in 0..=255u8 {
let data = [i];
assert_eq!(crc.calculate(&data), crc8_dvb_s2(&data));
}
}
}