开发自己的内核以嵌入基于FPGA的处理器系统



因此,在本周期第一篇文章中,据说最好使用处理器系统来控制我们的设备,该设备使用针对Redd复合体的FPGA实现,然后在第一篇和第二篇文章中将介绍如何制作此系统。 好的,完成了,我们甚至可以从列表中选择一些现成的内核以将其包括在其中,但是最终目标是管理我们自己的自定义内核。 现在该考虑如何在处理器系统中包含任意内核了。

该周期的所有文章:
为Redd中安装的FPGA开发最简单的“固件”,并以内存测试为例进行调试
为Redd中安装的FPGA开发最简单的“固件”。 第2部分。程序代码

为了理解当今的理论,您应该找到并下载Avalon接口规范文档,因为Avalon总线是NIOS II系统的基础总线。 我将参考章节,表格和数据来修订2018年9月26日的文档。

我们打开第3节专门讨论内存映射接口,或者说-3.2。 表9列出了总线信号。 请注意,所有这些信号都是可选的。 我在“必填”列中找不到单个信号为“是”的信号。 我们可能不会将此信号转发给我们的设备。 因此,在最简单的情况下,总线的实现极其简单。 该表的开头如下所示:



如您所见,所有信号都被很好地描述了(除了用英语完成)。 以下是各种情况下的时序图。 最简单的情况不会引起任何问题。 现在,我将从文档中获取时间表,并用半透明的填充内容覆盖某些行(它们都是可选的,我们有权排除任何考虑因素)。



吓人的 但是一切都很简单:给我们地址和选通脉冲,我们必须在readdata总线上设置数据。 反之亦然:给我们地址,写数据总线上的数据和写选通脉冲,我们必须对数据进行捕捉。 典型的同步总线一点也不可怕。

当存储器访问不是32位字时,需要隐蔽的一行。 当我们设计通用内核时,这非常重要。 但是,当我们设计一个一天的核心时,我们只需在文档中编写有关该核心的内容(我是脑中反对商标的人,但是有人可以将其限制在此范围内),我们需要使用32位字,仅此而已。 好了, 响应信号非常特殊,原则上我们不感兴趣。

有时很重要的一点是,当设备未就绪时,可以将总线的运行延迟几个时钟周期。 在这种情况下,应添加WaitRequest信号。 时序图将更改如下:



WaitRequest被挂起时 ,向导会知道我们的设备正忙。 如果未重置此信号,请小心,整个系统在处理后将“冻结”,因此只有重启FPGA才能重置它。 JTAG随系统挂起。 我上次观察到这种现象是在撰写本文时,因此记忆仍然生动。

在公司文档中进一步考虑了更高效的数据流水线和批处理事务案例,但是本文的任务不是考虑所有可能的选择,而是向读者展示工作方式,强调所有这些都不是令人恐惧的,因此我们将自己局限于这两个简单的选择。

让我们设计一些简单的设备,该设备会定期在总线上不可用。 首先想到的是串行接口。 在传输过程中,我们将等待系统。 在生活中,我强烈建议您不要这样做:处理器将一直停止直到忙碌的事务结束,但这对于本文来说是一个理想的情况,因为实现代码将是可以理解的,并且不会很麻烦。 通常,我们将制造一个串行发送器,该发送器可以将数据和芯片选择信号发送到两个设备。



让我们从最简单的轮胎选项开始。 让我们制作一个并行输出端口,该端口形成选择晶体的信号。



为此,我将采用上一篇文章中获得的项目,但是为了避免混淆,我将其放在AVALON_DEMO目录中。 我不会更改其他文件的名称。 在此目录中,创建my_cores目录。 目录名称可以是任何名称。 我们将在其中存储我们的核心。 没错,今天将是其中之一。 创建具有以下内容的CrazySerial.sv文件:
module CrazySerial ( input clk, input reset, input [1:0] address, input write, input [31:0] writedata, output reg [1:0] cs ); always @(posedge clk, posedge reset) begin if (reset == 1) begin cs <= 0; end else begin if (write) case (address) 2'h00: cs <= writedata [1:0]; default:; endcase end end endmodule 

让我们做对。 首先,接口线。 clkreset是时钟线和复位线。 地址 数据行的名称来自带有“ 内存映射接口”文档的信号列表的表。





实际上,我可以给任何名字。 逻辑线与物理线的链接将在稍后进行。 但是,如果您提供表中的名称,则开发环境将自行连接它们。 因此,最好从表中获取名称。

嗯, cs是从芯片出来的晶体选择线。

