DShot - subtilitas/rcbench GitHub Wiki
English · Deutsch
What the IOmcu puts on an output pin, and what comes back. Four drivers: servo PWM (pulse-width modulation), PPM (pulse-position modulation), DShot, and bidirectional DShot.
The rules every output obeys — travel, role, rest, slew, clamping, the ARMED state and
the silence timeout — are in shared/outputs/ and are the same for all four.
This page is about the wire.
More than one IOmcu will exist. The one the bring-up runs on is a Pico-form-factor module wired by hand; a later board carries its outputs on pins that are soldered rather than chosen. They do not have the same pins, and a pin index on one means a different pin -- or no pin -- on the other.
The IOmcu therefore says which board it is, in the hardware register of the identity page, and the panel offers that board's pins and no other. Changing board clears any selection, because a pin index on one board means a different pin on another.
A board the panel has a catalogue for uses that catalogue. It has been read by somebody, it names the exact signal holding each reserved pin, and it cannot change under a running bench.
A board the panel has never heard of describes its own pins, on the catalogue page: which GPIOs it brings out, the pad number printed beside each, and what holds the ones an output may not have. So an IOmcu newer than the panel is usable rather than blank.
A board that also serves the shape page says where its pads are -- the outline, the pitch, and which corner pad 1 sits at -- so the panel can draw it rather than only list it. Only a picture needs that: a board that does not answer is used from its catalogue and simply is not drawn. A picture drawn from a guessed shape would point at the wrong pad with the same confidence as the right one, which on a screen whose job is "find this pad on the board in front of you" is the failure worth avoiding.
What a board cannot do is make one of its pins safe. The IOmcu reserves its own set at its own end whatever the catalogue page says, so a catalogue that is wrong costs a pin rather than the safety line. A board that neither describes itself nor is known offers nothing: guessing a pin map is how an output ends up on the safety line.
A board whose outputs are soldered is shown rather than offered. The IOmcu configures itself and the panel displays what it reads back without letting it be edited, because a screen that re-bound pins which are not on connectors would be offering a choice the board cannot honour.
On a board that is wired by hand, no output pin is fixed in the firmware. A slot on the OUTPUTS page carries its pin number, so the pin is a setting rather than a build.
The IOmcu refuses a pin it must not drive: GP3 (the safety heartbeat), GP8 to GP12 (the CAN (Controller Area Network) controller's SPI (Serial Peripheral Interface) and interrupt), the four pins the bring-up module uses and does not bring out — GP23 (the MODE pin of the module's power converter), GP24 (VBUS sense), GP25 (LED1) and GP29 (VSYS sense) — and any number above the last GPIO (general-purpose input/output) the module has: GP30 and up, 29 being the top of the RP2350A. The set is the module's own and does not follow the SDK board file the image is built for. The PD mini's pins on the SUPPLY page are held to the same set. A refused slot is left unbound. The page still reads back what was asked for, and no register on it says whether a slot is bound, so an unbound slot reads back exactly like a driving one. What reaches the operator is a lead that does not move.
A PIO (programmable input/output) block addresses 32 pins from a base of 0 or 16, fixed while the block holds a program. Which block can reach a given pin is therefore not known until the pin arrives, and the block is chosen when the slot is bound. A pin no free block can reach is refused like any other.
Hardware PWM, not PIO: the part has 12 slices and 24 channels, and a servo pulse is exactly what a slice does. The counter runs at 1 MHz, so a pulse is a count of microseconds and the frame period is 1,000,000 divided by the rate.
| Frame rate | 40 to 560 Hz |
| Pulse | 400 to 2500 µs and at most the period minus 1 µs, refused outside |
| Resolution | 1 µs |
A pulse is never as long as its frame. The longest one a frame carries is the period minus 1 µs, rounded down: the counter's last count, which leaves the pin low for 1 µs.
| Rate | Period | Longest pulse |
|---|---|---|
| 560 Hz | 1785 µs | 1784 µs |
| 500 Hz | 2000 µs | 1999 µs |
| 400 Hz | 2500 µs | 2499 µs |
| 399 Hz and below | 2506 µs and more | 2500 µs, the ceiling |
A CHAN_CFG, OUTPUTS, BIND, SERVO or PROG write that would leave a PWM output
with a longer endpoint is refused with BAD_VALUE, and nothing changes. The
rate judged is the one the output runs at: SERVO's for a surface while that is
not 0, the slot's own otherwise. A standard 1000 to 2000 µs servo therefore
takes 499 Hz and is refused at 500 Hz and 560 Hz. A stored slot whose endpoint
does not fit its own rate starts unbound. The rule is out_pwm_pulse_max()
and outputs_pulse_check(); the link
has the pages' side.
A slice is two channels sharing one counter, so two pins on the same slice run at the same frame rate. A second binding that asks for a different rate on a slice already in use is refused rather than retimed, because retiming would move an output that nobody touched.
The SERVO page sets one frame rate for its target. With the standard servo
ports as PROG's TARGET that is every PWM output whose first channel is a
surface, over the rate the binding gives the slot; with a programming port as
target it is that port's pin alone, and every slot keeps the rate its binding
gives it (protocol 4.19, the link). It is refused with
BAD_VALUE when it would move a surface away from the rate of the output beside
it on its slice, and while it holds a rate a CHAN_CFG or OUTPUTS write that
would do the same -- a surface turned throttle, a throttle bound beside a
surface -- is refused too. It is not kept: a restart runs each slot at its own
rate.
GPIO numbers fold onto the 12 slices. GP0 to GP31 take slice (pin / 2) modulo 8; GP32 to GP47 take slice 8 + (pin / 2) modulo 4; the channel is the low bit of the pin number in both ranges. Two pins 16 apart below GP32 are therefore the same channel of the same slice, and above GP32 the distance is 8. A channel is one compare register and a compare register is one pulse width, so the second pin of such a pair is refused rather than bound: binding it would mux it onto the first pin's pulse width, and one lead would follow the other with nothing on any screen to say so.
Six of those pairs are both free on the header the IOmcu offers: GP0 and GP16, GP1 and GP17, GP2 and GP18, GP4 and GP20, GP5 and GP21, GP6 and GP22. Either pin of a pair may be used; the two together may not.
One pin, up to eight channels. A frame is a run of 300 µs marks; the time from the start of one mark to the start of the next is one channel's pulse width. After the last channel comes a terminating mark and then the sync gap, and that gap is what tells a receiver where the next frame starts.
Channels and frame rate are not independent. Eight channels at 2000 µs already spend 16 ms of a 22.5 ms frame. The bench refuses a frame whose sync gap would fall below 3000 µs, which is the shortest gap no receiver can read as a channel: the channel ceiling is 2500 µs.
The shortest frame that carries n channels whatever they are commanded to is
n × 2500 µs + 300 µs + 3000 µs
which is 23.3 ms for eight channels — so eight channels at 50 Hz is refused and eight at 40 Hz is not. The refusal happens when the slot is configured, not when the sticks reach the end of their travel.
Polarity is a pad inversion, not a different program.
The frame plays from a pair of DMA (direct memory access) channels that retrigger each other, so the processor is not in the timing path. The frame buffer is rewritten in place while it is being played: a channel updated mid-frame takes effect on the next frame, and the rest of that frame carries the previous values.
Sixteen bits: eleven of value, one asking for telemetry on the separate serial wire, and four of checksum. Most significant bit first. There is no start bit and no idle pattern — the bit period is the whole of the synchronisation, so both ends agree on the rate by configuration.
| Bit | High for |
|---|---|
| 0 | 37.5% of the bit period |
| 1 | 75% |
The state machine runs at eight times the bit rate, because eight is the smallest number of cycles that makes both those fractions whole. DShot600 is therefore 4.8 MHz.
The value is not a throttle across its whole range:
| Value | Meaning |
|---|---|
| 0 | motor stop |
| 1 to 47 | commands: beeps, direction, 3D mode, save settings, extended telemetry |
| 48 to 2047 | throttle |
A command has to be repeated ten times before an ESC (electronic speed controller) acts on it; sent once it does nothing, which looks exactly like a driver that is not working.
The bench maps travel so that a command of zero is motor stop and everything above it lands in 48 to 2047. A driver that mapped travel straight onto 0 to 2047 would send beeps and direction changes on its way up from idle. A command above the span of 1000 maps to zero, motor stop, not to full throttle.
out_dshot_bind() on a pin that is already bound with a different rate or mode
returns false and leaves the old output running. A caller releases the pin
first.
Frames go out at 1 kHz. That rate is not on the wire: the OUTPUTS page's rate field is a bit rate for DShot rather than a frame rate, so the bench chooses it. 1 kHz is twenty times the panel's poll rate and well inside every ESC's own signal timeout.
Nothing is sent while the bench is not driving. No frames means no edges, and an ESC stops on silence after its own timeout. Sending explicit zeros instead would keep it armed and waiting, which is not what a bench that is SAFE should look like from the ESC's side.
The same frame with the line inverted and the checksum complemented, so an ESC set up for one protocol ignores the other rather than acting on it. It is a separate driver number on the OUTPUTS page, not a flag: the two are different wires, not one wire with a setting.
After each frame the IOmcu releases the line and the ESC answers on it, about 30 µs later, with 21 bits at five quarters of the DShot rate. The turnaround happens inside the PIO block — the transmitter releases the pin and raises a flag, and the receiver starts on that flag — because 30 µs is not a deadline a loop can meet.
The reply is group-coded. Each nibble of a 16-bit value becomes a five-bit code
word chosen so the line changes level often, and the line is differential: each
bit is the previous one exclusive-ored with the data. Undoing it is
x ^ (x >> 1). The first bit sent is a zero, so the burst starts with a falling
edge from the idle level and can be found at all.
The 16 bits are twelve of payload and four of checksum, and the checksum is the complement of the one on an outgoing frame. Every single-bit error is caught: the check is the exclusive-or of the four nibbles, and a flipped bit changes exactly one of them.
The payload is a period, not a speed:
period_us = mantissa (9 bits) << exponent (3 bits)
electrical rpm = 60,000,000 / period_us
A payload of 0x0FFF means the motor is not turning.
The line is inverted by the pad's output override rather than by the PIO program, so the same pin still reads the line's true level once the transmitter has let go of it, and the pull-up holds the idle in between.
The override is applied after the state machine's initialisation, never
before. That initialisation begins with pio_gpio_init(), which is
gpio_set_function(), which assigns the pad's whole control register instead
of masking it. The override lives in that register and is lost. The pull-up is
not, because pulls are in the pad block, so a pin that lost its inversion still
idles high and looks like a protocol fault rather than a pin fault.
The bind reads the register back and refuses rather than driving a line whose polarity it could not set. Ordering is the whole of the polarity here, and nothing else in the driver would notice losing it.
The IOmcu samples the line at five times the reply's bit rate and decodes
in software. The ESC's bit rate comes from its own crystal, a percent or two
from the bench's, and 21 bits is long enough for that to walk a fixed sample
point off the end of a bit — so the decoder resets its phase at every
transition, the way a UART (universal asynchronous receiver-transmitter)
resynchronises on a start bit, and reads each bit from the middle of its window.
test_dshot_telem holds this against a reply laid down at 4.75 and 5.25 samples
a bit.
An ESC reports electrical periods and has no idea what it is bolted to, so
mechanical rpm (revolutions per minute) needs the motor's magnet count. That is
the one number the wire does not carry. The panel sends it from the Motor poles setting when an IOmcu starts answering, again whenever the setting
changes, and again before the write that switches to ARMED if a change is still owed, on
the CONTROL page. A write nobody answers stays owed and
goes out again at the next 50 ms poll; one the IOmcu refuses is not
retried, and waits for the next edit or link-up edge.
Until it arrives the IOmcu reports no speed at all. A speed derived from a guessed pole count is a plausible number with nothing to mark it as wrong, which is worse than an empty field.
That covers zero only. Any even count between 2 and 42 is accepted and sets the rpm valid bit -- an odd one is refused, because a typo that reached the far end would put a plausible wrong speed on the screen -- so an IOmcu holding an out-of-date count reports the actual speed times the motor's poles over the count it holds. Fitting a 12-pole motor to a bench still holding 14 reads 14.3 % low; leaving 12 in place for a 14-pole motor reads 16.7 % high. The direction follows the edit, and the ends of the range are a factor of 21 either way: a 42-pole motor converted as 2 reads twenty-one times its speed, and a 2-pole motor converted as 42 reads a twenty-first of it. The ordinary edit is the one to watch, because a factor of 21 is wrong on sight and 14 % is not.
An ESC sent command 13 interleaves temperature, voltage, current, stress and status frames between the speed ones, marked by the top nibble of the payload.
The bench asks with command 13 and asks again until the ESC answers. One ask is ten frames of command 13 in a row, 10 ms at 1 kHz. An ask goes out only in place of a zero-throttle frame, so it never delays a throttle, and command 13 is inside the command range, so nothing turns while it goes out:
- the first ask starts at the first zero-throttle frame of a run;
- the next starts 500 ms after the last repeat of the one before, while the throttle stays at zero;
- a throttle above zero goes out on its own frame, and an ask it interrupts starts again from its first repeat;
- asking stops at the first extended frame that comes back, or after 10 asks without one;
- every run asks again, because extended telemetry is a runtime setting an ESC forgets when it restarts, and an ESC can be swapped between runs.
The repetition is for ESCs with AM32 firmware. An ESC with AM32 2.21 takes a
command only while it is armed and its motor is stopped, and it arms itself
after 1 s of zero throttle, counted from the first frame it hears (Src/dshot.c:157,
Src/main.c:1353-1356). A bench that is SAFE sends no frames, and the AM32
firmware restarts after 0.5 s without one when armed and 2 s when not, so
every run starts from an ESC that has not armed. An ask made only in a run's
first 10 ms arrives before the ESC can take it. test_dshot_edt holds the
schedule against a model of those rules: an ESC with AM32 firmware starting
from silence at zero throttle takes the third ask, 1.02 s into the run. Speed
does not depend on any of this: an ESC with AM32 firmware answers a
bidirectional frame with a period whether or not it was asked.
Each of the ten frames carries the telemetry bit set. On a value of 1 to 47
that bit is what marks the frame as a command for the BLHeli_S firmware
family (Bluejay): it discards a command whose telemetry bit is clear and zeroes its
repeat counter with it, so ten frames without the bit never reach the six
repeats its command handler counts. The AM32 firmware has no such gate and
acts on either.
On a bidirectional pin the frame is 0x01B5 with the bit and 0x01A4
without it — a different payload and a different checksum nibble.
test_dshot_frame holds that word. The bit's other meaning is a request on
the separate serial telemetry wire, which nothing on this bench reads.
The two frame kinds cannot be told apart from the bits alone: the nibble that
marks an extended frame is an ordinary exponent and mantissa in a speed frame,
and only an ESC with extended telemetry enabled guarantees the normalisation
that separates them. The decoder therefore takes the mode as an argument
rather than inferring it, and outputs_hw.c passes true from the first repeat
of the first ask on. An ESC with AM32 firmware answers command 13 once, with a
status frame
(0xE00) after the sixth repeat, and that frame read as a speed is a period
of zero and no answer.
An ESC that does not know command 13 ignores it and keeps sending periods. Those still read as periods: a frame is taken for an extended one only when the mantissa's top bit is clear and the type nibble is not zero, which an ESC that normalises its exponent never sends. An ESC that does not normalise is the case this cannot survive, and no such ESC has been tried.
Temperature, voltage and current reach the bench numbers from here and from nowhere else: there is no measurement front end on the IOmcu. Each keeps its own staleness window of 2000 ms, against 200 ms for speed, because the extended frames are interleaved a few a second while a period comes back on every frame. Power is the product of voltage and current, and stays empty unless both arrived.
The implementation is written from the published description of the protocol. Everything below is exercised by the host suite against frames the same code builds, which proves the arithmetic and not the wire.
One reading is not on this list. It is against one ESC with no instrument on the pin, so it is evidence and not a measurement.
-
Bidirectional DShot600 against one ESC (#172): a Skystars 35A running AM32 2.21, target
JUPITER_35A_F421(the AM32 configurator shows it cut toJUPITER_35A_F42), on a 4S pack, bound to GP0. Release 0.8.2 decoded its speed (2593 rpm). Release 0.9.1, with the repeated ask above, turned its extended telemetry on: at 0 % throttle MOTOR & ESC showed 14.75 V, 72.0 A and an ESC temperature of 41 °C. The tester reported the voltage as correct. The current and the temperature were decoded and checked against nothing: 72.0 A at idle, and 81.00 A read by DshotDisplay from the same ESC on another day, look like an input with no current sensor behind it rather than a measurement.
Nothing here has been on an oscilloscope, and every bidirectional item below is unconfirmed beyond that one ESC:
- the reply rate of five quarters of the DShot rate;
- the leading-bit convention of the group code;
- the turnaround delay, and whether 30 µs is what an ESC actually waits;
- the units of the extended-telemetry current (1 A per count) and temperature (1 °C per count) frames, and the stress, status and debug frames; only the voltage (0.25 V per count) has been read against a value the tester knew;
- whether ESCs with firmware other than AM32 accept command 13 under the repeated ask: ESCs with Bluejay and BLHeli_32 firmware are untested;
- whether the specification's "wait at least 35 ms" note belongs to command 13 or to command 12 (save settings). The table admits both readings, and the bench waits neither way: the frame after an ask's tenth repeat is the throttle, or another zero-throttle frame, at the next 1 ms tick. Settling it needs the specification text rather than a board;
- every bit timing, against a real ESC's tolerance rather than against the specification.
| Frames, group code, checksum, speed, sampler | protocols/dshot/ |
| PPM frame layout | protocols/ppm/ |
| Travel, role, rest, slew, the ARMED state, timeout | shared/outputs/ |
| Which PWM slice and channel a GPIO reaches | shared/outputs/out_pwm_map.c |
| PIO programs and their drivers for PPM and DShot |
protocols/ppm/rp2350/, protocols/dshot/rp2350/
|
| Hardware PWM backend | firmware/iomcu/src/out_pwm.c |
| Binding the bank to the pins | firmware/iomcu/src/outputs_hw.c |