加入星计划,您可以享受以下权益:

  • 创作内容快速变现
  • 行业影响力扩散
  • 作品版权保护
  • 300W+ 专业用户
  • 1.5W+ 优质创作者
  • 5000+ 长期合作伙伴
立即加入
  • 正文
  • 相关推荐
  • 电子产业图谱
申请入驻 产业图谱

从电路到verilog | 编程综合运用,不得不从DDS的实例说起

2016/08/30
16
  • 1评论
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

 

实际上说道上一讲,老衲就可以体面撤退了。该说的都说了,细枝末节嘛,也不适合讲座这种短篇幅的东西来表现。可是老僧舍不得大家啊(实际上是舍不得出场费),所以凑个整两个九讲。话说回来,“九”这个数字一向与武林有关:《九阴真经》、《九阳真经》还有降龙 2×9 掌……


这一讲呢,将会综合应用前面的全部知识,来完成一个相对较大的系统。希望施主们打起七十二分的精神,如果哪些知识点忘掉了,也需要往回翻一翻、看一看。


再唠叨一句:讲座篇幅短短几十页,不可能包含 Verilog 语言全部的内容,就是与电路有关的也不全。想想看,IEEE 标准 200 多页,除了 DPI 也有 150 页的样子,这个讲座才几页啊。喜欢老衲啰嗦的老少爷们也不要失望,老和尚“慈悲为怀,善念为本”早就写得了《Verilog 传奇》的手抄本小册子。您老听着高兴,可以到茶房那里购买,“走过,路过,可别错过啊”。


好了,闲言少叙,书归正传。现在老僧开始给大伙儿讲讲 DDS。


1. 概念算法,两不耽误
按照一般论文绪论里面吹牛的定义:直接数字合成是一种数字电子方式,它从一个单一(或混合)的时钟源中产生任意波形和频率。与传统的频率合成器相比,DDS 具有低成本、低功耗、高分辨率和快速转换时间等优点,广泛使用在电信与电子仪器领域,是实现设备全数字化的一个关键技术。


DDS 的一般结构如图 1 所示,其数字逻辑部分包括控制模块、累加器和正弦函数发生器三个部分。控制模块可以串行或并行的方式装载并寄存用户输入的频率控制码;而累加器根据频率控制码在每个时钟周期内进行相位累加,得到一个相位值;正弦函数发生器则对该相位值计算数字化正弦波幅度。DDS 芯片输出的一般是数字化的正弦波,因此还需经过高速 D/A 转换器低通滤波器才能得到一个可用的模拟频率信号。

图 1 DDS 系统整体结构


要使 DDS 系统工作需要两个操作阶段:我们称之为编程和运行。


在编程这一阶段里,电子控制器把数据载入至存储器中。数据的每一个单元是一个用来表示当前时刻信号幅度的二进制数。存储器中这些数据的排列(数组)构成一张振幅表,表示每一时刻当前波形的振幅。举个例子,在一张振幅表中,前一半的数全为 0,后一半全为波形振幅的最大值(100%),这些数据就表示“方波”了。任何波形都可以通过简单地改变这些数据来产生。


在运行这一阶段中,累加器受系统时钟参考源的控制,每一个脉冲自增。相位累加器的输出(相位)通常就是数组中依次输出的各个数据。最后会被 DAC 依次转换成模拟波形。
先看控制模块,这个主要负责数字 DDS 的输入输出接口。例如,信号频率的输入可以通过串口线,由上位机输入;当然,也可以通过按键在 PCB 板上完成。对于的 ADC 接口,则与选择的具体器件有关。可能还需要显示一下频率数值,这个就和 BCD(Binary-Coded Decimal‎,BCD)译码器及其接口,乃至液晶显示( Liquid Crystal Display, LCD)接口有关联了。这部分很重要,但是不是本讲的重点,且容贫道一嘴带过。


累加器部分,更加简单的,就是一个允许设置累加步长的累加器了。更加复杂一点的结构是还允许设置初始值,也就是所谓初始相位。实际工程中,初始相位的设置意义不大,就不写入代码了。

 


现在讲到关键点正弦函数发生器了。一般而言,九成九的正弦函数发生器会采用 ROM 的形式。输入的地址对应规格化的角度值,输出的数据则对应该角度的正弦值。问题的核心是:到底存储多少数据。0 度到最接近 360 度的一个分度,自然是一个选择,但是这样实际上浪费了大量的存储空间。


