Example program for EVAL-CN0540-ARDZ.

Dependencies:   platform_drivers LTC26X6 AD77681

CN0540_FFT/cn0540_adi_fft.c

Committer:
jngarlitos
Date:
2021-12-06
Revision:
1:9dd7c64b4a64

File content as of revision 1:9dd7c64b4a64:

/******************************************************************************
 *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) );
}