├── .gitmodules ├── CMakeLists.txt ├── LICENSE ├── README.md ├── include └── pico_dshot │ └── dshot_encoder.h ├── pico_sdk_import.cmake └── src ├── dshot_encoder.cpp └── pio └── dshot_encoder.pio /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "pico-pio-loader"] 2 | path = pico-pio-loader 3 | url = https://github.com/cadouthat/pico-pio-loader.git 4 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.13) 2 | 3 | include(pico_sdk_import.cmake) 4 | 5 | if (NOT TARGET pico_pio_loader) 6 | add_subdirectory(pico-pio-loader) 7 | endif() 8 | 9 | project(pico_dshot C CXX ASM) 10 | set(CMAKE_C_STANDARD 11) 11 | set(CMAKE_CXX_STANDARD 17) 12 | pico_sdk_init() 13 | 14 | add_library(pico_dshot 15 | src/dshot_encoder.cpp) 16 | 17 | target_include_directories(pico_dshot PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) 18 | target_include_directories(pico_dshot PRIVATE ${CMAKE_CURRENT_LIST_DIR}/src ) 19 | 20 | target_link_libraries(pico_dshot 21 | pico_stdlib 22 | hardware_pio 23 | pico_pio_loader 24 | ) 25 | 26 | pico_generate_pio_header(pico_dshot ${CMAKE_CURRENT_LIST_DIR}/src/pio/dshot_encoder.pio) 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Connor Douthat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pico-dshot 2 | DShot library for the Raspberry Pi Pico using PIO 3 | 4 | Supports continuously encoding DShot output on GPIO(s) without ever interrupting the CPU. 5 | 6 | Requires 1 PIO state machine per encoder, and some PIO program space shared amongst all instances. 7 | -------------------------------------------------------------------------------- /include/pico_dshot/dshot_encoder.h: -------------------------------------------------------------------------------- 1 | #ifndef __PICO_DSHOT_DSHOT_ENCODER_H__ 2 | #define __PICO_DSHOT_DSHOT_ENCODER_H__ 3 | 4 | #include 5 | 6 | #include "hardware/pio.h" 7 | 8 | class DShotEncoder { 9 | public: 10 | DShotEncoder(uint dshot_gpio, PIO pio = pio0) 11 | : dshot_gpio(dshot_gpio), pio(pio) {} 12 | // TODO: cleanup in destructor 13 | 14 | // Init PIO, but do not output data yet 15 | // If enable_repeat is true, the PIO will continuously output the last provided value at 16 | // 8000 frames per second. 17 | // Warning: enable_repeat should only be used with the watchdog enabled, otherwise it is 18 | // unsafe as the PIO will carry on sending dshot frames even if the CPU is stuck. 19 | bool init(bool enable_repeat); 20 | 21 | // Note: enable_repeat applies to the latest value from any send function. 22 | // When enable_repeat is false, each send function call triggers a single dshot frame 23 | 24 | // Send a raw DShot command 25 | void sendCommand(uint16_t c); 26 | 27 | // Send throttle command once, for specified throttle in range [0, 1] 28 | void sendThrottle(double t); 29 | 30 | // Stop generating output (until next send command) 31 | // It is not necessary to call this when enable_repeat is false 32 | void stop(); 33 | 34 | // Util to compute the command value for a specified throttle in range [0, 1] 35 | static uint16_t getThrottleCommand(double t); 36 | 37 | private: 38 | static constexpr uint16_t MIN_THROTTLE_COMMAND = 48; 39 | static constexpr uint16_t MAX_THROTTLE_COMMAND = 2047; 40 | 41 | uint dshot_gpio; 42 | 43 | PIO pio; 44 | uint pio_offset; 45 | int pio_sm = -1; 46 | }; 47 | 48 | #endif 49 | -------------------------------------------------------------------------------- /pico_sdk_import.cmake: -------------------------------------------------------------------------------- 1 | # This is a copy of /external/pico_sdk_import.cmake 2 | 3 | # This can be dropped into an external project to help locate this SDK 4 | # It should be include()ed prior to project() 5 | 6 | if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH)) 7 | set(PICO_SDK_PATH $ENV{PICO_SDK_PATH}) 8 | message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')") 9 | endif () 10 | 11 | if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT)) 12 | set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT}) 13 | message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')") 14 | endif () 15 | 16 | if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH)) 17 | set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH}) 18 | message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')") 19 | endif () 20 | 21 | set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK") 22 | set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable") 23 | set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK") 24 | 25 | if (NOT PICO_SDK_PATH) 26 | if (PICO_SDK_FETCH_FROM_GIT) 27 | include(FetchContent) 28 | set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR}) 29 | if (PICO_SDK_FETCH_FROM_GIT_PATH) 30 | get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}") 31 | endif () 32 | # GIT_SUBMODULES_RECURSE was added in 3.17 33 | if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0") 34 | FetchContent_Declare( 35 | pico_sdk 36 | GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk 37 | GIT_TAG master 38 | GIT_SUBMODULES_RECURSE FALSE 39 | ) 40 | else () 41 | FetchContent_Declare( 42 | pico_sdk 43 | GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk 44 | GIT_TAG master 45 | ) 46 | endif () 47 | 48 | if (NOT pico_sdk) 49 | message("Downloading Raspberry Pi Pico SDK") 50 | FetchContent_Populate(pico_sdk) 51 | set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR}) 52 | endif () 53 | set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE}) 54 | else () 55 | message(FATAL_ERROR 56 | "SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git." 57 | ) 58 | endif () 59 | endif () 60 | 61 | get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}") 62 | if (NOT EXISTS ${PICO_SDK_PATH}) 63 | message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found") 64 | endif () 65 | 66 | set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake) 67 | if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE}) 68 | message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK") 69 | endif () 70 | 71 | set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE) 72 | 73 | include(${PICO_SDK_INIT_CMAKE_FILE}) 74 | -------------------------------------------------------------------------------- /src/dshot_encoder.cpp: -------------------------------------------------------------------------------- 1 | #include "pico_dshot/dshot_encoder.h" 2 | 3 | #include 4 | 5 | #include "pico/stdlib.h" 6 | #include "hardware/clocks.h" 7 | #include "hardware/pio.h" 8 | 9 | #include "pico_pio_loader/pico_pio_loader.h" 10 | 11 | #include "dshot_encoder.pio.h" 12 | 13 | bool DShotEncoder::init(bool enable_repeat) { 14 | pio_sm = pio_claim_unused_sm(pio, /*required=*/false); 15 | if (pio_sm < 0) { 16 | return false; 17 | } 18 | 19 | if (!pio_loader_add_or_get_offset(pio, &dshot_encoder_program, &pio_offset)) { 20 | pio_sm_unclaim(pio, pio_sm); 21 | pio_sm = -1; 22 | return false; 23 | } 24 | 25 | dshot_encoder_program_init(pio, pio_sm, pio_offset, dshot_gpio, enable_repeat); 26 | return true; 27 | } 28 | 29 | void DShotEncoder::sendCommand(uint16_t c) { 30 | // Shift for telemetry bit (0) 31 | c = c << 1; 32 | 33 | // Shift and include checksum 34 | uint16_t checksum = (c ^ (c >> 4) ^ (c >> 8)) & 0x0F; 35 | c = (c << 4) | checksum; 36 | 37 | pio_sm_put_blocking(pio, pio_sm, c); 38 | } 39 | 40 | void DShotEncoder::sendThrottle(double t) { 41 | sendCommand(getThrottleCommand(t)); 42 | } 43 | 44 | uint16_t DShotEncoder::getThrottleCommand(double t) { 45 | if (t < 0) t = 0; 46 | if (t > 1) t = 1; 47 | 48 | uint16_t c = MIN_THROTTLE_COMMAND + t * (MAX_THROTTLE_COMMAND - MIN_THROTTLE_COMMAND); 49 | if (c < MIN_THROTTLE_COMMAND) c = MIN_THROTTLE_COMMAND; 50 | if (c > MAX_THROTTLE_COMMAND) c = MAX_THROTTLE_COMMAND; 51 | return c; 52 | } 53 | 54 | void DShotEncoder::stop() { 55 | // Signal PIO to wait for the next push 56 | pio_sm_put_blocking(pio, pio_sm, 0); 57 | } 58 | -------------------------------------------------------------------------------- /src/pio/dshot_encoder.pio: -------------------------------------------------------------------------------- 1 | .program dshot_encoder 2 | 3 | .define public BIT_PERIOD 40 4 | 5 | .define ONE_HIGH 30 6 | .define ONE_LOW (BIT_PERIOD - ONE_HIGH) 7 | .define ONE_HIGH_DELAY (ONE_HIGH - 1) 8 | .define ONE_LOW_DELAY (ONE_LOW - 5) 9 | 10 | .define ZERO_HIGH 15 11 | .define ZERO_LOW (BIT_PERIOD - ZERO_HIGH) 12 | .define ZERO_HIGH_DELAY (ZERO_HIGH - 1) 13 | .define ZERO_LOW_DELAY (ZERO_LOW - 5) 14 | 15 | // 8kHz DShot300 -> 1500 frame period -> 1500 - (BIT_PERIOD * 16 + 1) = 859 delay 16 | .define FRAME_DELAY (32 - 2) 17 | .define FRAME_DELAY_COUNT 26 18 | .define FRAME_DELAY_REMAINDER (27 - 7) // 27 = 859 - 32 * 26, -7 for other instructions 19 | 20 | init: 21 | ; These instructions are executed manually in the init function 22 | ; pull block ; The very first value configures repeat enabled 23 | ; mov isr, osr ; Store in ISR 24 | ; Always block on the first pull 25 | jmp blocking_pull 26 | 27 | maybe_pull: 28 | mov y, isr 29 | jmp !y blocking_pull 30 | pull noblock 31 | ; Repeat mode is enabled, delay is needed to control frame rate 32 | nop [FRAME_DELAY_REMAINDER] 33 | set y, FRAME_DELAY_COUNT 34 | frame_delay_loop: 35 | jmp !y start_frame [FRAME_DELAY] 36 | jmp y-- frame_delay_loop 37 | blocking_pull: 38 | pull block 39 | start_frame: 40 | mov x, osr ; Store the value for re-use next time 41 | jmp !x blocking_pull ; wait for non-zero value 42 | out y, 16 ; discard 16 most significant bits 43 | check_bit: 44 | jmp !osre start_bit 45 | jmp maybe_pull 46 | start_bit: 47 | out y, 1 48 | jmp !y do_zero 49 | do_one: 50 | set pins, 1 [ONE_HIGH_DELAY] 51 | set pins, 0 [ONE_LOW_DELAY] 52 | jmp check_bit 53 | do_zero: 54 | set pins, 1 [ZERO_HIGH_DELAY] 55 | set pins, 0 [ZERO_LOW_DELAY] 56 | jmp check_bit 57 | 58 | 59 | % c-sdk { 60 | static inline void dshot_encoder_program_init(PIO pio, uint sm, uint offset, uint pin, bool enable_repeat) { 61 | pio_sm_config c = dshot_encoder_program_get_default_config(offset); 62 | 63 | sm_config_set_set_pins(&c, pin, 1); 64 | pio_gpio_init(pio, pin); 65 | pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true); 66 | 67 | sm_config_set_out_shift(&c, false, false, 32); 68 | 69 | double clocks_per_us = clock_get_hz(clk_sys) / 1000000; 70 | // 3.333us per bit for dshot300 71 | sm_config_set_clkdiv(&c, 3.333 / dshot_encoder_BIT_PERIOD * clocks_per_us); 72 | 73 | pio_sm_init(pio, sm, offset, &c); 74 | 75 | pio_sm_put_blocking(pio, sm, enable_repeat); 76 | 77 | pio_sm_exec_wait_blocking(pio, sm, pio_encode_pull(/*if_empty=*/false, /*block=*/true)); 78 | pio_sm_exec(pio, sm, pio_encode_mov(pio_isr, pio_osr)); 79 | 80 | // The PIO will begin waiting for the first command value 81 | pio_sm_set_enabled(pio, sm, true); 82 | } 83 | %} 84 | --------------------------------------------------------------------------------