基本结构
-
基本设计单元:module
-
module内的结构:
-
端口定义
- I/O说明
- 信号类型声明
-
功能描述
-
基本数据类型
-
整数常量:整数字面量
-
符号常量:parameter
-
网络变量:wire、wor(线或)、wand(线与)、tri1和tri0(上拉和下拉电阻)、suply1和suply0(逻辑1即电源,逻辑0即接地)一般用于assign等相关的组合逻辑
-
寄存器变量:reg(无符号整数)、integer(32位带符号整数)、real(64位带符号实数)、time(无符号时间)一般用于initial、always等相关的时序逻辑,需要被明确赋值
-
特殊数据的表达:
-
常量:<符号>[位宽]'[进制]<数字>
- 较长的数字之间可用“_”隔开
- 常量未表明位宽时,默认为32bit
- 不定值与高阻值:只能在二进制中使用,x表示不定值,z或?(case中常用)表示高阻值
运算符
逻辑比较
- 算数运算符:求模的符号与被模数一致。
- 逻辑运算符:结果不确定就返回x(即1'bx),为真就返回1(1b'1),反之返回0(1b'0)
- 关系运算符
- 条件运算符
- 等式运算符:
- ==为等于
- !=为不等于
- ===为全等:可以按位比较不定值和高阻值是否相同,一般用在case中
- !==为不全等:可以按位比较不定值和高阻值是否不相同,一般用在case中
位运算
- 位运算符:异或是\^,同或是~\^
- 缩位运算符:符号同位运算,是单目运算,但其实是对操作数是做递推运算,即第一位与第二位做位运算,再将结果与后面位做位运算
- 移位运算符:
- 无符号移位:<<、>>
- 有符号移位:<<<、>>>
- 位拼接:{}
- 便于赋值:assign {A,B}=X(会把X拆成两部分,分别给A和B )
- 增添新位:assgin A={A,1'b1,1'b0}
- 简化重复扩展:用{4{A}}代替{A,A,A,A}
- 嵌套:{ A, {B,C}, {3{D}} }
- 在拼接表达式中,所有位数必须指明位数
赋值运算
- 连续赋值:过程块以外的语句使用(比如assign),使用=。
- 过程赋值:过程块以内的语句使用,使用=(阻塞赋值)或<=(非阻塞赋值)。
- 非阻塞赋值:块结束的一瞬间,同时进行所有变量的赋值。所以赋值无法传递
- 阻塞赋值:在语句结束的一瞬间,进行变量赋值
- 为什么不建议用阻塞赋值:因为电路中只有非阻塞赋值,而没有阻塞赋值。所以阻塞赋值的综合结果可能是未知的
- 建议:连续使用=赋值,过程使用<=赋值
过程语句(initial/always)
always语句(可综合,类似于编程的顺序执行)
- 只有reg变量能在always中赋值,它们是过程变量
- 如果always块中包含多个语句,则==需要使用begin-end或者fork-join包含==
- always一定要有时序控制,否则会产生循环跳变,即仿真死锁
- always中最好不使用=赋值,使用<=
- 敏感信号:分为==边沿触发和电平触发==两种。可以为单个信号,也可以多个信号(用or隔开)。敏感信号不要为x/z,会阻挡进程
- 边沿触发:用于描述时序逻辑,posedge、negedge
- 电平触发:用于描述组合逻辑
- 多个信号时的注意事项:
- 一定要在敏感列表中列出所有可能的敏感信号,不然可能会产生一个锁存器(外部信号a实际上变化了,但always不敏感,所以内部的信号a没有跟着变化,即a锁存了)
- 多个always的注意事项:
- 语句不是按代码先后执行的,多个always可能会并列触发。
initial语句(不可综合)
-
用途:
-
在仿真一开始,对各变量进行初始化
- 在测试文件中生成激励波形作为仿真信号
块语句
顺序块begin-end(可综合,类似于编程的大括号)
-
强制要求块中的语句顺序执行
-
每条语句的延迟时间,是上一条语句的结束时间+设定延迟时间
并行块fork-join(不可综合)
- 强制要求块中的语句同时进行(但不一定同时被触发)
- 每天语句的开始时间,是并行块的开始时间+设定延迟时间
- 因此可以用延迟来构造时序
判断语句
if-else
- 拒绝并列if:语句不是按代码先后执行的,多信号可能会同时触发几个if语句导致产生矛盾,因此不要写并列的if,要写嵌套的。
- 坚持写全所有的情况或补全else:否则当不满足某些情况的时候,变量可能无法复位,导致产生锁存器
case
- case
- casez(不考虑z位的比较)
- casex(不考虑z和x位的比较)
- 可用?代替z或x
- 坚持写全所有的情况或补全default:否则当不满足某些情况的时候,变量可能无法复位,导致产生锁存器
循环语句
循环变量一定要用integer,否则溢出的时候会变负数
强制跳出循环语句使用disable语句
for
repeat(不可综合)
- repeat(n)指连续执行语句n次
while(可综合/不可综合)
- 只有while有时序控制时才能综合
- 一般用在testbench中才用
forever(不可综合)
- 构造一个无限循环
- 使用disable跳出
- 在begin后面起别名,用别名来跳出
- 一般用在initial中,不能写在地方
方法
- 主动调用:
- task:均能调用
- function:只能调用function
- 被调用:
- task:只能在过程块中调用
- function:均能被调用
- 返回值:
- task:用输出变量返回
- function:用内置寄存器返回
task
- 写在module里,只能在模块内调用
- 参数写法和module一致
- 如果定义是使用inout双向类型,则它既作为输入也作为输出
function
- 写在module里,但可以在任何地方调用
- 返回值输出:具有一个同名的内部寄存器,通过==给同名的寄存器赋值==,来输出返回值
- 记忆:比如,可以将function[7:0] func看成是创建了一个function类型、位宽为8的寄存器func
原语、模块描述层级
门电路原语
-
是基本操作指令,不可中断
-
和module写法一致,输出参数固定为第一个
模块描述层级
-
门级(门级描述)
-
需要知道所有的逻辑功能
-
使用门电路原语,是最终想要综合得到的层级(也是最底层的)
-
算法级(行为级描述)
-
只需要知道逻辑表达式
-
使用逻辑表达式
-
系统级(行为级描述)
-
只需要知道输入-输出真值表
-
使用case
-
算法级(行为级描述)
-
只需要知道输入-输出的条件关系
- 使用条件运算符
verilog的内置函数
暂略。
verilator
以下以c++为例,而不是system_c
基本结构
自定义文件
-
verilog文件
-
cpp激励文件:可以控制verilog文件的所有变量,以及文件中所有module的参数。一般用来构造外界时钟
自动生成文件
- Vxxx.cpp和Vxxx.h中间文件:
- verilator根据顶层文件负责==将xxx.v转化为Vxxx.cpp和Vxxx.h==
- 在.cpp内部会自动生成叫做Vxxx的类
-
有了这两个文件,cpp激励文件就可以在.cpp层面上进行变量的获取和控制
-
Makefile:verilator自动生成的文件,负责将中间文件和cpp激励文件进行编译,生成可执行文件和波形文件
激励文件
头文件
:使用更安全的指针声明 :testbench需要的类封装 - "Vxxx.h":根据verilog自动生成的头文件
仿真环境和顶层模组文件
在cpp激励文件中使用仿真环境制造时钟,使用顶层模组文件找到
-
仿真环境:
-
实例化testbench类封装为一个对象,这样就可以通过使用对象,来调用内置的类成员和方法
-
c++ const std::unique_ptr<VerilatedContext> contextp{new VerilatedContext}; -
contextp->debug()
-
contextp->randReset():设置随即种子
-
contextp->traceEverOn():设置是否打开波形追踪功能
- contextp->open():设置输出波形的文件名
-
contextp->timeInc():信号延时,相当于verilog中的#延时,单位默认为10ps
-
contextp->time():返回模拟时间
-
contextp->gotFinish():当verilog中调用$finish时为true
-
contextp->dump():存入波形文件
- 一般这样用,contextp->dump(contextp->time())
-
contextp->close():关闭波形文件
-
顶层模组文件:
-
Vxxx.h有一个类声明存放全部的定义信息和模组继承关系,在Vxxx.cpp中实现。
-
实例化一个对象,通过操作对象就可以指定想要操作的模组
-
c++ const std::unique_ptr<Vtop> top{new Vtop{contextp.get(), "TOP"}}; -
top->输入变量
-
top->输出变量
-
top->eval():同时更新所有的值,所有的赋值必须通过exal()才能得到更新,否则不变
- top->eval_step()
- top->eval_and_step()
-
contextp->final():