0 1 2 3 4

调试多重中断造成的问题

最近一直在调试一个M16C MCU的MP3播放的板子。M16C从UART端口读入周期为0.1秒的命令消息,根据命令执行Mp3播放、音量调节、播放呼叫声等功能。不执行任何命令时也会从HOST收到待机的命令。HOST发送的命令长度为13bytes,有关M16C的命令在前10bytes中,因此程序上忽视后面的3bytes。

这个简单的程序却出现一个令人头疼的问题:虽然它一直接受的是待机命令,但是却有一天一次左右的几率会启动播放呼叫声的功能,也即是说平均每接收864000次命令,大约会有一次误操作。

由于M16C的调试器年久失修,已经不能使用,只能用flash writer将添加了调试代码的编译好的程序写入芯片,然后根据调试程序通过UART端口的输出分析程序内部运作。这样的调试费时费力,而且不知道它会什么时候出问题。

于是我用Python写了两个小程序,一个用pySerial通过COM口接受调试程序发送的数据,一个用pyAudio监视呼叫声,如果出现呼叫声则录下来并且记录下发生的时刻。通过这两个小程序,逐步缩小问题出现的可能的范围,最终发现M16C内部的全局变量会出现意想不到的值:例如写全局变量music_cmd的语句只有music_cmd = receive_buffer[0] & 0x0f;这么一句,但是通过COM接受到的值却会变成0xaa这样的值。完全没有按照'&'运算符进行计算。

出现这样的问题只有两个可能:某段内存出现缓存区溢出,在汇编级别出现了问题。经过对程序的反复核查,没有发现出现缓存区溢出的现象,于是只好查看c语言编译之后的汇编语句,结果找到了原因所在。

M16C的中断服务程序有两种模式:INTERRUPT, INTERRUPT/B,前者会在执行中断时将所有的寄存器压住堆栈中,而后者则使用M16C特有的register bank,register bank是一套和正常寄存器一样的拷贝,也就是说所有的寄存器都有bank0和bank1,普通程序使用bank0,INTERRUPT/B中断服务程序使用bank1。register bank不像堆栈能保存多次中断的值,只能保存一次中断。因此它不适合多重中断,即中断服务程序中响应别的中断。而出问题的这个程序中所有的中断都用的是INTERRUPT/B模式,并且允许多重中断。这样在时钟中断处理接收到的消息时,可能会响应UART接收中断。

具体原因如下:

在时钟中断服务程序中有如下语句,由于是中断,它已经在使用bank1

music_cmd = receive_buffer[0] & 0x0f;
把它翻译成汇编,大致如下:
load _receive_buffer a
and a , 0fh
store a, _music_cmd
如果在and和store语句之间产生UART中断,则转到UART中断服务程序运行,此服务程序没有将寄存器push到堆栈之上,而是和时钟中断一样使用register bank1。在UART中断服务程序中使用寄存器a进行其它运算工作,这样当它返回时,已经改变了寄存器a的值。因此_music_cmd中出现了意想不到的值。

将所有的中断服务程序的模式都该为INTERRUPT,就解决了这个问题。这是一个典型的很难调试的问题,通过调试它总结了如下几条经验:

  • 要尽量动手写一些小工具帮助调试,写小工具可以一劳永逸,解放劳动力。
  • 当出现C语言级别找不到原因时,应该考虑查看调试编译后的汇编代码。
  • 除了程序逻辑之外,编译器的配置、内存的分配等都有可能造成BUG。

最后展示一下那个用pyAudio监视声音的小程序:

 01from pyaudio import PyAudio, paInt16
