使用LiteX快速创建FPGA SoC工程(2)-LiteX社区-FPGA CPLD-ChipDebug

使用LiteX快速创建FPGA SoC工程(2)

一、什么是Migen

Migen由几个关键的组件组成:

  1. 基础语言库:FHDL(Fragmented Hardware Description Language);
  2. 小型通用IP库;
  3. 仿真器;
  4. build系统;

FHDL是Migen的基础,通过FHDL来描述信号,并且在组合或者时序逻辑中操作这些信号。由于Migen是Python库,所以可以借助Python的灵活性来便捷地构建复杂的结构。FHDL还包含一个用于生成可综合Verilog的后端。

FHDL将代码分为组合语句(组合逻辑)、同步语句(时序逻辑)和复位值。

二、语法

2.1 表达式语句

2.1.1 Constant(常量)

Constant对象表示一个常量,即硬件描述语言的字面量。它类似于在verilog中直接写明的某个整数或者布尔值,但是也支持切片操作,并且可以具有与其所表示的值所暗示的不同的位宽或符号。

Ture和False分别代表1和0。

支持负整数,算术运算将返回自然结果。

为了简化语法,声明语句和运算符号将会自动将Python类型的整数和布尔值封装为Constant。Constant的一个别名是C。即下面的语句都是合法的:

a.eq(0) #将Constant(0)赋值给a

a.ea(a+1) #将Constant(a+1)赋值给a

a.eq(C(42)[0:1]) # 将Constant(42)的某一部分赋值给a

2.1.2 Signal(信号)

Signal对象代表了在电路中会发生变化的值,不区分wire和reg。每个Signal对象都有一个唯一的Python ID标识(使用id()函数获得),Verilog/VHDL后端负责在Python ID和Verilog/VHDL之间建立单向映射。

Signal对象的属性包括:

  • 有一个整数或者(整数,布尔值)对,来定义该Signal的位宽和最高bit位是否为符号位;或者,可以通过设置该Signal对象的min和max值来确定其位宽和符号;与Python的语法一致的是,下边界min包含,默认为0,而上边界max不包含,默认为2。
  • 一个name,用于指示Verilog/VHDL后端生成对应的HDL代码时为该Signal对象生成的名字;name的唯一作用是使得生成的Verilog/VHDL代码更易读;如果未指定name,则Migen会自动分析创建该Signal对象的代码,从中提取信息生成name;如果发生冲突,Migen会首先尝试在name前面添加模块层次结构来解决,如果还是冲突,则添加数字后缀;
  • 一个复位值;必须是整数,默认为0。当使用时序逻辑来修改该Signal的值时,复位值是该信号所代表的寄存器的初始化值。当Signal在组合逻辑中使用时,复位值是所有条件均不满足时的默认值;如果使用组合逻辑永久驱动该Signal,则复位值无效;

2.1.3 Operators(运算符)

大多数运算操作都直接使用Python的逻辑和算术运算符,以简化语法,例如:

a * b + c

2.1.4 Slice(切片索引)

切片索引操作与Python一致(而不是Verilog或者VHDL!!!),支持使用[x:]、[:y]等形式的隐式切片索引。分号“:”左边的界限是LSB,包含,右边的界限是MSB,不包含!

2.1.5 Concatenation(拼接)

使用Cat来完成拼接Singal的操作,为了与Slice保持一致,第一个参数作为拼接结果的较低bit位,这与Verilog中的“{}”工作方式相反。

2.1.6 复制

Replicate作用类似于Verilog中的”{count{expression}}”,下面两个语句是等价的:

Replicate(0,4)Cat(0,0,0,0)
Cat(0,0,0,0)

2.2 声明

2.2.1 Assign

将某个值赋值给另一个值,本质上是使用了_Assign对象,但是一般不直接使用该方法,而是调用Signal对象的eq()方法,来进行赋值(无论是组合逻辑还是时序逻辑),例如:

a[0].eq(b)

2.2.2 If

If语句的第一个参数必须为表达式(即Constant、Signal、Operator、Slice的组合等),然后后续的参数代表了条件满足时所需要执行的语句,后续的参数也可以嵌套If。此外,还定义了Else()、Elif()等方法,如下所示:

