dshot: add telemetry and publish esc_status message

This commit is contained in:
Beat Küng 2019-09-02 19:22:19 +02:00
parent d44302c03b
commit 65f3c7f93d
8 changed files with 552 additions and 36 deletions

View File

@ -3,9 +3,10 @@ uint8 CONNECTED_ESC_MAX = 8 # The number of ESCs supported. Current (Q2/2013)
uint8 ESC_CONNECTION_TYPE_PPM = 0 # Traditional PPM ESC
uint8 ESC_CONNECTION_TYPE_SERIAL = 1 # Serial Bus connected ESC
uint8 ESC_CONNECTION_TYPE_ONESHOOT = 2 # One Shoot PPM
uint8 ESC_CONNECTION_TYPE_ONESHOT = 2 # One Shot PPM
uint8 ESC_CONNECTION_TYPE_I2C = 3 # I2C
uint8 ESC_CONNECTION_TYPE_CAN = 4 # CAN-Bus
uint8 ESC_CONNECTION_TYPE_DSHOT = 5 # DShot
uint16 counter # incremented by the writing thread everytime new data is stored

View File

@ -36,10 +36,13 @@ px4_add_module(
STACK_MAIN 1200
SRCS
dshot.cpp
telemetry.cpp
DEPENDS
arch_io_pins
arch_dshot
mixer
mixer_module
output_limit
MODULE_CONFIG
module.yaml
)

View File

