食用清单:正点原子探索者开发板v2.2,主控STM32F407ZGT6,DDS模块AD9959(康威科技)

07月19日

前天晚上实现了高频信号同步,昨天晚上实现了相位可控。

原理上很简单:使用外部高频信号取代DDS上的晶振,直接将DDS的时钟挂在外部信号上,这样DDS输出的波形就一定是与外部信号同步的了。

工作前的基础:已经会使用DDS输出波形。我的驱动是在github上找的一个用hal库驱动AD9959(仅移植了这个项目的ad9959.c与ad9959.h,剩下的main和gpio什么的自己随便写写)。

由于我们要求输出信号与输入信号同频,而DDS的输出信号是数字合成出来的,因此DDS运行的主频是一定要比这个频率高几倍的,因此这里我们需要开启DDS内部的锁相环倍频器。

查看AD9959手册上的Frequency Range项下的REFCLK Multiplier Enabled子项可知在开启倍频器的情况下,外部输入的参考时钟频率范围为10-125MHz。

私人吐槽

> 这个手册上这个表格的缩进做错了……

> 从Internal VCO Output Frequency Range那一行开始下面的都需要往前减一个缩进……

> 甚至这是整个手册第4页就出现的表格1……也太不上心了……

事实上,如果查看手册后面的Reference Clock Modes章节可以发现如下这一段:

…As an alternative to clocking the part directly with a high frequency clock source, the system clock can be generated using the internal, PLL-based reference clock multiplier. …

Enabling the PLL allows multiplication of the reference clock frequency from 4× to 20×, in integer steps. …

Note that the output frequency of the PLL is restricted to a frequency range of 100 MHz to 500 MHz. However, there is a VCO gain control bit that must be used appropriately. The VCO gain control bit defines two ranges (low/high) of frequency output. The VCO gain control bit defaults to low (see Table 1 for details).

关键信息:PLL锁相环可以将参考时钟倍频,从4倍到20倍,以1倍为步进!

再结合上面的Table 1,在VCO gain control bit默认设低的情况下,内部的VCO输出频率范围为100~160MHz;而在设高的情况下,VCO输出频率范围为255-500MHz。这个VCO gain control bit在哪呢?在FR1寄存器里,跟倍频系数(接下来我们还会再用到这个系数)在一起。

也就是说这两个东西是在编程的时候可以自己写的一个寄存器。查看我的ad9959.c代码里面的Init函数,里面是这么写的:

1
2
3
4
5
6
7
8
uint8_t FR1_DATA[3] = {0xD3, 0x00, 0x00}; // 16 frequency doubling
uint8_t CFR_DATA[3] = {0x00, 0x03, 0x00}; // default Value = 0x000302
_Init_AD9959_GPIO();
InitIO_9959();
InitReset();

WriteData_AD9959(FR1_ADD, 3, FR1_DATA);
WriteData_AD9959(CFR_ADD, 3, CFR_DATA);

稍加计算可以得知这个FR1寄存器里写的0xD30000是0b1101 0011 0000 0000 0000 0000(按照他的分割方法的话是0b1 10100 11 000……),也就是说VCO gain control设为了1,而PLL divider ratio为2010(即0b10100),也就是说我们对REFCLK进行20倍频,并且倍频结果是大于255MHz的。查看模块上外置晶振的丝印可知晶振频率为25MHz,20倍频之后是500MHz,刚好跑在AD9959的上限频率上。那么我们改接跳线帽,使用外接信号取代晶振的时候,外界信号的频率范围就应该为:

这个25MHz看起来是不是不太高?离Table 1上标注的最高125MHz还差得远?这是由于除数这个20比较大。如果我们把PLL divider ratio改到下限410,那么外界信号的频率范围就会是:

这个频率看起来就高多了。在实际应用中,我们可以根据实际输入的信号频率来调整这个PLL divider ratio的取值,找到一个合适的范围。如果频率非常低,甚至低于前面那个12.75MHz,也可以把VCO gain control bit改为0,这样被除数就会从255-500MHz降低到100-160MHz。

