MAVLink Scheduler & Stream Rates
Executive Summary
You set SR1_POSITION to 50Hz, but your telemetry log shows only 10Hz. Why? The answer lies in the MAVLink Scheduler. ArduPilot uses a sophisticated "Bucket Scheduler" with strict prioritization and back-pressure flow control. It doesn't just blast data; it carefully manages the serial link bandwidth to ensure critical messages (like Heartbeats and Command ACKs) never get dropped in favor of bulk data.
Theory & Concepts
1. The "Token Bucket" Metaphor
Imagine a bucket with a hole in the bottom.
- Tokens: Bandwidth (Bytes).
- Refill: The serial link speed fills the bucket at a constant rate (e.g., 5760 bytes/sec).
- Drain: Sending a message removes tokens.
- The Logic: If the bucket is empty (no bandwidth), you can't send. You have to wait.
- Prioritization: Who gets to take tokens first? Heartbeats get first dibs. Status Text gets second. The "bulk" data (Attitude, GPS) fights for whatever scraps are left.
2. Jitter and Determinism
MAVLink is Best Effort, not Deterministic.
- CAN Bus: Guaranteed timing.
- MAVLink: "I'll send it when I can."
- Result: A 10Hz stream might arrive at intervals of 80ms, 120ms, 90ms, 110ms. Averaged out, it's 10Hz, but instantaneous timing is jittery. Do not rely on MAVLink for real-time control loops (use DroneCAN instead).
Architecture (The Engineer's View)
The scheduling logic is contained in GCS_MAVLINK::update_send(). This function is called every main loop cycle but runs for a maximum of 5 milliseconds (or less if the scheduler budget is tight).
1. The Priority Tiers
Messages are processed in three distinct tiers. A lower tier only runs if the higher tier has nothing to send.
- Tier 1: Deferred Messages (Critical):
- Examples:
HEARTBEAT,SYS_STATUS,POWER_STATUS. - Behavior: These are hard-coded or highly specific. They are checked every loop. If their interval has passed, they are sent immediately.
- Examples:
- Tier 2: Pushed Messages (Events):
- Examples:
COMMAND_ACK,STATUSTEXT(Error messages),PARAM_VALUE(in response to a read). - Behavior: When an event happens (e.g., you switch flight modes), a message ID is "pushed" onto a queue. The scheduler drains this queue as fast as possible.
- Examples:
- Tier 3: Streamed Messages (Bulk Data):
- Examples:
ATTITUDE,GLOBAL_POSITION_INT,VFR_HUD. - Behavior: These are the "Background Noise" of MAVLink. They are grouped into Buckets.
- Examples:
2. The Bucket System
ArduPilot handles hundreds of possible MAVLink messages. Checking a timer for every single one every loop would waste CPU.
Instead, it uses Stream Buckets.
- Initialization: At startup (or when you change
SRx_params), the code calculates the target interval (e.g., 50Hz = 20ms). - Assignment: Each message ID is assigned to one of ~10 Buckets based on its rate.
- Bucket 0: High Speed (e.g., 50Hz+).
- Bucket 9: Very Slow (e.g., 0.1Hz).
- Execution: The scheduler processes one bucket per loop.
- Loop 1: Check Bucket 0.
- Loop 2: Check Bucket 1.
- ...
- Effect: This distributes the CPU load.
3. Flow Control (Back Pressure)
This is the most common reason for missing data.
- The Check: Before sending any byte, the code calls
HAVE_PAYLOAD_SPACE. - The Hardware: It queries the UART driver: "Do you have room in your TX buffer?"
- The Drop: If the buffer is full (because the radio hasn't sent the previous bits yet), the scheduler aborts that message. It does not retry immediately. It moves on.
- Result: If your baud rate is too low for the requested stream rates, packets are silently dropped to prevent the flight controller from freezing while waiting for the radio.
Common Issues & Troubleshooting
"My Stream Rate is fluctuating"
- Cause: Bandwidth saturation. The "Back Pressure" mechanism is dropping packets randomly when the buffer fills.
- Fix: Reduce
SRx_params or increase Baud Rate.
"I requested 50Hz but got 10Hz"
- Cause 1:
SRx_params are "Requests", not guarantees. If the loop rate is slow or the buckets are full, you get what you get. - Cause 2: Link Throttling. Some radios (like ELRS) have small buffers. ArduPilot fills them instantly, then pauses.
"My OSD flickers"
- Cause: OSDs often listen to specific streams. If high-priority traffic (like a parameter download) floods the link, the "Tier 3" stream data gets delayed.
Source Code Reference
- Scheduler Loop:
GCS_MAVLINK::update_send() - Bucket Initialization:
initialise_message_intervals_from_streamrates()
Practical Guide: Debugging Missing Telemetry
You asked for ATTITUDE at 20Hz, but you are only getting 5Hz. Who is lying?
Step 1: Check the Stats
ArduPilot tracks dropped packets.
- Connect via Mission Planner.
- Go to the Status tab.
- Look for
stream_slowdown.- Value: This is a percentage (0-100) of how often the scheduler had to wait for the UART buffer.
- Interpretation: If
stream_slowdown> 5%, your baud rate is too low for the amount of data you are trying to push.
Step 2: Calculate the Cost
Bytes per second = Sum(PacketSize * Rate).
ATTITUDE(28 bytes) * 20 Hz = 560 B/s.GPS_RAW_INT(30 bytes) * 5 Hz = 150 B/s.- Total: 710 B/s.
- Capacity (57600 baud): ~5,700 B/s. (You are safe).
- Capacity (ELRS Airport): Variable, but often < 1000 B/s at low packet rates.
The Fix
- Reduce Rates: Set unimportant streams (
SRx_RC_CHAN,SRx_RAW_SENS) to 0. - Increase Baud: Move from 57600 to 115200 or 460800 if hardware permits.
- Use
MAV_CMD_SET_MESSAGE_INTERVAL: This bypasses the clumsySRx_groups and lets you request onlyATTITUDEwithout also gettingAHRS2,AHRS3, etc.