环境:win7 64 vivado 2014.1
开发板:zedboard version d xc7z020clg484-1
串口软件:SecureCRT
目标:对一段随机的音频信号进行实时频谱分析。从pc获取音频信号,经由PL的fft IP处理后送入OLED,进行音频频谱的实时显示。通过本实例学习vivado+zedboard软硬件设计的方法,学习控制zedboard外设的方法。本文在商品博客的基础上,把fft函数改为fft ip,实现相同的功能,对部分函数进行优化。
说明:本文的fft算法部分采用硬件实现的,使用的是vivado自带的ip,参考ug871。
注意:本文中所有的源码、工程文件在“我的资源”中可以找到,如果没有请联系作者本人。转载请注明出处。
正文:
本文将分为以下步骤:
1. 使用Vivado 建一个工程,添加oled、audio_ctrl等IP,完成硬件设计。generate,最后导入到SDK中
2. 在SDK中新建工程,添加文件,编译。下载到ZedBoard上进行调试
3. 程序分析
4. 总结
1. 使用Vivado 建一个工程,添加oled、audio_ctrl等IP,完成硬件设计。generate,最后导入到SDK中
1)新建目录audio_fre_hw,在其下新建vivado工程。选择zedboard开发板。
2)将ip_repo拷贝到audio_fre_hw/下。打开IP catalog目录,选择IP setting,添加/audio_fre_hw/ip_repo.
3)将system.tcl拷贝到audio_fre_hw/下。
4)打开vivado的TCL console,
输入:cd (dir)/project/audio_fre_sw。
输入:source system.tcl
此时可以看到自动创建了一个system.bd。
5)generate output product
6) create HDL wrapper
7) 添加约束文件。oled.xdc和zed_audio_constraints.xdc
8)generate bitstream
9)open implementation design & block design
10) export for SDK
2. 在SDK中新建工程,添加文件,编译。下载到ZedBoard上进行调试
11) 在SDK中新建工程,audio_fre_hw,选择空模板
12)添加源文件,见/src/sdk
13) 检查自动编译的结果是否正确。如果报错,提示缺少sin、cos等,在project->property->build setting->linker library中添加参数m,以使用数学函数库
添加完毕后,SDK会自动编译程序。出现“'Finished building: test_audio.elf.size'”说明成功。
14)使用音频线(音箱和电脑连接的那个线)连接pc的音频口和zedboard的LINE IN接口,将耳机的音频插头接入LINE OUT接口。使用miniUSB连接J14和J17接口用于串口通信和烧写程序。
15)打开SecureCRT并连接。选择Xilinx tools->Program FPGA,烧写程序,蓝灯DONE亮说明成功。
16)打开PC的音频播放器,随便播放一段音频。run->run as,选择GDB。
17)可通过开关选择可以进行控制。
SW0:0--播放原始音频;1--进行音频实时fft分析;
3. 程序分析
18) 关于fft
fft ip是参考ug871进行实现的,核心是vivado自带的fft,再加上通过HLS生成的前处理和后处理的IP核,共同构成了RealFFT核(本来想单独给它生成IP的,一时未验证成功)。博主本人之前尝试着把上篇软件的fft经HLS转为IP,虽然转成功了,但是vivado综合不了,猜想是数据类型和数据量的原因以至于无法传输大批量数据。本文默认的FFT_SIZE是1024,数据量更为庞大,那么是如何处理的呢?通过使用streaming方式使FFT IP直接与cpu通信,即采用DMA IP连接RealFFT IP和CPU,从而CPU只需要对DMA进行操作即可。因此数据的输入输出只需要和DMA打交道就可以了。这里贴上代码和个人注释,以供参考。这里面使用的simpleTransfer函数,只能处理长度为32位的数据,因此本文的输入输出数据均为short类型,更为复杂的类型还需后续研究。
void RealFFT_DMA(XAxiDma *InstancePtr,short *dataIn,compx * dataOut)
{
int i=0;
int status;
// *IMPORTANT* - flush contents of 'realdata' from data cache to memory
// before DMA. Otherwise DMA is likely to get stale or uninitialized data
Xil_DCacheFlushRange((unsigned)dataIn, 4 * FFT_SIZE * sizeof(short)); //refresh cache
// DMA enough data to push out first result data set completely ??why 4?why must be?why not 5,6,..?
// Request DMA transfer from PS to PL. Enough data to fill the front-end block and the FFT processing
// pipelines must be sent in order for spectral data to be ready when the PL to PS transfer is requested.
// Therefore, four data sets are sent before the first output set is requested:
status = XAxiDma_SimpleTransfer(InstancePtr, (u32)dataIn, //send enough data to DMA
4 * FFT_SIZE * sizeof(short), XAXIDMA_DMA_TO_DEVICE);
// Do multiple DMA xfers from the RealFFT core's output stream and
// display data for bins with significant energy. After the first frame,
// there should only be energy in bins around the frequencies specified
// in the generate_waveform() function - currently bins 191~193 only
for (i = 0; i < 2; i++) //8?2*4? 2 is ok initial value 8 is assuring the fft out is ok
{
// Setup DMA from PL to PS memory using AXI DMA's 'simple' transfer mode
status = XAxiDma_SimpleTransfer(InstancePtr, (u32)dataOut,
FFT_SIZE / 2 * sizeof(compx), XAXIDMA_DEVICE_TO_DMA); //send result to PS
// Poll the AXI DMA core
do
{
status = XAxiDma_Busy(InstancePtr, XAXIDMA_DEVICE_TO_DMA); //wait until transfer done
} while(status);
// Data cache must be invalidated for 'realspectrum' buffer after DMA
Xil_DCacheInvalidateRange((unsigned)dataOut, //invalidated cache
FFT_SIZE / 2 * sizeof(compx));
// DMA another frame of data to PL
//Push another set of stimulus data to the PL in order to start the accelerator processing the next frame:
if (!XAxiDma_Busy(InstancePtr, XAXIDMA_DMA_TO_DEVICE)) //continue sending data
status = XAxiDma_SimpleTransfer(InstancePtr, (u32)dataIn,
FFT_SIZE * sizeof(short), XAXIDMA_DMA_TO_DEVICE);
printf("\n\rFrame #%d received:\n\r", i);
printf("End of frame.\n\r");
}
printf("fft done.\n\r");
fflush(stdout);
printf("fft done 1.\n\r");
}
在使用时还必须注意dataIn和dataOut的类型,本文为:
short dataIn[FFT_SIZE*4];
compx out[FFT_SIZE/2];
float mag[FFT_SIZE/2];
数组长度必须使用上述所示,因为设计函数时没有考虑通用性,只考虑功能实现了。(新手不容易啊~~)
为什么要乘4呢?博主认为乘其他的也可以,但是没有去验证了,它只是为了多传点数据以供处理,包括for循环也是如此,目的都是为了最终的处理数据准确,不会因为时间问题处理错了或者没来得及传出来等。对于本例的音频数据,本文处理如下:
//====== audio value =============
get_audioData(dataIn);
for (i = 1; i < 4; i++)
for(j=0;j<FFT_SIZE;j++)
dataIn[j + i * FFT_SIZE]=dataIn[j];
19) OLED显示
本来在之前的例子中是么有这个问题的,但是由于本文采用了1024点,那么显示的时候就出问题了,因为OLED只能显示128的长度,因此必须重新写它的显示函数,而且对于音频数据,想要其显示的好看,还必须设置显示高度。经多次尝试,本文成功实现在OLED的中央显示最大幅值,两侧均匀分布。
void oled_show(float *mag)
{
printf("oled_show begin \r\n");
int outLED[oled_L];
int i,maxi;
float tmp;
u8 oled_equalizer_buf[oled_L];
for (i = 0; i < oled_L; i++) outLED[i] = 0;
for(i=1; i<FFT_SIZE/2; i++) //get the max mag & its fre
{
if(tmp<mag[i])
{
tmp=mag[i];
maxi=i;
}
//xil_printf("Cur Mag= %x MHz\r\n",mag[i]);
}
if(FFT_SIZE/2>=oled_L) //the max fre will be shown in the middle
{
if(maxi-64<0)
{
for(i=0;i<maxi+64;i++)
outLED[i+64-maxi]+=mag[i];
for(i=0;i<64-maxi;i++)
outLED[i]+=0;
}
else if(maxi+64>FFT_SIZE/2)
{
for(i=0;i<FFT_SIZE/2+64-maxi;i++)
outLED[i]+=mag[i+maxi-64];
for(i=FFT_SIZE/2;i<maxi+64;i++)
outLED[i+64-maxi]+=0;
}
else
{
for(i=0;i<oled_L;i++)
outLED[i]+=mag[i+maxi-64];
}
}
else
{
for(i=0;i<FFT_SIZE/2;i++)
outLED[i+64-maxi]+=mag[i];
for(i=0;i<i+64-maxi;i++)
outLED[i]+=0;
for(i=0;i<oled_L;i++)
outLED[i+64-maxi+FFT_SIZE/2]+=0;
}
oled_equalizer_buf[0]=mag[0]/2;
for (i=1; i<oled_L; i++)
{
oled_equalizer_buf[i]=outLED[i]*AMP; //control the height
// if(i%10==0)
// printf("oled_equalizer_buf[%d]=%d\r\n",i,oled_equalizer_buf[i]);
}
OLED_Clear();
OLED_Equalizer_128(oled_equalizer_buf);
OLED_Refresh_Gram();
//OLED_ShowString(0,0,"hello world!");
}
20) AUDIO数据获取
在"vivado+zedboard之audio驱动"一文中已经做了分析。此处就不详述了。
4. 总结
本文学习和使用了FFT、DMA等ip核,在使用audio、oled、fft前后处理等自定义ip的基础上,构建了音频分析仪的硬件部分,并在SDK中编写了控制程序,在zedboard中成功验证。可以实时显示音频频谱。关于例子本身还有诸多要优化的地方,参见上篇博文。