02import numpy as np
03from datetime import datetime
04import wave
05 
06 
07def save_wave_file(filename, data):
08    wf = wave.open(filename, 'wb')
09    wf.setnchannels(1)
10    wf.setsampwidth(2)
11    wf.setframerate(SAMPLING_RATE)
12    wf.writeframes("".join(data))
13    wf.close()
14 
15 
16NUM_SAMPLES = 2000
17SAMPLING_RATE = 8000
18LEVEL = 1500
19COUNT_NUM = 20
20SAVE_LENGTH = 8
21 
22pa = PyAudio()
23stream = pa.open(format=paInt16, channels=1, rate=SAMPLING_RATE, input=True, frames_per_buffer=NUM_SAMPLES)
24 
25save_count = 0
26save_buffer = []
27 
28while 1:
29    string_audio_data = stream.read(NUM_SAMPLES)
30    audio_data = np.fromstring(string_audio_data, dtype=np.short)
31    large_sample_count = np.sum( audio_data > LEVEL )
32    print np.max(audio_data)
33    if large_sample_count > COUNT_NUM:
34        save_count = SAVE_LENGTH
35    else:
36        save_count -= 1
37 
38    if save_count < 0:
39        save_count = 0
40 
41    if save_count > 0:
42        save_buffer.append( string_audio_data )
43    else:
44        if len(save_buffer) > 0:
45            filename = datetime.now().strftime("%Y-%m-%d_%H_%M_%S") + ".wav"
46            save_wave_file(filename, save_buffer)
47            save_buffer = []
48            print filename, "saved"


用TSP信号测量系统的脉冲响应

RY DSP开发 2008/06/10

在测量系统的脉冲响应的时候,如果直接使用脉冲信号的话,信噪比会很低。因此一般都采用TSP(Time-Stretched Pulse)信号测量。TSP信号将脉冲的能量平均分散到时间轴上,将TSP信号输入到系统中获取其响应之后,再将分散的能量还原,重新构成系统的脉冲响应。能量分散的规则为:脉冲的相位和频率的平方成比例。重构脉冲响应的时候,只需要将系统的TSP响应与逆TSP信号进行卷积即可。

TSP信号和逆TSP信号的设计在频域进行。TSP的频域特性为:

H(k) = exp(j*4*pi*M*k^2/N^2)  0<=k<=N/2
H(k) = H(N-k)                 N/2

逆TSP的频域特性为

G(k) = 1/H(k)

其中N为TSP信号的长度,M为TSP实际数据长度的2倍(整数)。由于M为整数,我们很容易看到当k=N/2的时候,H(k)是一个实数,并且频域上有共轭对称的关系,所以对H(k)进行IFFT运算之后将得到一组实数,这就是TSP信号。

下面是Python求TSP信号的程序和TSP(512, 0.5)信号的波形图。

01from numpy import *
02def TSP(N, M):
03    M = int(N*M/2)
04    freq = array(range(N/2+1), "h")
05    H = exp(1j * 4*M*pi*(freq)**2.0/(N**2.0))
06    H = hstack( (H, conj( H[-2:0:-1] ) ))
07    x = fft.ifft(H)
08    x = real( x )
09    x = hstack( (x[N/2-M:], x[:N/2-M]) )
10    return x
11     
12if __name__ == "__main__":
13    import pylab
14    tsp = TSP(512, 0.5)
15    for item in tsp:
16        print item, ","
17    pylab.plot(tsp)
18    pylab.show()

参数M决定了TSP信号的有效参数,有效长度越大,信噪比越好。然而由于TSP信号在时间轴上的收敛需要一定的时间,有效长度太大的话,TSP信号的收敛特性将受到影响。

采用上述的TSP信号我实际测量了一下排气管的脉冲响应,测量用的DSP程序大致流程如下:

  1. 从音箱连续输出REPEAT次长度为N的TSP信号
  2. 被测量系统的脉冲响应长度为I
  3. 两次TSP信号之间的间隔(上一个TSP信号结束到下一个TSP信号开始的时间)为T, T>I
  4. 从麦克风输入系统对TSP信号的响应,长度为N+T,对REPEAT次TSP响应求平均
  5. 把平均之后的TSP响应和逆TSP信号就卷积,要计算逆TSP信号,只需要反转TSP信号即可
  6. 卷积结果中的N到N+I部分就是系统的脉冲响应


下面是这部分的示例程序