实现本身是微不足道的。 复位时,输出清零。 如此-在每种情况下,我们都会检查是否存在写入信号。 如果地址等于零,则单击数据。 当然,可以在此处添加一个解码器,这将阻止一次选择两个设备,但是生活中的美好时光会使文章超负荷。 本文仅提供最必要的步骤,但是,请注意,生活中所有事情都可以做得更复杂。

太好了 我们准备将这些代码引入处理器系统。 我们转到Platform Designer ,选择我们在过去的实验中构建的系统作为输入文件:



我们提请注意左上角的“ 新组件”项:



要添加组件,请单击此项目。 在打开的对话框中,填写字段。 对于本文,仅填写组件名称:



现在转到“ 文件”选项卡,然后单击“ 添加文件”



添加先前创建的文件,在列表中选择它,然后单击分析综合文件



解析SystemVerilog没有错误,但是有一些概念上的错误。 它们是由于开发环境错误地连接了某些线路而造成的。 我们转到“ 信号和接口”选项卡,并在此处注意:



CS行被错误地分配给avalon_slave0接口readdata信号。 但是,由于我们从文档表中给它们起了名字,因此所有其他行都被正确识别。 但是问题线怎么办? 应该将它们分配给类似导管的接口。 为此,请单击“添加界面”项



在下拉菜单中,选择管道



我们得到一个新的界面:



如果需要,可以重命名。 的确,如果我们要创建多个外部接口,这肯定是必要的。 作为文章的一部分,我们将其保留为导管名。 现在,我们用鼠标钩住cs线并将其拖动到此界面中。 我们必须设法在导管_end线下抛出一个信号,然后我们才能这样做。 在其他地方,光标将显示为划线的圆圈。 最后,我们应该有:



chipdata将信号类型替换为readdata 。 最终图片:



但是错误仍然存​​在。 没有为阿瓦隆总线分配复位信号。 我们从列表中选择avalon_slave_0并查看其属性。



reset替换任何内容 。 同时,我们将检查其他接口属性。



可以看出,寻址是用语言表达的。 好了,这里还配置了文档中的许多其他内容。 在这种情况下获得的时间图将绘制在属性的最底部:



实际上,没有更多的错误了。 您可以单击“ 完成” 。 我们创建的模块出现在设备树中:



将其添加到处理器系统,连接时钟信号并重置。 我们将数据总线连接到Data Master处理器。 双击Conduit_end并为外部信号命名,例如lines 。 原来是这样的:



重要的是不要忘记,由于我们在系统中添加了一个块,因此必须确保该块不与地址空间中的任何人冲突。 在这种特殊情况下,图中没有冲突,但是无论如何,我将选择菜单项System-> Assign Base Addresses

仅此而已。 该块已创建,配置并添加到系统中。 点击生成HDL按钮,然后点击完成

我们对该项目进行草拟,然后转到Pin Planner并分配分支。 原来是这样的:



对应于接口连接器的触点B22和C22。

我们进行最终组装,将处理器系统加载到FPGA中。 现在我们需要优化程序代码。 启动Eclipse。

让我提醒您,我目前正在使用一个项目,该项目与我上次使用Redd时所处的目录不同。 为了避免混淆,我将从树中删除旧项目(但仅从树中删除,而不删除文件本身)。



接下来,我在空白树上单击鼠标右键,然后从菜单中选择“ 导入 ”:



Next- General->现有项目进入工作区



只需选择存储项目文件的目录:





从过去的实验继承的两个项目都将连接到开发环境。



我将突出显示框架中的下一项:
每次更改硬件配置后,再次为BSP项目选择Nios II-> Generate BSP菜单项。




实际上,此操作之后,一个新的块出现在\ AVALON_DEMO \ software \ SDRAMtest_bsp \ system.h文件中
 /* * CrazySerial_0 configuration * */ #define ALT_MODULE_CLASS_CrazySerial_0 CrazySerial #define CRAZYSERIAL_0_BASE 0x4011020 #define CRAZYSERIAL_0_IRQ -1 #define CRAZYSERIAL_0_IRQ_INTERRUPT_CONTROLLER_ID -1 #define CRAZYSERIAL_0_NAME "/dev/CrazySerial_0" #define CRAZYSERIAL_0_SPAN 16 #define CRAZYSERIAL_0_TYPE "CrazySerial" 

首先,我们对常量CRAZYSERIAL_0_BASE感兴趣

