DSP开发基础实验
实验目的
新建工程,对工程进行编译、链接,下载到目标板上后进行实时调试。利用代码调试工具跟踪程序的运行。根据.map文件,指出各段在存储器空间的地址,体会Memory Map的设计思想。
使用CCS进行DSP开发至少需要以下3种文件:1、.c或者.asm程序文件,这是用户写的程序;2、.cmd文件,即配置命令文件,其指定了链接器在链接时存放各段的物理地址;3、.lib文件,是由芯片厂商提供的运行支持库。TSM320C28x系列对应rts2800_ml.lib.当然除了这3类外,建立项目工程后还会产生.pjt文件,这其实就是一个文本文件,记录了项目中芯片的类型,程序目录,输出目录以及编译器设置等信息。
使用C语言在CCS下开发时,C编译器(Complier)首先将C语言翻译成汇编代码,这一步也会对程序进行一些可选的优化。然后汇编器(Assembler)将汇编代码转换符合公共目标格式COFF的机器码。连接器(Linker)对.obj文件重定位,根据配置命令文件.cmd指定各段的物理地址,输出DSP可执行文件。
DSP的仿真调试可以使用Simulator和Emulator方式。前者使用计算机的CPU进行仿真,可以实现程序功能的验证,但不能进行外设的模拟和实时性验证。后者是利用仿真器硬件,通过JTAG接口与DSP相连,使用边界扫描技术实时获得芯片的状态,程序是全速运行在DSP上的,因此所见即所得。
第一个实验旨在熟悉CCS的开发环境和DSP开发流程,故而实验程序已经提供,无需改动。
通过对源代码的分析,程序主要干了三件事:1、关闭看门狗;2、在CCS的output窗口输出字符串;3、在存储器中,从一片连续地址取数据,乘以一个增益后存入另一片连续地址。
具体实现如下:
1、关闭看门狗:
图1看门模块
图2 WDCR寄存器定义
直接对WDCR寄存器的WDDIS位写1,关闭看门狗。在对寄存器操作前还要先执行汇编语句eallow,操作完后再执行汇编语句edis.
2、在output窗口输出字符串:
puts语句即可实现:
puts("SineWave example started\n");
3.数据的复制:
首先在头文件中声明了一个结构体IOBuffer,结构体内包含两个长度同为BUFFSIZE的数组input[]和output[],类型为16位int. 然后在main函数中定义了1个该类型的结构体,使用下标遍历整个数组,将input[]中的数值乘以增益系数gian,存入output[]中。这些操作在函数processing()中实现。
这个程序已经足够简单,无需使用框图进一步解释。
子程序入口地址:
装载程序到DSP后,将鼠标停留在C语言编辑器的函数上,会显示函数的入口地址。如图6-1(a)和6-1(b),显示了dataIO()和processing()的入口地址,分别是0x003F81F5和0x003F81D8.
图3 (a). dataIO()的入口地址 (b). processing()的入口地址
从中还可以看到,dataIO()函数的入口地址比processing()函数大了29,说明processing()的汇编指令至多有29条。而且还说明,链接器在存储器中分配程序空间时,函数的物理地址不一定按照C语言编写时的顺序。实际上对于汇编指令调用函数来说,程序入口地址的顺序并不重要。
存储器地址:
通过菜单上的View – Watch,打开变量查看窗口。输入变量名currentBuffer.input和currentBuffer.output, 查看变量的值。结果如图6-2(a)和(b),两个变量的地址分别是0x00008480和0x00008500, 他们相差了128,也就是一个数组的长度。结构体中的变量是连续存放的。
图4 (a). currentBuffer.input的地址 (b). currentBuffer.output的地址
程序载入到DSP后,直接运行整个程序。使用Graph工具,以图像方式显示数组currentBuffer.input和currentBuffer.output中的数据。由于数组的类型是int型,长度为128,所以在GraphView中设置类型为"16bit singned int",长度为128,变量地址填6.1节中所述的物理地址,或者直接填变量名。运行一段时间后,两个数组的内容如图6-3所示:
图5 (a).input数组中的数据 (b).output数组中的数据
从整体来看,input数组中的数据是随机无需的,这是因为DSP在上电后,RAM中的数据是随机的。再看output中数据和input数据的关系,将光标都设置在第10点处,input中的数据是3430,而output中的数据是-13720,是前者的-4倍。证明output中的数据是从input中复制的,而且乘以了增益-4.
在processsing()函数之前的位置放置探针,向currentBuffer.input[]中导入正弦波形。程序中的空函数dataIO()正好提供了这样的位置。因此将探针点设置在dataIO()函数上,然后在File-File I/O中对探针进行设置,包括数据文件、地址、长度等。数据文件就是目录下的sine.dat,地址设为input[]数组的物理地址0x00008480,长度为128d.
图6探针位置
图7探针设置图8导入数据
导入数据后,程序复位,重新运行。刷新Graph窗口中的内容,结果图6-7.
图9 (a).从探针导入数据后的input[]数据 (b).运行之后的output[]数据
这时,input数组中的数据已经是正弦波了,而且运行完processing()函数后,数据也复制到了output中。为了进一步验证复制结果,与上节一样,用光标选取两个数组中相同的位置(本来想都取第35点,写报告时才发现手滑取错了TAT'),input中数据是98,output数据大致是其-4倍。由于乘以了负增益,output中整个波形关于x轴反转了。这些现象都验证了程序功能的正确性。
打开debug目录下编译链接后生成的.map文件,查看其内容。.text、.data、.bss段的起始地址和长度分别如下:
图10 .map文件中各段的地址和长度
其中.text段起始地址为0x003f81c5, 长度为0x0adb;.data没有被分配空间,地址和长度都为0;.bss起始地址为0x00000400,但长度为0.
根据TMS320F2812芯片的memory map(图6-9),可知text位于H0 SARAM中,bss段位于M1 SARAM中。
打开.cmd文件,确实没有定义.data段;.text分配在0x003f8080起始的存储器中,与.print、.cinit段在一起;.bss段为分配在起始地址为0x0400起始的M1 RAM中。
可见,根据芯片的memory map,.cmd文件指定了各段的存储地址,而编译链接后.map文件详细报告了各段的地址分配。这三者是统一的。
图 11 Memory map
图12 cmd文件中指定的段位置
第一个实验的主要目的是为了熟悉CCS开发环境,了解开发流程。由于之前在CCS上开发过430单片机和5000系列 DSP,何况所有的IDE都大同小异,所以这个实验很快就做完了。作为TI自己的软件,CCS的特点在于对芯片的调试支持非常到位,比如可以通过设置探针,导入和导出数据,可以用Graph工具对内存数据作图。而一般的开发工具似乎只能设置断点慢慢调试。
虽然这个实验很简单,但可以分析的问题还是很多的。实验时,我们记录了在用探针导入数据之前和导入之后数组内的数据,以此说明探针的作用;分析了processing()执行后,input[]和output[]中数据的关系,证明这个函数实现了数据的复制,并乘以了增益-4;通过比对.map和.cmd中各段的地址和长度,将数据对应到了Memory map中的物理存储器,从而对各段数据的存储位置有了直观的认识。
分析实验例程,学习DDS的原理,在此基础上对实验程序进行改写,产生线性调频信号:
采样时间内包含1024点离散数值。
DDS的结构框图如下所示,其中频率控制字控制相位累加器的步进量,根据相位累加器中的数值,从波形查找表中取对应的数值,送入DAC,最后经过低通滤波器滤除高频,就得到了波形的模拟输出。
图 1 DDS结构框图
实验箱上的DAC1信号为AD768,位宽16bit,以无符号数表示,0x8000表示0电位。DAC映射到了DSP的地址0x2900, 因此向DAC写数据只要写地址0x2900即可。
实验箱上8个LED数码管共阴,地址从0x2000开始,0x100递增。写入相应的码段之后,在0x2C00写任意数值,刷新锁存器即可。
在这个实验中,没有用到任何外设,因此可以将所有外设时钟关闭。除此之外,设定主时钟锁相环5倍频。这样,外部时钟为30M,CPU工作时钟为150M。实验程序的流程图表示如下:
图 2程序流程图
其中,波形查找表存储在0x0010 0000起始的内存中,这个是外部的SRAM,使用指针直接操作地址,实现数据存取。
计算波形数据时,使用math.h中的cos()函数,得到的是有符号数,乘以幅度后转换为int型。而DAC接收的是无符号类型,0x0000为-2v,0x8000为0v,0xFFFF为+2v,相当于将一个补码向上整体搬移了0x8000。因此将int型数据转换为DAC的数据,方法如下:
*(DAC1Addr)=(unsignedint)((*(RamAddr+1*i))<<2)+0x8000;
在将int型转换为unsigned int之前,还要先左移几位,防止向上搬移0x8000时溢出。
建立工程后,添加实验所给的例程,编译链接下载到DSP中。例程中实现了正弦波信号的产生,但很不幸的是,运行实验例程,通过示波器观察发现,实验箱上的DAC坏了..
图 3损坏的DAC输出
如图3,表现为:幅度大于一定值时,输出突然下降一个台阶。为了确认是DAC的问题,在别的实验箱上运行同样的程序,是没有问题的。
所以我们的实验,包括后面的2个实验,用到DAC时,都没有用到满幅度,这样,实际可用的DAC位数是小于16bit的,最大输出幅值也变小了。将正弦波幅度减小4倍后,输出正常。如图4所示,是减小正弦波幅度后的输出,波形正常,但此时峰峰值只有1v了。
图 4减小幅度,避开DAC故障后的输出
运行例程的价值在于:除了发现DAC故障外,还可以测出1024点输出所需的时间。在例程中,一个正弦波波形由1024点数据组成,测得正弦波频率为1.04626kHz,说明遍历输出一个正弦查找表需要1/1.04626k=9.5579e-4 s, 所以平均输出每个点需要时间:9.5579e-4/ 1024=9.33e-7 s. 无论输出什么波形,这个时间是不会变的。
按照实验要求,1024点的线性调频信号为:
分析可知,在2×0.0128=0.0256s时间内,有1024个离散点,因此每个点代表的时间为0.0256 / 1024 = 1/40000s. 换算成离散坐标,表达式如下:
从而,生成查找表的C代码就是:
for(i=0;i<1024;i++)
*(RamAddr+i)=(int)((cos(K*Pi*(-512+i)*(-512+i)/N/N)*2048));
使用Python对上述表达式进行了计算并作图,得到的波形如图5(a).
编译链接程序,下载到DSP上,然后运行程序。在Graph工具中查看起始地址0x0010 0000,长度1024点的数据图形,结果如图5(b)
图 5 (a)仿真结果 (b)实际运行结果
将输出连到示波器上观察,波形仍然正确。图6显示了示波器测量结果,从右边测量结果看,此时的频率是1.042kHz, 周期为960us.
图 6频率为1.042kHz的线性调频信号
在上一节中,已经正确产生了频率为1.042kHz,周期为960us的线性调频信号,但题目要求周期是0.0128×2=0.0256s,即25600us. 增加周期只需要增加每个点的输出时间即可,因此可以对每一个点重复输出数次,调整到所需的周期。25600是960的26.6倍,所以对每个点重复输出27次左右即可。通过实验,发现重复28次即可以调整到所需周期。这样,在while(1)内,程序代码就是如下的形式:
while(--j)
{
*(DAC1Addr)=(unsignedint)((*(RamAddr+1*i))<<2)+0x8000;
}
j=29;
经过周期调整的输出波形如图7,右边的测量结果显示,此时周期是25.4us,已经很接近要求了。
在DSP初始化设置后,操作LED数码管显示字符。数码管的操作方法在第3节原理中已经叙述,直接向特定的地址写数据就行了。我们让数码管显示了NJUST.Lys字样,"Lys"是我们指导老师名字的首字母,感谢老师的教导。
图 7 LED数码管显示
下载DSP程序后,鼠标悬停可以看到函数的地址。本程序中除了主函数mian()外,还有dispLED()函数。它们的地址分别是:0x0000 0000 和 0x0000002C.
图 8子程序的入口地址
根据memory map,它们都位于M0 Vector RAM 中。查看.map文件,.text段确实从0x0000 0000开始。这里本来是用于存放中断向量的,但程序没有用的中断,存放函数也无妨。
section page origin length input sections
-------- ---- ---------- ---------- ----------------
.text 0 00000000 000002eb
除了.text、.data、.bss段,实际占据内存长度的还有:
.cinit 0 003f8002 0000002d
.reset 0 003fffc0 00000002 DSECT
.stack 1 00000400 00000400 UNINITIALIZED
其中.cinit段是全局变量和静态变量的C初始化记录,.stack是运行时堆栈。
本实验在第1个实验做完后剩下的时间内接着做的,也比较简单。由于实验箱上的DAC损坏,电压最大幅度只能用到1/4,但这影响不大。首先通过运行实验例程,记录了1024点遍历所需的时间,为后面调整信号周期提供参考(5.1节)。第二步是计算出了线性调频信号的离散表达式(5.2节),并使用python进行了简单的仿真。波形仿真正确后下载到DSP运行,使用graph工具查看波形,与仿真结果相同,示波器也观察到了输出波形。根据题目要求的0.0256s周期,我们通过一个样点多次输出的方法,加大了信号周期,并认为每个点重复28次,可以得到比较接近0.0256s的周期。c程序和调整后的波形在(5.3)节中展示了。但由于对波形查找表的取值是在while(1)中循环的,延时由指令数决定,因而不能做到精确的频率控制。更好的方法是在定时器中断函数内读取查找表,这样就可以通过定时器计数值任意设置波形周期了。此外,还操作了LED数码管,正确显示了预设的字符(5.4节)。最后,查看.map文件,指出了子程序在物理存储器中的位置。
1. 熟悉DSP的软硬件开发平台;
2. 掌握TMS320F2812的ADC外设的使用;
3. 熟悉TMS320F2812的中断的设置;
4. 掌握代码调试的基本方法;
建立工程,根据实验例程,使用ADC进行数据的采集,存储以及模拟还原,验证ADC采样频率的设置,查看存储内容。
实验的ADC为TMS320F2821内部集成的12bitADC,输入范围为0-3V,最快转换时间80ns。虽然器件是0-3V的,但外围电路对输入电压进行了转换,可以输入负电压。DAC是外部器件AD768,16bit.
ADC的转换触发由定时器控制,因此本实验涉及许多寄存器的操作,主要包括系统时钟和外设两大类,此外,还需要设置相应的中断。
与时钟有关的寄存器有:PLLCR,设置锁相环倍频数;PCLKCR,控制外设时钟的使能;HISPCP和LOSPCP,控制高速外设和低速外设的时钟分配。ADC和定时器都挂在高速外设上。
ADC的功能设置主要用到3个设置寄存器ADCTRL1,ADCTRL2和ADCTRL3,控制转换通道涉及ADCMAC CONV(转换次数设置),ADCHSELSEQn(输入通道选择)。
ADC的时钟链路如下:
图 1 ADC的时钟链
F2812具有两个事件管理器(EV),分别为EVA和EVB,事件管理器的结构如图2所示。在本实验中,只用到了通用定时器。
通用定时器有4种工作模式,本实验使用连续增计数模式。这样,定时器的中断周期时间就为(TxPR+1)个定时器输入时钟周期。
图 2事件管理器框图
之前叙述过,实验程序通过EVA中断触发ADC采样,所以需要用到ADC中断。TMS320F2812使用外设中断扩展单元(PIE),将外设中断信号分组复用到CPU级中断,可以支持96个中断源。PIE的分组结构如图3,EVA中定时器的中断源对应INT1.6.
图 3 PIE单元分组结构
本实验涉及大量寄存器,因而使用了官方的库进行开发。在库中,每个外设或者模块作为一个头文件,寄存器都被抽象为联合体,可以操作整个寄存器,也可以操作特定的位。以外设使能寄存器寄PCLKCR为例,结构体定义如下。首先在PCLKCR_BITS中用位带(bit band)特性,构造了一个成员变量位数不定,内存连续的结构体。然后定义了一个联合体PCLKCR_REG,成员变量是PCLKCR_BITS和全部位all,联合体的成员占据同一内存。这样,在程序中只要使用联合体就可以描述一个寄存器,既可以读写特定的位,也可以整体一次操作。最后,头文件中又把所有寄存器的联合体包含到了一个结构体中。
例1 PCLKCR_REG寄存器的联合体描述
// Peripheral clock control register bit definitions:
struct PCLKCR_BITS{// bits description
Uint16 EVAENCLK:1;// 0 Enable high speed clk to EV-A
Uint16 EVBENCLK:1;// 1 Enable high speed clk to EV-B
Uint16 rsvd1:1;// 2
Uint16 ADCENCLK:1;// 3 Enable high speed clk to ADC
Uint16 rsvd2:4;// 7:4 reserved
Uint16 SPIENCLK:1;// 8 Enable low speed clk to SPI
Uint16 rsvd3:1;// 9 reserved
Uint16 SCIAENCLK:1;// 10 Enable low speed clk to SCI-A
Uint16 SCIBENCLK:1;// 11 Enable low speed clk to SCI-B
Uint16 MCBSPENCLK:1;// 12 Enable low speed clk to McBSP
Uint16 rsvd4:1;// 13 reserved
Uint16 ECANENCLK:1;// 14 Enable system clk to eCAN
};
union PCLKCR_REG{
Uint16 all;
struct PCLKCR_BITS bit;
};
以系统控制相关寄存器为例,寄存器在头文件中的三级组织结构如下所示:
图 4库的寄存器组织结构(以系统控制相关寄存器为例)
本实验的程序框图如下:
其中,在初始化设置中,使用到了以下函数进行设置:
InitSystem(); //初始化DSP内核寄存器
InitPieCtrl(); //调用PIE控制单元初始化函数
InitPieVectTable(); // 调用PIE向量表初始化函数
InitAdc(); //ADC初始化设置
而设置ADC、定时器、PIE到工作状态的代码在main()中实现,直接设置寄存器相关的位。上述设置完成的功能叙述如下:
初始化函数:
InitSystem();
禁用看门狗,
PLLCR DIV为10(d)(PLL5倍频)
HISPCP=0x01(高速外设时钟除2)
LOSPCP=0x02(低速外设时钟除4)
PCLKCR外设时钟使能寄存器设置为:0x0009,即EVAEN位和ADCEN位置1,打开EVA和ADC时钟
InitPieCtrl();
初始化PIECRTL的ENPIE为0,PIEIERx为0, PIEIFRx为0.
InitPieVectTable();
初始化中断向量表并将ENPIE置1
InitAdc();
ADCCRTL3的ADCBGRFDN设为0x03,打开参考电源;ADCPWDN位设置为1,打开ADC其他部分的电源,其余寄存器都为初始值。
在main()中:
PIE设置:
PieCtrlRegs.PIEIER1.bit.INTx6=1,将PIEIER1设置为0x0020;IER寄存器的INT1位写1,IER=0x0001, 打开INT1中断;用EINT汇编指令开全局中断,INTM设0。
ADC设置:
ADC设置为单通道转换,输入通道为ADCINA1。允许EVA触发SEQ1,并打开SEQ1中断。
EVA设置:
T1禁止比较输出,通过T1TOADC启动ADC周期中断方式;设为连续递增计数模式,T1CON的TPS设7,预分频2^7=128,T1PR设置为62,这样中断周期为30M×5/2/128/(62+1)=9.3kHz 而ADC的采样保持时钟根据图1可以算出来为30M×5/2/1/4=18.75M,远大于定时器中断周期。
按照题目要求,交替显示实验者学号。设计方法为:在ADC中断中计数,一旦计数值到达预设,就切换显示LED。为了不占用中断时间,LED的显示在main函数的while(1)中进行。为此,设计了如下的LED显示函数DispLed(), 使用switch语句切换不同的显示,函数示意如下:
void DispLed(void)
{
staticunsignedchar ca=1;//静态变量,点睛之笔
switch(ca)
{
case1:
// LED8
*LED8= CHAR_NULL;
// …………重复代码省略
//显示第一个学号
*(LEDWR)=0XFF;
ca++;
break;
case2:
*LED8= CHAR_NULL;
//显示第二个学号
*(LEDWR)=0XFF;
ca++;
break;
case3:
*LED8= CHAR_NULL;
*(LEDWR)=0XFF;
ca++;
break;
case4:
*LED8= CHAR_NULL;
//显示第二个学号
*(LEDWR)=0XFF;
ca=1;
break;
default:
ca=1;
}
这个函数本身具有静态变量,是完全封装的,无需输入参数和全局变量,每调用一次,就会将LED内容切换为下一个学号。
在while(1)中,判断定时器中断计数达到阀值后,就调用LED显示切换函数DispLed().这样就实现了LED内容的交替显示。
实验还要求存储ADC采集到的数据。我们设定存储数量为1024点,使用队列数据结构实现。队列是先进先出的,队列满了之后,每进入一个新数值,最早的数据就会被踢出。考虑到性能,使用循环数组实现队列功能。
图 5 循环数组实现队列的示意图(以长度12为例)
上图展示了本程序中所用到的队列结果,其原理是一个数组和一个游标index. 数组元素在内存中是按顺序排列的,这便于通过指针获得数组内容;而index代表了队列的头。队列满后,每到一个新数据,覆盖最早的数据,index也移动一位,当index指到数组末尾后,就要跳回数组起点。图中展示了长度为12的队列,buffer[7]是队头,buffer[8]是队尾,下一时刻buffer[8]将被覆盖,index从7变为8.
使用这样的数据结构,每到一个数据,只要写入一个数据,同时改变变量index的值,只有2步操作,尽可能地减少了指令数。
ADC的触发由定时器控制,而ADC本身的采样保持时钟很高,4.2节中已经分析其为18.75M. 定时器在连续增模式下,中断周期受到定时器时钟和计数值TxPR的控制,中断频率计算公式为:
实验时, =30M×5/2/128/(62+1)=9.3kHz
粗略验证:
当采样频率为9.3kHz时,输入一个频率为0.93kHz的正弦波,一个周期内将只能采集到10个点. 用信号源产生0.93kHz正弦波,输入ADC, 用示波器测量经过DAC还原后的输出。结果如图6,一个周期内确实有10个量化阶。证实了采样频率为9.3kHz.
图 6 9.3kHz采样频率输入0.93kHz信号
精确验证:
改写DSP程序,在每次ADC中断内将ADC采样值反转一次后输出到DAC,这样,在没有输出信号时,输出方波,方波翻转周期就是ADC采样率。在采样频率为9.3kHz下,方波周期为4.65kHz,即翻转频率9.3kHz,与理论相符。
图 7 9.3kHz采样率下DAC反转输出
通过设置TPS的值为2,即4分频,得到新的采样率,此时采样率为: =30M×5/2/4/(62+1)=297.619kHz. 使用同样的方法验证采样率。结果如图8. 结果也与预想相符。
图 8 297.619kHz采样频率下DAC反转输出
由4.4节中所述的数据结构方法,存储了1024点采样数据,保持在全局变量adBuff[1024]中,队列头变量为buffIndex.
在9.3kHz采样频率下,输入0.93kHz正弦波,使用Graph工具对信号绘图。图9展示了adBuffer的地址和数据内容,以及buffIndex的数值。
图 9 adBuffer的地址和数据内容,以及buffIndex的数值
4.3节中叙述了LED交替显示的具体实现。原理简述为:ADC中断中计数,while(1)函数中判断计数值是否到达阀值,如果到达,调用LEDdisplay()函数。而LEDdisplay()函数内用switch实现了不同的LED模式显示。
实验时,可以看到我组学号的交替显示。
本次实验使用到了ADC、EV外设,加上PIE的设置,设计的寄存器较多。使用官方的库函数进行配置,可以省时省力,减少出错。4.1节论述了库头文件中,使用C对寄存器结构的精妙构思:将每个寄存器的每位,用位带构成结构体;该结构体和整个寄存器内容构成联合体;相似功能的寄存器联合体再组合成结构体。这样不仅可以单独操作寄存器的位,也可以一次读写全部位。
在进实验室之前,我预先已经把要求的程序内容写好。在存储ADC采样值的时候,想到了队列结构,并使用循环数组实现了(4.4节),这样一次中断只需要写一次数据,改变一次队列头变量即可,是最优的存储方法,在约290kHz采样率下存储数据正常。
对采样频率的验证使用了2种方法,一种无需改动程序,输入特定频率信号,观察输出波形一个周期内的量化阶数,可以大致推断采样率;第二种方法是将ADC采样值反转一次后写到DAC输出,示波器上观察到的是50%占空比的方波,2倍方波频率就是采样率。
LED交替显示学号的要求也实现了,在4.3节具体说明了实现,5.4节中也用一句话总结了原理。
1. 巩固数字FIR滤波器的概念;
2. 理解定点DSP中数的定标、有限字长、溢出等概念;
3. 理解算法实现中实时的概念;
4. 掌握DSP开发过程以及基本调试方法;
5. 理解汇编以及高级语言开发DSP实现算法的区别。
确定FIR滤波器参数,设计滤波器系数,完成数据的定标。仿真有限精度截断对FIR冲击响应的影响。验证系统实时性,实验测量幅频响应。
我组在实验室北排,要求实现FIR带通滤波器。经过考虑,决定实现50kHz,40阶FIR滤波器,截至频率设为1000Hz和3000Hz.
可行性分析:
ADC的采样频率由定时器触发,设置EVA时钟分频位TPS=2,高速外设分频数HSPCLK=2,T1PR=374,此时定时器触发ADC的周期为:
30M×5/ 2 / 2^2 /(374+1)=0.05M=50kHz
即可以得到精确的50kHz采样率。
此时CPU时钟为150M, 平均每个中断周期内可以分配到3000条指令执行时间。40阶FIR滤波器包括40次乘加运算,由于使用了循环数组,只需少量的内存更新操作,再加上DSP的流水线结构,加上逻辑判断操作后,保守估计一个样点计算使用多达20条指令,40阶也只有800条,小于3000的裕量。
在50kHz采样率下,将通带设置在1kHz~3kHz,是满足采样定理的,一个周期可以分到20个样点左右,还原出来的波形直接输到示波器上,尚可辨别。
FIR滤波器可以利用matlab的可视化滤波器设计工具箱fdatool来设计,设计界面如图1所示。利用该工具可以直接得到41个滤波器系数,记为,这些都是绝对值小于1的小数。TMS320F2812是定点DSP,因此在DSP中运算时,使用的是精度有限的定点数。在C语言中实现小数点标定的方法是:使用扩大了2的n次方的int型表示小数,参与运算,在最后的结果上通过右移n位,得到真实的数值。
在本实验中,我们使用16位int型存储滤波器系数,标定为Q14. 从matlab的浮点小数转换为16位定点数,必然会造成精度的损失。利用matlab可以仿真截断后的冲击响应幅频特性。仿真结果如图2.
图 1 fdatool设置参数
图 2 FIR系数的定点仿真
FIR滤波器实验建立在实验3的基础上。在上次实验中,已经实现了ADC数据的采集和DAC的输出,设计FIR滤波器时,只需要重新设置EVA时钟中断周期,其余无需改动。本实验更多的关注点在于算法的设计。
F2812的片内ADC位数为16bit 无符号,由于附加了外部电路,输入模拟信号时,0v电平对应的二值数据为0x8000,这与DAC接收数据的格式是一样的。FIR滤波器系数使用Q14定标,转换为了16位int型。根据FIR滤波器的表达式:
(1)
ADC采样值16bit uint 和滤波器系数 16bit int 相乘,最大将扩展到32位。所以运算过程中的使用的数据类型转换如下所示:
图 3运算中数据类型的转换
其中,运算求和后得到的32bit long,在输给DAC之前,要转换移位为16bit. 滤波器系数定标时使用了Q14,原本对结果也右移14位即可,但实验所用DAC是坏的(实验2中提到过),只能用到14位,所以对求和结果右移了16位,避免了DAC因故障而跳变。
上一个实验中,对ADC采样数据的存储使用了循环数组来实现,在40阶FIR滤波器中,这个数组长度是41. 并且由于数组中存储的数据是一个"环", 环的起点下标由一个变量buffIndex来表示,所以需要仔细考虑采样点数据与滤波器系数的对应关系。下图展示了采样值与滤波器系数相乘时的对应关系,这将是整个算法的精华。
图 4循环数组中x[n]与h[n]的对应关系
从图中可以看到,由于存储采样点存储顺序不是与数组下标对应的,需要引入队列头位置变量buffIndex,与FIR系数h[n]对应起来。FIR的乘加可以分为part1和part2两部分计算,在part1中,h[0]对应了buffer[buffIndex], h[1]对应buffer[buffIndex-1],……以此类推,part1中共有buffIndex+1次乘加运算。同样的方法,根据图4,也可以得到part2中两个数组的对应关系。
记当前队列头的数组下标为index,则计算两部分乘加结果的方法可以用伪代码表示如下:
Order=41
Sum=0
firIndex=buffIndex
for(i=0; i<=index; i++) //计算part1的乘加
sum=sum + h[k]*buffer[firIndex]
firIndex=firIndex-1
end
firIndex=buffIndex //firIndex归位
for(i=index+1; i
sum=sum + h[k]*buffer[firIndex]
firIndex=firIndex-1
end
调整信号源产生正弦信号,输出频率从低频扫描高频,覆盖带通区域,调整输出信号幅度尽可能大,但使滤波结果不溢出。测量得到不同频率下输出幅度,作图结果如下:
图 5测得幅频响应
图 6实测幅频特性与理论值比较
观察实测结果,滤波器的通带位于1000Hz至3000Hz,这与理论是相符的,在通带之外,测得的衰减值小于理论,这可能是由于32位int型下误差的积累造成的。
以上分析的算法和所得实验结果是我们第二版程序,第一次实验时没有成功。第一次的实验使用了浮点表示滤波器系数,但由于没有分析清楚数据转换的格式,最后输入DAC的值产生了混乱,同时,在定点器件上模拟浮点运算,造成运算量大,迫近实时性极限。
最后一次实验,在进入实验室之前又重新写了实验程序,使用Q14表示FIR系数,理清了数据转换过程。然而最后DAC输出的波形出现了倒置。
图 7 DAC输出的波形倒置
分析认为,这是因为最后DAC的数据发生了溢出。在传递给DAC之前,又多右移了2位,输出结果正确。
FIR滤波器实验是建立在实验3的基础上的,ADC采样,DAC输出的链路已经调通,寄存器设置也无需作大改动。本实验主要考验软件算法的设计以及小数定标的选择。这个实验写了两个版本,均在进入实验室之前写好,程序中使用了条件编译,可以先测试ADC采样频率,然后切换到FIR计算功能,最大限度利用好实验时的宝贵时间。第一个版本使用了浮点小数做FIR系数,结果运算量巨大,最后输入到DAC的数据格式混乱,没有成功。下一次实验之前,用Q14定标的16位小数作FIR滤波器系数,写了第二个版本。并且为了防止实验时产生实时性问题,我们准备了3组滤波器方案,分别是25kHz采样率30阶,50kHz采样率30阶和50kHz采样率40阶。最后进入实验室实际运行时,50kHz采样率40阶完全是可行的。这得益于精心安排的数据存储结构和系数-样点对应方法(5.2节)。调试成功后,实测了滤波器的幅频特性响应,与理论进行了比较(6.1节),结果显示,在通带内实际和理论符合较好,在通带外的衰减能力实际并没有理论那么高。
好了,写到这里,报告就结束了。这份报告陆陆续续写了5天,在撰写上花费了大约40小时,而编程和实验室操作上用了不到20小时,最后的考试复习用了大约10小时。这份报告上花的时间比例已经远远超过它的分值了,可谓良心力作。全篇约一万字,搭配了大量图表,程序设计部分给出了框图或伪代码,力求说理清晰。总的来说,这个课是很有意思的,目前为止,我接触学习了51,cortex M3,msp430,TI DSP这些器件,本科也是圆满了。
附录
/****************************************************/
#include "DSP281x_Device.h"
#define CHAR_NULL 0X00
#define CHAR_L 0X38
#define CHAR_DH 0X08
#define CHAR_C 0X39
#define CHAR_A 0X77
#define CHAR_d 0X5E
#define CHAR_0 0X3f
#define CHAR_1 0X06
#define CHAR_2 0X5b
#define CHAR_3 0X4f
#define CHAR_4 0X66
#define CHAR_5 0X6d
#define CHAR_6 0X7d
#define CHAR_7 0X07
#define CHAR_8 0X7f
#define CHAR_9 0X6f
//LED地址
volatileint* LED8 =(volatileint*)0x2000;
volatileint* LEDWR =(volatileint*)0x2C00;
void ConfigureGpio(void);
void InitSystem(void);
void InitLed(void);
void DispLed(void);
interruptvoid adc_isr(void);
volatileunsignedint* DAOUT =(volatileunsignedint*)0x002900;
unsignedint countForLed=0;
unsignedint adBuff[1024]={0,};//--存放AD读取的值
unsignedint buffIndex=0;//adBuff中最新数值的索引
//adBuff [-------********...]
// |
// buffIndex tobe covered
void main(void)
{
InitSystem();//初始化DSP内核寄存器
InitPieCtrl();//调用PIE控制单元初始化函数
InitPieVectTable();//调用PIE向量表初始化函数
InitAdc();//调用ADC模块的基本初始化函数
InitLed();
EALLOW;
PieVectTable.ADCINT=&adc_isr;//重新设置PIE向量表中ADc的中断入口向量
EDIS;
PieCtrlRegs.PIEIER1.bit.INTx6=1;//使能PIE中断分组1中的ADC中断
IER=1;//使能和ADC中断相连的CPU INT1中断
EINT;//使能全局中断位INTM
ERTM;//使能全局实时调试中断DBGM
/* SET ADC*/
AdcRegs.ADCTRL1.bit.SEQ_CASC=0;
AdcRegs.ADCTRL1.bit.CONT_RUN=0;
AdcRegs.ADCTRL1.bit.CPS=0;
AdcRegs.ADCMAXCONV.all=0x0000;//单通道转换
AdcRegs.ADCCHSELSEQ1.bit.CONV00=0x0;//将ADCINA0设置为SEQ1的第一个转换通道
AdcRegs.ADCTRL2.bit.EVA_SOC_SEQ1=1;//使能EVA引起的中断
AdcRegs.ADCTRL2.bit.INT_ENA_SEQ1=1;
AdcRegs.ADCTRL3.bit.ADCCLKPS=2;//ADc模块的核心时钟频率=HSPCLK/4
/* SET EVA*/
EvaRegs.GPTCONA.bit.TCMPOE=0;//禁止比较输出
EvaRegs.GPTCONA.bit.T1PIN=0;
EvaRegs.GPTCONA.bit.T1TOADC=2;//设置周期中断标志启动ADC
EvaRegs.T1CON.bit.FREE=0;//防真挂起时,定时器1立即停止工作
EvaRegs.T1CON.bit.SOFT=0;
EvaRegs.T1CON.bit.TMODE=2;//连续增计数模式
EvaRegs.T1CON.bit.TPS=2;//***()设置定时器时钟频率为HSPCLK/(2^TPS)
EvaRegs.T1CON.bit.TENABLE=1;//允许定时器操作
EvaRegs.T1CON.bit.TCLKS10=0;//内部时钟
EvaRegs.T1CON.bit.TCLD10=0;//计数器为0时重载
EvaRegs.T1CON.bit.TECMPR=0;//禁止比较操作
EvaRegs.T1PR=62;//***()定时器周期寄存器
/* SET GPIO*/
EALLOW;
GpioMuxRegs.GPAMUX.all=0x0;
GpioMuxRegs.GPADIR.all=0xFFFF;
EDIS;
while(1)
{
}
}
void InitSystem(void)
{
EALLOW;
SysCtrlRegs.WDCR=0x00E8;//禁止看门狗模块
SysCtrlRegs.PLLCR.bit.DIV=10;//将CPU的PLL倍频系数设为5
SysCtrlRegs.HISPCP.all=0x1;//高速时钟的预定标器设置成除以2
SysCtrlRegs.LOSPCP.all=0x2;//低速时钟的预定标器设置成除以4
//根据需要时能各种外设模块的时钟
SysCtrlRegs.PCLKCR.bit.EVAENCLK=1;
SysCtrlRegs.PCLKCR.bit.EVBENCLK=0;
SysCtrlRegs.PCLKCR.bit.SCIAENCLK=0;
SysCtrlRegs.PCLKCR.bit.SCIBENCLK=0;
SysCtrlRegs.PCLKCR.bit.MCBSPENCLK=0;
SysCtrlRegs.PCLKCR.bit.SPIENCLK=0;
SysCtrlRegs.PCLKCR.bit.ECANENCLK=0;
SysCtrlRegs.PCLKCR.bit.ADCENCLK=1;
EDIS;
}
void InitLed(void)
{
volatileunsignedint* LED8 =(volatileunsignedint*)0x002000;
volatileunsignedint* LED7 =(volatileunsignedint*)0x002100;
volatileunsignedint* LED6 =(volatileunsignedint*)0x002200;
volatileunsignedint* LED5 =(volatileunsignedint*)0x002300;
volatileunsignedint* LED4 =(volatileunsignedint*)0x002400;
volatileunsignedint* LED3 =(volatileunsignedint*)0x002500;
volatileunsignedint* LED2 =(volatileunsignedint*)0x002600;
volatileunsignedint* LED1 =(volatileunsignedint*)0x002700;
volatileunsignedint* LEDWR =(volatileunsignedint*)0x002C00;
* LED8= CHAR_NULL;
* LED7= CHAR_d;
* LED6= CHAR_A;
* LED5= CHAR_NULL;
* LED4= CHAR_C;
* LED3= CHAR_DH;
* LED2= CHAR_3;
* LED1= CHAR_L;
* LEDWR=0x11;
}
interruptvoid adc_isr(void)
{
EALLOW;
GpioDataRegs.GPASET.all=0xFFFF;
* DAOUT= AdcRegs.ADCRESULT0;//将ADC中的转换结果直接赋给DA
//------中断服务程序中SGF添加的内容
if(buffIndex==1023)
{
buffIndex=0;
}
*(adBuff+(buffIndex++))=AdcRegs.ADCRESULT0;//共存储1024点,覆盖最早的数据
/*
if(countForLed++>6000) //在中断中计数,为LED切换提供延时
{
countForLed=0;
DispLed();
}
*/
//------
//重新初始化ADC采样序列
AdcRegs.ADCTRL2.bit.RST_SEQ1=1;//复位SEQ1
AdcRegs.ADCST.bit.INT_SEQ1_CLR=1;//清除中断位INT SEQ1
PieCtrlRegs.PIEACK.all=PIEACK_GROUP1;//清除PIE1的中断响应位
GpioDataRegs.GPACLEAR.all=0xFFFF;
EDIS;
}
void DispLed(void)
{
staticunsignedchar ca=1;//点睛之笔
switch(ca)
{
case1:
// LED8
*LED8= CHAR_NULL;
// LED7
*(LED8+0x100)= CHAR_NULL;
// LED6
*(LED8+0x200)= CHAR_2;
// LED5
*(LED8+0x300)= CHAR_2;
// LED4
*(LED8+0x400)= CHAR_2;
// LED3
*(LED8+0x500)= CHAR_0;
// LED2
*(LED8+0x600)= CHAR_1;
// LED1
*(LED8+0x700)= CHAR_2;
// WIRTE DATA TO LED
*(LEDWR)=0XFF;
ca++;
break;
case2:
// LED8
*LED8= CHAR_NULL;
// LED7
*(LED8+0x100)= CHAR_NULL;
// LED6
*(LED8+0x200)= CHAR_5;
// LED5
*(LED8+0x300)= CHAR_1;
// LED4
*(LED8+0x400)= CHAR_2;
// LED3
*(LED8+0x500)= CHAR_0;
// LED2
*(LED8+0x600)= CHAR_1;
// LED1
*(LED8+0x700)= CHAR_2;
// WIRTE DATA TO LED
*(LEDWR)=0XFF;
ca++;
break;
case3:
// LED8
// LED8
*LED8= CHAR_NULL;
// LED7
*(LED8+0x100)= CHAR_NULL;
// LED6
*(LED8+0x200)= CHAR_5;
// LED5
*(LED8+0x300)= CHAR_3;
// LED4
*(LED8+0x400)= CHAR_2;
// LED3
*(LED8+0x500)= CHAR_0;
// LED2
*(LED8+0x600)= CHAR_1;
// LED1
*(LED8+0x700)= CHAR_2;
// WIRTE DATA TO LED
*(LEDWR)=0XFF;
ca++;
break;
case4:
// LED8
*LED8= CHAR_NULL;
// LED7
*(LED8+0x100)= CHAR_NULL;
// LED6
*(LED8+0x200)= CHAR_9;
// LED5
*(LED8+0x300)= CHAR_2;
// LED4
*(LED8+0x400)= CHAR_1;
// LED3
*(LED8+0x500)= CHAR_0;
// LED2
*(LED8+0x600)= CHAR_1;
// LED1
*(LED8+0x700)= CHAR_2;
// WIRTE DATA TO LED
*(LEDWR)=0XFF;
ca=1;
break;
default:
ca=1;
}
}
/**************************************************×**/
/* ADC sample rate at 50KHz, bandpass 40-order FIR */
//----编译条件设置
#define _LEDSHOW 0
#define _ADC_SAPLPE_RATE_TEST 1
//---------------------------
#include "DSP281x_Device.h"
#define CHAR_NULL 0X00
#define CHAR_L 0X38
#define CHAR_DH 0X08
#define CHAR_C 0X39
#define CHAR_A 0X77
#define CHAR_d 0X5E
#define CHAR_0 0X3f
#define CHAR_1 0X06
#define CHAR_2 0X5b
#define CHAR_3 0X4f
#define CHAR_4 0X66
#define CHAR_5 0X6d
#define CHAR_6 0X7d
#define CHAR_7 0X07
#define CHAR_8 0X7f
#define CHAR_9 0X6f
#define ORDER 41
//LED地址
volatileint* LED8 =(volatileint*)0x2000;
volatileint* LEDWR =(volatileint*)0x2C00;
void ConfigureGpio(void);
void InitSystem(void);
void InitLed(void);
void DispLed(void);
interruptvoid adc_isr(void);
volatileunsignedint* DAOUT =(volatileunsignedint*)0x002900;
unsignedint countForLed=0;
unsignedint adBuff[ORDER]={0,};//--存放AD读取的值
int buffIndex=0;//adBuff中最新数值的索引
//adBuff [-------********...]
// |
// buffIndex tobe covered
int hdata[ORDER]=
{80,20,-71,-189,-325,-464,-593,-695,-756,-763,
-710,-593,-417,-192,67,339,603,836,1020,1136,
1177,1136,1020,836,603,339,67,-192,-417,-593,
-710,-763,-756,-695,-593,-464,-325,-189,-71,20,80};
// FIR滤波器系数
void main(void)
{
InitSystem();//初始化DSP内核寄存器
InitPieCtrl();//调用PIE控制单元初始化函数
InitPieVectTable();//调用PIE向量表初始化函数
InitAdc();//调用ADC模块的基本初始化函数
InitLed();
EALLOW;
PieVectTable.ADCINT=&adc_isr;//重新设置PIE向量表中ADc的中断入口向量
EDIS;
PieCtrlRegs.PIEIER1.bit.INTx6=1;//使能PIE中断分组1中的ADC中断
IER=1;//使能和ADC中断相连的CPU INT1中断
EINT;//使能全局中断位INTM
ERTM;//使能全局实时调试中断DBGM
/* SET ADC*/
AdcRegs.ADCTRL1.bit.SEQ_CASC=0;
AdcRegs.ADCTRL1.bit.CONT_RUN=0;
AdcRegs.ADCTRL1.bit.CPS=0;
AdcRegs.ADCMAXCONV.all=0x0000;//单通道转换
AdcRegs.ADCCHSELSEQ1.bit.CONV00=0x0;//将ADCINA0设置为SEQ1的第一个转换通道
AdcRegs.ADCTRL2.bit.EVA_SOC_SEQ1=1;//使能EVA引起的中断
AdcRegs.ADCTRL2.bit.INT_ENA_SEQ1=1;
AdcRegs.ADCTRL3.bit.ADCCLKPS=2;//ADc模块的核心时钟频率=HSPCLK/4
/* SET EVA*/
EvaRegs.GPTCONA.bit.TCMPOE=0;//禁止比较输出
EvaRegs.GPTCONA.bit.T1PIN=0;
EvaRegs.GPTCONA.bit.T1TOADC=2;//设置周期中断标志启动ADC
EvaRegs.T1CON.bit.FREE=0;//防真挂起时,定时器1立即停止工作
EvaRegs.T1CON.bit.SOFT=0;
EvaRegs.T1CON.bit.TMODE=2;//连续增计数模式
EvaRegs.T1CON.bit.TPS=2;//***()设置定时器时钟频率为HSPCLK/(2^TPS)
EvaRegs.T1CON.bit.TENABLE=1;//允许定时器操作
EvaRegs.T1CON.bit.TCLKS10=0;//内部时钟
EvaRegs.T1CON.bit.TCLD10=0;//计数器为0时重载
EvaRegs.T1CON.bit.TECMPR=0;//禁止比较操作
EvaRegs.T1PR=374;//***()定时器周期寄存器
/* SET GPIO*/
EALLOW;
GpioMuxRegs.GPAMUX.all=0x0;
GpioMuxRegs.GPADIR.all=0xFFFF;
EDIS;
while(1)
{
}
}
void InitSystem(void)
{
EALLOW;
SysCtrlRegs.WDCR=0x00E8;//禁止看门狗模块
SysCtrlRegs.PLLCR.bit.DIV=10;//将CPU的PLL倍频系数设为5
SysCtrlRegs.HISPCP.all=0x1;//高速时钟的预定标器设置成除以2
SysCtrlRegs.LOSPCP.all=0x2;//低速时钟的预定标器设置成除以4
//根据需要时能各种外设模块的时钟
SysCtrlRegs.PCLKCR.bit.EVAENCLK=1;
SysCtrlRegs.PCLKCR.bit.EVBENCLK=0;
SysCtrlRegs.PCLKCR.bit.SCIAENCLK=0;
SysCtrlRegs.PCLKCR.bit.SCIBENCLK=0;
SysCtrlRegs.PCLKCR.bit.MCBSPENCLK=0;
SysCtrlRegs.PCLKCR.bit.SPIENCLK=0;
SysCtrlRegs.PCLKCR.bit.ECANENCLK=0;
SysCtrlRegs.PCLKCR.bit.ADCENCLK=1;
EDIS;
}
void InitLed(void)
{
volatileunsignedint* LED8 =(volatileunsignedint*)0x002000;
volatileunsignedint* LED7 =(volatileunsignedint*)0x002100;
volatileunsignedint* LED6 =(volatileunsignedint*)0x002200;
volatileunsignedint* LED5 =(volatileunsignedint*)0x002300;
volatileunsignedint* LED4 =(volatileunsignedint*)0x002400;
volatileunsignedint* LED3 =(volatileunsignedint*)0x002500;
volatileunsignedint* LED2 =(volatileunsignedint*)0x002600;
volatileunsignedint* LED1 =(volatileunsignedint*)0x002700;
volatileunsignedint* LEDWR =(volatileunsignedint*)0x002C00;
* LED8= CHAR_NULL;
* LED7= CHAR_d;
* LED6= CHAR_A;
* LED5= CHAR_NULL;
* LED4= CHAR_C;
* LED3= CHAR_DH;
* LED2= CHAR_3;
* LED1= CHAR_L;
* LEDWR=0x11;
}
interruptvoid adc_isr(void)
{staticunsignedint flag=1;
int k=0;//FIR计算循环用
long sum=0;//FIR各抽头求和结果
int firIndex=0;//FIR滤波计算时所用的数组偏移量
EALLOW;
GpioDataRegs.GPASET.all=0xFFFF;
//* DAOUT= AdcRegs.ADCRESULT0; //将ADC中的转换结果直接赋给DA
//------中断服务程序中SGF添加的内容
#if _ADC_SAPLPE_RATE_TEST
/*测试ACD采样频率,不输入信号,观察输出反转频率即可*/
* DAOUT= AdcRegs.ADCRESULT0+flag*0x2000;
flag=(1U==flag?0:1U);
#endif
//采集&存储&FIR计算
#if !_ADC_SAPLPE_RATE_TEST
// *DAOUT= AdcRegs.ADCRESULT0;
if(buffIndex==ORDER)
{
buffIndex=0;
}
*(adBuff+(buffIndex))=AdcRegs.ADCRESULT0;//共存储ORDER点,覆盖最早的数据
firIndex=buffIndex;
//最后再buffIndex++,留着还有使用价值TAT
for(k=0;k<=buffIndex;k++)//计算firIndex前面部分
{
sum+=hdata[k]*adBuff[firIndex--];
}
firIndex=ORDER-1;//索引归位到数组最后
for(k=buffIndex+1;k<ORDER;k++)//计算firIndex后面部分
{
sum+=hdata[k]* adBuff[firIndex--];
}
*DAOUT=(unsignedint)(sum>>16);
buffIndex++;
#endif
/*
if(countForLed++>6000) //在中断中计数,为LED切换提供延时
{
countForLed=0;
DispLed();
}
*/
/* //------ */
//重新初始化ADC采样序列
AdcRegs.ADCTRL2.bit.RST_SEQ1=1;//复位SEQ1
AdcRegs.ADCST.bit.INT_SEQ1_CLR=1;//清除中断位INT SEQ1
PieCtrlRegs.PIEACK.all=PIEACK_GROUP1;//清除PIE1的中断响应位
GpioDataRegs.GPACLEAR.all=0xFFFF;
EDIS;
}
}