FPGA设计实战演练.高级技巧篇-----读书笔记

   第一章 从PCB开始研究FPGA设计问题

一、PCB布线

1、要求

·对所有器件进行电源滤波,均匀分配电源,降低系统噪声。

·匹配信号线,减小信号反射。

·降低并行走线之间的串扰。

·减小地反弹效应。

·进行阻抗匹配。

2、微带传输布局,走线在PCB的顶层或底层,只有一个参考平面

3、带状传输线布局,走线在PCB内层,有两个电压参考平面

 4、阻抗控制原则:单端走线控制阻抗为50Ω;差分走线控制阻抗为100Ω。生产厂家阻抗控制的偏差范围一般为正负5%左右。

5、减小微带线或带状线布局串扰的方法是:

·在布线要求允许的范围内,尽可能地加宽信号线之间的距离。走线之间尽量不要靠

近,距离保持在介质高度的3倍以上。

·传输线设计要使导体尽可能靠近地平面。这一方法使得传输线能够与地平面紧耦

合,有助于和邻近信号去耦合。

·尽可能使用差分布线方法,特别是关键网络(例如匹配长度以及每条走线串通回

转等)。

·如果存在明显的耦合,应在不同层之间布设互相垂直的单端信号。

·减小单端信号之间并行走线的长度,以较短的并行走线布线,以减小网络之间的长

耦合走线。

6、差分走线

 二、FPGA供电

1、单调性(或线性)要求,是指在上电过程中,电源轨是单调爬升而不会掉头往下掉(也称

单调非负特性)。也就是说,电源必须始终具有正斜率(或零斜率)。

2、给电源供电设计加入电压上电顺序和电源跟踪。这来源于FPGA的I/O电压必需要在核心电压上电以后才能上电的基本规律。

3、电源和地叠层分布

 

 三、退耦电容

1、退耦电容可以在电源轨波动时为其提供少量的瞬态能量。

2、为何需要退耦电容?

可以将一个退耦电容看做是一组电阻(R)、电感(L)和电容(C)的串联组合,根据真实的RLC模型可知,电容也将在某个频率点上容抗和感抗分量彼此有效抵消。所以说,一个真实的电容将会在某个频点时阻抗最小,这个阻抗最小的点被称为电容的谐振频率,在这个点上退耦电容可以提供最佳的滤除供电干扰的能力。

第二章  如何处理逻辑设计中的时钟域

1、单比特信号跨时钟域的同步处理

1、跨时钟域(Clock Domain Crossing,CDC)是指设计中存在着两个或两个以上异步时钟

域,跨时钟域设计问题目前是逻辑设计者经常面临的问题,解决这类问题的方法被称为

CDC技术,即跨时钟域技术。

2、产生压稳态的原因:

触发器的建立时间和保持时间在时钟上升沿左右定义了一个时间窗口,如果触发器

数据输人端口上的数据在这个时间窗口内发生变化(或者数据更新),那么就会产生时序违

规。存在这个时序违规是因为建立时间要求和保持时间要求被违反了,此时触发器内部的

某个节点可能会在一个电压范围内浮动,无法稳定在逻辑0或者逻辑1状态。换句话说,如

果数据在上述窗口中被采集,触发器中的晶体管不能可靠地设置为逻辑0或者逻辑1对应

的电平上。

 3、处理亚稳态的方法

1、双触发

a、 最常见的同步器就是使用两级寄存器,即使用寄存器打两拍的方式进行同步。

 应遵循原则:

级联的寄存器必须使用同一个采样时钟。

发送端时钟域寄存器输出和接收端异步时钟域级联寄存器输入之间不能有任何其他组合逻辑。

同步器中级联的寄存器中除了最后一个寄存器外所有的寄存器只能有一个扇出,即其只能驱动下一级寄存器的输入。

b、使用三级触发器对亚稳态进行同步处理

平均故障间隔时间(MTBF),对于前面讨论到的亚稳态同步器来说,这里的故障是指一个

被传输到同步触发器的信号,当它经过第一级同步触发器时处于亚稳态,在随后的一个时钟

周期内持续保持亚稳态并被采样到第二级同步器触发器, MTBF大好,所以一般在在高速设计中使用三级触发器同步。

4、同步快速信号到慢速时钟域

1、信号可能会丢失

如果丢失信号采样值对于设计来说是不允许的,那么有两种通用的应对方法可以解决这个问题:一个是开环解决方案,确保信号在无须确认的情况下可以被采集;另一个是闭环解决方案,即在跨时钟域边界时,信号需要接收端的反馈确认。

1、开环解决方案:

图2-11所示,需要将目标信号展宽到至少超过接收时钟域的时钟周期。前文提到,最佳的脉宽是至少为采样时钟周期的1.5倍,这样跨时钟域信号将会被接收时钟域的时钟至少稳定地采样一次。

 2、闭环解决方案:

在发送时钟域将控制信号当使能信号传递,并将其同步到接收时钟域,然后接收时钟域收到使能控制信号之后,将同步的控制信号反馈到发送时钟域,发生时钟域通过另外一个同步器接收此反馈回来的控制信号,并以它作为信号正确接收的握手信号。

缺点:延迟大

5、多比特信号跨时钟域同步处理

1、在接收时钟域有一个寄存器,它需要一个加载(Load)信号和一个使能(Enable)信号来加载一个数值到寄存器。如果加载和使能信号在发送时钟域的同一个时钟沿被驱动有效(即两个控制信号需要同时有效),那么这两个控制信号之间就有可能存在产生一个小歪斜的机会,这就可能导致在接收时钟域中这两个信号被同步到不同的时钟周期。在这种情况下,数据是不能被加载到寄存器的。

解决:将加载和使能两个控制信号融合成一个单比特控制信号

2、用多周期路径规划来处理

多周期路径规划是一种通用的安全传递多比特跨时钟域信号技术。多周期路径规划是指在传输非同步数据到接收时钟域时配上一个同步的控制信号,数据和控制信号被同时发送到接收时钟域,同时控制信号在接收时钟域使用两级寄存器同步到接收时钟域,使用此同步后的控制信号来加载数据,这样数据就可以在目的寄存器被安全加载。

 3、使用FIFO结构来处理多比特时钟域信号

1、FIFO在FPGA内一般是通过封装一个双口RAM来实现。

