ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
dshot.h
Go to the documentation of this file.
1// ProtoCore v1.0.16 - Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file dshot.h
6 * @brief DShot ESC digital throttle protocol codec (PROTOCORE_ENABLE_DSHOT).
7 *
8 * DShot is the digital replacement for analog PWM on brushless-motor ESCs (drones, robotics). Each
9 * command is a 16-bit frame - 11 bits of value, 1 telemetry-request bit, and a 4-bit CRC:
10 *
11 * bits 15..5 value (0 = disarm / command context, 1..47 = special commands, 48..2047 = throttle)
12 * bit 4 telemetry-request
13 * bits 3..0 CRC = xor of the three nibbles of (value<<1 | telemetry)
14 *
15 * For **bidirectional / "extended" DShot** (the ESC sends RPM/telemetry back on the same wire) the CRC
16 * is inverted. This is the wire codec: `protocore_dshot_encode` builds the 16-bit frame and
17 * `protocore_dshot_decode` validates the CRC and unpacks it. The physical layer (the bit-timed pulse train
18 * at 150/300/600/1200 kbit via the ESP32 RMT peripheral) is the app's transport - `protocore_dshot_bit_ns`
19 * gives the high-time for a 0/1 bit at a given rate so a driver can program the RMT symbols.
20 *
21 * Pure, zero heap, no stdlib, host-testable.
22 */
23
24#ifndef PROTOCORE_DSHOT_H
25#define PROTOCORE_DSHOT_H
26
27#include "protocore_config.h" // the entry point: protocore_types.h for the widths
28
29#if PROTOCORE_ENABLE_DSHOT
30
32
33// This module holds nothing between calls, so it carves no borrow and states none. An entry
34// takes one all the same, and never reads it, so every namespace in the tree is invoked the
35// same way.
36
37#define DSHOT_CMD_MOTOR_STOP 0 ///< disarm / zero throttle.
38
39#define DSHOT_CMD_BEACON1 1 ///< beep (1..5 = rising tones).
40
41#define DSHOT_CMD_BEACON5 5
42
43#define DSHOT_CMD_ESC_INFO 6 ///< request ESC info (telemetry bit must be set).
44
45#define DSHOT_CMD_SPIN_DIRECTION_1 7 ///< set spin direction normal (send 6x).
46
47#define DSHOT_CMD_SPIN_DIRECTION_2 8 ///< set spin direction reversed (send 6x).
48
49#define DSHOT_CMD_3D_MODE_OFF 9 ///< disable bidirectional 3D mode (send 6x).
50
51#define DSHOT_CMD_3D_MODE_ON 10 ///< enable bidirectional 3D mode (send 6x).
52
53#define DSHOT_CMD_SETTINGS_REQUEST 11
54
55#define DSHOT_CMD_SAVE_SETTINGS 12 ///< persist settings (send 6x).
56
57#define DSHOT_THROTTLE_MIN 48 ///< first real throttle step.
58
59#define DSHOT_THROTTLE_MAX 2047 ///< last throttle step (2000 steps of resolution).
60
61#define DSHOT_VALUE_MAX 2047 ///< widest value the 11-bit field holds.
62
63/** @brief The legacy analog-PWM ESC protocols (pulse width carries the throttle), for protocore_esc_pwm_ns. */
64typedef enum PROTO_ENUM_PACKED
65{
66 PROTOCORE_ESC_PWM, ///< standard servo PWM: 1000-2000 us.
67 PROTOCORE_ESC_ONESHOT125, ///< OneShot125: 125-250 us.
68 PROTOCORE_ESC_ONESHOT42, ///< OneShot42: 42-84 us.
69 PROTOCORE_ESC_MULTISHOT, ///< Multishot: 5-25 us.
70} protocore_esc_pwm;
71
72/** @brief What encode takes: value11, telemetry, bidirectional. */
73typedef struct
74{
75 uint16_t value11; ///< the 11-bit value (0..2047): a throttle (48..2047) or a special command (0..47)
76 proto_bool telemetry; ///< request telemetry on this frame
77 proto_bool bidirectional; ///< bidirectional/extended DShot (the CRC is inverted)
78} DshotEncodeArgs;
79
80/** @brief What decode takes: frame, value11, telemetry, bidirectional. */
81typedef struct
82{
83 uint16_t frame; ///< the received 16-bit frame
84 uint16_t *value11; ///< out: the 11-bit value (may be null)
85 proto_bool *telemetry; ///< out: the telemetry-request bit (may be null)
86 proto_bool bidirectional; ///< interpret the CRC as the inverted (bidirectional) form
87} DshotDecodeArgs;
88
89/** @brief What bit_ns takes: rate_kbit, bit. */
90typedef struct
91{
92 uint16_t rate_kbit; ///< one of 150, 300, 600, 1200. Others return 0
93 proto_bool bit; ///< the bit value (false = 0, true = 1)
94} DshotBitNsArgs;
95
96/** @brief What esc_pwm_ns takes: throttle_1000, mode. */
97typedef struct
98{
99 uint16_t throttle_1000; ///< throttle 0..1000 (clamped); 0 = min pulse (idle/arm), 1000 = max
100 protocore_esc_pwm mode; ///< one of protocore_esc_pwm
101} DshotEscPwmNsArgs;
102
103/**
104 * @brief DShot ESC digital throttle protocol codec (PROTOCORE_ENABLE_DSHOT). DShot is the digital replacement for ...
105 *
106 * A caller sets the members a call takes, invokes it through ::Dshot with the bytes it runs
107 * out of, and reads the outcome off the same handle.
108 *
109 * Dshot.encode_args.value11 = ...;
110 * Dshot.encode_args.telemetry = ...;
111 * Dshot.encode_args.bidirectional = ...;
112 * Dshot.encode(work);
113 * // Dshot.frame is what the call reports
114 *
115 * @var DshotNs::encode_args what encode takes: value11, telemetry, bidirectional
116 * @var DshotNs::decode_args what decode takes: frame, value11, telemetry, bidirectional
117 * @var DshotNs::bit_ns_args what bit_ns takes: rate_kbit, bit
118 * @var DshotNs::esc_pwm_ns_args what esc_pwm_ns takes: throttle_1000, mode
119 * @var DshotNs::ok true if the CRC is valid
120 * @var DshotNs::frame the 16-bit frame `(value<<5) | (telemetry<<4) | crc`, ready to ...
121 * @var DshotNs::ns the high time in nanoseconds, or 0 for an unknown rate
122 * @var DshotNs::encode build a 16-bit DShot frame
123 * @var DshotNs::decode validate + unpack a 16-bit DShot frame
124 * @var DshotNs::bit_ns high-time (ns) of a bit at a DShot rate. A DShot bit is one pulse ...
125 * @var DshotNs::esc_pwm_ns pulse width (ns) for an analog-PWM ESC protocol at a given throttle
126 *
127 * @c work is bytes the CALLER holds. This module reads none of them: it carries nothing
128 * between calls, so there is no state to keep and nothing to wipe. The parameter is there so
129 * a caller drives every namespace the same way.
130 */
131typedef struct
132{
133 DshotEncodeArgs encode_args;
134 DshotDecodeArgs decode_args;
135 DshotBitNsArgs bit_ns_args;
136 DshotEscPwmNsArgs esc_pwm_ns_args;
137 proto_bool ok;
138 uint16_t frame;
139 uint32_t ns;
140} DshotVars;
141
142/** @brief The operands and the outcome. */
143extern DshotVars DshotV;
144
145/** @brief The entries. */
146typedef struct
147{
148 void (*const encode)(uint8_t *work);
149 void (*const decode)(uint8_t *work);
150 void (*const bit_ns)(uint8_t *work);
151 void (*const esc_pwm_ns)(uint8_t *work);
152} DshotNs;
153
154// What the table binds, defined once in the .c and taking one parameter each: everything
155// else an entry needs is an operand in DshotV or a region of the borrow at a fixed offset.
156void protocore_dshot_encode(uint8_t *work);
157void protocore_dshot_decode(uint8_t *work);
158void protocore_dshot_bit_ns(uint8_t *work);
159void protocore_dshot_esc_pwm_ns(uint8_t *work);
160
161// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
162// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
163// `Dshot.encode(work)` resolves to a named function and becomes a DIRECT call. An extern table
164// leaves the call indirect and the symbol live at every level, -O2 -flto included.
165static const DshotNs Dshot __attribute__((unused)) = {
166 .encode = protocore_dshot_encode,
167 .decode = protocore_dshot_decode,
168 .bit_ns = protocore_dshot_bit_ns,
169 .esc_pwm_ns = protocore_dshot_esc_pwm_ns,
170};
171
173
174#endif // PROTOCORE_ENABLE_DSHOT
175
176#endif // PROTOCORE_DSHOT_H
PROTO_ENUM_PACKED
Application protocol spoken on a listener port or connection slot.
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
_Bool proto_bool
The truth value.
Definition types.h:64
#define PROTOCORE_END_DECLS
Definition types.h:97