If(tx_count16 == 0,
    tx_bitcount.eq(tx_bitcount + 1),
    If(tx_bitcount == 8,
        self.tx.eq(1)
    ).Elif(tx_bitcount == 9,
        self.tx.eq(1),
        tx_busy.eq(0)
    ).Else(
        self.tx.eq(tx_reg[0]),
        tx_reg.eq(Cat(tx_reg[1:], 0))
    )
)

 

等效于Verilog代码为:

if(tx_count16==0)begin
    tx_bitcount <= tx_bitcount + 1;
    if(tx_bitcount==8)begin
        tx <= 1
    end
    else if(tx_bitcount==9)begin
        tx <= 1;
        tx_busy <= 0;

    end
    else begin
        tx <= tx_reg[0];
        tx_reg <= {tx_reg[MSB:1], 0};
    end
end

 

2.2.3 Case

Case的第一个参数为要判断的表达式,第二个参数为一个字典,字典包含了一些键值对,即与表达式匹配时所需要执行的语句;特殊值default表示没有其他匹配时,执行该语句。

2.2.4 Array

Array中可以为任意的Python对象类型,只要一个module中对该Array中所有类型的对象的最终返回类型是Signal即可。下面是一些示例:

my_2d_array = Array(Array(Signal() for a in range(4)) for b in range(4))
out.eq(my_2d_array[x][y])
my_2d_array[x][y].eq(inp)

 

由于没有在Verilog中直接匹配的语句,因此在生成最终的代码之前,Array对象会被自动降级为多路复用器或条件语句。

对Array对象的任何越界访问都将引用最后一个元素!!!!

2.3 Specials

2.3.1 三态门I/O

定义三态门I/O端口的信号三元组(O, OE, I)由TSTriple对象表示(注意大小写!),该对象仅仅是一个容器,用于容纳稍后连接到三态I/O缓冲区的Signal,本身不代表真实的电路信号。

真正可以用作模块的special的对象是Tristate,它的行为与三态I/O缓冲器完全相同,定义如下:

Instance("Tristate",
  io_target=target,
  i_o=o,
  i_oe=oe,
  o_i=i
)

 

其中Signals:target、o和i可以为任意位宽,oe的位宽为1。target Signal应该连接到一个端口,而不能在模块内部使用。

可以通过在TSTriple对象上调用get_tristate方法来创建Tristate对象。

2.3.2 Instances(实例)

Instance对象代表了某个Verilog/VHDL模块的参数化实例,以及该实例的端口,这种技术一般用于以下情况:

  • 重用已有的Verilog/VHDL代码;
  • 使用特殊的FPGA功能(例如DCM,ICAP);
  • 实现无法用FHDL表达的逻辑(例如锁存器);
  • 将Migen系统分解为多个子系统;

创建Instance对象的构造器需要使用实例的类型(即实例化模块的名称)作为第一个参数,剩余的参数还包括:

  • Instance.Input/Instance.Output/Instance.InOut:描述与该实例的信号连接,参数的值为该实例的端口名称,以及该端口所连接的FHDL的Signal;
  • Instance.Parameter:设置实例的parameter;
  • Instance.ClockPort/Instance.ResetPort:用于将时钟和复位信号连接到实例。

唯一必须指定的参数是实例的端口名称。此外还可以为该实例指定时钟域,并且可以使用invert选项将时钟或者复位信号反相。

2.3.3 Memories

通过使用与Instance机制类似的机制来支持片上内存功能,一个Memory对象具有以下参数:

  • 宽度
  • 深度
  • 一个可选的整数列表,用于初始化memory;

为了访问实际硬件的内存,通过对Memory对象调用get_port方法来获得对应的端口,端口至少包括:地址信号a,读数据信号dat_r;其他的信号根据对Memory端口的配置情况决定,即配置get_port方法的选项,包括:

  • write_capable(默认为False):端口是否可用于写入内存,这会创建一个额外的we信号;
  • async_read(默认为False):读是异步操作还是同步操作;
  • has_re(默认为False):添加一个读时钟使能信号re(异步端口无效);
  • we_granularity(默认为0):如果为非0,则可能会发生少于宽度的写入,对应的we信号则将变成字节使能信号;
  • mode(默认为WRITE_FIRST):如果为READ_FIRST,则对于同一个地址同时发生读和写操作时,读取的是旧值;如果为WRITE_FIRST,则读取的是新值;如果为NO_CHANGE,则既不读取旧值也不读取新值,而是保持不变;
  • clock_domain(默认为sys):设置从该端口读写的时钟域;

