Example program for EVAL-CN0540-ARDZ.
Dependencies: platform_drivers LTC26X6 AD77681
Diff: CN0540_FFT/cn0540_adi_fft.c
- Revision:
- 1:9dd7c64b4a64
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/CN0540_FFT/cn0540_adi_fft.c Mon Dec 06 05:22:28 2021 +0000 @@ -0,0 +1,483 @@ +/****************************************************************************** + *Copyright (c)2020 Analog Devices, Inc. + * + * Licensed under the 2020-04-27-CN0540EC License(the "License"); + * you may not use this file except in compliance with the License. + * + ****************************************************************************/ + +#include "cn0540_adi_fft.h" +#include "cn0540_windowing.h" +#include "stdio.h" +#include "stdlib.h" +#include <math.h> +#include <stdbool.h> + +static arm_cfft_radix4_instance_f32 S; + +/** + * Initialize the FFT structure + * @param **fft_entry_init - the FFT data structure + * @param **fft_meas - the FFT measurements structure + */ +int32_t FFT_init_params(struct fft_entry **fft_entry_init, struct fft_measurements **fft_meas) +{ + struct fft_entry *fft_data_init; + struct fft_measurements *fft_meas_init; + + fft_data_init = (struct fft_entry *)malloc(sizeof(*fft_data_init)); + if (!fft_data_init) { + return -1; + } + fft_meas_init = (struct fft_measurements *)malloc(sizeof(*fft_meas_init)); + if (!fft_meas_init) { + return -1; + } + fft_data_init->window = BLACKMAN_HARRIS_7TERM; + fft_data_init->vref = (float)(4096); + fft_data_init->sample_rate = 32000; + fft_data_init->mclk = 16384; + fft_data_init->fft_length = 4096; + fft_data_init->bin_width = 0.0; + fft_data_init->fft_done = false; + + *fft_entry_init = fft_data_init; + + fft_meas_init->fundamental = 0.0; + fft_meas_init->pk_spurious_noise = 0.0; + fft_meas_init->pk_spurious_freq = 0; + fft_meas_init->THD = 0.0; + fft_meas_init->SNR = 0.0; + fft_meas_init->DR = 0.0; + fft_meas_init->SINAD = 0.0; + fft_meas_init->SFDR_dbc = 0.0; + fft_meas_init->SFDR_dbfs = 0.0; + fft_meas_init->ENOB = 0.0; + fft_meas_init->RMS_noise = 0.0; + fft_meas_init->average_bin_noise = 0.0; + fft_meas_init->max_amplitude = 0.0; + fft_meas_init->min_amplitude = 0.0; + fft_meas_init->pk_pk_amplitude = 0.0; + fft_meas_init->DC = 0.0; + fft_meas_init->transition_noise = 0.0; + fft_meas_init->max_amplitude_LSB = 0; + fft_meas_init->min_amplitude_LSB = 0; + fft_meas_init->pk_pk_amplitude_LSB = 0; + fft_meas_init->DC_LSB = 0; + fft_meas_init->transition_noise_LSB = 0.0; + + for (uint8_t i = 0; i < 7; i++) { + fft_meas_init->harmonics_mag_dbfs[i] = 0.0; + fft_meas_init->harmonics_freq[i] = 0; + fft_meas_init->harmonics_power[i] = 0.0; + } + + *fft_meas = fft_meas_init; + + return 0; +} + +/** + * Initialize the FFT module + * The funciton has to be called, everytime when user wants to change the number of samples + * @param sample_count - desired FFT samples + * @param *fft_data - fft_data structure + */ +void FFT_init(uint16_t sample_count, struct fft_entry *fft_data) +{ + arm_cfft_radix4_init_f32(&S, sample_count, 0, 1); + fft_data->fft_length = sample_count / 2; +} + +/** + * Update reference voltage and Master clock + * The funciton has to be called, everytime when user wants to change Vref or Mclk + * @param referemce - The reference voltage in mV + * @param master_clock - The master clock frequnecy in kHz + * @param sampling_rate - The samplingrate frequnecy in kHz + * @param *fft_data - fft_data structure + */ +void update_FFT_enviroment(uint16_t reference, uint16_t master_clock, uint16_t sampling_rate, struct fft_entry *fft_data) +{ + fft_data->vref = ((float)(reference)) / ((float)(1000.0)); // Convert reference voltage to V + fft_data->mclk = master_clock; // MCLK in kHz + fft_data->sample_rate = sampling_rate; // Sampling rate +} + + + + +/** + * Perform the FFT + * @param *data - pointer to sampled data + * @param *fft_data - fft_data structure + * @param *fft_meas - fft_meas structure + * @param sample_rate - sample rate based on MCLK, MCLK_DIV and Digital filter settings + */ +void perform_FFT(uint32_t *data, struct fft_entry *fft_data, struct fft_measurements *fft_meas, uint32_t sample_rate) +{ + uint32_t i; + int32_t shifted_data = 0; + double coeffs_sum = 0.0; + + fft_data->fft_done = false; + fft_data->sample_rate = sample_rate; // get sample rate + fft_data->bin_width = (float)(fft_data->sample_rate) / ((float)(fft_data->fft_length)*2); // get bin width + + + //Converting RAW adc data to codes + for(i = 0 ; i < fft_data->fft_length * 2 ; i++) { + if (data[i] & 0x800000) + shifted_data = (int32_t)((0xFF << 24) | data[i]); + else + shifted_data = (int32_t)((0x00 << 24) | data[i]); + + fft_data->codes[i] = shifted_data; // Codes in range 0 +/- ZERO SCALE + fft_data->zero_scale_codes[i] = shifted_data + ADC_ZERO_SCALE; // Codes shifted up by ZERO SCALE - range ZERO SCALE +/- ZERO SCALE + } + // Find max, min, pk-pk amplitude, DC offset, Tranition noise + FFT_waveform_stat(fft_data, fft_meas); + + // Converting codes without DC offset to "volts" without respect to Vref voltage + for (i = 0; i < fft_data->fft_length * 4; i++) { + // Filling array for FFT, conversion to voltage withour respect to Vref voltage + fft_data->fft_input[i] = ((((float)(fft_data->codes[i / 2])) / ADC_ZERO_SCALE)); + + // Voltage array with respect to full scale voltage, Vpp + fft_data->voltage[i / 2] = (float)((2*fft_data->vref / ADC_ZERO_SCALE) * fft_data->codes[i / 2]); + + // Imaginary part + fft_data->fft_input[++i] = 0; + } + // Apply windowing + FFT_windowing(fft_data, &coeffs_sum); + //perform FFT, passing the input array, complex FFT values will be stored to the iput array + arm_cfft_radix4_f32(&S, fft_data->fft_input); + //transform from complex FFT to magnitude + arm_cmplx_mag_f32(fft_data->fft_input, fft_data->fft_magnitude, fft_data->fft_length); + // Convert FFT magnitude to dB for plot + FFT_maginutde_do_dB(fft_data, coeffs_sum); + //Calculate THD + FFT_calculate_THD(fft_data, fft_meas); + // Calculate noise and its parameters from FFT points + FFT_calculate_noise(fft_data, fft_meas); + // FFT finish flag + fft_data->fft_done = true; +} + +/** + * Windowing function + * 7-term Blackman-Harris and Rectangular widow for now, prepared for more windowing functions if needed + * You can use precalculated coeficients for 4096 sample length + * @param *sample - pointer to sample + * @param sample_length - 2 * FFT_length, because working with samples, not with FFT yet + * @param *sum - pointer to sum of all the coeffs + * + */ +void static FFT_windowing(struct fft_entry *fft_data, double *sum) +{ + uint8_t j; + uint16_t i; + double term = 0.0; + const double sample_count = (double)((fft_data->fft_length * 2) - 1); + + for (i = 0; i < fft_data->fft_length * 4; i++) + { + switch (fft_data->window) { // Switch prepard for other windowing functions + case BLACKMAN_HARRIS_7TERM: + if (fft_data->fft_length == 2048) // For 4096 samples, precalculated coefficients are used + term = Seven_Term_Blackman_Harris_4096[i / 2]; + else { // 7-term BH windowing formula + for (j = 0; j < 7; j++) + term += seven_term_BH_coefs[j] * cos((double)((2.0 * PI * j * (i / 2))) / sample_count); + } + break; + case RECTANGULAR: // No window, all terms = 1 + term = 1; + break; + default: + break; + } + *sum += term; // Getting sum of all terms, which will be used for amplitude correction + fft_data->fft_input[i] *= (float)(term); // Multiplying each (real) sample by windowing term + term = 0.0; + i++; // +1, to consider only real (not imaginary) samples + } +} + + +/** + * Transfer magnitude to dB + * @param *fft_data - fft_data structure + * @param sum - sum of all windowing coeffs + */ +void static FFT_maginutde_do_dB(struct fft_entry *fft_data, double sum) +{ + uint16_t i; + float correction = 0; + + // Getting sum of coeffs + // If rectangular window is choosen = no windowing, sum of coeffs is number of samples + const float coeff_sum = ((fft_data->fft_length == 2048) && + (fft_data->window == BLACKMAN_HARRIS_7TERM)) ? ((float)(Seven_Term_Blackman_Harris_4096_sum)) : + ((fft_data->window == RECTANGULAR) ? ((float)(fft_data->fft_length * 2.0)) : (float)(sum)); + + for (i = 0; i < fft_data->fft_length; i++) { + // Apply a correction factor + // Divide magnigude by a sum of the windowing function coefficients + // Multiple by 2 because of power spread over spectrum below and above the Nyquist frequency + correction = (fft_data->fft_magnitude[i] * 2.0) / coeff_sum; + + // FFT magnitude with windowing correction + fft_data->fft_magnitude_corrected[i] = correction; + + //Convert to dB without respect to Vref + fft_data->fft_dB[i] = 20.0 * (log10f(correction)); + } +} + +/** + * THD Calculation with support of harmonics folding to 1-st nyquist zone + * @param *fft_data - fft_data structure + * @param *fft_meas - fft_meas structure + */ +void static FFT_calculate_THD(struct fft_entry *fft_data, struct fft_measurements *fft_meas) +{ + const uint16_t first_nyquist_zone = fft_data->fft_length; + uint16_t i, j, k = 0, fund_freq = 0, harmonic_position; + int8_t m, nyquist_zone; + float mag_helper = -200.0, freq_helper, sum = 0.0, fund_mag = -200.0; + float fund_pow_bins[21], harm_pow_bins[5][7]; + + // Looking for the fundamental frequency and amplitude + for(i = DC_BINS ; i < fft_data->fft_length ; i++) { // Not counting DC bins + if (fft_data->fft_dB[i] > fund_mag) { + fund_mag = fft_data->fft_dB[i]; + fund_freq = i; + } + } + + fft_meas->harmonics_freq[0] = fund_freq; // Fundamental frequency bin + fft_meas->harmonics_mag_dbfs[0] = fund_mag; // Fundamental magnitude in dBFS + fft_meas->fundamental = dbfs_to_volts(fft_data->vref, fund_mag); // Fundamental magnitude in V + + for(i = 1 ; i < 6 ; i++) { + if (fft_meas->harmonics_freq[0] * (i + 1) < first_nyquist_zone) // Checking if 2nd harmonic is outside of the first nyquist zone + harmonic_position = fft_meas->harmonics_freq[0] * (i + 1); + else { + nyquist_zone = 1 + (fft_meas->harmonics_freq[0] * (i + 1) / first_nyquist_zone); // Determine the nyquist zone + if(nyquist_zone % 2) // Odd nyquist zones: 3, 5, 7... + harmonic_position = first_nyquist_zone - (first_nyquist_zone * nyquist_zone - fft_meas->harmonics_freq[0] * (i + 1)); + else // Even nyquist zones: 2, 4, 6... + harmonic_position = first_nyquist_zone * nyquist_zone - fft_meas->harmonics_freq[0] * (i + 1); + } + // Extend searching range by 3 bins around expected position of the harmonic + for(m = -HARM_BINS ; m <= HARM_BINS ; m++) { + if (fft_data->fft_dB[harmonic_position + m] > mag_helper) { + mag_helper = fft_data->fft_dB[harmonic_position + m]; + freq_helper = (harmonic_position + m); + } + } + + fft_meas->harmonics_freq[i] = freq_helper; + fft_meas->harmonics_mag_dbfs[i] = mag_helper; + mag_helper = -200.0; + } + // Power leakage of the fundamental + for(i = fft_meas->harmonics_freq[0] - FUND_BINS ; i <= fft_meas->harmonics_freq[0] + FUND_BINS ; i++) { + sum += powf(((fft_data->fft_magnitude_corrected[i] / (2.0*SQRT_2))), 2.0); + fund_pow_bins[k] = fft_data->fft_magnitude_corrected[i]; + k++; + } + // Finishing the RSS of power-leaked fundamental + sum = sqrt(sum); + fft_meas->harmonics_power[0] = sum * 2.0 * SQRT_2; + sum = 0.0; + k = 0; + // Power leakage of the harmonics + for(j = 1 ; j <= 5 ; j++) { + for (i = fft_meas->harmonics_freq[j] - HARM_BINS; i <= fft_meas->harmonics_freq[j] + HARM_BINS; i++) { + sum += powf(((fft_data->fft_magnitude_corrected[i] / (2.0*SQRT_2))), 2.0); + harm_pow_bins[j - 1][k] = fft_data->fft_magnitude_corrected[i]; + k++; + } + // Finishing the RSS of power-leaked harmonics + k = 0; + sum = sqrt(sum); + fft_meas->harmonics_power[j] = sum * 2.0 * SQRT_2; + sum = 0.0; + } + // The THD formula + fft_meas->THD = sqrtf(powf(fft_meas->harmonics_power[1], 2.0) + powf(fft_meas->harmonics_power[2], 2.0) + powf(fft_meas->harmonics_power[3], 2.0) + powf(fft_meas->harmonics_power[4], 2.0) + powf(fft_meas->harmonics_power[5], 2.0)) / fft_meas->harmonics_power[0]; + // Back from volts to dB + fft_meas->THD = 20.0 * log10f(fft_meas->THD); +} + +/** + * Calculate amplitudes: min, max, pk-pk amplitude and DC part + * @param *fft_data - fft_data structure + * @param *fft_meas - fft_meas structure + */ +void static FFT_waveform_stat(struct fft_entry *fft_data, struct fft_measurements *fft_meas) +{ + uint16_t i; + int16_t max_position, min_position; + int32_t max = -ADC_ZERO_SCALE, min = ADC_ZERO_SCALE, offset_correction; + int64_t sum = 0; + double deviation = 0.0, mean; + + // summ of all coeffs, to find the Mean value + for(i = 0; i < fft_data->fft_length * 2; i++) + sum += fft_data->codes[i]; + + // Calculating mean value = DC offset + mean = (sum / (fft_data->fft_length * 2)); + + // DC part in LSBs + fft_meas->DC_LSB = (int32_t)(mean) + ADC_ZERO_SCALE; + offset_correction = (int32_t)(mean); + + // Min, Max amplitudes + Deviation + for (i = 0; i < fft_data->fft_length * 2; i++) { // Working with codes = fft_length * 2 + // Calculating the Deviation for Transition noise + deviation += pow(fft_data->codes[i] - mean, 2.0); + + // Looking for MAX value + if (fft_data->codes[i] > max) { + max = fft_data->codes[i]; + max_position = i; + } + // Looking for MIN value + if (fft_data->codes[i] < min) { + min = fft_data->codes[i]; + min_position = i; + } + } + // Amplitudes in Volts + fft_meas->max_amplitude = (2.0 * fft_data->vref * fft_data->codes[max_position]) / ADC_FULL_SCALE; + fft_meas->min_amplitude = (2.0 * fft_data->vref * fft_data->codes[min_position]) / ADC_FULL_SCALE; + fft_meas->pk_pk_amplitude = fft_meas->max_amplitude - fft_meas->min_amplitude; + fft_meas->DC = (2.0 * fft_data->vref * ((float)(((int32_t)(fft_meas->DC_LSB) - ADC_ZERO_SCALE)))) / ADC_FULL_SCALE; + + // Amplitudes in LSBs + fft_meas->max_amplitude_LSB = fft_data->codes[max_position] + ADC_ZERO_SCALE; + fft_meas->min_amplitude_LSB = fft_data->codes[min_position] + ADC_ZERO_SCALE; + fft_meas->pk_pk_amplitude_LSB = fft_meas->max_amplitude_LSB - fft_meas->min_amplitude_LSB; + + // Transition noise + deviation = (sqrt(deviation / (fft_data->fft_length * 2.0))); + fft_meas->transition_noise_LSB = (uint32_t)(deviation); + fft_meas->transition_noise = (2.0 * fft_data->vref * fft_meas->transition_noise_LSB) / ADC_FULL_SCALE; + + // RMS noise + fft_meas->RMS_noise = fft_meas->transition_noise; + + // Applying mean value to each sample = removing DC offset + for(i = 0 ; i < fft_data->fft_length * 2 ; i++) + fft_data->codes[i] -= offset_correction; + +} + +/** + * Calculate the RMS noise from the FFT plot + * @param *fft_data - fft_data structure + * @param *fft_meas - fft_meas structure + */ +void static FFT_calculate_noise(struct fft_entry *fft_data, struct fft_measurements *fft_meas) +{ + const float LW_DR_correction_const = 4.48; // Magic constant from the LabView FFT core correcting only dynamic range + uint16_t i, j; + float biggest_spur = -300; + double RSS = 0.0, mean = 0.0; + + // Initalizing pk_spurious variables + fft_meas->pk_spurious_noise = -200.0; + fft_meas->pk_spurious_freq = 0; + + for (i = 0; i < DC_BINS; i++) // Ignoring DC bins + fft_data->noise_bins[i] = 0.0; + for (i = DC_BINS; i < fft_data->fft_length; i++) { + // Ignoring spread near the fundamental + if ((i <= fft_meas->harmonics_freq[0] + FUND_BINS) && (i >= fft_meas->harmonics_freq[0] - FUND_BINS)) + fft_data->noise_bins[i] = 0.0; + + else if((i <= fft_meas->harmonics_freq[1] + HARM_BINS) && (i >= fft_meas->harmonics_freq[1] - HARM_BINS)) // Ignoring spread near harmonics + fft_data->noise_bins[i] = 0.0; + + else if((i <= fft_meas->harmonics_freq[2] + HARM_BINS) && (i >= fft_meas->harmonics_freq[2] - HARM_BINS)) + fft_data->noise_bins[i] = 0.0; + + else if((i <= fft_meas->harmonics_freq[3] + HARM_BINS) && (i >= fft_meas->harmonics_freq[3] - HARM_BINS)) + fft_data->noise_bins[i] = 0.0; + + else if((i <= fft_meas->harmonics_freq[4] + HARM_BINS) && (i >= fft_meas->harmonics_freq[4] - HARM_BINS)) + fft_data->noise_bins[i] = 0.0; + + else if((i <= fft_meas->harmonics_freq[5] + HARM_BINS) && (i >= fft_meas->harmonics_freq[5] - HARM_BINS)) + fft_data->noise_bins[i] = 0.0; + + else { + // Root Sum Square = RSS for noise calculations + fft_data->noise_bins[i] = fft_data->fft_magnitude_corrected[i]; + RSS += pow(((double)(fft_data->fft_magnitude_corrected[i] / (2.0*SQRT_2))), 2.0); + + // Average bin noise + mean += fft_data->fft_magnitude_corrected[i]; + + // Peak spurious amplitude + if(fft_data->fft_magnitude_corrected[i] > fft_meas->pk_spurious_noise) { + fft_meas->pk_spurious_noise = fft_data->fft_magnitude_corrected[i]; + fft_meas->pk_spurious_freq = i; + } + + } + } + mean /= (double)(fft_data->fft_length); + + // RSS of FFT spectrum without DC, Fund. and Harmonics + RSS = sqrt(RSS); + RSS = RSS * 2.0 * SQRT_2; + + // Peak spurious amplitude = Highest amplitude excluding DC, the Fundamental and the Harmonics + fft_meas->pk_spurious_noise = 20.0 * log10f(1.0 / fft_meas->pk_spurious_noise); + + // Looking for the biggest spur among harmonics + for(i = 1 ; i < 6 ; i++) { + if (fft_meas->harmonics_mag_dbfs[i] > biggest_spur) + biggest_spur = fft_meas->harmonics_mag_dbfs[i]; + } + // Looking for the biggest spur among harmonics and pk_spurious_noise + if(biggest_spur > fft_meas->pk_spurious_noise) + biggest_spur = fft_meas->pk_spurious_noise; + + // Spurious Free Dynamic Range SFDR related to the carrer = biggest spur - the Fundamental, [dBc] - Decibels related to the carrier + fft_meas->SFDR_dbc = biggest_spur - fft_meas->harmonics_mag_dbfs[0]; + + // Spurious Free Dynamic Range SFDR related to the full-scale = biggest spur - full-scale [dBFS], where full-scale is 0 dBFS + fft_meas->SFDR_dbfs = biggest_spur; + + // Average bin noise = Mean value of FFT spectrum excluding DC, the Fundamental and the Harmonics + fft_meas->average_bin_noise = (float)(20.0 * log10(mean)); + + // DR = 1 / RSS of FFT spectrum without DC, Fund. and Harmonics + Magic constant from the Labview FFT core + fft_meas->DR = (20.0 * log10f(1.0 / (float)(RSS))) + LW_DR_correction_const; + + // SNR = Power of the fundamental / RSS of FFT spectrum without DC, Fund. and Harmonics + fft_meas->SNR = 20.0 * log10f((fft_meas->harmonics_power[0]) / (RSS)); + + // SINAD + fft_meas->SINAD = -10.0 * log10f(powf(10.0, (fabs(fft_meas->SNR))*(-1.0) / 10.0) + powf(10.0, fabs(fft_meas->THD)*(-1.0) / 10.0)); + + // ENOB - Effective number of bits + fft_meas->ENOB = (fft_meas->SINAD - 1.67 + fabs(fft_meas->harmonics_mag_dbfs[0])) / 6.02; +} + +/** + * Convert dBFS to volts in Pk-Pk + * @param vref - reference voltage in volts + * @param *fft_meas - fft_meas structure + */ +float static dbfs_to_volts(float vref, float value) +{ + return ( 2 * vref * powf(10.0, value / 20.0) ); +}