01#define I 512
02#define N 512
03#define T 1024
04#define L (N+T)
05#define RI (N+L-1)
06#define REPEAT 10
07 
08extern const float TSP_DATA[];
09 
10float TSP[N];
11float TSP_INV[N];
12float TSP_RESPONSE[L];
13float IMPULSE_RESPONSE[I];
14int tsp_index = 0;
15int tsp_count = 0;
16 
17void InitTSPData(){
18    int i;
19    for(i=0;i<N;i++){
20        TSP[i] = TSP_DATA[i];
21        TSP_INV[i+1] = TSP_DATA[N-1-i];
22    }
23    TSP_INV[0] = 0;
24    for(i=0;i<L;i++){
25        TSP_RESPONSE[i] = 0;
26    }
27    tsp_index = 0;
28    tsp_count = 0;
29}
30 
31void conv(float x[], float y[], float z[], int lenx, int leny, int startz, int lenz)
32{
33    int k, j;
34    int l = lenx+leny-1;
35    int min, max;
36    float s;
37    for(k=startz;k<startz+lenz;k++){
38        s=0;
39        min = (k-leny+1<0) ? 0 : (k-leny+1);
40        max = (k<lenx-1) ? k : (lenx-1);
41        for(j=min;j<=max;j++){
42            s += x[j]*y[k-j];
43        }
44        z[k-startz] = s;
45    }
46}
47 
48void main()
49{
50    InitTSPData();
51}
52 
53void DoSample()
54{
55    float out;
56    TSP_RESPONSE[tsp_index] += InputSample();
57     
58    if(tsp_index < N){
59        out = TSP[tsp_index];
60    }
61    else{
62        out = 0;
63    }
64    tsp_index ++;
65    if(tsp_index == L){
66        tsp_index = 0;
67        tsp_count ++;
68        if(tsp_count == REPEAT){
69            for(i=0;i<L;i++){
70                TSP_RESPONSE[i] /= tsp_count;
71            }
72            conv(TSP_RESPONSE, TSP_INV, IMPULSE_RESPONSE, L, N, N, I);
73            asm(" nop");
74        }
75    }
76    OutputSample(out);
77}

 

用此程序实际测量的排气管脉冲响应图如下:


二次IIR滤波器

RY DSP开发 2007/09/26

在声音信号处理中,经常需要设计滤波器,二次IIR滤波器由于其参数设计简单,运算量少,运用的最为广泛。这里介绍几种常用的滤波器的参数计算。

Peak Equalizer

Peak Equalizer的设计参数为中心频率f,最大增益db,形状参数Q。下面这个Python小程序通过这三个参数计算滤波器的参数a和b,取样频率为44100Hz。

01def peak(f, db, Q):
02    A = 10.0**(db/40.0)
03    w0= 2*math.pi*f/44100.0
04    alpha = math.sin(w0)/2/Q
05    b0 = 1+alpha*A
06    b1 = -2*math.cos(w0)
07    b2 = 1-alpha*A
08    a0 = 1+alpha/A
09    a1 = -2*cos(w0)
10    a2 = 1-alpha/A
11    b0 /= a0
12    b1 /= a0
13    b2 /= a0
14    a1 /= a0
15    a2 /= a0
16    a0 /= a0
17    return [b0,b1,b2],[a0,a1,a2]
 


高、低通滤波器

二次高、低通滤波器的滤波倾斜度为-12dB/oct ,只有一个参数:滤波频率f。

01def lpf(f):
02    a = [1.0, math.sqrt(2), 1.0]
03    b = [0.0, 0.0, 1.0]
04    ad = [0.0] * 3
05    bd = [0.0] * 3
06    fs = 44100.0
07    f = 2*math.pi*f
08    b[2] *= f*f
09    a[1] *= f
10    a[2] *= f*f
11    ad[0] = a[2] - a[1]*2*fs + 4*fs*fs*a[0]
12    ad[1] = a[2]*2 - 8*fs*fs*a[0]
13    ad[2] = a[2] + 2*fs*a[1] + 4*fs*fs*a[0]
14    bd[0] = b[2] - b[1]*2*fs + 4*fs*fs*b[0]
15    bd[1] = b[2]*2 - 8*fs*fs*b[0]
16    bd[2] = b[2] + b[1]*2*fs + 4*fs*fs*b[0]
17    t = ad[2]
18    return [bd[2]/t, bd[1]/t, bd[0]/t], [ad[2]/t, ad[1]/t, ad[0]/t]
 

