verilator/include/verilated_timing.h

355 lines
13 KiB
C
Raw Normal View History

Timing support (#3363) Adds timing support to Verilator. It makes it possible to use delays, event controls within processes (not just at the start), wait statements, and forks. Building a design with those constructs requires a compiler that supports C++20 coroutines (GCC 10, Clang 5). The basic idea is to have processes and tasks with delays/event controls implemented as C++20 coroutines. This allows us to suspend and resume them at any time. There are five main runtime classes responsible for managing suspended coroutines: * `VlCoroutineHandle`, a wrapper over C++20's `std::coroutine_handle` with move semantics and automatic cleanup. * `VlDelayScheduler`, for coroutines suspended by delays. It resumes them at a proper simulation time. * `VlTriggerScheduler`, for coroutines suspended by event controls. It resumes them if its corresponding trigger was set. * `VlForkSync`, used for syncing `fork..join` and `fork..join_any` blocks. * `VlCoroutine`, the return type of all verilated coroutines. It allows for suspending a stack of coroutines (normally, C++ coroutines are stackless). There is a new visitor in `V3Timing.cpp` which: * scales delays according to the timescale, * simplifies intra-assignment timing controls and net delays into regular timing controls and assignments, * simplifies wait statements into loops with event controls, * marks processes and tasks with timing controls in them as suspendable, * creates delay, trigger scheduler, and fork sync variables, * transforms timing controls and fork joins into C++ awaits There are new functions in `V3SchedTiming.cpp` (used by `V3Sched.cpp`) that integrate static scheduling with timing. This involves providing external domains for variables, so that the necessary combinational logic gets triggered after coroutine resumption, as well as statements that need to be injected into the design eval function to perform this resumption at the correct time. There is also a function that transforms forked processes into separate functions. See the comments in `verilated_timing.h`, `verilated_timing.cpp`, `V3Timing.cpp`, and `V3SchedTiming.cpp`, as well as the internals documentation for more details. Signed-off-by: Krzysztof Bieganski <kbieganski@antmicro.com>
2022-08-22 12:26:32 +00:00
// -*- mode: C++; c-file-style: "cc-mode" -*-
//*************************************************************************
//
// Code available from: https://verilator.org
//
// Copyright 2022 by Wilson Snyder. This program is free software; you can
// redistribute it and/or modify it under the terms of either the GNU Lesser
// General Public License Version 3 or the Perl Artistic License Version 2.0.
// SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0
//
//*************************************************************************
///
/// \file
/// \brief Verilated timing header
///
/// This file is included automatically by Verilator in some of the C++ files
/// it generates if timing features are used.
///
/// This file is not part of the Verilated public-facing API.
/// It is only for internal use.
///
/// See the internals documentation docs/internals.rst for details.
///
//*************************************************************************
#ifndef VERILATOR_VERILATED_TIMING_H_
#define VERILATOR_VERILATED_TIMING_H_
#include "verilated.h"
// clang-format off
// Some preprocessor magic to support both Clang and GCC coroutines with both libc++ and libstdc++
#ifdef __clang__
# if __clang_major__ < 14
# ifdef __GLIBCXX__ // Using stdlibc++
# define __cpp_impl_coroutine 1 // Clang doesn't define this, but it's needed for libstdc++
# include <coroutine>
namespace std { // Bring coroutine library into std::experimental, as Clang < 14 expects it to be there
namespace experimental {
using namespace std;
}
}
# else // Using libc++
# include <experimental/coroutine> // Clang older than 14, coroutines are under experimental
namespace std {
using namespace experimental; // Bring std::experimental into the std namespace
}
# endif
# else // Clang >= 14
# if __GLIBCXX__ // Using stdlibc++
# define __cpp_impl_coroutine 1 // Clang doesn't define this, but it's needed for libstdc++
# endif
# include <coroutine>
# endif
#else // Not Clang
# include <coroutine>
#endif
// clang-format on
//=============================================================================
// VlCoroutineHandle is a non-copyable (but movable) coroutine handle. On resume, the handle is
// cleared, as we assume that either the coroutine has finished and deleted itself, or, if it got
// suspended, another VlCoroutineHandle was created to manage it.
class VlCoroutineHandle final {
VL_UNCOPYABLE(VlCoroutineHandle);
// MEMBERS
std::coroutine_handle<> m_coro; // The wrapped coroutine handle
#ifdef VL_DEBUG
const char* m_filename;
int m_linenum;
#endif
public:
// CONSTRUCTORS
// Construct
VlCoroutineHandle(std::coroutine_handle<> coro = nullptr, const char* filename = nullptr,
int linenum = 0)
2022-09-28 02:22:05 +00:00
: m_coro{coro}
Timing support (#3363) Adds timing support to Verilator. It makes it possible to use delays, event controls within processes (not just at the start), wait statements, and forks. Building a design with those constructs requires a compiler that supports C++20 coroutines (GCC 10, Clang 5). The basic idea is to have processes and tasks with delays/event controls implemented as C++20 coroutines. This allows us to suspend and resume them at any time. There are five main runtime classes responsible for managing suspended coroutines: * `VlCoroutineHandle`, a wrapper over C++20's `std::coroutine_handle` with move semantics and automatic cleanup. * `VlDelayScheduler`, for coroutines suspended by delays. It resumes them at a proper simulation time. * `VlTriggerScheduler`, for coroutines suspended by event controls. It resumes them if its corresponding trigger was set. * `VlForkSync`, used for syncing `fork..join` and `fork..join_any` blocks. * `VlCoroutine`, the return type of all verilated coroutines. It allows for suspending a stack of coroutines (normally, C++ coroutines are stackless). There is a new visitor in `V3Timing.cpp` which: * scales delays according to the timescale, * simplifies intra-assignment timing controls and net delays into regular timing controls and assignments, * simplifies wait statements into loops with event controls, * marks processes and tasks with timing controls in them as suspendable, * creates delay, trigger scheduler, and fork sync variables, * transforms timing controls and fork joins into C++ awaits There are new functions in `V3SchedTiming.cpp` (used by `V3Sched.cpp`) that integrate static scheduling with timing. This involves providing external domains for variables, so that the necessary combinational logic gets triggered after coroutine resumption, as well as statements that need to be injected into the design eval function to perform this resumption at the correct time. There is also a function that transforms forked processes into separate functions. See the comments in `verilated_timing.h`, `verilated_timing.cpp`, `V3Timing.cpp`, and `V3SchedTiming.cpp`, as well as the internals documentation for more details. Signed-off-by: Krzysztof Bieganski <kbieganski@antmicro.com>
2022-08-22 12:26:32 +00:00
#ifdef VL_DEBUG
2022-09-28 02:22:05 +00:00
, m_filename{filename}
, m_linenum{linenum}
Timing support (#3363) Adds timing support to Verilator. It makes it possible to use delays, event controls within processes (not just at the start), wait statements, and forks. Building a design with those constructs requires a compiler that supports C++20 coroutines (GCC 10, Clang 5). The basic idea is to have processes and tasks with delays/event controls implemented as C++20 coroutines. This allows us to suspend and resume them at any time. There are five main runtime classes responsible for managing suspended coroutines: * `VlCoroutineHandle`, a wrapper over C++20's `std::coroutine_handle` with move semantics and automatic cleanup. * `VlDelayScheduler`, for coroutines suspended by delays. It resumes them at a proper simulation time. * `VlTriggerScheduler`, for coroutines suspended by event controls. It resumes them if its corresponding trigger was set. * `VlForkSync`, used for syncing `fork..join` and `fork..join_any` blocks. * `VlCoroutine`, the return type of all verilated coroutines. It allows for suspending a stack of coroutines (normally, C++ coroutines are stackless). There is a new visitor in `V3Timing.cpp` which: * scales delays according to the timescale, * simplifies intra-assignment timing controls and net delays into regular timing controls and assignments, * simplifies wait statements into loops with event controls, * marks processes and tasks with timing controls in them as suspendable, * creates delay, trigger scheduler, and fork sync variables, * transforms timing controls and fork joins into C++ awaits There are new functions in `V3SchedTiming.cpp` (used by `V3Sched.cpp`) that integrate static scheduling with timing. This involves providing external domains for variables, so that the necessary combinational logic gets triggered after coroutine resumption, as well as statements that need to be injected into the design eval function to perform this resumption at the correct time. There is also a function that transforms forked processes into separate functions. See the comments in `verilated_timing.h`, `verilated_timing.cpp`, `V3Timing.cpp`, and `V3SchedTiming.cpp`, as well as the internals documentation for more details. Signed-off-by: Krzysztof Bieganski <kbieganski@antmicro.com>
2022-08-22 12:26:32 +00:00
#endif
2022-09-28 02:22:05 +00:00
{
}
Timing support (#3363) Adds timing support to Verilator. It makes it possible to use delays, event controls within processes (not just at the start), wait statements, and forks. Building a design with those constructs requires a compiler that supports C++20 coroutines (GCC 10, Clang 5). The basic idea is to have processes and tasks with delays/event controls implemented as C++20 coroutines. This allows us to suspend and resume them at any time. There are five main runtime classes responsible for managing suspended coroutines: * `VlCoroutineHandle`, a wrapper over C++20's `std::coroutine_handle` with move semantics and automatic cleanup. * `VlDelayScheduler`, for coroutines suspended by delays. It resumes them at a proper simulation time. * `VlTriggerScheduler`, for coroutines suspended by event controls. It resumes them if its corresponding trigger was set. * `VlForkSync`, used for syncing `fork..join` and `fork..join_any` blocks. * `VlCoroutine`, the return type of all verilated coroutines. It allows for suspending a stack of coroutines (normally, C++ coroutines are stackless). There is a new visitor in `V3Timing.cpp` which: * scales delays according to the timescale, * simplifies intra-assignment timing controls and net delays into regular timing controls and assignments, * simplifies wait statements into loops with event controls, * marks processes and tasks with timing controls in them as suspendable, * creates delay, trigger scheduler, and fork sync variables, * transforms timing controls and fork joins into C++ awaits There are new functions in `V3SchedTiming.cpp` (used by `V3Sched.cpp`) that integrate static scheduling with timing. This involves providing external domains for variables, so that the necessary combinational logic gets triggered after coroutine resumption, as well as statements that need to be injected into the design eval function to perform this resumption at the correct time. There is also a function that transforms forked processes into separate functions. See the comments in `verilated_timing.h`, `verilated_timing.cpp`, `V3Timing.cpp`, and `V3SchedTiming.cpp`, as well as the internals documentation for more details. Signed-off-by: Krzysztof Bieganski <kbieganski@antmicro.com>
2022-08-22 12:26:32 +00:00
// Move the handle, leaving a nullptr
VlCoroutineHandle(VlCoroutineHandle&& moved)
2022-09-28 02:22:05 +00:00
: m_coro{std::exchange(moved.m_coro, nullptr)}
Timing support (#3363) Adds timing support to Verilator. It makes it possible to use delays, event controls within processes (not just at the start), wait statements, and forks. Building a design with those constructs requires a compiler that supports C++20 coroutines (GCC 10, Clang 5). The basic idea is to have processes and tasks with delays/event controls implemented as C++20 coroutines. This allows us to suspend and resume them at any time. There are five main runtime classes responsible for managing suspended coroutines: * `VlCoroutineHandle`, a wrapper over C++20's `std::coroutine_handle` with move semantics and automatic cleanup. * `VlDelayScheduler`, for coroutines suspended by delays. It resumes them at a proper simulation time. * `VlTriggerScheduler`, for coroutines suspended by event controls. It resumes them if its corresponding trigger was set. * `VlForkSync`, used for syncing `fork..join` and `fork..join_any` blocks. * `VlCoroutine`, the return type of all verilated coroutines. It allows for suspending a stack of coroutines (normally, C++ coroutines are stackless). There is a new visitor in `V3Timing.cpp` which: * scales delays according to the timescale, * simplifies intra-assignment timing controls and net delays into regular timing controls and assignments, * simplifies wait statements into loops with event controls, * marks processes and tasks with timing controls in them as suspendable, * creates delay, trigger scheduler, and fork sync variables, * transforms timing controls and fork joins into C++ awaits There are new functions in `V3SchedTiming.cpp` (used by `V3Sched.cpp`) that integrate static scheduling with timing. This involves providing external domains for variables, so that the necessary combinational logic gets triggered after coroutine resumption, as well as statements that need to be injected into the design eval function to perform this resumption at the correct time. There is also a function that transforms forked processes into separate functions. See the comments in `verilated_timing.h`, `verilated_timing.cpp`, `V3Timing.cpp`, and `V3SchedTiming.cpp`, as well as the internals documentation for more details. Signed-off-by: Krzysztof Bieganski <kbieganski@antmicro.com>
2022-08-22 12:26:32 +00:00
#ifdef VL_DEBUG
2022-09-28 02:22:05 +00:00
, m_filename{moved.m_filename}
, m_linenum{moved.m_linenum}
Timing support (#3363) Adds timing support to Verilator. It makes it possible to use delays, event controls within processes (not just at the start), wait statements, and forks. Building a design with those constructs requires a compiler that supports C++20 coroutines (GCC 10, Clang 5). The basic idea is to have processes and tasks with delays/event controls implemented as C++20 coroutines. This allows us to suspend and resume them at any time. There are five main runtime classes responsible for managing suspended coroutines: * `VlCoroutineHandle`, a wrapper over C++20's `std::coroutine_handle` with move semantics and automatic cleanup. * `VlDelayScheduler`, for coroutines suspended by delays. It resumes them at a proper simulation time. * `VlTriggerScheduler`, for coroutines suspended by event controls. It resumes them if its corresponding trigger was set. * `VlForkSync`, used for syncing `fork..join` and `fork..join_any` blocks. * `VlCoroutine`, the return type of all verilated coroutines. It allows for suspending a stack of coroutines (normally, C++ coroutines are stackless). There is a new visitor in `V3Timing.cpp` which: * scales delays according to the timescale, * simplifies intra-assignment timing controls and net delays into regular timing controls and assignments, * simplifies wait statements into loops with event controls, * marks processes and tasks with timing controls in them as suspendable, * creates delay, trigger scheduler, and fork sync variables, * transforms timing controls and fork joins into C++ awaits There are new functions in `V3SchedTiming.cpp` (used by `V3Sched.cpp`) that integrate static scheduling with timing. This involves providing external domains for variables, so that the necessary combinational logic gets triggered after coroutine resumption, as well as statements that need to be injected into the design eval function to perform this resumption at the correct time. There is also a function that transforms forked processes into separate functions. See the comments in `verilated_timing.h`, `verilated_timing.cpp`, `V3Timing.cpp`, and `V3SchedTiming.cpp`, as well as the internals documentation for more details. Signed-off-by: Krzysztof Bieganski <kbieganski@antmicro.com>
2022-08-22 12:26:32 +00:00
#endif
2022-09-28 02:22:05 +00:00
{
}
Timing support (#3363) Adds timing support to Verilator. It makes it possible to use delays, event controls within processes (not just at the start), wait statements, and forks. Building a design with those constructs requires a compiler that supports C++20 coroutines (GCC 10, Clang 5). The basic idea is to have processes and tasks with delays/event controls implemented as C++20 coroutines. This allows us to suspend and resume them at any time. There are five main runtime classes responsible for managing suspended coroutines: * `VlCoroutineHandle`, a wrapper over C++20's `std::coroutine_handle` with move semantics and automatic cleanup. * `VlDelayScheduler`, for coroutines suspended by delays. It resumes them at a proper simulation time. * `VlTriggerScheduler`, for coroutines suspended by event controls. It resumes them if its corresponding trigger was set. * `VlForkSync`, used for syncing `fork..join` and `fork..join_any` blocks. * `VlCoroutine`, the return type of all verilated coroutines. It allows for suspending a stack of coroutines (normally, C++ coroutines are stackless). There is a new visitor in `V3Timing.cpp` which: * scales delays according to the timescale, * simplifies intra-assignment timing controls and net delays into regular timing controls and assignments, * simplifies wait statements into loops with event controls, * marks processes and tasks with timing controls in them as suspendable, * creates delay, trigger scheduler, and fork sync variables, * transforms timing controls and fork joins into C++ awaits There are new functions in `V3SchedTiming.cpp` (used by `V3Sched.cpp`) that integrate static scheduling with timing. This involves providing external domains for variables, so that the necessary combinational logic gets triggered after coroutine resumption, as well as statements that need to be injected into the design eval function to perform this resumption at the correct time. There is also a function that transforms forked processes into separate functions. See the comments in `verilated_timing.h`, `verilated_timing.cpp`, `V3Timing.cpp`, and `V3SchedTiming.cpp`, as well as the internals documentation for more details. Signed-off-by: Krzysztof Bieganski <kbieganski@antmicro.com>
2022-08-22 12:26:32 +00:00
// Destroy if the handle isn't null
~VlCoroutineHandle() {
// Usually these coroutines should get resumed; we only need to clean up if we destroy a
// model with some coroutines suspended
if (VL_UNLIKELY(m_coro)) m_coro.destroy();
}
// METHODS
// Move the handle, leaving a null handle
auto& operator=(VlCoroutineHandle&& moved) {
m_coro = std::exchange(moved.m_coro, nullptr);
return *this;
}
// Resume the coroutine if the handle isn't null
void resume();
#ifdef VL_DEBUG
void dump();
#endif
};
//=============================================================================
// VlDelayScheduler stores coroutines to be resumed at a certain simulation time. If the current
// time is equal to a coroutine's resume time, the coroutine gets resumed.
class VlDelayScheduler final {
// TYPES
struct VlDelayedCoroutine {
uint64_t m_timestep; // Simulation time when the coroutine should be resumed
VlCoroutineHandle m_handle; // The suspended coroutine to be resumed
// Comparison operator for std::push_heap(), std::pop_heap()
bool operator<(const VlDelayedCoroutine& other) const {
return m_timestep > other.m_timestep;
}
#ifdef VL_DEBUG
void dump();
#endif
};
using VlDelayedCoroutineQueue = std::vector<VlDelayedCoroutine>;
// MEMBERS
VerilatedContext& m_context;
VlDelayedCoroutineQueue m_queue; // Coroutines to be restored at a certain simulation time
public:
// CONSTRUCTORS
explicit VlDelayScheduler(VerilatedContext& context)
Timing support (#3363) Adds timing support to Verilator. It makes it possible to use delays, event controls within processes (not just at the start), wait statements, and forks. Building a design with those constructs requires a compiler that supports C++20 coroutines (GCC 10, Clang 5). The basic idea is to have processes and tasks with delays/event controls implemented as C++20 coroutines. This allows us to suspend and resume them at any time. There are five main runtime classes responsible for managing suspended coroutines: * `VlCoroutineHandle`, a wrapper over C++20's `std::coroutine_handle` with move semantics and automatic cleanup. * `VlDelayScheduler`, for coroutines suspended by delays. It resumes them at a proper simulation time. * `VlTriggerScheduler`, for coroutines suspended by event controls. It resumes them if its corresponding trigger was set. * `VlForkSync`, used for syncing `fork..join` and `fork..join_any` blocks. * `VlCoroutine`, the return type of all verilated coroutines. It allows for suspending a stack of coroutines (normally, C++ coroutines are stackless). There is a new visitor in `V3Timing.cpp` which: * scales delays according to the timescale, * simplifies intra-assignment timing controls and net delays into regular timing controls and assignments, * simplifies wait statements into loops with event controls, * marks processes and tasks with timing controls in them as suspendable, * creates delay, trigger scheduler, and fork sync variables, * transforms timing controls and fork joins into C++ awaits There are new functions in `V3SchedTiming.cpp` (used by `V3Sched.cpp`) that integrate static scheduling with timing. This involves providing external domains for variables, so that the necessary combinational logic gets triggered after coroutine resumption, as well as statements that need to be injected into the design eval function to perform this resumption at the correct time. There is also a function that transforms forked processes into separate functions. See the comments in `verilated_timing.h`, `verilated_timing.cpp`, `V3Timing.cpp`, and `V3SchedTiming.cpp`, as well as the internals documentation for more details. Signed-off-by: Krzysztof Bieganski <kbieganski@antmicro.com>
2022-08-22 12:26:32 +00:00
: m_context{context} {}
// METHODS
// Resume coroutines waiting for the current simulation time
void resume();
// Returns the simulation time of the next time slot (aborts if there are no delayed
// coroutines)
uint64_t nextTimeSlot();
// Are there no delayed coroutines awaiting?
bool empty() { return m_queue.empty(); }
// Are there coroutines to resume at the current simulation time?
bool awaitingCurrentTime() {
return !empty() && m_queue.front().m_timestep <= m_context.time();
}
#ifdef VL_DEBUG
void dump();
#endif
// Used by coroutines for co_awaiting a certain simulation time
auto delay(uint64_t delay, const char* filename, int linenum) {
struct Awaitable {
VlDelayedCoroutineQueue& queue;
uint64_t delay;
#ifdef VL_DEBUG
const char* filename;
int linenum;
#endif
bool await_ready() { return false; } // Always suspend
void await_suspend(std::coroutine_handle<> coro) {
#ifdef VL_DEBUG
queue.push_back({delay, VlCoroutineHandle{coro, filename, linenum}});
#else
queue.push_back({delay, coro});
#endif
// Move last element to the proper place in the max-heap
std::push_heap(queue.begin(), queue.end());
}
void await_resume() {}
};
#ifdef VL_DEBUG
return Awaitable{m_queue, m_context.time() + delay, filename, linenum};
#else
return Awaitable{m_queue, m_context.time() + delay};
#endif
}
};
//=============================================================================
// VlTriggerScheduler stores coroutines to be resumed by a trigger. It does not keep track of its
// trigger, relying on calling code to resume when appropriate. Coroutines are kept in two stages
// - 'uncommitted' and 'ready'. Whenever a coroutine is suspended, it lands in the 'uncommited'
// stage. Only when commit() is called, these coroutines get moved to the 'ready' stage. That's
// when they can be resumed. This is done to avoid resuming processes before they start waiting.
class VlTriggerScheduler final {
// TYPES
using VlCoroutineVec = std::vector<VlCoroutineHandle>;
// MEMBERS
VlCoroutineVec m_uncommitted; // Coroutines suspended before commit() was called
// (not resumable)
VlCoroutineVec m_ready; // Coroutines that can be resumed (all coros from m_uncommitted are
// moved here in commit())
public:
// METHODS
// Resumes all coroutines from the 'ready' stage
void resume(const char* eventDescription);
// Moves all coroutines from m_uncommitted to m_ready
void commit(const char* eventDescription);
// Are there no coroutines awaiting?
bool empty() { return m_ready.empty() && m_uncommitted.empty(); }
#ifdef VL_DEBUG
void dump(const char* eventDescription);
#endif
// Used by coroutines for co_awaiting a certain trigger
auto trigger(const char* eventDescription, const char* filename, int linenum) {
VL_DEBUG_IF(VL_DBG_MSGF(" Suspending process waiting for %s at %s:%d\n",
eventDescription, filename, linenum););
struct Awaitable {
VlCoroutineVec& suspended; // Coros waiting on trigger
#ifdef VL_DEBUG
const char* filename;
int linenum;
#endif
bool await_ready() { return false; } // Always suspend
void await_suspend(std::coroutine_handle<> coro) {
#ifdef VL_DEBUG
suspended.emplace_back(coro, filename, linenum);
#else
suspended.emplace_back(coro);
#endif
}
void await_resume() {}
};
#ifdef VL_DEBUG
return Awaitable{m_uncommitted, filename, linenum};
#else
return Awaitable{m_uncommitted};
#endif
}
};
//=============================================================================
// VlNow is a helper awaitable type that always suspends, and then immediately resumes a coroutine.
// Allows forcing the move of coroutine locals to the heap.
struct VlNow {
bool await_ready() { return false; } // Always suspend
bool await_suspend(std::coroutine_handle<>) { return false; } // Resume immediately
void await_resume() {}
};
//=============================================================================
// VlForever is a helper awaitable type for suspending coroutines forever. Used for constant
// wait statements.
struct VlForever {
bool await_ready() { return false; } // Always suspend
void await_suspend(std::coroutine_handle<> coro) { coro.destroy(); }
void await_resume() {}
};
//=============================================================================
// VlForkSync is used to manage fork..join and fork..join_any constructs.
class VlForkSync final {
// VlJoin stores the handle of a suspended coroutine that did a fork..join or fork..join_any.
// If the counter reaches 0, the suspended coroutine shall be resumed.
struct VlJoin {
size_t m_counter = 0; // When reaches 0, resume suspended coroutine
VlCoroutineHandle m_susp; // Coroutine to resume
};
// The join info is shared among all forked processes
std::shared_ptr<VlJoin> m_join;
public:
// Create the join object and set the counter to the specified number
void init(size_t count) { m_join.reset(new VlJoin{count, {}}); }
// Called whenever any of the forked processes finishes. If the join counter reaches 0, the
// main process gets resumed
void done(const char* filename, int linenum);
// Used by coroutines for co_awaiting a join
auto join(const char* filename, int linenum) {
assert(m_join);
VL_DEBUG_IF(
VL_DBG_MSGF(" Awaiting join of fork at: %s:%d\n", filename, linenum););
Timing support (#3363) Adds timing support to Verilator. It makes it possible to use delays, event controls within processes (not just at the start), wait statements, and forks. Building a design with those constructs requires a compiler that supports C++20 coroutines (GCC 10, Clang 5). The basic idea is to have processes and tasks with delays/event controls implemented as C++20 coroutines. This allows us to suspend and resume them at any time. There are five main runtime classes responsible for managing suspended coroutines: * `VlCoroutineHandle`, a wrapper over C++20's `std::coroutine_handle` with move semantics and automatic cleanup. * `VlDelayScheduler`, for coroutines suspended by delays. It resumes them at a proper simulation time. * `VlTriggerScheduler`, for coroutines suspended by event controls. It resumes them if its corresponding trigger was set. * `VlForkSync`, used for syncing `fork..join` and `fork..join_any` blocks. * `VlCoroutine`, the return type of all verilated coroutines. It allows for suspending a stack of coroutines (normally, C++ coroutines are stackless). There is a new visitor in `V3Timing.cpp` which: * scales delays according to the timescale, * simplifies intra-assignment timing controls and net delays into regular timing controls and assignments, * simplifies wait statements into loops with event controls, * marks processes and tasks with timing controls in them as suspendable, * creates delay, trigger scheduler, and fork sync variables, * transforms timing controls and fork joins into C++ awaits There are new functions in `V3SchedTiming.cpp` (used by `V3Sched.cpp`) that integrate static scheduling with timing. This involves providing external domains for variables, so that the necessary combinational logic gets triggered after coroutine resumption, as well as statements that need to be injected into the design eval function to perform this resumption at the correct time. There is also a function that transforms forked processes into separate functions. See the comments in `verilated_timing.h`, `verilated_timing.cpp`, `V3Timing.cpp`, and `V3SchedTiming.cpp`, as well as the internals documentation for more details. Signed-off-by: Krzysztof Bieganski <kbieganski@antmicro.com>
2022-08-22 12:26:32 +00:00
struct Awaitable {
const std::shared_ptr<VlJoin> join; // Join to await on
bool await_ready() { return join->m_counter == 0; } // Suspend if join still exists
void await_suspend(std::coroutine_handle<> coro) { join->m_susp = coro; }
void await_resume() {}
};
return Awaitable{m_join};
}
};
//=============================================================================
// VlCoroutine
// Return value of a coroutine. Used for chaining coroutine suspension/resumption.
class VlCoroutine final {
private:
// TYPES
struct VlPromise {
std::coroutine_handle<> m_continuation; // Coroutine to resume after this one finishes
VlCoroutine* m_corop = nullptr; // Pointer to the coroutine return object
~VlPromise();
VlCoroutine get_return_object() { return {this}; }
// Never suspend at the start of the coroutine
std::suspend_never initial_suspend() { return {}; }
// Never suspend at the end of the coroutine (thanks to this, the coroutine will clean up
// after itself)
std::suspend_never final_suspend() noexcept;
void unhandled_exception() { std::abort(); }
void return_void() const {}
};
// MEMBERS
VlPromise* m_promisep; // The promise created for this coroutine
public:
// TYPES
using promise_type = VlPromise; // promise_type has to be public
// CONSTRUCTORS
// Construct
VlCoroutine(VlPromise* promisep)
: m_promisep{promisep} {
Timing support (#3363) Adds timing support to Verilator. It makes it possible to use delays, event controls within processes (not just at the start), wait statements, and forks. Building a design with those constructs requires a compiler that supports C++20 coroutines (GCC 10, Clang 5). The basic idea is to have processes and tasks with delays/event controls implemented as C++20 coroutines. This allows us to suspend and resume them at any time. There are five main runtime classes responsible for managing suspended coroutines: * `VlCoroutineHandle`, a wrapper over C++20's `std::coroutine_handle` with move semantics and automatic cleanup. * `VlDelayScheduler`, for coroutines suspended by delays. It resumes them at a proper simulation time. * `VlTriggerScheduler`, for coroutines suspended by event controls. It resumes them if its corresponding trigger was set. * `VlForkSync`, used for syncing `fork..join` and `fork..join_any` blocks. * `VlCoroutine`, the return type of all verilated coroutines. It allows for suspending a stack of coroutines (normally, C++ coroutines are stackless). There is a new visitor in `V3Timing.cpp` which: * scales delays according to the timescale, * simplifies intra-assignment timing controls and net delays into regular timing controls and assignments, * simplifies wait statements into loops with event controls, * marks processes and tasks with timing controls in them as suspendable, * creates delay, trigger scheduler, and fork sync variables, * transforms timing controls and fork joins into C++ awaits There are new functions in `V3SchedTiming.cpp` (used by `V3Sched.cpp`) that integrate static scheduling with timing. This involves providing external domains for variables, so that the necessary combinational logic gets triggered after coroutine resumption, as well as statements that need to be injected into the design eval function to perform this resumption at the correct time. There is also a function that transforms forked processes into separate functions. See the comments in `verilated_timing.h`, `verilated_timing.cpp`, `V3Timing.cpp`, and `V3SchedTiming.cpp`, as well as the internals documentation for more details. Signed-off-by: Krzysztof Bieganski <kbieganski@antmicro.com>
2022-08-22 12:26:32 +00:00
m_promisep->m_corop = this;
}
// Move. Update the pointers each time the return object is moved
VlCoroutine(VlCoroutine&& other)
: m_promisep{std::exchange(other.m_promisep, nullptr)} {
if (m_promisep) m_promisep->m_corop = this;
}
~VlCoroutine() {
// Indicate to the promise that the return object is gone
if (m_promisep) m_promisep->m_corop = nullptr;
}
// METHODS
// Suspend the awaiter if the coroutine is suspended (the promise exists)
bool await_ready() const noexcept { return !m_promisep; }
// Set the awaiting coroutine as the continuation of the current coroutine
void await_suspend(std::coroutine_handle<> coro) { m_promisep->m_continuation = coro; }
void await_resume() const noexcept {}
};
#endif // Guard