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:
- The payload size byte
- The command byte
- 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 direction00size6Ccommand (108)6Cchecksum (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:
- Inner CRC8: Covers V2 content (flags, command, size, payload)
- 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
| Algorithm | Speed | Code Size | Error Detection |
|---|---|---|---|
| XOR | Very Fast | Minimal | Basic (single-bit, burst) |
| CRC8-DVB-S2 (bitwise) | Slower | ~50 bytes | Good (Hamming distance 4) |
| CRC8-DVB-S2 (table) | Fast | ~300 bytes | Good (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));
}
}
}