01def hpf(f):
02    a = [1.0, math.sqrt(2), 1.0]
03    b = [0.0, 0.0, 1.0]
04    ad = [0.0] * 3
05    bd = [0.0] * 3
06    fs = 44100.0
07    f = 2*math.pi*f
08    b[0],b[2] = b[2],b[0]
09    a[1] *= f
10    a[2] *= f*f
11    ad[0] = a[2] - a[1]*2*fs + 4*fs*fs*a[0]
12    ad[1] = a[2]*2 - 8*fs*fs*a[0]
13    ad[2] = a[2] + 2*fs*a[1] + 4*fs*fs*a[0]
14    bd[0] = b[2] - b[1]*2*fs + 4*fs*fs*b[0]
15    bd[1] = b[2]*2 - 8*fs*fs*b[0]
16    bd[2] = b[2] + b[1]*2*fs + 4*fs*fs*b[0]
17    t = ad[2]
18    return [bd[2]/t, bd[1]/t, bd[0]/t], [ad[2]/t, ad[1]/t, ad[0]/t]
 




shelving滤波器

Shelving滤波器提升或者抑制高频或者低频的增益,有三个参数: 中心频率f,增益db,类型type。type为0时,对低频进行增益处理,为1时对高频进行增益处理。

01def shelving(f, db, type):
02    if type==1:
03        f = 22050 - f
04    Wb = 2.0*f/44100.0*math.pi
05    K = math.tan(Wb/2)
06    g = 10.0 ** (db/20.0)
07    V = g ** (1.0/2.0) - 1
08    a0 = 1 + 2*K + K*K
09    a1 = 2*K*K - 2
10    a2 = 1 - 2*K + K*K
11    b0 = a0
12    b1 = a1
13    b2 = a2
14    b0 = b0 + 2*V*K*(K+1)
15    b1 = b1 + 2*V*K*(2*K)
16    b2 = b2 + 2*V*K*(K-1)
17    b0 = b0 + V*V*K*K*1
18    b1 = b1 + V*V*K*K*2
19    b2 = b2 + V*V*K*K*1
20    b0 = b0 / a0
21    b1 = b1 / a0
22    b2 = b2 / a0
23    a1 = a1 / a0
24    a2 = a2 / a0
25    a0 = a0 / a0
26    if type==1:
27        b1 = -b1
28        a1 = -a1
29    return [b0,b1,b2],[a0,a1,a2]
 


C++调用C的函数

RY DSP开发 2007/07/29

以前从来没有写过C++的程序,最近一个项目中的源程序都是cpp文件,需要调用一个用C语言写好的函数库,结果遇到了大麻烦。

这个库是别的公司提供的,包含一个库文件和一个头文件,还有调用的例子。这个例子能毫无问题地编译连接。但是把这个库文件放到我们自己的工程中,link时却总是报错,说是找不到我们要调用的函数。经过一番比较,发现我们的工程和那个例子之间唯一的差别就是:

例子程序的主函数所在的文件名是main.c,而我们工程中的文件名是main.cpp。这个问题总结一下就是:在main.c文件中的main函数中调用库函数没有问题,而在main.cpp文件中的main函数中调用库函数,就出现连接错误。

google一下,发现了解决方法:

把包含头文件的那句#include  "***.h"改为:

extern  "C"{
#include  "***.h"
}

那么为什么这么做就可以解决问题了呢?这是因为C++和C对函数符号所做的修改不一样。也就是说同样一个函数名,用C和C++编译之后的符号是不同的,这样连接是就出现了问题。extern "C"就是告诉编译器,它其中包含的所有符号全部按照C语言的规定来做,这样就能够正常连接C编译器编译的库函数了。关于这个问题的详细信息,可以google: extern "C"。有不少好文章介绍。


McBSP的DX引脚的状态

RY DSP开发 2007/07/13

