BLheli电调与Dshot通讯
电调(ESC)和协议协议综述
无刷电调基础知识以及BLHeli固件烧录和参数调整 - 梦幻之心星 - 博客园 (cnblogs.com)
电调固件
电调固件是在每个电调上运行的软件,它确定电调的性能,支持的协议以及可以使用的配置接口。电调可以使用的固件取决于硬件。
- SimonK:最古老的两种开源电调固件之一,已经过时不再更新。内嵌了一个开环foc驱动
- BLHeli:最古老的两种开源电调固件之一,由于其广泛的功能和友好的用户界面而变得流行。
- BLHeli_S:BLHeli固件的第二代。专门为具有Busybee处理器的电调开发。
- BLHeli_32:第三代和最新一代BLHeli固件。专门为32位电调编写,不再开源。
电调协议
电调协议是飞行控制器和电调用于通信的语言,决定了信号从飞控到电调的发送速度。
- 四轴飞行器使用的电调协议及信号宽度
- Standard PWM :1000us – 2000us
- Oneshot125:125us – 250us
- Oneshot 42:2us – 84us
- Multishot:5us – 25us
- Dshot
- Dshot150 :106.8us
- Dshot300 :53.4us
- Dshot600 :26.7us
- DShot1200:13.4us
- ProShot
Dshot
DShot(Digital Shot,是数字协议)(相对于oneshot,oneshot125,oneshot42,multishot等模拟协议)。它最初是由Felix在KISS提出的,后来被Betaflight和BLHeli_S开发团队采用。是用来和刷有BLheli固件的电调通讯的协议。
- BLHeli固件开源地址:https://github.com/bitdump/BLHeli.git
- BLHeli上位机:https://github.com/blheli-configurator/blheli-configurator.git
模拟电调和数字电调区别
模拟电调协议每隔几微秒就将电脉冲从飞控发送到电调。脉冲持续时间的长短(油门大小)决定了电机的功率。这种控制方案受到电噪声和电脉冲定时精度的限制。
使用 DShot 数字化意味着飞控将以数字编号的形式向电调发送精确的油门值。这个数字将有一个校验值,以便从飞控发送到电调的油门值永远不会被识别错误。
与Oneshot和Multishot相比,DShot有什么优势:
- 无需进行电调行程校准
- 更精确的传输信号,更强大的抗干扰能力
- 分辨率为2048,而其他协议分辨率为1000
- 比Oneshot协议更快
- 更安全,每个信号 电调都可以检测yifan损坏的数据
- DSHOT并不是效率最高的协议
DShot的速度
目前常用的DShot协议有:DShot600,DShot300,DShot150 (DShot1200已经在最新的betaflight4.11固件取消了),其传输速率:
•DShot600 – 600,000 bits/Sec
•DShot300 – 300,000 bits/Sec
•DShot150 – 150,000 bits/Sec
引入 DSHOT300 和 DSHOT150 是为了确保支持功能较弱的旧ESC(电调),DSHOT1200是较新的协仪,有些电调可能还不支持。
例如,DShot600的频率为600,000 / 1637500hz= 37.5 KHz,这意味着将一个油门值从飞控发送到电调需要大约26.7 uS。
各种电调传输协议速度对比与 Oneshot125、 Oneshot42和 Multishot 的速度相比(假设信号是100% 油门)
Oneshot125 – 250 uS
DShot150 – 106.7 uS
Oneshot42 – 84 uS
DShot300 – 53.3 uS
DShot600 – 26.7 uS
Multishot – 25 uS
快速传输速度的 DShot 理论上将允许高达33KHz的飞控运行频率。不要高达37.5 KHz,因为需要留有一些空间。虽然 DShot600没有 Multishot 那么快,但是只要它比飞控运行频率快就足够了。
Dshot600数据帧
一个 DShot 数据包由16位组成
-
11位表示油门值(2^10= 2048分辨率)
-
1位表示遥测请求是telemetry请求标志,tlm需要电调硬件支持
-
4位表示 CRC 校验(循环冗余校验)
-
速度600kbits/s,一帧信号的长度为26.7us。
-
对于DSHOT600,整个比特位的长度为1.67us(T0H+T0L或T1H+T1L),0的高电平时间为625ns,1的高电平时间是1250ns。
-
帧与帧之间需要一点间隔(2-3us),以区别不同的帧信号
-
11位油门值可以达到2048的分辨率,实际使用48-2047表示油门值,所以油门信号是2000的分辨率,0是上电后的默认值(锁定值),1-47表示一些命令和设置,一些值的意义如下:
- 1-5:beep(1= low freq. 5 = high freq
- esc信息请求(fw版本和通过tlm线发送的SN)
- 7:一个方向旋转
- 8:另一个方向旋转
- 9:3d模式关闭
- 10:3d模式打开
- 11:esc设置请求(saved settings over the TLM wire)
- 12:保存设置
BLheli固件烧录
比较简单,但是需要arduino uno,CH340,BLheli Suite
懒人免焊接傻瓜包会从零开始给电调刷BLHELI固件 – Elvin Play
双向dshot
Joe Lucid更进一步引入了双向 DSHOT协议
双向DSHOT信号使用反转电平(空闲为 1)。 FC 到 ESC 使用 DSHOT 帧,但最低 4 位是其他半字节异或后的反码(正常DSHOT的校验是不反码的)。
不作深入探索
BLheli固件
固件命名:
BLHeli_S代码除了修订版外,还用一个字母、另一个字母和两个数字命名。例如"A_L_10_REV16_0.HEX"。
第一个字母表示MCU的引脚;
第二个字母是L或H(L代表24MHz MCU,H代表48MHz MCU);
这两个数字表示FET的开关死区时间。单位为20.4ns。一些场效应晶体管驱动器具有自适应场效应晶体管死区时间控制,对于这些MOS管,则用00表示场效应晶体管开关死区时间。
可设置参数
启动功率:
启动功率可设置为0.031到1.5之间的相对值。这是启动期间允许的最大功率。实际应用的功率取决于节气门输入,可以更低,但最低电平是最高电平的四分之一。
启动功率也会影响双向操作,因为该参数用于限制方向反转期间应用的功率。
对于低转速,电机的最大功率是有限的,以便于检测低反电势电压。允许的最大功率可通过启动功率参数设置。较低的启动功率参数将为较低转速提供较低的最大功率(这从rev16.1开始实施)。
换向时间:
换向定时可设置为低/中低/中/中高/高,对应于00/7.50/150/22.50/300定时提前。
一般来说,一个中等设置将工作良好,但如果电机口吃它可以是有益的改变时间。一些高电感的电机可以有很长的换相退磁时间。这可能会导致电机停止或口吃时,快速油门增加,特别是在运行在低转速。将定时设置为高将允许更多的时间去消磁,通常是有帮助的。
消磁补偿:
消磁补偿是一种保护电机不因换相后较长的绕组退磁时间而失速的功能。典型的症状是发动机停止或快速增加油门时卡顿,特别是在低转速运行时。如上所述,设置高换向时间通常有帮助,但以效率为代价。
消磁补偿是解决这一问题的另一种方法。首先,它检测何时出现了demag情况。
- 在这种情况下,没有关于电机正时的信息,并且换相是盲目地以预测的正时进行的。
- 除此之外,在下一次换向前的一段时间内,电机电源被切断。计算出一个指标,表明demag情况有多严重。形势越严峻,越是停电。
当demag补偿设置为关闭时,电源永远不会切断。
当设置为低或高,电源被切断。对于高设置,断电更为猛烈。
通常,补偿参数的值越高,保护效果越好。
如果demag补偿设置得太高,最大功率可能会有所降低。
方向:
旋转方向可设置为前进/后退/双向前进/双向后退。
在双向模式下,中央油门为零,上面为前进方向旋转,下面为反向旋转。当选择双向操作时,TX编程被禁用。
嘟嘟声强度:
设置正常运行时的蜂鸣音强度。
信标强度:
设置蜂鸣信标蜂鸣时蜂鸣的强度。如果油门信号在给定时间内为零,电子悬架控制系统将开始发出蜂鸣声。请注意,设置高信标强度可能会导致电机或ESC过热!
信标延迟:
信标延迟设置信标哔哔声开始前的延迟。
TX编程:
如果禁用,则禁用油门校准。
最小油门、最大油门和中值油门:
这些设置设置ESC的油门范围。中央油门仅用于双向操作。为这些设置提供的值适用于正常的1000us到2000us输入信号,对于其他输入信号,这些值必须按比例缩放。
热保护:
可以启用或禁用热保护。温度阈值可以在800C和1400C之间编程(可编程阈值从rev16.3开始执行)。可编程阈值主要是为了支持硬件制造商使用,因为不同的硬件可以对所使用的各种组件的最高温度有不同的公差。
低转速功率保护:
可以启用或禁用低转速的功率限制。禁用它可能是必要的,以实现在低电源电压下运行的一些低kV电机的全功率。但是,禁用它会增加同步丢失的风险,并可能导致电机或电子稳定控制系统过热。
停止时制动:
可以启用或禁用制动停止。启用时,油门为零时将应用制动器。对于非零油门,此设置无效。
LED控制:
LED可以在支持它的ESC上控制。最多可以打开或关闭4个LED。
蜂鸣器含义
-
100%上电时,电子稳定控制系统鸣叫3次。
-
当检测到油门信号时,它会发出一声低沉的哔哔声。这表示开始警戒序列。
-
然后,当或如果油门为零,它会发出一声高音哔哔声。这标志着警戒序列的结束。
-
此外,如果在启用顺序期间检测到100%油门,电子悬架控制系统将开始油门校准。
-
如果电子稳定控制系统处于待命状态,并在给定时间内看到零油门,它会发出信标哔哔声,大约每三秒发出一声哔哔声。
热保护:
ESC测量MCU内的温度,如果温度过高,则限制电机功率。电机功率分四步限制:
- 如果温度高于临界值,电机功率限制在75%。
- 如果温度高于阈值加上50摄氏度,电机功率限制在50%。
- 如果温度高于阈值加上100摄氏度,电机功率限制在25%。
- 如果温度高于阈值+150℃,则电机功率限制为0%。
失速保护:
如果电机已尝试启动但几秒钟内未成功,它将停止尝试并等待油门归零,然后再尝试。
BLheli上位机——BLheli suite
同样可以通过arduino nano来设置如上一堆值,但是理论上直接用Dshot600就可以设置,可以尝试一下()?
其可以烧录,修改如上值
Dshot参考代码1
#define ESC_BIT_0 11
#define ESC_BIT_1 22
#define ESC_CMD_BUF_LEN 18
uint16_t ESC_CMD[ESC_CMD_BUF_LEN]={0};
static uint16_t prepareDshotPacket(const uint16_t value, int8_t requestTelemetry)
{
// 油门大小为11位 所以这里先左移一位 添加上请求回传标志共12位
uint16_t packet = (value << 1) | (requestTelemetry ? 1 : 0);
// 将12位数据分为3组 每组4位, 进行异或
// compute checksum
int csum = 0;
int csum_data = packet;
for (int i = 0; i < 3; i++) {
csum ^= csum_data; // xor data by nibbles
csum_data >>= 4;
}
//取最后四位 其他的不要
csum &= 0xf;
// append checksum 将CRC添加到后四位
packet = (packet << 4) | csum;
return packet;
}
static void pwmWriteDigital(uint16_t *esc_cmd, uint16_t value)
{
value = ( (value > 2047) ? 2047 : value );
value = prepareDshotPacket(value, 0);
esc_cmd[0] = (value & 0x8000) ? ESC_BIT_1 : ESC_BIT_0;
esc_cmd[1] = (value & 0x4000) ? ESC_BIT_1 : ESC_BIT_0;
esc_cmd[2] = (value & 0x2000) ? ESC_BIT_1 : ESC_BIT_0;
esc_cmd[3] = (value & 0x1000) ? ESC_BIT_1 : ESC_BIT_0;
esc_cmd[4] = (value & 0x0800) ? ESC_BIT_1 : ESC_BIT_0;
esc_cmd[5] = (value & 0x0400) ? ESC_BIT_1 : ESC_BIT_0;
esc_cmd[6] = (value & 0x0200) ? ESC_BIT_1 : ESC_BIT_0;
esc_cmd[7] = (value & 0x0100) ? ESC_BIT_1 : ESC_BIT_0;
esc_cmd[8] = (value & 0x0080) ? ESC_BIT_1 : ESC_BIT_0;
esc_cmd[9] = (value & 0x0040) ? ESC_BIT_1 : ESC_BIT_0;
esc_cmd[10] = (value & 0x0020) ? ESC_BIT_1 : ESC_BIT_0;
esc_cmd[11] = (value & 0x0010) ? ESC_BIT_1 : ESC_BIT_0;
esc_cmd[12] = (value & 0x8) ? ESC_BIT_1 : ESC_BIT_0;
esc_cmd[13] = (value & 0x4) ? ESC_BIT_1 : ESC_BIT_0;
esc_cmd[14] = (value & 0x2) ? ESC_BIT_1 : ESC_BIT_0;
esc_cmd[15] = (value & 0x1) ? ESC_BIT_1 : ESC_BIT_0;
HAL_TIM_PWM_Start_DMA(&htim1,TIM_CHANNEL_1,(uint32_t *)esc_cmd,ESC_CMD_BUF_LEN);
}
Dshot参考代码2
#ifndef __DSHOT_H__
#define __DSHOT_H__
#include "tim.h" // header from stm32cubemx code generate
#include <stdbool.h>
#include <math.h> // lrintf
/* User Configuration */
// Timer Clock
#define TIMER_CLOCK 100000000 // 100MHz
// MOTOR 1 (PA3) - TIM5 Channel 4, DMA1 Stream 3
#define MOTOR_1_TIM (&htim5)
#define MOTOR_1_TIM_CHANNEL TIM_CHANNEL_4
// MOTOR 2 (PA2) - TIM2 Channel 3, DMA1 Stream 1
#define MOTOR_2_TIM (&htim2)
#define MOTOR_2_TIM_CHANNEL TIM_CHANNEL_3
// MOTOR 3 (PA0) - TIM2 Channel 1, DMA1 Stream 5
#define MOTOR_3_TIM (&htim2)
#define MOTOR_3_TIM_CHANNEL TIM_CHANNEL_1
// MOTOR 4 (PA1) - TIM5 Channel 2, DMA1 Stream 4
#define MOTOR_4_TIM (&htim5)
#define MOTOR_4_TIM_CHANNEL TIM_CHANNEL_2
/* Definition */
#define MHZ_TO_HZ(x) ((x) * 1000000)
#define DSHOT600_HZ MHZ_TO_HZ(12)
#define DSHOT300_HZ MHZ_TO_HZ(6)
#define DSHOT150_HZ MHZ_TO_HZ(3)
#define MOTOR_BIT_0 7
#define MOTOR_BIT_1 14
#define MOTOR_BITLENGTH 20
#define DSHOT_FRAME_SIZE 16
#define DSHOT_DMA_BUFFER_SIZE 18 /* resolution + frame reset (2us) */
#define DSHOT_MIN_THROTTLE 48
#define DSHOT_MAX_THROTTLE 2047
#define DSHOT_RANGE (DSHOT_MAX_THROTTLE - DSHOT_MIN_THROTTLE)
/* Enumeration */
typedef enum
{
DSHOT150,
DSHOT300,
DSHOT600
} dshot_type_e;
/* Functions */
void dshot_init(dshot_type_e dshot_type);
void dshot_write(uint16_t* motor_value);
#endif /* __DSHOT_H__ */
#include "dshot.h"
/* Variables */
static uint32_t motor1_dmabuffer[DSHOT_DMA_BUFFER_SIZE];
static uint32_t motor2_dmabuffer[DSHOT_DMA_BUFFER_SIZE];
static uint32_t motor3_dmabuffer[DSHOT_DMA_BUFFER_SIZE];
static uint32_t motor4_dmabuffer[DSHOT_DMA_BUFFER_SIZE];
/* Static functions */
// dshot init
static uint32_t dshot_choose_type(dshot_type_e dshot_type);
static void dshot_set_timer(dshot_type_e dshot_type);
static void dshot_dma_tc_callback(DMA_HandleTypeDef *hdma);
static void dshot_put_tc_callback_function();
static void dshot_start_pwm();
// dshot write
static uint16_t dshot_prepare_packet(uint16_t value);
static void dshot_prepare_dmabuffer(uint32_t* motor_dmabuffer, uint16_t value);
static void dshot_prepare_dmabuffer_all();
static void dshot_dma_start();
static void dshot_enable_dma_request();
/* Functions */
void dshot_init(dshot_type_e dshot_type)
{
dshot_set_timer(dshot_type);
dshot_put_tc_callback_function();
dshot_start_pwm();
}
void dshot_write(uint16_t* motor_value)
{
dshot_prepare_dmabuffer_all(motor_value);
dshot_dma_start();
dshot_enable_dma_request();
}
/* Static functions */
static uint32_t dshot_choose_type(dshot_type_e dshot_type)
{
switch (dshot_type)
{
case(DSHOT600):
return DSHOT600_HZ;
case(DSHOT300):
return DSHOT300_HZ;
default:
case(DSHOT150):
return DSHOT150_HZ;
}
}
static void dshot_set_timer(dshot_type_e dshot_type)
{
uint16_t dshot_prescaler;
uint32_t timer_clock = TIMER_CLOCK; // all timer clock is same as SystemCoreClock in stm32f411
// Calculate prescaler by dshot type
dshot_prescaler = lrintf((float) timer_clock / dshot_choose_type(dshot_type) + 0.01f) - 1;
// motor1
__HAL_TIM_SET_PRESCALER(MOTOR_1_TIM, dshot_prescaler);
__HAL_TIM_SET_AUTORELOAD(MOTOR_1_TIM, MOTOR_BITLENGTH);
// motor2
__HAL_TIM_SET_PRESCALER(MOTOR_2_TIM, dshot_prescaler);
__HAL_TIM_SET_AUTORELOAD(MOTOR_2_TIM, MOTOR_BITLENGTH);
// motor3
__HAL_TIM_SET_PRESCALER(MOTOR_3_TIM, dshot_prescaler);
__HAL_TIM_SET_AUTORELOAD(MOTOR_3_TIM, MOTOR_BITLENGTH);
// motor4
__HAL_TIM_SET_PRESCALER(MOTOR_4_TIM, dshot_prescaler);
__HAL_TIM_SET_AUTORELOAD(MOTOR_4_TIM, MOTOR_BITLENGTH);
}
// __HAL_TIM_DISABLE_DMA is needed to eliminate the delay between different dshot signals
// I don't know why :(
static void dshot_dma_tc_callback(DMA_HandleTypeDef *hdma)
{
TIM_HandleTypeDef *htim = (TIM_HandleTypeDef *)((DMA_HandleTypeDef *)hdma)->Parent;
if (hdma == htim->hdma[TIM_DMA_ID_CC1])
{
__HAL_TIM_DISABLE_DMA(htim, TIM_DMA_CC1);
}
else if(hdma == htim->hdma[TIM_DMA_ID_CC2])
{
__HAL_TIM_DISABLE_DMA(htim, TIM_DMA_CC2);
}
else if(hdma == htim->hdma[TIM_DMA_ID_CC3])
{
__HAL_TIM_DISABLE_DMA(htim, TIM_DMA_CC3);
}
else if(hdma == htim->hdma[TIM_DMA_ID_CC4])
{
__HAL_TIM_DISABLE_DMA(htim, TIM_DMA_CC4);
}
}
static void dshot_put_tc_callback_function()
{
// TIM_DMA_ID_CCx depends on timer channel
MOTOR_1_TIM->hdma[TIM_DMA_ID_CC4]->XferCpltCallback = dshot_dma_tc_callback;
MOTOR_2_TIM->hdma[TIM_DMA_ID_CC3]->XferCpltCallback = dshot_dma_tc_callback;
MOTOR_3_TIM->hdma[TIM_DMA_ID_CC1]->XferCpltCallback = dshot_dma_tc_callback;
MOTOR_4_TIM->hdma[TIM_DMA_ID_CC2]->XferCpltCallback = dshot_dma_tc_callback;
}
static void dshot_start_pwm()
{
// Start the timer channel now.
// Enabling/disabling DMA request can restart a new cycle without PWM start/stop.
HAL_TIM_PWM_Start(MOTOR_1_TIM, MOTOR_1_TIM_CHANNEL);
HAL_TIM_PWM_Start(MOTOR_2_TIM, MOTOR_2_TIM_CHANNEL);
HAL_TIM_PWM_Start(MOTOR_3_TIM, MOTOR_3_TIM_CHANNEL);
HAL_TIM_PWM_Start(MOTOR_4_TIM, MOTOR_4_TIM_CHANNEL);
}
static uint16_t dshot_prepare_packet(uint16_t value)
{
uint16_t packet;
bool dshot_telemetry = false;
packet = (value << 1) | (dshot_telemetry ? 1 : 0);
// compute checksum
unsigned csum = 0;
unsigned csum_data = packet;
for(int i = 0; i < 3; i++)
{
csum ^= csum_data; // xor data by nibbles
csum_data >>= 4;
}
csum &= 0xf;
packet = (packet << 4) | csum;
return packet;
}
// Convert 16 bits packet to 16 pwm signal
static void dshot_prepare_dmabuffer(uint32_t* motor_dmabuffer, uint16_t value)
{
uint16_t packet;
packet = dshot_prepare_packet(value);
for(int i = 0; i < 16; i++)
{
motor_dmabuffer[i] = (packet & 0x8000) ? MOTOR_BIT_1 : MOTOR_BIT_0;
packet <<= 1;
}
motor_dmabuffer[16] = 0;
motor_dmabuffer[17] = 0;
}
static void dshot_prepare_dmabuffer_all(uint16_t* motor_value)
{
dshot_prepare_dmabuffer(motor1_dmabuffer, motor_value[0]);
dshot_prepare_dmabuffer(motor2_dmabuffer, motor_value[1]);
dshot_prepare_dmabuffer(motor3_dmabuffer, motor_value[2]);
dshot_prepare_dmabuffer(motor4_dmabuffer, motor_value[3]);
}
static void dshot_dma_start()
{
HAL_DMA_Start_IT(MOTOR_1_TIM->hdma[TIM_DMA_ID_CC4], (uint32_t)motor1_dmabuffer, (uint32_t)&MOTOR_1_TIM->Instance->CCR4, DSHOT_DMA_BUFFER_SIZE);
HAL_DMA_Start_IT(MOTOR_2_TIM->hdma[TIM_DMA_ID_CC3], (uint32_t)motor2_dmabuffer, (uint32_t)&MOTOR_2_TIM->Instance->CCR3, DSHOT_DMA_BUFFER_SIZE);
HAL_DMA_Start_IT(MOTOR_3_TIM->hdma[TIM_DMA_ID_CC1], (uint32_t)motor3_dmabuffer, (uint32_t)&MOTOR_3_TIM->Instance->CCR1, DSHOT_DMA_BUFFER_SIZE);
HAL_DMA_Start_IT(MOTOR_4_TIM->hdma[TIM_DMA_ID_CC2], (uint32_t)motor4_dmabuffer, (uint32_t)&MOTOR_4_TIM->Instance->CCR2, DSHOT_DMA_BUFFER_SIZE);
}
static void dshot_enable_dma_request()
{
__HAL_TIM_ENABLE_DMA(MOTOR_1_TIM, TIM_DMA_CC4);
__HAL_TIM_ENABLE_DMA(MOTOR_2_TIM, TIM_DMA_CC3);
__HAL_TIM_ENABLE_DMA(MOTOR_3_TIM, TIM_DMA_CC1);
__HAL_TIM_ENABLE_DMA(MOTOR_4_TIM, TIM_DMA_CC2);
}