将以下代码添加到main()函数中:
  while (true) { IOWR_ALTERA_AVALON_PIO_DATA (CRAZYSERIAL_0_BASE,0x00); IOWR_ALTERA_AVALON_PIO_DATA (CRAZYSERIAL_0_BASE,0x01); IOWR_ALTERA_AVALON_PIO_DATA (CRAZYSERIAL_0_BASE,0x02); IOWR_ALTERA_AVALON_PIO_DATA (CRAZYSERIAL_0_BASE,0x03); } 

我们开始调试,并用示波器查看各行的内容。 必须有增量二进制代码。 他在那里。



此外,访问端口的频率非常好:



大约25 MHz是总线频率的一半(2个时钟周期)。 有时访问时间不是2个周期,而是更长。 这是由于在程序中执行了分支操作。 通常,最简单的总线访问方式是可行的。

现在是时候添加例如串行端口的功能了。 为此,添加与总线相关的waitrequest接口信号以及一对串行端口信号scksdo 。 总计,我们在systemverilog上获得以下代码片段:



相同的文字:
 module CrazySerial ( input clk, input reset, input [1:0] address, input write, input [31:0] writedata, output waitrequest, output reg [1:0] cs, output reg sck, output sdo ); 


根据良好格式的规则,您需要制造一台用于传输数据的简单机器。 不幸的是,本文中最简单的机器看起来非常困难。 但是实际上,如果我不增加机器的功能(并且我不会在本文的框架内进行此操作),那么它将只有两种状态:传输正在进行中,传输正在进行中。 因此,我可以用一个信号对状态进行编码:
reg发送= 0;

在传输过程中,我需要一个位计数器,一个时钟分频器(我正在做一个故意慢的设备)和一个用于传输数据的移位寄存器。 添加适当的寄存器:
  reg [2:0] bit_cnt = 0; reg [3:0] clk_div = 0; reg [7:0] shifter = 0; 

我将频率除以10(遵循“为什么不呢?”的原则)。 因此,在第五步中,我将塞住SCK,在第十步中,放下该行,然后转到下一个数据位。 在所有其他措施上,只需增加分频器计数器即可。 重要的是不要忘记,在第四个小节上,您还需要增加计数器,在第九个上,将其清零。 如果我们省略到下一位的转换,则指定的逻辑如下所示:
  if (sending) begin case (clk_div) 4: begin sck <= 1; clk_div <= clk_div + 1; end 9: begin sck <= 0; clk_div <= 0; // <   > end default: clk_div <= clk_div + 1; endcase end else 

