第二十九章 快速傅里叶变换加速器实验
本章将介绍Kendryte K210的FFT模块的使用,通过傅里叶变换实验帮助读者了解FFT相关知识。通过本章的学习,读者将会对Kendryte K210内置的硬件FFT模块有初步认知和应用能力。 本章分为如下几个小节: 29.1 FFT介绍 29.2 硬件设计 29.3 程序设计 29.4 运行验证
29.1 FFT介绍 FFT即快速傅里叶变换,可以将一个时域信号变换到频域。因为有些信号在时域上是很难看出什么特征的,但是如果变换到频域之后,就很容易看出特征了,这就是很多信号分析采用FFT变换的原因。另外,FFT可以将一个信号的频谱提取出来,这在频谱分析方面也是经常用的。简而言之,FFT就是将一个信号从时域变换到频域方便我们分析处理。 在实际应用中,一般的处理过程是先对一个信号在时域进行采集,比如我们通过ADC,按照一定大小采样频率F去采集信号,采集N个点,那么通过对这N个点进行FFT运算,就可以得到这个信号的频谱特性。 这里还涉及到一个采样定理的概念:在进行模拟/数字信号的转换过程中,当采样频率F大于信号中最高频率fmax的2倍时(F>2*fmax),采样之后的数字信号完整地保留了原始信号中的信息,采样定理又称奈奎斯特定理。举个简单的例子:比如我们正常人发声,频率范围一般在8KHz以内,那么我们要通过采样之后的数据来恢复声音,我们的采样频率必须为8KHz的2倍以上,也就是必须大于16KHz才行。 模拟信号经过ADC采样之后,就变成了数字信号,采样得到的数字信号,就可以做FFT变换了。N个采样点数据,在经过FFT之后,就可以得到N个点的FFT结果。为了方便进行FFT运算,通常N取2的整数次方。 假设采样频率为F,对一个信号采样,采样点数为N,那么FFT之后结果就是一个N点的复数,每一个点就对应着一个频率点(以基波频率为单位递增),这个点的模值(sqrt(实部2+虚部2))就是该频点频率值下的幅度特性。具体跟原始信号的幅度有什么关系呢?假设原始信号的峰值为A,那么FFT的结果的每个点(除了第一个点直流分量之外)的模值就是A的N/2倍,而第一个点就是直流分量,它的模值就是直流分量的N倍。 这里还有个基波频率,也叫频率分辨率的概念,就是如果我们按照F的采样频率去采集一个信号,一共采集N个点,那么基波频率(频率分辨率)就是fk=F/N。这样,第n个点对应信号频率为:F*(n-1)/N;其中n≥1,当n=1时为直流分量。 关于FFT我们就简单介绍到这,大家可以通过网上进一步了解相关知识。 Kendryte K210内置快速傅里叶变换加速器FFT Accelerater。FFT加速器是用硬件的方式来实现 FFT 的基 2 时分运算,主要功能包括: 1. 支持多种运算长度,即支持64点、128点、256点以及512点运算 2. 支持两种运算模式,即FFT以及IFFT运算 3. 支持可配的输入数据位宽,即支持32位及64位输入 4. 支持可配的输入数据排列方式,即支持虚部、实部交替,纯实部以及实部、虚部分离三种数据排列方式 5. 支持DMA传输 Kendryte K210官方SDK提供了1个操作FFT的函数,这个函数介绍如下: 1, fft_complex_uint16_dma 函数 该函数主要用于FFT运算,该函数原型及参数描述如下所示: void fft_complex_uint16_dma(dmac_channel_number_t dma_send_channel_num, dmac_channel_number_t dma_receive_channel_num, uint16_t shift, fft_direction_t direction, const uint64_t *input, size_t point_num, uint64_t *output); /* FFT正变换或逆变换配置参数 */ typedef enum _fft_direction { FFT_DIR_BACKWARD, FFT_DIR_FORWARD, FFT_DIR_MAX, } fft_direction_t; 函数共有七个参数,第一个参数和第二个参数分别为DMA发送通道号和DMA接收通道号,第三个参数shift介绍比较复杂,官方文件的描述是“FFT 模块 16 位寄存器导致数据溢出 (-32768~32767),FFT 变换有 9 层,shift 决定哪一层需要移位操作 (如 0x1ff 表示 9 层都做移位操作;0x03 表示第第一层与第二层做移位操作),防止溢出。如果移位了,则变换后的幅值不是正常 FFT 变换的幅值,对应关系可以参考 fft_test 测试demo 程序。包含了求解频率点、相位、幅值的示例”,第四个参数为输入FFT 正变换或是逆变换,第五个参数为输入的数据序列,第六个参数为待运算的数据点数,只能为512/256/128/64,第七个参数为输出运算后的结果。 29.2 硬件设计 29.2.1 例程功能 1.程序对一组复数分别使用硬件FFT和软件FFT进行快速傅里叶正变换和快速傅里叶逆变换的运算,并同时记录运行消耗的时间,最后将数据通过串口打印。 29.2.2 硬件资源 1.USB接口 UARTHS_TX – IO5 UARTHS_RX – IO4 29.2.3 原理图 本章实验内容,主要讲解FFT模块的使用,无需关注原理图。 29.3 程序设计 29.3.1 main.c代码 本章例程均来自官方SDK的DEMO,本章仅简单介绍下maic.c文件部分函数的功能,代码如下所示: int main(void) { int32_t i; float tempf1[3]; fft_data_t *output_data; fft_data_t *input_data; uint16_t bit1_num = get_bit1_num(FFT_FORWARD_SHIFT); complex_hard_t data_hard[FFT_N] = {0}; complex data_soft[FFT_N] = {0}; for (i = 0; i < FFT_N; i++) { tempf1[0] = 0.3 * cosf(2 * PI * i / FFT_N + PI / 3) * 256; tempf1[1] = 0.1 * cosf(16 * 2 * PI * i / FFT_N - PI / 9) * 256; tempf1[2] = 0.5 * cosf((19 * 2 * PI * i / FFT_N) + PI / 6) * 256; data_hard.real = (int16_t)(tempf1[0] + tempf1[1] + tempf1[2] + 10); data_hard.imag = (int16_t)0; data_soft.real = data_hard.real; data_soft.imag = data_hard.imag; } for (int i = 0; i < FFT_N / 2; ++i) { input_data = (fft_data_t *)&buffer_input; input_data->R1 = data_hard[2 * i].real; input_data->I1 = data_hard[2 * i].imag; input_data->R2 = data_hard[2 * i + 1].real; input_data->I2 = data_hard[2 * i + 1].imag; } cycle[FFT_HARD][FFT_DIR_FORWARD] = read_cycle(); fft_complex_uint16_dma(DMAC_CHANNEL0, DMAC_CHANNEL1, FFT_FORWARD_SHIFT, FFT_DIR_FORWARD, buffer_input, FFT_N, buffer_output); cycle[FFT_HARD][FFT_DIR_FORWARD] = read_cycle() - cycle[FFT_HARD][FFT_DIR_FORWARD]; cycle[FFT_SOFT][FFT_DIR_FORWARD] = read_cycle(); fft_soft(data_soft, FFT_N); cycle[FFT_SOFT][FFT_DIR_FORWARD] = read_cycle() - cycle[FFT_SOFT][FFT_DIR_FORWARD]; for (i = 0; i < FFT_N / 2; i++) { output_data = (fft_data_t*)&buffer_output; data_hard[2 * i].imag = output_data->I1 ; data_hard[2 * i].real = output_data->R1 ; data_hard[2 * i + 1].imag = output_data->I2 ; data_hard[2 * i + 1].real = output_data->R2 ; } for (i = 0; i < FFT_N; i++) { hard_power = sqrt(data_hard.real * data_hard.real + data_hard.imag * data_hard.imag) * 2; soft_power = sqrt(data_soft.real * data_soft.real + data_soft.imag * data_soft.imag) * 2; } printf("\n[hard fft real][soft fft real][hard fft imag][soft fft imag]\n"); for (i = 0; i < FFT_N / 2; i++) printf("%3d:%7d %7d %7d %7d\n", i, data_hard.real, (int32_t)data_soft.real, data_hard.imag, (int32_t)data_soft.imag); printf("\nhard power soft power:\n"); printf("%3d : %f %f\n", 0, hard_power[0] / 2 / FFT_N * (1 << bit1_num), soft_power[0] / 2 / FFT_N); for (i = 1; i < FFT_N / 2; i++) printf("%3d : %f %f\n", i, hard_power / FFT_N * (1 << bit1_num), soft_power / FFT_N); printf("\nhard phase soft phase:\n"); for (i = 0; i < FFT_N / 2; i++) { hard_angel = atan2(data_hard.imag, data_hard.real); soft_angel = atan2(data_soft.imag, data_soft.real); printf("%3d : %f %f\n", i, hard_angel * 180 / PI, soft_angel * 180 / PI); } for (int i = 0; i < FFT_N / 2; ++i) { input_data = (fft_data_t *)&buffer_input; input_data->R1 = data_hard[2 * i].real; input_data->I1 = data_hard[2 * i].imag; input_data->R2 = data_hard[2 * i + 1].real; input_data->I2 = data_hard[2 * i + 1].imag; } cycle[FFT_HARD][FFT_DIR_BACKWARD] = read_cycle(); fft_complex_uint16_dma(DMAC_CHANNEL0, DMAC_CHANNEL1, FFT_BACKWARD_SHIFT, FFT_DIR_BACKWARD, buffer_input, FFT_N, buffer_output); cycle[FFT_HARD][FFT_DIR_BACKWARD] = read_cycle() - cycle[FFT_HARD][FFT_DIR_BACKWARD]; cycle[FFT_SOFT][FFT_DIR_BACKWARD] = read_cycle(); ifft_soft(data_soft, FFT_N); cycle[FFT_SOFT][FFT_DIR_BACKWARD] = read_cycle() - cycle[FFT_SOFT][FFT_DIR_BACKWARD]; for (i = 0; i < FFT_N / 2; i++) { output_data = (fft_data_t*)&buffer_output; data_hard[2 * i].imag = output_data->I1 ; data_hard[2 * i].real = output_data->R1 ; data_hard[2 * i + 1].imag = output_data->I2 ; data_hard[2 * i + 1].real = output_data->R2 ; } printf("\n[hard ifft real][soft ifft real][hard ifft imag][soft ifft imag]\n"); for (i = 0; i < FFT_N / 2; i++) printf("%3d:%7d %7d %7d %7d\n", i, data_hard.real, (int32_t)data_soft.real, data_hard.imag, (int32_t)data_soft.imag); printf("[hard fft test] [%d bytes] forward time = %ld us, backward time = %ld us\n", FFT_N, cycle[FFT_HARD][FFT_DIR_FORWARD]/(sysctl_clock_get_freq(SYSCTL_CLOCK_CPU)/1000000), cycle[FFT_HARD][FFT_DIR_BACKWARD]/(sysctl_clock_get_freq(SYSCTL_CLOCK_CPU)/1000000)); printf("[soft fft test] [%d bytes] forward time = %ld us, backward time = %ld us\n", FFT_N, cycle[FFT_SOFT][FFT_DIR_FORWARD]/(sysctl_clock_get_freq(SYSCTL_CLOCK_CPU)/1000000), cycle[FFT_SOFT][FFT_DIR_BACKWARD]/(sysctl_clock_get_freq(SYSCTL_CLOCK_CPU)/1000000)); while (1) ; return 0; } 可以看到,mian函数首先通过三角函数获得一组复数,然后将获得的复数转化成快速傅里叶变换的数据结构,作为输入的数据(待计算的数据),再分别用Kendryte K210的硬件FFT和软件FFT进行快速傅里叶变换的运算,同时记录各自运行所消耗的时间,然后对输出数据进行取模操作,接着是打印复数的实部和虚部的数据、模和相位等信息 ,接下来是进行快速傅里叶变换的逆运算,也就是将刚刚计算得到的值作为输入数据进行逆变换,和上面一样,分别使用硬件FFT和软件FFT进行快速傅里叶逆变换,同时记录运行所消耗的时间,最后将数据打印出来。 29.4 运行验证 将DNK210开发板连接到电脑主机,通过VSCode将固件烧录到开发板中,我们打开“串口终端”,可以看到串口中断不断的打印数据,如下图所示: 图29.4.1 “串行终端”窗口打印输出 由此可知,Kendryte K210内置的硬件FFT(快速傅里叶变换)性能非常出色,这赋予了它在信号处理和分析应用中显著的竞争优势。
|