本章重点学习同步逻辑中的触发器,锁存器的一些建模规范:
- 触发器建模
- 带异步置位/复位
- 带同步置位/复位
- 锁存器建模
5.1 触发器建模
触发器建模的关键是敏感列表的规范。SC_MODULE的规范写法中出现过sensitive 参数列表是事件敏感, 对触发器建模来讲,SC_MODULE的结构不变,只需要将事件敏感修改为沿敏感,写法如下, 这样的沿可用于指定bool或者sc_logic类型的信号或者端口。就sc_logic类型而言,非0到0的跳变被认为是负跳变沿,非1到1的跳变被认为是正跳变沿。
//规定用正跳变沿
sensitive_pos
//规定用负跳变沿
sensitive_neg
下面是一个D触发器模型, 敏感列表包含了指定于端口clk的沿敏感sensitive_pos, 表明只有在端口clk的正跳变沿时刻,数据输入d才得以传送到输出q。
//file basic_ff.h
#include "systemc.h"
SC_MODULE(basic_ff){
sc_in <bool> d, clk;
sc_out <bool> q;
void prc_basic_ff();
SC_CTOR(basic_ff){
SC_METHOD(prc_basic_ff);
sensitive_pos << clk;
}
};
//file basic_ff.cpp
#include "basic_ff.h"
void basic_ff::prc_basic_ff(){
q = d;
}
下面是一个包含多位端口的触发器示例,为prc_gang_ffs指定了负跳变沿敏感,所以这是一个由负跳变沿触发的触发器。
#include "systemc.h"
const int WIDTH =4;
SC_MODULE(gang_ffs){
sc_in <sc_uint<WIDTH> > current_state;
sc_in <bool> clock;
sc_out <sc_uint<WIDTH> > next_state;
void prc_gang_ffs();
SC_CTOR(gang_ffs){
SC_METHOD(prc_gang_ffs);
sensitive_neg << clock;
}
};
void gang_ffs::prc_gang_ffs(){
next_state = current_state;
}
5.2 多进程
一个模块可以具有多个SC_METHOD进程。每个进程可以模拟组合逻辑(使用事件敏感),也可以模拟同步逻辑(使用沿敏感)。进程之间的通信联系必须使用信号。
一个进程不能既有沿敏感,又有事件敏感,并且在沿敏感的规范中只可以使用一个bool型的信号或者端口。
下面是一个流水线的序列检测器。这个例子中,触发器用于生成信号。当输入数据流检测到‘101’序列时,检测器的输出为1。综合后的逻辑如下图。
#include "systemc.h"
SC_MODULE(seq_det){
sc_in <bool> clk, data;
sc_out <bool> seq_found;
void prc_seq_det();//同步逻辑进程
void prc_output();//组合逻辑进程
sc_signal <bool> first, second, third;
SC_CTOR(seq_det){
SC_METHOD(prc_seq_det);
sensitive_pos <<clk;
SC_METHOD(prc_output);
sensitive << first << second << third;
}
};
void seq_det::prc_seq_det(){
first = data;
second = first;
third = second;
}
void seq_det::prc_output(){
seq_found = first & !second & third;
}
需要注意到是,信号和端口赋值的∆延迟。对信号和端口的赋值不是立即发生的,而是总发生在一个∆延迟之后。进程之间是可以同步执行的,即当时钟正跳变沿到来时候,进程prc_seq_det被调用,first被赋值,但是second = first, third = second没有执行时候,first的变化导致了进程prc_output的执行,所以此刻second是上个时刻的值,third是上上个时刻的值,所以如果序列中出现了101,就能被检测出来了。
5.3 带异步置位和清零端的触发器
带异步置位和清零端的触发器编写时,清零输入的正或负跳变沿被指定为沿敏感列表的一部分。即若异步清零为低电平有效,则指定使用负跳变沿;若高电平有效,则使用正跳变沿。下面是一个带异步清零端的增/减计数器。
#include "systemc.h"
const int COUNT_SIZE = 4;
SC_MODULE(count4){
sc_in <bool> mclk, clear, updown;
sc_out <sc_uint <COUNT_SIZE> > data_out;
void sync_block();
SC_CTOR(count4){
SC_METHOD(sync_block);
sensitive_pos << mclk;
sensitive_neg << clear;
}
};
void count4::sync_block(){
if(!clear) //符合异步条件
data_out = 0;
else //不符合,使用时钟的正跳变沿,即mclk
if(updown)
data_out = data_out.read() + 1;
else
data_out = data_out.read() - 1;
}
下面是一个包含异步置位和清零位的触发器建模。置位和清零的输入沿必须指定为敏感列表的一部分。
#include "systemc.h"
const int STATE_BITS = 4;
SC_MODULE(async_states){
sc_in <bool> clk, reset, set;
sc_in <sc_uint <STATE_BITS> > current_state;
sc_out<sc_uint <STATE_BITS> > next_state;
void prc_async_state();
SC_CTOR(async_states){
SC_METHOD(prc_async_state);
sensitive_neg << clk <<reset;
sensitive_pos <<set;
}
};
void async_states::prc_async_state(){
if (!reset) //第一个异步条件
next_state = 0;
else if(set) //第二个异步条件
next_state = 5;
else //(隐含的)负跳变沿
next_state = current_state;
}
通常情况下,一个进程可以有多个被指定为其敏感列表的跳变沿。并且使用一条这样形式的if语句来描述该进程的行为,在该形式的语句中首先检查的不是时钟条件,而在最后那个else分支中才隐含了时钟条件。若指定使用负跳变沿敏感列表,则还需要使用非时钟条件的逻辑“非”;否则就要使用非时钟条件的正值。如下是这种条件语句的样板程序。
SC_METHOD(my_process);
sensitive_pos << a << b << clk;
sensitive_net << d << e << f;
void my_module::my_process(){
if(a)
<异步行为>
else if (b)
<异步行为>
else if (!d) //因为指定了负跳变沿,所以使用逻辑“非”
<异步行为>
else if (!f)
<异步行为>
else //时钟的正跳变沿
<按时钟节拍的行为>
}
5.4 带同步置位和清零端的触发器
在未带同步置位和清零端得触发器建模时,在敏感列表中只需指定一个时钟跳变沿即可。其置位和清零条件被清晰明确地编写在SC_METHOD进程的代码里。
如下是一个带低电平有效同步置位端的计数器。(prc_counter代码,书上写了未出现过的变量名:preset,不知道是写错了还是自己哪里没理解清)
#include "systemc.h"
const int COUNT_BITS = 4;
SC_MODULE(sync_count4){
sc_in <bool> mclk, clear, updown;
sc_in <sc_uint<COUNT_BITS> > data_in;
sc_out<sc_uint<COUNT_BITS> > data_out;
void prc_counter();
SC_CTOR(sync_count4){
SC_METHOD(prc_counter);
sensitive_pos << mclk;
}
};
void sync_count4::prc_counter(){
if(!preset)
data_out = data_in;
else
if(updown)
data_out = data_out.read()+1;
else
data_out = data_out.read()-1;
}
同步逻辑如下图:
5.5 多时钟和多相位时钟
在一个模块中,可以编写任意多个SC_METHOD进程,每个这样的进程都可以是同步或组合的进程。当出现多个同步进程时,可以在不同进程中使用不同的时钟来为设计的逻辑建模。
下面示例,进程prc_vt15ck在时钟vt15ck的负跳变沿触发,而进程prc_ds1ck则在时钟dslck的正跳变沿触发。
#include "systemc.h"
SC_MODULE(mult_clks){
sc_in <bool> vt15ck, addclk, adn, resetn, subclr, subn, ds1ck;
sc_out <bool> ds1_add, ds1_sub;
void prc_vt15ck();
void prc_ds1ck();
sc_signal <bool> add_state, sub_state;
SC_CTOR(mult_clks){
SC_METHOD(prc_vt15ck);
sensitive_neg << vt15ck;
SC_METHOD(prc_ds1ck);
sensitive_pos << ds1ck;
}
};
void mult_clks::prc_vt15ck(){
add_state = !(addclk | (adn | resetn));
sub_state = subclr ^ (subn & resetn);
}
void mult_clks::prc_ds1ck(){
ds1_add = add_state;
ds1_sub = sub_state;
}
如下是综合后生成的逻辑。信号add_state和信号sub_state在时钟vt15ck的负跳变沿时刻被赋值,并且它们的值在另外一个时钟ds1ck的正跳变沿时刻被赋予ds1_add和ds1_sub。在RTL建模时,通常规定不允许使用不同时钟沿对同一个信号或端口赋值。
在设计模块中可以使用同一个时钟的不同相位。下面举例说明。进程prc_rising由时钟zclk的正跳变沿触发,进程prc_falling则由时钟zclk的负跳变沿触发。一个进程的输出是另一个进程的输入,用信号d在进程之间通信实现。
#include "systemc.h"
SC_MODULE(multiphase)
{
sc_in <bool> zclk, a, b, c;
sc_out <bool> e;
void prc_rising();
void prc_falling();
sc_signal <bool> d;
SC_CTOR(multiphase){
SC_METHOD(prc_rising);
sensitive_pos << zclk;
SC_METHOD(prc_falling);
sensitive_neg << zclk;
}
};
void multiphase::prc_rising(){
e = d & c;
}
void multiphase::prc_falling(){
d = a & b;
}
5.6 锁存器建模
如果没有对进程的所有路径赋值,则在该进程综合后的逻辑中有可能为信号或端口生成锁存器。条件语句如if , switch,是可能在进程内生成多条执行路径的两条语句。
if语句生成锁存器的例子1:
当clk为0时候,输出端口z没有被赋值。因此根据该模型的定义,综合后将为端口z生成一个锁存器。
#include "systemc.h"
SC_MODULE(latched_alu){
sc_in <bool> clk, a,b;
sc_out <bool> z;
void prc_alu();
SC_CTOR(latched_alu){
SC_METHOD(prc_alu);
sensitive_pos << clk << a << b;
}
};
void latched_alu::prc_alu(){
if(clk)
z = !( a | b);
}
switch语句生成锁存器的一个例子2:
#include "systemc.h"
enum states{s0, s1, s2, s3};
const int Z_SIZE = 2;
SC_MODULE(state_update){
sc_in <states> current_state;
sc_out <sc_uint<Z_SIZE> > z;
void prc_state_update();
SC_CTOR(state_update){
SC_METHOD(prc_state_update);
sensitive << current_state;
}
};
void state_update::prc_state_update(){
switch(current_state)
{
case s0:
case s3: z=0; break;
case s1: z = 3; break;
}
}
避免生成锁存器
在大多数情况下,生成锁存器是不需要的。所以避免生成锁存器的关键是:若信号或端口在条件语句中被赋值,则必须确认条件语句的所有可能分支中,该信号或端口都被赋予一个值。
也就是,代码覆盖所有的条件分支,或者给端口或信号赋初值。
比如上面示例2,给prc_state_update函数修改如下:
//修改方案1
void state_update::prc_state_update(){
switch(current_state)
{
case s0:
case s3: z = 0; break;
case s1: z = 3; break;
default: z = 1; break;
}
}
//修改方案2
void state_update::prc_state_update(){
z = 1;
switch(current_state)
{
case s0:
case s3: z = 0; break;
case s1: z = 3; break;
}
}
另一个完整示例:
下面示例的prc_compute1, prc_compute是生成锁存器和不生成锁存器的写法,以及其综合后的逻辑图对比:
#include "systemc.h"
const int BITS = 2;
enum grade_type { fail, pass, excellent};
SC_MODULE(compute){
sc_in <sc_uint<BITS> >marks;
sc_out <grade_type> grade;
void prc_compute();
SC_CTOR(compute){
SC_METHOD(prc_compute);
sensitive << marks;
}
};
void compute::prc_compute(){
if(marks.read() < 5)
grade = fail;
else if (marks.read()<7)
grade = pass;
else
grade = excellent;
}
void compute::prc_compute1(){
if(marks.read() < 5)
grade = fail;
else if (marks.read()<7)
grade = pass;
}
5.7 小结
- 同步逻辑建模使用跳变沿敏感的SC_METHOD进程
- 一个模块可以包含任意多个进程,每个进程可以是表示组合逻辑的进程,或者是表示同步逻辑的进程
- 若在一个对时钟跳变沿敏感的进程中对信号或者端口进行赋值,则综合后生成的逻辑为触发器
- 同步逻辑中的异步置位和复位可以使用特定形式的if语句建模
- 必须在if语句或开关语句的所有分支中对信号或端口赋值,否则综合后将生成锁存器
- 若在if语句或开关语句前对信号或端口进行初始化,或者确保在条件语句的所有分支中都对其进行赋值,则综合后可以避免生成锁存器。