mirror of https://github.com/ArduPilot/ardupilot
768 lines
31 KiB
C++
768 lines
31 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/>.
|
|
|
|
Code by Andy Piper with help from betaflight
|
|
*/
|
|
|
|
#include "AP_GyroFFT.h"
|
|
|
|
#if HAL_GYROFFT_ENABLED
|
|
|
|
#include <GCS_MAVLink/GCS.h>
|
|
#include <AP_Logger/AP_Logger.h>
|
|
#include <Filter/HarmonicNotchFilter.h>
|
|
|
|
extern const AP_HAL::HAL& hal;
|
|
|
|
#ifndef FFT_DEFAULT_WINDOW_SIZE
|
|
// the H7 can cope with a longer length and these boards generally have BMI088 which needs a longer length
|
|
#if defined(STM32H7)
|
|
#define FFT_DEFAULT_WINDOW_SIZE 64
|
|
#else
|
|
#define FFT_DEFAULT_WINDOW_SIZE 32
|
|
#endif
|
|
#endif
|
|
#define FFT_DEFAULT_WINDOW_OVERLAP 0.5f
|
|
#define FFT_THR_REF_DEFAULT 0.35f // the estimated throttle reference, 0 ~ 1
|
|
#define FFT_SNR_DEFAULT 25.0f // a higher SNR is safer and this works quite well on a Pixracer
|
|
#define FFT_STACK_SIZE 1024
|
|
#define FFT_REQUIRED_HARMONIC_FIT 10.0f
|
|
|
|
// table of user settable parameters
|
|
const AP_Param::GroupInfo AP_GyroFFT::var_info[] = {
|
|
|
|
// @Param: ENABLE
|
|
// @DisplayName: Enable
|
|
// @Description: Enable Gyro FFT analyser
|
|
// @Values: 0:Disabled,1:Enabled
|
|
// @User: Advanced
|
|
// @RebootRequired: True
|
|
AP_GROUPINFO_FLAGS("ENABLE", 1, AP_GyroFFT, _enable, 0, AP_PARAM_FLAG_ENABLE),
|
|
|
|
// @Param: MINHZ
|
|
// @DisplayName: Minimum Frequency
|
|
// @Description: Lower bound of FFT frequency detection in Hz.
|
|
// @Range: 10 400
|
|
// @Units: Hz
|
|
// @User: Advanced
|
|
AP_GROUPINFO("MINHZ", 2, AP_GyroFFT, _fft_min_hz, 80),
|
|
|
|
// @Param: MAXHZ
|
|
// @DisplayName: Maximum Frequency
|
|
// @Description: Upper bound of FFT frequency detection in Hz.
|
|
// @Range: 10 400
|
|
// @Units: Hz
|
|
// @User: Advanced
|
|
AP_GROUPINFO("MAXHZ", 3, AP_GyroFFT, _fft_max_hz, 200),
|
|
|
|
// @Param: SAMPLE_MODE
|
|
// @DisplayName: Sample Mode
|
|
// @Description: Sampling mode (and therefore rate). 0: Gyro rate sampling, 1: Fast loop rate sampling, 2: Fast loop rate / 2 sampling, 3: Fast loop rate / 3 sampling. Takes effect on reboot.
|
|
// @Range: 0 4
|
|
// @User: Advanced
|
|
// @RebootRequired: True
|
|
AP_GROUPINFO("SAMPLE_MODE", 4, AP_GyroFFT, _sample_mode, 0),
|
|
|
|
// @Param: WINDOW_SIZE
|
|
// @DisplayName: FFT window size
|
|
// @Description: Size of window to be used in FFT calculations. Takes effect on reboot. Must be a power of 2 and between 32 and 1024. Larger windows give greater frequency resolution but poorer time resolution, consume more CPU time and may not be appropriate for all vehicles. Time and frequency resolution are given by the sample-rate / window-size. Windows of 256 are only really recommended for F7 class boards, windows of 512 or more H7 class.
|
|
// @Range: 32 1024
|
|
// @User: Advanced
|
|
// @RebootRequired: True
|
|
AP_GROUPINFO("WINDOW_SIZE", 5, AP_GyroFFT, _window_size, FFT_DEFAULT_WINDOW_SIZE),
|
|
|
|
// @Param: WINDOW_OLAP
|
|
// @DisplayName: FFT window overlap
|
|
// @Description: Percentage of window to be overlapped before another frame is process. Takes effect on reboot. A good default is 50% overlap. Higher overlap results in more processed frames but not necessarily more temporal resolution. Lower overlap results in lost information at the frame edges.
|
|
// @Range: 0 0.9
|
|
// @User: Advanced
|
|
// @RebootRequired: True
|
|
AP_GROUPINFO("WINDOW_OLAP", 6, AP_GyroFFT, _window_overlap, FFT_DEFAULT_WINDOW_OVERLAP),
|
|
|
|
// @Param: FREQ_HOVER
|
|
// @DisplayName: FFT learned hover frequency
|
|
// @Description: The learned hover noise frequency
|
|
// @Range: 0 250
|
|
// @User: Advanced
|
|
AP_GROUPINFO("FREQ_HOVER", 7, AP_GyroFFT, _freq_hover_hz, 80.0f),
|
|
|
|
// @Param: THR_REF
|
|
// @DisplayName: FFT learned thrust reference
|
|
// @Description: FFT learned thrust reference for the hover frequency and FFT minimum frequency.
|
|
// @Range: 0.01 0.9
|
|
// @User: Advanced
|
|
AP_GROUPINFO("THR_REF", 8, AP_GyroFFT, _throttle_ref, FFT_THR_REF_DEFAULT),
|
|
|
|
// @Param: SNR_REF
|
|
// @DisplayName: FFT SNR reference threshold
|
|
// @Description: FFT SNR reference threshold in dB at which a signal is determined to be present.
|
|
// @Range: 0.0 100.0
|
|
// @User: Advanced
|
|
AP_GROUPINFO("SNR_REF", 9, AP_GyroFFT, _snr_threshold_db, FFT_SNR_DEFAULT),
|
|
|
|
// @Param: ATT_REF
|
|
// @DisplayName: FFT attenuation for bandwidth calculation
|
|
// @Description: FFT attenuation level in dB for bandwidth calculation and peak detection. The bandwidth is calculated by comparing peak power output with the attenuated version. The default of 15 has shown to be a good compromise in both simulations and real flight.
|
|
// @Range: 0 100
|
|
// @User: Advanced
|
|
AP_GROUPINFO("ATT_REF", 10, AP_GyroFFT, _attenuation_power_db, 15),
|
|
|
|
// @Param: BW_HOVER
|
|
// @DisplayName: FFT learned bandwidth at hover
|
|
// @Description: FFT learned bandwidth at hover for the attenuation frequencies.
|
|
// @Range: 0 200
|
|
// @User: Advanced
|
|
AP_GROUPINFO("BW_HOVER", 11, AP_GyroFFT, _bandwidth_hover_hz, 20),
|
|
|
|
AP_GROUPEND
|
|
};
|
|
|
|
// The FFT splits the frequency domain into an number of bins
|
|
// A sampling frequency of 1000 and max frequency (Nyquist) of 500 at a window size of 32 gives 16 frequency bins each 31.25Hz wide
|
|
// The first bin is used to store the DC and Nyquist values combined.
|
|
// Eg [DC/Nyquist], [16,47), [47,78), [78,109) etc
|
|
// For a loop rate of 800Hz, 16 bins each 25Hz wide
|
|
// Eg X[0]=[DC/Nyquist], X[1]=[12,37), X[2]=[37,62), X[3]=[62,87), X[4]=[87,112)
|
|
// So middle frequency is X[n] * 25 and the range is X[n] * 25 - 12 < f < X[n] * 25 + 12
|
|
|
|
// Maximum tolerated number of cycles with missing signal
|
|
#define FFT_MAX_MISSED_UPDATES 3
|
|
|
|
const extern AP_HAL::HAL& hal;
|
|
|
|
AP_GyroFFT::AP_GyroFFT()
|
|
{
|
|
_thread_state._noise_needs_calibration = 0x07; // all axes need calibration
|
|
AP_Param::setup_object_defaults(this, var_info);
|
|
|
|
if (_singleton != nullptr) {
|
|
AP_HAL::panic("AP_GyroFFT must be singleton");
|
|
}
|
|
_singleton = this;
|
|
}
|
|
|
|
// initialize the FFT parameters and engine
|
|
void AP_GyroFFT::init(uint32_t target_looptime_us)
|
|
{
|
|
// if FFT analysis is not enabled we don't want to allocate any of the associated resources
|
|
if (!_enable) {
|
|
return;
|
|
}
|
|
|
|
_ins = &AP::ins();
|
|
|
|
// sanity check
|
|
if (_ins->get_raw_gyro_rate_hz() == 0) {
|
|
AP_HAL::panic("AP_GyroFFT must be initialized after AP_InertialSensor");
|
|
}
|
|
|
|
// check that we support the window size requested and it is a power of 2
|
|
_window_size = 1 << lrintf(log2f(_window_size.get()));
|
|
#if defined(STM32H7) || CONFIG_HAL_BOARD == HAL_BOARD_LINUX || CONFIG_HAL_BOARD == HAL_BOARD_SITL
|
|
_window_size = constrain_int16(_window_size, 32, 1024);
|
|
#else
|
|
_window_size = constrain_int16(_window_size, 32, 256);
|
|
#endif
|
|
|
|
// check that we have enough memory for the window size requested
|
|
// INS: XYZ_AXIS_COUNT * INS_MAX_INSTANCES * _window_size, DSP: 3 * _window_size, FFT: XYZ_AXIS_COUNT + 3 * _window_size
|
|
const uint32_t allocation_count = (XYZ_AXIS_COUNT * INS_MAX_INSTANCES + 3 + XYZ_AXIS_COUNT + 3) * sizeof(float);
|
|
if (allocation_count * FFT_DEFAULT_WINDOW_SIZE > hal.util->available_memory() / 2) {
|
|
gcs().send_text(MAV_SEVERITY_WARNING, "AP_GyroFFT: disabled, required %u bytes", (unsigned int)allocation_count * FFT_DEFAULT_WINDOW_SIZE);
|
|
return;
|
|
} else if (allocation_count * _window_size > hal.util->available_memory() / 2) {
|
|
gcs().send_text(MAV_SEVERITY_WARNING, "AP_GyroFFT: req alloc %u bytes (free=%u)", (unsigned int)allocation_count * _window_size, (unsigned int)hal.util->available_memory());
|
|
_window_size = FFT_DEFAULT_WINDOW_SIZE;
|
|
}
|
|
// save any changes that were made
|
|
_window_size.save();
|
|
|
|
// determine the FFT sample rate based on the gyro rate, loop rate and configuration
|
|
if (_sample_mode == 0) {
|
|
_fft_sampling_rate_hz = _ins->get_raw_gyro_rate_hz();
|
|
} else {
|
|
const uint16_t loop_rate_hz = 1000*1000UL / target_looptime_us;
|
|
_fft_sampling_rate_hz = loop_rate_hz / _sample_mode;
|
|
for (uint8_t axis = 0; axis < XYZ_AXIS_COUNT; axis++) {
|
|
_downsampled_gyro_data[axis] = (float*)hal.util->malloc_type(sizeof(float) * _window_size, DSP_MEM_REGION);
|
|
if (_downsampled_gyro_data[axis] == nullptr) {
|
|
gcs().send_text(MAV_SEVERITY_WARNING, "Failed to allocate window for AP_GyroFFT");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
_current_sample_mode = _sample_mode;
|
|
|
|
_ref_energy = new Vector3f[_window_size];
|
|
if (_ref_energy == nullptr) {
|
|
gcs().send_text(MAV_SEVERITY_WARNING, "Failed to allocate window for AP_GyroFFT");
|
|
return;
|
|
}
|
|
|
|
// make the gyro window match the window size plus the maximum that can be in play from the backend
|
|
if (!_ins->set_gyro_window_size(_window_size + INS_MAX_GYRO_WINDOW_SAMPLES)) {
|
|
return;
|
|
}
|
|
// current read marker is the beginning of the window
|
|
if (_sample_mode == 0) {
|
|
_circular_buffer_idx = _ins->get_raw_gyro_window_index();
|
|
} else {
|
|
_circular_buffer_idx = 0;
|
|
}
|
|
|
|
// initialise the HAL DSP subsystem
|
|
_state = hal.dsp->fft_init(_window_size, _fft_sampling_rate_hz);
|
|
if (_state == nullptr) {
|
|
gcs().send_text(MAV_SEVERITY_WARNING, "Failed to initialize DSP engine");
|
|
return;
|
|
}
|
|
|
|
// number of samples needed before a new frame can be processed
|
|
_window_overlap = constrain_float(_window_overlap, 0.0f, 0.9f);
|
|
_window_overlap.save();
|
|
_samples_per_frame = (1.0f - _window_overlap) * _window_size;
|
|
_samples_per_frame = 1 << lrintf(log2f(_samples_per_frame));
|
|
|
|
// The update rate for the output
|
|
const float output_rate = _fft_sampling_rate_hz / _samples_per_frame;
|
|
// establish suitable defaults for the detected values
|
|
for (uint8_t axis = 0; axis < XYZ_AXIS_COUNT; axis++) {
|
|
_thread_state._center_freq_hz[axis] = _fft_min_hz;
|
|
_thread_state._center_freq_hz_filtered[axis] = _fft_min_hz;
|
|
// calculate low-pass filter characteristics based on overlap size
|
|
_center_freq_filter[axis].set_cutoff_frequency(output_rate, output_rate * 0.48f);
|
|
// smooth the bandwidth output more aggressively
|
|
_center_bandwidth_filter[axis].set_cutoff_frequency(output_rate, output_rate * 0.25f);
|
|
// number of cycles to average over, two complete windows to be sure
|
|
_noise_calibration_cycles[axis] = (_window_size / _samples_per_frame) * 2;
|
|
}
|
|
|
|
// the number of cycles required to have a proper noise reference
|
|
_noise_cycles = (_window_size / _samples_per_frame) * XYZ_AXIS_COUNT;
|
|
// calculate harmonic multiplier
|
|
uint8_t first_harmonic = 0;
|
|
_harmonics = 1; // always search for 1
|
|
for (uint8_t i = 0; i < HNF_MAX_HARMONICS; i++) {
|
|
if (_ins->get_gyro_harmonic_notch_harmonics() & (1<<i)) {
|
|
if (first_harmonic == 0) {
|
|
first_harmonic = i + 1;
|
|
} else {
|
|
_harmonics++;
|
|
_harmonic_multiplier = float(i + 1) / first_harmonic;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// finally we are done
|
|
_initialized = true;
|
|
update_parameters();
|
|
// start running FFTs
|
|
start_update_thread();
|
|
}
|
|
|
|
// sample the gyros either by using a gyro window sampled at the gyro rate or making invdividual samples
|
|
// called from fast_loop thread - anything that requires atomic access to IMU data needs to be done here
|
|
void AP_GyroFFT::sample_gyros()
|
|
{
|
|
if (!analysis_enabled()) {
|
|
return;
|
|
}
|
|
|
|
WITH_SEMAPHORE(_sem);
|
|
|
|
// update counters for gyro window
|
|
if (_current_sample_mode == 0) {
|
|
// number of available samples are those in the IMU buffer less those we have already consumed
|
|
_sample_count = ((_ins->get_raw_gyro_window_index() - _circular_buffer_idx + get_buffer_size()) % get_buffer_size());
|
|
|
|
if (start_analysis()) {
|
|
hal.dsp->fft_start(_state, _ins->get_raw_gyro_window(_update_axis), _circular_buffer_idx, get_buffer_size());
|
|
// we have pushed a frame into the FFT loop, move the index to the beginning of the next frame
|
|
_circular_buffer_idx = (_circular_buffer_idx + _samples_per_frame) % get_buffer_size();
|
|
_sample_count -= _samples_per_frame;
|
|
}
|
|
}
|
|
// for loop rate sampling accumulate and average gyro samples
|
|
else {
|
|
_oversampled_gyro_accum += _ins->get_raw_gyro();
|
|
_oversampled_gyro_count++;
|
|
|
|
if ((_oversampled_gyro_count % _current_sample_mode) == 0) {
|
|
// calculate mean value of accumulated samples
|
|
Vector3f sample = _oversampled_gyro_accum / _current_sample_mode;
|
|
// fast sampling means that the raw gyro values have already been averaged over 8 samples
|
|
_downsampled_gyro_data[0][_downsampled_gyro_idx] = sample.x;
|
|
_downsampled_gyro_data[1][_downsampled_gyro_idx] = sample.y;
|
|
_downsampled_gyro_data[2][_downsampled_gyro_idx] = sample.z;
|
|
|
|
_oversampled_gyro_accum.zero();
|
|
_oversampled_gyro_count = 0;
|
|
_downsampled_gyro_idx = (_downsampled_gyro_idx + 1) % _state->_window_size;
|
|
_sample_count++;
|
|
|
|
if (start_analysis()) {
|
|
hal.dsp->fft_start(_state, _downsampled_gyro_data[_update_axis], _circular_buffer_idx, _state->_window_size);
|
|
_circular_buffer_idx = (_circular_buffer_idx + _samples_per_frame) % _state->_window_size;
|
|
_sample_count -= _samples_per_frame;
|
|
}
|
|
}
|
|
}
|
|
|
|
_global_state = _thread_state;
|
|
}
|
|
|
|
// analyse gyro data using FFT, returns number of samples still held
|
|
// called from FFT thread
|
|
uint16_t AP_GyroFFT::update()
|
|
{
|
|
if (!analysis_enabled()) {
|
|
return 0;
|
|
}
|
|
|
|
if (!_sem.take(HAL_SEMAPHORE_BLOCK_FOREVER)) {
|
|
return 0;
|
|
}
|
|
|
|
// only proceeed if a full frame has been pushed into the dsp
|
|
if (!_thread_state._analysis_started) {
|
|
uint16_t new_sample_count = _sample_count;
|
|
_sem.give();
|
|
return new_sample_count;
|
|
}
|
|
|
|
uint16_t start_bin = _config._fft_start_bin;
|
|
uint16_t end_bin = _config._fft_end_bin;
|
|
|
|
_sem.give();
|
|
|
|
// calculate FFT and update filters outside the semaphore
|
|
uint16_t bin_max = hal.dsp->fft_analyse(_state, start_bin, end_bin, _harmonics, _config._attenuation_cutoff);
|
|
|
|
// in order to access _config state we need the semaphore again
|
|
WITH_SEMAPHORE(_sem);
|
|
|
|
// something has been detected, update the peak frequency and associated metrics
|
|
update_ref_energy(bin_max);
|
|
calculate_noise(bin_max);
|
|
|
|
// ready to receive another frame
|
|
_thread_state._analysis_started = false;
|
|
|
|
return _sample_count;
|
|
}
|
|
|
|
// whether analysis can be run again or not
|
|
bool AP_GyroFFT::start_analysis() {
|
|
if (_thread_state._analysis_started) {
|
|
return false;
|
|
}
|
|
if (_sample_count >= _state->_window_size) {
|
|
_thread_state._analysis_started = true;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// update calculated values of dynamic parameters - runs at 1Hz
|
|
void AP_GyroFFT::update_parameters()
|
|
{
|
|
if (!_initialized) {
|
|
return;
|
|
}
|
|
|
|
WITH_SEMAPHORE(_sem);
|
|
|
|
// don't allow MAXHZ to go to Nyquist
|
|
_fft_max_hz = MIN(_fft_max_hz, _fft_sampling_rate_hz * 0.48);
|
|
_config._snr_threshold_db = _snr_threshold_db;
|
|
_config._fft_min_hz = _fft_min_hz;
|
|
_config._fft_max_hz = _fft_max_hz;
|
|
// determine the start FFT bin for all frequency detection
|
|
_config._fft_start_bin = MAX(floorf(_fft_min_hz.get() / _state->_bin_resolution), 1);
|
|
// determine the endt FFT bin for all frequency detection
|
|
_config._fft_end_bin = MIN(ceilf(_fft_max_hz.get() / _state->_bin_resolution), _state->_bin_count);
|
|
// actual attenuation from the db value
|
|
_config._attenuation_cutoff = powf(10.0f, -_attenuation_power_db / 10.0f);
|
|
_config._analysis_enabled = _analysis_enabled;
|
|
}
|
|
|
|
// thread for processing gyro data via FFT
|
|
void AP_GyroFFT::update_thread(void)
|
|
{
|
|
while (true) {
|
|
uint16_t remaining_samples = update();
|
|
// wait approximately until we are likely to have another frame ready
|
|
uint32_t delay = constrain_int32((int32_t(_window_size) - remaining_samples) * 1e6 / _fft_sampling_rate_hz, 0, 100000);
|
|
if (delay > 0) {
|
|
hal.scheduler->delay_microseconds(delay);
|
|
}
|
|
}
|
|
}
|
|
|
|
// start the update thread
|
|
void AP_GyroFFT::start_update_thread(void)
|
|
{
|
|
WITH_SEMAPHORE(_sem);
|
|
|
|
if (_thread_created) {
|
|
return;
|
|
}
|
|
|
|
if (!hal.scheduler->thread_create(FUNCTOR_BIND_MEMBER(&AP_GyroFFT::update_thread, void), "apm_fft", FFT_STACK_SIZE, AP_HAL::Scheduler::PRIORITY_IO, 1)) {
|
|
AP_HAL::panic("Failed to start AP_GyroFFT update thread");
|
|
}
|
|
|
|
_thread_created = true;
|
|
}
|
|
|
|
// self-test the FFT analyser - can only be done while samples are not being taken
|
|
// called from main thread
|
|
bool AP_GyroFFT::calibration_check()
|
|
{
|
|
if (!analysis_enabled()) {
|
|
return true;
|
|
}
|
|
|
|
// analysis is started in the main thread, don't trample on in-flight analysis
|
|
if (_global_state._analysis_started) {
|
|
return true;
|
|
}
|
|
|
|
// still calibrating noise so not ready
|
|
if (_global_state._noise_needs_calibration) {
|
|
return false;
|
|
}
|
|
|
|
// make sure the frequency maxium is below Nyquist
|
|
if (_fft_max_hz > _fft_sampling_rate_hz * 0.5f) {
|
|
gcs().send_text(MAV_SEVERITY_CRITICAL, "FFT: config MAXHZ %dHz > %dHz", _fft_max_hz.get(), _fft_sampling_rate_hz / 2);
|
|
return false;
|
|
}
|
|
|
|
// check for sane frequency resolution - for 1k backends with length 32 this will be 32Hz
|
|
if (_state->_bin_resolution > 50.0f) {
|
|
gcs().send_text(MAV_SEVERITY_WARNING, "FFT: resolution is %.1fHz, increase length", _state->_bin_resolution);
|
|
return true; // a low resolution is not fatal
|
|
}
|
|
|
|
// larger windows make the the self-test run too long, triggering the watchdog
|
|
if (AP_Logger::get_singleton()->log_while_disarmed()
|
|
|| _window_size > FFT_DEFAULT_WINDOW_SIZE * 2) {
|
|
return true;
|
|
}
|
|
float max_divergence = self_test_bin_frequencies();
|
|
|
|
if (max_divergence > _state->_bin_resolution * 0.5f) {
|
|
gcs().send_text(MAV_SEVERITY_WARNING, "FFT: self-test FAILED, max error %fHz", max_divergence);
|
|
}
|
|
return max_divergence <= _state->_bin_resolution * 0.5f;
|
|
}
|
|
|
|
// update the hover frequency input filter. should be called at 100hz when in a stable hover
|
|
// called from main thread
|
|
void AP_GyroFFT::update_freq_hover(float dt, float throttle_out)
|
|
{
|
|
if (!analysis_enabled()) {
|
|
return;
|
|
}
|
|
// we have chosen to constrain the hover frequency to be within the range reachable by the third order expo polynomial.
|
|
_freq_hover_hz = constrain_float(_freq_hover_hz + (dt / (10.0f + dt)) * (get_weighted_noise_center_freq_hz() - _freq_hover_hz), _fft_min_hz, _fft_max_hz);
|
|
_bandwidth_hover_hz = constrain_float(_bandwidth_hover_hz + (dt / (10.0f + dt)) * (get_weighted_noise_center_bandwidth_hz() - _bandwidth_hover_hz), 0, _fft_max_hz * 0.5f);
|
|
_throttle_ref = constrain_float(_throttle_ref + (dt / (10.0f + dt)) * (throttle_out * sq((float)_fft_min_hz.get() / _freq_hover_hz.get()) - _throttle_ref), 0.01f, 0.9f);
|
|
}
|
|
|
|
// save parameters as part of disarming
|
|
// called from main thread
|
|
void AP_GyroFFT::save_params_on_disarm()
|
|
{
|
|
if (!analysis_enabled()) {
|
|
return;
|
|
}
|
|
|
|
_freq_hover_hz.save();
|
|
_bandwidth_hover_hz.save();
|
|
_throttle_ref.save();
|
|
}
|
|
|
|
// return an average center frequency weighted by bin energy
|
|
// called from main thread
|
|
float AP_GyroFFT::get_weighted_noise_center_freq_hz()
|
|
{
|
|
if (!analysis_enabled()) {
|
|
return _fft_min_hz;
|
|
}
|
|
|
|
// there is generally a lot of high-energy, slightly lower frequency noise on yaw, however this
|
|
// appears to be a second-order effect as only targetting pitch and roll (x & y) produces much cleaner output all round
|
|
if (!_global_state._center_freq_energy.is_nan()
|
|
&& !is_zero(_global_state._center_freq_energy.x)
|
|
&& !is_zero(_global_state._center_freq_energy.y)) {
|
|
return (_global_state._center_freq_hz_filtered.x * _global_state._center_freq_energy.x
|
|
+ _global_state._center_freq_hz_filtered.y * _global_state._center_freq_energy.y)
|
|
/ (_global_state._center_freq_energy.x + _global_state._center_freq_energy.y);
|
|
}
|
|
else {
|
|
return (_global_state._center_freq_hz_filtered.x + _global_state._center_freq_hz_filtered.y) * 0.5f;
|
|
}
|
|
}
|
|
|
|
// log gyro fft messages
|
|
void AP_GyroFFT::write_log_messages()
|
|
{
|
|
if (!analysis_enabled()) {
|
|
return;
|
|
}
|
|
|
|
AP::logger().Write(
|
|
"FTN1",
|
|
"TimeUS,PkAvg,PkX,PkY,PkZ,BwAvg,BwX,BwY,BwZ,DnF",
|
|
"szzzzzzzzz",
|
|
"F---------",
|
|
"Qfffffffff",
|
|
AP_HAL::micros64(),
|
|
get_weighted_noise_center_freq_hz(),
|
|
get_noise_center_freq_hz().x,
|
|
get_noise_center_freq_hz().y,
|
|
get_noise_center_freq_hz().z,
|
|
get_weighted_noise_center_bandwidth_hz(),
|
|
get_noise_center_bandwidth_hz().x,
|
|
get_noise_center_bandwidth_hz().y,
|
|
get_noise_center_bandwidth_hz().z,
|
|
_ins->get_gyro_dynamic_notch_center_freq_hz());
|
|
|
|
AP::logger().Write(
|
|
"FTN2",
|
|
"TimeUS,FtX,FtY,FtZ,EnX,EnY,EnZ,SnX,SnY,SnZ,Bin",
|
|
"s%%%-------",
|
|
"F----------",
|
|
"QfffffffffB",
|
|
AP_HAL::micros64(),
|
|
get_raw_noise_harmonic_fit().x,
|
|
get_raw_noise_harmonic_fit().y,
|
|
get_raw_noise_harmonic_fit().z,
|
|
get_center_freq_energy().x,
|
|
get_center_freq_energy().y,
|
|
get_center_freq_energy().z,
|
|
get_noise_signal_to_noise_db().x,
|
|
get_noise_signal_to_noise_db().y,
|
|
get_noise_signal_to_noise_db().z,
|
|
get_center_freq_bin().z);
|
|
|
|
#if DEBUG_FFT
|
|
const uint32_t now = AP_HAL::millis();
|
|
// output at 1hz
|
|
if ((now - _last_output_ms) > 1000) {
|
|
// doing this from the update thread overflows the stack
|
|
WITH_SEMAPHORE(_sem);
|
|
gcs().send_text(MAV_SEVERITY_WARNING, "FFT: f:%.1f, fr:%.1f, b:%u, fd:%.1f",
|
|
_debug_state._center_freq_hz_filtered[_update_axis], _debug_state._center_freq_hz[_update_axis], _debug_max_bin, _debug_max_bin_freq);
|
|
gcs().send_text(MAV_SEVERITY_WARNING, "FFT: bw:%.1f, e:%.1f, r:%.1f, snr:%.1f",
|
|
_debug_state._center_bandwidth_hz[_update_axis], _debug_max_freq_bin, _ref_energy[_update_axis][_debug_max_bin], _debug_snr);
|
|
_last_output_ms = now;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// return an average noise bandwidth weighted by bin energy
|
|
// called from main thread
|
|
float AP_GyroFFT::get_weighted_noise_center_bandwidth_hz()
|
|
{
|
|
if (!analysis_enabled()) {
|
|
return 0.0f;
|
|
}
|
|
|
|
if (!_global_state._center_freq_energy.is_nan()
|
|
&& !is_zero(_global_state._center_freq_energy.x)
|
|
&& !is_zero(_global_state._center_freq_energy.y)) {
|
|
return (_global_state._center_bandwidth_hz.x * _global_state._center_freq_energy.x
|
|
+ _global_state._center_bandwidth_hz.y * _global_state._center_freq_energy.y)
|
|
/ (_global_state._center_freq_energy.x + _global_state._center_freq_energy.y);
|
|
}
|
|
else {
|
|
return (_global_state._center_bandwidth_hz.x + _global_state._center_bandwidth_hz.y) * 0.5f;
|
|
}
|
|
}
|
|
|
|
// calculate noise frequencies from FFT data provided by the HAL subsystem
|
|
// called from FFT thread
|
|
void AP_GyroFFT::calculate_noise(uint16_t max_bin)
|
|
{
|
|
_thread_state._center_freq_bin[_update_axis] = max_bin;
|
|
|
|
float weighted_center_freq_hz = 0;
|
|
|
|
// cacluate the SNR and center frequency energy
|
|
const float max_energy = MAX(1.0f, _state->_freq_bins[max_bin]);
|
|
const float ref_energy = MAX(1.0f, _ref_energy[_update_axis][max_bin]);
|
|
float snr = 10.f * log10f(max_energy) - log10f(ref_energy);
|
|
// if the bin energy is above the noise threshold then we have a signal
|
|
if (!_thread_state._noise_needs_calibration && !isnan(_state->_freq_bins[max_bin]) && snr > _config._snr_threshold_db) {
|
|
// if targetting more than one harmonic then make sure we get the fundamental
|
|
// on larger copters the second harmonic often has more energy
|
|
const float peak_freq_hz = constrain_float(_state->_max_bin_freq, (float)_config._fft_min_hz, (float)_config._fft_max_hz);
|
|
const float second_peak_freq_hz = constrain_float(_state->_second_bin_freq, (float)_config._fft_min_hz, (float)_config._fft_max_hz);
|
|
const float harmonic_fit = 100.0f * fabsf(peak_freq_hz - second_peak_freq_hz * _harmonic_multiplier) / peak_freq_hz;
|
|
|
|
// required fit of 10% is fairly conservative when testing in SITL
|
|
if (_harmonics > 1 && peak_freq_hz > second_peak_freq_hz && harmonic_fit < FFT_REQUIRED_HARMONIC_FIT) {
|
|
weighted_center_freq_hz = second_peak_freq_hz;
|
|
_thread_state._center_freq_energy[_update_axis] = _state->_freq_bins[_state->_second_energy_bin];
|
|
_thread_state._center_bandwidth_hz[_update_axis] = _center_bandwidth_filter[_update_axis].apply(_state->_second_noise_width_hz);
|
|
} else {
|
|
weighted_center_freq_hz = peak_freq_hz;
|
|
_thread_state._center_freq_energy[_update_axis] = _state->_freq_bins[max_bin];
|
|
_thread_state._center_bandwidth_hz[_update_axis] = _center_bandwidth_filter[_update_axis].apply(_state->_max_noise_width_hz);
|
|
}
|
|
// record how good the fit was
|
|
if (peak_freq_hz > second_peak_freq_hz) {
|
|
_thread_state._harmonic_fit[_update_axis] = harmonic_fit;
|
|
} else {
|
|
_thread_state._harmonic_fit[_update_axis] = 0.0f;
|
|
}
|
|
_thread_state._center_freq_hz[_update_axis] = weighted_center_freq_hz;
|
|
_thread_state._center_snr[_update_axis] = snr;
|
|
_missed_cycles = 0;
|
|
}
|
|
// if we failed to find a signal, carry on using the previous reading
|
|
else if (_missed_cycles++ < FFT_MAX_MISSED_UPDATES) {
|
|
weighted_center_freq_hz = _thread_state._center_freq_hz[_update_axis];
|
|
// Keep the previous center frequency and energy
|
|
}
|
|
// we failed to find a signal for more than FFT_MAX_MISSED_UPDATES cycles
|
|
else {
|
|
weighted_center_freq_hz = _config._fft_min_hz;
|
|
_thread_state._center_freq_hz[_update_axis] = _config._fft_min_hz;
|
|
_thread_state._center_freq_energy[_update_axis] = 0.0f;
|
|
_thread_state._center_snr[_update_axis] = 0.0f;
|
|
_thread_state._center_bandwidth_hz[_update_axis] = _center_bandwidth_filter[_update_axis].apply(_bandwidth_hover_hz);
|
|
}
|
|
|
|
_thread_state._center_freq_hz_filtered[_update_axis] = _center_freq_filter[_update_axis].apply(weighted_center_freq_hz);
|
|
|
|
#if DEBUG_FFT
|
|
WITH_SEMAPHORE(_sem);
|
|
_debug_state = _thread_state;
|
|
_debug_max_freq_bin = _state->_freq_bins[max_bin];
|
|
_debug_max_bin_freq = _state->_max_bin_freq;
|
|
_debug_snr = snr;
|
|
_debug_max_bin = max_bin;
|
|
#endif
|
|
_update_axis = (_update_axis + 1) % XYZ_AXIS_COUNT;
|
|
}
|
|
|
|
// calculate noise baseline from FFT data provided by the HAL subsystem
|
|
// called from FFT thread
|
|
void AP_GyroFFT::update_ref_energy(uint16_t max_bin)
|
|
{
|
|
if (!_thread_state._noise_needs_calibration) {
|
|
return;
|
|
}
|
|
// according to https://www.tcd.ie/Physics/research/groups/magnetism/files/lectures/py5021/MagneticSensors3.pdf sensor noise is not necessarily gaussian
|
|
// determine a PS noise reference at each of the possble center frequencies
|
|
if (_noise_cycles == 0 && _noise_calibration_cycles[_update_axis] > 0) {
|
|
for (uint16_t i = 1; i < _state->_bin_count; i++) {
|
|
_ref_energy[_update_axis][i] += _state->_freq_bins[i];
|
|
}
|
|
if (--_noise_calibration_cycles[_update_axis] == 0) {
|
|
for (uint16_t i = 1; i < _state->_bin_count; i++) {
|
|
const float cycles = (_window_size / _samples_per_frame) * 2;
|
|
// overall random noise is reduced by sqrt(N) when averaging periodigrams so adjust for that
|
|
_ref_energy[_update_axis][i] = (_ref_energy[_update_axis][i] / cycles) * sqrtf(cycles);
|
|
}
|
|
_thread_state._noise_needs_calibration &= ~(1 << _update_axis);
|
|
}
|
|
}
|
|
else if (_noise_cycles > 0) {
|
|
_noise_cycles--;
|
|
}
|
|
}
|
|
|
|
// perform FFT analysis on the range of frequencies supported by the analyser
|
|
// called from main thread
|
|
float AP_GyroFFT::self_test_bin_frequencies()
|
|
{
|
|
if (_state->_window_size * sizeof(float) > hal.util->available_memory() / 2) {
|
|
gcs().send_text(MAV_SEVERITY_WARNING, "FFT: unable to run self-test, required %u bytes", (unsigned int)(_state->_window_size * sizeof(float)));
|
|
return 0.0f;
|
|
}
|
|
|
|
GyroWindow test_window = (float*)hal.util->malloc_type(sizeof(float) * _state->_window_size, DSP_MEM_REGION);
|
|
// in the unlikely event we can't allocate a test window, skip the checks
|
|
if (test_window == nullptr) {
|
|
return 0.0f;
|
|
}
|
|
|
|
float max_divergence = 0;
|
|
|
|
for (uint16_t bin = _config._fft_start_bin; bin <= _config._fft_end_bin; bin++) {
|
|
// the algorithm will only ever return values in this range
|
|
float frequency = constrain_float(bin * _state->_bin_resolution, _fft_min_hz, _fft_max_hz);
|
|
max_divergence = MAX(max_divergence, self_test(frequency, test_window)); // test bin centers
|
|
frequency = constrain_float(bin * _state->_bin_resolution - _state->_bin_resolution / 4, _fft_min_hz, _fft_max_hz);
|
|
max_divergence = MAX(max_divergence, self_test(frequency, test_window)); // test bin off-centers
|
|
}
|
|
|
|
hal.util->free_type(test_window, sizeof(float) * _window_size, DSP_MEM_REGION);
|
|
return max_divergence;
|
|
}
|
|
|
|
// perform FFT analysis of a single sine wave at the selected frequency
|
|
// called from main thread
|
|
float AP_GyroFFT::self_test(float frequency, GyroWindow test_window)
|
|
{
|
|
for(uint16_t i = 0; i < _state->_window_size; i++) {
|
|
test_window[i]= sinf(2.0f * M_PI * frequency * i / _fft_sampling_rate_hz) * ToRad(20) * 2000;
|
|
}
|
|
|
|
_update_axis = 0;
|
|
|
|
hal.dsp->fft_start(_state, test_window, 0, _state->_window_size);
|
|
uint16_t max_bin = hal.dsp->fft_analyse(_state, _config._fft_start_bin, _config._fft_end_bin, _harmonics, _config._attenuation_cutoff);
|
|
|
|
if (max_bin <= 0) {
|
|
gcs().send_text(MAV_SEVERITY_WARNING, "FFT: self-test failed, failed to find frequency %f", frequency);
|
|
}
|
|
|
|
calculate_noise(max_bin);
|
|
|
|
float max_divergence = 0;
|
|
// make sure the selected frequencies are in the right bin
|
|
max_divergence = MAX(max_divergence, fabsf(frequency - _thread_state._center_freq_hz[0]));
|
|
if (_thread_state._center_freq_hz[0] < (frequency - _state->_bin_resolution * 0.5f) || _thread_state._center_freq_hz[0] > (frequency + _state->_bin_resolution * 0.5f)) {
|
|
gcs().send_text(MAV_SEVERITY_WARNING, "FFT: self-test failed: wanted %f, had %f", frequency, _thread_state._center_freq_hz[0]);
|
|
}
|
|
#if DEBUG_FFT
|
|
else {
|
|
gcs().send_text(MAV_SEVERITY_INFO, "FFT: self-test succeeded: wanted %f, had %f", frequency, _thread_state._center_freq_hz[0]);
|
|
}
|
|
#endif
|
|
|
|
return max_divergence;
|
|
}
|
|
|
|
// singleton instance
|
|
AP_GyroFFT *AP_GyroFFT::_singleton;
|
|
|
|
namespace AP {
|
|
|
|
AP_GyroFFT *fft()
|
|
{
|
|
return AP_GyroFFT::get_singleton();
|
|
}
|
|
|
|
}
|
|
|
|
#endif // HAL_GYROFFT_ENABLED
|