言归正传,我们现在用一个最粗暴的方法先来尝试一下信号同步。既然板上的晶振跑在25MHz,那我们就把跳线帽从接晶振改接为接SMA接口,然后用信号源给一个25MHz信号,编程让DDS也输出一个25MHz的信号,按理说这个合成的25MHz应该和外界参考25MHz同步。但是放到示波器上却发现波形在缓慢移动,并且速度还很均匀,没有左右来回晃动或者时快时慢,而是均匀缓慢地移动。这说明我们输出的波形虽然是“来源于”外部参考信号,但是设置的频率并没有设得跟外部参考信号完全一致。

为了解决这个问题,我们来看一下,当我们在代码中“设置频率”的时候,我们到底在做什么?下面贴一下我拿到的原始代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @brief Writes the frequency value to the specified channel.
*
* @param Channel The channel number to write the frequency to.(0 to 3)
* @param Freq The frequency value to be written.(1 to 500000000)
*/
void Write_Frequence(uint8_t Channel, uint32_t Freq)
{
if (Freq > 500000000 || Freq < 1)
{
Freq = 114514;
AD9959_error();
}

uint8_t CFTW0_DATA[4] = {0x00, 0x00, 0x00, 0x00};
Freq2Word(Freq, CFTW0_DATA);
Channel_Select(Channel);
WriteData_AD9959(CFTW0_ADD, 4, CFTW0_DATA); // CTW0 address 0x04.Output CH0 setting frequency
}

这段代码是我们直接调用的函数,里面只有一个检查频率范围是否正确,不正确的话调用一个错误函数,然后把频率转化为频率字,再选择通道,写寄存器。选择通道和写寄存器的过程大概率不会有问题,那么关键问题就是这个Freq2Word的过程中发生了什么。按F12跳转到定义发现

1
2
3
4
5
6
7
8
9
10
11
void Freq2Word(double f, uint8_t *fWord)
{
// fWord 4 bytes
uint32_t Temp;
Temp = (uint32_t)f * 8.589934592;
// The input frequency factor is divided into four bytes. 8.589934592=(2^32)/500000000
fWord[3] = (uint8_t)Temp;
fWord[2] = (uint8_t)(Temp >> 8);
fWord[1] = (uint8_t)(Temp >> 16);
fWord[0] = (uint8_t)(Temp >> 24);
}

也就是说他把我们的频率(浮点型)直接乘以一个神秘系数,然后取整赋值到4个byte的数据。这个神秘系数的来源写在了注释里(谢天谢地他写了注释),是,这个500M熟不熟悉?就是刚刚的25MHz晶振20倍频之后得到的500MHz,也就是DDS主频。也就是说DDS会把0到主频这个频率范围映射到频率字的0 − 232上。

那么这时我们的输出频率无法跟输入的25MHz完全同步的原因也就很显然了:25 × 106 × 8.589934592 = 214748364.8,在取整到整型的时候会损失掉0.8。那么是不是我们用整数除法代替这个神秘的浮点乘法就能解决这个问题呢?其实不然。尝试 ,结果和刚才完全一样。也就是说,这个取整损失0.8的问题并不是由浮点乘法带来的,而是232本就无法被20整除,问题的关键在于我们的PLL divider ratio取了20,不是一个232能够整除的数。

那么问题的解决办法也很显然了:就像之前降主频的时候做的一样,这里我们把PLL divider ratio调整到16(为了能被232整除,只能取4,8,16这三种),然后在Freq2Word的时候给神秘系数额外乘上2.5,就可以同步上了。为了将PLL divider ratio 改为16,那么FR1寄存器应该是0b1 10000 11 0000…=0xC30000。

