A BLHeli_32 ESC already measures speed, temperature, voltage and current for each motor it drives. Reading that telemetry in a test gives a per-motor speed without a tachometer and a per-channel temperature without a thermocouple. Learn how the KISS telemetry frame and bidirectional DShot eRPM work, how to convert eRPM to mechanical rpm, and how to parse frames in a TofuPilot plug and record them as per-motor measurements with limits.
Two Telemetry Channels
BLHeli_32 ESCs can report data back over two separate channels:
| KISS serial telemetry | Bidirectional DShot eRPM | |
|---|---|---|
| Wire | Dedicated telemetry pad (TX from the ESC), one wire shared by the four channels of a 4-in-1 ESC | The DShot signal wire itself |
| Format | 10-byte frame, 115200 baud, 8N1 | Short reply after each DShot frame, decoded by the controller |
| Content | Temperature, voltage, current, consumption, eRPM | eRPM only |
| Rate | One frame per request, one motor at a time | Every DShot frame, every motor |
| Requirement | Telemetry request bit set in the DShot frame | ESC firmware with bidirectional DShot support, controller that sends inverted DShot and decodes the reply |
Use KISS telemetry for temperature, voltage and a coarse current, and bidirectional DShot when you need speed at a high rate, for example during a ramp. Both report electrical speed, which you convert to mechanical rpm yourself.
The KISS Telemetry Frame
The frame layout below is the one Betaflight decodes in src/main/sensors/esc_sensor.c. One frame is 10 bytes, sent at 115200 baud, 8N1, multi-byte fields big-endian:
| Byte | Field | Unit |
|---|---|---|
| 0 | Temperature | °C |
| 1-2 | Voltage | 0.01 V |
| 3-4 | Current | 0.01 A |
| 5-6 | Consumption | mAh |
| 7-8 | Electrical speed | 100 eRPM |
| 9 | CRC-8 over bytes 0 to 8 | polynomial 0x07, initial value 0 |
The ESC sends a frame only when asked. The controller sets the telemetry request bit in one DShot frame addressed to one motor, and that ESC channel answers on the shared telemetry wire. On a 4-in-1 ESC the controller therefore polls the motors in turn: request motor 1, wait for its frame, request motor 2, and so on. A frame that fails its CRC is dropped, not corrected.
Field scaling is not identical across every ESC firmware and every telemetry mode. Check the speed field once against an optical tachometer and the voltage field against a meter before trusting the scaling on a new ESC model.
Bidirectional DShot eRPM
With bidirectional DShot, the controller sends an inverted DShot signal and the ESC replies on the same wire shortly after each frame. The reply carries the period of one electrical revolution in microseconds, in a compact exponent and mantissa format with its own 4-bit checksum, GCR encoded. The conversion is:
eRPM = 60,000,000 / period_usDecoding the reply is timing-critical: it must happen in the microcontroller that generates DShot, not on the station PC. The microcontroller then reports decoded eRPM values to the host over USB serial. It is the right source for a speed trace during a ramp, because it updates with every DShot frame instead of once per telemetry poll.
Convert eRPM to Mechanical rpm
Both channels report electrical revolutions. A brushless motor completes one electrical revolution per pole pair, so:
pole_pairs = magnets / 2rpm = eRPM / pole_pairs12N14P motor: 14 magnets, 7 pole pairs31,000 rpm -> 217,000 eRPM -> KISS speed field = 2170The "N" number in 12N14P counts stator slots and does not enter the formula; the "P" number counts magnets. Getting the pole pairs wrong scales every speed reading by a constant factor, and every unit still passes or fails together, so the error is easy to miss. A sanity check: no-load speed sits a little below KV x bus voltage. A 1250 KV motor on a 25.2 V bus gives at most 31,500 rpm, and the example motor measured 31,000 rpm at 100 % throttle without propeller.
Choose Where the Frame Is Parsed
Two wirings work with a TofuPilot station:
| Option | Wiring | Pros | Cons |
|---|---|---|---|
| A: host reads the telemetry wire | Telemetry pad to the RX of a 3.3 V USB-UART adapter, common ground; the microcontroller sets the request bit on command | Frame parsing visible in Python, raw frames available for debugging | Two serial ports, request and read must be coordinated |
| B: microcontroller forwards decoded values | Telemetry pad to a UART of the microcontroller | One serial port, telemetry polled continuously | Parsing lives in firmware |
This guide uses option A. The microcontroller that generates DShot accepts a TLM <motor> command over its own USB serial link, sets the telemetry bit in the next frame for that motor, and replies OK.
Step 1: Parse the Frame
Keep the parser free of I/O, so it can be unit-tested with recorded frames:
plugs/kiss.py25 lines
FRAME_SIZE = 10def crc8(data): crc = 0 for byte in data: crc ^= byte for _ in range(8): crc = ((crc << 1) ^ 0x07) & 0xFF if crc & 0x80 else (crc << 1) & 0xFF return crcdef parse_frame(frame, pole_pairs): """Return a dict for a valid 10-byte KISS frame, None if the CRC fails.""" if len(frame) != FRAME_SIZE or crc8(frame[:9]) != frame[9]: return None erpm = ((frame[7] << 8) | frame[8]) * 100 return { "temperature_c": frame[0], "voltage_v": ((frame[1] << 8) | frame[2]) / 100, "current_a": ((frame[3] << 8) | frame[4]) / 100, "consumption_mah": (frame[5] << 8) | frame[6], "erpm": erpm, "rpm": erpm / pole_pairs, }The CRC loop is a direct port of the updateCrc8 function in Betaflight: XOR the byte in, then eight shifts with polynomial 0x07.
Step 2: Write the Telemetry Plug
The plug owns the telemetry serial port and returns plain dicts, which cross the plug process boundary without trouble:
pyserial>=3.5plugs/esc_telemetry.py21 lines
import serialfrom plugs.kiss import FRAME_SIZE, parse_frameclass EscTelemetry: def __init__(self, port, pole_pairs): self.pole_pairs = pole_pairs self.ser = serial.Serial(port, 115200, timeout=0.05) def __del__(self): if hasattr(self, "ser"): self.ser.close() def flush(self): self.ser.reset_input_buffer() def read_frame(self): """Return the parsed frame, or None on timeout or CRC failure.""" raw = self.ser.read(FRAME_SIZE) return parse_frame(raw, self.pole_pairs)Plugs cannot call each other, so the phase coordinates the two: it flushes the telemetry port, asks the motor driver plug to request a frame, then reads. The operating system buffers the incoming bytes in between, so the frame is not lost if it arrives before read_frame() is called.
Step 3: Record Per-Motor Measurements With Limits
Run each motor at a fixed throttle, collect a series of frames and record one set of measurements per motor. One function serves the four phases through with::
procedure.yaml81 lines
name: ESC Telemetry Checkversion: 0.1.0execution: workers: 1plugs: - name: Motor Driver key: motor python: plugs.motor:MotorDriver scope: station with: port: /dev/ttyACM0 pole_pairs: 7 - name: ESC Telemetry key: esc_telemetry python: plugs.esc_telemetry:EscTelemetry scope: station with: port: /dev/ttyUSB0 pole_pairs: 7main: - name: Telemetry Motor 1 key: telemetry_m1 python: phases.telemetry:telemetry_check timeout: 45s with: motor_index: 1 throttle_pct: 70 frames: 50 measurements: &telemetry_measurements - name: Motor Speed key: motor_speed unit: rpm description: Mean of valid KISS frames at 70 % throttle, no propeller, eRPM / 7 validators: - operator: ">=" expected_value: 21000 - operator: "<=" expected_value: 24500 - name: ESC Temperature key: esc_temperature unit: °C validators: - operator: "<=" expected_value: 60 - name: ESC Voltage key: esc_voltage unit: V validators: - operator: ">=" expected_value: 21.0 - operator: "<=" expected_value: 25.5 - name: ESC Current key: esc_current unit: A description: ESC-reported, uncalibrated. Recorded for cross-check, no limits. - name: Frame Error Rate key: frame_error_rate unit: "%" validators: - operator: "<=" expected_value: 5 - name: Telemetry Motor 2 key: telemetry_m2 python: phases.telemetry:telemetry_check timeout: 45s with: motor_index: 2 throttle_pct: 70 frames: 50 measurements: *telemetry_measurements # Motors 3 and 4 follow the same patternteardown: - name: Stop All Motors python: phases.telemetry:stop_allThe YAML anchor (&telemetry_measurements) and alias (*telemetry_measurements) are standard YAML and keep the four phases on one set of limits. The speed window brackets the 22,600 rpm measured at 70 % on the example motor; the temperature limit is an example value. Set yours from known-good units.
phases/telemetry.py37 lines
import timeRAMP_RATE_PCT_S = 20def telemetry_check(motor, esc_telemetry, measurements, phase, log, motor_index, throttle_pct, frames): motor.ramp_to(motor_index, throttle_pct, RAMP_RATE_PCT_S) time.sleep(throttle_pct / RAMP_RATE_PCT_S + 2.0) # ramp, then settle valid, errors = [], 0 try: for _ in range(frames): esc_telemetry.flush() motor.request_telemetry(motor_index) frame = esc_telemetry.read_frame() if frame is None: errors += 1 else: valid.append(frame) time.sleep(0.02) finally: motor.ramp_to(motor_index, 0, RAMP_RATE_PCT_S) measurements.frame_error_rate = 100 * errors / frames if not valid: phase.fail(f"No valid telemetry frame from motor {motor_index} in {frames} requests") n = len(valid) measurements.motor_speed = sum(f["rpm"] for f in valid) / n measurements.esc_temperature = max(f["temperature_c"] for f in valid) measurements.esc_voltage = sum(f["voltage_v"] for f in valid) / n measurements.esc_current = sum(f["current_a"] for f in valid) / n log.info(f"Motor {motor_index}: {n} valid frames, {errors} errors")def stop_all(motor): motor.stop(50)The motor driver plug is the one that generates DShot on the bench; this phase assumes it exposes ramp_to, stop and request_telemetry, the last one sending TLM <motor> to the firmware. The finally block ramps the motor down even when a read raises, and the teardown phase stops every motor on any other exit.
Each run then carries speed, temperature, voltage, current and frame error rate per motor, tied to the serial number of the unit. On the dashboard, a channel whose temperature drifts up across units, or a frame error rate that climbs on one station, shows up in the control chart of that measurement before units start failing.
What Each Measurement Is Good For
| Measurement | Trust it for | Do not use it for |
|---|---|---|
| Motor speed (from eRPM) | Speed at a given throttle, desynchronization, wrong pole count | Absolute accuracy before one tachometer check |
| ESC temperature | Channel-to-channel comparison, thermal drift over a run | The motor winding temperature |
| ESC voltage | Bus sag at the ESC, cross-check against a meter | Calibrated bus voltage |
| ESC current | Gross faults, cross-check against a shunt | Pass/fail limits, especially at low current |
| Frame error rate | Wiring, ground and baud rate problems | Motor quality |
ESC current is reported with a 0.01 A field, but the sensing inside the ESC is not calibrated per unit and the offset dominates at the few amps a motor draws without propeller. Measure current with a shunt when it matters.
Common Pitfalls
Using the wrong pole count. 12N14P has 7 pole pairs, not 6 and not 12. A wrong value scales every rpm by a constant and survives unnoticed.
Requesting telemetry from several motors at once. A 4-in-1 ESC has one telemetry wire. Two answers at once collide and fail the CRC. Poll one motor at a time.
Reading the frame without flushing first. Old bytes in the buffer shift the frame, and every frame fails the CRC. Flush, request, read.
Forgetting the common ground. Without it, the USB-UART adapter reads noise and the frame error rate climbs.
Driving the telemetry pad with a 5 V adapter. The pad is a low-voltage logic output. Use a 3.3 V adapter.
Putting limits on ESC current. Record it and compare, but judge current with a shunt.
Leaving the phases in parallel. Set workers: 1: two phases polling the same telemetry wire corrupt each other's frames.
Key Points
- KISS telemetry is a 10-byte frame at 115200 baud: temperature, voltage (0.01 V), current (0.01 A), consumption (mAh), speed (100 eRPM) and a CRC-8 with polynomial 0x07.
- The ESC answers only when the telemetry bit is set for its motor; on a 4-in-1 ESC, poll the motors one at a time.
- Bidirectional DShot gives eRPM with every frame and is decoded in the microcontroller; eRPM = 60,000,000 / period in µs.
- Mechanical rpm = eRPM / pole pairs, 7 for a 12N14P motor.
- Record speed, temperature, voltage and frame error rate per motor with limits; record ESC current without limits and measure current with a shunt.
For the wider picture of motor and actuator test benches, see Motor and Actuator Testing with TofuPilot.