2、用格雷码做状态机

4、多时钟域设计分区划分

1、在时钟边界划分分区

2、多时钟域分区划分后的静态时序分析

3、设计中的门控时钟行波时钟的处理

行波时钟(Ripple Clock):行波时钟是指任何由寄存器驱动的时钟,常见的行波时钟为

利用逻辑(如计数器)进行时钟分频产生的时钟。

门控时钟:Altera给出的定义是,任何由非寄存器逻辑功能驱动的时钟都是门控时钟,

这些非寄存器资源大部分是FPGA内LAB里的LUT或ALUT。门控时钟可以提供一个

时钟开关功能,当然有时候门控时钟也可以是由其他组合逻辑驱动的时钟,比如时钟多路选

择器等。

5、衍生时钟处理指导原则 

1、第一个指导原则是尽量不要使用行波或门控时钟,取而代之的是使用时钟使能或

PLL。时钟分频以及逻辑门开关时钟这类技术通常都是可以使用时钟使能和PLL来替

换的。

2、如果确实需要使用派生时钟,那么建议设计中不要有跨派生时钟域的同步数据路径。

3、如果使用Altera器件,那么在跨时钟域路径上遇到保持时间时序问题时,可以在

QuartusⅡ软件中将Optimize Hold Timing选项设置到All Paths.。具体设置方法是选择

Assignments→Settings--Fitter Settings。

所以,在使用时钟多路选择器或其他由逻辑资源实现的时钟逻辑功能而不是使用时钟

控制模块时,一定要确保在一个给定时刻每个LUT或ALUT最多只能有一个输入在翻

转。

第三章 正确分析衍生时钟

1、门控时钟包括:

·时钟经过反相生成的时钟。

·时钟经过缓冲后生成的时钟。

·时钟经过使能信号控制生成的时钟。

·通过时钟多路选择器输出的时钟。

·时钟输出到FPGA外部后反馈回来的时钟。

2、衍生时钟包括:

·触发器翻转生成的时钟。

·行波计数器生成的时钟。

·同步计数器生成的时钟。

·PLL输出的时钟。

3、门控时钟分析处理

1、时钟反相生成的时钟,这会给目的寄存器产生时钟歪斜。

2、时钟经过缓冲后生成的时钟,对于时钟输入来说,最好的做法是除了将其连接到时钟专用输入脚之外,最好将它们连接到PLL。

3、经过使能处理后的时钟,有两种使能时钟。使用门控来关闭时钟的更佳方式是使用一个同步时钟使能,即动态使能时钟。

静态使能时钟:“静态”是指使能信号在器件上电和跳出复位后,就是电路正常工作时保持不变的情况。如果一个静态使能在电路正常工作的时候发生变化,那么在此变化状态后必须给电路执行复位操作。

动态使能时钟:动态使能是指使能信号可以在电路正常工作时发生变化。这类动态使能相对更加复杂一些,因为电路不但要容忍时钟在使能时潜在产生毛刺的风险,更为常见的是,使能信号必需要被同步到时钟且不能向下传递等时序问题。

实现同步门控时钟

always@ (posedge clk or negedge rst)
    if(!rst)
        q <= 1'b0
    else if(en)
        q <= d;
    else 
        q <= q;

建议使用ALTCLKCTRL函数来创建使能时钟

一个器件中时钟控制模块使用的数量受限于器件内的全局缓冲的数量;使用时钟控制模块时,如果使用了时钟使能信号且使能信号是静态信号的时候,则这些使能信号不需要进行I/0时序约束。

4、·通过时钟多路选择器输出的时钟

外部时钟:

create_clock [get_ports {clk)] -name clk_a -period 5.0-waveform {0.0 2.5}
create_clock [get_ports {clk)] -name clk_b-add -period 10.0-waveform {0.0 5.0}
set clock groups -exclusive -group {clk a}-group {clk_b}

1、每个频率都需要一个create clock语句来创建一个时钟,由于创建的这两个时钟(clka和ck_b)共享同一个时钟路径,所以必须割断这两个时钟之间的时序关系,用set_clock_groups命令。如果时钟输入的频率多于两种可能时,必须为每个可能的频率创建一个时钟分组。

内部时钟:

用于内部多路选择器的SDC命令和外部多路选择器基本类似,只是两个create_clock语句的目标是针对两个不同的端口:

create_clock [get_ports {clk_a)] -name clk_a -period 5.0-waveform {0.0 2.5}
create_clock [get_ports {clk_b)] -name clk_b-add -period 10.0-waveform {0.0 5.0}
set clock groups -exclusive -group {clk a}-group {clk_b}

为了动态地切换多路选择器的选择控制信号,同时为了确保多路选择器的输出上不出现毛刺,一个特殊的电路必需要加入到切换控制过程中。这种特殊电路通常都由状态机结构来实现,所以多路选择器的选择控制由状态机控制。

5、经外部反馈回来的时钟

反馈时钟是指那些在FPGA内部创建的时钟,输出到FPGA的引脚,然后再通过PCB走线连接到FPGA的其他引脚输入反馈回FPGA内部。

4、衍生时钟的分析与处理

1、触发器切换生成的时钟

时钟2分频约束

create_clock [get_ports (clk)]-name clk -period 5.0-waveform (0.0 2.5)
create_generated_clock [get_pins {inst24|regout)]-name clk_div2\
- divide_by 2\
- source [get_portd {clk}] \
- master_clock  [get_clocks {clk} ]

2、由行波计数器生成的时钟

使用这种结构后,最后一级的输出时钟和输入时钟之间存在很大的延时,因为中间经历了很多级寄存器,每一级都存在传播延时。 

为这样一个电路进行约束时,必须为每一级计数器创建一个时钟

3、由同步计数器生成的时钟

4、PLL生成的时钟

一个PLL最多可以支持两个输入时钟

1、优点

在不同的PVT情况保持稳定。相位补偿器通过反馈来监控输入和输出时钟之间的相位变化,而且通过调整Vco的输出来补偿温度和电压的影响。

去除抖动。带宽滤波器可以减少输出上的高频输入抖动。

移相。输出相位与输入相位相关,而且可以手动调节输出相位,同时由同一输入产生不同输出可以有不同的相位关系。

动态重配置。使用重配置机制,可以对输出之中的某些方面进行动态改变,比如相位、频率等。

