Line data Source code
1 : /*
2 : This program is free software: you can redistribute it and/or modify
3 : it under the terms of the GNU General Public License as published by
4 : the Free Software Foundation, either version 3 of the License, or
5 : (at your option) any later version.
6 :
7 : This program is distributed in the hope that it will be useful,
8 : but WITHOUT ANY WARRANTY; without even the implied warranty of
9 : MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 : GNU General Public License for more details.
11 :
12 : You should have received a copy of the GNU General Public License
13 : along with this program. If not, see <http://www.gnu.org/licenses/>.
14 :
15 : Code by Andy Piper with help from betaflight
16 : */
17 :
18 : #include "AP_GyroFFT.h"
19 :
20 : #if HAL_GYROFFT_ENABLED
21 :
22 : #include <GCS_MAVLink/GCS.h>
23 : #include <AP_Logger/AP_Logger.h>
24 : #include <Filter/HarmonicNotchFilter.h>
25 : #include <AP_BoardConfig/AP_BoardConfig.h>
26 : #include <AP_Arming/AP_Arming.h>
27 : #include <AP_Vehicle/AP_Vehicle.h>
28 : #if APM_BUILD_COPTER_OR_HELI || APM_BUILD_TYPE(APM_BUILD_ArduPlane)
29 : #include <AP_Motors/AP_Motors.h>
30 : #endif
31 : #include <stdio.h>
32 :
33 : extern const AP_HAL::HAL& hal;
34 :
35 : #ifndef FFT_DEFAULT_WINDOW_SIZE
36 : // the H7 can cope with a longer length and these boards generally have BMI088 which needs a longer length
37 : #if defined(STM32H7)
38 : #define FFT_DEFAULT_WINDOW_SIZE 64
39 : #else
40 : #define FFT_DEFAULT_WINDOW_SIZE 32
41 : #endif
42 : #endif
43 : #ifndef FFT_DEFAULT_WINDOW_OVERLAP
44 : #if defined(STM32H7)
45 : #define FFT_DEFAULT_WINDOW_OVERLAP 0.75f
46 : #else
47 : #define FFT_DEFAULT_WINDOW_OVERLAP 0.5f
48 : #endif
49 : #endif
50 : #define FFT_THR_REF_DEFAULT 0.35f // the estimated throttle reference, 0 ~ 1
51 : #define FFT_SNR_DEFAULT 25.0f // a higher SNR is safer and this works quite well on a Pixracer
52 : #define FFT_SNR_PFILT_DEFAULT 10.0f // post-filter there is much less noise so default should be lower
53 : #define FFT_STACK_SIZE 1024
54 : #define FFT_MIN_SAMPLES_PER_FRAME 16
55 : #define FFT_HARMONIC_FIT_DEFAULT 10
56 : #define FFT_HARMONIC_FIT_FILTER_HZ 15.0f
57 : #define FFT_HARMONIC_FIT_MULT 50.0f
58 : #define FFT_HARMONIC_FIT_TRACK_ROLL 4
59 : #define FFT_HARMONIC_FIT_TRACK_PITCH 5
60 :
61 : // table of user settable parameters
62 : const AP_Param::GroupInfo AP_GyroFFT::var_info[] = {
63 :
64 : // @Param: ENABLE
65 : // @DisplayName: Enable
66 : // @Description: Enable Gyro FFT analyser
67 : // @Values: 0:Disabled,1:Enabled
68 : // @User: Advanced
69 : // @RebootRequired: True
70 : AP_GROUPINFO_FLAGS("ENABLE", 1, AP_GyroFFT, _enable, 0, AP_PARAM_FLAG_ENABLE),
71 :
72 : // @Param: MINHZ
73 : // @DisplayName: Minimum Frequency
74 : // @Description: Lower bound of FFT frequency detection in Hz. On larger vehicles the minimum motor frequency is likely to be significantly lower than for smaller vehicles.
75 : // @Range: 20 400
76 : // @Units: Hz
77 : // @User: Advanced
78 : AP_GROUPINFO("MINHZ", 2, AP_GyroFFT, _fft_min_hz, 50),
79 :
80 : // @Param: MAXHZ
81 : // @DisplayName: Maximum Frequency
82 : // @Description: Upper bound of FFT frequency detection in Hz. On smaller vehicles the maximum motor frequency is likely to be significantly higher than for larger vehicles.
83 : // @Range: 20 495
84 : // @Units: Hz
85 : // @User: Advanced
86 : AP_GROUPINFO("MAXHZ", 3, AP_GyroFFT, _fft_max_hz, 450),
87 :
88 : // @Param: SAMPLE_MODE
89 : // @DisplayName: Sample Mode
90 : // @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.
91 : // @Range: 0 4
92 : // @User: Advanced
93 : // @RebootRequired: True
94 : AP_GROUPINFO("SAMPLE_MODE", 4, AP_GyroFFT, _sample_mode, 0),
95 :
96 : // @Param: WINDOW_SIZE
97 : // @DisplayName: FFT window size
98 : // @Description: Size of window to be used in FFT calculations. Takes effect on reboot. Must be a power of 2 and between 32 and 512. 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.
99 : // @Range: 32 1024
100 : // @User: Advanced
101 : // @RebootRequired: True
102 : AP_GROUPINFO("WINDOW_SIZE", 5, AP_GyroFFT, _window_size, FFT_DEFAULT_WINDOW_SIZE),
103 :
104 : // @Param: WINDOW_OLAP
105 : // @DisplayName: FFT window overlap
106 : // @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.
107 : // @Range: 0 0.9
108 : // @User: Advanced
109 : // @RebootRequired: True
110 : AP_GROUPINFO("WINDOW_OLAP", 6, AP_GyroFFT, _window_overlap, FFT_DEFAULT_WINDOW_OVERLAP),
111 :
112 : // @Param: FREQ_HOVER
113 : // @DisplayName: FFT learned hover frequency
114 : // @Description: The learned hover noise frequency
115 : // @Range: 0 250
116 : // @User: Advanced
117 : AP_GROUPINFO("FREQ_HOVER", 7, AP_GyroFFT, _freq_hover_hz, 80.0f),
118 :
119 : // @Param: THR_REF
120 : // @DisplayName: FFT learned thrust reference
121 : // @Description: FFT learned thrust reference for the hover frequency and FFT minimum frequency.
122 : // @Range: 0.01 0.9
123 : // @User: Advanced
124 : AP_GROUPINFO("THR_REF", 8, AP_GyroFFT, _throttle_ref, FFT_THR_REF_DEFAULT),
125 :
126 : // @Param: SNR_REF
127 : // @DisplayName: FFT SNR reference threshold
128 : // @Description: FFT SNR reference threshold in dB at which a signal is determined to be present.
129 : // @Range: 0.0 100.0
130 : // @User: Advanced
131 : AP_GROUPINFO("SNR_REF", 9, AP_GyroFFT, _snr_threshold_db, FFT_SNR_DEFAULT),
132 :
133 : // @Param: ATT_REF
134 : // @DisplayName: FFT attenuation for bandwidth calculation
135 : // @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.
136 : // @Range: 0 100
137 : // @User: Advanced
138 : AP_GROUPINFO("ATT_REF", 10, AP_GyroFFT, _attenuation_power_db, 15),
139 :
140 : // @Param: BW_HOVER
141 : // @DisplayName: FFT learned bandwidth at hover
142 : // @Description: FFT learned bandwidth at hover for the attenuation frequencies.
143 : // @Range: 0 200
144 : // @User: Advanced
145 : AP_GROUPINFO("BW_HOVER", 11, AP_GyroFFT, _bandwidth_hover_hz, 20),
146 :
147 : // @Param: HMNC_FIT
148 : // @DisplayName: FFT harmonic fit frequency threshold
149 : // @Description: FFT harmonic fit frequency threshold percentage at which a signal of the appropriate frequency is determined to be the harmonic of another. Signals that have a harmonic relationship that varies at most by this percentage are considered harmonics of each other for the purpose of selecting the harmonic notch frequency. If a match is found then the lower frequency harmonic is always used as the basis for the dynamic harmonic notch. A value of zero completely disables harmonic matching.
150 : // @Range: 0 100
151 : // @User: Advanced
152 : // @RebootRequired: True
153 : AP_GROUPINFO("HMNC_FIT", 12, AP_GyroFFT, _harmonic_fit, FFT_HARMONIC_FIT_DEFAULT),
154 :
155 : // @Param: HMNC_PEAK
156 : // @DisplayName: FFT harmonic peak target
157 : // @Description: The FFT harmonic peak target that should be returned by FTN1.PkAvg. The resulting value will be used by the harmonic notch if configured to track the FFT frequency. By default the appropriate peak is auto-detected based on the harmonic fit between peaks and the energy-weighted average frequency on roll on pitch is used. Setting this to 1 will always target the highest energy peak. Setting this to 2 will target the highest energy peak that is lower in frequency than the highest energy peak. Setting this to 3 will target the highest energy peak that is higher in frequency than the highest energy peak. Setting this to 4 will target the highest energy peak on the roll axis only and only the roll frequency will be used (some vehicles have a much more pronounced peak on roll). Setting this to 5 will target the highest energy peak on the pitch axis only and only the pitch frequency will be used (some vehicles have a much more pronounced peak on roll).
158 : // @Values: 0:Auto,1:Center Frequency,2:Lower-Shoulder Frequency,3:Upper-Shoulder Frequency,4:Roll-Axis,5:Pitch-Axis
159 : // @User: Advanced
160 : AP_GROUPINFO("HMNC_PEAK", 13, AP_GyroFFT, _harmonic_peak, 0),
161 :
162 : // @Param: NUM_FRAMES
163 : // @DisplayName: FFT output frames to retain and average
164 : // @Description: Number of output frequency frames to retain and average in order to calculate final frequencies. Averaging output frames can drastically reduce noise and jitter at the cost of latency as long as the input is stable. The default is to perform no averaging. For rapidly changing frequencies (e.g. smaller aircraft) fewer frames should be averaged.
165 : // @Range: 0 8
166 : // @User: Advanced
167 : // @RebootRequired: True
168 : AP_GROUPINFO("NUM_FRAMES", 14, AP_GyroFFT, _num_frames, 0),
169 :
170 : // @Param: OPTIONS
171 : // @DisplayName: FFT options
172 : // @Description: FFT configuration options. Values: 1:Apply the FFT *after* the filter bank,2:Check noise at the motor frequencies using ESC data as a reference
173 : // @Bitmask: 0:Enable post-filter FFT,1:Check motor noise
174 : // @User: Advanced
175 : // @RebootRequired: True
176 : AP_GROUPINFO("OPTIONS", 15, AP_GyroFFT, _options, 0),
177 :
178 : AP_GROUPEND
179 : };
180 :
181 : // The FFT splits the frequency domain into an number of bins
182 : // 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
183 : // The first bin is used to store the DC and Nyquist values combined.
184 : // Eg [DC/Nyquist], [16,47), [47,78), [78,109) etc
185 : // For a loop rate of 800Hz, 16 bins each 25Hz wide
186 : // Eg X[0]=[DC/Nyquist], X[1]=[12,37), X[2]=[37,62), X[3]=[62,87), X[4]=[87,112)
187 : // So middle frequency is X[n] * 25 and the range is X[n] * 25 - 12 < f < X[n] * 25 + 12
188 :
189 : const extern AP_HAL::HAL& hal;
190 :
191 33536 : AP_GyroFFT::AP_GyroFFT()
192 : {
193 2096 : _thread_state._noise_needs_calibration = 0x07; // all axes need calibration
194 2096 : AP_Param::setup_object_defaults(this, var_info);
195 :
196 2096 : if (_singleton != nullptr) {
197 0 : AP_HAL::panic("AP_GyroFFT must be singleton");
198 : }
199 2096 : _singleton = this;
200 2096 : }
201 :
202 : // initialize the FFT parameters and engine
203 2086 : __INITFUNC__ void AP_GyroFFT::init(uint16_t loop_rate_hz)
204 : {
205 : // if FFT analysis is not enabled we don't want to allocate any of the associated resources
206 2086 : if (!_enable) {
207 2066 : return;
208 : }
209 :
210 20 : _ins = &AP::ins();
211 :
212 : // sanity check
213 20 : if (_ins->get_raw_gyro_rate_hz() == 0) {
214 0 : AP_HAL::panic("AP_GyroFFT must be initialized after AP_InertialSensor");
215 : }
216 :
217 : // check that we support the window size requested and it is a power of 2
218 20 : _window_size.set(1 << lrintf(log2f(_window_size.get())));
219 : #if defined(STM32H7) || CONFIG_HAL_BOARD == HAL_BOARD_LINUX || CONFIG_HAL_BOARD == HAL_BOARD_SITL
220 20 : _window_size.set(constrain_int16(_window_size, 32, 512));
221 : #else
222 : _window_size.set(constrain_int16(_window_size, 32, 256));
223 : #endif
224 : // number of samples needed before a new frame can be processed
225 20 : _window_overlap.set(constrain_float(_window_overlap, 0.0f, 0.9f));
226 20 : _samples_per_frame = (1.0f - _window_overlap) * _window_size;
227 : // if we allow too small a number of samples per frame the output rate gets very high
228 : // this is particularly a problem on IMUs with higher sample rates (e.g. BMI088)
229 : // 16 gives a maximum output rate of 2Khz / 16 = 125Hz per axis or 375Hz in aggregate
230 20 : _samples_per_frame = MAX(FFT_MIN_SAMPLES_PER_FRAME, 1 << lrintf(log2f(_samples_per_frame)));
231 20 : if (_num_frames > 0) {
232 4 : _num_frames.set(constrain_int16(_num_frames, 2, AP_HAL::DSP::MAX_SLIDING_WINDOW_SIZE));
233 : }
234 :
235 : // check that we have enough memory for the window size requested
236 : // INS: XYZ_AXIS_COUNT * INS_MAX_INSTANCES * _window_size, DSP: 3 * _window_size, FFT: XYZ_AXIS_COUNT + 3 * _window_size
237 20 : const uint32_t allocation_count = (XYZ_AXIS_COUNT * INS_MAX_INSTANCES + 3 + XYZ_AXIS_COUNT + 3 + _num_frames) * sizeof(float);
238 20 : if (allocation_count * FFT_DEFAULT_WINDOW_SIZE > hal.util->available_memory() / 2) {
239 0 : GCS_SEND_TEXT(MAV_SEVERITY_WARNING, "AP_GyroFFT: disabled, required %u bytes", (unsigned int)allocation_count * FFT_DEFAULT_WINDOW_SIZE);
240 0 : return;
241 20 : } else if (allocation_count * _window_size > hal.util->available_memory() / 2) {
242 0 : 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());
243 0 : _window_size.set(FFT_DEFAULT_WINDOW_SIZE);
244 : }
245 : // save any changes that were made
246 20 : _window_size.save();
247 :
248 : // determine the FFT sample rate based on the gyro rate, loop rate and configuration
249 20 : if (_sample_mode == 0) {
250 19 : _fft_sampling_rate_hz = _ins->get_raw_gyro_rate_hz();
251 : } else {
252 1 : _fft_sampling_rate_hz = loop_rate_hz / _sample_mode;
253 4 : for (uint8_t axis = 0; axis < XYZ_AXIS_COUNT; axis++) {
254 3 : if (!_downsampled_gyro_data[axis].set_size(_window_size + _samples_per_frame)) {
255 0 : GCS_SEND_TEXT(MAV_SEVERITY_WARNING, "Failed to allocate window for AP_GyroFFT");
256 0 : return;
257 : }
258 : }
259 : }
260 20 : _current_sample_mode = _sample_mode & 0x07; // mask matches previous 3 bit storage and param range
261 :
262 1492 : _ref_energy = NEW_NOTHROW Vector3f[_window_size];
263 20 : if (_ref_energy == nullptr) {
264 0 : GCS_SEND_TEXT(MAV_SEVERITY_WARNING, "Failed to allocate window for AP_GyroFFT");
265 0 : return;
266 : }
267 :
268 : // make the gyro window match the window size plus a buffer to cope with the backend
269 : // getting too far ahead.
270 20 : if (!_ins->set_gyro_window_size(_window_size + _samples_per_frame)) {
271 0 : return;
272 : }
273 :
274 : #if AP_INERTIALSENSOR_HARMONICNOTCH_ENABLED
275 : // check for harmonics across all harmonic notch filters
276 : // note that we only allow one harmonic notch filter linked to the FFT code
277 20 : uint32_t harmonics = 0;
278 20 : uint8_t num_notches = 0;
279 80 : for (auto ¬ch : _ins->harmonic_notches) {
280 60 : if (notch.params.enabled()) {
281 9 : harmonics |= notch.params.harmonics();
282 9 : num_notches = MAX(num_notches, notch.num_dynamic_notches);
283 : }
284 : }
285 20 : if (harmonics == 0) {
286 : // this allows use of FFT to find peaks with all notch filters disabled
287 13 : harmonics = 3;
288 : }
289 : // count the number of active harmonics or dynamic notchs
290 20 : _tracked_peaks = constrain_int16(MAX(__builtin_popcount(harmonics),
291 : num_notches), 1, FrequencyPeak::MAX_TRACKED_PEAKS);
292 :
293 : // calculate harmonic multiplier. this assumes the harmonics configured on the
294 : // harmonic notch reflect the multiples of the fundamental harmonic that should be tracked
295 20 : if (_harmonic_fit > 0) {
296 20 : uint8_t first_harmonic = 0;
297 100 : for (uint8_t i = 0; i < HNF_MAX_HARMONICS; i++) {
298 96 : if (harmonics & (1<<i)) {
299 36 : if (first_harmonic == 0) {
300 20 : first_harmonic = i + 1;
301 : } else {
302 16 : _harmonic_multiplier = float(i + 1) / first_harmonic;
303 16 : break;
304 : }
305 : }
306 : }
307 : // if no harmonic specified then select a simple 2x multiple
308 20 : if (is_zero(_harmonic_multiplier)) {
309 4 : _harmonic_multiplier = 2.0f;
310 : }
311 : }
312 : #endif // AP_INERTIALSENSOR_HARMONICNOTCH_ENABLED
313 :
314 : // initialise the HAL DSP subsystem
315 20 : _state = hal.dsp->fft_init(_window_size, _fft_sampling_rate_hz, _num_frames);
316 20 : if (_state == nullptr) {
317 0 : GCS_SEND_TEXT(MAV_SEVERITY_WARNING, "Failed to initialize DSP engine");
318 0 : return;
319 : }
320 :
321 : // per-axis frame time
322 20 : _frame_time_ms = _samples_per_frame * 1000 / _fft_sampling_rate_hz;
323 : // The update rate for the output, defaults are 1Khz / (1 - 0.5) * 32 == 62hz
324 20 : const float output_rate = static_cast<float>(_fft_sampling_rate_hz) / static_cast<float>(_samples_per_frame);
325 : // establish suitable defaults for the detected values
326 80 : for (uint8_t axis = 0; axis < XYZ_AXIS_COUNT; axis++) {
327 60 : _thread_state._center_freq_hz[axis] = _fft_min_hz;
328 :
329 240 : for (uint8_t peak = 0; peak < FrequencyPeak::MAX_TRACKED_PEAKS; peak++) {
330 180 : _thread_state._center_freq_hz_filtered[axis][peak] = _fft_min_hz;
331 : }
332 : // number of cycles to average over, two complete windows to be sure
333 60 : _noise_calibration_cycles[axis] = (_window_size / _samples_per_frame) * 2;
334 : // harmonic frequency fit should change relatively slowly
335 60 : _harmonic_fit_filter[axis].set_cutoff_frequency(output_rate, MIN(output_rate * 0.48f, FFT_HARMONIC_FIT_FILTER_HZ));
336 : }
337 :
338 : // configure a filter for frequency, bandwidth and energy for each of the three tracked noise peaks
339 : // filter more aggressively post-filter since the noise is harder to detect
340 20 : const float scale_factor = using_post_filter_samples() ? 0.1f : 1.0f;
341 80 : for (uint8_t peak = 0; peak < FrequencyPeak::MAX_TRACKED_PEAKS; peak++) {
342 : // calculate low-pass filter characteristics based on window size and overlap
343 60 : _center_freq_filter[peak].set_cutoff_frequency(output_rate, output_rate * 0.48f * scale_factor);
344 : // the bin energy jumps around a lot so requires more filtering
345 60 : _center_freq_energy_filter[peak].set_cutoff_frequency(output_rate, output_rate * 0.25f * scale_factor);
346 : // smooth the bandwidth output more aggressively
347 60 : _center_bandwidth_filter[peak].set_cutoff_frequency(output_rate, output_rate * 0.25f * scale_factor);
348 : }
349 :
350 : // turn down the SNR threshold if examining post-filter
351 20 : if (using_post_filter_samples()) {
352 2 : _snr_threshold_db.set_default(FFT_SNR_PFILT_DEFAULT);
353 : }
354 :
355 : // the number of cycles required to have a proper noise reference
356 20 : _noise_cycles = (_window_size / _samples_per_frame) * XYZ_AXIS_COUNT;
357 :
358 : // finally we are done
359 20 : _initialized = true;
360 20 : update_parameters(true);
361 : // start running FFTs
362 20 : if (start_update_thread()) {
363 20 : set_analysis_enabled(true);
364 : }
365 : }
366 :
367 : // sample the gyros either by using a gyro window sampled at the gyro rate or making individual samples
368 : // called from fast_loop thread - this function does not take out a semaphore to avoid waiting on the FFT thread
369 41686253 : void AP_GyroFFT::sample_gyros()
370 : {
371 41686253 : if (!analysis_enabled()) {
372 40883858 : return;
373 : }
374 :
375 : // update counters for gyro window
376 802395 : if (_current_sample_mode > 0) {
377 : // for loop rate sampling accumulate and average gyro samples
378 31554 : _oversampled_gyro_accum += _ins->get_gyro_for_fft();
379 31554 : _oversampled_gyro_count++;
380 :
381 31554 : if ((_oversampled_gyro_count % _current_sample_mode) == 0) {
382 : // calculate mean value of accumulated samples
383 31554 : Vector3f sample = _oversampled_gyro_accum / _current_sample_mode;
384 : // fast sampling means that the raw gyro values have already been averaged over 8 samples
385 31554 : _downsampled_gyro_data[0].push(sample.x);
386 31554 : _downsampled_gyro_data[1].push(sample.y);
387 31554 : _downsampled_gyro_data[2].push(sample.z);
388 :
389 31554 : _oversampled_gyro_accum.zero();
390 31554 : _oversampled_gyro_count = 0;
391 : }
392 : }
393 : }
394 :
395 : // update the state as as required
396 : // called from main thread at 400Hz - anything that requires atomic access to IMU data needs to be done here
397 41684408 : void AP_GyroFFT::update()
398 : {
399 41684408 : if (!analysis_enabled()) {
400 40882053 : return;
401 : }
402 :
403 802355 : WITH_SEMAPHORE(_sem);
404 :
405 802355 : _config._analysis_enabled = _analysis_enabled;
406 802355 : _global_state = _thread_state;
407 :
408 : // calculate health based on being 5 frames behind, SITL needs longer
409 : #if CONFIG_HAL_BOARD == HAL_BOARD_SITL
410 802355 : const uint32_t output_delay = _frame_time_ms * FFT_MAX_MISSED_UPDATES * 2;
411 : #else
412 : const uint32_t output_delay = _frame_time_ms * FFT_MAX_MISSED_UPDATES;
413 : #endif
414 802355 : uint32_t now = AP_HAL::millis();
415 802355 : _rpy_health.x = (now - _global_state._health_ms.x <= output_delay);
416 802355 : _rpy_health.y = (now - _global_state._health_ms.y <= output_delay);
417 802355 : _rpy_health.z = (now - _global_state._health_ms.z <= output_delay);
418 :
419 802355 : _health = _global_state._health;
420 802355 : if (!_rpy_health.x) {
421 206812 : _health.x = 0;
422 : }
423 802355 : if (!_rpy_health.y) {
424 208601 : _health.y = 0;
425 : }
426 802355 : if (!_rpy_health.z) {
427 208288 : _health.z = 0;
428 : }
429 802355 : }
430 :
431 : // analyse gyro data using FFT, returns number of samples still held
432 : // called from FFT thread
433 535336241 : uint16_t AP_GyroFFT::run_cycle()
434 : {
435 535336241 : if (!analysis_enabled()) {
436 18 : return 0;
437 : }
438 :
439 535336223 : if (!_sem.take(HAL_SEMAPHORE_BLOCK_FOREVER)) {
440 0 : return 0;
441 : }
442 :
443 : // do we have enough samples for another pass?
444 535336223 : if (!start_analysis()) {
445 535057667 : uint16_t new_sample_count = get_available_samples(_update_axis);
446 535057667 : _sem.give();
447 535057667 : return new_sample_count;
448 : }
449 :
450 : // take a copy of the config inside the semaphore
451 278556 : EngineConfig config = _config;
452 :
453 278556 : _sem.give();
454 :
455 278556 : uint32_t now = AP_HAL::micros();
456 :
457 : // get the appropriate gyro buffer
458 278556 : FloatBuffer& gyro_buffer = (_sample_mode == 0 ?_ins->get_raw_gyro_window(_update_axis) : _downsampled_gyro_data[_update_axis]);
459 : // if we have many more samples than the window size then we are struggling to
460 : // stay ahead of the gyro loop so drop samples so that this cycle will use all available samples
461 278556 : if (gyro_buffer.available() > uint32_t(_state->_window_size + uint16_t(_samples_per_frame >> 1))) { // half the frame size is a heuristic
462 281 : gyro_buffer.advance(gyro_buffer.available() - _state->_window_size);
463 : }
464 : // let's go!
465 278556 : hal.dsp->fft_start(_state, gyro_buffer, _samples_per_frame);
466 :
467 : // calculate FFT and update filters outside the semaphore
468 278556 : uint16_t bin_max = hal.dsp->fft_analyse(_state, config._fft_start_bin, config._fft_end_bin, config._attenuation_cutoff);
469 :
470 : // something has been detected, update the peak frequency and associated metrics
471 278556 : update_ref_energy(bin_max);
472 278556 : calculate_noise(false, config);
473 :
474 : // record how we are doing
475 278556 : _thread_state._last_output_us[_update_axis] = AP_HAL::micros();
476 278556 : _output_cycle_micros = _thread_state._last_output_us[_update_axis] - now;
477 :
478 : #if AP_SIM_ENABLED && HAL_LOGGING_ENABLED
479 : // extra logging when running simulations
480 : // @LoggerMessage: FTN3
481 : // @Description: Additional FFT Noise Frequency Peak
482 : // @Field: TimeUS: microseconds since system startup
483 : // @Field: Id: update axis
484 : // @Field: Pk1: Peak 1 frequency
485 : // @Field: Pk2: Peak 2 frequency
486 : // @Field: Pk3: Peak 3 Frequency
487 : // @Field: Bw1: Peak 1 noise bandwidth
488 : // @Field: Bw2: Peak 2 noise bandwidth
489 : // @Field: Bw3: Peak 3 noise bandwidth
490 : // @Field: En1: Peak 1 Maximum energy
491 : // @Field: En2: Peak 2 Maximum energy
492 : // @Field: En3: Peak 3 Maximum energy
493 557112 : AP::logger().WriteStreaming(
494 : "FTN3",
495 : "TimeUS,Id,Pk1,Pk2,Pk3,Bw1,Bw2,Bw3,En1,En2,En3",
496 : "s#zzzzzz---",
497 : "F----------",
498 : "QBfffffffff",
499 : AP_HAL::micros64(),
500 278556 : _update_axis,
501 278556 : _state->_peak_data[0]._freq_hz,
502 278556 : _state->_peak_data[1]._freq_hz,
503 278556 : _state->_peak_data[2]._freq_hz,
504 278556 : _state->_peak_data[0]._noise_width_hz,
505 278556 : _state->_peak_data[1]._noise_width_hz,
506 278556 : _state->_peak_data[2]._noise_width_hz,
507 278556 : _state->_freq_bins[_state->_peak_data[0]._bin],
508 278556 : _state->_freq_bins[_state->_peak_data[1]._bin],
509 278556 : _state->_freq_bins[_state->_peak_data[2]._bin]);
510 : #endif
511 :
512 : // move onto the next axis
513 278556 : _update_axis = (_update_axis + 1) % XYZ_AXIS_COUNT;
514 :
515 : // ready to receive another frame, because lock contention is so expensive we don't lock
516 : // around this flag but rather rely on the semaphore at the beginning of the loop to
517 : // ensure eventual visibility to the main loop
518 278556 : _thread_state._analysis_started = false;
519 :
520 : // samples remaining in the next axis
521 278556 : return get_available_samples(_update_axis);
522 : }
523 :
524 : // whether analysis can be run again or not
525 : // called from FFT thread with the semaphore held
526 535336223 : bool AP_GyroFFT::start_analysis() {
527 535336223 : if (_thread_state._analysis_started) {
528 0 : return false;
529 : }
530 : // don't run any more gyro cycles once noise is calibrated and the self-test is running
531 535336223 : if (!_thread_state._noise_needs_calibration && !_calibrated) {
532 535054761 : return false;
533 : }
534 :
535 281462 : if (get_available_samples(_update_axis) >= _state->_window_size) {
536 278556 : _thread_state._analysis_started = true;
537 278556 : return true;
538 : }
539 2906 : return false;
540 : }
541 :
542 : // update calculated values of dynamic parameters - runs at 1Hz
543 149237 : void AP_GyroFFT::update_parameters(bool force)
544 : {
545 149237 : if (!_initialized && !force) {
546 148903 : return;
547 : }
548 : #if AP_ARMING_ENABLED
549 : // lock contention is very costly, so don't allow configuration
550 : // updates while flying
551 2091 : if (AP::arming().is_armed() && !force) {
552 1757 : return;
553 : }
554 : #endif
555 :
556 334 : WITH_SEMAPHORE(_sem);
557 :
558 : // don't allow MAXHZ to go to Nyquist
559 334 : _fft_max_hz.set(MIN(_fft_max_hz, _fft_sampling_rate_hz * 0.48));
560 334 : _config._snr_threshold_db = _snr_threshold_db;
561 334 : _config._fft_min_hz = _fft_min_hz;
562 334 : _config._fft_max_hz = _fft_max_hz;
563 : // determine the start FFT bin for all frequency detection
564 334 : _config._fft_start_bin = MAX(floorf(_fft_min_hz.get() / _state->_bin_resolution), 1);
565 : // determine the endt FFT bin for all frequency detection
566 334 : _config._fft_end_bin = MIN(ceilf(_fft_max_hz.get() / _state->_bin_resolution), _state->_bin_count);
567 : // actual attenuation from the db value
568 334 : _config._attenuation_cutoff = powf(10.0f, -_attenuation_power_db * 0.1f);
569 334 : }
570 :
571 : // thread for processing gyro data via FFT
572 535336241 : void AP_GyroFFT::update_thread(void)
573 : {
574 : while (true) {
575 535336241 : uint16_t remaining_samples = run_cycle();
576 : // this is to stop us burning CPU while waiting for samples, the reduction by _samples_per_frame is a heuristic to prevent waiting too long
577 : // and missing frames (easy to see in SITL because the noise will keep calibrating)
578 : // we always delay by at least 1us to give logging a chance to run at the same priority
579 535336241 : uint32_t delay = constrain_int32((int16_t)_state->_window_size - (int16_t)remaining_samples, 0, _samples_per_frame)
580 535336241 : * 1e6 / _fft_sampling_rate_hz;
581 : #if CONFIG_HAL_BOARD == HAL_BOARD_SITL
582 : // in SITL the gyros do not run in a different thread
583 535336241 : if (delay > 0) {
584 133955 : hal.scheduler->delay_microseconds(delay);
585 : }
586 : #else
587 : hal.scheduler->delay_microseconds(MAX(delay, 1U));
588 : #endif
589 535336221 : }
590 : }
591 :
592 : // start the update thread
593 20 : bool AP_GyroFFT::start_update_thread(void)
594 : {
595 20 : WITH_SEMAPHORE(_sem);
596 :
597 20 : if (_thread_created) {
598 0 : return true;
599 : }
600 :
601 20 : if (!hal.scheduler->thread_create(FUNCTOR_BIND_MEMBER(&AP_GyroFFT::update_thread, void), "apm_fft", FFT_STACK_SIZE, AP_HAL::Scheduler::PRIORITY_IO, 0)) {
602 0 : AP_HAL::panic("Failed to start AP_GyroFFT update thread");
603 : return false;
604 : }
605 :
606 20 : _thread_created = true;
607 20 : return true;
608 20 : }
609 :
610 : // self-test the FFT analyser - can only be done while samples are not being taken
611 : // called from main thread
612 45825 : bool AP_GyroFFT::pre_arm_check(char *failure_msg, const uint8_t failure_msg_len)
613 : {
614 45825 : if (!analysis_enabled()) {
615 45674 : return true;
616 : }
617 :
618 : // already calibrated
619 151 : if (_calibrated) {
620 131 : return true;
621 : }
622 :
623 : // analysis is started in the main thread, don't trample on in-flight analysis
624 20 : if (_global_state._analysis_started) {
625 0 : hal.util->snprintf(failure_msg, failure_msg_len, "FFT still analyzing");
626 0 : return false;
627 : }
628 :
629 : // still calibrating noise so not ready
630 20 : if (_global_state._noise_needs_calibration) {
631 0 : hal.util->snprintf(failure_msg, failure_msg_len, "FFT calibrating noise");
632 0 : return false;
633 : }
634 :
635 : // make sure the frequency maximum is below Nyquist
636 20 : if (_fft_max_hz > _fft_sampling_rate_hz * 0.5f) {
637 0 : hal.util->snprintf(failure_msg, failure_msg_len, "FFT config MAXHZ %dHz > %dHz", _fft_max_hz.get(), _fft_sampling_rate_hz / 2);
638 0 : return false;
639 : }
640 :
641 : // check for sane frequency resolution - for 1k backends with length 32 this will be 32Hz
642 20 : if (_state->_bin_resolution > 50.0f) {
643 0 : GCS_SEND_TEXT(MAV_SEVERITY_WARNING, "FFT: resolution is %.1fHz, increase length", _state->_bin_resolution);
644 0 : return true; // a low resolution is not fatal
645 : }
646 : #if 0 // these calculations do not result in a long enough expected delay
647 : // in order to test all frequencies we need to endure a long pause for higher windows lengths
648 : // see timings AP_HAL_ChibiOS/DSP.cpp for per step timings on different hardware
649 : #if defined(STM32H7)
650 : const uint32_t cycle_time = (16 * (1 << (_window_size / 32)) + 5) * (_config._fft_end_bin - _config._fft_start_bin + 1) * 2 / 1000; // H7
651 : #else
652 : const uint32_t cycle_time = (29 * (1 << (_window_size / 32)) + 5) * (_config._fft_end_bin - _config._fft_start_bin + 1) * 2 / 1000; // F4
653 : #endif
654 : #endif
655 20 : EXPECT_DELAY_MS(2000); // tested on an H7 at 1024 window
656 :
657 20 : float max_divergence = self_test_bin_frequencies();
658 : // for longer FFT lengths the resolution gets below 1Hz
659 20 : if (max_divergence > MAX(_state->_bin_resolution * 0.5f, 1)) {
660 0 : hal.util->snprintf(failure_msg, failure_msg_len, "FFT self-test failed, max error %fHz", max_divergence);
661 : }
662 :
663 20 : _calibrated = max_divergence <= MAX(_state->_bin_resolution * 0.5f, 1);
664 :
665 20 : if (_calibrated) {
666 : // provide the user with some useful information about what they have configured
667 20 : GCS_SEND_TEXT(MAV_SEVERITY_INFO, "FFT: calibrated %.1fKHz/%.1fHz/%.1fHz", _fft_sampling_rate_hz * 0.001f,
668 : _state->_bin_resolution * 0.5, 1000.0f * XYZ_AXIS_COUNT / _frame_time_ms);
669 : }
670 :
671 20 : return _calibrated;
672 20 : }
673 :
674 : // we may have disabled the FFT arming check, in which case make sure the engine can still run
675 1241 : bool AP_GyroFFT::prepare_for_arming()
676 : {
677 1241 : _calibrated = true;
678 1241 : return true;
679 : }
680 :
681 : // update the hover frequency input filter. should be called at 100hz when in a stable hover
682 : // called from main thread
683 1489615 : void AP_GyroFFT::update_freq_hover(float dt, float throttle_out)
684 : {
685 1489615 : if (!analysis_enabled()) {
686 1434479 : return;
687 : }
688 :
689 : // throttle averaging for average fft calculation
690 55136 : if (is_zero(_avg_throttle_out)) {
691 18 : _avg_throttle_out = throttle_out;
692 : } else {
693 55118 : _avg_throttle_out = constrain_float(_avg_throttle_out + (dt / (10.0f + dt)) * (throttle_out - _avg_throttle_out), 0.01f, 0.9f);
694 : }
695 :
696 : // we have chosen to constrain the hover frequency to be within the range reachable by the third order expo polynomial.
697 55136 : _freq_hover_hz.set(constrain_float(_freq_hover_hz + (dt / (10.0f + dt)) * (get_weighted_noise_center_freq_hz() - _freq_hover_hz), _fft_min_hz, _fft_max_hz));
698 55136 : _bandwidth_hover_hz.set(constrain_float(_bandwidth_hover_hz + (dt / (10.0f + dt)) * (get_weighted_noise_center_bandwidth_hz() - _bandwidth_hover_hz), 0, _fft_max_hz * 0.5f));
699 55136 : _throttle_ref.set(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));
700 : }
701 :
702 : // save parameters as part of disarming
703 : // called from main thread
704 1103 : void AP_GyroFFT::save_params_on_disarm()
705 : {
706 1103 : if (!analysis_enabled()) {
707 1080 : return;
708 : }
709 :
710 23 : _freq_hover_hz.save();
711 23 : _bandwidth_hover_hz.save();
712 23 : _throttle_ref.save();
713 : }
714 :
715 : // notch tuning
716 2 : void AP_GyroFFT::start_notch_tune()
717 : {
718 2 : if (!analysis_enabled()) {
719 0 : return;
720 : }
721 :
722 2 : if (!hal.dsp->fft_start_average(_state)) {
723 0 : GCS_SEND_TEXT(MAV_SEVERITY_WARNING, "FFT: Unable to start FFT averaging");
724 : }
725 : // throttle averaging for average fft calculation
726 2 : _avg_throttle_out = 0.0f;
727 : #if APM_BUILD_COPTER_OR_HELI || APM_BUILD_TYPE(APM_BUILD_ArduPlane)
728 2 : AP_Motors* motors = AP::motors();
729 2 : if (motors != nullptr) {
730 2 : _avg_throttle_out = motors->get_throttle_hover();
731 : }
732 : #endif
733 : }
734 :
735 : // calculate the frequency to be used for the harmonic notch
736 3 : float AP_GyroFFT::calculate_notch_frequency(float* freqs, uint16_t numpeaks, float harmonic_fit, uint8_t& harmonics)
737 : {
738 3 : float harmonic = freqs[0];
739 3 : harmonics = 1;
740 :
741 9 : for (uint8_t i = 1; i < numpeaks; i++) {
742 6 : if (freqs[i] < harmonic) {
743 32 : for (uint8_t x = 2; x <=HNF_MAX_HARMONICS; x++) {
744 30 : if (is_harmonic_of(harmonic, freqs[i], x, harmonic_fit)) {
745 2 : harmonic = freqs[i];
746 : }
747 : }
748 : }
749 : }
750 : // select the harmonics that were matched
751 12 : for (uint8_t i = 0; i < numpeaks; i++) {
752 153 : for (uint8_t x = 1; x <=HNF_MAX_HARMONICS; x++) {
753 144 : if (is_harmonic_of(freqs[i], harmonic, x, harmonic_fit)) {
754 8 : harmonics |= 1<<(x - 1);
755 : }
756 : }
757 : }
758 3 : return harmonic;
759 : }
760 :
761 6 : void AP_GyroFFT::stop_notch_tune()
762 : {
763 6 : if (!analysis_enabled()) {
764 4 : return;
765 : }
766 :
767 4 : float freqs[FrequencyPeak::MAX_TRACKED_PEAKS] {};
768 :
769 4 : uint16_t numpeaks = hal.dsp->fft_stop_average(_state, _config._fft_start_bin, _config._fft_end_bin, freqs);
770 :
771 4 : if (numpeaks == 0) {
772 2 : return;
773 : }
774 :
775 : uint8_t harmonics;
776 2 : float harmonic = calculate_notch_frequency(freqs, numpeaks, _harmonic_fit, harmonics);
777 :
778 2 : GCS_SEND_TEXT(MAV_SEVERITY_INFO, "FFT: Found peaks at %.1f/%.1f/%.1fHz", freqs[0], freqs[1], freqs[2]);
779 2 : GCS_SEND_TEXT(MAV_SEVERITY_NOTICE, "FFT: Selected %.1fHz", harmonic);
780 :
781 : // if we don't have a throttle value then all bets are off
782 2 : if (is_zero(_avg_throttle_out) || is_zero(harmonic)) {
783 0 : GCS_SEND_TEXT(MAV_SEVERITY_WARNING, "FFT: Unable to calculate notch: need stable hover");
784 0 : AP_Notify::events.autotune_failed = true;
785 0 : return;
786 : }
787 :
788 : #if AP_INERTIALSENSOR_HARMONICNOTCH_ENABLED
789 2 : if (!_ins->setup_throttle_gyro_harmonic_notch(harmonic, (float)_fft_min_hz.get(), _avg_throttle_out, harmonics)) {
790 0 : GCS_SEND_TEXT(MAV_SEVERITY_WARNING, "FFT: Unable to set throttle notch with %.1fHz/%.2f",
791 : harmonic, _avg_throttle_out);
792 0 : AP_Notify::events.autotune_failed = true;
793 : // save results to FFT slots
794 0 : _throttle_ref.set(_avg_throttle_out);
795 0 : _freq_hover_hz.set(harmonic);
796 : } else {
797 2 : GCS_SEND_TEXT(MAV_SEVERITY_NOTICE, "FFT: Notch frequency %.1fHz and ref %.2f selected", harmonic, _avg_throttle_out);
798 2 : AP_Notify::events.autotune_complete = true;
799 : }
800 : #endif // AP_INERTIALSENSOR_HARMONICNOTCH_ENABLED
801 : }
802 :
803 : // return the noise peak that is being tracked
804 : // called from main thread
805 191009 : AP_GyroFFT::FrequencyPeak AP_GyroFFT::get_tracked_noise_peak() const
806 : {
807 : // if the user has specified a specific axis to track then use that
808 191009 : if (_harmonic_peak > FrequencyPeak::MAX_TRACKED_PEAKS) {
809 0 : switch (_harmonic_peak) {
810 0 : case FFT_HARMONIC_FIT_TRACK_ROLL:
811 0 : if (_global_state._harmonic_fit.x < _harmonic_fit) {
812 0 : return FrequencyPeak(_global_state._tracked_peak.x);
813 : }
814 0 : break;
815 0 : case FFT_HARMONIC_FIT_TRACK_PITCH:
816 0 : if (_global_state._harmonic_fit.y < _harmonic_fit) {
817 0 : return FrequencyPeak(_global_state._tracked_peak.y);
818 : }
819 0 : break;
820 0 : default:
821 0 : break;
822 : }
823 0 : return FrequencyPeak::CENTER;
824 : }
825 : // if the user has specified a specific peak to track then use that
826 191009 : if (_harmonic_peak > 0) {
827 0 : return FrequencyPeak(constrain_int16(_harmonic_peak - 1, FrequencyPeak::CENTER, FrequencyPeak::UPPER_SHOULDER));
828 : }
829 :
830 : // required fit of 10% is fairly conservative when testing in SITL, testing shows that it's safer to
831 : // require both tracked axes to fit - biasing towards the highest energy peak
832 191009 : if (_global_state._harmonic_fit.x < _harmonic_fit && _global_state._harmonic_fit.y < _harmonic_fit) {
833 29952 : return FrequencyPeak(_global_state._tracked_peak.x);
834 : }
835 :
836 161057 : return FrequencyPeak::CENTER;
837 : }
838 :
839 : // weighted center frequency
840 170588 : float AP_GyroFFT::get_weighted_freq_hz(FrequencyPeak peak) const
841 : {
842 170588 : const Vector3f& energy = get_center_freq_energy(peak);
843 170588 : const Vector3f& freq = get_noise_center_freq_hz(peak);
844 :
845 170588 : if (!energy.is_nan() && !is_zero(energy.x) && !is_zero(energy.y)) {
846 170246 : return (freq.x * energy.x + freq.y * energy.y) / (energy.x + energy.y);
847 : } else {
848 342 : return (freq.x + freq.y) * 0.5f;
849 : }
850 : }
851 :
852 : // return an average center frequency weighted by bin energy
853 : // called from main thread
854 130919 : float AP_GyroFFT::get_weighted_noise_center_freq_hz() const
855 : {
856 130919 : if (!analysis_enabled()) {
857 259 : return _fft_min_hz;
858 : }
859 :
860 130660 : if (_health.is_zero()) {
861 : #if APM_BUILD_COPTER_OR_HELI || APM_BUILD_TYPE(APM_BUILD_ArduPlane)
862 : // if we are post-filter sampling then throttle estimate will be useless
863 17643 : if (using_post_filter_samples()) {
864 8179 : return 0.0f;
865 : }
866 9464 : AP_Motors* motors = AP::motors();
867 9464 : if (motors != nullptr && !is_zero(_throttle_ref)) {
868 : // FFT is not healthy, fallback to FFT's throttle-based estimate
869 9464 : return constrain_float(_fft_min_hz * MAX(1.0f, sqrtf(motors->get_throttle_out() / _throttle_ref)), _fft_min_hz, _fft_max_hz);
870 : }
871 : #endif
872 : }
873 :
874 113017 : const FrequencyPeak peak = get_tracked_noise_peak();
875 : // pitch was good or required, roll was not, use pitch only
876 113017 : if (!_health.x || _harmonic_peak == FFT_HARMONIC_FIT_TRACK_PITCH) {
877 234 : return get_noise_center_freq_hz(peak).y; // Y-axis
878 : }
879 : // roll was good or required, pitch was not, use roll only
880 112783 : if (!_health.y || _harmonic_peak == FFT_HARMONIC_FIT_TRACK_ROLL) {
881 341 : return get_noise_center_freq_hz(peak).x; // X-axis
882 : }
883 :
884 112442 : return get_weighted_freq_hz(peak);
885 : }
886 :
887 : // return all the center frequencies weighted by bin energy
888 : // called from main thread
889 44424 : uint8_t AP_GyroFFT::get_weighted_noise_center_frequencies_hz(uint8_t num_freqs, float* freqs) const
890 : {
891 44424 : if (!analysis_enabled()) {
892 199 : freqs[0] = _fft_min_hz;
893 199 : return 1;
894 : }
895 :
896 44225 : if (_health.is_zero()) {
897 : #if APM_BUILD_COPTER_OR_HELI || APM_BUILD_TYPE(APM_BUILD_ArduPlane)
898 : // if we are post-filter sampling then throttle estimate will be useless
899 23181 : if (using_post_filter_samples()) {
900 18133 : return 0;
901 : }
902 5048 : AP_Motors* motors = AP::motors();
903 5048 : if (motors != nullptr) {
904 : // FFT is not healthy, fallback to FFT's throttle-based estimate
905 5048 : freqs[0] = constrain_float(_fft_min_hz * MAX(1.0f, sqrtf(motors->get_throttle_out() / _throttle_ref)), _fft_min_hz, _fft_max_hz);
906 5048 : return 1;
907 : }
908 : #endif
909 : }
910 :
911 : // pitch was good or required, roll was not, use pitch only
912 21044 : if (!_health.x || _harmonic_peak == FFT_HARMONIC_FIT_TRACK_PITCH) {
913 383 : const uint8_t tracked_peaks = MIN(_health.y, num_freqs);
914 1375 : for (uint8_t i = 0; i < tracked_peaks; i++) {
915 992 : freqs[i] = get_noise_center_freq_hz(FrequencyPeak(i)).y; // Y-axis
916 : }
917 383 : return tracked_peaks;
918 : }
919 : // roll was good or required, pitch was not, use roll only
920 20661 : if (!_health.y || _harmonic_peak == FFT_HARMONIC_FIT_TRACK_ROLL) {
921 729 : const uint8_t tracked_peaks = MIN(_health.x, num_freqs);
922 1790 : for (uint8_t i = 0; i < tracked_peaks; i++) {
923 1061 : freqs[i] = get_noise_center_freq_hz(FrequencyPeak(i)).x; // X-axis
924 : }
925 729 : return tracked_peaks;
926 : }
927 :
928 19932 : const uint8_t tracked_peaks = MIN(MAX(_health.x, _health.y), num_freqs);
929 78078 : for (uint8_t i = 0; i < tracked_peaks; i++) {
930 58146 : freqs[i] = get_weighted_freq_hz(FrequencyPeak(i));
931 : }
932 19932 : return tracked_peaks;
933 : }
934 :
935 : // return noise energy at the requested frequency
936 1202 : float AP_GyroFFT::has_noise_at_frequency_hz(float freq) const
937 : {
938 1202 : if (!analysis_enabled()) {
939 0 : return 0.0f;
940 : }
941 :
942 1202 : float max_energy = 0.0f;
943 :
944 : // check each axis of each peak to see if it contains the pass frequency
945 4808 : for (uint8_t i = 0; i < _tracked_peaks; i++) {
946 3606 : const Vector3f& noise = get_noise_center_freq_hz(FrequencyPeak(i));
947 3606 : const Vector3f& snr = get_noise_signal_to_noise_db(FrequencyPeak(i));
948 :
949 14424 : for (uint8_t j = 0; j < XYZ_AXIS_COUNT; j++) {
950 10818 : if (!_rpy_health[j]) {
951 5904 : continue;
952 : }
953 :
954 : // only check one bin either side of the frequency
955 4914 : if ((noise[j] - _state->_bin_resolution) < freq && (noise[j] + _state->_bin_resolution) > freq) {
956 619 : max_energy = MAX(snr[j], max_energy);
957 : }
958 : }
959 : }
960 1202 : return max_energy;
961 : }
962 :
963 77992 : float AP_GyroFFT::calculate_weighted_freq_hz(const Vector3f& energy, const Vector3f& freq) const
964 : {
965 : // there is generally a lot of high-energy, slightly lower frequency noise on yaw, however this
966 : // appears to be a second-order effect as only targetting pitch and roll (x & y) produces much cleaner output all round
967 77992 : if (!energy.is_nan() && !is_zero(energy.x) && !is_zero(energy.y)) {
968 77971 : return (freq.x * energy.x + freq.y * energy.y)
969 77971 : / (energy.x + energy.y);
970 : }
971 : else {
972 21 : return (freq.x + freq.y) * 0.5f;
973 : }
974 : }
975 :
976 : // @LoggerMessage: FTN1
977 : // @Description: FFT Filter Tuning
978 : // @Field: TimeUS: microseconds since system startup
979 : // @Field: PkAvg: peak noise frequency as an energy-weighted average of roll and pitch peak frequencies
980 : // @Field: BwAvg: bandwidth of weighted peak frequency where edges are determined by FFT_ATT_REF
981 : // @Field: SnX: signal-to-noise ratio on the roll axis
982 : // @Field: SnY: signal-to-noise ratio on the pitch axis
983 : // @Field: SnZ: signal-to-noise ratio on the yaw axis
984 : // @Field: FtX: harmonic fit on roll of the highest noise peak to the second highest noise peak
985 : // @Field: FtY: harmonic fit on pitch of the highest noise peak to the second highest noise peak
986 : // @Field: FtZ: harmonic fit on yaw of the highest noise peak to the second highest noise peak
987 : // @Field: FHX: FFT health, X-axis
988 : // @Field: FHY: FFT health, Y-axis
989 : // @Field: FHZ: FFT health, Z-axis
990 : // @Field: Tc: FFT cycle time
991 :
992 : #if HAL_LOGGING_ENABLED
993 :
994 : // log gyro fft messages
995 2211214 : void AP_GyroFFT::write_log_messages()
996 : {
997 2211214 : if (!analysis_enabled()) {
998 2188358 : return;
999 : }
1000 :
1001 45712 : AP::logger().WriteStreaming(
1002 : "FTN1",
1003 : "TimeUS,PkAvg,BwAvg,SnX,SnY,SnZ,FtX,FtY,FtZ,FHX,FHY,FHZ,Tc",
1004 : "szz---%%%---s",
1005 : "F-----------F",
1006 : "QffffffffBBBI",
1007 : AP_HAL::micros64(),
1008 22856 : get_weighted_noise_center_freq_hz(),
1009 22856 : get_weighted_noise_center_bandwidth_hz(),
1010 22856 : get_noise_signal_to_noise_db().x,
1011 22856 : get_noise_signal_to_noise_db().y,
1012 22856 : get_noise_signal_to_noise_db().z,
1013 22856 : get_raw_noise_harmonic_fit().x,
1014 22856 : get_raw_noise_harmonic_fit().y,
1015 22856 : get_raw_noise_harmonic_fit().z,
1016 22856 : _health.x, _health.y, _health.z, _output_cycle_micros);
1017 :
1018 22856 : log_noise_peak(0, FrequencyPeak::CENTER);
1019 22856 : if (_tracked_peaks> 1) {
1020 22856 : log_noise_peak(1, FrequencyPeak::LOWER_SHOULDER);
1021 22856 : log_noise_peak(2, FrequencyPeak::UPPER_SHOULDER);
1022 : }
1023 :
1024 : #if DEBUG_FFT
1025 : const uint32_t now = AP_HAL::millis();
1026 : // output at 1hz
1027 : if ((now - _last_output_ms) > 1000) {
1028 : // doing this from the update thread overflows the stack
1029 : WITH_SEMAPHORE(_sem);
1030 : GCS_SEND_TEXT(MAV_SEVERITY_WARNING, "FFT: f:%.1f, fr:%.1f, b:%u, fd:%.1f",
1031 : _debug_state._center_freq_hz_filtered[FrequencyPeak::CENTER][_update_axis], _debug_state._center_freq_hz[_update_axis], _debug_max_bin, _debug_max_bin_freq);
1032 : GCS_SEND_TEXT(MAV_SEVERITY_WARNING, "FFT: bw:%.1f, e:%.1f, r:%.1f, snr:%.1f",
1033 : _debug_state._center_bandwidth_hz_filtered[FrequencyPeak::CENTER][_update_axis], _debug_max_freq_bin, _ref_energy[_debug_max_bin][_update_axis], _debug_snr);
1034 : _last_output_ms = now;
1035 : }
1036 : #endif
1037 : }
1038 :
1039 : // @LoggerMessage: FTN2
1040 : // @Description: FFT Noise Frequency Peak
1041 : // @Field: TimeUS: microseconds since system startup
1042 : // @Field: Id: peak id where 0 is the centre peak, 1 is the lower shoulder and 2 is the upper shoulder
1043 : // @Field: PkX: noise frequency of the peak on roll
1044 : // @Field: PkY: noise frequency of the peak on pitch
1045 : // @Field: PkZ: noise frequency of the peak on yaw
1046 : // @Field: BwX: bandwidth of the peak frequency on roll where edges are determined by FFT_ATT_REF
1047 : // @Field: BwY: bandwidth of the peak frequency on pitch where edges are determined by FFT_ATT_REF
1048 : // @Field: BwZ: bandwidth of the peak frequency on yaw where edges are determined by FFT_ATT_REF
1049 : // @Field: SnX: signal-to-noise ratio on the roll axis
1050 : // @Field: SnY: signal-to-noise ratio on the pitch axis
1051 : // @Field: SnZ: signal-to-noise ratio on the yaw axis
1052 : // @Field: EnX: power spectral density bin energy of the peak on roll
1053 : // @Field: EnY: power spectral density bin energy of the peak on roll
1054 : // @Field: EnZ: power spectral density bin energy of the peak on roll
1055 :
1056 : // write a single log message
1057 68568 : void AP_GyroFFT::log_noise_peak(uint8_t id, FrequencyPeak peak) const
1058 : {
1059 137136 : AP::logger().WriteStreaming("FTN2", "TimeUS,Id,PkX,PkY,PkZ,BwX,BwY,BwZ,SnX,SnY,SnZ,EnX,EnY,EnZ", "s#zzzzzz------", "F-------------", "QBffffffffffff",
1060 : AP_HAL::micros64(),
1061 : id,
1062 68568 : get_noise_center_freq_hz(peak).x,
1063 68568 : get_noise_center_freq_hz(peak).y,
1064 68568 : get_noise_center_freq_hz(peak).z,
1065 68568 : get_noise_center_bandwidth_hz(peak).x,
1066 68568 : get_noise_center_bandwidth_hz(peak).y,
1067 68568 : get_noise_center_bandwidth_hz(peak).z,
1068 68568 : get_noise_signal_to_noise_db(peak).x,
1069 68568 : get_noise_signal_to_noise_db(peak).y,
1070 68568 : get_noise_signal_to_noise_db(peak).z,
1071 68568 : get_center_freq_energy(peak).x,
1072 68568 : get_center_freq_energy(peak).y,
1073 68568 : get_center_freq_energy(peak).z);
1074 68568 : }
1075 :
1076 : #endif
1077 :
1078 : // return an average noise bandwidth weighted by bin energy
1079 : // called from main thread
1080 77992 : float AP_GyroFFT::get_weighted_noise_center_bandwidth_hz() const
1081 : {
1082 77992 : if (!analysis_enabled()) {
1083 0 : return 0.0f;
1084 : }
1085 :
1086 77992 : const FrequencyPeak peak = get_tracked_noise_peak();
1087 :
1088 77992 : return calculate_weighted_freq_hz(get_center_freq_energy(peak), get_noise_center_bandwidth_hz(peak));
1089 : }
1090 :
1091 : // calculate noise frequencies from FFT data provided by the HAL subsystem
1092 : // called from FFT thread
1093 279734 : void AP_GyroFFT::calculate_noise(bool calibrating, const EngineConfig& config)
1094 : {
1095 : // calculate the SNR and center frequency energy
1096 279734 : float weighted_center_freq_hz = 0.0f;
1097 :
1098 279734 : uint8_t num_peaks = calculate_tracking_peaks(weighted_center_freq_hz, calibrating, config);
1099 :
1100 279734 : _thread_state._center_freq_bin[_update_axis] = _state->_peak_data[_thread_state._center_peak[_update_axis]]._bin;
1101 279734 : _thread_state._center_freq_hz[_update_axis] = weighted_center_freq_hz;
1102 : // record the last time we had a good signal on this axis
1103 279734 : if (num_peaks > 0) {
1104 241602 : _thread_state._health_ms[_update_axis] = AP_HAL::millis();
1105 : } else {
1106 38132 : _thread_state._health_ms[_update_axis] = 0;
1107 : }
1108 279734 : _thread_state._health[_update_axis] = num_peaks;
1109 279734 : FrequencyPeak tracked_peak = FrequencyPeak::CENTER;
1110 :
1111 : // record the tracked peak for harmonic fit, but only if we have more than one noise peak
1112 : // this checks filtered energies and so can allow energies to be closer together
1113 279734 : if (num_peaks > 1 && _tracked_peaks > 1 && !is_zero(get_tl_noise_center_freq_hz(FrequencyPeak::CENTER, _update_axis))) {
1114 228377 : if (get_tl_noise_center_freq_hz(FrequencyPeak::CENTER, _update_axis) > get_tl_noise_center_freq_hz(FrequencyPeak::LOWER_SHOULDER, _update_axis)
1115 : // ignore the fit if there is too big a discrepancy between the energies
1116 228377 : && get_tl_center_freq_energy(FrequencyPeak::CENTER, _update_axis) < get_tl_center_freq_energy(FrequencyPeak::LOWER_SHOULDER, _update_axis) * FFT_HARMONIC_FIT_MULT) {
1117 64703 : tracked_peak = FrequencyPeak::LOWER_SHOULDER;
1118 124158 : } else if (num_peaks > 2 && get_tl_noise_center_freq_hz(FrequencyPeak::CENTER, _update_axis) > get_tl_noise_center_freq_hz(FrequencyPeak::UPPER_SHOULDER, _update_axis)
1119 : // ignore the fit if there is too big a discrepancy between the energies
1120 287832 : && get_tl_center_freq_energy(FrequencyPeak::CENTER, _update_axis) < get_tl_center_freq_energy(FrequencyPeak::UPPER_SHOULDER, _update_axis) * FFT_HARMONIC_FIT_MULT) {
1121 15447 : tracked_peak = FrequencyPeak::UPPER_SHOULDER;
1122 : }
1123 : }
1124 :
1125 279734 : _thread_state._tracked_peak[_update_axis] = tracked_peak;
1126 :
1127 : // if targetting more than one harmonic then make sure we get the fundamental
1128 : // on larger copters the second harmonic often has more energy
1129 : // if the highest peak is above the second highest then check for harmonic fit
1130 : // comparisons are made using filter, normalised data
1131 279734 : if (_thread_state._tracked_peak[_update_axis] != FrequencyPeak::CENTER) {
1132 : // calculate the fit and filter at 10hz
1133 80150 : const float harmonic_fit = 100.0f * fabsf(get_tl_noise_center_freq_hz(FrequencyPeak::CENTER, _update_axis)
1134 80150 : - get_tl_noise_center_freq_hz(tracked_peak, _update_axis) * _harmonic_multiplier)
1135 80150 : / get_tl_noise_center_freq_hz(FrequencyPeak::CENTER, _update_axis);
1136 :
1137 : // calculate the fit and filter at 10hz
1138 80150 : if (isfinite(harmonic_fit)) {
1139 80150 : _thread_state._harmonic_fit[_update_axis] = _harmonic_fit_filter[_update_axis].apply(harmonic_fit);
1140 : }
1141 : } else {
1142 199584 : _thread_state._harmonic_fit[_update_axis] = 100.0f;
1143 : }
1144 : #if DEBUG_FFT
1145 : WITH_SEMAPHORE(_sem);
1146 : _debug_state = _thread_state;
1147 : _debug_max_freq_bin = _state->get_freq_bin(_state->_peak_data[FrequencyPeak::CENTER]._bin);
1148 : _debug_max_bin_freq = _state->_peak_data[FrequencyPeak::CENTER]._freq_hz;
1149 : _debug_snr = snr;
1150 : _debug_max_bin = _state->_peak_data[FrequencyPeak::CENTER]._bin;
1151 : #endif
1152 279734 : }
1153 :
1154 :
1155 : // calculate noise peaks based on the frequencies closest to the recent historical average, switching peaks around as necessary
1156 279734 : uint8_t AP_GyroFFT::calculate_tracking_peaks(float& weighted_center_freq_hz, bool calibrating, const EngineConfig& config)
1157 : {
1158 279734 : uint8_t num_peaks = 0;
1159 279734 : FrequencyData freqs(*this, config);
1160 :
1161 : // the noise peaks are returned by the HAL in decreasing order of magnitude, however each peak can temporarily
1162 : // switch places with another depending on a whole host of hardware and software factors
1163 : // thus we must be able to temporarily reassign the peaks so that the filtered values track
1164 : // a continuous frequency
1165 : DistanceMatrix distance_matrix;
1166 279734 : find_distance_matrix(distance_matrix, freqs, config);
1167 :
1168 279734 : FrequencyPeak center = find_closest_peak(FrequencyPeak::CENTER, distance_matrix);
1169 279734 : FrequencyPeak lower = find_closest_peak(FrequencyPeak::LOWER_SHOULDER, distance_matrix, 1 << center);
1170 279734 : FrequencyPeak upper = find_closest_peak(FrequencyPeak::UPPER_SHOULDER, distance_matrix, 1 << center | 1 << lower);
1171 :
1172 : // if we have had the maximum number of swapped cycles, force a full calculation
1173 279734 : if (calibrating || _distorted_cycles[_update_axis] == 0) {
1174 13030 : num_peaks = calculate_tracking_peaks(weighted_center_freq_hz, freqs, config);
1175 : #if DEBUG_FFT
1176 : printf("Skipped update, order would have been is %d/%.1f(%.1f) %d/%.1f(%.1f) %d/%.1f(%.1f) n = %d\n",
1177 : center, _state->_peak_data[center]._freq_hz, get_tl_noise_center_freq_hz(FrequencyPeak::CENTER, _update_axis),
1178 : lower, _state->_peak_data[lower]._freq_hz, get_tl_noise_center_freq_hz(FrequencyPeak::LOWER_SHOULDER, _update_axis),
1179 : upper, _state->_peak_data[upper]._freq_hz, get_tl_noise_center_freq_hz(FrequencyPeak::UPPER_SHOULDER, _update_axis), num_peaks);
1180 : #endif
1181 13030 : return num_peaks;
1182 : }
1183 :
1184 : // another peak is closer to what is currently considered the center frequency
1185 266704 : if (center != FrequencyPeak::CENTER || lower != FrequencyPeak::LOWER_SHOULDER || upper != FrequencyPeak::UPPER_SHOULDER) {
1186 61413 : if (lower != FrequencyPeak::NONE && calculate_filtered_noise(FrequencyPeak::LOWER_SHOULDER, lower, freqs, config)) {
1187 59679 : num_peaks++;
1188 : } else {
1189 1734 : lower = FrequencyPeak::NONE;
1190 : }
1191 61413 : if (upper != FrequencyPeak::NONE && calculate_filtered_noise(FrequencyPeak::UPPER_SHOULDER, upper, freqs, config)) {
1192 27316 : num_peaks++;
1193 : } else {
1194 34097 : upper = FrequencyPeak::NONE;
1195 : }
1196 61413 : if (center != FrequencyPeak::NONE && calculate_filtered_noise(FrequencyPeak::CENTER, center, freqs, config)) {
1197 61413 : num_peaks++;
1198 : } else {
1199 0 : center = FrequencyPeak::NONE;
1200 : }
1201 61413 : weighted_center_freq_hz = freqs.get_weighted_frequency(center);
1202 61413 : _thread_state._center_peak[_update_axis] = center;
1203 61413 : update_snr_values(freqs);
1204 : // if two adjacent peaks have simply swapped, we will allow this to continue indefinitely
1205 : // as there is no loss of fidelity
1206 61413 : if (!((center == FrequencyPeak::LOWER_SHOULDER && lower == FrequencyPeak::CENTER)
1207 6013 : || (center == FrequencyPeak::UPPER_SHOULDER && upper == FrequencyPeak::CENTER))) {
1208 53964 : _distorted_cycles[_update_axis]--;
1209 : }
1210 61413 : return num_peaks;
1211 : }
1212 :
1213 205291 : num_peaks = calculate_tracking_peaks(weighted_center_freq_hz, freqs, config);
1214 :
1215 205291 : return num_peaks;
1216 : }
1217 :
1218 : // calculate the noise and whether valid for each peak
1219 218321 : uint8_t AP_GyroFFT::calculate_tracking_peaks(float& weighted_center_freq_hz, const FrequencyData& freqs, const EngineConfig& config)
1220 : {
1221 218321 : uint8_t num_peaks = 0;
1222 218321 : if (calculate_filtered_noise(FrequencyPeak::LOWER_SHOULDER, FrequencyPeak::LOWER_SHOULDER, freqs, config)) {
1223 166908 : num_peaks++;
1224 : }
1225 218321 : if (calculate_filtered_noise(FrequencyPeak::UPPER_SHOULDER, FrequencyPeak::UPPER_SHOULDER, freqs, config)) {
1226 154839 : num_peaks++;
1227 : }
1228 218321 : if (calculate_filtered_noise(FrequencyPeak::CENTER, FrequencyPeak::CENTER, freqs, config)) {
1229 179913 : num_peaks++;
1230 : }
1231 : // record the number of cycles where something was tracked
1232 218321 : _distorted_cycles[_update_axis] = constrain_int16(_distorted_cycles[_update_axis] + 1, 0, FFT_MAX_MISSED_UPDATES);
1233 218321 : weighted_center_freq_hz = freqs.get_weighted_frequency(FrequencyPeak::CENTER);
1234 218321 : _thread_state._center_peak[_update_axis] = FrequencyPeak::CENTER;
1235 :
1236 218321 : update_snr_values(freqs);
1237 :
1238 218321 : return num_peaks;
1239 : }
1240 :
1241 : // calculate noise frequencies from FFT data provided by the HAL subsystem
1242 : // target_peak is the filtered record we want to apply the new fft data to, source peak is where the fft data is coming from
1243 : // called from FFT thread
1244 803749 : bool AP_GyroFFT::calculate_filtered_noise(FrequencyPeak target_peak, FrequencyPeak source_peak, const FrequencyData& freqs, const EngineConfig& config)
1245 : {
1246 803749 : if (source_peak > FrequencyPeak::MAX_TRACKED_PEAKS) {
1247 : // if we failed to find a signal, carry on using the previous readings
1248 0 : if (_missed_cycles[_update_axis][target_peak]++ < FFT_MAX_MISSED_UPDATES) {
1249 0 : return true; // the peak is synthetic
1250 : }
1251 0 : update_tl_center_freq_energy(target_peak, _update_axis, 0.0f);
1252 0 : update_tl_noise_center_bandwidth_hz(target_peak, _update_axis, _bandwidth_hover_hz);
1253 0 : update_tl_noise_center_freq_hz(target_peak, _update_axis, config._fft_min_hz);
1254 0 : return false;
1255 : }
1256 :
1257 803749 : AP_HAL::DSP::FrequencyPeakData* peak_data = &_state->_peak_data[source_peak];
1258 :
1259 803749 : const uint16_t nb = peak_data->_bin;
1260 :
1261 803749 : if (freqs.is_valid(FrequencyPeak(source_peak))) {
1262 : // total peak energy requires an integration, as an approximation use amplitude * noise width * 5/6
1263 601373 : update_tl_center_freq_energy(target_peak, _update_axis, _state->get_freq_bin(nb) * peak_data->_noise_width_hz * 0.8333f);
1264 601373 : update_tl_noise_center_bandwidth_hz(target_peak, _update_axis, peak_data->_noise_width_hz);
1265 601373 : update_tl_noise_center_freq_hz(target_peak, _update_axis, freqs.get_weighted_frequency(FrequencyPeak(source_peak)));
1266 601373 : _missed_cycles[_update_axis][target_peak] = 0;
1267 601373 : return true;
1268 : }
1269 :
1270 : // if we failed to find a signal, carry on using the previous readings
1271 202376 : if (_missed_cycles[_update_axis][target_peak]++ < FFT_MAX_MISSED_UPDATES) {
1272 48695 : return true; // the peak is synthetic
1273 : }
1274 :
1275 : // we failed to find a signal for more than FFT_MAX_MISSED_UPDATES cycles
1276 153681 : update_tl_center_freq_energy(target_peak, _update_axis, _state->get_freq_bin(nb) * peak_data->_noise_width_hz * 0.8333f); // use the actual energy detected rather than 0
1277 153681 : update_tl_noise_center_bandwidth_hz(target_peak, _update_axis, _bandwidth_hover_hz);
1278 153681 : update_tl_noise_center_freq_hz(target_peak, _update_axis, config._fft_min_hz);
1279 :
1280 153681 : return false;
1281 : }
1282 :
1283 279734 : void AP_GyroFFT::update_snr_values(const FrequencyData& freqs)
1284 : {
1285 279734 : _thread_state._center_freq_snr[FrequencyPeak::CENTER][_update_axis] = freqs.get_signal_to_noise(FrequencyPeak::CENTER);
1286 279734 : _thread_state._center_freq_snr[FrequencyPeak::LOWER_SHOULDER][_update_axis] = freqs.get_signal_to_noise(FrequencyPeak::LOWER_SHOULDER);
1287 279734 : _thread_state._center_freq_snr[FrequencyPeak::UPPER_SHOULDER][_update_axis] = freqs.get_signal_to_noise(FrequencyPeak::UPPER_SHOULDER);
1288 279734 : }
1289 :
1290 :
1291 : // filter values through a median sliding window followed by low pass filter
1292 : // this eliminates temporary spikes in the detected frequency that are either pure noise
1293 : // or a different peak that will erroneously bias the peak we are tracking
1294 2265162 : float AP_GyroFFT::MedianLowPassFilter3dFloat::apply(uint8_t axis, float sample)
1295 : {
1296 2265162 : _median_filter[axis].apply(sample);
1297 2265162 : const float a = _median_filter[axis].get_sample(0);
1298 2265162 : const float b = _median_filter[axis].get_sample(1);
1299 2265162 : const float c = _median_filter[axis].get_sample(2);
1300 2265162 : float median = MAX(MIN(a, b), MIN(MAX(a, b), c));
1301 2265162 : return _lowpass_filter[axis].apply(median);
1302 : }
1303 :
1304 : // initialize a FrequencyData structure with peak frequency information for use in the swapping algorithm
1305 279734 : AP_GyroFFT::FrequencyData::FrequencyData(const AP_GyroFFT& gyrofft, const EngineConfig& config)
1306 : {
1307 1118936 : for (uint8_t i = 0; i < FrequencyPeak::MAX_TRACKED_PEAKS; i++) {
1308 839202 : valid[i] = gyrofft.get_weighted_frequency(FrequencyPeak(i), frequency[i], snr[i], config);
1309 : }
1310 279734 : }
1311 :
1312 : // calculate noise frequencies from FFT data provided by the HAL subsystem
1313 839202 : bool AP_GyroFFT::get_weighted_frequency(FrequencyPeak peak, float& weighted_peak_freq_hz, float& snr, const EngineConfig& config) const
1314 : {
1315 839202 : AP_HAL::DSP::FrequencyPeakData* peak_data = &_state->_peak_data[peak];
1316 :
1317 839202 : const uint16_t bin = peak_data->_bin;
1318 :
1319 : // calculate the SNR and center frequency energy
1320 839202 : const float max_energy = MAX(1.0f, _state->get_freq_bin(bin));
1321 839202 : const float ref_energy = MAX(1.0f, _ref_energy[bin][_update_axis]);
1322 839202 : snr = 10.f * (log10f(max_energy) - log10f(ref_energy));
1323 :
1324 : // if the bin energy is above the noise threshold then we have a signal
1325 839202 : if (!_thread_state._noise_needs_calibration && isfinite(_state->get_freq_bin(bin)) && snr > config._snr_threshold_db) {
1326 632245 : weighted_peak_freq_hz = constrain_float(peak_data->_freq_hz, (float)config._fft_min_hz, (float)config._fft_max_hz);
1327 632245 : return true;
1328 : }
1329 :
1330 206957 : weighted_peak_freq_hz = (float)config._fft_min_hz;
1331 :
1332 206957 : return false;
1333 : }
1334 :
1335 : // calculate a matrix of distances between the current filtered estimates and instantaneous values from the current cycle
1336 279734 : void AP_GyroFFT::find_distance_matrix(DistanceMatrix& distance_matrix, const FrequencyData& freqs, const EngineConfig& config) const
1337 : {
1338 : float curr_freqs[FrequencyPeak::MAX_TRACKED_PEAKS];
1339 : // get the current frequency estimate for all peaks
1340 1118936 : for (uint8_t i = 0; i < FrequencyPeak::MAX_TRACKED_PEAKS; i++) {
1341 839202 : curr_freqs[i] = get_tl_noise_center_freq_hz(FrequencyPeak(i), _update_axis);
1342 : }
1343 : // calculate the matrix
1344 1118936 : for (uint8_t i = 0; i < FrequencyPeak::MAX_TRACKED_PEAKS; i++) {
1345 3356808 : for (uint8_t j = 0; j < FrequencyPeak::MAX_TRACKED_PEAKS; j++) {
1346 2517606 : distance_matrix[i][j] = fabsf((freqs.is_valid(FrequencyPeak(i)) ?
1347 2517606 : freqs.get_weighted_frequency(FrequencyPeak(i)) : FLT_MAX) - curr_freqs[j]);
1348 : }
1349 : }
1350 279734 : }
1351 :
1352 : // return the instantaneous peak that is closest to the target estimate peak
1353 839202 : AP_GyroFFT::FrequencyPeak AP_GyroFFT::find_closest_peak(const FrequencyPeak target, const DistanceMatrix& distance_matrix, uint8_t ignore) const
1354 : {
1355 : // find the closest peak to target
1356 839202 : uint8_t closest = target;
1357 3356808 : for (uint8_t i = 0; i < FrequencyPeak::MAX_TRACKED_PEAKS; i++) {
1358 2517606 : if (distance_matrix[i][target] < distance_matrix[closest][target] && (1 << i & ~ignore)) {
1359 111629 : closest = i;
1360 : }
1361 : }
1362 : // didn't find anything
1363 839202 : if (!(1<<closest & ~ignore)) {
1364 41347 : return FrequencyPeak::NONE;
1365 : }
1366 797855 : return FrequencyPeak(closest);
1367 : }
1368 :
1369 : // calculate noise baseline from FFT data provided by the HAL subsystem
1370 : // called from FFT thread
1371 278556 : void AP_GyroFFT::update_ref_energy(uint16_t max_bin)
1372 : {
1373 278556 : if (!_thread_state._noise_needs_calibration) {
1374 277980 : return;
1375 : }
1376 :
1377 : // according to https://www.tcd.ie/Physics/research/groups/magnetism/files/lectures/py5021/MagneticSensors3.pdf sensor noise is not necessarily gaussian
1378 : // determine a PS noise reference at each of the possible center frequencies
1379 576 : if (_noise_cycles == 0 && _noise_calibration_cycles[_update_axis] > 0) {
1380 15360 : for (uint16_t i = 1; i < _state->_bin_count; i++) {
1381 14976 : _ref_energy[i][_update_axis] += _state->get_freq_bin(i);
1382 : }
1383 384 : if (--_noise_calibration_cycles[_update_axis] == 0) {
1384 2208 : for (uint16_t i = 1; i < _state->_bin_count; i++) {
1385 2148 : const float cycles = (static_cast<float>(_window_size) / static_cast<float>(_samples_per_frame)) * 2;
1386 : // overall random noise is reduced by sqrt(N) when averaging periodigrams so adjust for that
1387 2148 : _ref_energy[i][_update_axis] = (_ref_energy[i][_update_axis] / cycles) * sqrtf(cycles);
1388 : }
1389 :
1390 60 : WITH_SEMAPHORE(_sem);
1391 60 : _thread_state._noise_needs_calibration &= ~(1 << _update_axis);
1392 60 : }
1393 384 : }
1394 192 : else if (_noise_cycles > 0) {
1395 192 : _noise_cycles--;
1396 : }
1397 : }
1398 :
1399 : // perform FFT analysis on the range of frequencies supported by the analyser
1400 : // called from main thread
1401 20 : float AP_GyroFFT::self_test_bin_frequencies()
1402 : {
1403 20 : if (_state->_window_size * sizeof(float) > hal.util->available_memory() / 2) {
1404 0 : GCS_SEND_TEXT(MAV_SEVERITY_WARNING, "FFT: unable to run self-test, required %u bytes", (unsigned int)(_state->_window_size * sizeof(float)));
1405 0 : return 0.0f;
1406 : }
1407 :
1408 20 : FloatBuffer test_window(_state->_window_size);
1409 : // in the unlikely event we can't allocate a test window, skip the checks
1410 20 : if (test_window.get_size() == 0) {
1411 0 : return 0.0f;
1412 : }
1413 :
1414 20 : float max_divergence = 0;
1415 :
1416 609 : for (uint16_t bin = _config._fft_start_bin; bin <= _config._fft_end_bin; bin++) {
1417 : // the algorithm will only ever return values in this range
1418 589 : float frequency = constrain_float(bin * _state->_bin_resolution, _fft_min_hz, _fft_max_hz);
1419 589 : max_divergence = MAX(max_divergence, self_test(frequency, test_window)); // test bin centers
1420 589 : frequency = constrain_float(bin * _state->_bin_resolution - _state->_bin_resolution / 4, _fft_min_hz, _fft_max_hz);
1421 589 : max_divergence = MAX(max_divergence, self_test(frequency, test_window)); // test bin off-centers
1422 : }
1423 :
1424 20 : return max_divergence;
1425 20 : }
1426 :
1427 : // perform FFT analysis of a single sine wave at the selected frequency
1428 : // called from main thread
1429 1178 : float AP_GyroFFT::self_test(float frequency, FloatBuffer& test_window)
1430 : {
1431 1178 : test_window.clear();
1432 124954 : for(uint16_t i = 0; i < _state->_window_size; i++) {
1433 123776 : if (!test_window.push(sinf(2.0f * M_PI * frequency * i / _fft_sampling_rate_hz) * radians(20) * 2000)) {
1434 0 : AP_HAL::panic("Could not create FFT test window");
1435 : }
1436 : }
1437 :
1438 1178 : _update_axis = 0;
1439 :
1440 : // if using averaging we need to process _num_frames in order to not bias the result
1441 2690 : for (uint8_t i = 1; i < _num_frames; i++) {
1442 1512 : hal.dsp->fft_start(_state, test_window, 0);
1443 1512 : hal.dsp->fft_analyse(_state, _config._fft_start_bin, _config._fft_end_bin, _config._attenuation_cutoff);
1444 : }
1445 : // final cycle is the one we want
1446 1178 : hal.dsp->fft_start(_state, test_window, 0);
1447 1178 : uint16_t max_bin = hal.dsp->fft_analyse(_state, _config._fft_start_bin, _config._fft_end_bin, _config._attenuation_cutoff);
1448 :
1449 1178 : if (max_bin == 0) {
1450 0 : GCS_SEND_TEXT(MAV_SEVERITY_WARNING, "FFT: self-test failed, failed to find frequency %.1f", frequency);
1451 : }
1452 :
1453 1178 : calculate_noise(true, _config);
1454 :
1455 1178 : float max_divergence = 0;
1456 : // make sure the selected frequencies are in the right bin
1457 1178 : max_divergence = MAX(max_divergence, fabsf(frequency - _thread_state._center_freq_hz[0]));
1458 1178 : if (_thread_state._center_freq_hz[0] < (frequency - MAX(_state->_bin_resolution * 0.5f, 1)) || _thread_state._center_freq_hz[0] > (frequency + MAX(_state->_bin_resolution * 0.5f, 1))) {
1459 0 : GCS_SEND_TEXT(MAV_SEVERITY_WARNING, "FFT: self-test failed: wanted %.1f, had %.1f", frequency, _thread_state._center_freq_hz[0]);
1460 : }
1461 : #if DEBUG_FFT
1462 : else {
1463 : GCS_SEND_TEXT(MAV_SEVERITY_INFO, "FFT: self-test succeeded: wanted %.1f, had %.1f", frequency, _thread_state._center_freq_hz[0]);
1464 : }
1465 : #endif
1466 :
1467 1178 : return max_divergence;
1468 : }
1469 :
1470 : // singleton instance
1471 : AP_GyroFFT *AP_GyroFFT::_singleton;
1472 :
1473 : namespace AP {
1474 :
1475 50265 : AP_GyroFFT *fft()
1476 : {
1477 50265 : return AP_GyroFFT::get_singleton();
1478 : }
1479 :
1480 : }
1481 :
1482 : #endif // HAL_GYROFFT_ENABLED
|