@ -46,6 +46,7 @@
#include <lib/parameters/param.h>
#include <lib/perf/perf_counter.h>
#include <px4_arch/dshot.h>
#include <px4_atomic.h>
#include <px4_config.h>
#include <px4_getopt.h>
#include <px4_log.h>
@ -60,6 +61,9 @@
#include <uORB/topics/actuator_outputs.h>
#include <uORB/topics/multirotor_motor_limits.h>
#include <uORB/topics/parameter_update.h>
#include <uORB/topics/esc_status.h>
#include "telemetry.h"
using namespace time_literals;
@ -148,6 +152,8 @@ public:
void updateOutputs(bool stop_motors, uint16_t outputs[MAX_ACTUATORS],
unsigned num_outputs, unsigned num_control_groups_updated) override;
void mixerChanged() override;
private:
enum class DShotConfig {
Disabled = 0,
@ -157,8 +163,22 @@ private:
DShot1200 = 1200,
};
struct Telemetry {
DShotTelemetry handler;
uORB::PublicationData<esc_status_s> esc_status_pub{ORB_ID(esc_status)};
int last_motor_index{-1};
};
void updateTelemetryNumMotors();
void initTelemetry(const char *device);
void handleNewTelemetryData(int motor_index, const DShotTelemetry::EscData &data);
MixingOutput _mixing_output{*this, MixingOutput::SchedulingPolicy::Auto, false, false};
Telemetry *_telemetry{nullptr};
static char _telemetry_device[20];
static px4::atomic_bool _request_telemetry_init;
Mode _mode{MODE_NONE};
uORB::Subscription _param_sub{ORB_ID(parameter_update)};
@ -188,10 +208,14 @@ private:
DEFINE_PARAMETERS(
(ParamInt<px4::params::DSHOT_CONFIG>) _param_dshot_config,
(ParamFloat<px4::params::DSHOT_MIN>) _param_dshot_min
(ParamFloat<px4::params::DSHOT_MIN>) _param_dshot_min,
(ParamInt<px4::params::MOT_POLE_COUNT>) _param_mot_pole_count
)
};
char DShotOutput::_telemetry_device[] {};
px4::atomic_bool DShotOutput::_request_telemetry_init{false};
DShotOutput::DShotOutput() :
CDev("/dev/dshot"),
OutputModuleInterface(MODULE_NAME, px4::wq_configurations::hp_default),
@ -214,6 +238,7 @@ DShotOutput::~DShotOutput()
perf_free(_cycle_perf);
perf_free(_cycle_interval_perf);
delete _telemetry;
}
int
@ -484,6 +509,79 @@ DShotOutput::update_dshot_out_state(bool on)
up_dshot_arm(on);
}
void DShotOutput::updateTelemetryNumMotors()
{
if (!_telemetry) {
return;
}
int motor_count = 0;
if (_mixing_output.mixers()) {
motor_count = _mixing_output.mixers()->get_multirotor_count();
}
_telemetry->handler.setNumMotors(motor_count);
}
void DShotOutput::initTelemetry(const char *device)
{
if (!_telemetry) {
_telemetry = new Telemetry{};
if (!_telemetry) {
PX4_ERR("alloc failed");
return;
}
}
int ret = _telemetry->handler.init(device);
if (ret != 0) {
PX4_ERR("telemetry init failed (%i)", ret);
}
updateTelemetryNumMotors();
}
void DShotOutput::handleNewTelemetryData(int motor_index, const DShotTelemetry::EscData &data)
{
// fill in new motor data
esc_status_s &esc_status = _telemetry->esc_status_pub.get();
if (motor_index < esc_status_s::CONNECTED_ESC_MAX) {
esc_status.esc_online_flags |= 1 << motor_index;
esc_status.esc[motor_index].timestamp = data.time;
esc_status.esc[motor_index].esc_rpm = ((int)data.erpm * 100) / (_param_mot_pole_count.get() / 2);
esc_status.esc[motor_index].esc_voltage = (float)data.voltage * 0.01f;
esc_status.esc[motor_index].esc_current = (float)data.current * 0.01f;
esc_status.esc[motor_index].esc_temperature = data.temperature;
// TODO: accumulate consumption and use for battery estimation
}
// publish when motor index wraps (which is robust against motor timeouts)
if (motor_index < _telemetry->last_motor_index) {
esc_status.timestamp = hrt_absolute_time();
esc_status.esc_connectiontype = esc_status_s::ESC_CONNECTION_TYPE_DSHOT;
esc_status.esc_count = _telemetry->handler.numMotors();
++esc_status.counter;
// FIXME: mark all ESC's as online, otherwise commander complains even for a single dropout
esc_status.esc_online_flags = (1 << esc_status.esc_count) - 1;
_telemetry->esc_status_pub.update();
// reset esc data (in case a motor times out, so we won't send stale data)
memset(&esc_status.esc, 0, sizeof(_telemetry->esc_status_pub.get().esc));
esc_status.esc_online_flags = 0;
}
_telemetry->last_motor_index = motor_index;
}
void DShotOutput::mixerChanged()
{
updateTelemetryNumMotors();
}
void DShotOutput::updateOutputs(bool stop_motors, uint16_t outputs[MAX_ACTUATORS],
unsigned num_outputs, unsigned num_control_groups_updated)
@ -494,6 +592,10 @@ void DShotOutput::updateOutputs(bool stop_motors, uint16_t outputs[MAX_ACTUATORS
int requested_telemetry_index = -1;
if (_telemetry) {
requested_telemetry_index = _mixing_output.reorderedMotorIndex(_telemetry->handler.getRequestMotorIndex());
}
if (stop_motors) {
for (int i = 0; i < (int)num_outputs; i++) {
@ -534,10 +636,24 @@ DShotOutput::Run()
update_dshot_out_state(outputs_on);
}
if (_telemetry) {
int telem_update = _telemetry->handler.update();
if (telem_update >= 0) {
handleNewTelemetryData(telem_update, _telemetry->handler.latestESCData());
}
}
if (_param_sub.updated()) {
update_params();
}
// telemetry device update request?
if (_request_telemetry_init.load()) {
initTelemetry(_telemetry_device);
_request_telemetry_init.store(false);
}
// check at end of cycle (updateSubscriptions() can potentially change to a different WorkQueue thread)
_mixing_output.updateSubscriptions(true);
@ -1108,6 +1224,17 @@ int DShotOutput::custom_command(int argc, char *argv[])
PortMode new_mode = PORT_MODE_UNSET;
const char *verb = argv[0];
if (!strcmp(verb, "telemetry")) {
if (argc > 1) {
// telemetry can be requested before the module is started
strncpy(_telemetry_device, argv[1], sizeof(_telemetry_device) - 1);
_telemetry_device[sizeof(_telemetry_device) - 1] = '\0';
_request_telemetry_init.store(true);
}
return 0;
}
/* start the FMU if not running */
if (!is_running()) {
int ret = DShotOutput::task_spawn(argc, argv);
@ -1234,6 +1361,9 @@ to use DShot as ESC communication protocol instead of PWM.
PRINT_MODULE_USAGE_COMMAND("mode_pwm1");
#endif
PRINT_MODULE_USAGE_COMMAND_DESCR("telemetry", "Enable Telemetry on a UART");
PRINT_MODULE_USAGE_ARG("<device>", "UART device", false);
PRINT_MODULE_USAGE_DEFAULT_COMMANDS();
return 0;
@ -1290,6 +1420,9 @@ int DShotOutput::print_status()
perf_print_counter(_cycle_perf);
perf_print_counter(_cycle_interval_perf);
_mixing_output.printStatus();
if (_telemetry) {
PX4_INFO("telemetry on: %s", _telemetry_device);
}
return 0;
}

View File

@ -0,0 +1,54 @@
module_name: DShot Driver
serial_config:
- command: dshot telemetry ${SERIAL_DEV}
port_config_param:
name: DSHOT_TEL_CFG
group: DShot
parameters:
- group: DShot
definitions:
DSHOT_CONFIG:
description:
short: Configure DShot
long: |
This enables/disables DShot. The different modes define different
speeds, for example DShot150 = 150kb/s. Not all ESCs support all modes.
Note: this enables DShot on the FMU outputs. For boards with an IO it is the
AUX outputs.
type: enum
values:
0: Disable (use PWM/Oneshot)
150: DShot150
300: DShot300
600: DShot600
1200: DShot1200
reboot_required: true
default: 0
DSHOT_MIN:
description:
short: Minimum DShot Motor Output
long: |
Minimum Output Value for DShot in percent. The value depends on the ESC. Make
sure to set this high enough so that the motors are always spinning while
armed.
type: float
unit: '%'
min: 0
max: 1
decimal: 2
increment: 0.01
default: 0.055
MOT_POLE_COUNT: # only used by dshot so far, so keep it under the dshot group
description:
short: Number of magnetic poles of the motors
long: |
Specify the number of magnetic poles of the motors.
It is required to compute the RPM value from the eRPM returned with the ESC telemetry.
Either get the number from the motor spec sheet or count the magnets on the bell of the motor (not the stator magnets).
Typical motors for 5 inch props have 14 poles.
type: int32
default: 14

View File

@ -0,0 +1,265 @@
/****************************************************************************
*
* Copyright (c) 2019 PX4 Development Team. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
* 3. Neither the name PX4 nor the names of its contributors may be
* used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
****************************************************************************/
#include "telemetry.h"
#include <px4_log.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <errno.h>
using namespace time_literals;
#define DSHOT_TELEMETRY_UART_BAUDRATE 115200
DShotTelemetry::~DShotTelemetry()
{
deinit();
}
int DShotTelemetry::init(const char *uart_device)
{
deinit();
_uart_fd = ::open(uart_device, O_RDONLY | O_NOCTTY);
if (_uart_fd < 0) {
PX4_ERR("failed to open serial port: %s err: %d", uart_device, errno);
return -errno;
}
_current_motor_index_request = -1;
return setBaudrate(DSHOT_TELEMETRY_UART_BAUDRATE);
}
void DShotTelemetry::deinit()
{
if (_uart_fd >= 0) {
close(_uart_fd);
_uart_fd = -1;
}
}
int DShotTelemetry::update()
{
if (_uart_fd < 0) {
return -1;
}
if (_current_motor_index_request == -1) {
// nothing in progress, start a request
_current_motor_index_request = 0;
_current_request_start = 0;
_frame_position = 0;
return -1;
}
// read from the uart. This must be non-blocking, so check first if there is data available
int bytes_available = 0;
int ret = ioctl(_uart_fd, FIONREAD, (unsigned long)&bytes_available);
if (ret != 0 || bytes_available <= 0) {
// no data available. Check for a timeout
const hrt_abstime now = hrt_absolute_time();
if (_current_request_start > 0 && now - _current_request_start > 30_ms) {
PX4_DEBUG("ESC telemetry timeout for motor %i (frame pos=%i)", _current_motor_index_request, _frame_position);
requestNextMotor();
}
return -1;
}
const int buf_length = ESC_FRAME_SIZE;
uint8_t buf[buf_length];
int num_read = read(_uart_fd, buf, buf_length);
ret = -1;
for (int i = 0; i < num_read; ++i) {
if (decodeByte(buf[i])) {
ret = _current_motor_index_request;
requestNextMotor();
}
}
return ret;
}
bool DShotTelemetry::decodeByte(uint8_t byte)
{
_frame_buffer[_frame_position++] = byte;
if (_frame_position == ESC_FRAME_SIZE) {
PX4_DEBUG("got ESC frame for motor %i", _current_motor_index_request);
uint8_t checksum = crc8(_frame_buffer, ESC_FRAME_SIZE - 1);
uint8_t checksum_data = _frame_buffer[ESC_FRAME_SIZE - 1];
if (checksum == checksum_data) {
_latest_data.time = hrt_absolute_time();
_latest_data.temperature = _frame_buffer[0];
_latest_data.voltage = (_frame_buffer[1] << 8) | _frame_buffer[2];
_latest_data.current = (_frame_buffer[3] << 8) | _frame_buffer[4];
_latest_data.consumption = (_frame_buffer[5]) << 8 | _frame_buffer[6];
_latest_data.erpm = (_frame_buffer[7] << 8) | _frame_buffer[8];
PX4_DEBUG("Motor %i: temp=%i, V=%i, cur=%i, consumpt=%i, rpm=%i", _current_motor_index_request,
_latest_data.temperature, _latest_data.voltage, _latest_data.current, _latest_data.consumption,
_latest_data.erpm);
}
return true;
}
return false;
}
uint8_t DShotTelemetry::updateCrc8(uint8_t crc, uint8_t crc_seed)
{
uint8_t crc_u = crc ^ crc_seed;
for (int i = 0; i < 8; ++i) {
crc_u = (crc_u & 0x80) ? 0x7 ^ (crc_u << 1) : (crc_u << 1);
}
return crc_u;
}
uint8_t DShotTelemetry::crc8(const uint8_t *buf, uint8_t len)
{
uint8_t crc = 0;
for (int i = 0; i < len; ++i) {
crc = updateCrc8(buf[i], crc);
}
return crc;
}
void DShotTelemetry::requestNextMotor()
{
_current_motor_index_request = (_current_motor_index_request + 1) % _num_motors;
_current_request_start = 0;
_frame_position = 0;
}
int DShotTelemetry::getRequestMotorIndex()
{
if (_current_request_start != 0) {
// already in progress, do not send another request
return -1;
}
_current_request_start = hrt_absolute_time();
return _current_motor_index_request;
}
int DShotTelemetry::setBaudrate(unsigned baud)
{
int speed;
switch (baud) {
case 9600: speed = B9600; break;
case 19200: speed = B19200; break;
case 38400: speed = B38400; break;
case 57600: speed = B57600; break;
case 115200: speed = B115200; break;
case 230400: speed = B230400; break;
default:
return -EINVAL;
}
struct termios uart_config;
int termios_state;
/* fill the struct for the new configuration */
tcgetattr(_uart_fd, &uart_config);
//
// Input flags - Turn off input processing
//
// convert break to null byte, no CR to NL translation,
// no NL to CR translation, don't mark parity errors or breaks
// no input parity check, don't strip high bit off,
// no XON/XOFF software flow control
//
uart_config.c_iflag &= ~(IGNBRK | BRKINT | ICRNL |
INLCR | PARMRK | INPCK | ISTRIP | IXON);
//
// Output flags - Turn off output processing
//
// no CR to NL translation, no NL to CR-NL translation,
// no NL to CR translation, no column 0 CR suppression,
// no Ctrl-D suppression, no fill characters, no case mapping,
// no local output processing
//
// config.c_oflag &= ~(OCRNL | ONLCR | ONLRET |
// ONOCR | ONOEOT| OFILL | OLCUC | OPOST);
uart_config.c_oflag = 0;
//
// No line processing
//
// echo off, echo newline off, canonical mode off,
// extended input processing off, signal chars off
//
uart_config.c_lflag &= ~(ECHO | ECHONL | ICANON | IEXTEN | ISIG);
/* no parity, one stop bit, disable flow control */
uart_config.c_cflag &= ~(CSTOPB | PARENB | CRTSCTS);
/* set baud rate */
if ((termios_state = cfsetispeed(&uart_config, speed)) < 0) {
return -errno;
}
if ((termios_state = cfsetospeed(&uart_config, speed)) < 0) {
return -errno;
}
if ((termios_state = tcsetattr(_uart_fd, TCSANOW, &uart_config)) < 0) {
return -errno;
}
return 0;
}

View File

@ -31,39 +31,69 @@
*
****************************************************************************/
/**
* Configure DShot
*
* This enabled/disabled DShot. The different modes define different
* speeds, for example DShot150 = 150kb/s. Not all ESCs support all modes.
*
* Note: this enabled DShot on the FMU outputs. For boards with an IO it is the
* AUX outputs.
*
* @value 0 Disable (use PWM/Oneshot)
* @value 150 DShot150
* @value 300 DShot300
* @value 600 DShot600
* @value 1200 DShot1200
* @reboot_required true
* @group DShot
*/
PARAM_DEFINE_INT32(DSHOT_CONFIG, 0);
#pragma once
#include <drivers/drv_hrt.h>
class DShotTelemetry
{
public:
struct EscData {
hrt_abstime time;
int8_t temperature; ///< [deg C]
int16_t voltage; ///< [0.01V]
int16_t current; ///< [0.01A]
int16_t consumption; ///< [mAh]
int16_t erpm; ///< [100ERPM]
};
~DShotTelemetry();
int init(const char *uart_device);
void deinit();
void setNumMotors(int num_motors) { _num_motors = num_motors; }
int numMotors() const { return _num_motors; }
/**
* Minimum DShot Motor Output
*
* Minimum Output Value for DShot in percent. The value depends on the ESC. Make
* sure to set this high enough so that the motors are always spinning while
* armed.
*
* @unit norm
* @min 0
* @max 1
* @decimal 2
* @increment 0.01
* @group DShot
* Read telemetry from the UART (non-blocking) and handle timeouts.
* @return -1 if no update, >= 0 for the motor index. Use @latestESCData() to get the data.
*/
PARAM_DEFINE_FLOAT(DSHOT_MIN, 0.055f);
int update();
/**
* Get the motor index for which telemetry should be requested.
* @return -1 if no request should be made, motor index otherwise
*/
int getRequestMotorIndex();
const EscData &latestESCData() const { return _latest_data; }
private:
static constexpr int ESC_FRAME_SIZE = 10;
/**
* set the Baudrate
* @param baud
* @return 0 on success, <0 on error
*/
int setBaudrate(unsigned baud);
void requestNextMotor();
bool decodeByte(uint8_t byte);
static inline uint8_t updateCrc8(uint8_t crc, uint8_t crc_seed);
static uint8_t crc8(const uint8_t *buf, uint8_t len);
int _uart_fd{-1};
int _num_motors{0};
uint8_t _frame_buffer[ESC_FRAME_SIZE];
int _frame_position{0};
EscData _latest_data;
int _current_motor_index_request{-1};
hrt_abstime _current_request_start{0};
};

View File

@ -374,6 +374,23 @@ MixingOutput::reorderOutputs(uint16_t values[MAX_ACTUATORS])
*/
}
int MixingOutput::reorderedMotorIndex(int index)
{
if ((MotorOrdering)_param_mot_ordering.get() == MotorOrdering::Betaflight) {
switch (index) {
case 0: return 1;
case 1: return 2;
case 2: return 3;
case 3: return 0;
}
}
return index;
}
int MixingOutput::controlCallback(uintptr_t handle, uint8_t control_group, uint8_t control_index, float &input)
{
const MixingOutput *output = (const MixingOutput *)handle;
@ -420,6 +437,8 @@ void MixingOutput::resetMixer()
_mixers = nullptr;
_groups_required = 0;
}
_interface.mixerChanged();
}
int MixingOutput::loadMixer(const char *buf, unsigned len)
@ -447,6 +466,7 @@ int MixingOutput::loadMixer(const char *buf, unsigned len)
PX4_DEBUG("loaded mixers \n%s\n", buf);
updateParams();
_interface.mixerChanged();
return ret;
}

View File

@ -66,6 +66,9 @@ public:
virtual void updateOutputs(bool stop_motors, uint16_t outputs[MAX_ACTUATORS],
unsigned num_outputs, unsigned num_control_groups_updated) = 0;
/** called whenever the mixer gets updated/reset */
virtual void mixerChanged() {};
};
/**
@ -148,11 +151,18 @@ public:
uint16_t &reverseOutputMask() { return _reverse_output_mask; }
uint16_t &failsafeValue(int index) { return _failsafe_value[index]; }
/** Disarmed values: if ramp_up is true, then disarmedValue < minValue needs to hold */
/** Disarmed values: disarmedValue < minValue needs to hold */
uint16_t &disarmedValue(int index) { return _disarmed_value[index]; }
uint16_t &minValue(int index) { return _min_value[index]; }
uint16_t &maxValue(int index) { return _max_value[index]; }
/**
* Get the motor index that maps from PX4 convention to the configured one
* @param index motor index in [0, num_motors-1]
* @return reordered motor index. When out of range, the input index is returned
*/
int reorderedMotorIndex(int index);
protected:
void updateParams() override;