转到下一页很容易。 他们对移位寄存器进行了移位,然后,如果当前位是第七位,则通过切换机器的状态来停止工作,否则它们将增加位计数器。
  shifter <= {shifter[6:0],1'b0}; if (bit_cnt == 7) begin sending <= 0; end else begin bit_cnt <= bit_cnt + 1; end 

实际上,仅此而已。 输出位始终取自移位寄存器的高位:
  assign sdo = shifter [7]; 

并且是当前修订版中最重要的一行。 传输串行数据时, waitrequest信号始终翘起为一。 也就是说,它是发送信号的副本,用于设置机器的状态:
  assign waitrequest = sending; 

好了,当写入地址1时(回想一下,这里我们用32位字寻址),我们将数据捕捉到移位寄存器中,将计数器清零并开始传输过程:
  if (write) //... 2'h01: begin bit_cnt <= 0; clk_div <= 0; sending <= 1; shifter <= writedata [7:0]; end default:; endcase end 

现在,我将所有片段描述为单个文本:
 module CrazySerial ( input clk, input reset, input [1:0] address, input write, input [31:0] writedata, output waitrequest, output reg [1:0] cs, output reg sck, output sdo ); reg sending = 0; reg [2:0] bit_cnt = 0; reg [3:0] clk_div = 0; reg [7:0] shifter = 0; always @(posedge clk, posedge reset) begin if (reset == 1) begin cs <= 0; sck <= 0; sending <= 0; end else begin if (sending) begin case (clk_div) 4: begin sck <= 1; clk_div <= clk_div + 1; end 9: begin clk_div <= 0; shifter <= {shifter[6:0],1'b0}; sck <= 0; if (bit_cnt == 7) begin sending <= 0; end else begin bit_cnt <= bit_cnt + 1; end end default: clk_div <= clk_div + 1; endcase end else if (write) case (address) 2'h00: cs <= writedata [1:0]; 2'h01: begin bit_cnt <= 0; clk_div <= 0; sending <= 1; shifter <= writedata [7:0]; end default:; endcase end end assign sdo = shifter [7]; assign waitrequest = sending; endmodule 


我们开始将新代码引入系统。 实际上,该路径与创建组件时的路径相同,但是某些步骤已经可以省略。 现在,我们将熟悉优化过程。 转到平台设计器 。 如果仅更改Verilog代码,则对完成的系统执行Generate HDL操作将非常简单。 但是,由于模块有新行(即接口已更改),因此需要重做。 为此,请在树中将其选中,按鼠标右键,然后选择“ 编辑”



我们正在编辑现有系统。 因此,只需转到“ 文件”选项卡,然后单击“ 分析合成文件”



可以预见的是发生了错误。 但是我们已经知道,错误的路线应该受到谴责。 因此,我们转到Signals&Interfaces选项卡,将scksdo沿着同一行从avalon_slave_0接口拖动导管_end接口:



还要为其重命名“ 信号类型”字段。 结果应如下所示:



实际上,仅此而已。 单击Finish ,为处理器系统调用Generate HDL File ,在Quartus中起草项目,分配新的支路:



这些是接口连接器的触点A21和A22,我们进行最终组装,并填写FPGA中的“固件”。

铁更新。 现在的程序。 让我们去Eclipse。 我们记得在那里做什么? 没错,不要忘记选择Generate BSP

实际上,仅此而已。 仍然需要向程序添加功能。 我们将一对字节传输到串行端口,但是将第一个字节发送到由cs [0]行选择的设备,并将第二个字节发送到cs [1]行
  IOWR_ALTERA_AVALON_PIO_DATA (CRAZYSERIAL_0_BASE,0x01); IOWR_ALTERA_AVALON_PIO_DATA (CRAZYSERIAL_0_BASE+4,0x12); IOWR_ALTERA_AVALON_PIO_DATA (CRAZYSERIAL_0_BASE,0x02); IOWR_ALTERA_AVALON_PIO_DATA (CRAZYSERIAL_0_BASE+4,0x34); IOWR_ALTERA_AVALON_PIO_DATA (CRAZYSERIAL_0_BASE,0x00); 

请注意,那里没有可用性检查。 包裹一个接一个地走。 但是,在示波器上,所有结果都非常一致



黄色射线为cs [0] ,绿色射线sdo ,紫色射线sck ,蓝色射线cs [1] 。 可以看出,0x12代码分配给第一个设备,0x34代码分配给第二个设备。

读取方法与此类似,但是除了无法正常读取连接器底脚的内容外,我只是想出一个漂亮的例子。 但是那个例子太简陋了,以至于做起来都不有趣。 但是在这里值得注意的是,在读取此总线设置时可能非常重要:



如果有读取行,则读取时序图将出现在设置对话框中。 它将显示此参数的影响。 当读取连接器的支脚时,它仍然不会被注意到,但是当完全从相同的FIFO或RAM中读取时。 可以将RAM配置为在提交地址后立即发布数据,也可以同步发布数据。 在第二种情况下,增加了延迟。 毕竟,总线设置了地址,设置了选通脉冲。但是在时钟信号的最近沿没有数据。 它们将出现在此前沿之后。也就是说,系统具有单延迟延迟。 只需设置此参数即可将其考虑在内。 简而言之,如果您没有阅读预期的内容,请首先检查是否需要配置延迟。 其余的-阅读和写作没有什么不同。

好吧,让我再次提醒您,最好不要为了长期运行而取消总线就绪状态,否则很有可能会大大降低系统性能。 就绪信号可以将事务保持几个时钟周期,而不是最多80个时钟周期,如我的示例所示。 但是首先,对于本文而言,任何其他示例都不方便;其次,对于一日内核,这是完全可以接受的。 您将充分了解自己的行为,并避免总线阻塞的情况。 的确,如果核心能够在分配给它的时间中幸存下来,那么这种假设可能会破坏未来的生活,因为每个人都会忘记它,并使一切变慢。 但是会晚一些。

尽管如此,我们已经学会了使处理器内核控制我们的内核。 在这个可寻址的世界中,一切都是清楚的,现在是时候应对流媒体世界了。 但是我们将在下一篇文章中,甚至在几篇文章中进行介绍。

结论


本文展示了如何连接任意Verilog内核来控制Nios II处理器系统。 显示了与Avalon总线最简单的连接以及总线可以处于繁忙状态的连接的选项。 提供了到文献的链接,您可以从中找到内存映射模式下Avalon总线的其他工作模式。

生成的项目可以在此处下载。

Source: https://habr.com/ru/post/zh-CN454938/


All Articles