DSP reset之后,不对McBSP进行任何处理,其DX引脚的状态会是怎样的呢?

最近我碰到了这个问题。硬件上McBSP0、1、2的DX引脚分别与不同的DAC相连,帧信号和时钟信号统一设置为外部输入。一般情况下,这3个McBSP都处于工作状态,不过在某种特殊的配置下,McBSP2不需要输出,这种情况下,DSP启动的时候,通过别的引脚判断是否需要McBSP2的输出。

既然不需要输出,那么我的程序就跳过了McBSP2的初始化,使其处于DSP reset之后的缺省工作状态,我认为McBSP2处于reset状态的话,DX2引脚上不会有输出,因此DAC也不会输出声音波形。

可是在实际调试过程中,与McBSP2相连的声音输出端口偶尔会出现巨大的爆裂噪声。这种噪声显然是数字信号噪声,而非电路中的模拟信号的噪声。 因此推断DAC从DX2引脚读入了非0数据。而McBSP2处于reset状态,应该不会输出信号的。

这个问题困扰了我很久,最后我终于发现,在Reset状态下,McBSP不控制DX引脚,因此DX引脚处于浮动状态,硬件上的其它回路或者引脚可能会产生噪声使得DX引脚在0,1之间跳变,因为帧信号和时钟信号一直都有,因此DX引脚的这些变化会被DAC转换为模拟信号输出。这样的噪声因此毫无规律,而且通常音量很大。

那么怎么解决这个问题呢?初始化McBSP2之后,输出0当然是一种解决办法,不过这样太麻烦了。我直接配置McBSP2的PCR寄存器,设置McBSP2的DX引脚为泛用I/O模式,并且为输出低电压的状态。这样DX引脚就完全由McBSP控制了,不会因为其它电路的影响而上下浮动。

这样这个问题就完全解决了。

 


计算波形的包络

RY DSP开发 2007/04/13

前面的文章中介绍过如何产生Sine Sweep波(频率扫描正弦波)。今天我们来看看如何运用这种波形来测量滤波器的频率响应。

假设我们要测量某种mp3播放器的均衡器的频率响应。先把sweep波的wave文件保存到mp3播放器中,然后我们用电脑分别录下 开/关 均衡器时的播放结果。由于录音回路的频率响应可能不为常数,因此需要录下关掉均衡器时的结果。如果用wave编辑软件看这两个录音结果的话,就可以大致看出均衡器的频率响应了。例如下面是我做的的一个例子,为了特意让关均衡器的时候频率响应不恒定,我打开了一个高通滤波器。

均衡器关闭时
均衡器开启时

通过看这两个波形,均衡器的频率响应一目了然。但是我们希望把它做成图,测量出中心频率和最大增益。这就需要检测出两个波形的包络,把同时刻两个包络的差值找出来。

下面我们来看看如何计算出包络。这个算法很简单:用一个变量e去跟踪输入x的变化,当x大于e时,e以很快的速度向x靠近,当x小于e时,e慢慢地靠近x

01ATTACK_TIME = 0.001
02RELEASE_TIME = 0.5
03SAMPLINGE_RATE = 44100
04def findenvelop(data):
05    envelop = []
06    e = 0.0
07    ga, gr = exp(-1.0/(SAMPLINGE_RATE*ATTACK_TIME)),
08exp(-1.0/(SAMPLINGE_RATE*RELEASE_TIME))
09    ga1, gr1 = 1.0 - ga, 1.0 - gr
10    for x in data:
11        if e < x:
12            e *= ga
13            e += ga1*x
14        else:
15            e *= gr
16            e += gr1*x
17        envelop.append(e)
18    return envelop
 

分别对上面的两个声音文件进行包络计算,然后将相应频率的数值的差(实际上是求商)转换成dB,绘制成如下的频率响应图,同时用程序找出差的最大值: 10dB,在300Hz附近。



整数开平方算法

RY DSP开发 2007/04/12
本文介绍一种计算整数的开方算法。本算法只采用移位、加减法、判断和循环实现,因为它不需要浮点运算,也不需要乘除运算,因此可以很方便地运用到各种芯片上去。

