摘要:本节介绍了 SystemC 仿真内核的一些操作,然后将其与原始通道的行为联系起来。
大多数建模语言,例如 VHDL,都使用仿真内核。 内核的目的是确保并行活动(并发)被正确建模。 在 VHDL 的情况下,做出了一个基本决定,即模拟的行为不应依赖于在模拟时间的每个步骤中执行过程的顺序。 本节首先描述 VHDL 中发生的事情,因为 SystemC 模仿了 VHDL 仿真内核的一些关键方面,但也允许定义其他计算模型。 一旦理解了 VHDL,就可以扩展讨论以结合更通用的 SystemC 仿真内核。
例如,假设在 SystemC 中有两个 SC_THREAD,都对触发器敏感。
- SC_THREAD(proc_1);
- sensitive << Trig.pos();
- SC_THREAD(proc_2);
- sensitive << Trig.pos();
当触发器由低变高时,哪个进程会先运行?更重要的是,这重要吗?在使用 VHDL 的类似情况下,您真的不在乎。这是因为在 VHDL 中,进程之间的通信是通过信号完成的,并且进程执行和信号更新分为两个独立的阶段。
VHDL 仿真内核依次执行每个进程,但任何由此产生的信号变化都不会立即发生。 SystemC 和 sc_signal 也是如此。准确地说,分配计划在将来发生,这意味着当所有当前活动的进程都已被评估并达到需要暂停并等待某些事件发生的点时。
可能没有经过模拟时间。如果是这种情况,并且信号有待处理的更新,则对这些信号做出反应的进程将再次运行,而不会经过时间。这被称为“增量周期”,并且具有在没有模拟时间经过的情况下明确确定通信进程执行顺序的效果。
SystemC 也可以做到这一点,但也可以以其他方式对并发、通信和时间进行建模。
当 VHDL 中的信号或 SystemC 中的 sc_signals 用于进程间通信时,仿真是确定性的; 它在任何模拟工具上的行为都相同。
然而,在 SystemC 中,该语言允许不确定性。 例如,假设在一个类中声明的变量从两个不同的 SC_THREAD 访问,如上所述。 这是一个例子:
- SC_MODULE(nondet)
- {
- sc_in Trig;
-
- int SharedVariable;
- void proc_1()
- {
- SharedVariable = 1;
- cout << SharedVariable << endl;
- }
-
- void proc_2()
- {
- SharedVariable = 2;
- cout << SharedVariable << endl;
- }
-
- SC_CTOR(nondet)
- {
- SC_THREAD(proc_1);
- sensitive << Trig.pos();
- SC_THREAD(proc_2);
- sensitive << Trig.pos();
- }
- };
看完背景,现在可以总结一下SystemC仿真内核的操作了。
SystemC 仿真内核支持增量周期的概念。增量周期由评估阶段和更新阶段组成。这通常用于对无法立即更改的原始通道进行建模,例如 sc_signal。通过分离评估和更新两个阶段,可以保证确定性行为(因为原始通道在更新阶段发生之前不会改变值 - 在评估阶段它不能立即改变)。
但是,SystemC 可以对软件进行建模,在这种情况下,能够使进程在没有增量周期的情况下运行(即不执行更新阶段)很有用。这需要立即通知事件(立即通知)。立即通知可能会导致不确定的行为。
事件的通知是通过调用 sc_event 类的 notify() 方法来实现的。
对于 notify 方法,需要考虑三种情况。
notify() 方法取消任何未决通知,并对现有通知的状态进行各种检查。
现在可以描述模拟内核的行为:
注意函数 update() 和 request_update()。内核提供这些函数专门用于对原始通道(如 sc_signal)进行建模。如果在评估阶段通过调用 request_update() 请求 update() 实际上在更新阶段运行。
那么如何编写原始通道呢?它实际上非常简单!首先,所有原始通道都基于类 sc_prim_channel - 您可以将其视为分层通道的 sc_module 的原始通道等效项。
这是 FIFO 通道的代码,它是“内置”sc_fifo 通道的一个大大简化的版本。它的简化之处在于它只提供了阻塞方法,并且它不是模板类(它只适用于 char 类型)。
这是接口fifo_if.h
- #include "systemc.h"
- class fifo_out_if : virtual public sc_interface
- {
- public:
- virtual void write(char) = 0; // blocking write
- virtual int num_free() const = 0; // free entries
- protected:
- fifo_out_if()
- {
- };
- private:
- fifo_out_if (const fifo_out_if&); // disable copy
- fifo_out_if& operator= (const fifo_out_if&); // disable
- };
-
- class fifo_in_if : virtual public sc_interface
- {
- public:
- virtual void read(char&) = 0; // blocking read
- virtual char read() = 0;
- virtual int num_available() const = 0; // available
- // entries
- protected:
- fifo_in_if()
- {
- };
- private:
- fifo_in_if(const fifo_in_if&); // disable copy
- fifo_in_if& operator= (const fifo_in_if&); // disable =
- };
这是频道第一部分的代码。
- #include "systemc.h"
- #include "fifo_if.h"
-
- class fifo
- : public sc_prim_channel, public fifo_out_if,
- public fifo_in_if
- {
- protected:
- int size; // size
- char* buf; // fifo buffer
- int free; // free space
- int ri; // read index
- int wi; // write index
- int num_readable;
- int num_read;
- int num_written;
-
- sc_event data_read_event;
- sc_event data_written_event;
-
- public:
- // constructor
- explicit fifo(int size_ = 16)
- : sc_prim_channel(sc_gen_unique_name("myfifo"))
- {
- size = size_;
- buf = new char[size];
- reset();
- }
-
- ~fifo() //destructor
- {
- delete [] buf;
- };
注意:
接下来的几个函数用于计算是否有可用空间以及有多少可用空间。 该算法使用循环缓冲区,由写入索引 (wi) 和读取索引 (ri) 访问。
- int num_available() const
- {
- return num_readable - num_read;
- }
-
- int num_free() const
- {
- return size - num_readable - num_written;
- }
这是阻塞写入功能。 请注意,如果 num_free() 返回零,则函数调用 wait(data_read_event)。 这是动态灵敏度的一个例子。 调用 write 的线程将被挂起,直到通知 data_read_event。
- void write(char c) // blocking write
- {
- if (num_free() == 0)
- wait(data_read_event);
- num_written++;
- buf[wi] = c;
- wi = (wi + 1) % size;
- free--;
- request_update();
- }
这是清除 FIFO 的复位函数。
- void reset()
- {
- free = size;
- ri = 0;
- wi = 0;
- }
这是读取功能。 行为类似于 write 函数,只是这次如果没有可用空间(FIFO 已满)则进程阻塞。
- void read(char& c) // blocking read
- {
- if (num_available() == 0)
- wait(data_written_event);
- num_read++;
- c = buf[ri];
- ri = (ri + 1) % size;
- free++;
- request_update();
- }
为方便起见,这里有一个 read 的“快捷”版本,所以我们可以使用
char c = portname->read();
- char read() // shortcut read function
- {
- char c;
- read(c);
- return c;
- }
最后是 update() 方法本身。 这在模拟内核的更新阶段被调用。 它检查在评估阶段是否读取或写入了数据,然后酌情调用 notify(SC_ZERO_TIME) 以告知阻塞的 read() 或 write() 函数它们可以继续。
- void update()
- {
- if (num_read > 0)
- data_read_event.notify(SC_ZERO_TIME);
- if (num_written > 0)
- data_written_event.notify(SC_ZERO_TIME);
- num_readable = size - free;
- num_read = 0;
- num_written = 0;
- }
- };
设计的顶层看起来与分层通道非常相似。 这是(来自main.cpp)
- #include "systemc.h"
- #include "producer.h"
- #include "consumer.h"
- #include "fifo.h"
-
- int sc_main(int argc, char* argv[])
- {
- sc_clock ClkFast("ClkFast", 1, SC_NS);
- sc_clock ClkSlow("ClkSlow", 500, SC_NS);
-
- fifo fifo1;
-
- producer P1("P1");
- P1.out(fifo1);
- P1.Clock(ClkFast);
-
- consumer C1("C1");
- C1.in(fifo1);
- C1.Clock(ClkSlow);
-
- sc_start(5000, SC_NS);
-
- return 0;
- };
本章展示了编写原始通道的一瞥。 还有更多详细信息,您可能需要查看 sc_signal 或 sc_fifo 的源代码以了解更多信息。
需要特别注意的是动态灵敏度的使用——注意阻塞的读写功能实际上是如何覆盖对时钟信号的静态灵敏度的。