作者:Alan Yang,来源:DigiKey 员工DigiKey社区
本文,我们将探讨微控制器(uC)到现场可编程门阵列(FPGA)串行外设接口(SPI)。主要目标是使FPGA更容易控制。我们将探索在DigilentBASYS-3 (XilinxAtrix-7)开发板上开发,使用Verilog硬件语言,并在Arduino Nano Every上开发相应的微控制器代码。它们一起提供具有16位循环冗余校验(CRC)的分组数据传输,该校验与经过测试的16 MHz SPI串行时钟一起操作。Verilog实现应该避免任何与Xilinx特定的依赖关系,使其以最小的修改可移植到其他平台。然而,这个跨平台的理想规定还有待测试。
图 1 :uC到FPGA SPI接口的台架测试,采用Digilent BASYS 3和Arduino Nano Every。uC正在复用SSD和Basys 3板上的16个led。
许多人将FPGA作为数字逻辑的一部分,使用原理图输入工具开发使用逻辑原语的简单组合电路。许多人继续学习专门介绍Verilog或VHDL的FPGA。有些人继续学习高级逻辑或在尖端项目中实现FPGA。遗憾的是,很少有人超越独立模块,将多个模块集成到一个更大的系统中。这是有原因的。
虽然FPGA是一项了不起的技术,但它以难以控制而闻名。与微控制器(uC)相比,FPGA的复杂性要高一个数量级。硬件结构必须从头开始构建或从教科书或互联网上找到的示例实例化。
本系列文章旨在帮助您在FPGA中进行系统设计。并给出在需要速度的时间关键、并行和确定性电路中使用FPGA的最佳属性。然后,我们利用微控制器的灵活性、相对易于编程和通信堆栈(包括无线和云功能)的优势。看待这种情况的一种方法是将FPGA视为通过中等高速数据总线连接的强大微控制器外设。
目标受众是可以在Verilog中对FPGA进行编程以及对微控制器进行编程的个人或团队。这可能很适合顶尖学生,因为团队方法允许同时在FPGA和uC上工作。
由于我们的重点是FPGA,因此希望尽量减少微控制器的工作量。之所以选择Arduino Nano Every,是因为它是一种常见且众所周知的微控制器。本文的大多数读者将非常熟悉微控制器和C编程。
而不是简单地介绍Verilog和uC代码,我们将探索与设计过程相关的推理和挑战。其结果是一系列说教性的文章。然而,我相信它们会提供有用的信息。
举例说明系统定义
系统是一起工作的组件的集合。对于FPGA应用,这可能包括用于数据采集、滤波、控制的模块,以及向用户呈现信息或集成到更大的自动化系统中的方法。
为了更好地定义这个术语,让我们考虑一个具有挑战性的基于FPGA和uC的示例。假设我们希望构建一个系统来测量三相400 Hz波形的真实功率和无功功率。设计要求是提供RMS电压,RMS电流,以及准确测量信号之间的相位差。让我们进一步假设必须测量高达20 kHz的线路谐波。
为了确保最佳性能,我们假设同时进行电压和电流测量,这意味着需要6个独立的模数转换器(ADC)。奈奎斯特采样要求我们以每秒至少40,000个样本的速率进行测量。总的来说,这需要每秒24万个样本。在此之上,系统必须执行RMS和相位角计算。RMS计算机制应包括过滤,以呈现短期和长期的整合,允许快速响应瞬变,同时保持长期稳定。最重要的是确定谐波的快速傅立叶变换(FFT)。
有很多方法可以设计这样一个系统。一个高端统一通信系统或几个协调的统一通信系统可以执行该任务。但这并不是本文的重点。相反,我们将认识到数据采集和滤波器方面完全在入门级FPGA的能力范围内。
基于FPGA的并行系统可以很容易地完成这些任务。设计单个模块并不太难。事实上,许多人已经将单个ADC集成到FPGA中。真正的挑战是将所有模块粘合在一起,在需要的时候将数据移动到需要的地方。
FPGA粘合剂
就我个人而言,我遇到的最困难的FPGA学习挑战之一是顽固地忘记统一通信系统编程技术。Verilog和VHDL是硬件描述语言,而像C这样的语言是一种过程性的抽象语言,以消除硬件依赖性。我花了很长时间才理解FPGA硬件描述中固有的并行性——所有的东西,都是一次性的。
对于我们的第一个例子,我们需要可视化6个adc。我们需要可视化用于将它们连接在一起的各种控制线和数据线。接下来是保存中间结果的寄存器,执行均方根计算的平方和求和操作的乘数器和加法器,以及许多其他状态机硬件来协调这些活动。
这种基于FPGA的硬件的粘合剂是寄存器传输级(RTL)设计方法。术语寄存器传输(RT)意味着一种用于将数据从一个寄存器传输到另一个寄存器的控制机制,通常在寄存器之间插入大量组合逻辑。这与微处理器中使用的管道过程有关。例如,在第一个时钟周期,数据被呈现给加法器。在第二个时钟周期,加法器执行运算。在第三个时钟周期,数据被传送到存储器。本例中的控制器是负责初始化RT流程的状态机。出于我们的目的,我们将假设所有寄存器都属于单个时钟域。
这个想法值得重复。
在我们的RTL设计中,一个状态机或状态机集合将控制和协调数据从一个寄存器到另一个寄存器的传输。所有的操作都假定在同一个时钟域内。
回想一下,每个寄存器都是内存。这个术语适用于从单个d型触发器到FPGA大块存储器实例化的任何事物。让我们使用简单的8位寄存器来探索这个概念。
下面的例子包含了所有的RTL机制和我们的定时规定。Q输出是一个寄存器。从@(posedge clk)语句和Verilog的非块<=操作符的使用可以看出,寄存器更新与时钟的正边缘是同步的。
module reg_8bit(
input clk,
input load,
input wire [7:0] D,
output reg [7:0] Q
);
always @(posedge clk) begin
if (load)
Q <= D;
end
endmodule
考虑负载信号的性质。它必须在时钟上升沿之前是稳定的。假设所有信号都在同一时钟域中,并且假设负载信号本身是由状态机的注册输出驱动的,那么合成工具将尽最大努力确保满足这一关键时序稳定性。
请注意,在时钟的上升沿上断言了一个“load”命令行。D上的数据将在时钟的下一个上升沿上变成Q。这对新程序员来说是一个陷阱,会导致意外的单时钟延迟。为了跟踪这种RTL行为,从状态和状态的角度来思考是很重要的。最后,要注意负载信号的宽度(周期)。
在同步RTL系统中,信号的导通时间可以不小于时钟周期(上升沿到上升沿)。这可以通过理解时钟上升沿上的相关状态机更新来解释。
编程技巧 :本文中提到的设计RTL约束限制了FPGA的性能,并可能导致不必要地使用FGPA结构。不过,用@(posedge clk)规定登记所有信号,一般会提高系统稳定性。这是一个很好的起点,你可以稍后修改以满足你的需求。
频闪寄存器传输
在前面的例子中,我们注意到“加载”信号的时间(宽度)可能会变化。因为这是一个同步系统,所以宽度总是时钟的函数。最小on time是系统时钟的一个周期。这种短信号有几个不同的名称,包括频闪、滴答或脉冲。在本系列文章中,我们将使用频闪这个术语。
前面,我们将RTL定义为具有一系列寄存器的设计方法。数据在寄存器之间传输,所有这些寄存器都被假设在相同的时钟域中。在寄存器之间放置组合逻辑以修改数据,并理解所有逻辑操作必须在域时钟周期内完成。例如,对于100 MHz时钟,所有FPGA信号必须在10ns内稳定下来,以便为下一个时钟事件做好准备。
RTL过程需要一个控制器或协调控制器的集合来控制寄存器。控制寄存器的一种方法是每个控制器产生一个频闪信号来推进感兴趣的寄存器。
一个简单的基于频闪的控制器如下图所示。该RTL将在给定100 MHz时钟的情况下以20 kHz的速率产生频闪;它是一个mod-5000计数器。它是一个控制器,在这个意义上,频闪器可以用来启动一个每秒重复20000次的过程,这样的ADC。
module pulse_20k ( // mod 5000 for a 100 MHz clock
input clk,
output reg zero_strobe,
output reg [12:0] count // 13 bits to hold numbers from 0 to 4999
);
always @(posedge clk) begin
zero_strobe <= 1'b0; // default
count <= count + 1;
if (count >= 4999) begin // Count starts at 0
count <= 13'd0;
zero_strobe <= 1'b1;
end
end
endmodule
观察频闪和零计数发生在同一个时钟周期。这可能看起来有悖直觉,除非我们通过state / state-next镜头来观察状态机。当计数器处于状态4999时,(count >= 4999)条件将为真。在本例中,计数为4999是mod-5000计数器的最大计数。在时钟的下一个上升沿上,计数变回0并同时断言zero_strobe。这就像时钟的最大分钟数是60分钟。
此时我们就可以开始设计更复杂的控制器了。我们当然会在本文的后续部分中介绍。目前,简单的基于时间的控制器已经达到了它的目的。我们知道,它会在时钟域内产生一个同步的频闪。这个频闪器可以用作一个更大的RTL系统的一部分。
双缓冲
在这一点上,我们简要地探讨了RTL操作,并小心地在单个时钟域中保留同步寄存器传输。现在我们将探讨当寄存器具有不同宽度时的RTL操作。这种情况很常见,特别是在使用SPI等通信协议时。在这种情况下,SPI通常通过在连续字节上操作来处理数据,而相关的FPGA硬件可能是2到4字节宽。
例如,考虑一个10位脉冲宽度调制器(PWM)。给定reg_B1和reg_B0,可以执行如下操作:
assign reg_PWM = {reg_B1, reg_B0}[9:0];
那是一个合理的拼接。但事情出错的可能性很大。问题是reg_B1和reg_B0的更新时间。如果它们的更新之间有任何延迟,reg_PWM可能会以错误的值结束。
假设reg_B1和reg_B0是从SPI接口派生的。在这种情况下,寄存器将在不同的时间更新。作为最坏的情况,假设PWM命令从255上升到256。在某个时刻,PWM以25%的占空比运行(命令255到10位PWM)。现在假设SPI更新了reg_B1,而reg_B0仍然存在。占空比现在将跳到50%(511到10位PWM的命令)。它将保持在这个错误的值,直到reg_B0被SPI更新。这种PWM的跳变,即使是短时间的命令也会对系统稳定性产生不良影响。考虑一下如果PWM在闭环系统中控制电机,这将导致的不稳定性。
解决方案是添加一个称为双缓冲器的中间寄存器,如图2所示。这允许reg_B1自然更新为reg_B0。之后,当两个寄存器都知道要更新时,控制器可以将内容传输到一个2字节的寄存器,称为双缓冲区。这确保了PWM等器件被更新为已知的完整寄存器,而不是中间值。
图 2 :双缓冲区RTL的框图表示。
第一部分的结尾
在本文中,我们探讨了FPGA设计的一些系统级注意事项。虽然这当然不是一个完整的列表,但基本的RTL方法,具有单个时钟边界的同步设计以及频闪的使用应该在您的脑海中清晰。这些信息将帮助我们理解下一期文章中介绍的SPI模块。本文提供的线索如何SPI模块的输出频闪可用于控制数据流从uC主进入FPGA。