我们先来看看10进制下是如何手工计算开方的。
先看下面两个算式,
x = 10*p + q  (1)
公式(1)左右平方之后得:
x^2 = 100*p^2 + 20pq + q^2 (2)
现在假设我们知道x^2和p,希望求出q来,求出了q也就求出了x^2的开方x了。
我们把公式(2)改写为如下格式:
q = (x^2 - 100*p^2)/(20*p+q) (3)

这个算式左右都有q,因此无法直接计算出q来,因此手工的开方算法和手工除法算法一样有一步需要猜值。

我们来一个手工计算的例子:计算1234567890的开方

首先我们把这个数两位两位一组分开,计算出最高位为3。也就是(3)中的p,最下面一行的334为余数,也就是公式(3)中的(x^2 - 100*p^2)近似值

       3
---------------
| 12 34 56 78 90
9
---------------
| 3 34

下面我们要找到一个0-9的数q使它最接近满足公式(3)。我们先把p乘以20写在334左边:

       3  q
---------------
| 12 34 56 78 90
9
---------------
6q| 3 34

我们看到q为5时(60+q*q)的值最接近334,而且不超过334。于是我们得到:

       3  5
---------------
| 12 34 56 78 90
9
---------------
65| 3 34
| 3 25
---------------
9 56

接下来就是重复上面的步骤了,这里就不再啰嗦了。

这个手工算法其实和10进制关系不大,因此我们可以很容易的把它改为二进制,改为二进制之后,公式(3)就变成了:

q = (x^2 - 4*p^2)/(4*p+q) (4)

我们来看一个例子,计算100(二进制1100100)的开方:

      1  0  1  0
---------------
| 1 10 01 00
1
---------------
100| 0 10
| 0 00
---------------
| 10 01
1001| 10 01
---------------
0 00

这里每一步不再是把p乘以20了,而是把p乘以4,也就是把p右移两位,而由于q的值只能为0或者1,所以我们只需要判断余数(x^2 - 4*p^2)和(4*p+1)的大小关系,如果余数大于等于(4*p+q)那么该上一个1,否则该上一个0。

下面给出完成的C语言程序,其中root表示p,rem表示每步计算之后的余数,divisor表示(4*p+1),通过a>>30取a的最高2位,通过a<<=2将计算后的最高2位剔除。其中root的两次<<1相当于4*p。程序完全是按照手工计算改写的,应该不难理解。

01unsigned short sqrt(unsigned long a){
02  unsigned long rem = 0;
03  unsigned long root = 0;
04  unsigned long divisor = 0;
05  for(int i=0; i<16; i++){
06    root <<= 1;
07    rem = ((rem << 2) + (a >> 30));
08    a <<= 2;
09    divisor = (root<<1) + 1;
10    if(divisor <= rem){
11      rem -= divisor;
12      root++;
13    }
14  }
15  return (unsigned short)(root);
16}
 


Run-time和load-time初始化

RY DSP开发 2007/03/01

在CCS的build options中的linker选项卡中,Autoinit Model有两个选项:Run-Time Autoinitialization和Load-Time Initialization。本文简单介绍一下这两个选项的区别和用法。

因为这两个选项和.cinit和.bss Section有关,所以先简单的介绍一下这两个section相关的知识。从C语言的角度来看的话,.bss section是用来存放C语言中的全局变量的。而.cinit则用来存放全局变量的初始值。例如如果有下面的全局变量buf的话,

short buf[4] = {0x01, 0x02, 0x03, 0x04};

那么buf放在.bss中,而初始值0x01, 0x02, 0x03, 0x04放在.cinit中。.cinit中储存的实际上是一个copy table,它对于每个需要初始化的全局变量,都有一个复制项与之对应,以55x系为例,上面的这段程序产生的复制项为:

00 04 00 12 34 00 00 01 00 02 00 03 00 04 
----- ----------- ------------------------
1 2 3
  1. 复制的word数
  2. 复制的目标地址,也就是buf的地址(这里假设为0x1234)
  3. 要复制的数据,也就是初始化数据0x01, 0x02, 0x03, 0x04

