mirror of
https://github.com/ArduPilot/ardupilot
synced 2025-01-05 15:38:29 -04:00
1b7b705856
Co-authored-by: Torsten Z <t.zunker@fettec.net> Co-authored-by: Peter Barker <pb-gh@barker.dropbear.id.au> - use ArduPilot's coding guidelines and naming conventions - control motor speed - copy ESC telemetry data into MAVLink telemetry - save ESC telemetry data in dataflash logs - use RPM telemetry for dynamic notch filter frequencies - sum the current telemetry info from all ESCs and use it as virtual battery current monitor sensor - average the voltage telemetry info and use it as virtual battery voltage monitor sensor - average the temperature telemetry info and use it as virtual battery temperature monitor sensor - report telemetry communication error rate in the dataflash logs - warn the user if there is a gap in the bitmask parameter. - re-enumerate all ESCs if not armed (motors not spinning) when - there is a gap in their address space IDs - communication with one of the ESCs is lost - some of the configured ESCs are not found - some of the configured ESCs are not correctly configured - allows the user to configure motor rotation direction per ESC (only gets updated if not armed) - adds a serial simulator of FETtec OneWire ESCs - adds autotest (using the simulator) to fly a copter over a simulated serial link connection
960 lines
36 KiB
C++
960 lines
36 KiB
C++
/*
|
||
This program 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 program 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 <http://www.gnu.org/licenses/>.
|
||
*/
|
||
|
||
/* Initial protocol implementation was provided by FETtec */
|
||
/* Strongly modified by Amilcar Lucas, IAV GmbH */
|
||
|
||
#include <AP_Math/AP_Math.h>
|
||
#include <AP_Scheduler/AP_Scheduler.h>
|
||
#include <AP_SerialManager/AP_SerialManager.h>
|
||
#include <SRV_Channel/SRV_Channel.h>
|
||
#include <GCS_MAVLink/GCS.h>
|
||
|
||
#include "AP_FETtecOneWire.h"
|
||
#if HAL_AP_FETTEC_ONEWIRE_ENABLED
|
||
|
||
extern const AP_HAL::HAL& hal;
|
||
|
||
const AP_Param::GroupInfo AP_FETtecOneWire::var_info[] {
|
||
|
||
// @Param: MASK
|
||
// @DisplayName: Servo channel output bitmask
|
||
// @Description: Servo channel mask specifying FETtec ESC output. Set bits must be contiguous.
|
||
// @Bitmask: 0:SERVO1,1:SERVO2,2:SERVO3,3:SERVO4,4:SERVO5,5:SERVO6,6:SERVO7,7:SERVO8,8:SERVO9,9:SERVO10,10:SERVO11,11:SERVO12
|
||
// @RebootRequired: True
|
||
// @User: Standard
|
||
AP_GROUPINFO_FLAGS("MASK", 1, AP_FETtecOneWire, _motor_mask_parameter, 0, AP_PARAM_FLAG_ENABLE),
|
||
|
||
// @Param: RVMASK
|
||
// @DisplayName: Servo channel reverse rotation bitmask
|
||
// @Description: Servo channel mask to reverse rotation of FETtec ESC outputs.
|
||
// @Bitmask: 0:SERVO1,1:SERVO2,2:SERVO3,3:SERVO4,4:SERVO5,5:SERVO6,6:SERVO7,7:SERVO8,8:SERVO9,9:SERVO10,10:SERVO11,11:SERVO12
|
||
// @User: Standard
|
||
AP_GROUPINFO("RVMASK", 2, AP_FETtecOneWire, _reverse_mask_parameter, 0),
|
||
|
||
#if HAL_WITH_ESC_TELEM
|
||
// @Param: POLES
|
||
// @DisplayName: Nr. electrical poles
|
||
// @Description: Number of motor electrical poles
|
||
// @Range: 2 50
|
||
// @RebootRequired: False
|
||
// @User: Standard
|
||
AP_GROUPINFO("POLES", 3, AP_FETtecOneWire, _pole_count_parameter, 14),
|
||
#endif
|
||
|
||
AP_GROUPEND
|
||
};
|
||
|
||
AP_FETtecOneWire *AP_FETtecOneWire::_singleton;
|
||
|
||
AP_FETtecOneWire::AP_FETtecOneWire()
|
||
{
|
||
AP_Param::setup_object_defaults(this, var_info);
|
||
|
||
#if CONFIG_HAL_BOARD == HAL_BOARD_SITL
|
||
if (_singleton != nullptr) {
|
||
AP_HAL::panic("AP_FETtecOneWire must be singleton");
|
||
}
|
||
#endif
|
||
_singleton = this;
|
||
|
||
_response_length[uint8_t(msg_type::OK)] = 1;
|
||
_response_length[uint8_t(msg_type::BL_START_FW)] = 0; // Bootloader only
|
||
#if HAL_AP_FETTEC_ONEWIRE_GET_STATIC_INFO
|
||
_response_length[uint8_t(msg_type::REQ_TYPE)] = 1;
|
||
_response_length[uint8_t(msg_type::REQ_SN)] = 12;
|
||
_response_length[uint8_t(msg_type::REQ_SW_VER)] = 2;
|
||
#endif
|
||
_response_length[uint8_t(msg_type::SET_FAST_COM_LENGTH)] = 1;
|
||
_response_length[uint8_t(msg_type::SET_TLM_TYPE)] = 1;
|
||
}
|
||
|
||
/**
|
||
initialize the serial port, scan the bus, setup the found ESCs
|
||
|
||
*/
|
||
void AP_FETtecOneWire::init()
|
||
{
|
||
if (_uart == nullptr) {
|
||
const AP_SerialManager& serial_manager = AP::serialmanager();
|
||
_uart = serial_manager.find_serial(AP_SerialManager::SerialProtocol_FETtecOneWire, 0);
|
||
if (_uart == nullptr) {
|
||
return; // no serial port available, so nothing to do here
|
||
}
|
||
_uart->set_flow_control(AP_HAL::UARTDriver::FLOW_CONTROL_DISABLE);
|
||
_uart->set_unbuffered_writes(true);
|
||
_uart->set_blocking_writes(false);
|
||
#if HAL_AP_FETTEC_HALF_DUPLEX
|
||
if (_uart->get_options() & _uart->OPTION_HDPLEX) { //Half-Duplex is enabled
|
||
_use_hdplex = true;
|
||
_uart->begin(2000000U);
|
||
GCS_SEND_TEXT(MAV_SEVERITY_INFO, "FTW using Half-Duplex");
|
||
} else {
|
||
_uart->begin(500000U);
|
||
GCS_SEND_TEXT(MAV_SEVERITY_INFO, "FTW using Full-Duplex");
|
||
}
|
||
#else
|
||
_uart->begin(500000U);
|
||
#endif
|
||
}
|
||
|
||
if (_scan.state != scan_state_t::DONE) {
|
||
scan_escs();
|
||
return;
|
||
}
|
||
|
||
#if HAL_WITH_ESC_TELEM
|
||
_update_rate_hz = AP::scheduler().get_loop_rate_hz();
|
||
_crc_error_rate_factor = 100.0f/(float)_update_rate_hz; //to save the division in loop, precalculate by the motor loops 100%/400Hz
|
||
#endif
|
||
|
||
// do not read telemetry information until a fast-throttle command is send
|
||
// and if HAL_WITH_ESC_TELEM is disabled this also ensures correct operation
|
||
_requested_telemetry_from_esc = -1;
|
||
|
||
// get the user-configured FETtec ESCs bitmask parameter
|
||
// if the user changes this parameter, he will have to reboot
|
||
_motor_mask = uint16_t(_motor_mask_parameter.get());
|
||
uint16_t smask = _motor_mask; // shifted version of the _motor_mask user parameter
|
||
uint16_t mmask = 0; // will be a copy of _motor_mask with only the contiguous LSBs set
|
||
|
||
static_assert(MOTOR_COUNT_MAX <= sizeof(_motor_mask)*8, "_motor_mask is too narrow for MOTOR_COUNT_MAX ESCs");
|
||
static_assert(MOTOR_COUNT_MAX <= sizeof(_reverse_mask)*8, "_reverse_mask is too narrow for MOTOR_COUNT_MAX ESCs");
|
||
static_assert(MOTOR_COUNT_MAX <= sizeof(smask)*8, "smask is too narrow for MOTOR_COUNT_MAX ESCs");
|
||
static_assert(MOTOR_COUNT_MAX <= sizeof(mmask)*8, "mmask is too narrow for MOTOR_COUNT_MAX ESCs");
|
||
|
||
_nr_escs_in_bitmask = 0;
|
||
// count the number of contiguous user-configured FETtec ESCs in the bitmask parameter
|
||
for (uint8_t i = 0; i < MOTOR_COUNT_MAX; i++) {
|
||
if ((smask & 0x01) == 0x00) {
|
||
break;
|
||
}
|
||
smask >>= 1;
|
||
_nr_escs_in_bitmask++;
|
||
|
||
// build a copy of _motor_mask_parameter with only the contiguous LSBs set
|
||
mmask |= 0x1;
|
||
mmask <<= 1;
|
||
}
|
||
|
||
// tell SRV_Channels about ESC capabilities
|
||
SRV_Channels::set_digital_outputs(mmask, 0);
|
||
|
||
_initialised = true;
|
||
}
|
||
|
||
/**
|
||
check if the current configuration is OK
|
||
*/
|
||
void AP_FETtecOneWire::configuration_check()
|
||
{
|
||
if (hal.util->get_soft_armed()) {
|
||
return; // checks are only done when vehicle is disarmed, because the GCS_SEND_TEXT() function calls use lots of resources
|
||
}
|
||
|
||
// for safety, only update the reversed motors bitmask when motors are disarmed
|
||
_reverse_mask= _reverse_mask_parameter;
|
||
|
||
const uint32_t now = AP_HAL::millis();
|
||
if ((now - _last_config_check_ms < 3000) && _last_config_check_ms != 0) { // only runs once every 3 seconds
|
||
return;
|
||
}
|
||
_last_config_check_ms = now;
|
||
|
||
#if CONFIG_HAL_BOARD != HAL_BOARD_SITL
|
||
if (!_uart->is_dma_enabled()) {
|
||
GCS_SEND_TEXT(MAV_SEVERITY_WARNING, "FTW UART needs DMA");
|
||
return;
|
||
}
|
||
#endif
|
||
|
||
const bool all_escs_found = _found_escs_count >= _nr_escs_in_bitmask;
|
||
const bool all_escs_configured = _found_escs_count == _configured_escs;
|
||
const bool all_escs_contiguous = _fast_throttle.max_id - _fast_throttle.min_id < _found_escs_count;
|
||
bool telem_rx_missing = false;
|
||
#if HAL_WITH_ESC_TELEM
|
||
// TLM recovery, if e.g. a power loss occurred but FC is still powered by USB.
|
||
const uint16_t active_esc_mask = AP::esc_telem().get_active_esc_mask();
|
||
const uint8_t num_active_escs = __builtin_popcount(active_esc_mask & _motor_mask);
|
||
|
||
telem_rx_missing = (num_active_escs < _nr_escs_in_bitmask) && (_sent_msg_count > 2 * MOTOR_COUNT_MAX);
|
||
#endif
|
||
|
||
if (__builtin_popcount(_motor_mask_parameter.get()) != _nr_escs_in_bitmask) {
|
||
GCS_SEND_TEXT(MAV_SEVERITY_WARNING, "FTW: gap in SERVO_FTW_MASK parameter bits");
|
||
}
|
||
|
||
if (!all_escs_contiguous){
|
||
GCS_SEND_TEXT(MAV_SEVERITY_WARNING, "FTW: gap in IDs found");
|
||
}
|
||
|
||
if (!all_escs_found) {
|
||
GCS_SEND_TEXT(MAV_SEVERITY_WARNING, "FTW: found only %u of %u ESCs", _found_escs_count, _nr_escs_in_bitmask);
|
||
}
|
||
|
||
if (!all_escs_configured) {
|
||
GCS_SEND_TEXT(MAV_SEVERITY_WARNING, "FTW: configured only %u of %u ESCs", _configured_escs, _found_escs_count);
|
||
}
|
||
|
||
#if HAL_WITH_ESC_TELEM
|
||
if (telem_rx_missing) {
|
||
GCS_SEND_TEXT(MAV_SEVERITY_WARNING, "FTW: got TLM from only %u of %u ESCs", num_active_escs, _nr_escs_in_bitmask);
|
||
}
|
||
#endif
|
||
|
||
if (!all_escs_contiguous || !all_escs_found || !all_escs_configured || telem_rx_missing) {
|
||
// re-init the entire device driver
|
||
#if HAL_WITH_ESC_TELEM
|
||
_sent_msg_count = 0;
|
||
#endif
|
||
_scan.state = scan_state_t::WAIT_FOR_BOOT;
|
||
_initialised = false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
transmits a FETtec OneWire frame to an ESC
|
||
@param esc_id id of the ESC
|
||
@param bytes 8 bit array of bytes. Where byte 1 contains the command, and all following bytes can be the payload
|
||
@param length length of the bytes array (max 4)
|
||
@return false if length is bigger than MAX_TRANSMIT_LENGTH, true on write success
|
||
*/
|
||
bool AP_FETtecOneWire::transmit(const uint8_t esc_id, const uint8_t* bytes, uint8_t length)
|
||
{
|
||
/*
|
||
a frame looks like:
|
||
byte 1 = frame header (master is always 0x01)
|
||
byte 2 = target ID (5bit)
|
||
byte 3 & 4 = frame type (always 0x00, 0x00 used for bootloader. here just for compatibility)
|
||
byte 5 = frame length over all bytes
|
||
byte 6 - X = request type, followed by the payload
|
||
byte X+1 = 8bit CRC
|
||
*/
|
||
uint8_t transmit_arr[FRAME_OVERHEAD+MAX_TRANSMIT_LENGTH] = {0x01};
|
||
transmit_arr[1] = esc_id+uint8_t(1); // one-indexed ESC ID
|
||
if (length > _uart->txspace() || length > MAX_TRANSMIT_LENGTH) {
|
||
return false; // no, do not send at all
|
||
}
|
||
transmit_arr[4] = length + FRAME_OVERHEAD;
|
||
for (uint8_t i = 0; i < length; i++) {
|
||
transmit_arr[i + 5] = bytes[i];
|
||
}
|
||
transmit_arr[length + 5] = crc8_dvb_update(0, transmit_arr, length + 5); // crc
|
||
_uart->write(transmit_arr, length + FRAME_OVERHEAD);
|
||
#if HAL_AP_FETTEC_HALF_DUPLEX
|
||
if (_use_hdplex) {
|
||
_ignore_own_bytes += length + 6;
|
||
}
|
||
#endif
|
||
return true;
|
||
}
|
||
|
||
void AP_FETtecOneWire::move_preamble_in_receive_buffer(uint8_t search_start_pos)
|
||
{
|
||
uint8_t i;
|
||
for (i=search_start_pos; i<receive_buf_used; i++) {
|
||
if ((uint8_t)receive_buf[i] == 0x02 ||
|
||
(uint8_t)receive_buf[i] == 0x03) {
|
||
break;
|
||
}
|
||
}
|
||
consume_bytes(i);
|
||
}
|
||
|
||
void AP_FETtecOneWire::consume_bytes(uint8_t n)
|
||
{
|
||
if (n == 0) {
|
||
return;
|
||
}
|
||
memmove(receive_buf, &receive_buf[n], receive_buf_used-n);
|
||
receive_buf_used = receive_buf_used - n;
|
||
}
|
||
|
||
/**
|
||
reads the FETtec OneWire answer frame of an ESC
|
||
@param bytes 8 bit byte array, where the received answer gets stored in
|
||
@param length the expected answer length
|
||
@param return_full_frame can be return_type::RESPONSE or return_type::FULL_FRAME
|
||
@return receive_response enum
|
||
*/
|
||
AP_FETtecOneWire::receive_response AP_FETtecOneWire::receive(uint8_t* bytes, uint8_t length, return_type return_full_frame)
|
||
{
|
||
/*
|
||
a frame looks like:
|
||
byte 1 = frame header (0x02 = bootloader, 0x03 = ESC firmware)
|
||
byte 2 = sender ID (5bit)
|
||
byte 3 & 4 = frame type (always 0x00, 0x00 used for bootloader. here just for compatibility)
|
||
byte 5 = frame length over all bytes
|
||
byte 6 - X = answer type, followed by the payload
|
||
byte X+1 = 8bit CRC
|
||
*/
|
||
|
||
if (length > MAX_RECEIVE_LENGTH) {
|
||
return receive_response::REQ_OVERLENGTH;
|
||
}
|
||
|
||
#if HAL_AP_FETTEC_HALF_DUPLEX
|
||
//ignore own bytes
|
||
if (_use_hdplex) {
|
||
while (_ignore_own_bytes > 0 && _uart->available()) {
|
||
_ignore_own_bytes--;
|
||
_uart->read();
|
||
}
|
||
}
|
||
#endif
|
||
|
||
// read as much from the uart as we can:
|
||
const uint8_t bytes_to_read = ARRAY_SIZE(receive_buf) - receive_buf_used;
|
||
uint32_t nbytes = _uart->read(&receive_buf[receive_buf_used], bytes_to_read);
|
||
if (nbytes == 0) {
|
||
return receive_response::NO_ANSWER_YET;
|
||
}
|
||
receive_buf_used += nbytes;
|
||
|
||
move_preamble_in_receive_buffer();
|
||
|
||
// we know what length of message should be present
|
||
const uint8_t raw_length = FRAME_OVERHEAD + length;
|
||
if (receive_buf_used < raw_length) {
|
||
return receive_response::NO_ANSWER_YET;
|
||
}
|
||
|
||
if (crc8_dvb_update(0, receive_buf, raw_length-1) != receive_buf[raw_length-1]) {
|
||
// bad message; shift away this preamble byte to try to find
|
||
// another message
|
||
move_preamble_in_receive_buffer(1);
|
||
return receive_response::CRC_MISSMATCH;
|
||
}
|
||
|
||
// message is good
|
||
switch (return_full_frame) {
|
||
case return_type::RESPONSE:
|
||
memcpy(bytes, &receive_buf[5], length);
|
||
break;
|
||
case return_type::FULL_FRAME:
|
||
memcpy(bytes, receive_buf, raw_length);
|
||
break;
|
||
}
|
||
|
||
consume_bytes(raw_length);
|
||
|
||
return receive_response::ANSWER_VALID;
|
||
}
|
||
|
||
/**
|
||
Pulls a complete request between flight controller and ESC
|
||
@param esc_id id of the ESC
|
||
@param command 8bit array containing the command that should be send including the possible payload
|
||
@param response 8bit array where the response will be stored in
|
||
@param return_full_frame can be return_type::RESPONSE or return_type::FULL_FRAME
|
||
@param req_len transmit request length
|
||
@return pull_state enum
|
||
*/
|
||
AP_FETtecOneWire::pull_state AP_FETtecOneWire::pull_command(const uint8_t esc_id, const uint8_t* command, uint8_t* response,
|
||
return_type return_full_frame, const uint8_t req_len)
|
||
{
|
||
if (!_pull_busy) {
|
||
_pull_busy = transmit(esc_id, command, req_len);
|
||
} else if (receive(response, _response_length[command[0]], return_full_frame) == receive_response::ANSWER_VALID) {
|
||
_scan.rx_try_cnt = 0;
|
||
_scan.trans_try_cnt = 0;
|
||
_pull_busy = false;
|
||
return pull_state::COMPLETED;
|
||
}
|
||
|
||
// it will try multiple times to read the response of a request
|
||
if (_scan.rx_try_cnt > 1) {
|
||
_scan.rx_try_cnt = 0;
|
||
|
||
_pull_busy = false; // re-transmit the request, in the hope of getting a valid response later
|
||
|
||
if (_scan.trans_try_cnt > 4) {
|
||
// the request re-transmit failed multiple times
|
||
_scan.trans_try_cnt = 0;
|
||
return pull_state::FAILED;
|
||
} else {
|
||
_scan.trans_try_cnt++;
|
||
}
|
||
} else {
|
||
_scan.rx_try_cnt++;
|
||
}
|
||
return pull_state::BUSY;
|
||
}
|
||
|
||
/**
|
||
Scans for all ESCs in bus. Configures fast-throttle and telemetry for the ones found.
|
||
Should be periodically called until _scan.state == scan_state_t::DONE
|
||
*/
|
||
void AP_FETtecOneWire::scan_escs()
|
||
{
|
||
uint8_t response[FRAME_OVERHEAD + MAX_RECEIVE_LENGTH];
|
||
uint8_t request[2];
|
||
|
||
const uint32_t now = AP_HAL::micros();
|
||
if (now - _scan.last_us < (_scan.state == scan_state_t::WAIT_START_FW ? 5000U : 2000U)) {
|
||
// the scan_escs() call period must be bigger than 2000 US,
|
||
// as the bootloader has some message timing requirements. And we might be in bootloader
|
||
return;
|
||
}
|
||
_scan.last_us = now;
|
||
|
||
switch (_scan.state) {
|
||
|
||
// initial state, wait for a ESC(s) cold-start
|
||
case scan_state_t::WAIT_FOR_BOOT:
|
||
_found_escs_count = 0;
|
||
_scan.id = 0;
|
||
for (uint8_t i = 0; i < MOTOR_COUNT_MAX; i++) {
|
||
_found_escs[i].active = false;
|
||
}
|
||
if (now > 500000U) {
|
||
_scan.state = scan_state_t::IN_BOOTLOADER;
|
||
}
|
||
break;
|
||
|
||
// is bootloader running?
|
||
case scan_state_t::IN_BOOTLOADER:
|
||
request[0] = uint8_t(msg_type::OK);
|
||
switch (pull_command(_scan.id, request, response, return_type::FULL_FRAME, 1)) {
|
||
case pull_state::BUSY:
|
||
break;
|
||
case pull_state::COMPLETED:
|
||
if (response[0] == 0x02) {
|
||
_scan.state = scan_state_t::START_FW; // is in bootloader, must start firmware
|
||
} else {
|
||
if (!_found_escs[_scan.id].active) {
|
||
_found_escs_count++; // found a new ESC not in bootloader
|
||
}
|
||
_found_escs[_scan.id].active = true;
|
||
#if HAL_AP_FETTEC_ONEWIRE_GET_STATIC_INFO
|
||
_scan.state = scan_state_t::ESC_TYPE;
|
||
#else
|
||
_scan.state = scan_state_t::NEXT_ID;
|
||
#endif
|
||
}
|
||
break;
|
||
case pull_state::FAILED:
|
||
_scan.state = scan_state_t::NEXT_ID;
|
||
break;
|
||
}
|
||
break;
|
||
|
||
// start the firmware
|
||
case scan_state_t::START_FW:
|
||
request[0] = uint8_t(msg_type::BL_START_FW);
|
||
if (transmit(_scan.id, request, 1)) {
|
||
_scan.state = scan_state_t::WAIT_START_FW;
|
||
}
|
||
break;
|
||
|
||
// wait for the firmware to start
|
||
case scan_state_t::WAIT_START_FW:
|
||
_uart->discard_input(); // discard the answer to the previous transmit
|
||
_scan.state = scan_state_t::IN_BOOTLOADER;
|
||
break;
|
||
|
||
#if HAL_AP_FETTEC_ONEWIRE_GET_STATIC_INFO
|
||
// ask the ESC type
|
||
case scan_state_t::ESC_TYPE:
|
||
request[0] = uint8_t(msg_type::REQ_TYPE);
|
||
switch (pull_command(_scan.id, request, response, return_type::RESPONSE, 1)) {
|
||
case pull_state::BUSY:
|
||
break;
|
||
case pull_state::COMPLETED:
|
||
_found_escs[_scan.id].esc_type = response[0];
|
||
_scan.state = scan_state_t::SW_VER;
|
||
break;
|
||
case pull_state::FAILED:
|
||
_scan.state = scan_state_t::NEXT_ID;
|
||
break;
|
||
}
|
||
break;
|
||
|
||
// ask the software version
|
||
case scan_state_t::SW_VER:
|
||
request[0] = uint8_t(msg_type::REQ_SW_VER);
|
||
switch (pull_command(_scan.id, request, response, return_type::RESPONSE, 1)) {
|
||
case pull_state::BUSY:
|
||
break;
|
||
case pull_state::COMPLETED:
|
||
_found_escs[_scan.id].firmware_version = response[0];
|
||
_found_escs[_scan.id].firmware_sub_version = response[1];
|
||
_scan.state = scan_state_t::SN;
|
||
break;
|
||
case pull_state::FAILED:
|
||
_scan.state = scan_state_t::NEXT_ID;
|
||
break;
|
||
}
|
||
break;
|
||
|
||
// ask the serial number
|
||
case scan_state_t::SN:
|
||
request[0] = uint8_t(msg_type::REQ_SN);
|
||
switch (pull_command(_scan.id, request, response, return_type::RESPONSE, 1)) {
|
||
case pull_state::BUSY:
|
||
break;
|
||
case pull_state::COMPLETED:
|
||
for (uint8_t i = 0; i < SERIAL_NR_BITWIDTH; i++) {
|
||
_found_escs[_scan.id].serial_number[i] = response[i];
|
||
}
|
||
_scan.state = scan_state_t::NEXT_ID;
|
||
break;
|
||
case pull_state::FAILED:
|
||
_scan.state = scan_state_t::NEXT_ID;
|
||
break;
|
||
}
|
||
break;
|
||
#endif
|
||
|
||
// increment ESC ID and jump to IN_BOOTLOADER
|
||
case scan_state_t::NEXT_ID:
|
||
_scan.state = scan_state_t::IN_BOOTLOADER;
|
||
_scan.id++; // re-run this state machine with the next ESC ID
|
||
if (_scan.id == MOTOR_COUNT_MAX) {
|
||
_scan.id = 0;
|
||
if (_found_escs_count) {
|
||
// one or more ESCs found, scan is completed, now configure the ESCs found
|
||
config_fast_throttle();
|
||
_scan.id = _fast_throttle.min_id;
|
||
_configured_escs = 0;
|
||
_scan.state = scan_state_t::CONFIG_FAST_THROTTLE;
|
||
}
|
||
}
|
||
break;
|
||
|
||
// configure fast-throttle command header
|
||
case scan_state_t::CONFIG_FAST_THROTTLE:
|
||
switch (pull_command(_scan.id, _fast_throttle.command, response, return_type::RESPONSE, 4)) {
|
||
case pull_state::BUSY:
|
||
break;
|
||
case pull_state::COMPLETED:
|
||
#if HAL_WITH_ESC_TELEM
|
||
_scan.state = scan_state_t::CONFIG_TLM;
|
||
#else
|
||
_configured_escs++;
|
||
_scan.state = scan_state_t::CONFIG_NEXT_ACTIVE_ESC;
|
||
#endif
|
||
break;
|
||
case pull_state::FAILED:
|
||
_scan.state = scan_state_t::CONFIG_NEXT_ACTIVE_ESC;
|
||
break;
|
||
}
|
||
break;
|
||
|
||
#if HAL_WITH_ESC_TELEM
|
||
// configure telemetry mode
|
||
case scan_state_t::CONFIG_TLM:
|
||
request[0] = uint8_t(msg_type::SET_TLM_TYPE);
|
||
request[1] = 1; // Alternative telemetry mode -> a single ESC sends it's full telem (Temp, Volt, Current, ERPM, Consumption, CrcErrCount) in a single frame
|
||
switch (pull_command(_scan.id, request, response, return_type::RESPONSE, 2)) {
|
||
case pull_state::BUSY:
|
||
break;
|
||
case pull_state::COMPLETED:
|
||
_configured_escs++;
|
||
_scan.state = scan_state_t::CONFIG_NEXT_ACTIVE_ESC;
|
||
break;
|
||
case pull_state::FAILED:
|
||
_scan.state = scan_state_t::CONFIG_NEXT_ACTIVE_ESC;
|
||
break;
|
||
}
|
||
break;
|
||
#endif
|
||
|
||
// increment ESC ID and jump to CONFIG_FAST_THROTTLE
|
||
case scan_state_t::CONFIG_NEXT_ACTIVE_ESC:
|
||
do {
|
||
_scan.id++;
|
||
} while (_scan.id < MOTOR_COUNT_MAX && _found_escs[_scan.id].active == false);
|
||
_scan.state = scan_state_t::CONFIG_FAST_THROTTLE;
|
||
if (_scan.id == MOTOR_COUNT_MAX) {
|
||
_scan.id = 0;
|
||
_scan.state = scan_state_t::DONE; // one or more ESCs found, scan is completed
|
||
}
|
||
break;
|
||
|
||
case scan_state_t::DONE:
|
||
INTERNAL_ERROR(AP_InternalError::error_t::flow_of_control);
|
||
break;
|
||
}
|
||
}
|
||
|
||
/**
|
||
configure the fast-throttle command.
|
||
Should be called once after scan_escs() is completted and before config_escs()
|
||
*/
|
||
void AP_FETtecOneWire::config_fast_throttle()
|
||
{
|
||
_fast_throttle.min_id = MOTOR_COUNT_MAX;
|
||
_fast_throttle.max_id = 0;
|
||
for (uint8_t i = 0; i < MOTOR_COUNT_MAX; i++) {
|
||
if (_found_escs[i].active) {
|
||
if (i < _fast_throttle.min_id) {
|
||
_fast_throttle.min_id = i;
|
||
}
|
||
if (i > _fast_throttle.max_id) {
|
||
_fast_throttle.max_id = i;
|
||
}
|
||
}
|
||
}
|
||
|
||
_fast_throttle.byte_count = 1;
|
||
int16_t bit_count = 12 + (_found_escs_count * 11);
|
||
_fast_throttle.bits_to_add_left = bit_count - 16;
|
||
while (bit_count > 0) {
|
||
_fast_throttle.byte_count++;
|
||
bit_count -= 8;
|
||
}
|
||
_fast_throttle.command[0] = uint8_t(msg_type::SET_FAST_COM_LENGTH);
|
||
_fast_throttle.command[1] = _fast_throttle.byte_count; // just for older ESC FW versions since 1.0 001 this byte is ignored as the ESC calculates it itself
|
||
_fast_throttle.command[2] = _fast_throttle.min_id+1; // one-indexed min ESC id
|
||
_fast_throttle.command[3] = _found_escs_count; // count of ESCs that will get signals
|
||
}
|
||
|
||
#if HAL_WITH_ESC_TELEM
|
||
/**
|
||
increment message packet count for every ESC
|
||
*/
|
||
void AP_FETtecOneWire::inc_sent_msg_count()
|
||
{
|
||
_sent_msg_count++;
|
||
if (_sent_msg_count > 4 * _update_rate_hz) { // resets every four seconds
|
||
_sent_msg_count = 0;
|
||
for (int i=0; i<_found_escs_count; i++) {
|
||
_found_escs[i].error_count_since_overflow = _found_escs[i].error_count; //save the current ESC error state
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
calculates tx (outgoing packets) error-rate by converting the CRC error counts reported by the ESCs into percentage
|
||
@param esc_id id of ESC, that the error is calculated for
|
||
@param current_error_count the error count given by the esc
|
||
@return the error in percent
|
||
*/
|
||
float AP_FETtecOneWire::calc_tx_crc_error_perc(const uint8_t esc_id, uint16_t current_error_count)
|
||
{
|
||
_found_escs[esc_id].error_count = current_error_count; //Save the error count to the esc
|
||
uint16_t corrected_error_count = (uint16_t)((uint16_t)_found_escs[esc_id].error_count - (uint16_t)_found_escs[esc_id].error_count_since_overflow); //calculates error difference since last overflow.
|
||
return (float)corrected_error_count*_crc_error_rate_factor; //calculates percentage
|
||
}
|
||
|
||
/**
|
||
if init is complete checks if the requested telemetry is available.
|
||
@param t telemetry datastructure where the read telemetry will be stored in.
|
||
@param centi_erpm 16bit centi-eRPM value returned from the ESC
|
||
@param tx_err_count Ardupilot->ESC communication CRC error counter
|
||
@param tlm_from_id receives the ID from the ESC that has respond with its telemetry
|
||
@return receive_response enum
|
||
*/
|
||
AP_FETtecOneWire::receive_response AP_FETtecOneWire::decode_single_esc_telemetry(TelemetryData& t, int16_t& centi_erpm, uint16_t& tx_err_count, uint8_t &tlm_from_id)
|
||
{
|
||
receive_response ret = receive_response::NO_ANSWER_YET;
|
||
static constexpr uint8_t TELEM_LENGTH = 11;
|
||
static_assert(MAX_RECEIVE_LENGTH >= TELEM_LENGTH, "MAX_RECEIVE_LENGTH is too small");
|
||
|
||
if (_found_escs_count > 0) {
|
||
uint8_t telem[FRAME_OVERHEAD + TELEM_LENGTH];
|
||
ret = receive((uint8_t *) telem, TELEM_LENGTH, return_type::FULL_FRAME);
|
||
|
||
if (ret == receive_response::ANSWER_VALID) {
|
||
if (telem[1] <= _fast_throttle.min_id || telem[1] > _fast_throttle.max_id+1) {
|
||
return receive_response::NO_ANSWER_YET; // this data came from an unexpected ESC
|
||
}
|
||
tlm_from_id = (uint8_t)telem[1]-1; // convert external ESC's one-indexed IDs to Ardupilot's internal zero-indexed IDs
|
||
|
||
t.temperature_cdeg = int16_t(telem[5+0] * 100);
|
||
t.voltage = float((telem[5+1]<<8)|telem[5+2]) * 0.01f;
|
||
t.current = float((telem[5+3]<<8)|telem[5+4]) * 0.01f;
|
||
centi_erpm = (telem[5+5]<<8)|telem[5+6];
|
||
t.consumption_mah = float((telem[5+7]<<8)|telem[5+8]);
|
||
tx_err_count = (telem[5+9]<<8)|telem[5+10];
|
||
}
|
||
}
|
||
return ret;
|
||
}
|
||
#endif
|
||
|
||
/**
|
||
if init is complete sends a single fast-throttle frame containing the throttle for all found OneWire ESCs.
|
||
@param motor_values a 16bit array containing the throttle values that should be sent to the motors. 0-2000 where 1001-2000 is positive rotation and 0-999 reversed rotation
|
||
@param tlm_request the ESC to request telemetry from (-1 for no telemetry, 0 for ESC1, 1 for ESC2, 2 for ESC3, ...)
|
||
*/
|
||
void AP_FETtecOneWire::escs_set_values(const uint16_t* motor_values, const int8_t tlm_request)
|
||
{
|
||
if (_found_escs_count > 0) {
|
||
// 8 bits - OneWire Header
|
||
// 4 bits - telemetry request
|
||
// 11 bits - throttle value per ESC
|
||
// 8 bits - frame CRC
|
||
// 7 dummy for rounding up the division by 8
|
||
uint8_t fast_throttle_command[(8+4+(11*MOTOR_COUNT_MAX)+8+7)/8] { 0 };
|
||
uint8_t act_throttle_command = 0;
|
||
|
||
// byte 1:
|
||
// bit 0,1,2,3 = ESC ID, Bit 4 = MSB bit of first ESC (11bit) throttle value, bit 5,6,7 = frame header
|
||
// so AAAABCCC
|
||
// A = ID from the ESC telemetry is requested from. ESC ID == 0 means no request.
|
||
// B = MSB from first throttle value
|
||
// C = frame header
|
||
static_assert(MOTOR_COUNT_MAX<=15, "OneWire supports at most 15 ESCs, because of the 4 bit limitation bellow");
|
||
fast_throttle_command[0] = (tlm_request+1) << 4; // convert from zero indexed to one-indexed. -1 (AP no telemetry) gets correctly converted to 0 (ESC no telemetry)
|
||
fast_throttle_command[0] |= ((motor_values[act_throttle_command] >> 10) & 0x01) << 3;
|
||
fast_throttle_command[0] |= 0x01;
|
||
|
||
// byte 2:
|
||
// AAABBBBB
|
||
// A = next 3 bits from (11bit) throttle value
|
||
// B = 5bit target ID
|
||
fast_throttle_command[1] = (((motor_values[act_throttle_command] >> 7) & 0x07)) << 5;
|
||
fast_throttle_command[1] |= 0x1F; // All IDs
|
||
|
||
// following bytes are the rest 7 bit of the first (11bit) throttle value,
|
||
// and all bits from all other values, followed by the CRC byte
|
||
uint8_t bits_left_from_command = 7;
|
||
uint8_t act_byte = 2;
|
||
uint8_t bits_from_byte_left = 8;
|
||
int16_t bits_to_add_left = _fast_throttle.bits_to_add_left; // must be signed
|
||
while (bits_to_add_left > 0) {
|
||
if (bits_from_byte_left >= bits_left_from_command) {
|
||
fast_throttle_command[act_byte] |=
|
||
(motor_values[act_throttle_command] & ((1 << bits_left_from_command) - 1))
|
||
<< (bits_from_byte_left - bits_left_from_command);
|
||
bits_to_add_left -= bits_left_from_command;
|
||
bits_from_byte_left -= bits_left_from_command;
|
||
act_throttle_command++;
|
||
bits_left_from_command = 11;
|
||
if (bits_to_add_left == 0) {
|
||
act_byte++;
|
||
bits_from_byte_left = 8;
|
||
}
|
||
} else {
|
||
fast_throttle_command[act_byte] |=
|
||
(motor_values[act_throttle_command] >> (bits_left_from_command - bits_from_byte_left))
|
||
& ((1 << bits_from_byte_left) - 1);
|
||
bits_to_add_left -= bits_from_byte_left;
|
||
bits_left_from_command -= bits_from_byte_left;
|
||
act_byte++;
|
||
bits_from_byte_left = 8;
|
||
if (bits_left_from_command == 0) {
|
||
act_throttle_command++;
|
||
bits_left_from_command = 11;
|
||
}
|
||
}
|
||
}
|
||
|
||
fast_throttle_command[_fast_throttle.byte_count - 1] =
|
||
crc8_dvb_update(0, fast_throttle_command, _fast_throttle.byte_count - 1);
|
||
|
||
#if HAL_AP_FETTEC_HALF_DUPLEX
|
||
// last byte of signal can be used to make sure the first TLM byte is correct, in case of spike corruption
|
||
if (_use_hdplex) {
|
||
_ignore_own_bytes = _fast_throttle.byte_count - 1;
|
||
}
|
||
|
||
_last_crc = fast_throttle_command[_fast_throttle.byte_count - 1];
|
||
#endif
|
||
|
||
// No command was yet sent, so no reply is expected and all information
|
||
// on the receive buffer is either garbage or noise. Discard it
|
||
_uart->discard_input();
|
||
|
||
// send throttle commands to all configured ESCs in a single packet transfer
|
||
if (_uart->txspace() > _fast_throttle.byte_count) {
|
||
_uart->write(fast_throttle_command, _fast_throttle.byte_count);
|
||
}
|
||
}
|
||
}
|
||
|
||
bool AP_FETtecOneWire::pre_arm_check(char *failure_msg, const uint8_t failure_msg_len) const
|
||
{
|
||
if (_motor_mask_parameter == 0) {
|
||
return true; // No FETtec ESCs are expected, no need to run further pre-arm checks
|
||
}
|
||
|
||
if (_uart == nullptr) {
|
||
hal.util->snprintf(failure_msg, failure_msg_len, "No uart");
|
||
return false;
|
||
}
|
||
|
||
if (__builtin_popcount(_motor_mask_parameter.get()) != _nr_escs_in_bitmask) {
|
||
hal.util->snprintf(failure_msg, failure_msg_len, "Invalid motor mask; need consecutive bits only");
|
||
return false;
|
||
}
|
||
|
||
const bool all_escs_contiguous = _fast_throttle.max_id - _fast_throttle.min_id < _found_escs_count;
|
||
if (!all_escs_contiguous){
|
||
hal.util->snprintf(failure_msg, failure_msg_len, "gap in IDs found");
|
||
return false;
|
||
}
|
||
|
||
const bool all_escs_found = _found_escs_count >= _nr_escs_in_bitmask;
|
||
if (!all_escs_found) {
|
||
hal.util->snprintf(failure_msg, failure_msg_len, "found only %u of %u ESCs", _found_escs_count, _nr_escs_in_bitmask);
|
||
return false;
|
||
}
|
||
|
||
const bool all_escs_configured = _found_escs_count == _configured_escs;
|
||
if (!all_escs_configured) {
|
||
hal.util->snprintf(failure_msg, failure_msg_len, "configured only %u of %u ESCs", _configured_escs, _found_escs_count);
|
||
return false;
|
||
}
|
||
|
||
#if HAL_WITH_ESC_TELEM
|
||
const uint16_t active_esc_mask = AP::esc_telem().get_active_esc_mask();
|
||
const uint8_t num_active_escs = __builtin_popcount(active_esc_mask & _motor_mask);
|
||
|
||
bool telem_rx_missing = (num_active_escs < _nr_escs_in_bitmask) && (_sent_msg_count > 2 * MOTOR_COUNT_MAX);
|
||
if (telem_rx_missing) {
|
||
hal.util->snprintf(failure_msg, failure_msg_len, "got TLM from only %u of %u ESCs", num_active_escs, _nr_escs_in_bitmask);
|
||
return false;
|
||
}
|
||
#endif
|
||
|
||
return true;
|
||
}
|
||
|
||
/// periodically called from SRV_Channels::push()
|
||
void AP_FETtecOneWire::update()
|
||
{
|
||
if (!_initialised) {
|
||
init();
|
||
return; // the rest of this function can only run after fully initted
|
||
}
|
||
|
||
const uint32_t now = AP_HAL::micros();
|
||
if (now - _scan.last_us < 700U) {
|
||
// the update() call period must be bigger than 700 us,
|
||
// as to have time to receive the telemetry data
|
||
return;
|
||
}
|
||
_scan.last_us = now;
|
||
|
||
// get ESC set points
|
||
uint16_t motor_pwm[MOTOR_COUNT_MAX] {};
|
||
for (uint8_t i = 0; i < _nr_escs_in_bitmask; i++) {
|
||
const SRV_Channel* c = SRV_Channels::srv_channel(i);
|
||
if (c == nullptr) { // this should never ever happen, but just in case ...
|
||
motor_pwm[i] = 1000; // stop motor
|
||
continue;
|
||
}
|
||
// check if safety switch has been pushed
|
||
if (hal.util->safety_switch_state() == AP_HAL::Util::SAFETY_DISARMED) {
|
||
motor_pwm[i] = 1000; // stop motor
|
||
} else {
|
||
motor_pwm[i] = constrain_int16(c->get_output_pwm(), 1000, 2000);
|
||
}
|
||
if (_reverse_mask & (1U << i)) {
|
||
motor_pwm[i] = 2000-motor_pwm[i];
|
||
}
|
||
}
|
||
|
||
#if HAL_WITH_ESC_TELEM
|
||
// receive and decode the telemetry data from one ESC
|
||
// but do not process it any further to reduce timing jitter in the escs_set_values() function call
|
||
TelemetryData t {};
|
||
int16_t centi_erpm = 0; // initialize to prevent false positive error: ‘centi_erpm’ may be used uninitialized in this function
|
||
uint16_t tx_err_count = 0; // initialize to prevent false positive error: ‘tx_err_count’ may be used uninitialized in this function
|
||
receive_response tlm_ok = receive_response::NO_ANSWER_YET;
|
||
uint8_t tlm_from_id = 0;
|
||
if (_requested_telemetry_from_esc != -1) {
|
||
tlm_ok = decode_single_esc_telemetry(t, centi_erpm, tx_err_count, tlm_from_id);
|
||
if (_nr_escs_in_bitmask) {
|
||
_requested_telemetry_from_esc++;
|
||
if (_requested_telemetry_from_esc > _fast_throttle.max_id) {
|
||
_requested_telemetry_from_esc = _fast_throttle.min_id; // restart from the first ESC
|
||
}
|
||
}
|
||
} else {
|
||
_requested_telemetry_from_esc = _fast_throttle.min_id; // start from the first ESC
|
||
}
|
||
#endif
|
||
|
||
if (_nr_escs_in_bitmask) {
|
||
// send motor setpoints to ESCs, and request for telemetry data
|
||
escs_set_values(motor_pwm, _requested_telemetry_from_esc);
|
||
|
||
#if HAL_WITH_ESC_TELEM
|
||
// now that escs_set_values() has been executed we can fully process the telemetry data from the ESC
|
||
|
||
inc_sent_msg_count(); // increment message packet count for every ESC
|
||
|
||
if (_requested_telemetry_from_esc != -1 && tlm_ok == receive_response::ANSWER_VALID) { //only use telemetry if it is ok.
|
||
if (_pole_count_parameter < 2) { // if user set parameter is invalid use 14 Poles
|
||
_pole_count_parameter = 14;
|
||
}
|
||
const float tx_err_rate = calc_tx_crc_error_perc(tlm_from_id, tx_err_count);
|
||
update_rpm(tlm_from_id-_fast_throttle.min_id, centi_erpm*100*2/_pole_count_parameter.get(), tx_err_rate);
|
||
|
||
update_telem_data(tlm_from_id-_fast_throttle.min_id, t, TelemetryType::TEMPERATURE|TelemetryType::VOLTAGE|TelemetryType::CURRENT|TelemetryType::CONSUMPTION);
|
||
}
|
||
#endif
|
||
}
|
||
|
||
// Now that all real-time tasks above have been done, do some periodic checks.
|
||
configuration_check();
|
||
}
|
||
|
||
#if HAL_AP_FETTEC_ESC_BEEP
|
||
/**
|
||
makes all connected ESCs beep
|
||
@param beep_frequency a 8 bit value from 0-255. higher make a higher beep
|
||
*/
|
||
void AP_FETtecOneWire::beep(const uint8_t beep_frequency)
|
||
{
|
||
if (_found_escs_count > 0) {
|
||
const uint8_t request[2] = {uint8_t(msg_type::BEEP), beep_frequency};
|
||
const uint8_t spacer[2] = {0, 0};
|
||
for (uint8_t i = _fast_throttle.min_id; i <= _fast_throttle.max_id; i++) {
|
||
transmit(i, request, sizeof(request));
|
||
// add two zeros to make sure all ESCs can catch their command as we don't wait for a response here
|
||
_uart->write(spacer, sizeof(spacer));
|
||
#if HAL_AP_FETTEC_HALF_DUPLEX
|
||
if (_use_hdplex) {
|
||
_ignore_own_bytes += 2;
|
||
}
|
||
#endif
|
||
}
|
||
}
|
||
}
|
||
#endif
|
||
|
||
#if HAL_AP_FETTEC_ESC_LIGHT
|
||
/**
|
||
sets the racewire color for all ESCs
|
||
@param r red brightness
|
||
@param g green brightness
|
||
@param b blue brightness
|
||
*/
|
||
void AP_FETtecOneWire::led_color(const uint8_t r, const uint8_t g, const uint8_t b)
|
||
{
|
||
if (_found_escs_count > 0) {
|
||
const uint8_t request[4] = {uint8_t(msg_type::SET_LED_TMP_COLOR), r, g, b};
|
||
const uint8_t spacer[2] = {0, 0};
|
||
for (uint8_t i = _fast_throttle.min_id; i <= _fast_throttle.max_id; i++) {
|
||
transmit(i, request, sizeof(request));
|
||
// add two zeros to make sure all ESCs can catch their command as we don't wait for a response here
|
||
_uart->write(spacer, sizeof(spacer));
|
||
#if HAL_AP_FETTEC_HALF_DUPLEX
|
||
if (_use_hdplex) {
|
||
_ignore_own_bytes += 2;
|
||
}
|
||
#endif
|
||
}
|
||
}
|
||
}
|
||
#endif
|
||
|
||
#endif // HAL_AP_FETTEC_ONEWIRE_ENABLED
|