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 implementation code
|
|
|
|
///
|
|
|
|
/// This file must be compiled and linked against all Verilated objects
|
|
|
|
/// that use timing features.
|
|
|
|
///
|
|
|
|
/// See the internals documentation docs/internals.rst for details.
|
|
|
|
///
|
|
|
|
//=========================================================================
|
|
|
|
|
|
|
|
#include "verilated_timing.h"
|
|
|
|
|
|
|
|
//======================================================================
|
|
|
|
// VlCoroutineHandle:: Methods
|
|
|
|
|
|
|
|
void VlCoroutineHandle::resume() {
|
|
|
|
// Only null if we have a fork..join_any and one of the other child processes resumed the
|
|
|
|
// main process
|
|
|
|
if (VL_LIKELY(m_coro)) {
|
|
|
|
VL_DEBUG_IF(VL_DBG_MSGF(" Resuming: "); dump(););
|
|
|
|
m_coro();
|
|
|
|
m_coro = nullptr;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#ifdef VL_DEBUG
|
|
|
|
void VlCoroutineHandle::dump() {
|
|
|
|
VL_DEBUG_IF(VL_PRINTF("Process waiting at %s:%d\n", m_filename, m_linenum););
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
//======================================================================
|
|
|
|
// VlDelayScheduler:: Methods
|
|
|
|
|
|
|
|
#ifdef VL_DEBUG
|
|
|
|
void VlDelayScheduler::VlDelayedCoroutine::dump() {
|
|
|
|
VL_DEBUG_IF(VL_DBG_MSGF(" Awaiting time %lu: ", m_timestep););
|
|
|
|
m_handle.dump();
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
void VlDelayScheduler::resume() {
|
|
|
|
#ifdef VL_DEBUG
|
|
|
|
dump();
|
|
|
|
VL_DEBUG_IF(VL_DBG_MSGF(" Resuming delayed processes\n"););
|
|
|
|
#endif
|
|
|
|
while (awaitingCurrentTime()) {
|
|
|
|
if (m_queue.front().m_timestep != m_context.time()) {
|
|
|
|
VL_FATAL_MT(__FILE__, __LINE__, "",
|
|
|
|
"%Error: Encountered process that should've been resumed at an "
|
|
|
|
"earlier simulation time. Missed a time slot?");
|
|
|
|
}
|
|
|
|
// Move max element in the heap to the end
|
|
|
|
std::pop_heap(m_queue.begin(), m_queue.end());
|
|
|
|
VlCoroutineHandle handle = std::move(m_queue.back().m_handle);
|
|
|
|
m_queue.pop_back();
|
|
|
|
handle.resume();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
uint64_t VlDelayScheduler::nextTimeSlot() {
|
|
|
|
if (empty()) {
|
|
|
|
VL_FATAL_MT(__FILE__, __LINE__, "", "%Error: There is no next time slot scheduled");
|
|
|
|
}
|
|
|
|
return m_queue.front().m_timestep;
|
|
|
|
}
|
|
|
|
|
|
|
|
#ifdef VL_DEBUG
|
|
|
|
void VlDelayScheduler::dump() {
|
|
|
|
if (m_queue.empty()) {
|
|
|
|
VL_DEBUG_IF(VL_DBG_MSGF(" No delayed processes:\n"););
|
|
|
|
} else {
|
|
|
|
VL_DEBUG_IF(VL_DBG_MSGF(" Delayed processes:\n"););
|
|
|
|
for (auto& susp : m_queue) susp.dump();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
//======================================================================
|
|
|
|
// VlTriggerScheduler:: Methods
|
|
|
|
|
|
|
|
void VlTriggerScheduler::resume(const char* eventDescription) {
|
|
|
|
#ifdef VL_DEBUG
|
|
|
|
dump(eventDescription);
|
|
|
|
VL_DEBUG_IF(VL_DBG_MSGF(" Resuming processes waiting for %s\n", eventDescription););
|
|
|
|
#endif
|
|
|
|
for (auto& susp : m_ready) susp.resume();
|
|
|
|
m_ready.clear();
|
|
|
|
commit(eventDescription);
|
|
|
|
}
|
|
|
|
|
|
|
|
void VlTriggerScheduler::commit(const char* eventDescription) {
|
|
|
|
#ifdef VL_DEBUG
|
|
|
|
if (!m_uncommitted.empty()) {
|
|
|
|
VL_DEBUG_IF(
|
|
|
|
VL_DBG_MSGF(" Committing processes waiting for %s:\n", eventDescription);
|
|
|
|
for (auto& susp
|
|
|
|
: m_uncommitted) {
|
|
|
|
VL_DBG_MSGF(" - ");
|
|
|
|
susp.dump();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
m_ready.reserve(m_ready.size() + m_uncommitted.size());
|
|
|
|
m_ready.insert(m_ready.end(), std::make_move_iterator(m_uncommitted.begin()),
|
|
|
|
std::make_move_iterator(m_uncommitted.end()));
|
|
|
|
m_uncommitted.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
#ifdef VL_DEBUG
|
|
|
|
void VlTriggerScheduler::dump(const char* eventDescription) {
|
|
|
|
if (m_ready.empty()) {
|
|
|
|
VL_DEBUG_IF(
|
|
|
|
VL_DBG_MSGF(" No ready processes waiting for %s\n", eventDescription););
|
|
|
|
} else {
|
|
|
|
VL_DEBUG_IF(for (auto& susp
|
|
|
|
: m_ready) {
|
|
|
|
VL_DBG_MSGF(" Ready processes waiting for %s:\n", eventDescription);
|
|
|
|
VL_DBG_MSGF(" - ");
|
|
|
|
susp.dump();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
if (!m_uncommitted.empty()) {
|
|
|
|
VL_DEBUG_IF(
|
|
|
|
VL_DBG_MSGF(" Uncommitted processes waiting for %s:\n", eventDescription);
|
|
|
|
for (auto& susp
|
|
|
|
: m_uncommitted) {
|
|
|
|
VL_DBG_MSGF(" - ");
|
|
|
|
susp.dump();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
|
|
//======================================================================
|
|
|
|
// VlForkSync:: Methods
|
|
|
|
|
|
|
|
void VlForkSync::done(const char* filename, int linenum) {
|
2022-09-05 10:27:24 +00:00
|
|
|
VL_DEBUG_IF(
|
|
|
|
VL_DBG_MSGF(" Process forked at %s:%d finished\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
|
|
|
if (m_join->m_counter > 0) m_join->m_counter--;
|
|
|
|
if (m_join->m_counter == 0) m_join->m_susp.resume();
|
|
|
|
}
|
|
|
|
|
|
|
|
//======================================================================
|
|
|
|
// VlCoroutine:: Methods
|
|
|
|
|
|
|
|
VlCoroutine::VlPromise::~VlPromise() {
|
|
|
|
// Indicate to the return object that the coroutine has finished or been destroyed
|
|
|
|
if (m_corop) m_corop->m_promisep = nullptr;
|
|
|
|
// If there is a continuation, destroy it
|
|
|
|
if (m_continuation) m_continuation.destroy();
|
|
|
|
}
|
|
|
|
|
|
|
|
std::suspend_never VlCoroutine::VlPromise::final_suspend() noexcept {
|
|
|
|
// Indicate to the return object that the coroutine has finished
|
|
|
|
if (m_corop) {
|
|
|
|
m_corop->m_promisep = nullptr;
|
|
|
|
// Forget the return value, we won't need it and it won't be able to let us know if
|
|
|
|
// it's destroyed
|
|
|
|
m_corop = nullptr;
|
|
|
|
}
|
|
|
|
// If there is a continuation, resume it
|
|
|
|
if (m_continuation) {
|
|
|
|
m_continuation();
|
|
|
|
m_continuation = nullptr;
|
|
|
|
}
|
|
|
|
return {};
|
|
|
|
}
|