/*
* This file is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This file is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see .
*
* Bi-directional dshot based on Betaflight, code by Andy Piper and Siddharth Bharat Purohit
*/
#include
#include "RCOutput.h"
#include
#include "hwdef/common/stm32_util.h"
#include
#include
#ifdef HAL_WITH_BIDIR_DSHOT
using namespace ChibiOS;
extern const AP_HAL::HAL& hal;
#if RCOU_DSHOT_TIMING_DEBUG
#define DEBUG_CHANNEL 1
#define TOGGLE_PIN_CH_DEBUG(pin, channel) do { if (channel == DEBUG_CHANNEL) palToggleLine(HAL_GPIO_LINE_GPIO ## pin); } while (0)
#else
#define TOGGLE_PIN_CH_DEBUG(pin, channel) do {} while (0)
#endif
#define TELEM_IC_SAMPLE 16
/*
* enable bi-directional telemetry request for a mask of channels. This is used
* with DShot to get telemetry feedback
*/
void RCOutput::set_bidir_dshot_mask(uint16_t mask)
{
_bdshot.mask = (mask >> chan_offset);
// we now need to reconfigure the DMA channels since they are affected by the value of the mask
for (uint8_t i = 0; i < NUM_GROUPS; i++ ) {
pwm_group &group = pwm_group_list[i];
if (((group.ch_mask << chan_offset) & mask) == 0) {
// this group is not affected
continue;
}
set_group_mode(group);
}
}
bool RCOutput::bdshot_setup_group_ic_DMA(pwm_group &group)
{
// check if already allocated
if (group.has_ic_dma()) {
return true;
}
// allocate input capture DMA handles
for (uint8_t i = 0; i < 4; i++) {
if (!group.is_chan_enabled(i) ||
!group.dma_ch[i].have_dma || !(_bdshot.mask & (1 << group.chan[i]))) {
continue;
}
pwmmode_t mode = group.pwm_cfg.channels[i].mode;
if (mode == PWM_COMPLEMENTARY_OUTPUT_ACTIVE_LOW ||
mode == PWM_COMPLEMENTARY_OUTPUT_ACTIVE_HIGH) {
// Complementary channels don't support input capture
// Return error
return false;
}
if (!group.bdshot.ic_dma_handle[i]) {
// share up channel if required
if (group.dma_ch[i].stream_id == group.dma_up_stream_id) {
group.bdshot.ic_dma_handle[i] = group.dma_handle;
} else {
group.bdshot.ic_dma_handle[i] = new Shared_DMA(group.dma_ch[i].stream_id, SHARED_DMA_NONE,
FUNCTOR_BIND_MEMBER(&RCOutput::bdshot_ic_dma_allocate, void, Shared_DMA *),
FUNCTOR_BIND_MEMBER(&RCOutput::bdshot_ic_dma_deallocate, void, Shared_DMA *));
}
if (!group.bdshot.ic_dma_handle[i]) {
return false;
}
}
}
// We might need to do sharing of timers for telemetry feedback
// due to lack of available DMA channels
for (uint8_t i = 0; i < 4; i++) {
// we must pull all the allocated channels high to prevent them going low
// when the pwm peripheral is stopped
if (group.chan[i] != CHAN_DISABLED && _bdshot.mask & group.ch_mask) {
// bi-directional dshot requires less than MID2 speed and PUSHPULL in order to avoid noise on the line
// when switching from output to input
palSetLineMode(group.pal_lines[i], PAL_MODE_ALTERNATE(group.alt_functions[i])
| PAL_STM32_OTYPE_PUSHPULL | PAL_STM32_PUPDR_PULLUP | PAL_STM32_OSPEED_MID1);
}
if (!group.is_chan_enabled(i) || !(_bdshot.mask & (1 << group.chan[i]))) {
continue;
}
uint8_t curr_chan = i;
if (group.bdshot.ic_dma_handle[i]) {
// we are all good just set and continue
group.bdshot.telem_tim_ch[i] = curr_chan;
} else {
// I guess we have to share, but only channels 1 & 2 or 3 & 4
if (curr_chan % 2 == 0) {
curr_chan = curr_chan + 1;
} else {
curr_chan = curr_chan - 1;
}
if (!group.dma_ch[curr_chan].have_dma) {
// We can't find a DMA channel to use so
// return error
return false;
}
if (group.bdshot.ic_dma_handle[i]) {
INTERNAL_ERROR(AP_InternalError::error_t::dma_fail);
return false;
}
// share up channel if required
if (group.dma_ch[curr_chan].stream_id == group.dma_up_stream_id) {
group.bdshot.ic_dma_handle[i] = group.dma_handle;
} else {
// we can use the next channel
group.bdshot.ic_dma_handle[i] = new Shared_DMA(group.dma_ch[curr_chan].stream_id, SHARED_DMA_NONE,
FUNCTOR_BIND_MEMBER(&RCOutput::bdshot_ic_dma_allocate, void, Shared_DMA *),
FUNCTOR_BIND_MEMBER(&RCOutput::bdshot_ic_dma_deallocate, void, Shared_DMA *));
}
if (!group.bdshot.ic_dma_handle[i]) {
return false;
}
group.bdshot.telem_tim_ch[i] = curr_chan;
group.dma_ch[i] = group.dma_ch[curr_chan];
}
}
// now allocate the starting channel
for (uint8_t i = 0; i < 4; i++) {
if (group.chan[i] != CHAN_DISABLED && group.bdshot.ic_dma_handle[i] != nullptr) {
group.bdshot.curr_telem_chan = i;
break;
}
}
return true;
}
/*
allocate DMA channel
*/
void RCOutput::bdshot_ic_dma_allocate(Shared_DMA *ctx)
{
for (uint8_t i = 0; i < NUM_GROUPS; i++ ) {
pwm_group &group = pwm_group_list[i];
for (uint8_t icuch = 0; icuch < 4; icuch++) {
if (group.bdshot.ic_dma_handle[icuch] == ctx && group.bdshot.ic_dma[icuch] == nullptr) {
chSysLock();
group.bdshot.ic_dma[icuch] = dmaStreamAllocI(group.dma_ch[icuch].stream_id, 10, bdshot_dma_ic_irq_callback, &group);
chSysUnlock();
#if STM32_DMA_SUPPORTS_DMAMUX
if (group.bdshot.ic_dma[icuch]) {
dmaSetRequestSource(group.bdshot.ic_dma[icuch], group.dma_ch[icuch].channel);
}
#endif
}
}
}
}
/*
deallocate DMA channel
*/
void RCOutput::bdshot_ic_dma_deallocate(Shared_DMA *ctx)
{
for (uint8_t i = 0; i < NUM_GROUPS; i++ ) {
pwm_group &group = pwm_group_list[i];
for (uint8_t icuch = 0; icuch < 4; icuch++) {
if (group.bdshot.ic_dma_handle[icuch] == ctx && group.bdshot.ic_dma[icuch] != nullptr) {
chSysLock();
dmaStreamFreeI(group.bdshot.ic_dma[icuch]);
group.bdshot.ic_dma[icuch] = nullptr;
chSysUnlock();
}
}
}
}
// see https://github.com/betaflight/betaflight/pull/8554#issuecomment-512507625
// called from the interrupt
#pragma GCC push_options
#pragma GCC optimize("O2")
void RCOutput::bdshot_receive_pulses_DMAR(pwm_group* group)
{
// make sure the transaction finishes or times out, this function takes a little time to run so the most
// accurate timing is from the beginning. the pulse time is slightly longer than we need so an extra 10U
// should be plenty
chVTSetI(&group->dma_timeout, chTimeUS2I(group->dshot_pulse_send_time_us + 30U + 10U),
bdshot_finish_dshot_gcr_transaction, group);
uint8_t curr_ch = group->bdshot.curr_telem_chan;
group->pwm_drv->tim->CR1 = 0;
group->pwm_drv->tim->CCER = 0;
// Configure Timer
group->pwm_drv->tim->SR = 0;
group->pwm_drv->tim->CCMR1 = 0;
group->pwm_drv->tim->CCMR2 = 0;
group->pwm_drv->tim->DIER = 0;
group->pwm_drv->tim->CR2 = 0;
group->pwm_drv->tim->PSC = group->bdshot.telempsc;
group->dshot_state = DshotState::RECV_START;
//TOGGLE_PIN_CH_DEBUG(54, curr_ch);
group->pwm_drv->tim->ARR = 0xFFFF; // count forever
group->pwm_drv->tim->CNT = 0;
// Initialise ICU channels
bdshot_config_icu_dshot(group->pwm_drv->tim, curr_ch, group->bdshot.telem_tim_ch[curr_ch]);
// do a little DMA dance when sharing with UP
#if STM32_DMA_SUPPORTS_DMAMUX
if (group->has_shared_ic_up_dma()) {
dmaSetRequestSource(group->dma, group->dma_ch[curr_ch].channel);
}
#endif
const stm32_dma_stream_t *ic_dma =
group->has_shared_ic_up_dma() ? group->dma : group->bdshot.ic_dma[curr_ch];
// Configure DMA
dmaStreamSetPeripheral(ic_dma, &(group->pwm_drv->tim->DMAR));
dmaStreamSetMemory0(ic_dma, group->dma_buffer);
dmaStreamSetTransactionSize(ic_dma, GCR_TELEMETRY_BIT_LEN);
dmaStreamSetFIFO(ic_dma, STM32_DMA_FCR_DMDIS | STM32_DMA_FCR_FTH_FULL);
dmaStreamSetMode(ic_dma,
STM32_DMA_CR_CHSEL(group->dma_ch[curr_ch].channel) |
STM32_DMA_CR_DIR_P2M | STM32_DMA_CR_PSIZE_WORD | STM32_DMA_CR_MSIZE_WORD |
STM32_DMA_CR_MINC | STM32_DMA_CR_PL(3) |
STM32_DMA_CR_TEIE | STM32_DMA_CR_TCIE);
// setup for transfers. 0x0D is the register
// address offset of the CCR registers in the timer peripheral
group->pwm_drv->tim->DCR = (0x0D + group->bdshot.telem_tim_ch[curr_ch]) | STM32_TIM_DCR_DBL(0);
// Start Timer
group->pwm_drv->tim->EGR |= STM32_TIM_EGR_UG;
group->pwm_drv->tim->SR = 0;
group->pwm_drv->tim->CR1 = TIM_CR1_ARPE | STM32_TIM_CR1_URS | STM32_TIM_CR1_UDIS | STM32_TIM_CR1_CEN;
dmaStreamEnable(ic_dma);
}
void RCOutput::bdshot_config_icu_dshot(stm32_tim_t* TIMx, uint8_t chan, uint8_t ccr_ch)
{
switch(ccr_ch) {
case 0: {
/* Disable the Channel 1: Reset the CC1E Bit */
TIMx->CCER &= (uint32_t)~TIM_CCER_CC1E;
const uint32_t CCMR1_FILT = TIM_CCMR1_IC1F_1; // 4 samples per output transition
// Select the Input and set the filter and the prescaler value
if (chan == 0) {
MODIFY_REG(TIMx->CCMR1,
(TIM_CCMR1_CC1S | TIM_CCMR1_IC1F | TIM_CCMR1_IC1PSC),
(TIM_CCMR1_CC1S_0 | CCMR1_FILT));
} else {
MODIFY_REG(TIMx->CCMR1,
(TIM_CCMR1_CC1S | TIM_CCMR1_IC1F | TIM_CCMR1_IC1PSC),
(TIM_CCMR1_CC1S_1 | CCMR1_FILT));
}
// Select the Polarity as Both Edge and set the CC1E Bit
MODIFY_REG(TIMx->CCER,
(TIM_CCER_CC1P | TIM_CCER_CC1NP | TIM_CCER_CC1E),
(TIM_CCER_CC1P | TIM_CCER_CC1NP | TIM_CCER_CC1E));
MODIFY_REG(TIMx->DIER, TIM_DIER_CC1DE, TIM_DIER_CC1DE);
break;
}
case 1: {
// Disable the Channel 2: Reset the CC2E Bit
TIMx->CCER &= (uint32_t)~TIM_CCER_CC2E;
const uint32_t CCMR1_FILT = TIM_CCMR1_IC2F_1;
// Select the Input and set the filter and the prescaler value
if (chan == 0) {
MODIFY_REG(TIMx->CCMR1,
(TIM_CCMR1_CC2S | TIM_CCMR1_IC2F | TIM_CCMR1_IC2PSC),
(TIM_CCMR1_CC2S_1 | CCMR1_FILT));
} else {
MODIFY_REG(TIMx->CCMR1,
(TIM_CCMR1_CC2S | TIM_CCMR1_IC2F | TIM_CCMR1_IC2PSC),
(TIM_CCMR1_CC2S_0 | CCMR1_FILT));
}
// Select the Polarity as Both Edge and set the CC2E Bit
MODIFY_REG(TIMx->CCER,
TIM_CCER_CC2P | TIM_CCER_CC2NP | TIM_CCER_CC2E,
(TIM_CCER_CC2P | TIM_CCER_CC2NP | TIM_CCER_CC2E));
MODIFY_REG(TIMx->DIER, TIM_DIER_CC2DE, TIM_DIER_CC2DE);
break;
}
case 2: {
// Disable the Channel 3: Reset the CC3E Bit
TIMx->CCER &= (uint32_t)~TIM_CCER_CC3E;
const uint32_t CCMR2_FILT = TIM_CCMR2_IC3F_1;
// Select the Input and set the filter and the prescaler value
if (chan == 2) {
MODIFY_REG(TIMx->CCMR2,
(TIM_CCMR2_CC3S | TIM_CCMR2_IC3F | TIM_CCMR2_IC3PSC),
(TIM_CCMR2_CC3S_0 | CCMR2_FILT));
} else {
MODIFY_REG(TIMx->CCMR2,
(TIM_CCMR2_CC3S | TIM_CCMR2_IC3F | TIM_CCMR2_IC3PSC),
(TIM_CCMR2_CC3S_1 | CCMR2_FILT));
}
// Select the Polarity as Both Edge and set the CC3E Bit
MODIFY_REG(TIMx->CCER,
(TIM_CCER_CC3P | TIM_CCER_CC3NP | TIM_CCER_CC3E),
(TIM_CCER_CC3P | TIM_CCER_CC3NP | TIM_CCER_CC3E));
MODIFY_REG(TIMx->DIER, TIM_DIER_CC3DE, TIM_DIER_CC3DE);
break;
}
case 3: {
// Disable the Channel 4: Reset the CC4E Bit
TIMx->CCER &= (uint32_t)~TIM_CCER_CC4E;
const uint32_t CCMR2_FILT = TIM_CCMR2_IC4F_1;
// Select the Input and set the filter and the prescaler value
if (chan == 2) {
MODIFY_REG(TIMx->CCMR2,
(TIM_CCMR2_CC4S | TIM_CCMR2_IC4F | TIM_CCMR2_IC4PSC),
(TIM_CCMR2_CC4S_1 | CCMR2_FILT));
} else {
MODIFY_REG(TIMx->CCMR2,
(TIM_CCMR2_CC4S | TIM_CCMR2_IC4F | TIM_CCMR2_IC4PSC),
(TIM_CCMR2_CC4S_0 | CCMR2_FILT));
}
// Select the Polarity as Both Edge and set the CC4E Bit
MODIFY_REG(TIMx->CCER,
(TIM_CCER_CC4P | TIM_CCER_CC4NP | TIM_CCER_CC4E),
(TIM_CCER_CC4P | TIM_CCER_CC4NP | TIM_CCER_CC4E));
MODIFY_REG(TIMx->DIER, TIM_DIER_CC4DE, TIM_DIER_CC4DE);
break;
}
default:
break;
}
}
/*
unlock DMA channel after a bi-directional dshot transaction completes
*/
__RAMFUNC__ void RCOutput::bdshot_finish_dshot_gcr_transaction(void *p)
{
pwm_group *group = (pwm_group *)p;
chSysLockFromISR();
#ifdef HAL_GPIO_LINE_GPIO56
TOGGLE_PIN_DEBUG(56);
#endif
uint8_t curr_telem_chan = group->bdshot.curr_telem_chan;
// the DMA buffer is either the regular outbound one because we are sharing UP and CH
// or the input channel buffer
const stm32_dma_stream_t *dma =
group->has_shared_ic_up_dma() ? group->dma : group->bdshot.ic_dma[curr_telem_chan];
// record the transaction size before the stream is released
dmaStreamDisable(dma);
group->bdshot.dma_tx_size = MIN(uint16_t(GCR_TELEMETRY_BIT_LEN),
GCR_TELEMETRY_BIT_LEN - dmaStreamGetTransactionSize(dma));
stm32_cacheBufferInvalidate(group->dma_buffer, group->bdshot.dma_tx_size);
memcpy(group->bdshot.dma_buffer_copy, group->dma_buffer, sizeof(uint32_t) * group->bdshot.dma_tx_size);
group->dshot_state = DshotState::RECV_COMPLETE;
// if using input capture DMA and sharing the UP and CH channels then clean up
// by assigning the source back to UP
#if STM32_DMA_SUPPORTS_DMAMUX
if (group->has_shared_ic_up_dma()) {
dmaSetRequestSource(group->dma, group->dma_up_channel);
}
#endif
// rotate to the next input channel
group->bdshot.prev_telem_chan = group->bdshot.curr_telem_chan;
group->bdshot.curr_telem_chan = bdshot_find_next_ic_channel(*group);
// tell the waiting process we've done the DMA
chEvtSignalI(group->dshot_waiter, group->dshot_event_mask);
#ifdef HAL_GPIO_LINE_GPIO56
TOGGLE_PIN_DEBUG(56);
#endif
chSysUnlockFromISR();
}
/*
decode returned data from bi-directional dshot
*/
bool RCOutput::bdshot_decode_dshot_telemetry(pwm_group& group, uint8_t chan)
{
if (!group.is_chan_enabled(chan)) {
return true;
}
// evaluate dshot telemetry
group.bdshot.erpm[chan] = bdshot_decode_telemetry_packet(group.bdshot.dma_buffer_copy, group.bdshot.dma_tx_size);
group.dshot_state = DshotState::IDLE;
#if RCOU_DSHOT_TIMING_DEBUG
// Record Stats
if (group.bdshot.erpm[chan] != 0xFFFF) {
group.bdshot.telem_rate[chan]++;
} else {
#ifdef HAL_GPIO_LINE_GPIO57
TOGGLE_PIN_DEBUG(57);
#endif
group.bdshot.telem_err_rate[chan]++;
#ifdef HAL_GPIO_LINE_GPIO57
TOGGLE_PIN_DEBUG(57);
#endif
}
uint64_t now = AP_HAL::micros64();
if (chan == DEBUG_CHANNEL && (now - group.bdshot.last_print) > 1000000) {
hal.console->printf("TELEM: %d <%d Hz, %.1f%% err>", group.bdshot.erpm[chan], group.bdshot.telem_rate[chan],
100.0f * float(group.bdshot.telem_err_rate[chan]) / (group.bdshot.telem_err_rate[chan] + group.bdshot.telem_rate[chan]));
hal.console->printf(" %ld ", group.bdshot.dma_buffer_copy[0]);
for (uint8_t l = 1; l < group.bdshot.dma_tx_size; l++) {
hal.console->printf(" +%ld ", group.bdshot.dma_buffer_copy[l] - group.bdshot.dma_buffer_copy[l-1]);
}
hal.console->printf("\n");
group.bdshot.telem_rate[chan] = 0;
group.bdshot.telem_err_rate[chan] = 0;
group.bdshot.last_print = now;
}
#endif
return group.bdshot.erpm[chan] != 0xFFFF;
}
// Find next valid channel for dshot telem
uint8_t RCOutput::bdshot_find_next_ic_channel(const pwm_group& group)
{
uint8_t chan = group.bdshot.curr_telem_chan;
for (uint8_t i = 1; i < 4; i++) {
const uint8_t next_chan = (chan + i) % 4;
if (group.is_chan_enabled(next_chan) &&
group.bdshot.ic_dma_handle[next_chan] != nullptr) {
return next_chan;
}
}
return chan;
}
/*
DMA UP channel interrupt handler. Used to mark DMA send completed for DShot
*/
__RAMFUNC__ void RCOutput::dma_up_irq_callback(void *p, uint32_t flags)
{
pwm_group *group = (pwm_group *)p;
chSysLockFromISR();
// there is a small chance the shared UP CH codepath will get here
if (group->bdshot.enabled && group->dshot_state == DshotState::RECV_START) {
chSysUnlockFromISR();
return;
}
// check nothing bad happened
if ((flags & STM32_DMA_ISR_TEIF) != 0) {
INTERNAL_ERROR(AP_InternalError::error_t::dma_fail);
}
dmaStreamDisable(group->dma);
if (group->in_serial_dma && irq.waiter) {
// tell the waiting process we've done the DMA
chEvtSignalI(irq.waiter, serial_event_mask);
} else if (!group->in_serial_dma && group->bdshot.enabled) {
group->dshot_state = DshotState::SEND_COMPLETE;
// sending is done, in 30us the ESC will send telemetry
bdshot_receive_pulses_DMAR(group);
} else {
// non-bidir case, this prevents us ever having two dshot pulses too close together
if (is_dshot_protocol(group->current_mode)) {
// since we could be sending a dshot command, wait the full telemetry pulse width
// dshot mandates a minimum pulse separation of 40us
chVTSetI(&group->dma_timeout, chTimeUS2I(group->dshot_pulse_send_time_us + 30U + 40U), dma_unlock, p);
} else {
// WS2812 mandates a minimum pulse separation of 50us
chVTSetI(&group->dma_timeout, chTimeUS2I(50U), dma_unlock, p);
}
}
chSysUnlockFromISR();
}
// DMA IC channel handler. Used to mark DMA receive completed for DShot
__RAMFUNC__ void RCOutput::bdshot_dma_ic_irq_callback(void *p, uint32_t flags)
{
chSysLockFromISR();
// check nothing bad happened
if ((flags & STM32_DMA_ISR_TEIF) != 0) {
INTERNAL_ERROR(AP_InternalError::error_t::dma_fail);
}
chSysUnlockFromISR();
}
/*
returns the bitrate in Hz of the given output_mode
*/
uint32_t RCOutput::bdshot_get_output_rate_hz(const enum output_mode mode)
{
switch (mode) {
case MODE_PWM_DSHOT150:
return 150000U * 5 / 4;
case MODE_PWM_DSHOT300:
return 300000U * 5 / 4;
case MODE_PWM_DSHOT600:
return 600000U * 5 / 4;
case MODE_PWM_DSHOT1200:
return 1200000U * 5 / 4;
default:
// use 1 to prevent a possible divide-by-zero
return 1;
}
}
// decode a telemetry packet from a GCR encoded stride buffer, take from betaflight decodeTelemetryPacket
// see https://github.com/betaflight/betaflight/pull/8554#issuecomment-512507625 for a description of the protocol
uint32_t RCOutput::bdshot_decode_telemetry_packet(uint32_t* buffer, uint32_t count)
{
uint32_t value = 0;
uint32_t oldValue = buffer[0];
uint32_t bits = 0;
uint32_t len;
for (uint32_t i = 1; i <= count; i++) {
if (i < count) {
int32_t diff = buffer[i] - oldValue;
if (bits >= 21) {
break;
}
len = (diff + TELEM_IC_SAMPLE/2) / TELEM_IC_SAMPLE;
} else {
len = 21 - bits;
}
value <<= len;
value |= 1 << (len - 1);
oldValue = buffer[i];
bits += len;
}
if (bits != 21) {
return 0xffff;
}
static const uint32_t decode[32] = {
0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 10, 11, 0, 13, 14, 15,
0, 0, 2, 3, 0, 5, 6, 7, 0, 0, 8, 1, 0, 4, 12, 0 };
uint32_t decodedValue = decode[value & 0x1f];
decodedValue |= decode[(value >> 5) & 0x1f] << 4;
decodedValue |= decode[(value >> 10) & 0x1f] << 8;
decodedValue |= decode[(value >> 15) & 0x1f] << 12;
uint32_t csum = decodedValue;
csum = csum ^ (csum >> 8); // xor bytes
csum = csum ^ (csum >> 4); // xor nibbles
if ((csum & 0xf) != 0xf) {
return 0xffff;
}
decodedValue >>= 4;
if (decodedValue == 0x0fff) {
return 0;
}
decodedValue = (decodedValue & 0x000001ff) << ((decodedValue & 0xfffffe00) >> 9);
if (!decodedValue) {
return 0xffff;
}
uint32_t ret = (1000000 * 60 / 100 + decodedValue / 2) / decodedValue;
return ret;
}
#pragma GCC pop_options
#endif // HAL_WITH_BIDIR_DSHOT