那么对于这个.cinit中的copy table具体由谁来完成复制操作呢,这样就有了Load-Time和Run-Time的这两个选择。

先来看看Run-Time Autoinitialization。这段英文的的意思是“运行时初始化”,实际上就是在main函数之前被运行的c_int00中的一段代码完成这个复制工作。我们知道c_int00是用来初始化C语言程序运行所需要的环境的,这个初始化的一部分就是初始化全局变量的初始值。因此在c_int00中初始化全局变量是理所当然的。

然而这样做存在一个问题:.cinit中的copy table只在c_int00中用一次,如果把它放在DSP的on chip RAM中的话,实在是太浪费了。因此通常的做法是将.cinit放到flash内存中。假设系统没有flash内存,而是采用的serial boot之类的启动方式,由别的芯片通过McBSP将DSP的程序传输到DSP中的话,我们就不得不把.cinit放到RAM中了。如果初始化数据很多的话,显然是非常浪费内存的。为了解决这个问题,可以采用Load-Time Initialization。

所谓Load-Time Initialization,就是在将程序load进DSP内存的同时,初始化.bss中的全局变量。我们以55x系列的serial boot为例来解释一下Load-Time Initialization。out文件的格式比较复杂,所以通常都是先通过hex5x工具将out文件转化为一个boot table格式的文件,然后通过DSP ROM中的boot程序接收并分析这个boot table完成各个section的初始化工作,最后跳转到c_int00运行。boot table的格式和.cinit中的copy table类似,都有复制个数,目标地址以及需要复制的数据。例如下图是hex5x产生的boot table的格式。

DSP ROM中的serial boot程序一边接收这个boot table的数据一边将数据复制到相应的地址中去。因此如果能将.cinit中的copy table转换到boot table中去的话,就不需要在c_int00中对.cinit进行操作了,也就不需要额外的内存用来储存.cinit了。只要选择了Load-Time Initialization选项,这个工作就都由计算机自动完成了。我们来看看它具体是如何做的。

当以Load-Time Initialization方式对项目进行link的时候,linker会在输出的out文件中的.cinit的section header中添加一个STYP_COPY标签。这个标签会改变hex5x工具对这个section的操作。也就是说当hex5x发现某个section header中有STYP_COPY标签的话,它就把这个section中的数据当作copy table一项一项添加到boot table中去了。因此最终产生的boot table不是将整个.cinit复制到DSP的内存中,而是将.cinit中的每个复制项复制到.bss相应的位置。这样就在boot load的同时完成了全局变量的初始化工作。


产生Sine Sweep波

在滤波器设计中,经常需要知道滤波器的频率特性。测量滤波器的脉冲响应,能够精确地获取频率特性。但是如果无法进行精确的数字的脉冲响应运算的话,那么用Sine Sweep波(频率逐渐变高的正弦波,也叫做频率扫描)来测量系统的频率特性是再适合不过的了。例如我们可以用Sine Sweep波来测量某个Mp3播放器的的频率特性,以及设置不同的EQ(均衡器)之后的频率特性。我们可以让播放器播放某个特定的Sine Sweep文件,用录音设备记录下它的输出声音,查看录下来的波形就能大概知道系统的频率特性了。

为了符合人类的听觉特性,通常频率响应图的频率轴都采用对数坐标。因此我们要产生的Sweep波的频率变化就需要成指数增长。下面具体谈谈如何推导出这种 Sine Sweep波的函数方程。Sweep波的频率需要根据时间成指数增长,因此它的函数方程为

freq(f) = f0*k^t

其中f0为初始频率,k为增长系数,例如如果我们要产生f0 - f1的t0秒的Sweep波 的话,那么

k = exp(log(f1/f0)/t0)

我们知道频率为F的正弦波的函数为

sweep(t)=sin(2*PI*F*t)

那么把频率的变化 方程带入此式得

sweep(t)=sin(2*PI*f0*k^t*t)