颠来倒去一句话:有 0 度到 90 度的正弦值,就可以得到全部是个象限的正弦值。如图 2,其中给出了不同象限的正弦值的计算公式以及其他设计中需要的内容。图中的公式就是所谓周期性的公式了,用于非第一象限的正弦值的计算。另外为了方便,给出了一些特殊角度的地址,以及各个象限的地址开头两比特值和正弦值的符号信息。


图 2 四个象限的正弦函数


为了实现用 0 到 90 度的正弦函数值,计算 0 到 360 的正弦值的目的,需要将图 9.1 里面的正弦函数发生器劈为四份,如图 9.3 所示。第一部分是 0 到 90 度的正弦函数发生器,这个无需多说。第二个是地址转换模块,它去掉原来 0 到 360 度对应的“长地址”的最高两个比特(90 度例外,只去掉最高一个比特),这样得到 0 到 90 度对应的“短地址”。第三部分叫符号产生模块,负责根据当前角度对应的象限计算正弦值的符号。注意图 9.2 的地址与符号部分,正弦值的符号实际上就是对应地址的最高位。由于 0 到 90 度的正弦函数发生器可能计算时候有时延,所以符号产生模块还负责这个时延的矫正。最后一个模块是正弦值处理模块,就是正数值透传、负数值变为补码的功能。


图 3 正弦函数发生器(0 到 360 度)结构


地址转换与符号产生模块和正弦值处理模块的代码也无需详述。
把系统结构中除了 0 到 90 度的正弦函数发生器的各个部分打包,叫做 DDS 框架,其架构与代码如图 4 所示。


图 4 DDS 框架与其他部分的关系

 


2. 非线数值,折线逼近
有道是“一将名成万古枯”,在数字逻辑设计领域虽然没有那么惨烈,却也是“领导动动嘴。地下跑断腿”。就拿前面提到的“用直线 / 折线拟合正弦函数的”这个思想为例,提出想法的人可以简单地就是如图 5 的勾上几笔,看似就圆满了。


但是接下来的工作还有很多:几段折线合适?这些折线的位置如何确定?系数如何计算?接着还有定点化如何完成?最后有数字逻辑系统的结构与实现细节。这些老大可以不考虑,工程师却不能不研究。


图 5 折线法计算正余弦值的示意图


按照横轴 / 角度均匀选择线段的起始点显然是一个最简单的方法,但是这绝对不是一个好的方法。理由很简单,正弦曲线的曲率 -- 或者叫导数 -- 变化时不均匀的。均匀选择线段会造成曲率大的地方,计算误差大;而曲率小的地方,计算误差也小。这显然是不合理的。


具体算法呢,超出了这套书的范围(欲知详情,请看小册子),按下不表。反正施主们得到了下面的数值:


以横轴采样个数 64 个以及最大允许误差 0.02 作为输入,可以得到一个正弦函数的 14 段折线拟合,有关信息见表 1。


表 1 折线拟合正弦函数中,线段信息

折线标号

起点(度)

系数

折线标号

起点(度)

系数

a

b

a

b

0

0

0.9996

0

7

21.4286

0.9260

0.0188

1

4.2857

0.9971

0.0001

8

25.7143

0.8953

0.0318

2

7.1429

0.9921

0.0006

9

30.0000

0.8529

0.0529

3

10.0000

0.9847

0.0017

10

35.7143

0.7478

0.1158

4

12.8571

0.9748

0.0037

11

50.0000

0.5405

0.2916

5

15.7143

0.9625

0.0068

12

67.1429

0.2577

0.6160

6

18.5714

0.9478

0.0112

13

85.7143

0.0498

0.9217


对于每段折线都有:
 
其中,x 为弧度值。例如,对于 5 度(弧度 0.0873)的角度值,其正弦函数值为 sin(0.0873) ≈ 0.0887;此时 x 位于第二段折线的范围里面,按照折线计算 y = 0.9921 × 0.0873 + 0.0006 ≈ 0.0872;误差大约为 0.001,这已经算是很精确了。通过程序扫描,这个算法最大的误差为 0.019。


然后是定点化,忘记如何做的请回去复习则个。


考察折线法有关的数值的取值范围,定义其定点化格式如表 2,其中“value_width”是待输出的结果的位宽。需要注意的是 a_f 由于其取值范围的特殊,设计中比其他有关数值多了一个比特。


表 2 折线法有关的数值的取值范围与定点化格式

 

折线起始点 / 地址

a_f

b

结果

取值范围

[0, 2π]