全局缓冲。PLL的输出使用全局缓冲减少了寄存器之间的歪斜。此外,PLL的负延时补偿可以抵消由全局缓冲加入的延时(包括输入缓冲以及布线延时),这在试图满足I/O时序要求时非常有用。

第四章  复位电路的实现及其时序分析处理

一、同步复位设计处理

always @ (posedge clk)
    if(rst_n == 1'b0) //同步复位
        

1、同步复位是基于复位这样的一个前提,即复位信号只是在寄存器时钟的有效沿时影响该寄存器的状态。

2、在Altera FPGA中,有两种方法可以将复位信号送达寄存器。一种是随数据门控输入;另一种是使用LAB块控制信号,如synclr。

3、而大部分时候,输人到FPGA的复位信号是异步信号,在这种情况下,复位信号必须在内部先同步后再送达寄存器。 

 module test(
    input  reset_n,
    input  clk,
    

);


wire rst_n;
reg reg3, reg4;

assign rst_n = reg4;

always@ (posedge clk)
    begin
        reg3 <= reset_n;
        reg4 <= reg3;
    
    end

always@ (posedge clk)
    if(rst_n == 1'b0)
        


endmodule

二、异步复位设计处理

1、异步复位的最大优点是,它们没有像同步复位那样插入到数据路径中异步复位不像同步复位那样依赖于时钟。

2、电路使用异步复位并没有任何问题,只是在复位被释放时可能会出现问题。当复位信号被撤除,并且不能通过时序分析中的Recovery和Removal检查时,那么复位信号边沿就很有可能会落入一个亚稳态区域, 

亚稳态带来的后果是寄存器的输出(基于输入到寄存器的输入数据)需要花费更多的时间来回复到其正确的状态。这个额外增加的时间可能会导致寄存器下一级的建立时间失败,从而导致系统错误。显然,需要不惜一切代价来避免这种情况。避免亚稳态的方法之一是在寄存该异步复位的寄存器后增加一系列寄存器,然后使用这些寄存器的输出作为设计的复位。

module async_reset(
    input clock,
    input reset_n,
    input data_a,
    output out a
);
reg regl,reg2,reg3;
assign out_a = reg3;

always @(posedge clock,negedge reset n)
    begin
        if(!reset n)
            reg1<=1‘b0;
        else
            regl <data_a;
    end
always @(posedge clock)
    begin
        reg2<=regl;
        reg3<=reg2;
    end

endmodule

三 异步复位同步化(异步复位同步释放设计处理)

1、异步复位的优势是不参与数据路径,所以不影响数据路径速度,而复位几乎是瞬间起作用;而同步复位的优势是百分之百地同步时序分析,且具有抗噪声性能。这种复位其实就是通常所说的异步复位同步释放。

module sync_async_reset(
    input clock,
    input reset_n,
    input data_a,
    input data_b,
    output out_a,
    output out_b
);

reg reg1, reg2;
reg reg3, reg4;
reg rst_n;

assign rst_n = reg4;
assign out_a = reg1;
assign out_b = reg2;


always@ (posedge clock or negedge reset_n)
    if(reset_n == 1'b0) begin
        reg3 <= 1'b0;
        reg4 <= 1'b0;
    end
    else begin
        reg3 <= 1'b1;
        reg4 <= reg3;
    end

always@ (posedge clock or negedge rst_n)
    if(rst_n == 1'b0) begin
        reg1 <= 1'b0;
        reg2 <= 1'b0;
    end
    else begin
        reg1 <= data_a;
        reg2 <= data_b;
    end


endmodule

2、使用上述同步化异步复位的一个代价是它们很容易受到噪声和窄脉冲的干扰。同样地,如果可能,最好对输入到FPGA的异步复位先进行滤波和去抖动。

四、Recovery 和Removal 分析

从宏观上来说,Recovery和Removal时序满足要求可以确保逻辑同时从复位状态跳出,也即每个寄存器在前一个时钟沿处都处于复位状态,下一个时钟沿所有寄存器都从复位状态释放。

1、除了数据路径驱动目的寄存器的异步端口外,Recovery分析非常类似于建立时间分析。

2、除了数据路径驱动目的寄存器的异步端口外,Removal分析非常类似于保持时间分析。

当异步信号被撤除时,Removal分析是保证信号在时钟沿到来后还能保持足够长的时间有效,确保触发器都还保持处于非正常工作状态(如果是复位信号,即复位状态)。

Recovery分析是保证异步信号在要求的时钟沿到来足够长时间之前撤除,这样允许所有寄存器能够同时恢复到正常工作状态。

五、为何总是建议使用异步复位

1、为了更有效的利用器件资源。如果设计者将其复位设成同步模式,那么他们就不得不使用FPGA内部寄存器的数据输入端口,这样就不可避免地要伤害设计时序并增加设计面积。

2、复位被异步地设计,可以使其更加健壮。当被异步设计时,复位信号在复位设计时无须依靠时钟信号。虽然有些设计并无这种要求,但是大部分设计都有这样的要求,而且在时钟被关闭时复位设计可能是一个非常重要的考虑因素。

六、分析并解决Recovery故障

1、Recovery故障原因

(1)源寄存器并未布置在靠近全局驱动器附近

(2) 该网络本身并未使用全局布线资源,而且经过很长布线走遍整个芯片内部。

(3) 该网络使用了全局布线资源,但是全局延时实际上相比时钟来说还是太慢。

(4) 这个寄存器跨越时钟域,因此 Recovery 时序要求就无法实现。

2、解决

(1) 如果寄存器驱动全局布线资源,而且并未放置在全局驱动器附近,那么此时用户可以对这个寄存器添加一个位置约束,强制将其布局到全局资源附近。
(2) 如果异步复位违反了时序要求,并且并未使用全局布线资源,那么请尝试使用全局布线资源。

(3) 如果异步复位信号使用全局布线资源还是无法满足时序要求,可以尝试使用本地布线资源。

(4) 如果跨越了时钟域,那么在进入新的时钟域之后对其进行重新同步

五、如何写好状态机

摩尔状态机:输出只与当前状态值有关,且只在时钟边沿到来时才会有状态变化。

米勒状态机:输出不仅与当前状态值有关,而且与当前输入值有关。

 一、选择状态机的编码方式

顺序码:状态编码遵循传统的状态二进制序列。

如果使用顺序编码,从状态“01”到“10”的转换过程中很可能会出现过渡状态“11”或“00”。这是因为中间信号crrent_state在状态转换过程中,状态寄存器的高位翻转和低位翻转时间有可能不一致,当高位翻转速度快时,会产生过渡状态“11”,当低位翻转速度快时会产生过渡状态“00”。所以,可想而知,如果状态机的状态值更多的话,则产生过渡状态的概率就更大。假如,状态机并未使用全部编码(比如只有5个状态,编码中有3个未用),由于这种过渡状态的反馈作用,将直接导致电路进入非法状态,若此时电路不具备自启动功能,那么电路就无法返回正常工作状态(彻底跑飞了)

格雷码:除了相邻状态编码之间只有一个位变化外,其他和顺序码类似。(00 01 11 10)

假如使用的是格雷编码,由于相邻两个数据之间只有一位不同,所以可在很大程度上消除由延时引起的过渡状态。使用格雷码虽然可以大大降低产生过渡状态的概率,但是如果当一个状态到下一个状态有多种转换路径时,就不能保证状态跳转时只有一个位变化,这样将无法发挥格雷码的特点。所以,需要仔细分析状态机的结构,如果状态机中某个状态跳转方向多于一个,此时慎用格雷编码,可以采用独热编码

独热码:这种方法是在状态机中为每一种状态分配一个触发器。只有一个触发器当前设置为有效,其余均设置为无效,故称为“独热”。

二、合理选择及使用单进程或多进程来设计状态机

1、多进程状态机

 输出进程推荐使用时序逻辑对输出进行描述,如果使用组合逻辑容易产生毛刺。

对输出进行寄存。

2、单进程状态机

1、为了使系统更加稳定,可以在输出组合逻辑后面加一级寄存器,采用时钟边沿触发,这

样可以在很大程度上剔除毛刺,抑制噪声干扰。一般单进程状态机都可以达到这样的设计

目的。

单进程

//单进程状态机
process (clk,reset)
    begin
    if reset =‘1‘then
        current_state <=st0;
    else if (rising_edge(clk))then
        current state <next state;
        case current state is
            when sto=>
                if in1 ‘1‘then
                    next_state <st1;
                end if;
                    out1<=‘0‘;
                    out2<=‘0‘;
            when st1=>
                if in2 ‘1‘then
                next_state<=st2;
                end if;
                out1<=‘1‘;
                out2<=0‘;
            when st2=>
                if in1 ‘0‘and in2=‘1‘then
                next_state<=st3;
                end if;
                out1<=‘0‘;
                out2<=‘1‘:
            when st3=>
            if in1 =‘0‘and in2=‘0‘then
                next_state <=st0;
            end if;
                out1<=1‘;
                out2<=‘1‘;
            when others =next_state<=sto;
        end case;
    end if;
end process;


双进程状态机

//双进程
process (clk,reset)
begin
    if reset =‘1‘then
        current state <=st0;
    elsif (rising_edge(clk))then
        current state <next state;
    end if;
end process;


process (inl,in2,current_state)
begin
    case current state is
        when sto=>
            if inl =‘1‘then
                next state <=st1;
            end if;
                out1<=‘0‘;
                out2<=‘0‘;
        when st1=>
            if in2 =‘1‘then
                next state <=st2;
            end if;
                out1<=‘1‘;
                out2<=0‘;
        when st2=>
            if in1 =‘0‘and in2 =‘1‘then
                next state<=st3;
            end if;
                out1<=0‘;
                out2<=1‘;
        when st3=>
            if in1 =‘0‘and in2 =‘0‘then
                next state <sto;
            end if;
                out1<=1‘;
                out2<=‘1‘;
        when others=>next state <=sto;
    end case;
end process;

三进程状态机

process (clk,reset)
    begin
        if reset =‘1‘then
            current state <=st0;
        elsif (rising_edge(clk))then
            currentstate <next_state;
    end if;
end process;


process (in1,in2,current state)
begin
    case current state is
        when sto=>
            if inl =‘1‘then
                next_state <st1;
            end if;
        when st1=>
            if in2 =‘1‘then
                next state <st2;
            end if;
        when st2=>
            if inl =‘0‘and in2 =‘1‘then
                next_state <st3;
            end if;
        when st3=>
            if in1 =‘0‘and in2 =‘0‘then
                next_state <sto;
            end if;
        when others=>next state <sto;
    end case;
end process;
    
    
process (clk,reset)
begin
    if reset=‘1‘then
        out1<=‘0‘;
        out2<=‘0‘;
    elsif (rising_edge(clk))then
        case current state is
            when sto=> 
                out1<=‘0‘;
                out2<=‘0‘;
            when st1=>
                out1<=1‘;
                out2<=‘0‘;
            when st2=>
                out1<=0‘;
                out2<=‘1‘;
            when st3=>
                out1<=‘1‘;
                out2<=1‘
            when others =
                out1<=‘0‘;
                out2<=‘0‘:
        end case;
    end if;
end process;

3、设计综合工具能够识别的状态机

1、原则

1、给状态机的输出分配默认值,防止综合器产生不必要的锁存器。

2、将状态机逻辑和所有的算术逻辑功能以及数据路径分离,包括与状态机输出值的分配分离,这也是为何推荐大家尽量使用多进程来描述状态机的原因。

3、如果设计中包含一个在多个状态都要使用的运算,那么在状态机外面定义这个运算,然后让状态机的输出逻辑来使用该运算结果

4、使用简单的同步或异步复位来确保状态机定义了一个上电初始状态。如果状态机设计中,复位逻辑比较复杂,比如同时使用了一个异步复位和异步加载信号来定义初始状态,那么综合工具只会将状态机综合成普通逻辑。

4、采用verilog写状态机


module verilog_fsm(
    input clk,
    input reset,
    input [3:0] in_1, in_2;

    output [4:0] out
);

parameter state_0 = 3'b000;
parameter state_1 = 3'b001;
parameter state_2 = 3'b010;
parameter state_3 = 3'b011;
parameter state_4 = 3'b100;
reg [4:0] tmp_out_0, tmp_out_1, tmp_out_2;
reg [2:0] state, next_state;

always@ (posedge clk or posedge reset)
    if(reset)
        state <= state_0;
    else 
        state <= next_state;

always@ (*)
    begin
        tmp_out_0 = in_1 + in_2;
        tmp_out_1 = in_1 - in_2;
        case(state)
            state 0:begin
                tmp_out_2= in1 + 5'b00001;
                next_state=state_1;
            end
            state_1:begin
                if (in_1<in_2)begin
                    next_state = state_2;
                    tmp_out2 = tmp_out_0;
                end
                else begin
                    next_state = state_3;
                    tmp_out_2 = tmp_out_1;
                end
            end
            state_2:begin
                tmp_out_2 = tmp_out - 5'b00001;
                next_state = state_3;
            end
            state_3:begin
                tmp_out_2=tmp_out_1+5'b00001;
                next_state=state_0;
            end
            state_4:begin
                tmp_out_2 = in_2+5'b00001;
                next_state=state_0;
            end
            default:begin
                tmp_out_2=5'b00000;
                next state=state_0;
            end
        endcase 
    end


endmodule

第六章  如何在书写代码时进行速度优化

速度面积互换,并行及同步设计原则

一、逻辑设计中速度的概念

速度:设计吞吐量、设计延时及设计时序

1、逻辑设计延时:逻辑设计延时指的是输入数据和数据被处理后输出之间的时间间隔,它的典型单位是普通时间单位(s、s、us等)或者是系统时钟周期数。

2、插入寄存器的方法:是通过增加设计流水来提升设计时序的目的。

二、时序收敛的早期考虑

1、设计步骤

(1)制定设计指导文件,也可以称其为总体规划文件。

(2)设计功能代码,即编写设计源代码。

(3)进行功能仿真。

(4)编译设计。

(5)时序分析,查看设计是否满足时序收敛的要求。

(6)假如设计未满足时序收敛要求,调整编译器设置,比如启动物理综合等。

(7)重新编译,或重复上一步直到达到时序收敛要求。

三、考虑时序优化

1、编写时序收敛代码的总体规则

1、在进行代码设计之前一定要进行良好的计划和规划

2、第二个基本原则,就是在代码编写过程中,要时刻记得所编写的代码具体实现的电路到底是什么,不要停留在行为级,要深人到数据路径、寄存器等。

3、第三个基本原则,就是要求大家在进行代码设计的时候一定要“理论联系实际”,也即设计出来的代码需要和目标器件相匹配,当然首先是需要根据设计需求选择合适的器件。 

4、第四个基本原则,就是其他的一些推荐原则,比如之前讨论到的并行原则、同步原则(尽量使设计百分之百地同步于时钟),不使用门控时钟和行波时钟,转而使用时钟使能、时钟控制模块函数或者寄存器等。

2、通过减少关键路径上的组合逻辑单元数来优化时序

1、关键路径重组:通过交换LE(或ALM)中查找表(LUT)的输入端口的位置,就可以大大缩小关键路径的延时。

 3、适当进行逻辑复制以优化设计速度

逻辑复制:是指当某个信号的扇出比较大时,会造成该信号到各个目的逻辑节点的路径变得过长,从而成为设计中的关键路径,为了解决这个问题,可以通过在书写代码的时候对该信号进行复制,以达到“分担”信号扇出过多的目的。

4、在组合逻辑中插入寄存器优化时序

现代逻辑设计大部分是同步设计,如果寄存器和寄存器之间的逻辑路径过长,就会拖累设计的关键路径,这时就可以考虑在该路径上插入额外的寄存器,这种插入寄存器的方法,也被称为插入流水线。

 未插入流水的代码

//未插入流水
process (clk,clr)
begin
    if clr='0' then
        atemp<=(others=>'0');
        btemp<=(others=>'0');
        ctemp<=(others=>'0')
        dtemp<=(others=>'0');
        result<=(others=>'0');
    elsif clk event and clk= '1' then
        atemp <= a;
        btemp<= b;
        ctemp<= c;
        dtemp<= d;
        result<=(atemp*btemp)*(ctemp*dtemp);
    end if;
end process;

插入流水的代码(共三级流水)

process (clk,clr)
begin
    if clr=‘0‘then
        atemp<=(others=>'0');
        btemp<=(others=>'0');
        ctemp<=(others=>'0');
        dtemp<=(others=>'0');
        int1<=(others=>'0');
        int2<=(others=>'0');
        result<=(others=>'0');
    elsif clk‘event and clk=‘1‘then
        atemp <=a;
        btemp<=b;
        ctemp<=c;
        dtemp<=d;
        int1 <atemp*btemp;
        int2 <ctemp*dtemp;
        result<=int1*int2;
    end if;
end process;

5、通过寄存器平衡优化时序

如设计规范要求不允许插入额外的寄存器(插入寄存器意味着增加一个时钟周期计算延时

即设计对计算延时有特殊要求)时,此时可以通过所谓的寄存器平衡来达到优化速度的目

的,这种技巧在Altera的编译工具中被称为寄存器重定时(Retiming)。

1、操作符平衡

2、寄存器平衡:从概念上来讲,该技巧是将任意两个寄存器之间的逻辑重新平均配,以达到最小化这两个寄存器之间的最大延时的目的。这个技巧通常用于关键路径和其相邻路径之间的逻辑分布高度不平衡的情况。

6、通过并行结构优化时序

和操作符平衡差不多

0ut1<=I1+I2+I3+I4:

0ut2<=(I1+I2)+(I3+I4);

7、通过消除代码中的优先级优化速

消除if else 中的判断优先级

1、用case语句

2,用并列的if语句

本章讨论了如何减少关键路径延时、逻辑复制、插入寄存器增加流水、寄存器平衡、使用并行结构以及消除代码中的优先级等优化设计速度的方法。

第七章  如何在书写代码时进行面积优化

面积优化,就是让设计在消耗尽可能少的门电路资源的情况下实现指定的功能。

一、操作符平衡

res = a * b * c * d

优化

res = (a * b)* (b * d)

二、 打破设计流水

由第六章的3.4可以看到撤除第二级流水对设计的面积并无影响,但已无法对设计的速度进行优化。

只撤除第1级流水

process (clk,clr)
begin
    if clr=‘0‘then
        int1<=(others=>'0');
        int2<=(others=>'0');
        result<=(others=>'0');
    elsif clk‘event and clk=‘1‘then
        int1 <a*b;
        int2 <c*d;
        result<=int1*int2;
    end if;
end process;

只撤除最后一级流水

process (clk,clr)
begin
    if clr=‘0‘then
        atemp<=(others=>'0');
        btemp<=(others=>'0');
        ctemp<=(others=>'0');
        dtemp<=(others=>'0');
        int1<=(others=>'0');
        int2<=(others=>'0');
        result<=(others=>'0');
    elsif clk‘event and clk=‘1‘then
        atemp <=a;
        btemp<=b;
        ctemp<=c;
        dtemp<=d;
        int1 <atemp*btemp;
        int2 <ctemp*dtemp;
       
    end if;
end process;

 result<=int1*int2;

三、资源共享

1、在互斥中共享操作符

process (clk,rst)
variable tmp_q:std_logic_vector(7 DOWNTO 0);
begin
    if rst =‘0‘then
        tmp_q =(others=>0‘);
    elsif clk‘event and clk=‘1‘then
        if upddn =‘1‘then
            tmp_q tmp_q+1;
        else
            tmp_q:=tmp_q-1氵
        end if;
    end if;
    q<=tmp_q;
end process;

优化------>增加ALUT共享操作符

process (clk,rst)
variable tmp_g:std_logic_vector(7 DOWNTO 0);
variable dir integer range -1 to 1;
begin
    if rst =‘0‘then
        tmp_g =(others=>‘0‘);
    elsif clk‘event and clk=‘1‘then
        if upddn=‘1‘then
            dir =1;
        else
            dir:=-1;
        end if;
    tmp_q =tmp_q+dir;
    end if;
    q<=tmp_qi
end process;

2、共享表达式

如  y = a * b * c;

z = b * c * d;

优化 

y = a * ( b * c)

z = (b * c) * d

代码优化

temp = b * c

y = a * temp;
z = temp * d;

3、共享逻辑功能模块

使用可复用的module 

四、复位对设计面积的影响

1、在进行逻辑电路设计时,经过分析,如果确信可以不加复位,那么最好慎用复位。一般判断的原则是,类似移位寄存器这类直通型电路,只是起到了一个传输信号的作用,本身并未对信号产生任何影响。其传输信号正确与否,在于其输入端的信号是否正确。

2、另外,如果确信需要使用复位信号,最好使用异步复位,因为根据逻辑器件本身寄存器自身结构,可知寄存器的异步复位不需要消耗额外的资源。

五、从器件角度理解如何节省资源

1、利用厂家原语讲行面积优化

2、巧用触发器的控制端口,触发器由外部复位信号进行异步复位

3、寄存器控制端口优先级

 4、多路选择器优化

为了提升设计时序,在没有优先级要求的情况下尽量避免使用优先级多路复用器。

第8章  代码优化设计实例分析

主要是时序优化

一、对设计时序进行优化的实例分析

1、实例一,同步电路时序分析

1、由过多逻辑层级造成时序违规

解决:插入流水,用非阻塞赋值

2、实例二,异步电路及时序例外分析

多周期路径规划

3、利用PLL对设计进行时序优化

二、修改代码优化面积具体实例分析

第9章  如何编写可综合代码

一、普通if和case语句可综合代码书写规则

1、if 语句

//层层判断 会产生最大路径a
module mult if(a,b,c,d,sel,z);
input a,b,c,d;
input [3:0] sel;
output z;
reg z;
always @(a or b or c or d or sel)
begin
    z=0;
    if (sel[0])z=a;
    if (sel[1])z=b;
    if (sel[2])z=c;
    if (sel[3])z=d;
end
endmodule


//用if-else
module mult if(a,b,c,d,sel,z);
input a,b,c,d;
input [3:0] sel;
output z;
reg z;
always @(a or b or c or d or sel)
begin
    z=0;   //初始化后不加else不会产生锁存器
    if (sel[0])z=a;
    else if (sel[1])z=b;
    else if (sel[2])z=c;
    else if (sel[3])z=d;
end
endmodule

2、case语句

1、建议慎用casex或casez语句,

二、如何调整if和case语句中关键信号的路径

1、多个if语句

//使b_is_late路径更近
module mult if(a,b,c,d,sel,z);
input a,b_is_late,c,d;
input [3:0] sel;
output z;
reg z;
always @(a or b or c or d or sel)
begin
    z=0;
    if (sel[0])z=a;
    if (sel[2])z=c;
    if (sel[3])z=d;
    if (sel[1] & ~(sel[2] | sel[3]))z=b_is_late;
    else 
        z=0;
end
endmodule

 2、单个if语句

module single if_late_v(A,C,CTRL_is_late_arriving,Z);
input [6:1]A;
input [5:1]C;
input CTRL is_late_arriving;
output Z;
reg Z;

always @(C or A or CTRL is_late arriving)
begin
    if(c[1]==1‘b1)
        Z=A[1]:
    else if (C[2]==1‘bo)
        Z=A[2];
    e1se if(c[3] ==1‘b1)
        Z=A[3];
    else if(C[4] ==1‘b1 && CTRL is_late arriving==1‘b0)
//late arriving signal in if condition
        Z=A[4];
    e1se if(c[5] =-1‘b0)
        Z=A[5]:
    else
        Z=A[6];
end
endmodule

优化

module single if improved(A,C,CTRL is late arriving,Z);
input [6:1]A;
input [5:1]C;
input CTRL is_late_arriving;
output Z;
reg Z;
reg Z1;
wire Z2, prev_cond;

always @(A or C)
begin
    if(C[1]==1‘b1)
        Z1=A[1];
    else if (C[2]==1‘b0)
        Z1=A[2];
    else if (C[3]==1‘b1)
        21=A[3]:
    e1se if(C[5]==1‘b0)
        //removed the branch with the
        Z1=A[5];
        //late-arriving control signal
    else
        Z1=A[6];
end

assign 22 =A[4];
assign prev_cond=(C[1]==1‘b1)(C[2]==1‘b0)(C[3] ==1‘b1);

always @(C or prev_cond or CTRL is_late_arriving or 21 or 22)
begin
    if (C[4] ==1‘b1 & CTRL_is_late_arriving==1‘b0)
        if (prev_cond)
            Z=Z1;
        else
            Z=Z2;
    else 
         Z = Z1;
end

endmodule

3、if - case 嵌套

module case_in_if_01_v(A, DATA_is_late_arriving, C, sel, Z); 
input [8:1]A;
input DATA_is_late_arriving;
input [2:0] sel;
input [5:1] C;
output Z;
reg Z;
always (sel or C or A or DATA_is_late_arriving)
begin
    if (C[1])
        Z=A[5];
    else if(c[2]==1'b0)
        Z=A[4];
    else if (C[3])
        Z=A[1];
    else if(c[4])
        case (sel)
            3'b010:z=A[8];
            3'b011:Z=DATA_is_late_arriving;
            3'b101:z=A[7];
            3'b110:z=A[6];
            default:Z=A[2];
        endcase
    else if(c[5]==1'b0)
        Z=A[2];
    else
        Z = A[3];
end
endmodule

优化

module case_in_if_01_improved(A, DATA_is_late_arriving, C, sel, Z); 
input [8:1]A;
input DATA_is_late_arriving;
input [2:0] sel;
input [5:1] C;
output Z;
reg Z;
reg Z1, FIRST_IF;
always (sel or C or A or DATA_is_late_arriving)
begin
    if (C[1])
        Z1=A[5];
    else if(c[2]==1'b0)
        Z1=A[4];
    else if (C[3])
        Z1=A[1];
    else if(c[4])
        case (sel)
            3'b010:z1=A[8];
            //3'b011:Z=DATA_is_late_arriving;
            3'b101:z1=A[7];
            3'b110:z1=A[6];
            default:Z1=A[2];
        endcase
    else if(c[5]==1'b0)
        Z1 =A[2];
    else
        Z1 = A[3];

    FIRST_IF = (C[1] == 1'b1) || (C[2] == 1'b0) || (C[3] == 1'b1);
    if(!FIRST_IF && C[4] && (sel == 3'b011))
        Z = DATA_is_late_arriving;
    else
        Z = Z1;
end
endmodule

4、case 嵌套if

module if_in_case(sel,X,A,B,C,D,Z)
input 【2:0】sel;
input X,A,B,C,D;
output Z;
reg Z;
always @(sel or X or A or B or C or D)
    begin
        case (sel)
            3'b000:Z=A;
            3'b001:Z=B;
            3'b010:
                if(X == 1'b1)
                    Z=C;
                else
                    Z=D;
            3'b100:Z=A^B;
            3'b101:Z=!(A&&B);
            3'b111:Z=!A;
            default:Z=!B;
        endcase
    end
endmodule

优化

module if_in_case(sel,X,A,B,C,D,Z)
input [2:0] sel;
input X,A,B,C,D;
output Z;
reg Z;
reg Z1, Z2;
reg [1:0] i_sel;
always @(sel or X or A or B or C or D)
    begin
        i_sel = {sel[2], sel[0]};
        case(i_sel)
            2'b00:Z1 = A;
            2'b01:Z1 = B;
            2'b10:Z1 = A^B;
            2'b01:Z1 = !(A&&B);
            default: Z1 = !B;
        endcase
        case(i_sel)
            2'b00: if(X == 1'b1)
                        Z2 = C;
                    else 
                        Z2 = D;
            2'b11: Z2 = !A;
            default: Z2 = !B;
        endcase
        if(sel[1])
            Z = Z2;
        else
            Z = Z1;


    end
endmodule

三、通过复制数据路径提高设计性能

module BEFORE (ADDRESS,PTR1,PTR2,B,CONTROL,COUNT)
input 【7:0 PTR1,PTR2;
input 【15:0】ADDRESS,B;
input CONTROL;
CONTROL
output 【15:0】COUNT;
parameter 【7:0】BASE=8‘b10000000;
wire 【7:0】PTR,OFFSET;
wire 【15:0】ADDR;

assign PTR =(CONTROL == 1‘b1)PTR1:PTR2;
assign OFFSET BASE-PTR;//Could be any function f(BASE,PTR)
assign ADDR ADDRESS-(8‘h00,OFFSET);
assign COUNT ADDR+B;
endmodule

优化

module PRECOMPUTED(ADDRESS,PTR1,PTR2,B,CONTROL,COUNT);
input 【7:0 PTR1,PTR2;
input 【15:0】ADDRESS,B;
input CONTROL;
output 【15:0】COUNT;
parameter 【7:0】BASE =8‘b10000000;
wire 【7:0】OFFSET1,OFFSET2;
wire 【15:0】ADDR1,ADDR2,COUNT1,COUNT2;

assign OFFSET1 = BASE-PTR1;
assign OFFSET2 = BASE-PTR2;
assign ADDR1 = ADDRESS-{8‘h00,OFFSET1);
assign ADDR2 = ADDRESS-{8‘h00,OFFSET2);
assign COUNT1 = ADDR1 + B;
assign COUNT2 = ADDR2 + B;
assign COUNT =(CONTROL ==1‘b1)COUNT1: COUNT2;

endmodule

1、如何更好地处理f条件语句中的算术操作

if(a + b < 24)   替换成   if(a < 24 - b)

四、代码可综合常用指导原则

1、避免创建不必要的锁存器

补齐if - else  和 case 语句,如果不补齐,则需要在进程一开始给所有的输出赋一个初始值

2、进程的敏感列表要完备

3、for循环的使用以及避免组合逻辑回环

1、循环语句在可综合代码中不能用于算法迭代实现。
如: x=1;   for(i =0; i< N; i++)  x = x * i;

这在软件上能很好执行,因为每一次迭代,会有一个内部寄存器被更新为x的当前值。但在硬件上不可以综合,因为这在逻辑中产生了组合回环,如下。

 解决这种组合回环的根本性方案就是在所有的反馈环上插入寄存器,如。

always@ (posedge clk)
    for(i = 0; i < 32; i = i + 1)
        out[i] = Y[i*2] ^ x[i];

此外,在使用for循环的时候不要将固定的赋值语句摆在for循环内部。对于VHDL来说,HDL编译器肯定会将or循环打开,所以这种结构最终会根据循环次数展开成多次赋值操作。将固定的赋值语句从for循环中删除并摆在for循环外部,就会节省编译器很多。

4、阻塞和非阻塞赋值

1、阻塞赋值是立即赋值非阻塞赋值是在仿真周期结束时(即所有变量读取都已完成)才将新值赋给寄存器。这样,代码段的行为不再受always语句执行顺序的影响。

5、可综合代码设计对时钟和复位的要求

要尽量避免在设计中使用内部逻辑产生的有条件的复位信号,而且要确保设计中寄存器仅仅由一个简单复位信号控制。 

第十章   综合以及布局布线优化

一、综合级速度与面积优化设置

1、当器件资源利用率接近100%时,综合级的速度优化可能不会总是产生一个更快的设计。而实际上,此时综合级的面积优化反而可能获得一个更快的设计。

2、基于综合的门级优化主要包括状态机编码、并行结构与交错多路选择器的选择以及逻辑复制等。

二、使用设计助手和优化顾问

三、对设计执行早期时序估算

四、综合网表优化

1、网表优化就是针对设计的综合网表进行优化。网表是指使用了Altera定义的原语来描述的设计。网表的格式有多种,可以是标准的电子设计交换格式(EDF),也可以是一个第三方综合工具产生的Verilog的Quartus映射文件格式(VQM),当然还可以是Quartus Ⅱ自身综合工具生成的网表文件

五、物理综合

1、在开始进入正题之前,先要搞清楚两个概念,即物理综合和逻辑综合。

逻辑综合是将HDL描述语言转换成不含布局布线信息且能够映射(Mp)到物理器件的门电路过程。

物理综合是通过改变网表的布局(Placement)从而获得不错的综合结果。

2、编译主要分为综合和布局布线(Altera称其为Fitting)两步,综合阶段的优化主要是针对面积、速度或二者兼之来对一个电路的逻辑结构进行优化,然后布局布线工具在对电路进行布局和布线时确保逻辑中的关键部分尽可能靠近以及尽可能使用最快的布线资源。

1、针对性能的物理综合优化选项

1.对组合逻辑施加物理综合,减少关键信号的路径

2.执行寄存器重定时

3.执行自动异步信号流水(Perform Automatic Asynchronous Signal Pipelining)

Recovery Time:异步信号在时钝沿到来必须稳定的最小时间。

Removal Time:异步信号在时钟沿到来必须稳定的最小时间。

4.寄存器复制(Perform Register Duplication)

六、了解逻辑单元所见即所得结构

1、所以布线延时会随着“曼哈顿”(Manhattan)距离而线性地增加,走线速度由快到慢的顺序排列 

2、综合网表建议

3、综合及优化

一个ALM(Adaptive Logic Module)包括最多2个lcell_comb模块和2个lcell_ff模快,如图10-37所示。

第十一章   预先布图规划

1、增量编译  是QuartusⅡ软件中一个可选并可提升默认编译性能的编译流程,所以需要设计者进行一些设置或操作才能正确地启用该编译流程。

优点:

对于大型设计可以平均减少75%的编译时间。

可以保留未更改模块的设计性能。

提供可重复使用的设计结果并减少编译次数。

利于开启团队设计流程

2、除了对设计进行设计分区,在增量编译的时候还可以给设计创建预先布图规划,对应分区创建的工具,创建这种布图规划的工具叫做Logiclock。

一、使用增量编译的设计流程

1、一般,标准的增量编译设计流程将会使用单独的顶层工程,并将其分成多个分区,所有分区都可以在同一个QuartusⅡ工程之中一起编译和优化。

2、对设计进行分区和布图规划,保证逻辑被合理的组合在一起,优化设计和实现。

3、如果设计并未很好遵循设计分区指导,由于跨边界优化的限制,对设计进行分区可能会导致资源消耗的增加。布图规划同样也会造成资源消耗的增加

4、相对普通设计来说,对设计进行分区以及布图规划约束,还可能会带来布线资源的增加。

二、设计分区划分指导原则

1.寄存分区的输入和输出

2.尽可能减少跨分区边界端口,尽量减少各个分区跨分区边界的I/O路径,保持逻辑路径位于单一的分区之内以利于设计优化。这样做,不但可以使分区更加独立,而且可以优化逻辑和布局。

3.研究是否需要在分区间进行逻辑优化

(1)将逻辑保持在同一个分区进行优化和合并。

(2)将常量和逻辑保持在同一个分区之中。

(3)避免驱动多个分区I/O或连接多个I/O在一起的信号。

(4)在目的分区中翻转时钟。

(5)打包跨分区边界的寄存器时直接连接I/O引脚到I/O寄存器。

(6)不要使用内部三态。

(7)将所有三态和使能逻辑划分在同一个分区中。

(8)将所有双向I/0寄存器划分在同一个分区中(针对老器件)。

三、如何对第三方设计进行分区划分

1.给分区分配逻辑资源

2、根据需要分配全局布线信号和时钟网络

3、约束分配虚拟引脚

4.根据需要执行时序预算安排

5.直接驱动时钟

6、给底层分区重新创建PLL

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
FPGA设计实战演练高级技巧)是一种通过实践来提高FPGA设计能力的训练方法。在这个实践中,学习者将深入研究FPGA的复杂知识和技术,从而提高他们在FPGA设计中的技能水平。 首先,在这个实践中,学习者将学习更高级和复杂的FPGA设计技巧。这些技巧包括使用高级设计语言,如VHDL或Verilog,实现复杂的功能模块和子系统。学习者还会学习到如何优化设计,以减少资源利用和功耗,提高FPGA设计的性能。 其次,学习者将学习如何设计和实现高级的通信接口和协议。这包括学习FPGA与其他设备之间的通信接口,如UART、I2C、SPI和PCIe,并学习如何通过FPGA实现这些接口的协议。 此外,学习者还将学习如何使用现有的IP核来加快FPGA设计开发过程。他们将学习如何在设计中使用Xilinx和Altera等厂商提供的IP核,以实现各种功能。学习者还将学习如何使用IP核的参数和接口进行配置和定制。 最后,学习者将参与到实际的项目中,应用所学到的高级技巧进行FPGA设计。他们将面对真实的设计挑战,需要解决各种问题,并设计出可靠和高效的解决方案。 通过这种高级技巧FPGA设计实践,学习者将不仅提高他们的FPGA设计能力,还能够在实际项目中应用他们所学到的知识。这种实践方法使学习者能够更好地理解FPGA设计的复杂性,并能够独立完成复杂的FPGA设计任务。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

eachanm

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值