2.4 Module模块

Module对应于Verilog的module。Migen中的Module都是派生自Module类的Python对象,该类定义了一些在派生类中使用的特殊属性,通过这些属性来描述派生类的电路逻辑。

2.4.1 组合逻辑

通过将对Signal的赋值语句添加到Module的comb属性中,以生成对应的组合逻辑,例如下面的模块生成了一个或门:

class ORGate(Module):
  def __init__(self):
    self.a = Signal()
    self.b = Signal()
    self.x = Signal()

    ###

    self.comb += self.x.eq(self.a | self.b)

 

为了提高代码的可读性,建议将模块的接口放在__init__函数的开头,并使用三个#号将其与Implement隔开。

2.4.2 时序逻辑

通过将对Signal赋值的语句添加到Module的sync属性中,以生成对应的时序逻辑。

由于可能存在多个时钟域,因此如果需要向非默认的时钟域(即sys)添加时序逻辑(例如foo时钟域),需要指明时钟域:

self.sync.foo += statement

默认时钟域不需要写明,下面两个语句是等效的:

self.sync += statement

self.sync.sys += statement

2.4.3 子模块和specials

可以通过使用Module的submodules和specials属性向该模块添加子模块和特殊属性。

  1. 匿名添加:通过直接对submodules属性使用+=运算符来添加子模块或者子模块的列表;例如,self.submodules += some_other_module
  2. 命名添加:self.submodules.foo = module_foo,然后该子模块就可以作为Module对象的属性进行访问,即self.foo (而不是 self.submodules.foo );缺点是一次只能添加一个子模块;

2.4.4 时钟域

使用ClockDomain创建一个时钟域的实现,并赋值给Module的clock_domains属性,该实现包含:该时钟域的名称、一个时钟Signal和一个可选的复位Signal。

如果时钟域的名称可以从Python的变量名中提取,则可以省略;如果通过这种方式生成时钟域名称,则前缀_、cd_、_cd_将会被从名称中移除,例如以下四行代码都是创建一个名称为pix的时钟域:

self.clock_domains.pix = ClockDomain()
self.clock_domains._pix = ClockDomain()
self.clock_domains.cd_pix = ClockDomain()
self.clock_domains._cd_pix = ClockDomain()

2.4.5 时钟域管理

当一个模块具有多个命名子模块,并且这些子模块定义了一个或多个具有相同名称的时钟域,则这些时钟域的名称将会被加上对应的子模块名称作和下划线作为前缀。

当匿名子模块所定义的时钟域的名称相同时,将会出现错误。

2.4.6 Finalization mechanism(终结机制)

有时候,希望仅在用户完成对该模块的操作之后才生成对应的具体的电路逻辑(即与Rocketchip中的LazyModule相似),例如FSM模块支持动态定义状态,并且只有在添加了所有状态后才知道状态信号的宽度。一种解决方案是在FSM的构造函数的参数中添加相应的参数,但是对用户来说不够友好(因为每次需要新增一种延迟生成电路逻辑的机制,都需要添加对应的构造器参数)。

更好的解决方案是在FSM模块转换为Verilog/VHDL代码之前,自动创建状态信号,Migen使用所谓的终结机制来支持这一特点。

Module的do_finalize方法可以创建逻辑,所Module可以通过重载这一方法,来实现上述功能,过程为:

  1. 当前Module的Finalization过程启动;
  2. 如果当前Module已经被finalized(例如,手动),则该过程在该模块中停止;否则进行第3步;
  3. 当前模块的子模块被递归的启动Finalization;
  4. 为当前模块调用do_finalize方法;
  5. 当前模块的do_finalize方法所创建的任何新的子模块都会递归的完成Finalization;

在生成Verilog/VHDL代码时,会自动启动Finalization。同时也可以手动地通过调用任意模块的finalize方法来触发。

2.4.5节所述的时钟管理机制就是在Finalization期间发生的。

三、生成可综合的代码

任何FHDL module都可以转换为可综合的Verilog代码。通过使用migen.fhdl.verilog的convert函数来实现这一功能:

# define FHDL module MyDesign here

if __name__ == "__main__":
  from migen.fhdl.verilog import convert
  convert(MyDesign()).write("my_design.v")

 

migen.build组件提供了调用第三方FPGA工具的脚本,以及一些用于轻松部署设计的开发板数据库。

请登录后发表评论

    没有回复内容