这个公式是否正确,我写了一个程序测试,程序按照f0=20Hz, f1=20kHz, t0=50s 设置,结果发现37秒左右的时候就已经达到了20kHz的频率了。所以这个公式是错误的。

那么错在什么地方呢?我们从频率的基本概念入手,重新考虑这个问题。所谓频率就是相位的变化率,所以频率是相位的导数,相位是频率的积分。当频率函数为常数F也就是freq(t)=F时,那么相位函数为freq(t)的积分与2PI的乘积,phase(t)=2*PI*F*t,这样恒定频率F的正弦波的方程才是sin(2*PI*F*t)。

如果频率函数为

freq(t) = f0*k^t

则计算相位函数需要对f0*k^t积分,得

phase(t) = 2*PI*f0*(k^t-1)/log(k)

所以正确的Sine sweep波的函数为:

sweep(t) = sin(2*PI*f0*(k^t-1)/log(k))

这里给出一个输出Sine Sweep Wave文件的Python程序 。其核心的计算部分为:

01def sinesweep(f0, f1, sweeptime, samplingrate, peak):
02   k = exp(log(float(f1)/f0)/sweeptime)
03   data = array.array("h", "\x00"*sweeptime*samplingrate*2)
04   dt = 1.0/samplingrate
05   t = 0.0
06   p = 2*pi*f0/log(k)
07   for i in xrange(sweeptime*samplingrate):
08       data[i] = int(peak*sin( p*(pow(k,t)-1) ))
09       t += dt
10   return data
 

第四个参数是输出波形的振幅,最大为0x7fff。由于Python本身不太适合这样的重量级计算,因此程序运行需要一些时间,如果安装了psyco库的话,取消

import psyco
psyco.profile()

两行的注释能够加速程序3倍左右。以后我会详细讲解如何利用这个Sine Sweep测量Mp3播放器的频率响应。


用Python分析COFF文件

TI的编译器,连接器等工具产生的目标文件格式为Common Object File Format(简称COFF)。COFF格式是AT&T开发的,在UNIX系统中所采用的目标文件格式。TI的编译器产生的obj文件以及连接之后产生的out文件都是COFF格式的。用CCS载入out文件到DSP的内存的时候,CCS会分析这个out文件,将其中储存的代码段和初始数据等段中的数据复制到DSP的内存中,并且设置程序指针到Entry Point(入口地址)。Hex5x, Hex6x等转换工具通过分析out文件,将其输出为一个boot table,方便各种自举(自启动)程序的编写。例如我们经常把这样的boot table写入flash, 制作成能从flash自举的应用程序。

了解COFF文件的格式,有助于我们了解一些自启动的细节,明白CCS loader和hex转换工具的工作原理,或者编写一些特殊用途的启动程序。例如多启动或者部分更新正在运行的程序等等。

在TI的文档中有COFF文件格式的详细介绍。例如TMS320C55x Assembly Language Tools User’s Guide或者TMS320C6000 Assembly Language Tools User's Guide等。文档很详细,下面只是简单介绍一下COFF文件的基本构造。

 COFF最开始是文件头和附加文件头,附加文件头中保存有Entry Point的地址。其后就是Section Header表,其中有各个section的物理地址、长度以及初始化数据的在文件中的偏移地址等信息。所有的section header结束之后就是数据区了。也就是需要复制到DSP 内存中去的数据。其后还有连接用的relocation信息,如果out文件的话不需要这些信息,relocation之后是符号表,例如C语言的全局变量,函数等符号都在这个表中储存。最后是字符串表,section的名称或者符号名称太长的时候就把这些名称字符串储存到字符串表中。
根据这样的结构,自己动手写一个out->boot table的程序其实也很简单,只需要从out文件中获取entry pointer、需要复制的各个section的地址、长度以及数据即可。

这里给出一个Python程序示范:COFF Reader ,它可以显示出out文件的结构。此程序只是一个示范,并无实用价值,参照它阅读COFF格式的文档也许会更加容易一些。运行此程序需要事先在电脑中安装Python 2.4。解压之后运行coff.py文件即可。左边是程序的界面截图。

 


0 1 2 3 4