1
2
3
4
5
6
7
8
uint8_t FR1_DATA[3] = {0xC3, 0x00, 0x00}; // 16 frequency doubling
uint8_t CFR_DATA[3] = {0x00, 0x03, 0x00}; // default Value = 0x000302
_Init_AD9959_GPIO();
InitIO_9959();
InitReset();

WriteData_AD9959(FR1_ADD, 3, FR1_DATA);
WriteData_AD9959(CFR_ADD, 3, CFR_DATA);
1
2
3
4
5
6
7
8
9
10
void Freq2Word(double f, uint8_t *fWord)
{
// fWord 4 bytes
uint32_t Temp;
Temp = (uint32_t)f * 8.589934592 * 2.5; // The input frequency factor is divided into four bytes. 8.589934592=(2^32)/500000000
fWord[3] = (uint8_t)Temp;
fWord[2] = (uint8_t)(Temp >> 8);
fWord[1] = (uint8_t)(Temp >> 16);
fWord[0] = (uint8_t)(Temp >> 24);
}

缓慢调整信号源提供的外部信号频率,可以发现DDS输出频率也在跟着变,并且在示波器上两个波形相对静止还挺稳定的,效果还是不错的。

同步上了非常高兴,晚上美美下班。然而第二天过来再测了一下发现天塌了。虽然确实能同步上了,但是输出信号与输入信号的相位差略显随机。每次重启都会显示出不同的相位差,并且固定在-98度,-8度,+82度,+172度这四种取值附近随机刷新一个(可恶的倍频器!)。于是当天的任务就变成了怎么把相位也同步上。

AD9959是可以设置输出信号的相位的,并且是0-359度可调,步进1度。如果能让单片机知道目前的相位差是多少,那么要调整过去就很简单了。那么接下来的事情就很明显了:用一个鉴相器输出REFCLK和OUTPUT之间的相位差(一般是映射到直流电平上),用ADC采集这个直流电平,然后调整回去。

从隔壁组借来一个鉴相器,用信号源和示波器先简单测一下,发现相位差越小输出电平越高,范围大概是40mV-1.9V,可以直接拿ADC采集。czq同学反复告诫我输入的信号强度不能超过0dbm,换算一下大概是300mV的VPeak,而AD9959的Table1给出的范围是200mV-1000mV,也还算是在范围内,能用。

把电路接好:信号源给出参考信号同时接到示波器通道1,DDS的REFCLK,以及鉴相器的一个输入端;DDS的输出端接到示波器的通道2以及鉴相器的另一个输入端;鉴相器的输出端接到单片机的ADC上。再简单编一个程序,demo里就粗暴一点把360个相位全遍历一遍,用ADC检测哪个相位时电平最高就把哪个相位记录下来就行。

尝试跑了一下,发现每次输出都有一个固定的20度相位差,并且重启单片机也不会改变;在单片机上打印了一下检测到的最高电平,倒也确实是1.9V左右。反正是在做工程不是搞学术,也顾不得那么多了,直接手动给他加上个20度。再稍微改进了一下代码,遍历360个相位也太慢了,根据ADC检测到的信号大小可以反馈调节,ADC检测到电平比较低的时候可以步子迈大一点嘛直接跳过去,然后再加一个遍历次数的限制,最终程序就写好了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
   uint16_t max=0, temp=0, i=0, cnt=0;
while (max<2500 && cnt<50) {
Write_Phase(1, i);
AD9959_IO_Update();
delay_ms(10);
temp=Get_Adc_Average(ADC_CHANNEL_0, 20);
if (max<temp) {
max=temp;
phase0=i;
}
i+=60*(2500-temp)/2500;
i%=360;
cnt+=1;
}
//phase0+=26;
phase0+=20;
phase0%=360;

Write_Phase(1, phase0);
AD9959_IO_Update();
LCD_ShowString(0, lcddev.height-40, 320, 32, 32, (u8*)"synchronized!");

最终实现的效果也还是不错的,详见测试视频

测试视频在此……吗?

……本来想上传视频的结果语雀上传视频要会员,那算了。吃大份去吧语雀