• [SystemC]Primitive Channels and the Kernel


    SystemC Primitive Channels and the Kernel

           摘要:本节介绍了 SystemC 仿真内核的一些操作,然后将其与原始通道的行为联系起来。

    一、Simulation Kernels

           大多数建模语言,例如 VHDL,都使用仿真内核。 内核的目的是确保并行活动(并发)被正确建模。 在 VHDL 的情况下,做出了一个基本决定,即模拟的行为不应依赖于在模拟时间的每个步骤中执行过程的顺序。 本节首先描述 VHDL 中发生的事情,因为 SystemC 模仿了 VHDL 仿真内核的一些关键方面,但也允许定义其他计算模型。 一旦理解了 VHDL,就可以扩展讨论以结合更通用的 SystemC 仿真内核。

           例如,假设在 SystemC 中有两个 SC_THREAD,都对触发器敏感。

    1. SC_THREAD(proc_1);
    2. sensitive << Trig.pos();
    3. SC_THREAD(proc_2);
    4. sensitive << Trig.pos();

           当触发器由低变高时,哪个进程会先运行?更重要的是,这重要吗?在使用 VHDL 的类似情况下,您真的不在乎。这是因为在 VHDL 中,进程之间的通信是通过信号完成的,并且进程执行和信号更新分为两个独立的阶段。

           VHDL 仿真内核依次执行每个进程,但任何由此产生的信号变化都不会立即发生。 SystemC 和 sc_signal 也是如此。准确地说,分配计划在将来发生,这意味着当所有当前活动的进程都已被评估并达到需要暂停并等待某些事件发生的点时。

           可能没有经过模拟时间。如果是这种情况,并且信号有待处理的更新,则对这些信号做出反应的进程将再次运行,而不会经过时间。这被称为“增量周期”,并且具有在没有模拟时间经过的情况下明确确定通信进程执行顺序的效果。

           SystemC 也可以做到这一点,但也可以以其他方式对并发、通信和时间进行建模。

    二、Non-determinism

           当 VHDL 中的信号或 SystemC 中的 sc_signals 用于进程间通信时,仿真是确定性的; 它在任何模拟工具上的行为都相同。

           然而,在 SystemC 中,该语言允许不确定性。 例如,假设在一个类中声明的变量从两个不同的 SC_THREAD 访问,如上所述。 这是一个例子:

    1. SC_MODULE(nondet)
    2. {
    3. sc_in Trig;
    4. int SharedVariable;
    5. void proc_1()
    6. {
    7. SharedVariable = 1;
    8. cout << SharedVariable << endl;
    9. }
    10. void proc_2()
    11. {
    12. SharedVariable = 2;
    13. cout << SharedVariable << endl;
    14. }
    15. SC_CTOR(nondet)
    16. {
    17. SC_THREAD(proc_1);
    18. sensitive << Trig.pos();
    19. SC_THREAD(proc_2);
    20. sensitive << Trig.pos();
    21. }
    22. };
    • 在此示例中,哪个 SC_THREAD 将首先运行是未定义的 - 无法确定哪个将首先运行。
    • 对于硬件建模,这是不可接受的。但是对于软件建模,这可能代表一个使用共享变量的系统,并且非确定性并不重要——所需要的只是保证两个进程不能同时访问该变量。
    • 软件工程师使用互斥(mutex)或信号量等概念来应对这种情况。

    三、Events and Notifications

           看完背景,现在可以总结一下SystemC仿真内核的操作了。

           SystemC 仿真内核支持增量周期的概念。增量周期由评估阶段和更新阶段组成。这通常用于对无法立即更改的原始通道进行建模,例如 sc_signal。通过分离评估和更新两个阶段,可以保证确定性行为(因为原始通道在更新阶段发生之前不会改变值 - 在评估阶段它不能立即改变)。

           但是,SystemC 可以对软件进行建模,在这种情况下,能够使进程在没有增量周期的情况下运行(即不执行更新阶段)很有用。这需要立即通知事件(立即通知)。立即通知可能会导致不确定的行为。

           事件的通知是通过调用 sc_event 类的 notify() 方法来实现的。

           对于 notify 方法,需要考虑三种情况。

    • 不带参数的 notify():立即通知。对事件敏感的进程将在当前评估阶段运行
    • 带有零时间参数的 notify():增量通知。对事件敏感的进程将在下一个增量周期的评估阶段运行
    • 带有非零时间参数的 notify():定时通知。对事件敏感的进程将在未来某个模拟时间的评估阶段运行

           notify() 方法取消任何未决通知,并对现有通知的状态进行各种检查。

           现在可以描述模拟内核的行为:

    1. 初始化:以未指定的顺序执行所有进程(SC_CTHREADs 除外)。
    2. 评估:选择一个准备好运行并恢复其执行的进程。这可能会导致立即发生事件通知,这可能会导致其他进程准备好在同一阶段运行。
    3. 重复步骤 2,直到没有准备好运行的进程。
    4. 更新:执行步骤 2 中对 request_update() 的调用导致的所有挂起的 update() 调用。
    5. 如果在第 2 步或第 4 步期间发出任何增量事件通知,请确定哪些进程由于所有这些事件而准备好运行并返回到第 2 步。
    6. 如果没有定时事件,则模拟结束。
    7. 将模拟时间提前到最早挂起的定时事件通知的时间。
    8. 由于当前时间的所有定时事件,确定哪些进程已准备好运行,然后返回步骤 2。
       

           注意函数 update() 和 request_update()。内核提供这些函数专门用于对原始通道(如 sc_signal)进行建模。如果在评估阶段通过调用 request_update() 请求 update() 实际上在更新阶段运行。

    四、A primitive channel

           那么如何编写原始通道呢?它实际上非常简单!首先,所有原始通道都基于类 sc_prim_channel - 您可以将其视为分层通道的 sc_module 的原始通道等效项。

           这是 FIFO 通道的代码,它是“内置”sc_fifo 通道的一个大大简化的版本。它的简化之处在于它只提供了阻塞方法,并且它不是模板类(它只适用于 char 类型)。

           这是接口fifo_if.h

    1. #include "systemc.h"
    2. class fifo_out_if : virtual public sc_interface
    3. {
    4. public:
    5. virtual void write(char) = 0; // blocking write
    6. virtual int num_free() const = 0; // free entries
    7. protected:
    8. fifo_out_if()
    9. {
    10. };
    11. private:
    12. fifo_out_if (const fifo_out_if&); // disable copy
    13. fifo_out_if& operator= (const fifo_out_if&); // disable
    14. };
    15. class fifo_in_if : virtual public sc_interface
    16. {
    17. public:
    18. virtual void read(char&) = 0; // blocking read
    19. virtual char read() = 0;
    20. virtual int num_available() const = 0; // available
    21. // entries
    22. protected:
    23. fifo_in_if()
    24. {
    25. };
    26. private:
    27. fifo_in_if(const fifo_in_if&); // disable copy
    28. fifo_in_if& operator= (const fifo_in_if&); // disable =
    29. };
    • 基本上,有一种读写方法,这两种方法都是阻塞的,即如果 FIFO 为空(读)或满(写),它们就会挂起。

           这是频道第一部分的代码。

    1. #include "systemc.h"
    2. #include "fifo_if.h"
    3. class fifo
    4. : public sc_prim_channel, public fifo_out_if,
    5. public fifo_in_if
    6. {
    7. protected:
    8. int size; // size
    9. char* buf; // fifo buffer
    10. int free; // free space
    11. int ri; // read index
    12. int wi; // write index
    13. int num_readable;
    14. int num_read;
    15. int num_written;
    16. sc_event data_read_event;
    17. sc_event data_written_event;
    18. public:
    19. // constructor
    20. explicit fifo(int size_ = 16)
    21. : sc_prim_channel(sc_gen_unique_name("myfifo"))
    22. {
    23. size = size_;
    24. buf = new char[size];
    25. reset();
    26. }
    27. ~fifo() //destructor
    28. {
    29. delete [] buf;
    30. };

    注意:

    • 通道来自 sc_prim_channel,而不是 sc_module
    • 构造函数自动生成一个内部名称,因此用户不必指定一个
    • 构造函数使用动态内存分配,所以还必须有析构函数来删除声明的内存
    • 创建了两个 sc_event 对象。 当空间变得可用(如果写入被阻止)或数据变得可用(当读取被阻止)时,这些用于向被阻止的读取和写入进程发出信号
       

           接下来的几个函数用于计算是否有可用空间以及有多少可用空间。 该算法使用循环缓冲区,由写入索引 (wi) 和读取索引 (ri) 访问。

    1. int num_available() const
    2. {
    3. return num_readable - num_read;
    4. }
    5. int num_free() const
    6. {
    7. return size - num_readable - num_written;
    8. }

           这是阻塞写入功能。 请注意,如果 num_free() 返回零,则函数调用 wait(data_read_event)。 这是动态灵敏度的一个例子。 调用 write 的线程将被挂起,直到通知 data_read_event。

    1. void write(char c) // blocking write
    2. {
    3. if (num_free() == 0)
    4. wait(data_read_event);
    5. num_written++;
    6. buf[wi] = c;
    7. wi = (wi + 1) % size;
    8. free--;
    9. request_update();
    10. }
    • 一旦进程在 data_read_event 之后恢复,它将字符存储在循环缓冲区中,然后调用 request_update()。
    • request_update() 确保模拟内核在内核更新阶段调用 update()。

           这是清除 FIFO 的复位函数。

    1. void reset()
    2. {
    3. free = size;
    4. ri = 0;
    5. wi = 0;
    6. }

           这是读取功能。 行为类似于 write 函数,只是这次如果没有可用空间(FIFO 已满)则进程阻塞。

    1. void read(char& c) // blocking read
    2. {
    3. if (num_available() == 0)
    4. wait(data_written_event);
    5. num_read++;
    6. c = buf[ri];
    7. ri = (ri + 1) % size;
    8. free++;
    9. request_update();
    10. }

           为方便起见,这里有一个 read 的“快捷”版本,所以我们可以使用

    char c = portname->read();
    • 语法:
    1. char read() // shortcut read function
    2. {
    3. char c;
    4. read(c);
    5. return c;
    6. }

           最后是 update() 方法本身。 这在模拟内核的更新阶段被调用。 它检查在评估阶段是否读取或写入了数据,然后酌情调用 notify(SC_ZERO_TIME) 以告知阻塞的 read() 或 write() 函数它们可以继续。

    1. void update()
    2. {
    3. if (num_read > 0)
    4. data_read_event.notify(SC_ZERO_TIME);
    5. if (num_written > 0)
    6. data_written_event.notify(SC_ZERO_TIME);
    7. num_readable = size - free;
    8. num_read = 0;
    9. num_written = 0;
    10. }
    11. };

           设计的顶层看起来与分层通道非常相似。 这是(来自main.cpp)

    1. #include "systemc.h"
    2. #include "producer.h"
    3. #include "consumer.h"
    4. #include "fifo.h"
    5. int sc_main(int argc, char* argv[])
    6. {
    7. sc_clock ClkFast("ClkFast", 1, SC_NS);
    8. sc_clock ClkSlow("ClkSlow", 500, SC_NS);
    9. fifo fifo1;
    10. producer P1("P1");
    11. P1.out(fifo1);
    12. P1.Clock(ClkFast);
    13. consumer C1("C1");
    14. C1.in(fifo1);
    15. C1.Clock(ClkSlow);
    16. sc_start(5000, SC_NS);
    17. return 0;
    18. };
    •  请注意,由于使用了 sc_gen_unique_name(),因此无需为 FIFO 原始通道命名。

    五、结论

           本章展示了编写原始通道的一瞥。 还有更多详细信息,您可能需要查看 sc_signal 或 sc_fifo 的源代码以了解更多信息。

           需要特别注意的是动态灵敏度的使用——注意阻塞的读写功能实际上是如何覆盖对时钟信号的静态灵敏度的。

  • 相关阅读:
    【杂谈】快来看看如何使用LGMT这样的蜜汁缩写来进行CodeReview吧!
    C/C++-内存
    无硫防静电手指套:高科技产业的纯净与安全新选择
    东哥套现,大佬隐退?
    android端MifareClassicTool
    Kubernetes客户端认证(二)—— 基于ServiceAccount的JWTToken认证
    fiddler如何抓模拟器中APP的包
    配置Hive使用Spark执行引擎
    花菁染料CY5.5标记角叉藻胶;CY5.5-Furcellaran;Furcellaran-CY5.5定制合成
    Unity自用工具:基于种子与地块概率的开放世界2D地图生成
  • 原文地址:https://blog.csdn.net/gsjthxy/article/details/126682872