[0, 2π]

[0, 1]

[0, 1]

定点化格式

Q(address_width+1).0

Q(value_width+1).(value_address)

Q(value_width).(value_address)

Q(value_width).(value_address)


对应定点化计算的公式为:


其中,A_f 和 B_f 是定点化后的折线的系数。


此时,y 有(value_width + address_width +1 )个比特的位宽,而要求结果只有 value_width 个比特的位宽,需要进行截位处理。根据已知条件,这个截位也有些特殊。计算结果 y 的最高比特一定为零,这个是公式的含义告诉我们的(提示一下:正弦值)。因此上系统输出的结果应该是 y 从第二个最高比特开始的 value_width 个比特。
以 address_width = 6 和 value_width = 7 为例,可以获得折线的信息如表 3 所示。


表 3 折线拟合正弦函数中,线段定点化信息(十六进制表示)

折线标号

起点的短地址

系数

折线标号

起点的短地址

系数

A_f

B_f

A_f

B_f

0

00

c8   

00         

7

0f

b9

03

1

03

c7

01

8

12

b3

05

2

05

c6

01

9

15

ab

07

3

07

c5

01

10

19

96

0f

4

09

c3

01

11

23

6c

26

5

0b

c1

01

12

2f

34

4f

6

0d

be

02

13

3c

0a

76


另外可以得到:相对浮点运算,定点运算又带来了 0.011 的计算误差。


至此,定点化结束,可以交给数字逻辑设计的有关人员,开始具体实现方面的工作了。
具体的设计思路倒也不难:是“先选系数后计算”,也就是先判断所在折线序号,得到对应系数再计算,其基本结构见图 6 所示;值得一提的是,只需要这些折线的终点或者起点里面的一个信息就足够了,因为折线是首尾相连的。这里老衲采用了终点。还有一个小技巧是,最后一个折线的终点也是多余的,因为它必然对应最高的地址。


图 6 “先选系数后计算”结构


多路选择模块稍稍复杂一点点,这里捎带脚介绍一下。如图 7 所示,多路选择模块由若干比较器和一个用“case”书写的选择器组成。比较器中 points 为各段折线对应的重点。选择器这有一种特殊的编码决定选择哪一个信号,具体编码大伙儿自己分析。


图 7 多路选择模块的结构


在实现细节上还有一个很多人会跌掉的坑,那就是关于 90 度的处理。搞不好,就会在 90 度的时候,计算出 0 度的正弦值。这样输出的曲线就会出现在应该最大值 1 的时候,输出一个深坑(0 值)的情况。对于这种情况,可以特殊问题特殊处理,直接赋值 90 度的正弦值。在本回书的例子里面,采用了一个迂回的方式,设 90 度为最接近它的最大角度(例如,步长为 1 度的时候,输入的“短地址”为 90 度,计算时候的地址则取为 89 度),在假设系统 8 比特输出位宽的前提下,不会产生输出误差的。

 


在例 1 中给出了折线法计算 0 度到 90 度正弦值的代码,结合前面介绍过的 DDS 的框架,就可以得到完整的 DDS 系统了。


【例 1】折线法计算 0 度到 90 度正弦值的代码
module line_sin_0_90
#(parameter ADDRESS_WIDTH = 8,
//Bit width for phase counter and step
parameter VALUE_WIDTH = 8
//Output value's bit width
)
  (
    input CLK,
    input RESET,
    input[ADDRESS_WIDTH - 1 - 1: 0] address,
    output reg[VALUE_WIDTH - 1 - 1 : 0] value
  );

//Definition for Variables in the module
integer loop;
//Loop variable

reg[ADDRESS_WIDTH - 1 - 2: 0] intra_address;
//Intra address for calculation
//Change 90 degree to 111....111
//Cut the MSB
reg[ADDRESS_WIDTH - 1 - 2: 0] intra_address_delay;
//Delay for coefficients selection

reg[VALUE_WIDTH - 1 : 0] ceo_a;
//Fixed point 1.VALUE_WIDTH
reg[VALUE_WIDTH - 2 : 0] ceo_b;
//Fixed point 0.VALUE_WIDTH
//coefficients for the lines

wire[VALUE_WIDTH - 1 + 1 + ADDRESS_WIDTH - 2 - 1: 0] long_value;
//Long function value without truncation
//Length: a * address
wire[VALUE_WIDTH:0] short_value;
//Truncation value

//definition JUDGEMENTS and Coefficients Constant
//Application part: Insert DEFINITION OF JUDGEMENTS and Coefficients Constant(DO NOT REMOVE!)
//Below code/s was/were generated by the application
reg [12 : 0] judgments;
wire [7 : 0] a[13 : 0];
wire [6 : 0] b[13 : 0];
wire [5 : 0] points[12 : 0];
//Above code/s was/were generated by the application

//Logical
//Constant Tables
//Application part: Insert Constant Tables (DO NOT REMOVE!)
//Below code/s was/were generated by the application
//coefficients a and b
assign a[0] = 8'b11001000;
assign b[0] = 7'b0000000;
assign a[1] = 8'b11000111;
……
assign a[13] = 8'b00001010;
assign b[13] = 7'b1110110;

//Connect points between lines
assign points[0] = 6'b000011;
……
assign points[12] = 6'b111100;
//Above code/s was/were generated by the application

//Value calculation on the line
assign long_value = ceo_a * intra_address_delay;
assign short_value =
        long_value[VALUE_WIDTH - 1 + 1 + ADDRESS_WIDTH - 2 - 1 : ADDRESS_WIDTH - 2]
           + {1'b0, ceo_b};

always @ (posedge CLK or negedge RESET)
//Address adjust: 90 degree operation
begin
    if (!RESET)
    //Reset enable
    begin
        intra_address <= {(ADDRESS_WIDTH - 1){1'b0}};
    end
    else
    begin
        if (address[ADDRESS_WIDTH - 1 -1])
        //90 degree
        begin
            intra_address <= {(ADDRESS_WIDTH - 2){1'b1}};
            //To 111...111
        end
        else
        begin
            intra_address <= address[ADDRESS_WIDTH - 1 - 2: 0];
            //Keep original value       
        end
    end
end

always @ (posedge CLK or negedge RESET)
//Address delay: waiting for coefficients selection
begin
    if (!RESET)
    //Reset enable
    begin
        intra_address_delay <= {(ADDRESS_WIDTH - 1){1'b0}};
    end
    else
    begin
        intra_address_delay <= intra_address;
    end
end

always @ (posedge CLK or negedge RESET)
//coefficients selection
begin
    if (!RESET)
    //Reset enable
    begin
        ceo_a <= {(VALUE_WIDTH + 1){1'b0}};
        ceo_b <= {(VALUE_WIDTH ){1'b0}};
    end
    else
    begin
        case(judgments)
//Application part: Insert Coefficient Selection codes (DO NOT REMOVE!)        
//Below code/s was/were generated by the application
         13'b1111111111111:
         begin
             ceo_a <= a[0];
             ceo_b <= b[0];
         end

         13'b1111111111110:
         begin
             ceo_a <= a[1];
             ceo_b <= b[1];
         end

         ……

         13'b0000000000000:
         begin
             ceo_a <= a[13];
             ceo_b <= b[13];
         end

//Above code/s was/were generated by the application
        
         default:
         begin
            ceo_a <= {(VALUE_WIDTH + 1){1'b0}};
               ceo_b <= {(VALUE_WIDTH ){1'b0}};
         end
        endcase
    end
end

always @ (posedge CLK or negedge RESET)
//Function value output
begin
    if (!RESET)
    //Reset enable
    begin
        value <= {(VALUE_WIDTH - 1){1'b0}};
    end
    else
    begin
        value <= short_value[VALUE_WIDTH - 1 : 0];
        //Truncation: MSB and tails
    end
end

//Judgment for which line here is
always @(*)
begin
//Application part: Insert Comparisons (DO NOT REMOVE!)
//Below code/s was/were generated by the application
    for (loop = 0; loop < 13; loop = loop + 1)
    begin
        judgments[loop] <= (intra_address < points[loop]);
    end
//Above code/s was/were generated by the application
end

endmodule

 

3. 系统验证,中规中矩
这个系统包含很多模块,其他模块好说,进行仿真验证的时候,肉眼都可以看出它到底合适不。就是 0 到 90 度的正弦函数发生器相对输出复杂,例如对于 7 比特输入就有 65 个输出信号需要对比。对于这样的模块,最好的验证方法是测试向量方法。这个方法以前提到过,直接上代码如例 2。


【例 2】0 到 90 度的正弦函数发生器验证代码
`timescale 10 ns/100 ps
//Simulation time assignment

//Insert the modules

module sin_0_90_test;
//definition for Variables
reg clk;
reg reset;

reg[7:0] cntr;

wire[6:0] result;

reg[6:0] test_vector_monory[64:0];
wire[6:0] test_vector;
//Test Vector Value

wire[7:0] addr;
//Address of test vectors

wire right;
//Results right?

//Connection to the modules
ROM_sin_0_90 LS (.CLK(clk), .RESET(reset), .address(cntr[6:0]),
                  .value(result));

assign test_vector = test_vector_monory[addr];
assign right = (test_vector == result);
assign addr = (cntr >= 3) ? ( cntr - 3): ( 64 - 3 +  cntr);
//Delay for operation

//Clock generation
……

//Reset operation
……

//Load the test vectors
    initial 
    begin
      $readmemh("sin2line_test_vector_2016_3_13_17_3_10.txt",
                  test_vector_monory);
    end
       
//Couner as input
    ……

endmodule


代码中,“test_vector_monory”里面存储了其他程序产生的对应正弦值,“addr”是对于实际处理模块带来的时延的修正。如果“right”信号一直是高电平,则说明 0 到 90 度的正弦函数发生器功能正确。


到了最后的系统验证的阶段了,当然也可以采用上面的测试向量的方法完成。书不重叙,这里再教施主们一个输出信号进行性能验证的手段。这个手法也在前面一章里面介绍过,莫过于写文件罢了。但是具体到正弦函数的性能验证,却还是有些技巧的。
对于正弦函数性能的测试,大多数人一定会想到快速傅里叶变换(Fast Fourier Transform,FFT)。信号频谱的最高峰就是正弦信号的线谱,其平方为正弦信号的能量;其他部分就是系统产生的噪声,噪声能量为其能量值和。这样就可以得到系统最重要的性能:信噪比 (信号噪声比,Signal-to-noise ratio ,SNR)。但是实际中,FFT 也是优缺点的。这就是对于非整周期采样的数据,会有频谱散射效应。有兴趣的人士可以用任何具备 FFT 功能的工具做一个有趣的实验。分别产生两个序列:第一个序列为 0 度到 359 度的正弦值,第二个序列为 0 度到 358 度的正弦值。分别对于这两个序列做 FFT 并且画出幅度值,两者的区别立现。


针对这个情况,在采集 DDS 系统的输出的时候,不能任意采集。采集的样本必须满足“满周期”的性质。细心的观众一定主要到了,在 DDS 的框架里面有一个叫“zero_address”的信号。这个信号当长地址为全零的时候为高电平。换句话说,该信号两次为高之间的信号为满周期的信号。


现在没什么能够拦阻人类进入 DDS 时代的脚步了,前进!图 8 是本讲介绍的 DDS 的输出,其中还表现了一次频率变化的过程。


图 8 DDS 的输出信号


至于输出三角波锯齿波或者方波的 DDS,相对于上面的系统,基本不值一提,就不多说了。


最后再说一下,限于篇幅不好详细展开,有求知欲听众千万要买小册子啊!


这正是:

语言本来无情物,上天入地多用途。一般时钟做输入,产生正弦片外吐。
外围框架无特殊,函数产生玄妙出。验证方法有用处,整周采样成数组。

与非网原创内容,谢绝转载!

系列汇总:

之一:温故而知新:从电路里来,到 Verilog 里去!

之二:Verilog 编程无法一蹴而就,语言层次讲究“名正则言顺”

之三:数字逻辑不容小窥,电路门一统江湖

之四:Verilog 语言:还真的是人格分裂的语言

之五:Verilog 不难学,聊聊时序逻辑那些事儿

之六:数字电路设计:有理论、有电路、有代码“三位一体”

之七:熟读语言要素,不会编程也懂 verilog

之八:IP 设计可企及,宏和参数只是为了合并同类模块

之九:欲要系统能跑起,仿真验证是真谛

相关推荐

电子产业图谱

本名:吴涛,通信专业博士,毕业后十多年从事无线通讯产品的研发工作。了解W-CDMA、TDS-CDMA和LTE的标准协议、接收机算法以及系统架构和开发。从事过关于W-CDMA的FPGA IP core设计工作,也完成过W-CDMA和TDS-CDMA的接收机理论研究和链路仿真工作。综合上面的工作,最终选择了无线通讯的系统设计和标准设计工作。目前拥有100多个已授权的发明专利,是某通讯行业标准文件的第一作者,亦有专利思想被写入3GPP协议。已出版FPGA设计专业著作《IP核芯志-数字逻辑设计思想》和《Verilog传奇-从电路出发的HDL代码设计》。